"use client" import { useCallback, useEffect, useState } from "react" import type { Editor } from "@tiptap/react" import type { DocPageLayout } from "@/lib/drive/doc-page-setup" import { DOCS_PAGE_GAP_PX } from "@/lib/drive/docs-page-layout-constants" import { docsPageLengthToScreen, docsScreenLengthToPage, docsZoomToScale, } from "@/lib/drive/docs-ruler-scale" export type DocsParagraphIndents = { leftPx: number firstLinePx: number rightPx: number } export type DocsRulerSyncState = { currentPage: number pageTopInViewport: number pageHeightScaled: number /** Canvas client width — ruler track content area matches this. */ canvasWidth: number /** Scrollbar gutter so ruler track centers like the canvas viewport. */ canvasScrollbarWidth: number canvasScrollLeft: number indents: DocsParagraphIndents } const DEFAULT_INDENTS = (layout: DocPageLayout): DocsParagraphIndents => ({ leftPx: layout.marginsPx.left, firstLinePx: layout.marginsPx.left, rightPx: layout.widthPx - layout.marginsPx.right, }) function readIndents( editor: Editor | null, scale: number, layout: DocPageLayout ): DocsParagraphIndents { const fallback = DEFAULT_INDENTS(layout) if (!editor || editor.isDestroyed) return fallback const prose = editor.view.dom as HTMLElement | null if (!prose) return fallback const { from } = editor.state.selection const domPos = editor.view.domAtPos(from) let el = domPos.node as HTMLElement if (el.nodeType === Node.TEXT_NODE) el = el.parentElement ?? el const block = el.closest?.( "p, h1, h2, h3, h4, li, blockquote, pre, td, th" ) as HTMLElement | null if (!block || !prose.contains(block)) return fallback const pageStack = prose.closest( "[data-docs-page-stack]" ) as HTMLElement | null if (!pageStack) return fallback const pageRect = pageStack.getBoundingClientRect() const blockRect = block.getBoundingClientRect() const leftPx = docsScreenLengthToPage(blockRect.left - pageRect.left, scale) const style = getComputedStyle(block) const textIndent = parseFloat(style.textIndent) || 0 return { leftPx: Math.max(layout.marginsPx.left, leftPx), firstLinePx: Math.max(layout.marginsPx.left, leftPx + textIndent), rightPx: layout.widthPx - layout.marginsPx.right, } } import { computePageTopInViewport, resolveCurrentPageInViewport, } from "./docs-ruler-sync-math" export function useDocsRulerSync({ canvasRef, rulerTrackRef, editor, pageLayout, zoom, pageCount, }: { canvasRef: React.RefObject rulerTrackRef: React.RefObject editor: Editor | null pageLayout: DocPageLayout zoom: number pageCount: number narrowViewport: boolean }) { const scale = docsZoomToScale(zoom) const pageHeight = pageLayout.heightPx const pageHeightScaled = docsPageLengthToScreen(pageHeight, scale) const gapScaled = docsPageLengthToScreen(DOCS_PAGE_GAP_PX, scale) const pageStride = pageHeightScaled + gapScaled const [state, setState] = useState(() => ({ currentPage: 0, pageTopInViewport: 0, pageHeightScaled, canvasWidth: 0, canvasScrollbarWidth: 0, canvasScrollLeft: 0, indents: DEFAULT_INDENTS(pageLayout), })) const sync = useCallback(() => { const canvas = canvasRef.current if (!canvas) return const canvasRect = canvas.getBoundingClientRect() const stack = canvas.querySelector("[data-docs-page-stack]") as HTMLElement | null let pageTopInViewport = 0 let currentPage = 0 if (stack) { const stackRect = stack.getBoundingClientRect() const stackTopInViewport = stackRect.top - canvasRect.top currentPage = resolveCurrentPageInViewport( stackTopInViewport, canvas.clientHeight, pageStride, pageCount ) pageTopInViewport = computePageTopInViewport( stackTopInViewport, currentPage, pageStride ) } setState({ currentPage, pageTopInViewport, pageHeightScaled, canvasWidth: canvas.clientWidth, canvasScrollbarWidth: Math.max(0, canvas.offsetWidth - canvas.clientWidth), canvasScrollLeft: canvas.scrollLeft, indents: readIndents(editor, scale, pageLayout), }) }, [canvasRef, editor, pageCount, pageHeightScaled, pageLayout, pageStride, scale]) useEffect(() => { sync() const canvas = canvasRef.current if (!canvas) return canvas.addEventListener("scroll", sync, { passive: true }) const ro = new ResizeObserver(sync) ro.observe(canvas) const rulerTrack = rulerTrackRef.current if (rulerTrack) ro.observe(rulerTrack) return () => { canvas.removeEventListener("scroll", sync) ro.disconnect() } }, [canvasRef, rulerTrackRef, sync]) useEffect(() => { if (!editor || editor.isDestroyed) return const onSelection = () => sync() editor.on("selectionUpdate", onSelection) editor.on("transaction", onSelection) return () => { editor.off("selectionUpdate", onSelection) editor.off("transaction", onSelection) } }, [editor, sync]) useEffect(() => { sync() }, [pageLayout, zoom, pageCount, sync]) return state }