Native theme fidelity suite + Material 3 fidelity fixes#5274
Conversation
Adds a data-driven fidelity test suite (scripts/fidelity-app) that renders each component under the native theme alongside the REAL native OS widget (off-screen rasterized) and measures per-component visual fidelity, gated by a one-way ratchet vs a committed baseline. Android round raises overall Material 3 fidelity 94.9% -> 96.2% via real framework fixes (verified pixel vs the native golden, no metric softening): - FloatingActionButton: honor a fabDiameterMM theme constant for the Material 56dp fixed diameter instead of the icon*11/4 (~71dp) heuristic. FAB 85->98. - Tabs.paintAnimatedIndicator: read tabsAnimatedIndicatorThicknessMm as a float (an int read dropped "0.45" -> 2x-too-thick indicator). - Tabs.paintBottomDivider: new opt-in (tabsBottomDividerBool) full-width M3 divider painted directly (a border-bottom does not paint on the custom tab-row Container); colour from the TabsDivider UIID (light/dark aware). - DefaultLookAndFeel: disabled-unchecked checkbox/radio box reads the *UncheckedColorUIID's own .disabled style, so the greyed box outline can differ from the darker disabled label text (Material renders them distinctly). Theme (native-themes/android-material/theme.css) + recompiled shipped res. Host tooling: ProcessScreenshots --mode fidelity, RenderFidelityReport, FidelityGate (ratchet), cn1ss.sh helpers, run-*-fidelity-tests.sh, and the scripts-fidelity GitHub workflow. iOS round is blocked: rendering the native UIKit reference inside a ParparVM native method NPEs whenever it does real UIKit work (a trivial stub delivers; not a threading or marshaling fault). Documented in the iOS NativeWidgetFactory impl; needs a ParparVM fix or a PeerComponent+screenshot redesign. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Compared 12 screenshots: 12 matched. |
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cloudflare Preview
|
Native fidelity (Android, Material 3)54 pairs compared -- median 95.6%, worst 91.3% ( Distribution --
Geometry vs native (bbox offset / size ratio / center offset / corner radius) -- gated separately from the visual score
Side-by-side comparisons (worst first)
|
|
Compared 144 screenshots: 144 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
- Switch.java: replace a non-ASCII U+2248 with ~ (Android port javac uses US-ASCII encoding and failed on it). - scripts/javase/screenshots: refresh the 7 simulator goldens that shifted with the framework/theme changes (rendered on CI Linux to match the test env). - scripts-fidelity.yml: TEMPORARY seed -- run the Android fidelity suite with FIDELITY_UPDATE_GOLDENS=1 + FIDELITY_UPDATE_BASELINE=1 so the native goldens and baseline are regenerated on CI's emulator density (the committed ones were rendered on a different local emulator, so 50/54 pairs "could not be compared"). Reverted in a follow-up once the CI-density artifacts are committed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Compared 216 screenshots: 216 matched. |
The native goldens + ratchet baseline are now the ones the seed run regenerated on CI's own emulator (e.g. Tabs 377x100 vs the local 1039x277), so the fidelity gate compares like-for-like instead of failing 50/54 pairs on size mismatch. Removes the temporary FIDELITY_UPDATE_* seed so the job is a real one-way ratchet again. CI baseline overall fidelity: 96.2%. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Compared 139 screenshots: 139 matched. |
|
Compared 137 screenshots: 137 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
|
Compared 167 screenshots: 167 matched. |
|
Compared 142 screenshots: 142 matched. Benchmark Results
Detailed Performance Metrics
|
iOS fidelity native references now render (48 delivered, was 0). The earlier "ParparVM can't render UIKit in a native method" conclusion was wrong: it was three mundane MRC (non-ARC) memory bugs in NativeWidgetFactoryImpl.m -- 1. knownKind: cached an AUTORELEASED +[NSSet setWithObjects:] in a static, which dangled once the autorelease pool drained between native calls; the 2nd call derefed freed memory. ParparVM turns that EXC_BAD_ACCESS into a bogus Java NPE (which read as "buildAndRender NPEs"). Fixed: -[alloc initWithObjects:] (+1). 2. The rendered NSData was autoreleased and built on the main queue (UIKit layout -- e.g. SF-Symbol buttons -- hangs off-main, so the build is dispatch_sync'd to main); when dispatch_sync returned, main's pool drained and freed it before the EDT's writeToFile. Fixed: -retain it across the boundary, -release after. 3. (UIKit build moved to the main thread to avoid the off-main layout hang.) Report (RenderFidelityReport): lead with median / worst-pair / 25th-percentile / distribution buckets instead of a single misleading mean; add a per-pair percentage table (Fidelity, SSIM, mean-delta, delta-vs-baseline) sorted worst first; list unscored pairs explicitly; render the side-by-side cards for every pair worst-first. Workflow: drop continue-on-error on the iOS job (no longer a blocker); reseed per-environment goldens (FIDELITY_UPDATE_GOLDENS) while the committed baseline remains the portable ratchet floor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… app The off-screen UIKit factory render was bunk: it rasterized DETACHED widgets at scale=1.0, so a 30pt button was 30px inside a 1087px tile (tiny, wrong size), and UINavigationBar/UITabBar rendered blank without a window. Replaced it for iOS with the approach Shai asked for: - scripts/fidelity-app/ios-native-ref/NativeRef.swift: a standalone native iOS app that lays each reference UIKit widget out in a REAL UIWindow and captures it with drawHierarchy(afterScreenUpdates:) -- so nav/tab bars render correctly -- at CN1's pixel density (so the PNG overlays the CN1 render 1:1, no scaling). Built directly with swiftc (no Xcode project) by scripts/build-ios-native-ref.sh, which runs it on the simulator and copies the PNGs into the committed iOS goldens. - run-ios-fidelity-tests.sh: iOS now compares the CN1 render against these COMMITTED goldens (generated offline, not same-run) instead of the broken factory native. - ProcessScreenshots: tolerate a few px of cross-environment rounding (golden 1088 vs CN1 1087) by cropping both to their common top-left region before diffing -- a true 1:1 overlay, never a scale. Result: all 50 iOS pairs now compare against real, correctly-sized native widgets (Toolbar was 0% blank -> a real centred-vs-left-aligned title diff). Seeded the iOS ratchet baseline (mean 62.3%); the low scores are the genuine untuned-iOSModern-theme gaps to drive up next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Compared 143 screenshots: 143 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
The native and CN1 tiles both anchor the widget top-left, but their pixel sizes can diverge -- a few px of cross-environment rounding (iOS offline goldens), or a larger native-vs-CN1 tile-geometry gap that flakes between Android emulator runs (e.g. CN1 320 vs native 377). Failing those as "size_mismatch" broke the gate. Now both are cropped to their common top-left region and overlaid 1:1 (never a scale); the structural metric still crops to each widget's content bbox, so an honest extent difference scores lower rather than erroring. Only a degenerate overlap (<8px) is an error. TEMPORARY: FIDELITY_UPDATE_BASELINE=1 on both run steps to reseed the ratchet baselines on CI under the new comparison (reverted once the baselines are committed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The old score was the mean colour agreement over all widget-content pixels, so a
large flat region that happened to match -- e.g. a dark nav-bar fill against a
dark tile -- could carry the score into the high 80s even when the actual widget
(the title) was centred in one render and left-aligned at a totally different
font size in the other. "Mostly got points for being black."
Now fidelity = min(fillSim, structSim):
- fillSim = mean colour agreement over content pixels (the old term; catches
wrong fill colours).
- structSim = the same agreement WEIGHTED BY local-gradient salience SQUARED, so
flat fills count for ~nothing and the strongest edges -- glyph
strokes, crisp outlines, separators -- dominate. A mis-placed or
mis-sized title lands its strokes on the other render's flat fill,
collapsing this term.
A widget must now agree in BOTH fill AND structure/placement. Effect on the iOS
Toolbar that triggered this: 89.3% -> ~59% (dark) / 36% (light), matching the
independent SSIM (~56%), while genuinely-similar widgets (an off switch, disabled
buttons) stay in the mid-80s. This is stricter for Android too; the CI seed run
reseeds both ratchet baselines under it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per Shai's note that the native toolbar/widgets weren't using the modern look, the native-reference app now uses the iOS 26 Liquid Glass options: - buttons: UIButton.Configuration.glass() (tinted action), prominentGlass() (filled/CTA -> a real glass capsule), clearGlass() (borderless text button). - UINavigationBar / UITabBar: standard + scrollEdge appearances configured with configureWithDefaultBackground() = the glass material, not the legacy opaque fill. Regenerated the committed iOS goldens. (The glass translucency reads subtly over the flat reference tile -- its blur only develops over scene content, which we do not put behind the widget so the diff stays widget-vs-widget -- but the modern configurations/appearances are now what the reference uses.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Liquid Glass only reveals itself over content behind it, so the glass widgets (buttons, nav/tab bars) are now rendered over a single committed backdrop -- glass-backdrop.png, a simple smooth diagonal gradient. The SAME PNG is used by both sides (the native NativeRef app bundles it; the CN1 FidelityDeviceRunner loads it as the tile background for the glass component ids on iOS), so the only difference left between the two renders is the glass itself, not the background. A smooth gradient (no hard edges) is deliberate: it makes the frosted glass clearly visible while adding almost no gradient "structure", so the salience-weighted metric keeps scoring the widget difference rather than being inflated by a matching backdrop. Non-glass widgets and all of Android stay on the plain tile. Regenerated the iOS goldens; the CI iOS run reseeds the baseline against them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…; Material 1.13.0 - Regenerate iOS native references on iOS 26 (real Liquid Glass), force 8-bit PNGs - Slider.paintNativeSlider: iOS continuous-track + soft drop-shadow capsule thumb - Toolbar circular glass commands, Tabs glass pill, dark-mode glass translucency, disabled fixes - Honest geometric-mean fidelity metric (fillSim x ssim) - Bump Android Material 1.12.0 -> 1.13.0 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lider/tabs tuning iOS: bigger toolbar glass circles + white dark glyphs; Button/RaisedButton cn1-pill; checkbox unchecked plain circle; tabs centered + smaller icons + subtler dark selection; switch thumb fills track (no ring); slider taller + narrower thumb + disabled translucency; progressbar 2x height. Android: Material 1.13.0; switch off-thumb x inset; disabled-dark button translucency; native pressed-state hotspot/state fix. Reseed iOS baseline (iOS 26). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1.13 needs AGP 8.1.1+); refresh JS+JavaSE theme goldens - scripts-fidelity.yml iOS build: ARCHS=arm64 (x86_64 sim slice fails ParparVM SIMD neon module) - Material 1.13.0 pulls dynamicanimation:1.1.0 requiring AGP 8.1.1; current build pins 8.1.0 -> revert to 1.12.0 (latest M3 the pipeline supports) - Refresh 32 JS theme screenshot goldens + JavaSE ios-modern render for the theme changes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Native fidelity (iOS Modern, Metal)68 pairs compared -- median 94.4%, worst 83.5% ( Distribution --
Geometry vs native (bbox offset / size ratio / center offset / corner radius) -- gated separately from the visual score
Side-by-side comparisons (worst first)
|
…line) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pties; drop redundant FQN The quality gate scans whole files the PR touches, surfacing the fidelity work's intentional catch-and-default blocks. Enable EmptyCatchBlock allowCommentedBlocks (its intended escape hatch), comment the bare catches, and shorten an unnecessary com.codename1.ui.Font FQN in UIManager. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
… changes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The master-merge resolution wrapped the watch capture in a block and dispatched it onto the main queue (master's change) WHILE still holding the drain lock (my change). On the watch the render pump paints on the main queue, so dispatching the capture to main and holding the drain lock starves the pump: the bitmap is never drawn, currentFrame returns nil, and every CN1SS shot comes back as a 1x1 placeholder (whole watch suite "differs"). Restore the proven inline capture on the calling thread under the drain lock (green at 1bb601a); keep master's failure NSLog. retain/release stay balanced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… lock) My previous resolution kept the old external drain lock. But the master merge brought a coordinated watch-pipeline change (drawFrame / main-thread PNG decode in CodenameOne_GLViewController.m): watch render and PNG encode are main-thread-affine, and master's watch drawFrame drains on the main thread itself. Wrapping master's main-queue capture in the old drain lock starves the render pump, so every frame comes back nil (1x1 placeholders for the whole watch suite). Use master's capture verbatim -- dispatch onto the main queue, no external lock -- which is the version coordinated with the merged watch drawFrame. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause of the whole-watch-suite 1x1 placeholders after the master merge: master's merge added drawFrame:allowInactive: where the default drawFrame: path uses allowInactive:NO and SKIPS the draw when the app is not UIApplicationStateActive. During a headless CN1SS watch test the app is inactive, so the screenshot capture's drawFrame:CGRectZero skipped the paint, leaving an empty bitmap that the harness delivers as a 1x1 placeholder for every screenshot (no native error -- the draw was simply never issued). The non-watch readback path already forces allowInactive:YES; do the same for the watch capture. Declare the allowInactive: variant in both GLViewController interface blocks so IOSNative.m can call it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hardened tv tolerance (flat tiles 24/0.30) flagged CheckBoxRadio (light+dark), MultiButton_light and Showcase (light+dark) -- stale goldens whose sub-threshold drift the old 48/1.0 gate masked. Reviewed each CI render: MultiButton_light now shows correctly rounded cells, CheckBoxRadio renders proper ring/filled/disabled states, Showcase is a clean composite. Accept the current (correct) renders as the goldens. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The whole watch CN1SS suite rendered 1x1 blanks after the master merge. Root cause: my task#76 fix restructured the watchOS CG flush in CN1WatchViewController.m (removed the @synchronized(self) flush, added CN1WatchDrainLockObject) to solve the OLD concurrent-drain 1x1. Master independently reworked the watch pipeline (drawFrame:allowInactive: + main-thread PNG decode) and fixes the same race by serialising capture on the main thread -- and its drawFrame expects the original flush structure. task#76's restructured flush + master's drawFrame = nothing flushes, so every frame is blank. Master's watch suite is green with the base CN1WatchViewController, so revert task#76's two watch-only files (CN1WatchViewController.m, CN1WatchHost.h) to master's version and adopt master's coherent pipeline. CN1WatchDrainLockObject was referenced only by task#76's own code (IOSNative.m no longer uses it), so the revert is self-contained; the glass work in METALView.m is unrelated (watch renders via CoreGraphics, not Metal). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
THE root cause of the whole-watch-suite 1x1 blanks. For the iOS glass- fidelity capture I widened forceScreenRenderForCapture from desktop-only (if(!isDesktop())return) to run on every platform, so it now drives a Metal screenTexture readback (flushBufferForReadback) before the native screenshot. watchOS renders through the CoreGraphics backend, not Metal: forcing that paint+flush consumes/empties the CG frame the native watch screenshot then reads, so every capture comes back empty and the harness delivers a 1x1 placeholder for all 185 watch screens (no native error -- the frame is simply blank). This is why none of the capture-code fixes moved it: the damage happens in forceScreenRenderForCapture BEFORE the native capture runs. Gate on metalRendering (false on the watch CG backend, true on desktop/iOS/tvOS Metal), so watch stays on its normal production screenshot path while the iOS/desktop glass capture keeps its forced readback. Master's screenshot is green with this method desktop-gated; the metalRendering gate makes watch behave the same. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up to the forceScreenRenderForCapture Metal gate. With that gate correctly skipping the Metal readback on watch, the native capture ran on its own and HUNG -- 0 of 216 screenshots streamed, 1200s timeout -- because my earlier drawFrame:CGRectZero allowInactive:YES forces a blocking draw while the headless watch app is inactive, waiting on a frame that never schedules. Master's green watch suite uses the default allowInactive:NO, which returns the already-painted frame when inactive instead of blocking. Revert to master's call so the whole watch capture chain (forceScreenRenderForCapture gated off + IOSNative capture + CN1WatchViewController) matches master's proven-green pipeline exactly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The watch suite renders again (216/216 real frames) now that the capture chain matches master. Three goldens were stale: * ChatInput_dark -- the dark ChatAttach/ChatVoice pill fix renders the +/Mic as circles (golden had the square-border regression). * ValidatorLightweightPicker and graphics-partial-flush-clip-escape -- both were last captured under the reverted task#76 drain-lock flush (commit 305ca23). Master's pipeline renders the picker toolbar border correctly and no longer leaks the magenta strip outside the clip at the bottom edge (the old golden preserved that escape), so the new renders are the more correct ones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…escapes) Same staleness as the watch variant: the committed golden (my branch's 399e43b refresh) preserved the magenta strip leaking outside the clip at the bottom edge. Under the merged master clip-clamp fix (#5273) the GL render no longer leaks -- the correct behavior, consistent with the metal/tv/watch renders. Deterministic across two runs, not a flake. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ty-suite # Conflicts: # scripts/android/screenshots/ButtonTheme_dark.png # scripts/android/screenshots/ButtonTheme_light.png # scripts/android/screenshots/SwitchTheme_dark.png # scripts/android/screenshots/SwitchTheme_light.png # scripts/ios/screenshots-watch/ButtonTheme_dark.png # scripts/ios/screenshots-watch/ButtonTheme_light.png # scripts/javascript/screenshots/ButtonTheme_dark.png # scripts/javascript/screenshots/ButtonTheme_light.png # scripts/javascript/screenshots/SwitchTheme_dark.png # scripts/javascript/screenshots/SwitchTheme_light.png # scripts/linux/screenshots-arm/LightweightPickerButtons.png # scripts/linux/screenshots-arm/LightweightPickerButtons_above_center.png # scripts/linux/screenshots-arm/LightweightPickerButtons_below_right.png # scripts/linux/screenshots-arm/LightweightPickerButtons_between_mixed.png # scripts/linux/screenshots-arm/ValidatorLightweightPicker.png # scripts/linux/screenshots/LightweightPickerButtons.png # scripts/linux/screenshots/LightweightPickerButtons_above_center.png # scripts/linux/screenshots/LightweightPickerButtons_below_right.png # scripts/linux/screenshots/LightweightPickerButtons_between_mixed.png # scripts/linux/screenshots/ValidatorLightweightPicker.png # scripts/windows/screenshots/LightweightPickerButtons.png # scripts/windows/screenshots/LightweightPickerButtons_above_center.png # scripts/windows/screenshots/LightweightPickerButtons_below_right.png # scripts/windows/screenshots/LightweightPickerButtons_between_mixed.png # scripts/windows/screenshots/ValidatorLightweightPicker.png # scripts/windows/screenshots/graphics-partial-flush-clip-escape.png
The VM merge (#5327 + master's LightweightPicker test change) left the conflicted goldens resolved to master's copies, but the merged tree renders this branch's theme under the new VM -- so the gates correctly flagged every affected screen. Reviewed representative renders from each family (JS ButtonTheme_ios_dark glass buttons, Android SwitchTheme_dark Material switches, Windows clip-escape edge margins) and accepted the CI renders: * javascript: ButtonTheme/SwitchTheme x default+ios x light/dark (8) * android: ButtonTheme/SwitchTheme light/dark (4) * watch: ButtonTheme light/dark (2) * linux + linux-arm: LightweightPickerButtons x4, Validator, SwitchTheme light/dark, kotlin (8 each) * windows (shared x64/arm64/cross): LightweightPickerButtons x4, Validator, graphics-partial-flush-clip-escape (6) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dark ChatInput +/Mic pill fix never reached the metal, GL and mac goldens: the comparator's stock gate (channelDelta=4, mismatch=0.30% = ~9k px on a 1179x2556 capture) swallowed the ~2-3k px square-vs-round corner change, so those three pipelines kept passing against stale square goldens while JS/watch/tv (smaller captures or explicit tolerances) were properly refreshed. Add CN1SS_MAX_CHANNEL_DELTA / CN1SS_MAX_MISMATCH_PERCENT pass-through in cn1ss_process_and_report and default the iOS sim (GL + Metal) and Mac Catalyst runners to 0.05% (~1.5k px) -- tight enough to catch widget-level drift, still above sub-pixel AA jitter. Per-test .tolerance files continue to override for legitimately noisy screens (GPU 3D, maps, video, toast timing). The tightened gate will flag every accumulated stale golden on the next run (ChatInput_dark among them); those get refreshed from the CI renders rather than hand-picked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 0.05% gate flagged every golden that had silently drifted under the old 0.30% slop: the theme screens whose fixes never propagated to these pipelines (ChatInput_dark circles, ChatView, Dialog, List, SpanLabel, Toolbar -- light+dark) plus graphics/chart primitives that shifted slightly under the ParparVM update (transform-rotation, fill-shape family, chart-transform, SVGStatic, kotlin) and the animation screenshot tests. Reviewed representative renders (metal + mac ChatInput_dark now show the round +/Mic buttons, GL DialogTheme_dark renders the dark glass dialog correctly) and accepted the CI renders: 13 GL, 18 Metal, 9 Mac. Any screen that turns out to be genuinely nondeterministic at 0.05% will re-flag with different pixels and gets a per-test .tolerance file then. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Second run at the 0.05% gate isolated the genuinely nondeterministic screens: metal SVGStatic re-flagged with fresh pixels immediately after its golden was refreshed (the nondeterminism signature), and GL SVGStatic/chart-rotated-pie straddle the threshold (passed the first tightened run, flagged the second). These vector-heavy renders carry run-to-run AA jitter in the 0.05-0.30% band, so pin them to the pre-tightening gate (4/0.30) via per-test .tolerance files -- exactly the mechanism the tightened default was designed to compose with -- and refresh their goldens to the current renders. Everything else holds at 0.05%. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… test The dark MultiButton override redeclared `border:` (to recolor the hairline) without repeating border-radius, so the compiler emitted a plain rectangular Border for ALL dark states while light kept its rounded shapes -- the dark cell rendered square, light rounded. Same bug class as the dark ChatInput attach/voice circles. Repeat the shape in the dark rules (border-radius on the cell, round type on pressed to match light's cn1-round-border) and regenerate the .res mirrors. New NativeThemeLightDarkConsistencyTest loads both shipped native theme .res files and asserts that whenever light AND dark declare a border for the same UIID+state, the two are the same shape class -- recoloring is appearance-specific, geometry is not. Verified it FAILS on the pre-fix res (flagged all 4 MultiButton states: light= RoundRectBorder/RoundBorder vs dark=Border) and passes after. Runs in the core-unittests gate so this class of light/dark drift can no longer ship silently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ty-suite # Conflicts: # scripts/ios/screenshots-metal/ButtonTheme_dark.png # scripts/ios/screenshots-metal/ButtonTheme_light.png # scripts/ios/screenshots-metal/ChatView_dark.png # scripts/ios/screenshots-metal/ChatView_light.png # scripts/ios/screenshots-metal/DialogTheme_dark.png # scripts/ios/screenshots-metal/DialogTheme_light.png # scripts/ios/screenshots-metal/ShowcaseTheme_dark.png # scripts/ios/screenshots-metal/ShowcaseTheme_light.png # scripts/ios/screenshots-metal/SpanLabelTheme_dark.png # scripts/ios/screenshots-metal/SpanLabelTheme_light.png # scripts/ios/screenshots-metal/SwitchTheme_dark.png # scripts/ios/screenshots-metal/SwitchTheme_light.png # scripts/ios/screenshots-metal/TextFieldTheme_dark.png # scripts/ios/screenshots-metal/TextFieldTheme_light.png # scripts/ios/screenshots-tv/ChatView_dark.png # scripts/ios/screenshots-tv/ChatView_light.png # scripts/ios/screenshots-tv/SpanLabelTheme_dark.png # scripts/ios/screenshots-tv/SpanLabelTheme_light.png # scripts/ios/screenshots-watch/ChatView_dark.png # scripts/ios/screenshots-watch/ChatView_light.png # scripts/ios/screenshots-watch/DialogTheme_dark.png # scripts/ios/screenshots-watch/DialogTheme_light.png # scripts/ios/screenshots-watch/SpanLabelTheme_dark.png # scripts/ios/screenshots-watch/SpanLabelTheme_light.png # scripts/ios/screenshots/ButtonTheme_dark.png # scripts/ios/screenshots/ButtonTheme_light.png # scripts/ios/screenshots/ChatView_dark.png # scripts/ios/screenshots/ChatView_light.png # scripts/ios/screenshots/DialogTheme_dark.png # scripts/ios/screenshots/DialogTheme_light.png # scripts/ios/screenshots/ShowcaseTheme_dark.png # scripts/ios/screenshots/ShowcaseTheme_light.png # scripts/ios/screenshots/SpanLabelTheme_dark.png # scripts/ios/screenshots/SpanLabelTheme_light.png # scripts/ios/screenshots/TextFieldTheme_dark.png # scripts/ios/screenshots/TextFieldTheme_light.png # scripts/mac-native/screenshots/ButtonTheme_dark.png # scripts/mac-native/screenshots/ButtonTheme_light.png # scripts/mac-native/screenshots/ChatView_dark.png # scripts/mac-native/screenshots/ChatView_light.png # scripts/mac-native/screenshots/DialogTheme_dark.png # scripts/mac-native/screenshots/DialogTheme_light.png # scripts/mac-native/screenshots/ShowcaseTheme_dark.png # scripts/mac-native/screenshots/ShowcaseTheme_light.png # scripts/mac-native/screenshots/SpanLabelTheme_dark.png # scripts/mac-native/screenshots/SpanLabelTheme_light.png # scripts/mac-native/screenshots/SwitchTheme_dark.png # scripts/mac-native/screenshots/SwitchTheme_light.png # scripts/mac-native/screenshots/TextFieldTheme_dark.png # scripts/mac-native/screenshots/TextFieldTheme_light.png
…rk MultiButton The merged tree renders #5358's corrected vertical TextArea/text alignment (shifts every screen with text components, including the measurement-overlay labels on ButtonTheme/SwitchTheme) and the dark MultiButton rounded-rect fix. Verified MultiButtonTheme_ios_dark now renders rounded dark cells; accepted all 16 mac + 30 JS flagged renders. iOS-family (GL/Metal/tv/watch) churn lands with their runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ty-suite Conflicts resolved by keeping the branch side: - scripts/cn1playground/pom.xml: keep the deliberate 8.0-SNAPSHOT pin (master's 7.0.258 release bump must not touch the playground pin). - 14 scripts/javascript goldens: master's #5361 regenerated them on a tree without this branch's theme changes; ours were captured from the merged tree post-#5358 and remain correct for this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… shift + rounded dark MultiButton GL (16), Metal (16), tv (5), watch (9) renders reviewed against the mac goldens from the merged tree. The old GL/Metal goldens carried pre-#5351 renders where the Metal/GL foreground erase dropped button pills and text-field glass cells (ButtonTheme_dark golden showed bare blue text and H=7mm vs the correct glass pills at H=5mm); the new renders restore them. Remaining churn is the #5358 TextArea vertical alignment shift plus the rounded dark MultiButton fix (74599f4), now confirmed rounded on metal, tv and watch. Deliberately NOT accepted: AnimateLayoutScreenshotTest-family goldens (their committed renders lost the tile text in the final frames on all ParparVM runtimes -- iOS GL/Metal/JS -- while JVM/ART runtimes keep it; that regression is being tracked down separately and the goldens will be refreshed when it is fixed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tile labels' text vanished in the last two animation frames on iOS GL/Metal and JS while JavaSE/mac/Android kept it. Root cause proven by probing the native drawLabelString: textSpaceW fell 712/417/269/147/39/-58 across the six frames - a constant 434px padding deficit, i.e. 2 x 12mm at the phone's ~460ppi. Style.setPadding(int...) keeps whatever unit the style inherited from the theme (javadoc: units come from setPaddingUnit), and the iOS/JS base themes declare Label padding in millimeters while the JVM-port themes resolve pixels. So this test's setPadding(12,...) meant 12mm per side on the mm-unit ports: the text space went negative as the tiles narrowed into the grid - 3-points truncation at 80%, empty clip at 100%. Not a VM bug and not a #5327 regression: the iOS golden lacked final-frame text since the test was introduced. Pin UNIT_TYPE_PIXELS on the container and tile styles so the scene renders identically on every port. Verified on the iOS 26 simulator: all six tiles slim, text visible in all six frames. Expect AnimateLayoutScreenshotTest golden churn on iOS GL/Metal/tv/watch and JS only; px-unit pipelines (linux/mac/windows/android) render identically and must show zero churn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… frames) Accept the eb1e4e1 CI renders on the seven ParparVM/mm-unit-affected pipelines: iOS GL, iOS Metal, tvOS, watchOS, JavaScript, Windows (x64 and cross-compiled render byte-identically, one shared golden) and mac-native. Every render reviewed: all six tiles now lay out with the intended 12px padding and the Tile 1..Tile 6 labels are visible in every frame including the final two, which previously lost their text on the high-DPI iOS pipelines. The bytecode pipelines (Linux JavaSE, Android) passed with zero churn, confirming the padding-unit diagnosis. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The GL suite flapped on chart-transform on a rerun (0.21% of pixels moved, scattered AA over the 3D-transformed plot) right after passing the same commit with only 'landscape' flagged -- the nondeterminism signature, same family as chart-rotated-pie. Pin the pre-tightening 0.30% gate for this one screen on both iOS sim pipelines. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nit fix The GTK-native pipelines run ParparVM-translated code with the same mm-unit base theme, so they churn from the px-unit pin like the other native ports (missed on the first wave: the arm64 job flaked before its gate and x64's flag surfaced on the follow-up run). Both arches render byte-identically; text visible in all six frames. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Third distinct GL flap in consecutive waves (landscape -> chart-transform -> triangle grid), each a different screen with ~0.1% scattered vector-AA noise while all other screens match. Same treatment as chart-rotated-pie and chart-transform: pin the pre-tightening 0.30% gate for this screen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
It is very exciting to see all this work on bringing the CN1 UI close to native liquid glass (and same for Android though I follow that a little less currently), it is in my view a hugely important enabler for CN1 adoption. It is, however, a bit difficult to follow everything so I wondered if at some point it would be possible to share some complete examples, eg a couple of screens from known (real) apps and how close CN1 can get to that UI? |
|
@ThomasH99 this is a PR, it's work in progress. It isn't meant to be readable. A blog post will follow this weekend. |
|
Yes, sorry, I clicked the wrong link and only realized afterwards. Looking at the screenshots things are definitely going forward, so looking forward to the blog post :-) |
















































































































































































































































What
Two things, built on each other:
scripts/fidelity-app): for every component with a native equivalent, the real native OS widget and the CN1 component under the native theme are rendered in comparable environments and scored per (component, state, appearance). CI ratchets the scores one-way (FidelityGate) -- a change can only improve fidelity, never silently regress it.The iOS-26 selection "drop": a real magnifying lens over the glyphs -- CN1 (this PR) morphing side-by-side against the native tab bar is in
ios-modern-tab-morph-fidelity.png.Architecture (response to the glass/material review)
All eight points are addressed; the glass/material system is now a typed rendering model with explicit geometry and motion validation:
GlassRecipe(blur/chrome/pill/panel): named, bounded, measured material definitions. Themes assign a recipe per UIID (ToolbarGlassRecipe: "chrome");Componentresolves the recipe and forwards its parameters to the port. The per-parameter constant soup (ToolbarGlassSatDark, ...) is gone.TabSelectionMorph: pure, unit-tested motion model (t + cells + tokens → pill rect, lens rect, magnify/aberration/tint, bar-grow).Tabspaints from the model. Same discipline for the switch:SwitchThumbDroplet.tabsMorphPreset: ios26|subtle) plus three high-level scalars (duration,tabsMorphLensIntensityPct,tabsMorphSpringPct). The 13 envelope constants were deleted; the presets are pinned by unit test.fidelity-tests.yamldeclaresmaterial: normal|glass|lensper test; the comparator picks its scoring mode from that declaration (platform-resolved), not from corner/backdrop heuristics. Verified zero score drift across the full artifact set.MorphFrameValidator: monotonic travel, distinct frames, bounded overshoot) with a labelled frame strip per run. The same points are pinned numerically against the model inTabSelectionMorphTest/SwitchThumbDropletTest(including the t=0.90 spring overshoot).CN1_GLASS_PROFILEbuild: composition ~90ms avg on backdrop change vs ~5.3ms on a cache hit (17x; 475 hits / 253 misses across a suite run). The selection lens is a pure GPU fragment shader on the frame's command buffer (no sync/readback; this is what took the morph from ~6fps to frame rate).Framework changes (each verified against the native golden)
fillLinearGradientGlobalhad inverted the horizontal/vertical mapping since the original 2012 port — every on-screen linear-gradient background on iOS painted with its axis swapped (the mutable-image path was correct). Found the moment the new geometry masks made the gradient isolation tile honest: CN1 ran the blue→green ramp left-to-right where native runs top-to-bottom, invisible to the tolerant whole-tile score (94.9%). This is the validation infrastructure paying for itself.backdrop-filter: blur()paint integration on all three ports; iOS Metal live-screen glass/blur/lens ops (cn1_fs_lensfragment shader; GPU→GPU, no readback for the lens); glass shape-masking to the component's pill/rounded border; Apple SF Symbols for iOS icons with Material fallback (FontImage.createSFOrMaterial).tabsEqualWidthBool), M3 indicator thickness fix (float, was silently 2× too thick), opt-in full-width bottom divider.fabDiameterMM(Material's fixed 56dp) instead of the legacy icon-derived ~71dp..disabledstyle (diverges from label text, as Material renders).dialogMaxWidthPercentInt) so alert bodies wrap into a card.Style.letterSpacing, res format v1.13/v1.14 (gradients, filters), and the tunednative-themes/{ios-modern,android-material}/theme.css+ regenerated shipped.resmirrors.Validation infrastructure
ProcessScreenshots --mode fidelity(intent-driven scoring, backdrop masking, geometry block),RenderFidelityReport(PR comment: score + material + collapsed geometry tables + side-by-side cards),FidelityGate(one-way fidelity + geometry ratchet),MorphFrameValidator(frame goldens + motion properties + strips),FidelityComposite(contact sheet).GlassPanel{Grey,Red,Grad,Photo}(blend vs 4 backdrops),TabsGeom/TabOne(geometry over flat grey),GlassText/GlassIcon(single element over a matched capsule) -- so glass, geometry and glyph deltas are attributable.Native references: local capture, versioned golden sets
Native references are captured locally, never generated by CI -- CI only renders the CN1 side and compares against committed goldens. Two standalone capture apps drive REAL windows (
ios-native-ref/NativeRef.swiftviascripts/build-ios-native-ref.sh;android-native-ref/viascripts/build-android-native-ref.sh), which is what makes honest pressed states possible (a held touch with the ripple/highlight settled -- 8 Android + 6 iOS pressed references are in the sets) and adds native animation videos (scripts/record-{ios,android}-native-anim.sh->goldens/<set>-anim/: the iOS 26 tab lens morph and switch toggle, and their Material counterparts) as the human reference beside the deterministic CN1 morph frames.Each golden set is pinned to the OS design generation it was captured on --
goldens/ios-26-metal(iOS 26 simulator; the CI job asserts a matching runtime) andgoldens/android-m3(the CI emulator profile: API 36, 160dpi) -- with its own ratchet baseline. When iOS 27 lands, the migration is phased: capture a NEW set on the new OS, add a theme variant + CI matrix row, and gate both looks side by side until the old one is deliberately retired. iOS captures are proven deterministic (68 goldens byte-identical across two runs).Current numbers
Native fidelity (...)comments).Coverage & what's still missing
native-themes/COVERAGE.mdtracks the full audit: 14 iOS + 13 Android native controls covered and measured, and the explicit backlog (segmented control, stepper, search bar, chips, bottom sheets, date/time pickers, badges, snackbar/toast, slider droplet thumb, ...) with suggested CN1 building blocks. The "How to add a component" recipe is documented there.Developer guide
The theming chapter documents the Liquid Glass materials (recipes), the tab morph (presets + gif + knob table) and the frame-validation discipline (
docs/developer-guide/Native-Themes.asciidoc).🤖 Generated with Claude Code