Skip to content

Commit 8e37536

Browse files
authored
Windows fixes: caption clearance, GUI subsystem, elision budget, dark titlebars (#122)
* Hand the full snap quantum back to the text wrap/elision budget - Edge snapping rounds each frame edge independently, so an exact-fit text frame at a fractional position can lose up to a FULL device pixel of width — the old 0.5/scale hand-back plus the 0.125 elision slack under-covered it, and the TS scaffold's centered counter digit painted as "…" on Windows at scale 1 - textWrapMaxWidth now returns width + 1/scale with the epsilon policy documented at the seam; regression tests sweep fractional origins for every digit at scales 1/1.25/1.5/2 plus the exact centered-row scaffold shape * Reserve the Windows caption cluster in drag-header layout - A window-drag header that never consumed the chrome channel's trailing inset laid right-aligned content under the DWM min/max/close cluster; the caption punch-out then visibly truncated it (system-monitor-ts's header status) - The runtime now runs a one-retry clearance pass: when the built layout leaves drag-header CONTENT under the platform-reported control cluster, it stamps the cluster into DesignTokens.window_controls and rebuilds once, and widget layout trims the drag row's content box on the cluster's side - apps that already pad (soundboard) never trigger and keep byte-identical layout - Regression: engine tests for both cluster sides and the content-only trigger, plus runtime tests faking Windows-shaped chrome through the null platform for the naive and the contract header shapes * Give Windows release exes the GUI subsystem and pin it at packaging - Every app exe was console-subsystem (zig's default), so dev AND packaged apps flashed a terminal behind the window; the posture is now: Debug keeps the console (dev logs live there), release-shaped builds get /SUBSYSTEM:WINDOWS - redirected logging still works because only console AUTO-allocation is subsystem-gated - native package reads the wrapped exe's PE subsystem (peSubsystem), warns with the rebuild teaching when it wraps a console binary, and reports it in PackageStats.windows_console_subsystem; tests pin the reader and both packaging outcomes over synthetic PE headers - Verified on the Windows box: ReleaseFast ts-smoke.exe reads subsystem=2, launches headed with no console, automation and file-redirected logs intact * Dark-mode titlebars on Windows and a kinder scaffold empty state - Standard-chrome windows set DWMWA_USE_IMMERSIVE_DARK_MODE from the OS app scheme at creation (pre-show, no light-caption flash) and on appearance broadcasts; hidden-titlebar windows keep their pixel-sampled caption fidelity - The TS scaffold's status bar branches on {stampedMs < 0} to say "press Stamp for a timestamp" instead of "stamped: -1ms"; fresh scaffold verified through native check and the ts-core e2e scaffold pins * Emit the GUI-subsystem posture into the scaffold's standalone build.zig - The web-frontend scaffold (Next/Vite/React/Svelte/Vue, native init --full) generates its own build.zig and never set exe.subsystem, so those apps still shipped console-subsystem Windows release exes - and the packaging warn had no remedy, since rebuilding can't fix a scaffold that never sets it - The generated build.zig now carries the same release-only assignment as the SDK build graph (Debug keeps the console for dev logs), with the condensed rationale - The Vite template test pins the emitted condition and assignment lines * Resolve a drag header's anchored children against the cleared rect - The collision scan counts anchored descendants of the drag header, but the remedy only trimmed the flow content box - an anchored floater triggered the one retry and then stayed under the caption cluster (retry paid, nothing moved) - layoutWidgetDepth now passes the anchor base through windowControlsClearedContent for window_drag widgets only, covering both the trailing (Windows) and leading (macOS) clusters; non-drag widgets' anchored children are untouched by construction - Engine test pins the anchored-only collision converging in one pass (scan fires, remedy moves it, re-scan stays quiet), the macOS mirror, and byte-identical non-drag anchoring under stamped tokens * Run the window-control clearance retry in secondary windows too - rebuildWindowSlot laid out exactly once, so a model-declared hidden-inset window's drag header still rendered content under the OS caption cluster; it now runs the same collision scan + one-retry pass as the main rebuild, stamping the cluster into a local copy of the slot's tokens. - windowControlsReservation takes the canvas label so both rebuild paths share it, and the slot's build+layout pass is factored into buildWindowSlotPass (same arena reuse as the main path's buildLayoutPass retry). - ui_app_window_tests: a colliding secondary drag header re-lays clear of the cluster while the main canvas stays unstamped, and a padded secondary header keeps its own layout. * Read only the PE headers for the packaging subsystem check - peIsConsoleSubsystem slurped the whole exe through readPath (capped at 128 MiB) and swallowed every error as false, so a console exe over the cap packaged without the promised warning and packaging allocated the entire binary to read 2 bytes. - peSubsystemAtPath reads the 0x40-byte DOS header, bounds the rest to e_lfanew + 94 bytes through the existing pure peSubsystem parser, and rejects e_lfanew past a 1 MiB ceiling as not-a-real-PE; only genuinely-unreadable files still degrade to no-claim. - Tests: a sparse console exe past the old cap still warns, and a bogus 8 MiB e_lfanew answers nothing under the failing allocator (no full-file or offset-sized allocation). * Persist the subsystem verdict in the report and stats summary - package-manifest.zon now carries .subsystem = "gui"/"console" whenever the posture check ran (a Windows package with a binary), threaded through writeReport like asset_count; other targets make no claim - the stats summary prints "subsystem: gui" or "subsystem: console (a terminal window opens behind the app - rebuild with `native build`)" alongside the web-layer and signing verdicts - PackageStats.windows_console_subsystem is now a ternary (?bool) so an unprobed package cannot pass for a GUI verdict; the synthetic-PE tests pin both report fields and the no-binary null * Stop claiming a gui subsystem the PE parse never established - Replace the bool verdict with WindowsSubsystem { gui, console, unknown }: gui and console only when the optional header said so, unknown for non-PE/truncated bytes, benign read errors, and unmodeled subsystem values; null still means the check never ran - Diagnostic prints "subsystem: unknown (unrecognized executable format)" and the report writes .subsystem = "unknown" instead of affirming gui; console warning behavior and OutOfMemory propagation unchanged - Pin unknown for a non-PE file, a truncated PE, and a native-subsystem exe; retarget the sparse-oversized and offset-ceiling probes onto the verdict helper * Package a release-shaped exe from the web-frontend scaffold - The emitted build.zig defaulted -Doptimize to Debug for everything, so the documented `zig build package` wrapped a Debug, console-subsystem exe; the package step now builds its own exe that defaults to ReleaseFast (mirroring `native build`) while an explicit -Doptimize or --release still pins both roles. - Registers -Doptimize by hand instead of standardOptimizeOption so the graph can tell unset from explicit, the same optimizeMode split build/app.zig uses; the --optimize arg, the artifact name, and the Windows GUI-subsystem posture all key on the package exe's actual mode. - Extends the Vite template pins to the package-exe shape and forbids the stale dev-exe wiring. * Judge drag-header text by its painted bounds in the caption scan - The collision scan intersected each candidate's full frame with the caption cluster, so a grow/stretch centered title spanning the header row false-positived while its glyphs sat clear — and the paid retry visibly shifted the title. - Single-line text leaves now intersect their aligned painted bounds: measured width through the same tokens seam the layout ran with, capped at the frame, placed per text_alignment; controls keep the frame test, and span paragraphs or explicit newlines fall back to it conservatively. - Threads tokens from both windowControlsReservation call sites, pins the centered-title no-retry case at engine and runtime level, and keeps the trailing true-positive, the macOS leading mirror, and post-remedy re-scan convergence green.
1 parent 07b259f commit 8e37536

20 files changed

Lines changed: 1486 additions & 74 deletions

build/app.zig

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,18 @@ pub fn addAppArtifacts(b: *std.Build, dep: *std.Build.Dependency, app_options: A
388388
.name = app_options.name,
389389
.root_module = app_mod,
390390
});
391+
// Windows subsystem posture: release-shaped exes (`native build`,
392+
// and therefore everything `native package --target windows` wraps)
393+
// are GUI-subsystem, so launching the app never flashes a console
394+
// window behind it. Debug exes keep the console subsystem — the dev
395+
// loop's logs live there, and a double-clicked Debug binary opening
396+
// its own log console is a feature. Redirected logging still works
397+
// on GUI exes (handles inherit; only console AUTO-allocation is
398+
// gated by the subsystem), so automation harnesses that pipe
399+
// `app.exe > log 2>&1` keep their logs either way.
400+
if (target.result.os.tag == .windows and app_optimize != .Debug) {
401+
exe.subsystem = .windows;
402+
}
391403
linkPlatform(b, dep, target, app_mod, exe, selected_platform, web_engine, web_layer, cef_dir, cef_auto_install);
392404
const install = b.addInstallArtifact(exe, .{});
393405
b.getInstallStep().dependOn(&install.step);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fix: **Exact-fit text no longer elides under geometry pixel snapping on Windows**: edge snapping rounds a frame's two edges independently, so a hug-sized text box at a fractional position could come back up to a full device pixel narrower than the label it was measured for — past the elision slack, so the TS scaffold's centered counter painted "…" instead of its digit at 100% scale. The wrap/elision budget (`textWrapMaxWidth`) now hands back the full snap quantum (1/scale, was 0.5/scale), and the epsilon policy is documented at the seam: painted width may exceed the snapped frame by less than `1/scale + text_elision_slack`, always below the smallest real overflow (a glyph).
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
improvement: **The TS scaffold's status bar earns its empty state**: a fresh scaffold said "stamped: -1ms" until the first Stamp press; the template's markup now branches on `{stampedMs < 0}` and says "press Stamp for a timestamp" instead — teaching the if/else markup shape in the starter while it's at it.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fix: **Drag headers lay out clear of the Windows caption buttons**: on hidden-titlebar windows, a `window-drag` header whose app never consumed the chrome channel's trailing inset rendered right-aligned content UNDER the DWM min/max/close cluster (system-monitor-ts's status text was truncated by the caption punch-out). The runtime now detects the collision after layout and re-lays the view once with the cluster reserved (`DesignTokens.window_controls`, stamped like `text_measure`), so drag-header content stops at the cluster's edge on every app — markup and builder, Zig and TS — while headers that already pad through the chrome insets (soundboard's trailing spacer) keep a byte-identical layout. The same mechanism covers the macOS mirror (a leading traffic-light cluster). Anchored floating children of the drag header resolve against the cleared rect too, so an end-aligned floater moves out from under the buttons just like flow content; anchored children of non-drag widgets keep byte-identical placement.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fix: **Standard titlebars follow dark mode on Windows**: windows with standard chrome now set `DWMWA_USE_IMMERSIVE_DARK_MODE` from the OS app color scheme — at creation (before first show, so a dark launch never flashes a light caption) and again on every appearance broadcast — so a dark-themed app no longer sits under a glaring white titlebar. Hidden-titlebar windows keep their higher-fidelity pixel-sampled caption color, and chromeless windows have no caption to tint.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fix: **Windows release exes are GUI-subsystem**: `native build` (and therefore everything `native package --target windows` wraps) no longer produces console-subsystem binaries, so launching a packaged app never flashes a terminal window behind it. Debug exes keep the console — the dev loop's logs live there — and redirected logging (`app.exe > log 2>&1`) still works on GUI exes. Packaging now reads the exe's PE subsystem, warns loudly when a console-subsystem binary is wrapped (stale zig-out or hand-supplied `--binary`), and carries the finding in the package stats, pinned by tests over synthetic PE headers. The web-frontend scaffold's standalone `build.zig` (Next/Vite/React/Svelte/Vue and `native init --full`) emits the same release-only assignment, so scaffolded apps get the posture without the SDK build graph.

src/platform/windows/webview2_host.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4480,6 +4480,23 @@ static bool windowsAppsUseDarkTheme() {
44804480
return dark;
44814481
}
44824482

4483+
/* Standard-chrome titlebars follow the OS app color scheme: the
4484+
* immersive-dark attribute flips the DWM caption to its dark palette,
4485+
* so an app rendering the dark theme (apps follow the OS scheme through
4486+
* the appearance channel by default) does not sit under a glaring white
4487+
* titlebar. Hidden-titlebar windows own their caption fidelity already
4488+
* (syncHiddenCaptionColor samples the presented header pixels — a
4489+
* strictly better signal), and chromeless windows have no caption at
4490+
* all, so both stay out of this. Older builds reject the attribute and
4491+
* keep the system default. */
4492+
static void applyStandardTitlebarColorScheme(Window &window) {
4493+
if (!window.hwnd || windowUsesHiddenTitlebar(window) || windowIsChromeless(window)) return;
4494+
const DwmApi &dwm = dwmApi();
4495+
if (!dwm.set_window_attribute) return;
4496+
const BOOL dark = windowsAppsUseDarkTheme() ? TRUE : FALSE;
4497+
dwm.set_window_attribute(window.hwnd, kDwmwaUseImmersiveDarkMode, &dark, sizeof(dark));
4498+
}
4499+
44834500
/* Appearance from OS settings: the apps dark preference, disabled
44844501
* client-area animations as the reduce-motion signal, and the high
44854502
* contrast accessibility flag. Emitted once after START and again
@@ -4500,6 +4517,10 @@ static void emitAppearanceIfChanged(Host *host, bool force) {
45004517
host->appearance_color_scheme = dark;
45014518
host->appearance_reduce_motion = reduce_motion;
45024519
host->appearance_high_contrast = high_contrast;
4520+
/* Standard-chrome captions track the scheme live: flipping the OS
4521+
* theme flips the titlebar with the appearance event the app
4522+
* re-themes from. */
4523+
for (auto &entry : host->windows) applyStandardTitlebarColorScheme(entry.second);
45034524
WindowsEvent event = {};
45044525
event.kind = kAppearance;
45054526
event.color_scheme = dark;
@@ -4967,6 +4988,9 @@ static bool createNativeWindow(Host *host, Window &window) {
49674988
DragAcceptFiles(hwnd, TRUE);
49684989
window.hwnd = hwnd;
49694990
applyMenusToWindow(host, window);
4991+
/* Before the first show, so a dark-scheme launch never flashes a
4992+
* light caption. */
4993+
applyStandardTitlebarColorScheme(window);
49704994
if (windowUsesHiddenTitlebar(window)) {
49714995
applyHiddenTitlebarFrame(window);
49724996
/* The create-time WM_NCCALCSIZE ran before this window was

src/primitives/canvas/layout_audit.zig

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,11 @@ pub const layout_audit_epsilon: f32 = 0.5;
6969
/// call while the line breakers accumulate per-word measures, so an
7070
/// exact-fit line can land one f32 ulp past its own frame (~1e-3 px) —
7171
/// runtimes hide that under the pixel-snap wrap budget (`textWrapMaxWidth`
72-
/// hands back up to 0.5/scale). The audit widens its simulated wrap by an
73-
/// eighth of a point: far above accumulated float noise, below the
74-
/// smallest real overflow (a glyph) and below the smallest runtime snap
75-
/// budget (0.5/scale at any real scale factor), so exact-fit text stays
76-
/// clean and genuine wraps still report.
72+
/// hands back the full snap quantum, 1/scale). The audit widens its
73+
/// simulated wrap by an eighth of a point: far above accumulated float
74+
/// noise, below the smallest real overflow (a glyph) and below the
75+
/// smallest runtime snap budget (1/scale at any real scale factor), so
76+
/// exact-fit text stays clean and genuine wraps still report.
7777
pub const layout_audit_wrap_slack: f32 = 0.125;
7878

7979
/// Findings reported per audit pass. A view with more than this many

src/primitives/canvas/root.zig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,11 @@ pub const disclosureSettledOpen = @import("widget_tree.zig").disclosureSettledOp
695695
pub const disclosureContentBottom = @import("widget_tree.zig").disclosureContentBottom;
696696
pub const isWidgetConcealedByDisclosure = @import("widget_tree.zig").isWidgetConcealedByDisclosure;
697697
pub const anchoredWidgetFrame = @import("widget_layout.zig").anchoredWidgetFrame;
698+
/// Window-control reservation trigger (widget_layout.zig): true when a
699+
/// laid-out tree left drag-header CONTENT under the OS window-control
700+
/// cluster, so runtimes know to stamp `DesignTokens.window_controls`
701+
/// and re-lay the view clear of it.
702+
pub const windowDragContentUnderWindowControls = @import("widget_layout.zig").windowDragContentUnderWindowControls;
698703
/// Split-pane geometry (widget_layout.zig): divider band width, the
699704
/// fraction clamp band from the panes' min widths, and the in-place
700705
/// subtree re-layout the runtime reconcile uses when it restores a

src/primitives/canvas/tokens.zig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const std = @import("std");
2+
const geometry = @import("geometry");
23
const canvas = @import("root.zig");
34
const text_metrics = @import("text_metrics.zig");
45
const geist_theme = @import("themes/geist.zig");
@@ -1521,6 +1522,20 @@ pub const DesignTokens = struct {
15211522
/// actually draws. Not themed and not part of overrides: the runtime
15221523
/// stamps it after theme resolution.
15231524
text_measure: ?*const text_metrics.TextMeasureProvider = null,
1525+
/// The OS window-control cluster's frame in the canvas's local
1526+
/// coordinates (Windows: the DWM min/max/close buttons on the
1527+
/// trailing edge; macOS: the traffic lights on the leading edge),
1528+
/// for hidden-titlebar windows whose content extends under the
1529+
/// band. Null (the default) lays out exactly as before. The runtime
1530+
/// stamps it — like `text_measure`, never themed and not part of
1531+
/// overrides — and only after a build proved the app's own drag
1532+
/// header left content UNDER the cluster (an app that already pads
1533+
/// via the chrome channel's insets keeps its byte-identical
1534+
/// layout). `window_drag` widgets then lay their content out clear
1535+
/// of the cluster (`widget_layout.windowControlsClearedContent`),
1536+
/// so a header that never consumed the chrome insets still keeps
1537+
/// its trailing status text out from under the caption buttons.
1538+
window_controls: ?geometry.RectF = null,
15241539

15251540
pub fn theme(options: ThemeOptions) DesignTokens {
15261541
// The pack resolves the register (palette, control tables, and

0 commit comments

Comments
 (0)