mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-04 02:08:17 +00:00
feat: use jump_from_click
from typst-ide (#1399)
This commit is contained in:
parent
791a7f8314
commit
21c6492254
6 changed files with 226 additions and 314 deletions
|
@ -22,12 +22,13 @@ use serde::Serialize;
|
|||
use serde_json::Value as JsonValue;
|
||||
use sync_lsp::just_ok;
|
||||
use tinymist_assets::TYPST_PREVIEW_HTML;
|
||||
use tinymist_project::{ProjectInsId, WorldProvider};
|
||||
use tinymist_project::{LspWorld, ProjectInsId, WorldProvider};
|
||||
use tinymist_std::error::IgnoreLogging;
|
||||
use tinymist_std::typst::TypstDocument;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use typst::layout::{Frame, FrameItem, Point, Position};
|
||||
use typst::layout::{Abs, Frame, FrameItem, Point, Position, Size};
|
||||
use typst::syntax::{LinkedNode, Source, Span, SyntaxKind};
|
||||
use typst::visualize::Geometry;
|
||||
use typst::World;
|
||||
use typst_preview::{
|
||||
frontend_html, ControlPlaneMessage, ControlPlaneResponse, ControlPlaneRx, ControlPlaneTx,
|
||||
|
@ -94,6 +95,23 @@ impl typst_preview::CompileView for PreviewCompileView {
|
|||
Some(SourceSpanOffset { span, offset })
|
||||
}
|
||||
|
||||
// todo: use vec2bbox to handle bbox correctly
|
||||
fn resolve_frame_loc(
|
||||
&self,
|
||||
pos: &reflexo::debug_loc::DocumentPosition,
|
||||
) -> Option<(SourceSpanOffset, SourceSpanOffset)> {
|
||||
let TypstDocument::Paged(doc) = self.doc()? else {
|
||||
return None;
|
||||
};
|
||||
let world = &self.snap.world;
|
||||
|
||||
let page = pos.page_no.checked_sub(1)?;
|
||||
let page = doc.pages.get(page)?;
|
||||
|
||||
let click = Point::new(Abs::pt(pos.x as f64), Abs::pt(pos.y as f64));
|
||||
jump_from_click(world, &page.frame, click)
|
||||
}
|
||||
|
||||
fn resolve_document_position(&self, loc: Location) -> Vec<Position> {
|
||||
let world = &self.snap.world;
|
||||
let Location::Src(src_loc) = loc;
|
||||
|
@ -866,6 +884,85 @@ impl Notification for NotifDocumentOutline {
|
|||
const METHOD: &'static str = "tinymist/documentOutline";
|
||||
}
|
||||
|
||||
/// Determine where to jump to based on a click in a frame.
|
||||
pub fn jump_from_click(
|
||||
world: &LspWorld,
|
||||
frame: &Frame,
|
||||
click: Point,
|
||||
) -> Option<(SourceSpanOffset, SourceSpanOffset)> {
|
||||
// Try to find a link first.
|
||||
for (pos, item) in frame.items() {
|
||||
if let FrameItem::Link(_dest, size) = item {
|
||||
if is_in_rect(*pos, *size, click) {
|
||||
// todo: url reaction
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no link, search for a jump target.
|
||||
for (mut pos, item) in frame.items().rev() {
|
||||
match item {
|
||||
FrameItem::Group(group) => {
|
||||
// TODO: Handle transformation.
|
||||
if let Some(span) = jump_from_click(world, &group.frame, click - pos) {
|
||||
return Some(span);
|
||||
}
|
||||
}
|
||||
|
||||
FrameItem::Text(text) => {
|
||||
for glyph in &text.glyphs {
|
||||
let width = glyph.x_advance.at(text.size);
|
||||
if is_in_rect(
|
||||
Point::new(pos.x, pos.y - text.size),
|
||||
Size::new(width, text.size),
|
||||
click,
|
||||
) {
|
||||
let (span, span_offset) = glyph.span;
|
||||
let mut span_offset = span_offset as usize;
|
||||
let Some(id) = span.id() else { continue };
|
||||
let source = world.source(id).ok()?;
|
||||
let node = source.find(span)?;
|
||||
if matches!(node.kind(), SyntaxKind::Text | SyntaxKind::MathText)
|
||||
&& (click.x - pos.x) > width / 2.0
|
||||
{
|
||||
span_offset += glyph.range().len();
|
||||
}
|
||||
|
||||
let span_offset = SourceSpanOffset {
|
||||
span,
|
||||
offset: span_offset,
|
||||
};
|
||||
|
||||
return Some((span_offset, span_offset));
|
||||
}
|
||||
|
||||
pos.x += width;
|
||||
}
|
||||
}
|
||||
|
||||
FrameItem::Shape(shape, span) => {
|
||||
let Geometry::Rect(size) = shape.geometry else {
|
||||
continue;
|
||||
};
|
||||
if is_in_rect(pos, size, click) {
|
||||
let span = (*span).into();
|
||||
return Some((span, span));
|
||||
}
|
||||
}
|
||||
|
||||
FrameItem::Image(_, size, span) if is_in_rect(pos, *size, click) => {
|
||||
let span = (*span).into();
|
||||
return Some((span, span));
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Find the output location in the document for a cursor position.
|
||||
fn jump_from_cursor(document: &TypstDocument, source: &Source, cursor: usize) -> Vec<Position> {
|
||||
let Some(node) = LinkedNode::new(source.root())
|
||||
|
@ -933,6 +1030,12 @@ fn find_in_frame(frame: &Frame, span: Span, min_dis: &mut u64, p: &mut Point) ->
|
|||
None
|
||||
}
|
||||
|
||||
/// Whether a rectangle with the given size at the given position contains the
|
||||
/// click position.
|
||||
fn is_in_rect(pos: Point, size: Size, click: Point) -> bool {
|
||||
pos.x <= click.x && pos.x + size.x >= click.x && pos.y <= click.y && pos.y + size.y >= click.y
|
||||
}
|
||||
|
||||
fn bind_streams(previewer: &mut Previewer, websocket_rx: mpsc::UnboundedReceiver<HyperWebsocket>) {
|
||||
previewer.start_data_plane(
|
||||
websocket_rx,
|
||||
|
|
|
@ -22,6 +22,7 @@ pub enum RenderActorRequest {
|
|||
RenderIncremental,
|
||||
EditorResolveSpanRange(Range<SourceSpanOffset>),
|
||||
WebviewResolveSpan(ResolveSpanRequest),
|
||||
WebviewResolveFrameLoc(DocumentPosition),
|
||||
ResolveSourceLoc(ResolveSourceLocRequest),
|
||||
ChangeCursorPosition(ChangeCursorPositionRequest),
|
||||
}
|
||||
|
@ -34,6 +35,7 @@ impl RenderActorRequest {
|
|||
Self::EditorResolveSpanRange(_) => false,
|
||||
Self::WebviewResolveSpan(_) => false,
|
||||
Self::ResolveSourceLoc(_) => false,
|
||||
Self::WebviewResolveFrameLoc(_) => false,
|
||||
Self::ChangeCursorPosition(_) => false,
|
||||
}
|
||||
}
|
||||
|
@ -94,6 +96,16 @@ impl RenderActor {
|
|||
self.editor_resolve_span_range(spans.0..spans.1);
|
||||
}
|
||||
}
|
||||
RenderActorRequest::WebviewResolveFrameLoc(frame_loc) => {
|
||||
log::debug!("RenderActor: resolving WebviewResolveFrameLoc: {frame_loc:?}");
|
||||
let spans = self.resolve_span_by_frame_loc(&frame_loc);
|
||||
|
||||
log::debug!("RenderActor: resolved WebviewResolveSpan: {spans:?}");
|
||||
// end position is used
|
||||
if let Some(spans) = spans {
|
||||
self.editor_resolve_span_range(spans.0..spans.1);
|
||||
}
|
||||
}
|
||||
RenderActorRequest::ResolveSourceLoc(req) => {
|
||||
log::debug!("RenderActor: resolving ResolveSourceLoc: {req:?}");
|
||||
|
||||
|
@ -283,6 +295,15 @@ impl RenderActor {
|
|||
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Gets the span range of the given frame loc.
|
||||
pub fn resolve_span_by_frame_loc(
|
||||
&mut self,
|
||||
pos: &DocumentPosition,
|
||||
) -> Option<(SourceSpanOffset, SourceSpanOffset)> {
|
||||
let view = self.view.read();
|
||||
view.as_ref()?.resolve_frame_loc(pos)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OutlineRenderActor {
|
||||
|
|
|
@ -156,8 +156,14 @@ impl<
|
|||
let path = path.into_iter().map(ElementPoint::from).collect::<Vec<_>>();
|
||||
self.render_sender.send(RenderActorRequest::WebviewResolveSpan(ResolveSpanRequest(path))).log_error("WebViewActor");
|
||||
};
|
||||
} else if msg.starts_with("src-point") {
|
||||
let path = msg.split(' ').nth(1).unwrap();
|
||||
let path = serde_json::from_str(path);
|
||||
if let Ok(path) = path {
|
||||
self.render_sender.send(RenderActorRequest::WebviewResolveFrameLoc(path)).log_error("WebViewActor");
|
||||
};
|
||||
} else {
|
||||
let err = self.webview_websocket_conn.send(Message::Text(format!("error, received unknown message: {}", msg))).await;
|
||||
let err = self.webview_websocket_conn.send(Message::Text(format!("error, received unknown message: {msg}"))).await;
|
||||
log::info!("WebviewActor: received unknown message from websocket: {msg} {err:?}");
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ pub use actor::editor::{
|
|||
};
|
||||
pub use args::*;
|
||||
pub use outline::Outline;
|
||||
use tinymist_std::debug_loc::DocumentPosition;
|
||||
use tinymist_std::error::IgnoreLogging;
|
||||
|
||||
use std::{collections::HashMap, future::Future, path::PathBuf, pin::Pin, sync::Arc};
|
||||
|
@ -370,6 +371,14 @@ pub trait CompileView: Send + Sync {
|
|||
None
|
||||
}
|
||||
|
||||
/// Resolve a physical location in the document.
|
||||
fn resolve_frame_loc(
|
||||
&self,
|
||||
_pos: &DocumentPosition,
|
||||
) -> Option<(SourceSpanOffset, SourceSpanOffset)> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Resolve the document position.
|
||||
fn resolve_document_position(&self, _by: Location) -> Vec<Position> {
|
||||
vec![]
|
||||
|
|
|
@ -32,7 +32,7 @@ const CssClassToType = [
|
|||
] as const;
|
||||
|
||||
function castToSourceMappingElement(
|
||||
elem: Element
|
||||
elem: Element,
|
||||
): [SourceMappingType, Element, string] | undefined {
|
||||
if (elem.classList.length === 0) {
|
||||
return undefined;
|
||||
|
@ -46,7 +46,7 @@ function castToSourceMappingElement(
|
|||
}
|
||||
|
||||
function castToNestSourceMappingElement(
|
||||
elem: Element
|
||||
elem: Element,
|
||||
): [SourceMappingType, Element, string] | undefined {
|
||||
while (elem) {
|
||||
const result = castToSourceMappingElement(elem);
|
||||
|
@ -63,9 +63,7 @@ function castToNestSourceMappingElement(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
function castChildrenToSourceMappingElement(
|
||||
elem: Element
|
||||
): [SourceMappingType, Element, string][] {
|
||||
function castChildrenToSourceMappingElement(elem: Element): [SourceMappingType, Element, string][] {
|
||||
return Array.from(elem.children)
|
||||
.map(castToNestSourceMappingElement)
|
||||
.filter((x) => x) as [SourceMappingType, Element, string][];
|
||||
|
@ -80,14 +78,11 @@ export function removeSourceMappingHandler(docRoot: HTMLElement) {
|
|||
}
|
||||
}
|
||||
|
||||
function findIndexOfChild(elem: Element, child: Element) {
|
||||
const children = castChildrenToSourceMappingElement(elem);
|
||||
// console.log(elem, "::", children, "=>", child);
|
||||
return children.findIndex((x) => x[1] === child);
|
||||
}
|
||||
|
||||
export function resolveSourceLeaf(elem: Element, path: ElementPoint[]): [Element, number] | undefined {
|
||||
const page = elem.getElementsByClassName('typst-page')[0];
|
||||
export function resolveSourceLeaf(
|
||||
elem: Element,
|
||||
path: ElementPoint[],
|
||||
): [Element, number] | undefined {
|
||||
const page = elem.getElementsByClassName("typst-page")[0];
|
||||
let curElem = page;
|
||||
|
||||
for (const point of path.slice(1)) {
|
||||
|
@ -110,221 +105,68 @@ export function resolveSourceLeaf(elem: Element, path: ElementPoint[]): [Element
|
|||
return [curElem, 0];
|
||||
}
|
||||
|
||||
// const rotateColors = [
|
||||
// "green",
|
||||
// "blue",
|
||||
// "red",
|
||||
// "orange",
|
||||
// "purple",
|
||||
// "yellow",
|
||||
// "cyan",
|
||||
// "magenta",
|
||||
// ];
|
||||
|
||||
function getCharIndex(elem: Element, mouseX: number, mouseY: number) {
|
||||
let useIndex = 0;
|
||||
let foundIndex = -1;
|
||||
const textRect = elem.getBoundingClientRect();
|
||||
|
||||
type SelRect = Pick<DOMRect, 'left' | 'right' | 'top' | 'bottom'>;
|
||||
let previousSelRect: SelRect | undefined = undefined;
|
||||
const unionRect = (a: SelRect, b?: SelRect) => {
|
||||
if (!b) {
|
||||
return a;
|
||||
}
|
||||
return {
|
||||
left: Math.min(a.left, b.left),
|
||||
top: Math.min(a.top, b.top),
|
||||
right: Math.max(a.right, b.right),
|
||||
bottom: Math.max(a.bottom, b.bottom),
|
||||
};
|
||||
}
|
||||
const inRect = (rect: SelRect, x: number, y: number) => {
|
||||
return rect.left <= x && x <= rect.right &&
|
||||
rect.top <= y && y <= rect.bottom;
|
||||
}
|
||||
|
||||
const enum TextFlowDirection {
|
||||
LeftToRight = 0,
|
||||
RightToLeft = 1,
|
||||
TopToBottom = 2,
|
||||
BottomToTop = 3,
|
||||
};
|
||||
|
||||
let textFlowDir = TextFlowDirection.LeftToRight;
|
||||
const isHorizontalFlow = () => {
|
||||
return textFlowDir === TextFlowDirection.LeftToRight ||
|
||||
textFlowDir === TextFlowDirection.RightToLeft;
|
||||
}
|
||||
|
||||
{
|
||||
let use0: Element = undefined!;
|
||||
let use1: Element = undefined!;
|
||||
for (const use of elem.children) {
|
||||
if (use.tagName !== 'use') {
|
||||
continue;
|
||||
}
|
||||
if (!use0) {
|
||||
use0 = use;
|
||||
continue;
|
||||
}
|
||||
use1 = use;
|
||||
break;
|
||||
}
|
||||
|
||||
if (use0 && use1) {
|
||||
const use0Rect = use0.getBoundingClientRect();
|
||||
const use1Rect = use1.getBoundingClientRect();
|
||||
|
||||
const use0Center = {
|
||||
x: (use0Rect.left + use0Rect.right) / 2,
|
||||
y: (use0Rect.top + use0Rect.bottom) / 2,
|
||||
};
|
||||
const use1Center = {
|
||||
x: (use1Rect.left + use1Rect.right) / 2,
|
||||
y: (use1Rect.top + use1Rect.bottom) / 2,
|
||||
};
|
||||
const vec = {
|
||||
x: use1Center.x - use0Center.x,
|
||||
y: use1Center.y - use0Center.y,
|
||||
};
|
||||
const angle = Math.atan2(vec.y, vec.x);
|
||||
// console.log('angle', angle);i
|
||||
if (angle > -Math.PI / 4 && angle < Math.PI / 4) {
|
||||
textFlowDir = TextFlowDirection.LeftToRight;
|
||||
} else if (angle < -Math.PI / 4 && angle > -Math.PI * 3 / 4) {
|
||||
textFlowDir = TextFlowDirection.TopToBottom;
|
||||
} else if (angle > Math.PI / 4 && angle < Math.PI * 3 / 4) {
|
||||
textFlowDir = TextFlowDirection.BottomToTop;
|
||||
} else {
|
||||
textFlowDir = TextFlowDirection.RightToLeft;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const use of elem.children) {
|
||||
if (use.tagName !== 'use') {
|
||||
continue;
|
||||
}
|
||||
const useRect = use.getBoundingClientRect();
|
||||
const selRect = isHorizontalFlow() ? {
|
||||
left: useRect.left,
|
||||
right: useRect.right,
|
||||
top: textRect.top,
|
||||
bottom: textRect.bottom,
|
||||
} : {
|
||||
left: textRect.left,
|
||||
right: textRect.right,
|
||||
top: useRect.top,
|
||||
bottom: useRect.bottom,
|
||||
};
|
||||
previousSelRect = unionRect(selRect, previousSelRect);
|
||||
|
||||
// draw sel rect for debugging
|
||||
// const selRectElem = document.createElement('div');
|
||||
// selRectElem.style.position = 'absolute';
|
||||
// selRectElem.style.left = `${selRect.left}px`;
|
||||
// selRectElem.style.top = `${selRect.top}px`;
|
||||
// selRectElem.style.width = `${selRect.right - selRect.left}px`;
|
||||
// selRectElem.style.height = `${selRect.bottom - selRect.top}px`;
|
||||
// selRectElem.style.border = `1px solid ${rotateColors[useIndex % rotateColors.length]}`;
|
||||
// selRectElem.style.zIndex = '100';
|
||||
// document.body.appendChild(selRectElem);
|
||||
// console.log(textRect, selRect);
|
||||
|
||||
// set index to end range of this char
|
||||
useIndex++;
|
||||
if (inRect(selRect, mouseX, mouseY)) {
|
||||
foundIndex = useIndex;
|
||||
} else if (previousSelRect) { // may fallback to space in between chars
|
||||
if (inRect(previousSelRect, mouseX, mouseY)) {
|
||||
foundIndex = useIndex - 1;
|
||||
previousSelRect = selRect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return foundIndex;
|
||||
}
|
||||
|
||||
export function installEditorJumpToHandler(docRoot: HTMLElement) {
|
||||
const collectElementPath = async (event: MouseEvent, elem: Element) => {
|
||||
const visitChain: [SourceMappingType, Element, string][] = [];
|
||||
const resolveFrameLoc = async (event: MouseEvent, elem: Element) => {
|
||||
const x = event.clientX;
|
||||
const y = event.clientY;
|
||||
|
||||
let mayPageElem: [SourceMappingType, Element, string] | undefined = undefined;
|
||||
|
||||
while (elem) {
|
||||
let srcElem = castToSourceMappingElement(elem);
|
||||
if (srcElem) {
|
||||
if (srcElem[0] === SourceMappingType.CharIndex) {
|
||||
const textElem = elem.parentElement?.parentElement?.parentElement!;
|
||||
let foundIndex = -1;
|
||||
if (textElem) {
|
||||
foundIndex = getCharIndex(textElem, event.clientX, event.clientY);
|
||||
}
|
||||
if (foundIndex !== -1) {
|
||||
(srcElem[1] as any) = foundIndex;
|
||||
visitChain.push(srcElem);
|
||||
}
|
||||
} else {
|
||||
visitChain.push(srcElem);
|
||||
}
|
||||
mayPageElem = castToSourceMappingElement(elem);
|
||||
if (mayPageElem && mayPageElem[0] === SourceMappingType.Page) {
|
||||
break;
|
||||
}
|
||||
if (elem === docRoot) {
|
||||
break;
|
||||
return;
|
||||
}
|
||||
elem = elem.parentElement!;
|
||||
}
|
||||
|
||||
if (visitChain.length === 0) {
|
||||
if (!mayPageElem) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// console.log('visitChain', visitChain);
|
||||
const pageElem = mayPageElem[1];
|
||||
console.log(mayPageElem, pageElem);
|
||||
|
||||
let startIdx = 1;
|
||||
if (visitChain.length >= 1 && visitChain[0][0] === SourceMappingType.CharIndex) {
|
||||
startIdx = 2;
|
||||
}
|
||||
for (let idx = startIdx; idx < visitChain.length; idx++) {
|
||||
if (visitChain[idx - 1][0] === SourceMappingType.CharIndex) {
|
||||
throw new Error("unexpected");
|
||||
}
|
||||
const pageRect = pageElem.getBoundingClientRect();
|
||||
const pageX = x - pageRect.left;
|
||||
const pageY = y - pageRect.top;
|
||||
|
||||
const childIdx = findIndexOfChild(
|
||||
visitChain[idx][1],
|
||||
visitChain[idx - 1][1]
|
||||
);
|
||||
if (childIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
(visitChain[idx - 1][1] as any) = childIdx;
|
||||
}
|
||||
const xPercent = pageX / pageRect.width;
|
||||
const yPercent = pageY / pageRect.height;
|
||||
const pageNumber = pageElem.getAttribute("data-page-number")!;
|
||||
const dataWidthS = pageElem.getAttribute("data-page-width")!;
|
||||
const dataHeightS = pageElem.getAttribute("data-page-height")!;
|
||||
|
||||
visitChain.reverse();
|
||||
console.log(pageNumber, dataWidthS, dataHeightS);
|
||||
|
||||
const pg = visitChain[0];
|
||||
if (pg[0] !== SourceMappingType.Page) {
|
||||
if (!pageNumber || !dataWidthS || !dataHeightS) {
|
||||
return undefined;
|
||||
}
|
||||
const childIdx = findIndexOfChild(pg[1].parentElement!, visitChain[0][1]);
|
||||
if (childIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
(visitChain[0][1] as any) = childIdx;
|
||||
const dataWidth = Number.parseFloat(dataWidthS);
|
||||
const dataHeight = Number.parseFloat(dataHeightS);
|
||||
|
||||
const sourceNodePath = visitChain;
|
||||
return sourceNodePath;
|
||||
return {
|
||||
page_no: Number.parseInt(pageNumber) + 1,
|
||||
x: xPercent * dataWidth,
|
||||
y: yPercent * dataHeight,
|
||||
};
|
||||
};
|
||||
|
||||
removeSourceMappingHandler(docRoot);
|
||||
const sourceMappingHandler = ((docRoot as any).sourceMappingHandler = async (
|
||||
event: MouseEvent
|
||||
event: MouseEvent,
|
||||
) => {
|
||||
let elem = event.target! as Element;
|
||||
|
||||
const elementPath = await collectElementPath(event, elem);
|
||||
if (!elementPath) {
|
||||
const frameLoc = await resolveFrameLoc(event, elem);
|
||||
if (!frameLoc) {
|
||||
return;
|
||||
}
|
||||
console.log("element path", elementPath);
|
||||
console.log("frameLoc", frameLoc);
|
||||
window.typstWebsocket.send(`src-point ${JSON.stringify(frameLoc)}`);
|
||||
|
||||
const triggerWindow = document.body || document.firstElementChild;
|
||||
const basePos = triggerWindow.getBoundingClientRect();
|
||||
|
@ -338,22 +180,20 @@ export function installEditorJumpToHandler(docRoot: HTMLElement) {
|
|||
left,
|
||||
top,
|
||||
"typst-debug-react-ripple",
|
||||
"typst-debug-react-ripple-effect .4s linear"
|
||||
"typst-debug-react-ripple-effect .4s linear",
|
||||
);
|
||||
|
||||
window.typstWebsocket.send(`srcpath ${JSON.stringify(elementPath)}`);
|
||||
return;
|
||||
});
|
||||
|
||||
docRoot.addEventListener("click", sourceMappingHandler);
|
||||
}
|
||||
|
||||
export interface TypstDebugJumpDocument {
|
||||
}
|
||||
export interface TypstDebugJumpDocument {}
|
||||
|
||||
export function provideDebugJumpDoc<
|
||||
TBase extends GConstructor<TypstDocumentContext>
|
||||
>(Base: TBase): TBase & GConstructor<TypstDebugJumpDocument> {
|
||||
export function provideDebugJumpDoc<TBase extends GConstructor<TypstDocumentContext>>(
|
||||
Base: TBase,
|
||||
): TBase & GConstructor<TypstDebugJumpDocument> {
|
||||
return class DebugJumpDocument extends Base {
|
||||
constructor(...args: any[]) {
|
||||
super(...args);
|
||||
|
|
|
@ -11,9 +11,7 @@ export interface TypstSvgDocument {
|
|||
}
|
||||
|
||||
export function provideSvgDoc<
|
||||
TBase extends GConstructor<
|
||||
TypstDocumentContext & Partial<TypstCanvasDocument>
|
||||
>,
|
||||
TBase extends GConstructor<TypstDocumentContext & Partial<TypstCanvasDocument>>,
|
||||
>(Base: TBase): TBase & GConstructor<TypstSvgDocument> {
|
||||
return class SvgDocument extends Base {
|
||||
/// canvas render ctoken
|
||||
|
@ -55,9 +53,7 @@ export function provideSvgDoc<
|
|||
}
|
||||
|
||||
const t2 = performance.now();
|
||||
patchSvgToContainer(this.hookedElem, patchStr, (elem) =>
|
||||
this.decorateSvgElement(elem, mode)
|
||||
);
|
||||
patchSvgToContainer(this.hookedElem, patchStr, (elem) => this.decorateSvgElement(elem, mode));
|
||||
const t3 = performance.now();
|
||||
|
||||
if (this.cursorPaths) {
|
||||
|
@ -101,10 +97,7 @@ export function provideSvgDoc<
|
|||
const rectNextBase = foundUseNext?.getBBox();
|
||||
const rect = {
|
||||
// Some char does not have position so they are resolved to 0
|
||||
right:
|
||||
rectBase.width !== 0
|
||||
? rectBase.x + rectBase.width
|
||||
: rectNextBase?.x || 0,
|
||||
right: rectBase.width !== 0 ? rectBase.x + rectBase.width : rectNextBase?.x || 0,
|
||||
// todo: have bug
|
||||
// top: textBase.height / 2,
|
||||
};
|
||||
|
@ -131,10 +124,7 @@ export function provideSvgDoc<
|
|||
ry = Math.abs(ry);
|
||||
|
||||
// Creates a circle with 5px radius (but regard vertical and horizontal scale)
|
||||
const t = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"ellipse"
|
||||
);
|
||||
const t = document.createElementNS("http://www.w3.org/2000/svg", "ellipse");
|
||||
t.classList.add("typst-svg-cursor");
|
||||
t.setAttribute("cx", `${rect.right}`);
|
||||
// t.setAttribute('cy', `${rect.top}`);
|
||||
|
@ -201,16 +191,12 @@ export function provideSvgDoc<
|
|||
let topEstimate = top - 1,
|
||||
bottomEstimate = top + height + 1;
|
||||
if (ch) {
|
||||
const pages = Array.from(ch).filter((x) =>
|
||||
x.classList.contains("typst-page")
|
||||
);
|
||||
const pages = Array.from(ch).filter((x) => x.classList.contains("typst-page"));
|
||||
let minTop = 1e33,
|
||||
maxBottom = -1e33,
|
||||
accumulatedHeight = 0;
|
||||
for (const page of pages) {
|
||||
const pageHeight = Number.parseFloat(
|
||||
page.getAttribute("data-page-height")!
|
||||
);
|
||||
const pageHeight = Number.parseFloat(page.getAttribute("data-page-height")!);
|
||||
const translateY = Number.parseFloat(page.getAttribute("data-y")!);
|
||||
if (translateY + pageHeight > topEstimate) {
|
||||
minTop = Math.min(minTop, accumulatedHeight);
|
||||
|
@ -236,7 +222,7 @@ export function provideSvgDoc<
|
|||
topEstimate,
|
||||
// hi.x, hi.y
|
||||
left + width + 1,
|
||||
bottomEstimate
|
||||
bottomEstimate,
|
||||
);
|
||||
console.log(
|
||||
"render_in_window with partial rendering enabled window",
|
||||
|
@ -246,16 +232,10 @@ export function provideSvgDoc<
|
|||
width,
|
||||
height,
|
||||
", patch scale",
|
||||
patchStr.length
|
||||
patchStr.length,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
"render_in_window with partial rendering disabled",
|
||||
0,
|
||||
0,
|
||||
1e33,
|
||||
1e33
|
||||
);
|
||||
console.log("render_in_window with partial rendering disabled", 0, 0, 1e33, 1e33);
|
||||
patchStr = this.kModule.render_in_window(0, 0, 1e33, 1e33);
|
||||
}
|
||||
|
||||
|
@ -333,8 +313,7 @@ export function provideSvgDoc<
|
|||
|
||||
private decorateSvgElement(svg: SVGElement, mode: PreviewMode) {
|
||||
const container = this.cachedDOMState;
|
||||
const kShouldMixinCanvas =
|
||||
this.previewMode === PreviewMode.Doc && this.shouldMixinCanvas();
|
||||
const kShouldMixinCanvas = this.previewMode === PreviewMode.Doc && this.shouldMixinCanvas();
|
||||
|
||||
// the <rect> could only have integer width and height
|
||||
// so we scale it by 100 to make it more accurate
|
||||
|
@ -354,7 +333,7 @@ export function provideSvgDoc<
|
|||
const nextPages: SvgPage[] = (() => {
|
||||
/// Retrieve original pages
|
||||
const filteredNextPages = Array.from(svg.children).filter((x) =>
|
||||
x.classList.contains("typst-page")
|
||||
x.classList.contains("typst-page"),
|
||||
);
|
||||
|
||||
if (mode === PreviewMode.Doc) {
|
||||
|
@ -368,9 +347,7 @@ export function provideSvgDoc<
|
|||
}
|
||||
})().map((elem, index) => {
|
||||
const width = Number.parseFloat(elem.getAttribute("data-page-width")!);
|
||||
const height = Number.parseFloat(
|
||||
elem.getAttribute("data-page-height")!
|
||||
);
|
||||
const height = Number.parseFloat(elem.getAttribute("data-page-height")!);
|
||||
maxWidth = Math.max(maxWidth, width);
|
||||
return {
|
||||
index,
|
||||
|
@ -420,7 +397,7 @@ export function provideSvgDoc<
|
|||
inserter: (pageInfo) => {
|
||||
const foreignObject = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"foreignObject"
|
||||
"foreignObject",
|
||||
);
|
||||
elem.appendChild(foreignObject);
|
||||
foreignObject.setAttribute("width", `${width}`);
|
||||
|
@ -436,11 +413,7 @@ export function provideSvgDoc<
|
|||
for (let i = 0; i < nextPages.length; i++) {
|
||||
/// Retrieve page width, height
|
||||
const nextPage = nextPages[i];
|
||||
const {
|
||||
width: pageWidth,
|
||||
height: pageHeight,
|
||||
elem: pageElem,
|
||||
} = nextPage;
|
||||
const { width: pageWidth, height: pageHeight, elem: pageElem } = nextPage;
|
||||
|
||||
/// Switch a dummy svg page to canvas mode
|
||||
if (kShouldMixinCanvas && isDummyPatchElem(pageElem)) {
|
||||
|
@ -459,32 +432,19 @@ export function provideSvgDoc<
|
|||
|
||||
/// center the page and add margin
|
||||
const calculatedPaddedX = (newWidth - pageWidth) / 2;
|
||||
const calculatedPaddedY =
|
||||
accumulatedHeight + (i == 0 ? 0 : heightMargin);
|
||||
const calculatedPaddedY = accumulatedHeight + (i == 0 ? 0 : heightMargin);
|
||||
const translateAttr = `translate(${calculatedPaddedX}, ${calculatedPaddedY})`;
|
||||
|
||||
/// Create inner rectangle
|
||||
const innerRect = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"rect"
|
||||
);
|
||||
const innerRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||
innerRect.setAttribute("class", "typst-page-inner");
|
||||
innerRect.setAttribute("data-page-width", pageWidth.toString());
|
||||
innerRect.setAttribute("data-page-height", pageHeight.toString());
|
||||
innerRect.setAttribute(
|
||||
"width",
|
||||
Math.floor(pageWidth * INNER_RECT_UNIT).toString()
|
||||
);
|
||||
innerRect.setAttribute(
|
||||
"height",
|
||||
Math.floor(pageHeight * INNER_RECT_UNIT).toString()
|
||||
);
|
||||
innerRect.setAttribute("width", Math.floor(pageWidth * INNER_RECT_UNIT).toString());
|
||||
innerRect.setAttribute("height", Math.floor(pageHeight * INNER_RECT_UNIT).toString());
|
||||
innerRect.setAttribute("x", "0");
|
||||
innerRect.setAttribute("y", "0");
|
||||
innerRect.setAttribute(
|
||||
"transform",
|
||||
`${translateAttr} ${INNER_RECT_SCALE}`
|
||||
);
|
||||
innerRect.setAttribute("transform", `${translateAttr} ${INNER_RECT_SCALE}`);
|
||||
if (this.pageColor) {
|
||||
innerRect.setAttribute("fill", this.pageColor);
|
||||
}
|
||||
|
@ -497,6 +457,7 @@ export function provideSvgDoc<
|
|||
pageElem.setAttribute("transform", translateAttr);
|
||||
pageElem.setAttribute("data-x", `${calculatedPaddedX}`);
|
||||
pageElem.setAttribute("data-y", `${calculatedPaddedY}`);
|
||||
pageElem.setAttribute("data-page-number", `${nextPage.index}`);
|
||||
|
||||
/// Insert rectangles
|
||||
// todo: this is buggy not preserving order?
|
||||
|
@ -505,15 +466,9 @@ export function provideSvgDoc<
|
|||
firstRect = innerRect;
|
||||
}
|
||||
|
||||
const clipPath = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"clipPath"
|
||||
);
|
||||
const clipPath = document.createElementNS("http://www.w3.org/2000/svg", "clipPath");
|
||||
|
||||
const clipRect = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"rect"
|
||||
);
|
||||
const clipRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||
|
||||
clipRect.setAttribute("x", "0");
|
||||
clipRect.setAttribute("y", "0");
|
||||
|
@ -526,8 +481,7 @@ export function provideSvgDoc<
|
|||
clipPath.setAttribute("id", clipId);
|
||||
pageElem.setAttribute("clip-path", `url(#${clipId})`);
|
||||
|
||||
let pageHeightEnd =
|
||||
pageHeight + (i + 1 === nextPages.length ? 0 : heightMargin);
|
||||
let pageHeightEnd = pageHeight + (i + 1 === nextPages.length ? 0 : heightMargin);
|
||||
|
||||
if (this.isContentPreview) {
|
||||
// --typst-preview-toolbar-fg-color
|
||||
|
@ -535,21 +489,14 @@ export function provideSvgDoc<
|
|||
// console.log('create page number indicator', scale);
|
||||
const pageNumberIndicator = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"text"
|
||||
);
|
||||
pageNumberIndicator.setAttribute(
|
||||
"class",
|
||||
"typst-preview-svg-page-number"
|
||||
"text",
|
||||
);
|
||||
pageNumberIndicator.setAttribute("class", "typst-preview-svg-page-number");
|
||||
pageNumberIndicator.setAttribute("x", "0");
|
||||
pageNumberIndicator.setAttribute("y", "0");
|
||||
const onPaddedX = calculatedPaddedX + pageWidth / 2;
|
||||
const onPaddedY =
|
||||
calculatedPaddedY + pageHeight + heightMargin + fontSize / 2;
|
||||
pageNumberIndicator.setAttribute(
|
||||
"transform",
|
||||
`translate(${onPaddedX}, ${onPaddedY})`
|
||||
);
|
||||
const onPaddedY = calculatedPaddedY + pageHeight + heightMargin + fontSize / 2;
|
||||
pageNumberIndicator.setAttribute("transform", `translate(${onPaddedX}, ${onPaddedY})`);
|
||||
pageNumberIndicator.setAttribute("font-size", fontSize.toString());
|
||||
pageNumberIndicator.textContent = `${i + 1}`;
|
||||
svg.append(pageNumberIndicator);
|
||||
|
@ -558,18 +505,12 @@ export function provideSvgDoc<
|
|||
} else {
|
||||
if (this.cursorPosition && this.cursorPosition[0] === i + 1) {
|
||||
const [_, x, y] = this.cursorPosition;
|
||||
const cursor = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"circle"
|
||||
);
|
||||
const cursor = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
||||
cursor.setAttribute("cx", (x * INNER_RECT_UNIT).toString());
|
||||
cursor.setAttribute("cy", (y * INNER_RECT_UNIT).toString());
|
||||
cursor.setAttribute("r", (5 * scale * INNER_RECT_UNIT).toString());
|
||||
cursor.setAttribute("fill", "#86C166CC");
|
||||
cursor.setAttribute(
|
||||
"transform",
|
||||
`${translateAttr} ${INNER_RECT_SCALE}`
|
||||
);
|
||||
cursor.setAttribute("transform", `${translateAttr} ${INNER_RECT_SCALE}`);
|
||||
svg.appendChild(cursor);
|
||||
}
|
||||
}
|
||||
|
@ -593,9 +534,7 @@ export function provideSvgDoc<
|
|||
if (ch?.tagName === "foreignObject") {
|
||||
const canvasDiv = ch.firstElementChild as HTMLDivElement;
|
||||
|
||||
const pageNumber = Number.parseInt(
|
||||
canvasDiv.getAttribute("data-page-number")!
|
||||
);
|
||||
const pageNumber = Number.parseInt(canvasDiv.getAttribute("data-page-number")!);
|
||||
const pageInfo = n2CMapping.get(pageNumber);
|
||||
if (pageInfo) {
|
||||
pageInfo.container = canvasDiv as HTMLDivElement;
|
||||
|
@ -616,7 +555,7 @@ export function provideSvgDoc<
|
|||
|
||||
console.assert(
|
||||
this.canvasRenderCToken === undefined,
|
||||
"No!!: canvasRenderCToken should be undefined"
|
||||
"No!!: canvasRenderCToken should be undefined",
|
||||
);
|
||||
|
||||
const tok = (this.canvasRenderCToken = new TypstCancellationToken());
|
||||
|
@ -633,7 +572,7 @@ export function provideSvgDoc<
|
|||
}
|
||||
});
|
||||
},
|
||||
{ timeout: 1000 }
|
||||
{ timeout: 1000 },
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -648,10 +587,7 @@ export function provideSvgDoc<
|
|||
if (firstPage) {
|
||||
const rectHeight = Math.ceil(newHeight).toString();
|
||||
|
||||
const outerRect = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"rect"
|
||||
);
|
||||
const outerRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||
outerRect.setAttribute("class", "typst-page-outer");
|
||||
outerRect.setAttribute("data-page-width", newWidth.toString());
|
||||
outerRect.setAttribute("data-page-height", rectHeight);
|
||||
|
@ -680,7 +616,7 @@ export function provideSvgDoc<
|
|||
|
||||
if (svg) {
|
||||
let svgWidth = Number.parseFloat(
|
||||
svg.getAttribute("data-width")! || svg.getAttribute("width")! || "1"
|
||||
svg.getAttribute("data-width")! || svg.getAttribute("width")! || "1",
|
||||
);
|
||||
if (svgWidth < 1e-5) {
|
||||
svgWidth = 1;
|
||||
|
@ -692,14 +628,11 @@ export function provideSvgDoc<
|
|||
}
|
||||
|
||||
private statSvgFromDom() {
|
||||
const { width: containerWidth, boundingRect: containerBRect } =
|
||||
this.cachedDOMState;
|
||||
const { width: containerWidth, boundingRect: containerBRect } = this.cachedDOMState;
|
||||
// scale derived from svg width and container with.
|
||||
// svg.setAttribute("data-width", `${newWidth}`);
|
||||
|
||||
const computedRevScale = containerWidth
|
||||
? this.docWidth / containerWidth
|
||||
: 1;
|
||||
const computedRevScale = containerWidth ? this.docWidth / containerWidth : 1;
|
||||
// respect current scale ratio
|
||||
const revScale = computedRevScale / this.currentScaleRatio;
|
||||
const left = (window.screenLeft - containerBRect.left) * revScale;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue