Native theme fidelity suite + Material 3 fidelity fixes#5274
Native theme fidelity suite + Material 3 fidelity fixes#5274shai-almog wants to merge 187 commits into
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>
From the fe0910b JS artifact (Linux reference): the two TabsTheme_ios captures now show the magnified selection drop with the soft accent cast over the Home tab (glyph visible). 163 other JS captures compared equal - the lens op affects only the Tabs selection region. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The GL (ios-ui-tests) pipeline kept hitting an xcodebuild flake before capture across the last few boards; it captured cleanly this run. Reviewed: dark frosted pill with the backdrop blur reading through, selection behind Home - matches the already-accepted Metal/watch/tv goldens. This closes the TabsTheme_dark churn across all iOS pipelines. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…button margin - Native-Themes.asciidoc: remove the "[IMPORTANT] requires 8.0+ runtime" block. The theme ships built into the framework it's used with; there is no released-vs-modern version-mismatch scenario to warn about. - fidelity-app no longer commits its own AndroidMaterialTheme.res / iOSModernTheme.res. common/pom.xml copies them from the single source (repo-root Themes/) at build time, matching the playground. The committed duplicates are removed + gitignored; build-native-themes.sh no longer mirrors into the fidelity tree. Themes/ is now the only checked-in copy for that consumer. - iOS-modern Button gets a real form-like margin (1mm 2mm) so apps don't render buttons flush to the screen edges / stacked edge-to-edge (ButtonTheme screenshots looked cramped). The fidelity tile zeroes the margin per-widget in Cn1WidgetRenderer so the tight native-vs-CN1 comparison (which also crops to content bbox) is unaffected - the margin is purely how real apps should look. The full-form ButtonTheme screenshots will churn; reseed follows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The iOS Button margin (1mm 2mm) re-renders the full-form iOS-theme captures that contain buttons: Button/Dialog/PaletteOverride/Showcase. Reviewed ButtonTheme_ios_light - buttons now have proper horizontal insets + vertical gaps instead of flush edge-to-edge. 157 other JS captures compared equal; only the iOS variants changed (the margin is iOS-theme only). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… iOS GL/Metal/watch/tv) The iOS-modern theme now ships a small default Button margin (1mm 2mm) so buttons don't touch each other or the screen edge, per review feedback that the zero-margin theme "makes screenshot tests pass but is not how apps should look". The four theme-showcase screens (ButtonTheme/DialogTheme/ PaletteOverrideTheme/ShowcaseTheme, light+dark) render with the new insets on every native pipeline. Fidelity gate is unaffected (comparator crops to the widget bbox and the fidelity renderer zeroes the margin for the tight compare). tvOS also drifts a handful of chart/graphics/SVG goldens by 1-572px (<0.01%); those stay within CN1SS tolerance and did not trip the gate, so they are left untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…l to normal) The pressed button rows in the ButtonTheme showcase looked identical to the default rows. Root cause was NOT a screenshot-capture failure: both CN1's pressed() and the native reference's isHighlighted are applied and captured. The real reason is that on iOS 26 a Liquid Glass button's press feedback is INTERACTIVE (scale + lens shimmer driven by the live touch), so isHighlighted on an offscreen button changes nothing -- the native "pressed" reference was capturing ~normal, and the CN1 pressed styles were tuned to match that non-difference (some even brightened, the wrong direction). While a finger stays down, iOS holds a sustained ~12% dim. This models that: - native-ref (NativeRef.swift): for Button/RaisedButton/FlatButton pressed, overlay a capsule-clipped 0.12 black dim so the committed reference reflects the real held look. The ~0.96 held scale is deliberately NOT modeled: the comparator crops to the content bbox and overlays 1:1 with no scaling, so a one-sided scale would misalign; the dim is the metric-compatible, themeable, showcase-visible part of the held state. - theme.css: retune Button/RaisedButton/FlatButton .pressed (light+dark) to 0.88 x the normal glass render. Matching the 0.12 black overlay on both the fill and the backdrop-through gives alpha_p = 0.12 + 0.88*alpha_n and fill_p = 0.88*alpha_n*fill_n/alpha_p -- an exact composite match, verified on-device: the 6 *_pressed_* tiles now score 88-93% (on par with normal), up to +4.8 vs the prior baseline, none regressed. Recaptured the 6 native pressed references on iOS 26.3; regenerated the iOS-modern .res. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…own dim Follow-up to the held-down pressed modeling (c60cd13). The ButtonTheme showcase is the only screen with a pressed button row, so it is the only showcase golden that changes: the Pressed / Raised-pressed pills now render the sustained ~12% held dim instead of looking identical to Default / Raised. Accepted on every pipeline that renders it (JS ios variants, Mac, iOS GL + Metal, watchOS, tvOS). tvOS light stayed within tolerance; only its dark tripped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ckbox size, thinner glass Addresses the batch of iOS-modern fidelity feedback: - TextField/TextArea: were filled #f2f2f7 == the grouped form background, so the field was invisible. Now a contrasting grouped-list cell (white light / #2c2c2e dark) + a faint rim + backdrop blur -- the iOS 26 glass field, visible on the form. The fidelity tile now scores the field over the grey grouped backdrop (new "grouped" backdrop token in FidelityDeviceRunner + NativeRef + yaml) where a white cell actually contrasts; the native TextField reference is recaptured as a grouped cell to match. - Picker: had NO rule and rendered as a bare label. Added a Picker UIID with the contrasting-glass field chrome (light + dark) so it reads as a tappable field. - MultiButton: dark override was #000000 and rendered as a light pill with unreadable text; now a #2c2c2e grouped glass cell (light = white cell), so the MultiLine rows read in both appearances. - CheckBox/RadioButton: glyph shrunk 5.9mm -> 4.83mm (a ~24pt SF symbol, the size it takes in a native list, was oversized next to the label) and given a left margin so it aligns with the label column. Native-ref glyphs drop to 24pt in lockstep; the fidelity renderer zeroes the alignment margin for the tight tile. - Buttons: thinned the glass fills toward real Liquid Glass (Button 0.34 -> 0.20 light, 0.36 -> 0.24 dark) with a stronger hairline rim for flat-bg definition; pressed dims recomputed to 0.88x the thinner fill. Local iOS fidelity validation was blocked by a stale-.res cache in the local build (the sim rendered a frozen old theme against the new native refs, showing false regressions); CI builds clean, so the fidelity + showcase results there are the real validation. Baseline intentionally not touched here -- it will be re-anchored from the clean CI numbers if the deliberate changes shift it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ase churn Follow-up to ddc28c2 (the iOS-modern theme review overhaul), validated on CI's clean build: - Fidelity: only TextField_normal_light / TextField_disabled_light regress (~-7), the intentional visible-field tradeoff -- a visible grouped-cell field scores ~90 where the old INVISIBLE field (fill == form bg) scored "97". Re-anchored those two baseline floors to the honest CI scores. Every other tile passed; CheckBox/RadioButton actually IMPROVED (e.g. CheckBox_normal_dark 95.07->96.52) now that the 4.83mm glyph matches the recaptured 24pt native refs. - Showcase goldens accepted across every pipeline that renders the theme (JS, JavaSE, Mac, iOS GL + Metal + watchOS + tvOS): the fields/picker are now visible cells, dark MultiButton is a readable dark cell, checkbox/radio are list-sized and aligned, buttons are thinner glass. tvOS only tripped its dark MultiButton/Picker/TextField (the rest stayed within tolerance). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The baseline edit in 13c486e was left unstaged, so CI still gated the two TextField-light tiles against the old 97.x floors and failed. Commit the re-anchored floors (TextField_normal_light 90.40, TextField_disabled_light 91.35) matching the clean CI scores for the intentional visible-field change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…/Picker/ChatInput Four of the five reported iOS-modern issues (all theme-only): - CheckBox.disabled reset margin:0, so the disabled box lost the 2mm left inset and sat out of line with the enabled boxes. Restore the alignment margin (light + dark). RadioButton.disabled already inherits it. - MultiButton dark had a 0.10 white rim that drew a SQUARE rectangle over the rounded cell (visible on dark; the light cell's 0.08 rim is too faint to show it). Drop the dark rim; the #2c2c2e-on-#1c1c1e fill carries the cell. - Picker dark derived TextField, but cn1-derive copies the BASE (light) white fill, so the dark picker rendered white-on-white. Give it explicit dark cell values (#2c2c2e + white text). - ChatInput's attach (+) and voice (Mic) buttons derive the LIGHT Button glass and had no dark override, so in dark mode they showed a pale grey glass halo on the dark bar. Add dark Button-glass overrides for both. Not fixed here: PaletteOverride not rendering magenta. Confirmed PRE-EXISTING (the pre-overhaul golden is also blue) -- the accent @cn1-bind fan-out is emitted (light + dark) and the UIManager apply logic looks correct, but it doesn't produce the override colour at render. The screenshot test only asserts golden match, so a blue golden was accepted and the binding never actually fired. Needs a runtime trace; tracked as a separate framework follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nders solid The round-2 border removal broke MultiButton dark: with the border gone but the backdrop-filter:blur(20px) still inherited from Button, the JS port washed the #2c2c2e cell out to a light grey pill (round-1's border had forced an opaque render path). MultiButton is an opaque grouped-list cell, not a glass button, so override backdrop-filter to blur(0) on the base UIID. The solid fill now renders in both appearances (white light / #2c2c2e dark) with no glass wash and no rim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ight in JS) blur(0) didn't fix it -- the border PRESENCE (not the blur) is what makes the JS port render the cell's solid fill; a fully borderless dark cell rendered light grey regardless. So keep a rim but drop it to the light cell's faintness (rgba(255,255,255,0.05)) so the square corners the 0.10 rim showed on dark fade out. Reverted the blur(0) override (it was a no-op). Dark cell now renders solid #2c2c2e with a barely-there edge.
…rners The dark cell needs a rim to render its solid fill (borderless washed light), but a plain border boxes the rounded cell with a square. cn1-background-type: cn1-round-border draws the rim along the 3mm radius, so the cell keeps its solid #2c2c2e fill AND a rounded edge -- no square. Compiles no-CEF.
…oth cells cn1-round-border fixed the square but the inherited Button glass blur painted a light circular halo in each dark cell. With the rim + round-border now forcing the solid fill, backdrop-filter:blur(0) removes that halo (MultiButton is an opaque grouped cell, not glass). Apply round-border to the light cell too for a consistent rounded rim.
…ded rect Root cause of the whole MultiButton saga: it derived Button, inheriting cn1-pill-border (a pill/circle background -- the light halo/circle in each cell) and the glass backdrop blur. cn1-round-border is ALSO a circle/pill; there is no rounded-rect token -- the compiler auto-selects RoundRectBorder from border-radius ONLY when no round/pill background type is set. So drop cn1-derive: Button: the cell is now an opaque RoundRectBorder rounded rectangle (white light / #2c2c2e dark) with a faint rim, no glass, no circle, no square.
…/ChatInput) Goldens for the four round-2 theme fixes, verified on CI: - CheckBoxRadioTheme (light+dark): disabled box regains its alignment margin. - MultiButtonTheme (light): now a plain RoundRectBorder grouped cell -- no Button derive, so no pill/glass halo and no square rim (dark matched already). - PickerTheme (dark): dark contrasting cell instead of white-on-white. - ChatInput (dark): attach/voice buttons use the dark glass, not a light halo. Accepted across every pipeline that flagged them (JS ios_ variants, Mac, iOS GL + Metal + watchOS; tvOS only tripped PickerTheme_dark). Unrelated sub-tolerance AA byte-drift on other screens was left untouched.
…ty-suite # Conflicts: # Ports/iOSPort/nativeSources/IOSNative.m # scripts/build-android-app.sh
The dark-mode override for the chat +/Mic buttons redeclared `border` without a background-type, so the compiler emitted a plain rectangular Border and the buttons rendered as square outlined boxes on the dark bar (light mode was fine -- it derives Button, which sets cn1-pill-border). Add `cn1-background-type: cn1-pill-border` to both dark rules, matching how the light Button pairs `border` with the pill type, so they render as dark circular affordances again. Regenerated the iOS-modern .res mirrors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tvOS goldens all shared one blanket tolerance (channelDelta=48, mismatchPercent=1.0). On a 4K frame (3840x2160 = 8.3M px) that is ~19% per-channel drift and ~83k px of structural slop, so real changes slipped through -- e.g. the MultiButton dark cells kept a stale square-border golden after the border was rounded, because the border recolor sits under delta=48 and the corner rounding moved <1% of pixels. Split the tolerance by tile type: * FLAT (non-glass) tiles -- MultiButton, PaletteOverride, SpanLabel, List, CheckBox/Radio, TextField, Picker -- have no backdrop-blur and thus none of the run-to-run GPU glass noise, so they drop to channelDelta=24 / mismatch=0.30%. 24 clears cross-runner text AA while catching recolors and border/shape changes. * GLASS tiles -- Button, Tabs, Toolbar, ChatInput/ChatView, Dialog, Switch, FAB, Showcase -- keep channelDelta=48 (still above the ~40 glass-noise band) but tighten mismatch 1.0 -> 0.40%. Domain tiles (graphics-*, VectorMap*, Media360, RichText, RealOsm, CodeEditor) keep their own justified 3D/vector/map tolerances. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dark ChatAttachButton/ChatVoiceButton pill fix (cn1-pill-border) changes the iOS-modern +/Mic buttons from square outlines to circles. The JS showcase re-rendered them correctly; refresh the stale golden. Only the _ios_dark variant is affected (light and the default-theme variant were already correct). Co-Authored-By: Claude Fable 5 <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>
















































































































































































































































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