From 9d2e59baf6c61f8b084dc6053cc92d671a76fecb Mon Sep 17 00:00:00 2001 From: Lycoon Date: Tue, 21 Jul 2026 01:38:40 +0200 Subject: [PATCH 1/2] fix inflated font size on mobile --- components/editor/DocumentEditorPanel.tsx | 37 +++++++++++++++++---- components/editor/EditorPanel.module.css | 40 +++++++++++++++++++---- src-tauri/Cargo.lock | 3 -- src-tauri/Cargo.toml | 6 ---- src-tauri/src/lib.rs | 38 +++++---------------- src/app/layout.tsx | 32 ------------------ styles/globals.css | 8 ----- styles/scriptio.css | 22 ------------- 8 files changed, 72 insertions(+), 114 deletions(-) diff --git a/components/editor/DocumentEditorPanel.tsx b/components/editor/DocumentEditorPanel.tsx index 9081cc16..b4934d8e 100644 --- a/components/editor/DocumentEditorPanel.tsx +++ b/components/editor/DocumentEditorPanel.tsx @@ -211,19 +211,22 @@ const DocumentEditorPanel = ({ // size via a compact --display-margin-scale — no page rectangles, so nothing // shifts while writing. Paged (endless off): render the real fixed-size page — // page breaks, headers, footers, exactly like desktop — and scale the whole - // page down with `zoom` so it fits the viewport width. A fixed page rectangle - // means its boundaries never move as you type, so there are no layout shifts. - // `zoom` is a uniform visual scale and pagination is measured off-screen, so - // page count / numbering are unaffected either way. + // page down with transform: scale() so it fits the viewport width. A fixed + // page rectangle means its boundaries never move as you type, so there are no + // layout shifts. The scale is purely visual (NOT `zoom`: WebKit clamps + // zoom-shrunk fonts to a 9px rendered minimum, inflating the screenplay font + // — see the paged-mode rule in EditorPanel.module.css) and pagination is + // measured off-screen, so page count / numbering are unaffected either way. useEffect(() => { const container = containerEl; if (!container) return; const pageSize = SCREENPLAY_FORMATS[pageFormat as keyof typeof SCREENPLAY_FORMATS]; - // Only the phone paged view is zoomed. Endless reflows (no zoom); desktop + // Only the phone paged view is scaled. Endless reflows (no scale); desktop // shows the page at 1:1. if (!isPhone || isEndlessScroll || !pageSize) { container.style.removeProperty("--editor-zoom"); + container.style.removeProperty("--editor-layout-height"); return; } @@ -239,8 +242,28 @@ const DocumentEditorPanel = ({ apply(); const ro = new ResizeObserver(apply); ro.observe(container); - return () => ro.disconnect(); - }, [containerEl, isPhone, isEndlessScroll, pageFormat]); + + // transform: scale() doesn't shrink the layout box the way `zoom` did, so + // the CSS collapses the leftover (1 − scale) tail of the editor's layout + // height with a negative margin. Track the untransformed height here + // (offsetHeight ignores transforms) and expose it as a CSS var. + const editorDOM = editor?.view?.dom; + let heightObserver: ResizeObserver | undefined; + if (editorDOM) { + const applyHeight = () => { + container.style.setProperty("--editor-layout-height", `${editorDOM.offsetHeight}px`); + }; + applyHeight(); + heightObserver = new ResizeObserver(applyHeight); + heightObserver.observe(editorDOM); + } + + return () => { + ro.disconnect(); + heightObserver?.disconnect(); + container.style.removeProperty("--editor-layout-height"); + }; + }, [containerEl, isPhone, isEndlessScroll, pageFormat, editor]); // ---- Orphaned comment cleanup ---- // Comments anchor to a node's data-id. When that node is deleted the comment diff --git a/components/editor/EditorPanel.module.css b/components/editor/EditorPanel.module.css index fed958b6..9706d7c6 100644 --- a/components/editor/EditorPanel.module.css +++ b/components/editor/EditorPanel.module.css @@ -265,13 +265,41 @@ /* PAGED mode (endless off): render exactly like desktop — real fixed-size * pages, page breaks, headers, footers — and scale the whole page down to fit - * the viewport with `zoom` (--editor-zoom is computed in DocumentEditorPanel). - * Because the page is a fixed rectangle, its boundaries never move while - * writing, so there are no layout shifts — the tradeoff being smaller text - * than endless mode's reflow. The page keeps its canonical width (from the - * injected `.pagination` rule) and stays centred as it scales. */ + * the viewport with transform: scale() (--editor-zoom is computed in + * DocumentEditorPanel). Because the page is a fixed rectangle, its boundaries + * never move while writing, so there are no layout shifts — the tradeoff + * being smaller text than endless mode's reflow. + * + * transform, NOT `zoom`: WebKit multiplies computed font sizes by `zoom` + * (usedZoom) and then applies its "smart minimum font size" + * (minimumLogicalFontSize = 9, StyleFontSizeFunctions.cpp): any absolute + * font-size ≥ 9px whose zoomed result lands below 9px is clamped back up to + * a rendered 9px. On a phone (zoom ≈ 0.48) the 16px screenplay font came out + * as 9px rendered — reported as 9 / zoom ≈ 18.7px computed — cramming + * oversized glyphs into the fixed 16px line boxes and wrapping text + * differently than the offscreen pagination measurement. There is NO CSS + * opt-out (text-size-adjust and text autosizing are different code paths and + * do not participate). transform leaves computed font sizes at their + * canonical values — layout is pixel-identical to the measurement pass — and + * only scales at paint time, so the clamp never triggers. + * + * Unlike zoom, transform doesn't shrink the layout box, so two compensations + * are needed (both !important to beat the injected + * `.pagination { margin: 0 auto !important }`; this three-class selector + * then wins on specificity — same trick as the endless width override): + * - centring: equal negative side margins centre the canonical-width box in + * the wrapper (margin auto would pin an overflowing box to the left); + * transform-origin top center then scales it in place. + * - height: negative margin-bottom collapses the (1 − zoom) tail of the + * unscaled layout height so the scroll extent matches what is visible. + * --editor-layout-height (the editor's untransformed offsetHeight) is fed + * by a ResizeObserver in DocumentEditorPanel. */ .editor_wrapper:not(.endless_scroll) :global(.ProseMirror) { - zoom: var(--editor-zoom, 1); + transform: scale(var(--editor-zoom, 1)); + transform-origin: top center; + margin-left: calc((100% - var(--page-width)) / 2) !important; + margin-right: calc((100% - var(--page-width)) / 2) !important; + margin-bottom: calc((var(--editor-zoom, 1) - 1) * var(--editor-layout-height, 0px)) !important; } .editor_shadow { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9b502f5d..ec5b1bf5 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -7,8 +7,6 @@ name = "Scriptio" version = "0.0.0" dependencies = [ "log", - "objc2", - "objc2-foundation", "serde", "serde_json", "tauri", @@ -2596,7 +2594,6 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.10.0", "block2", - "libc", "objc2", "objc2-core-foundation", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f1dadddc..3b809be7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -28,9 +28,3 @@ tauri-plugin-dialog = "2.6.0" tauri-plugin-fs = "2.4.5" tauri-plugin-clipboard-manager = "2" tauri-plugin-opener = "2.5.3" - -# iOS: disable WKWebView text autosizing (see textAutosizing fix in lib.rs). -# Versions pinned to what Tauri already resolves so no extra tree is pulled. -[target.'cfg(target_os = "ios")'.dependencies] -objc2 = "0.6" -objc2-foundation = { version = "0.3", features = ["NSString", "NSValue"] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index bf5f3e15..487b7490 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3,6 +3,14 @@ fn some_noop_command() { // This does nothing, used for a handshake check } +// NOTE on the iOS "inflated font" bug (paged mode, 16px → ~18.7px): this was +// WebKit's smart minimum font size (minimumLogicalFontSize = 9), which clamps +// `specifiedSize × zoom` up to a rendered 9px whenever the specified size is +// ≥ 9px — reported as 9 / zoom ≈ 18.7px. It is NOT text autosizing, so the +// former native `_setTextAutosizingEnabled:` workaround here was a no-op (and +// private API, an App Store risk). The real fix is CSS-side: the phone paged +// view scales with transform: scale() instead of `zoom`, which never enters +// the clamp's code path. See the paged-mode rule in EditorPanel.module.css. #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() @@ -20,36 +28,6 @@ pub fn run() { )?; } - // iOS WKWebView inflates ("text autosizing") the editor font: the - // screenplay page lays out at a fixed ~818px width and the phone CSS - // scales it down with `zoom`, but WKWebView then boosts the font by - // the column-width/viewport ratio (~2x) so 12pt renders at ~33px, - // overflowing the fixed 16px line box. CSS can't stop it here — - // `-webkit-text-size-adjust: none` is honoured in getComputedStyle - // yet ignored by the autosizer — so turn the feature off natively on - // WKPreferences via the private `textAutosizingEnabled` key (KVC). - #[cfg(target_os = "ios")] - { - use tauri::Manager; - if let Some(webview) = app.webview_windows().values().next().cloned() { - let _ = webview.with_webview(|wv| unsafe { - use objc2::msg_send; - use objc2::runtime::AnyObject; - use objc2_foundation::{NSNumber, NSString}; - - let view: *mut AnyObject = wv.inner().cast(); - if view.is_null() { - return; - } - let configuration: *mut AnyObject = msg_send![view, configuration]; - let preferences: *mut AnyObject = msg_send![configuration, preferences]; - let disabled = NSNumber::new_bool(false); - let key = NSString::from_str("textAutosizingEnabled"); - let _: () = msg_send![preferences, setValue: &*disabled, forKey: &*key]; - }); - } - } - Ok(()) }) .invoke_handler(tauri::generate_handler![some_noop_command]) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7e8abe41..6a1c889a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -13,38 +13,6 @@ const URL = "https://scriptio.app/"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - - {/* Disable WebKit text autosizing. - * - * MUST live here as a raw