fix: bidirectional jump in slide mode (#1873)
Some checks are pending
tinymist::ci / Duplicate Actions Detection (push) Waiting to run
tinymist::ci / Check Clippy, Formatting, Completion, Documentation, and Tests (Linux) (push) Waiting to run
tinymist::ci / Check Minimum Rust version and Tests (Windows) (push) Waiting to run
tinymist::ci / E2E Tests (darwin-arm64 on macos-latest) (push) Blocked by required conditions
tinymist::ci / E2E Tests (linux-x64 on ubuntu-22.04) (push) Blocked by required conditions
tinymist::ci / E2E Tests (linux-x64 on ubuntu-latest) (push) Blocked by required conditions
tinymist::ci / E2E Tests (win32-x64 on windows-2022) (push) Blocked by required conditions
tinymist::ci / E2E Tests (win32-x64 on windows-latest) (push) Blocked by required conditions
tinymist::ci / prepare-build (push) Waiting to run
tinymist::ci / build-binary (push) Blocked by required conditions
tinymist::ci / build-vsc-assets (push) Blocked by required conditions
tinymist::ci / build-vscode (push) Blocked by required conditions
tinymist::ci / build-vscode-others (push) Blocked by required conditions
tinymist::ci / publish-vscode (push) Blocked by required conditions
tinymist::gh_pages / build-gh-pages (push) Waiting to run

This commit is contained in:
Myriad-Dreamin 2025-07-06 00:34:24 +08:00 committed by GitHub
parent 32bd813be6
commit 2b7482b263
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 157 additions and 99 deletions

View file

@ -96,7 +96,7 @@ export function previewActivate(context: vscode.ExtensionContext, isCompat: bool
);
const launchBrowsingPreview = launch("webview", "doc", { isBrowsing: true });
const launchDevPreview = launch("webview", "doc", { isDev: true });
const launchDevPreview = (mode: "doc" | "slide") => launch("webview", mode, { isDev: true });
// Registers preview commands, check `package.json` for descriptions.
context.subscriptions.push(
vscode.commands.registerCommand("tinymist.browsingPreview", launchBrowsingPreview),
@ -104,16 +104,19 @@ export function previewActivate(context: vscode.ExtensionContext, isCompat: bool
vscode.commands.registerCommand("typst-preview.browser", launch("browser", "doc")),
vscode.commands.registerCommand("typst-preview.preview-slide", launch("webview", "slide")),
vscode.commands.registerCommand("typst-preview.browser-slide", launch("browser", "slide")),
vscode.commands.registerCommand("typst-preview.eject", isCompat ? ejectPreviewPanelCompat : ejectPreviewPanelLsp),
vscode.commands.registerCommand("tinymist.previewDev", launchDevPreview),
vscode.commands.registerCommand(
"typst-preview.revealDocument",
isCompat ? revealDocumentCompat : revealDocumentLsp,
),
vscode.commands.registerCommand(
"typst-preview.sync",
isCompat ? panelSyncScrollCompat : panelSyncScrollLsp,
),
vscode.commands.registerCommand("tinymist.previewDev", launchDevPreview("doc")),
vscode.commands.registerCommand("tinymist.previewDevSlide", launchDevPreview("slide")),
...(isCompat
? [
vscode.commands.registerCommand("typst-preview.eject", ejectPreviewPanelCompat),
vscode.commands.registerCommand("typst-preview.revealDocument", revealDocumentCompat),
vscode.commands.registerCommand("typst-preview.sync", panelSyncScrollCompat),
]
: [
vscode.commands.registerCommand("typst-preview.eject", ejectPreviewPanelLsp),
vscode.commands.registerCommand("typst-preview.revealDocument", revealDocumentLsp),
vscode.commands.registerCommand("typst-preview.sync", panelSyncScrollLsp),
]),
vscode.commands.registerCommand("tinymist.doInspectPreviewState", () => {
const tasks = Array.from(activeTask.values()).map((t) => {
return {
@ -186,7 +189,12 @@ export function previewActivate(context: vscode.ExtensionContext, isCompat: bool
};
}
async function launchForURI(uri: vscode.Uri, kind: "browser" | "webview", mode: "doc" | "slide", opts?: LaunchOpts) {
async function launchForURI(
uri: vscode.Uri,
kind: "browser" | "webview",
mode: "doc" | "slide",
opts?: LaunchOpts,
) {
const doc =
vscode.workspace.textDocuments.find((doc) => {
return doc.uri.toString() === uri.toString();
@ -318,7 +326,7 @@ export async function openPreviewInWebView({
uri: activeEditor.document.uri.toString(),
};
const updateActivePanel =() => {
const updateActivePanel = () => {
if (panel.active) {
extensionState.mut.focusingPreviewPanelContext = {
panel,

View file

@ -0,0 +1,20 @@
#import "@preview/touying:0.6.1": *
#import themes.dewdrop: *
#show: dewdrop-theme.with(aspect-ratio: "16-9")
= Section 1
== Subsection 1
=== Title
Hello, Touying!
= Section 2
== Subsection 2
=== Title
Hello, Touying!

View file

@ -1,5 +1,5 @@
import { triggerRipple } from "./typst-animation.mjs";
import type { GConstructor, TypstDocumentContext } from "./typst-doc.mjs";
import { PreviewMode, type GConstructor, type TypstDocumentContext } from "./typst-doc.mjs";
const enum SourceMappingType {
Text = 0,
@ -225,5 +225,83 @@ export function provideDebugJumpDoc<TBase extends GConstructor<TypstDocumentCont
});
}
}
scrollTo(pageRect: ScrollRect, pageNo: number, innerLeft: number, innerTop: number) {
if (this.previewMode === PreviewMode.Slide) {
this.setPartialPageNumber(pageNo);
return;
}
const windowRoot = document.body || document.firstElementChild;
const basePos = windowRoot.getBoundingClientRect();
const left = innerLeft - basePos.left;
const top = innerTop - basePos.top;
// evaluate window viewport 1vw
const pw = window.innerWidth * 0.01;
const ph = window.innerHeight * 0.01;
const xOffsetInnerFix = 7 * pw;
const yOffsetInnerFix = 38.2 * ph;
const xOffset = left - xOffsetInnerFix;
const yOffset = top - yOffsetInnerFix;
const widthOccupied = (100 * 100 * pw) / pageRect.width;
const pageAdjustLeft = pageRect.left - basePos.left - 5 * pw;
const pageAdjust = pageRect.left - basePos.left + pageRect.width - 95 * pw;
// default single-column or multi-column layout
if (widthOccupied >= 90 || widthOccupied < 50) {
window.scrollTo({ behavior: "smooth", left: xOffset, top: yOffset });
} else {
// for double-column layout
// console.log('occupied adjustment', widthOccupied, page);
const xOffsetAdjsut = xOffset > pageAdjust ? pageAdjust : pageAdjustLeft;
window.scrollTo({ behavior: "smooth", left: xOffsetAdjsut, top: yOffset });
}
// grid ripple for debug vw
// triggerRipple(
// windowRoot,
// svgRect.left + 50 * vw,
// svgRect.top + 1 * vh,
// "typst-jump-ripple",
// "typst-jump-ripple-effect .4s linear",
// "green",
// );
// triggerRipple(
// windowRoot,
// pageRect.left - basePos.left + vw,
// pageRect.top - basePos.top + vh,
// "typst-jump-ripple",
// "typst-jump-ripple-effect .4s linear",
// "red",
// );
// triggerRipple(
// windowRoot,
// pageAdjust,
// pageRect.top - basePos.top + vh,
// "typst-jump-ripple",
// "typst-jump-ripple-effect .4s linear",
// "red",
// );
triggerRipple(
windowRoot,
left,
top,
"typst-jump-ripple",
"typst-jump-ripple-effect .4s linear",
);
}
};
}
type ScrollRect = Pick<DOMRect, "left" | "top" | "width" | "height">;

View file

@ -463,6 +463,19 @@ export class TypstDocumentContext<O = any> {
addViewportChange() {
this.addChangement(["viewport-change", ""]);
}
setPartialPageNumber(page: number): boolean {
if (page <= 0 || page > this.kModule.retrievePagesInfo().length) {
return false;
}
this.partialRenderPage = page - 1;
this.addViewportChange();
return true;
}
getPartialPageNumber(): number {
return this.partialRenderPage + 1;
}
}
export interface TypstDocument<T> {
@ -536,16 +549,11 @@ export function provideDoc<T extends TypstDocumentContext>(
}
setPartialPageNumber(page: number): boolean {
if (page <= 0 || page > this.kModule.retrievePagesInfo().length) {
return false;
}
this.impl.partialRenderPage = page - 1;
this.addViewportChange();
return true;
return this.impl.setPartialPageNumber(page);
}
getPartialPageNumber(): number {
return this.impl.partialRenderPage + 1;
return this.impl.getPartialPageNumber();
}
setOutineData(outline: any) {

View file

@ -350,7 +350,7 @@ export function provideSvgDoc<
const height = Number.parseFloat(elem.getAttribute("data-page-height")!);
maxWidth = Math.max(maxWidth, width);
return {
index,
index: mode === PreviewMode.Slide ? this.partialRenderPage : index,
elem,
width,
height,

View file

@ -8,6 +8,7 @@ interface Window {
initTypstSvg(docRoot: SVGElement): void;
currentPosition(elem: Element): TypstPosition | undefined;
handleTypstLocation(elem: Element, page: number, x: number, y: number);
documents: any[];
typstWebsocket: WebSocket;
}
const acquireVsCodeApi: any;

View file

@ -9,6 +9,8 @@ import "./styles/outline.css";
import { wsMain, PreviewMode } from "./ws";
import { setupDrag } from "./drag";
window.documents = [];
/// Main entry point of the frontend program.
main();

View file

@ -1,5 +1,3 @@
import { triggerRipple } from "typst-dom/typst-animation.mjs";
// debounce https://stackoverflow.com/questions/23181243/throttling-a-mousemove-event-to-fire-no-more-than-5-times-a-second
// ignore fast events, good for capturing double click
// @param (callback): function to be run when done
@ -240,6 +238,7 @@ window.currentPosition = function (elem: Element) {
return result;
};
type ScrollRect = Pick<DOMRect, "left" | "top" | "width" | "height">;
window.handleTypstLocation = function (elem: Element, pageNo: number, x: number, y: number) {
const docRoot = findAncestor(elem, "typst-doc");
if (!docRoot) {
@ -247,76 +246,12 @@ window.handleTypstLocation = function (elem: Element, pageNo: number, x: number,
return;
}
type ScrollRect = Pick<DOMRect, "left" | "top" | "width" | "height">;
const scrollTo = (pageRect: ScrollRect, innerLeft: number, innerTop: number) => {
const windowRoot = document.body || document.firstElementChild;
const basePos = windowRoot.getBoundingClientRect();
// scrollTo(pageRect: ScrollRect, pageNo: number, innerLeft: number, innerTop: number)
const left = innerLeft - basePos.left;
const top = innerTop - basePos.top;
// evaluate window viewport 1vw
const pw = window.innerWidth * 0.01;
const ph = window.innerHeight * 0.01;
const xOffsetInnerFix = 7 * pw;
const yOffsetInnerFix = 38.2 * ph;
const xOffset = left - xOffsetInnerFix;
const yOffset = top - yOffsetInnerFix;
const widthOccupied = (100 * 100 * pw) / pageRect.width;
const pageAdjustLeft = pageRect.left - basePos.left - 5 * pw;
const pageAdjust = pageRect.left - basePos.left + pageRect.width - 95 * pw;
// default single-column or multi-column layout
if (widthOccupied >= 90 || widthOccupied < 50) {
window.scrollTo({ behavior: "smooth", left: xOffset, top: yOffset });
} else {
// for double-column layout
// console.log('occupied adjustment', widthOccupied, page);
const xOffsetAdjsut = xOffset > pageAdjust ? pageAdjust : pageAdjustLeft;
window.scrollTo({ behavior: "smooth", left: xOffsetAdjsut, top: yOffset });
const scrollTo = (pageRect: ScrollRect, pageNo: number, innerLeft: number, innerTop: number) => {
for (const doc of window.documents) {
doc.impl.scrollTo(pageRect, pageNo, innerLeft, innerTop);
}
// grid ripple for debug vw
// triggerRipple(
// windowRoot,
// svgRect.left + 50 * vw,
// svgRect.top + 1 * vh,
// "typst-jump-ripple",
// "typst-jump-ripple-effect .4s linear",
// "green",
// );
// triggerRipple(
// windowRoot,
// pageRect.left - basePos.left + vw,
// pageRect.top - basePos.top + vh,
// "typst-jump-ripple",
// "typst-jump-ripple-effect .4s linear",
// "red",
// );
// triggerRipple(
// windowRoot,
// pageAdjust,
// pageRect.top - basePos.top + vh,
// "typst-jump-ripple",
// "typst-jump-ripple-effect .4s linear",
// "red",
// );
triggerRipple(
windowRoot,
left,
top,
"typst-jump-ripple",
"typst-jump-ripple-effect .4s linear",
);
};
const renderMode = docRoot.getAttribute("data-render-mode");
@ -359,7 +294,7 @@ window.handleTypstLocation = function (elem: Element, pageNo: number, x: number,
console.log("canvas mode jump", left, top, canvasRect, dataWidth, dataHeight, x, y);
scrollTo(canvasRect, left, top);
scrollTo(canvasRect, pageNo, left, top);
return;
}
@ -394,7 +329,7 @@ window.handleTypstLocation = function (elem: Element, pageNo: number, x: number,
const left = svgRect.left + (x / dataWidth) * svgRect.width;
const top = svgRect.top + (y / dataHeight) * svgRect.height;
scrollTo(pageRect, left, top);
scrollTo(pageRect, pageNo, left, top);
return;
}
}

View file

@ -218,6 +218,8 @@ export async function wsMain({ url, previewMode, isContentPreview }: WsArgs) {
}
function setupSocket(svgDoc: TypstDocument): () => void {
window.documents.push(svgDoc);
// todo: reconnect setTimeout(() => setupSocket(svgDoc), 1000);
$ws = webSocket<ArrayBuffer>({
url,
@ -249,6 +251,10 @@ export async function wsMain({ url, previewMode, isContentPreview }: WsArgs) {
const dispose = () => {
disposed = true;
svgDoc.dispose();
const index = window.documents.indexOf(svgDoc);
if (index >= 0) {
window.documents.splice(index, 1);
}
for (const sub of subsribes.splice(0, subsribes.length)) {
sub.unsubscribe();
}