From eede44feb4c5c4b59594862fc567b51fc7b73bd4 Mon Sep 17 00:00:00 2001 From: Nikhil Krishna Date: Fri, 3 Jul 2026 11:37:45 +0200 Subject: [PATCH 1/5] Two fixes for visible artefacts when resizing the window on Windows (WebView2), found while chasing content that visibly trails the resize: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Set background_color on every window builder to match the theme's --bg-main (light #fafaf9 / dark #1d1b16, System→light). WebView2 was painting its default white on window-open and at newly-exposed edges during resize; this paints a solid themed background on both the window and webview layers immediately. Regression test added. 2. content-visibility: auto on div.line so off-screen lines are skipped during layout. On a 10k-line file a resize re-wrapped every line every frame; forced-layout median dropped from 129ms to 27ms (4.7x), p95 178ms→31ms, max 303ms→33ms (measured via CDP in-document A/B). Content and the bottom status bar no longer trail the resize as severely. --- src-tauri/src/config.rs | 34 +++++++++++++++++++++++++++ src-tauri/src/excalidraw_window.rs | 3 +++ src-tauri/src/lib.rs | 1 + src-tauri/src/mcp/mod.rs | 3 +++ src-tauri/src/mermaid_window.rs | 3 +++ src/styles/components/code-viewer.css | 7 ++++++ 6 files changed, 51 insertions(+) diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 9a418dc..4225908 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -22,6 +22,21 @@ pub enum Theme { Dark, } +/// Native window/webview background color matching the frontend's `--bg-main`. +/// Set on every window builder so WebView2 (Windows) paints a solid background +/// before content renders — otherwise its default white shows as a flash on +/// window-open and as a blank strip at the growing edges during live resize. +/// +/// System resolves to light: dark mode is an opt-in `[data-theme="dark"]` +/// override, so light is today's effective default. Light `--bg-main` is +/// `#fafaf9`; dark is `#1d1b16`. +pub fn window_background_color(theme: Theme) -> tauri::webview::Color { + match theme { + Theme::Dark => tauri::webview::Color(0x1d, 0x1b, 0x16, 0xff), + Theme::Light | Theme::System => tauri::webview::Color(0xfa, 0xfa, 0xf9, 0xff), + } +} + /// Application configuration stored in config.json. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { @@ -607,6 +622,25 @@ mod tests { assert_eq!(config.theme, Theme::System); } + #[test] + fn window_background_matches_css_bg_main() { + // Must match `--bg-main` in src/styles/tokens.css, or WebView2 flashes + // its default background on open/resize. System resolves to light + // because dark is an opt-in [data-theme="dark"] override. + assert_eq!( + window_background_color(Theme::Light), + tauri::webview::Color(0xfa, 0xfa, 0xf9, 0xff) + ); + assert_eq!( + window_background_color(Theme::System), + window_background_color(Theme::Light) + ); + assert_eq!( + window_background_color(Theme::Dark), + tauri::webview::Color(0x1d, 0x1b, 0x16, 0xff) + ); + } + #[test] fn config_deserializes_without_theme() { // Old configs without theme field should default to System diff --git a/src-tauri/src/excalidraw_window.rs b/src-tauri/src/excalidraw_window.rs index 38d148a..9cda218 100644 --- a/src-tauri/src/excalidraw_window.rs +++ b/src-tauri/src/excalidraw_window.rs @@ -155,6 +155,9 @@ pub async fn open_excalidraw_window( .title("Excalidraw") .inner_size(width, height) .min_inner_size(600.0, 400.0) + .background_color(crate::config::window_background_color( + crate::config::load_config().theme, + )) .visible(false); #[cfg(target_os = "macos")] let b = b diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 81768fe..f3a0b75 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -130,6 +130,7 @@ pub fn run(state: AppState, context: tauri::Context, json_output: bool) { ) .title("annot") .inner_size(1000.0, 700.0) + .background_color(config::window_background_color(config::load_config().theme)) .visible(false); // Will be shown after content loads #[cfg(target_os = "macos")] let b = b diff --git a/src-tauri/src/mcp/mod.rs b/src-tauri/src/mcp/mod.rs index ba84029..7673bcb 100644 --- a/src-tauri/src/mcp/mod.rs +++ b/src-tauri/src/mcp/mod.rs @@ -313,6 +313,9 @@ fn run_session_with_state( ) .title("annot") .inner_size(1000.0, 700.0) + .background_color(crate::config::window_background_color( + crate::config::load_config().theme, + )) .visible(false); // Will be shown after content loads #[cfg(target_os = "macos")] let b = b diff --git a/src-tauri/src/mermaid_window.rs b/src-tauri/src/mermaid_window.rs index 6664f39..d69aaf8 100644 --- a/src-tauri/src/mermaid_window.rs +++ b/src-tauri/src/mermaid_window.rs @@ -104,6 +104,9 @@ pub async fn open_mermaid_window( .title(format!("{}:{}-{}", filename, start_line, end_line)) .inner_size(600.0, 500.0) .min_inner_size(300.0, 200.0) + .background_color(crate::config::window_background_color( + crate::config::load_config().theme, + )) .visible(false); #[cfg(target_os = "macos")] let b = b diff --git a/src/styles/components/code-viewer.css b/src/styles/components/code-viewer.css index 446e495..acf7ca4 100644 --- a/src/styles/components/code-viewer.css +++ b/src/styles/components/code-viewer.css @@ -127,6 +127,13 @@ div.line { display: flex; white-space: pre-wrap; tab-size: 4; + /* Skip layout/paint for off-screen lines. On large files (10k+ lines) a + window resize otherwise re-wraps every line every frame, so content and + the bottom status bar visibly trail the resize. content-visibility lets + the engine skip off-screen rows; contain-intrinsic-size keeps their + reserved height stable so the scrollbar doesn't jump. */ + content-visibility: auto; + contain-intrinsic-size: auto 1.5em; } /* Hover highlight — CSS-driven so mouse-move never re-renders 10k lines. From 98a276aee0e47f2f5a44bb0aee16fb7782938d27 Mon Sep 17 00:00:00 2001 From: Nikhil Krishna Date: Fri, 3 Jul 2026 12:36:07 +0200 Subject: [PATCH 2/5] perf(ui): freeze content while growing to kill large-file resize trail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a 10k-line file (~112k DOM nodes) the per-frame style+layout walk can't keep up with a window resize, so content visibly trailed the dragged edge. Measured the cost breakdown via CDP: style-recalc, not layout, dominates, and it's proportional to node count — no pure-CSS reflow tuning closes the gap. Fix: while the window is GROWING, add a `.resizing` class that sets content-visibility: hidden on the content tree (~7x cheaper per frame, measured), freezing the last-painted frame; the outer scroller keeps a themed background so the newly-exposed area shows clean empty space, and content snaps back ~120ms after the drag settles. Grow-only on purpose: shrinking has no trailing edge, and freezing a fast shrink outran the browser's cached paint and flashed blank. Also drop the status bar's backdrop-filter: its background is opaque, so the blur had zero visual effect but cost a full-width blur pass every resize frame. And fix div.line contain-intrinsic-size to the real 22px line-height so off-screen height estimates are accurate. Small/medium files were already smooth and stay so. --- src/routes/+page.svelte | 35 ++++++++++++++++++++++++++- src/styles/components/code-viewer.css | 33 +++++++++++++++++++++---- src/styles/components/status-bar.css | 6 +++-- 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index a0efd10..caca6b4 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -2,7 +2,7 @@ import { invoke } from "@tauri-apps/api/core"; import { listen, emit } from "@tauri-apps/api/event"; import { getCurrentWindow } from "@tauri-apps/api/window"; - import { onMount } from "svelte"; + import { onMount, onDestroy } from "svelte"; import type { ContentResponse, ContentNode, ContentMetadata, Line, JSONContent, ExitMode, Tag, DiffMetadata, HunkInfo, MarkdownMetadata, SectionInfo, ConfigSnapshot } from "$lib/types"; import { getLineNumber, getDiffKind, isSelectable, isPortalLine, isCodeBlockLine, isCodeBlockFence, isTableLine, isHorizontalRule } from "$lib/line-utils"; import { rangeToKey, keyToRange, isLineInRange, validateRange, type Range } from "$lib/range"; @@ -81,6 +81,31 @@ let contentEl: HTMLDivElement | null = $state(null); let scrollRafId: number | null = null; + // While the window is GROWING, suspend rendering of the content tree + // (content-visibility: hidden via the `.resizing` class). On a 10k-line file + // the reflow can't fill the newly-exposed area fast enough, so content + // visibly trails the growing edge; freezing the last-painted frame until the + // drag settles hides that entirely. + // + // Only on grow, not shrink: shrinking has no trailing edge (existing content + // already covers the smaller window), and freezing a fast shrink outran the + // browser's cached paint and flashed blank. Live content on shrink is fine. + // + // No per-frame resize signal exists, so we debounce: set on a resize that + // grew either dimension, clear ~120ms after the last resize event. + let resizing = $state(false); + let resizeTimer: number | null = null; + let lastW = globalThis.innerWidth; + let lastH = globalThis.innerHeight; + function handleWindowResize() { + const grew = globalThis.innerWidth > lastW || globalThis.innerHeight > lastH; + lastW = globalThis.innerWidth; + lastH = globalThis.innerHeight; + if (grew && !resizing) resizing = true; + if (resizeTimer) clearTimeout(resizeTimer); + resizeTimer = window.setTimeout(() => { resizing = false; resizeTimer = null; }, 120); + } + // Current file/hunk derived from indices (diff mode) let currentFile = $derived.by(() => { if (!diffMetadata || diffMetadata.files.length === 0) return null; @@ -603,6 +628,8 @@ onMount(async () => { const window = getCurrentWindow(); + globalThis.addEventListener('resize', handleWindowResize); + // Apply theme before any content renders (prevents flash) await initTheme(); @@ -698,6 +725,11 @@ } }); }); + + onDestroy(() => { + globalThis.removeEventListener('resize', handleWindowResize); + if (resizeTimer) clearTimeout(resizeTimer); + }); @@ -754,6 +786,7 @@