Turn on the desktop native themes, and fix what turning them on exposed - #5861
shai-almog wants to merge 54 commits into
Conversation
Four things a desktop form needs that the framework either lacked or built and never wired up. **Tab and Shift-Tab move focus.** The traversal machinery is years old -- TabIterator, getNextComponent, preferredTabIndex -- and no key was ever connected to it, so its only callers were the "next field while editing" paths in TextEditUtil and Picker. Connecting Tab to that iterator would have been wrong, and the reason is worth recording: its filter opens with `getTabIndex() >= 0`, Component.preferredTabIndex defaults to -1, and TextArea is the ONLY class in the framework that ever calls setPreferredTabIndex(0). So the iterator holds text areas and nothing else -- correct for next-field-while-editing, and for Tab it would walk between a form's text fields and skip every button, checkbox and slider between them. The desktop order is therefore its own: same walk, same comparator shape, but filtered on focusability, which is what a pointer can reach and so what a keyboard must reach. An explicit preferredTabIndex still sorts first; zero counts as unnumbered rather than as first, because that is what setTraversable(true) writes and reading it as a position would put every TextArea ahead of its own label. Wraps at both ends so the keyboard cannot strand itself. **Escape cancels.** On a Dialog it does what the window's close control does, which was already written twice as "the back command, or dispose when there isn't one" -- now written once, as Dialog.cancel(), with both former sites calling it. On a plain Form it fires the back command and does nothing at all when there is none: Escape must never be able to exit an application. Window honours its close operation, so one that refuses the close button refuses this too. Enter needed nothing: ports already map it to GAME_KEY_CODE_FIRE and keyReleased already fires getDefaultCommand() on GAME_FIRE. Both keys are gated on isDesktop(), and the gate is asserted in both directions -- a gate nobody tests from the other side is not a gate. **desktopTitleBarMode had no reader.** All three desktop native themes have carried this constant since they landed, and nothing ever read it: Form went to Display.impl.getDesktopTitleBarMode(), which is sourced from the build hint. The constant was dead text. It cannot simply be consulted either, because that method is documented to answer a usable mode and so cannot say "nobody asked" -- it answers "toolbar" both for a port with no opinion and for a project that deliberately chose the legacy look. Hence getConfiguredDesktopTitleBarMode(), which reports exactly that distinction: build hint first, then the theme constant, then the port. JavaSEPort's static drops its "toolbar" default for null and coalesces at each reader, which also stops injectDesktopThemeConstants from writing that default over a theme's own constant. **The four interactive-scrollbar UIIDs are seeded.** LookAndFeel.initScroll picks DesktopScroll/DesktopScrollThumb and the horizontal pair when interactiveScrollBool is on, and UIManager.resetThemeProps seeded only the four mobile ones. A theme that turned the constant on without defining all four therefore drew its track and thumb out of the blank default style: an invisible scrollbar, still reserving its gutter, with nothing reporting a problem. Seeded like their mobile counterparts plus the two things the desktop bar needs and the mobile one does not -- a gutter wide enough to grab, and thumb hover/pressed styles so the highlight exists before a theme styles it. Guarded like every other seed here, so the desktop themes still suppress them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… had All three exist in every desktop toolkit and had no Codename One equivalent, which is why a desktop form built out of the existing components reads as one undifferentiated column of controls. **Separator** -- NSBox in separator mode, MenuFlyoutSeparator, GtkSeparator. Applications drew their own out of a Label with a bottom border or a fixed-height Container with a background colour. Those look approximately right on one platform and wrong everywhere else, because what differs between platforms is exactly the part they hard-code: the thickness, the colour and the air either side. All three come from the Separator UIID here -- border where there is one, background colour otherwise, margin for the air, and separatorThicknessMM for the thickness. Never thinner than a pixel: a theme may ask for a hairline, a hairline rounds to zero millimetres worth of pixels on a dense screen, and zero makes the rule invisible rather than thin. Not focusable, so the new desktop traversal skips it. The constant is read as a string like every other *MM constant (the theme format has no float accessor) and a malformed one falls back rather than throwing out of a paint. **GroupBox** -- NSBox with a title, GtkFrame with a label widget, WinUI headered content. Two UIIDs: GroupBox styles the frame and GroupBoxTitle the caption. Nothing hard-codes where the caption sits relative to the top edge, because the three platforms disagree about it; a theme that wants it inset into the edge uses a negative top margin. An empty caption hides the strip entirely rather than leaving a gap nothing explains. Adds route into the content pane, including the constrained form -- the two-slot BorderLayout underneath is an implementation detail, and BorderLayout.SOUTH from a caller means "below the other controls in this group", never "outside the box". **Stepper** -- NSStepper, NumberBox with spin buttons, GtkSpinButton. The nearest existing thing is Slider, which is a different control for a different job: a slider is for a value whose exact number does not matter and a stepper is for one where it does. Every hand-built composite got the same two things wrong, so both are handled here: the field cannot hold a number this control could not produce (out-of-range typing is corrected under the caret, and empty or unparseable text is left alone so clearing the field to retype it does not watch it fill itself back in), and the button that would step past a bound is disabled rather than accepting a press that does nothing. Fires only when the value actually moved. The value is an int, deliberately. A fractional stepper is a real control on some platforms and doing it properly needs a format, a locale and a parse policy; guessing those is worse than not offering them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…wo behaviours The three desktop native themes shipped with a scrollbar derived from the mobile one and nothing defined for any surface Codename One still draws itself. Both are fixed here, and both were the kind of gap nothing reports: the controls simply look wrong. **The scrollbar was `cn1-derive: ScrollThumb` and a transparent track.** That is a desktop scrollbar with no gutter, no minimum thumb length and no highlight, in themes whose whole job is to look like the platform -- while the MOBILE themes (ios-modern, android-material) carried the full desktop treatment. Backwards. Each theme now sizes its own gutter from the platform's figure (12px Fluent, 15px Aqua overlay, 13px Adwaita), insets the thumb inside it, and gives the thumb its hover and drag colours. Two things about the states are worth recording, both measured rather than assumed by reverting one theme and reading the compiled `.res` back: - The highlight states are `.selected` and `.pressed`, NOT `.hover`. LookAndFeel's InteractiveScrollThumb returns getSelectedStyle() under the pointer and getPressedStyle() while dragged. A `.hover` rule here compiles and is never painted. - `cn1-derive` emits the whole state family by copying the base, so the old stub produced `sel#bgColor = 8a8a8a` against `bgColor = 8a8a8a` -- a highlight that is present in the resource and invisible on screen. Worse in dark mode: derive is flattened against the LIGHT parent, so the dark thumb came out 8a8a8a, the light mobile grey, rather than the theme's own 9a9a9a. The test therefore asserts the two values DIFFER; asserting the key exists passes on exactly the defect being fixed. **Nothing defined the surfaces CN1 still draws on a desktop.** On Windows and macOS the menu BAR is the platform's own, but the right-click menu, the overflow menu, the tooltip and the dialog's command area are ours -- and the whole of GNOME's headerbar mode is. PopupContentPane, CommandList, Command, TouchCommand, Tooltip, TooltipDialog, DialogCommandArea, DialogButton and DialogButtonDefault were undefined in all three, so each fell through to UIManager's blank default: black text on white, on a dark window. Same for ToolbarSearch (written by SearchBar), the Accordion pair (seeded by the framework with a line border and phone metrics) and the Tabs trio (only the tabs* constants existed). Plus the three new components' UIIDs: Separator, GroupBox/GroupBoxTitle, Stepper/StepperField/StepperButton, and Link. **interactiveScrollBool and defaultNativeWindowModeBool are now theme constants.** Both are behaviours a native desktop theme IS, and the theme is the right seam for them: these three files install only on the desktop, so an application still on the legacy theme is untouched and no port-side isDesktop() gate is needed. Dialogs therefore open as real operating system windows by default under these themes; anchored popups never do, and the constant is ignored where there is no windowing system. Aqua adds no hover rules, which is asserted rather than remembered: AppKit restyles none of these controls on rollover, the captured reference says so, and eighteen .hover rules were already removed from that theme for the same reason. Its scrollbar knob is the deliberate exception, because NSScroller does darken -- through sel#/press#, like every other interactive thumb. Every coloured addition has a $Dark counterpart, and that is asserted too: the derive flattening above is exactly how a dark-mode rule goes missing without anything noticing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ned one Component.addContextMenuListener has been here for a long time -- it fires on a secondary mouse button, a stylus barrel button or a long press -- and nothing ever turned it into a menu. Every application that wanted one built its own popup, which is why none of them looked like the platform. ContextMenu is the popup, styled through PopupContentPane, CommandList and Command, which the desktop themes now define. Component.setContextMenuCommands is the short way in: give a component its commands and the menu opens by itself. When the commands depend on what was clicked -- which row, which cell -- a listener plus ContextMenu.show is still the way. Three details that are decisions rather than mechanics: The listener and the commands are resolved in ONE walk up the tree, not two. A row inside a table that carries its own commands must not be overruled by the table's listener merely declining to consume, and two passes would do exactly that. The buttons carry the command's NAME rather than the command itself. Button(Command) fires the command from inside its own action event, which runs it while the menu is still showing -- so a command that opens a form or another dialog would put it underneath a menu still holding the popup layer, and the menu would outlive the screen it belonged to. Here the menu is disposed first and the command dispatched after, which is the order Dialog already uses for its own command buttons. It never becomes an operating system window, even now that the desktop themes ask for that by default: setNativeWindowMode(false) per instance outranks both the static default and the theme constant. The reasons are the ones already written on Dialog.showPopupDialogImpl -- the rectangle it points at is in its host's coordinate space, a separate window would never receive the click meant to dismiss it, and it would steal focus from its opener every time it appeared. Empty means nothing opens, in both directions: setting null or an empty array removes the menu rather than leaving an empty one, and ContextMenu.show refuses to open one. A menu with no items is a rectangle the user has to dismiss to learn it was empty. resolveContextMenuOwner is the routing without the showing. The showing is a modal popup that parks its caller until the user dismisses it, so a test that asserted the routing through it would simply hang -- which is what the first version of these tests did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MenuBar.updateCommands hands the commands to setNativeCommands and RETURNS -- it draws no soft buttons, because on a platform with a real menu bar drawing them too would duplicate every command. CodenameOneImplementation.setNativeCommands is an empty method. So on a platform without a menu bar, asking for COMMAND_BEHAVIOR_NATIVE sent every command to a method that discards them and drew nothing: the application simply had no commands, and nothing anywhere in that path could tell that from "the platform handled it". Latent for as long as the constant has existed, because nothing asked for it. The desktop native themes now do -- `commandBehavior: Native` is right for the three platforms they model -- and two of the ports that will install them, Windows and Linux, implement no native menu bar at all: no setNativeCommands, no WindowManager.setCommands, no menu native source. Installing those themes without this would have deleted the commands from every Windows and Linux desktop application, silently. isNativeCommandsSupported() is the missing question, and setCommandBehavior is where it is asked. That method already normalises a behaviour the platform cannot honour -- BUTTON_BAR becomes SOFTKEY on a non-touch device -- so NATIVE becomes DEFAULT on a platform with no menu bar in the same place, and every reader downstream is fixed at once rather than one at a time. JavaSE and the iOS/macOS ports answer true on the desktop, where they really do build a JMenuBar and an NSMenu; everything else keeps drawing what it has always drawn. Form.isDesktopHideToolbar() needs the same question asked separately, because it is driven by desktopTitleBarMode rather than by commandBehavior. Hiding the Toolbar takes away the side menu, which is the only place the commands are drawn, so a port that cannot take them natively keeps its Toolbar. The title still goes to the OS title bar there, because that part works everywhere -- the result is the legacy look, not a broken one. Asserted in both directions, and the guards verified to bite by removing each one and watching the matching test fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The three desktop native themes have been built, measured and gating since they landed, and no port installed one. Windows and Linux staged AndroidMaterialTheme -- a phone design language on a desktop, complete with Material ripples, a hamburger side menu and a fading touch scrollbar -- and macOS defaulted to the modern iOS theme, which is the same mistake with a different phone. That was sequencing, not oversight: the flip restyles every screen and reseeds each port's committed screenshot baselines. It was deferred until those could be reseeded alongside it, which is what this change does. Three one-line flips, exactly where the deferral notes said they were: - maven/windows/pom.xml stages WindowsFluentTheme.res - maven/linux/pom.xml stages GnomeAdwaitaTheme.res - MacOSBuildHints.getThemeMode()'s unset branch answers "aqua" The JavaSE desktop default is deliberately NOT flipped. resolveDesktopNativeTheme keeps answering legacy when nothing asks, because that default reaches every desktop application ever built with Codename One, not only ours -- and an application that wants the platform look already has desktop.themeMode and the cross-platform nativeTheme=native to ask with. The three native ports have no such history: none has shipped. hellocodenameone gains desktop.titleBar=native and desktop.interactiveScrollbars=true, which is what the Maven archetype and the initializr already put in every new project. The app's own theme.css sets includeNativeBool: true, so it inherits whichever theme the port stages with no CSS change. DesktopModeScreenshotTest used to switch desktop mode on for itself and switch it back off in done(), because it was the only screen in the suite that ran with the desktop chrome. It is not any more, and keeping the local opt-in would now HIDE a regression rather than demonstrate a feature: whatever the test turned on for itself would look right even if the suite-wide settings had stopped working. Its baseline is also the record of a real difference between the ports. macOS and the Java SE build have a menu bar, so the Toolbar is hidden and the commands move into it, out of the raster. Windows and Linux have none yet, so the Toolbar stays and draws them -- Form. isDesktopHideToolbar() will not hide the only place the commands exist. Both are correct, and the two baselines are what say so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hanged COVERAGE.md's Desktop section has been a verbatim copy of the iOS section since 3959d68. That commit set out to remove a block the merge had duplicated and removed the wrong copy: what sits under "Desktop: Windows Fluent, macOS Aqua, GNOME Adwaita" today is UIButton .glass, UITabBar and UIPickerView. So the file has had no record at all of which desktop widgets are covered, which is the one thing that section exists to say. Restored from 2ceb4f0, the last commit that had it, and updated. What the update records: - Which theme each port installs now, and that the Java SE default is deliberately still legacy -- that one default reaches every desktop application ever built rather than only ours. - The constants the desktop themes declare, and why they are theme constants rather than port hooks: the three files install only on a desktop, so nothing needs an isDesktop() gate. - That commandBehavior: Native is safe on a port with no menu bar because setCommandBehavior normalises it away, which it did not before. - The native menu bar on Windows and Linux, added to the honest gap list rather than left to be discovered. README.md's Layout block listed ios-modern and android-material only, and its Rebuilding section the two themes those produce -- the three desktop ones have been missing from both since they landed. Added, along with a section on the behaviour a desktop theme declares, including the two traps this change had to find by measuring: the interactive scrollbar thumb highlights through .selected and .pressed rather than .hover, and cn1-derive on those UIIDs produces a highlight identical to the resting colour. The guide gains the keyboard conventions, the context menu, the three new components, and the note that Windows and Linux keep their Toolbar because they have nowhere native to put the commands -- which is a difference a developer will see and should not have to rediscover. Vale and LanguageTool both clean on the two chapters, checked against the committed versions first to be sure the findings were mine rather than inherited. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The desktop matrix scored nine controls, all of them form fields. Everything that makes a window look like a desktop window -- the scrollbar, the menus, the chrome -- was unmeasured, including the interactive scrollbar that is the headline feature of this whole area. Thirteen rows added: ScrollBar, Separator, GroupBox, Stepper, LinkButton, SearchField, ListRow, Tabs, Toolbar, Disclosure, MenuBar, MenuItem, Tooltip. All at the existing 240x56 tile, because the tile size is a constant in each of the three standalone capture apps and nothing here needed a bigger canvas. A Dialog row does, which is why there isn't one; it is in the gap table with the prerequisite named. The CN1 scrollbar tile is the bar and nothing else. `LookAndFeel.drawVerticalScroll` paints the theme's track and thumb across any component, so the tile is the same thing the reference apps build -- a scrolling container would put its content in the comparison too. Its hover and drag states come from OVERRIDING `isVScrollThumbHover()` and `isVScrollThumbGrabbed()`, which are the two public methods `drawScroll` asks to choose between the unselected, selected and pressed thumb styles. That renders exactly the pixels a real hover produces, with no test-only hook added to the framework and no synthetic pointer to get wrong. Three rows are not scored on every platform, and that is the honest answer rather than a gap. A reference has to be renderable into a view: - macOS cannot capture a scrollbar. Measured, not assumed: an NSScroller reports usableParts=allScrollerParts, knobProportion 0.4, isHidden=false and a 17x56 frame, and renders NOTHING through NSView.cacheDisplay -- detached and inside a real NSScrollView, in both .legacy and .overlay styles, with AppleShowScrollBars=Always already set by the capture script. The tile comes back holding one colour, the backdrop, every time. Same class of limitation as Aqua vibrancy, and cacheDisplay is the path that needs no Screen Recording consent, which a hosted runner cannot grant at all. - NSMenu belongs to the window server, so the menu rows are Windows and GNOME. - The AppKit and GTK tooltips are separate windows; a WinUI ToolTip is an ordinary Control, which is the only reason that row exists. Two rows lost a state rather than compare different things. A native expander's expanded state reveals its content, so "expanded native" against "selected-styled CN1 header" is not one measurement -- DesktopDisclosure scores its resting header. And a menu item's highlight is hover on both platforms that have one, so DesktopMenuItem uses hover like every other row rather than a selected style nothing drives. The macOS reference is verified end to end here: 92 tiles, zero blockers, every new tile reviewed by eye. That review is what found three defects the app's own blockers had already flagged -- an NSTableRowView with no intrinsic size in either axis laying out to 240x0, an NSStackView whose fittingSize had no width so the stepper showed its chevrons and no field, and a CGColor read from a dynamic NSColor freezing at the wrong appearance so the light toolbar tile was painted with the dark window background. check-fidelity-spec.py's label table gains all eight new text-bearing rows, which is the check that caught all six of the first wave rendering different strings on the two sides. Its scraper needed two fixes to do it: follow a `MakeXxx()` factory arm the way it already followed MakeComboBox, and take the first NON-EMPTY literal -- the macOS disclosure builds an empty-titled button for the triangle before the label that says "Details", and match one read the empty string. Verified to bite by drifting the GNOME group box label. No goldens yet, deliberately: they come from the runner that scores them, never from a developer's Mac, which has a chosen accent colour, a chosen appearance and custom fonts. The desktop legs will report the new pairs as missing_expected and fail until fidelity-desktop-native-ref.yml is dispatched and its output reviewed and committed -- a new row failing is correct, and is not the same as being skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neither port had one: no setNativeCommands, no getDesktopTitleBarMode, no menu native source. So the desktop themes' `commandBehavior: Native` and `desktopTitleBarMode: native` both had to be normalised away there, and the commands stayed in the Codename One Toolbar. Correct, and not what a desktop application looks like. Windows gets a Win32 HMENU hung on the application window with SetMenu; Linux gets a GtkMenuBar packed above the drawing area. Both take the row format IOSImplementation.setNativeCommands already writes for the macOS menu -- "<menuHint>\t<label>\t<shortcutKeyChar>\t<shortcutModifiers>\t<commandId>" -- so the three ParparVM desktop ports share one encoding rather than each inventing its own alongside Command's placement constants. Decisions worth recording, because none of them is forced by the API: **Where About, Preferences and Quit go.** Neither platform has an application menu, so the macOS mapping does not transfer: About goes under Help and the other two under File, which is where a Windows or GNOME user looks. That is why neither hint table is a copy of the macOS one. **Win32 menu ids are not command ids.** They are 16 bit and share a space with control notifications, so the ids handed to Win32 come from a private base and are mapped back. A Codename One command id is a 32-bit counter that would start colliding with WM_COMMAND's control notifications the moment it passed 0xFFFF. The high word is also checked before the range, so a notification whose control id happens to land in the range is not mistaken for a menu item. **Accelerators are drawn by hand on Windows and by GTK on Linux.** Win32 draws nothing itself -- the text after a tab IS the accelerator display -- so a shortcut not spelled into the label would respond and show nowhere. GTK's accel group draws it and binds it in one call. **Both rebuilds are BLOCKING hand-offs to the UI thread**, and that is load bearing twice over. SetMenu and GTK are not legal from the EDT, so a marshal is required either way -- but stringToUTF8 returns this thread's scratch buffer, which the next conversion on this thread overwrites, so a posted message would read it after it had moved on. A blocking send cannot: nothing else runs on this thread until it returns. **The Linux window gained a GtkBox between the window and the overlay**, and it stays EMPTY until commands arrive. A box with one child that expands lays the overlay out exactly as it was laid out as the window's direct child, so an application that publishes no commands renders identically to before -- which is what keeps the screenshot baselines of every such app untouched. **A selection goes back through the ordinary event queue** as CN1_EVENT_MENU_COMMAND, on the same number in both ports so the two desktop wire protocols do not drift apart. That is what puts the command on the EDT rather than on the pump or GTK thread. Each port keeps one superseded generation of its command map, because a selection can be drained after the form that published it has been replaced -- without it, the command a user clicked on the way out of a screen resolves to nothing and silently does not run. The C and C++ here have never been compiled: neither toolchain exists on the machine this was written on, so CI is the first verifier. What could be checked was: scripts/check-native-signatures.sh resolves all 483 Windows and 481 Linux natives including the two new ones, and both natives sit INSIDE their file's extern "C" block -- the exact mistake this PR series already shipped once in cn1_windows_window.cpp, where the name was right, the verifier passed, and only a real Windows build reported the undefined symbol. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Compared 12 screenshots: 12 matched. |
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
The first capture run answered two questions this could only guess at, and both answers narrow a row rather than widen it. **A WinUI ScrollBar has no highlight state a capture can ask for.** None of PointerOver, UncheckedPointerOver, CheckedPointerOver or MouseOver is a visual state of that control, and neither is Pressed or Dragging -- the capture app's blocker said so by name rather than writing six tiles identical to normal. GTK can state it: PRELIGHT and ACTIVE are what the CSS pseudo-classes resolve from, and the captured GNOME hover and pressed tiles are genuinely different from their normal one. So the resting bar is one row scored on Windows and GNOME, and the highlight is a second row scored on GNOME alone -- which is the only place all three of "the reference renders", "the reference can be put in the state" and "the state is visible" are true at once. **A WinUI ListViewItem draws its own pointer-over chrome.** It goes through ListViewItemPresenter, which paints rather than exposing a visual state, so hover is dropped from the row. Selected is a real property on all three and is still scored. Neither is a theme problem and neither is fixable by tuning, which is why both are recorded in the gap table with what was measured rather than left to be rediscovered by the next person who wonders why the scrollbar highlight is not scored on Windows. macOS and GNOME captured cleanly on that run -- 92 and 104 tiles, no blockers -- so this is the only change the run asked for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neither is reachable on a healthy run, which is exactly why they would have sat there. **Linux created a fresh GtkAccelGroup on every rebuild** and added it to the window. The menu items go away with the bar, but the groups do not: a window would end up holding one group per form that had ever published commands. Created once and reused. **Windows leaked a popup whose title failed to widen.** The popup was created before the label conversion and only appended to the bar afterwards, so a failed conversion left it allocated and unreachable -- DestroyMenu on the bar cannot reach a menu that was never appended to it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cloudflare Preview
|
Both found by scoring the CN1 side against the first CI capture and then looking at the tiles, which is the step the golden protocol asks for and the only one that catches this class of problem: the numbers were plausible, and one of them was measuring a control cut in half. **NSTabView was clipped through its own tab strip.** Left to its fitting size it is taller than the tile, so the pill came out sliced along its top edge. Full height now, like the group box. A reference that is cut in half measures nothing, whatever the number underneath says. **A selected NSTableRowView drew the unemphasized grey.** Outside a focused table that is the default, and grey is what macOS shows for a selection in a window the user is NOT working in -- not what a selected row looks like while they are. The CN1 row is correctly accent-filled, so the comparison scored a correct style against a reference in the wrong state: 67%. isEmphasized makes it the accent fill in both appearances. Neither is a theme finding and neither would have been visible from the score alone. Also corrects DesktopModeScreenshotTest's note, which said Windows and Linux have no native menu bar. They do now, so the commands leave the raster on every desktop port and the Toolbar-stays branch is asserted by DesktopChromeTest rather than by a screenshot no port produces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A WinUI TabView is a document-tab control and puts a close affordance on every tab. A Codename One Tabs has no such thing, so the tile compared two tabs against two tabs plus two buttons and charged the difference to the theme. IsClosable false on both items. Found by looking at the captured tiles, which is the review step the golden protocol asks for -- the score alone would have read as "the Fluent tab styling needs work". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
112 new tiles across the three desktop sets, captured by fidelity-desktop-native-ref.yml run 35370740065 on the hosted runners -- never locally, so no developer's accent colour, appearance or installed fonts are baked in. Every new tile was reviewed by eye before this commit, which is the step that found the four reference bugs fixed in the commits above it. Set sizes now match their manifests exactly: macOS 88, GNOME 104, Windows 100. macOS and GNOME carry fewer rows than Windows, and that is the recorded per-platform scoping, not a short capture -- NSMenu and both tooltips are window-server surfaces, and AppKit cannot render a scrollbar into a view at all. Reproducibility, checked by comparing this run against the previous one: - GNOME reproduced BYTE-FOR-BYTE across two independent runs, all 104 tiles including the manifest. So nondeterminism is not a property of the suite. - macOS differed on exactly the four tiles the reference fixes targeted and nowhere else, 84 of 88 identical. - Windows differed on three tiles beyond the intended one, and all three are the documented residual: 2-3 pixels, +/-1 in a channel, on an anti-aliased edge. The README said that residual was the slider thumb; it is now measured on the tooltip's rounded border too, so it is recorded as a property of anti-aliased edges on that runner rather than of one control. One file is deliberately NOT in the Windows set. The capture also writes Button_normal_light.png, the BitBlt self-check that proves the Mica backdrop reached the window; it is excluded from tiles_written because it is not a tile, has no CN1 counterpart, and would make the golden count disagree with the number of pairs the gate can score. No baselines here. Those are recorded separately, from the runs that SCORE these, so the commit that defines the goldens and the commit that defines the ratchet stay two reviewable changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Exit 139, partway through the dark pass, several widgets after the menu bar tile had been written -- and the two runs before it completed all 104 tiles from the same binary. That shape is a deferred free: the damage is done at teardown of one widget and surfaces later. The menu bar was the only new widget handing a GObject back to GTK and then dropping its own reference. GtkPopoverMenuBar keeps the model it was built from and rebuilds its items from it while it lives, and the popovers it creates hold the submenu, so unreffing both made the lifetime depend on GTK's teardown order rather than on ours. The models are now held for the life of the process: two objects in a tool that writes a hundred PNGs and exits, against a capture that fails one run in three. That is a diagnosis from the crash's shape rather than from a backtrace, because there was no backtrace to read -- which is the second half of this change. A fatal-signal handler now prints the frames and re-raises, so the shell still sees the real signal and the job still fails, and the next occurrence says where instead of only that. backtrace_symbols_fd rather than backtrace_symbols: it writes straight to the fd and allocates nothing, which is what makes it safe from a signal handler. The build gains -g and -rdynamic, without which the frames print as bare addresses. If it recurs the handler will say so plainly; it will not be quietly retried. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
MacOSStubThemeTest required the generated stub to call setIosMode("modern"), and the theme
flip broke it -- correctly, since the default is aqua now. But the literal was never what
the test was guarding. Its own comment says so: the point was that iOS7Theme.res declares no
@darkModeBool, so a stub defaulting to it leaves an application with no dark mode however
carefully it asks, and every dark screenshot comes out light. "modern" was simply the theme
that happened to satisfy that.
So it is now two tests. One pins the value, because the value is a decision worth pinning.
The other names the property -- the default is never the theme with no dark mode -- so the
next person to move this default is told what it has to keep rather than reading a literal
and guessing. Aqua declares the constant too, which DesktopNativeThemeContentTest asserts
against the compiled resource rather than the CSS.
The flip also made a latent hazard load bearing, so that gets a test as well.
installNativeTheme() asks for its resource by name at run time and falls back to the legacy
theme WITHOUT SAYING SO when the lookup returns null -- the file's own comment warns about
it. While the default was modern, iOSModernTheme.res was the only theme anyone staged, so
nothing could go wrong; with the default on aqua, a MacOSAquaTheme.res that fails to reach
buildinRes is a silent revert to the iOS 7 look on every macOS build. stageThemeResources
copies every .res it finds, so the flip is safe today, and now something says so.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Compared 172 screenshots: 172 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 157 screenshots: 157 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
|
Compared 172 screenshots: 172 matched. Benchmark ResultsDetailed Performance Metrics
|
The desktop themes set defaultNativeWindowModeBool, and the Windows port's screenshot suite
then captured two of LightweightPickerButtons' four placements, found the two it did capture
byte-identical, and timed out waiting for the rest.
The lightweight Picker positions its popup by hand -- setX/setY/setWidth/setHeight, then
show(top, bottom, left, right) -- and native window mode documents exactly those margins as
ignored. So in a window the popup comes out centred and every placement variant collapses
onto the same picture.
This is a bug in the native-window-dialog feature rather than in the theme default: an
application calling Dialog.setDefaultNativeWindowMode(true) hit it just as hard. ComboBox and
InfiniteProgress had already opted out by hand, so the hazard was known -- it had simply
never been reachable, because nothing defaulted the mode to true. Six more sites had not:
TooltipManager, both Toolbar side menus, both Picker popups, Validator and
FloatingActionButton.
There is no choke point to fix it at. Dialog.usesNativeWindow() already exempts menus and
anchored popups, but AbstractDialog is an interface whose own comment forbids new members --
the core is Java 5, so a new method breaks every implementation and throws AbstractMethodError
on the compiled ones. So each site says so itself, and a test enumerates them.
That test reads SOURCE, and the first version of it did not:
The obvious runtime test walks the current form looking for a popup that wants a window.
It passed with every fix removed. Reaching one of these popups means pressing the control
that owns it, which parks the caller, so the popup does not exist while the test can look.
A probe that cannot fail proves nothing.
What it counts is constructions against opt-outs, per file, because the bug that started
this was the SECOND of a pair being missed -- and it immediately found a third: Validator
builds two, and the one I had fixed was not the field initialiser that is used until a
constraint supplies its own message.
It also strips comments first, because BubbleTransition shows a dialog in a javadoc EXAMPLE
and counting that reported a file with no dialogs in it as an unclassified builder. A check
whose findings have to be filtered by hand stops being read.
A second test fails when a new framework source starts building one of these and is in
neither list, so the classification stays a decision rather than a default. Seven existing
sources were classified by it: the file choosers, crash reporter, signature pad, share sheet
and country list are dialogs the USER operates and correctly take the default, and MenuBar is
exempt by mechanism because usesNativeWindow() already refuses a window for a menu.
Also lands the fidelity baselines from run 35371376318 -- 86 macOS, 102 GNOME, 98 Windows
pairs, all with geometry, means 84.0 / 83.8 / 85.3 against 84.6 / 85.9 / 82.1 for the
nine-row matrix. And drops DesktopSeparator, because the gate refused to baseline it and was
right to: a 1px hairline a few levels off the surface does not clear the comparator's content
threshold, so the tile is 98.2% backdrop holding exactly TWO colours, geometry comes back
{"empty": true}, and shape and size agreement reach 1.0 because two empty masks agree
perfectly. Windows light scored 96.45% on a comparison that had looked at nothing. The
component is still themed and still asserted; it is the overlay comparison that cannot see it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Compared 223 screenshots: 223 matched. |
…open Found by looking at a captured screenshot rather than at a score. TabsTheme_light on the Linux port came back as three plain boxes with hard borders and no indication whatever of which tab was open -- and the DesktopTabs fidelity row had scored 35-68%, the worst in the set, which I had written down as "CN1's tabs do not look like a native tab control". That diagnosis was wrong. Tabs writes the `Tab` UIID onto every tab button and marks the open one with that button's own SELECTED style. These themes styled `SelectedTab` and `UnselectedTab`, which nothing in the framework writes at all -- I added them on the assumption that the names meant what they say. Dead rules. With nothing styling `Tab`, the strip fell through to UIManager.resetThemeProps, which seeds `Tab.sel#derive: Tab`. That seed makes the selected tab derive from the unselected one, so the two are pixel-identical by construction. A tab strip that cannot show which tab is open is a usability defect, not a fidelity gap, and no amount of tuning the score would have found it. `Tab` is now styled per platform, with the selected treatment each one actually uses: a card fill on Fluent, the accent pill on Aqua, an accent rule under the label on Adwaita. Plus `TabbedPane`, `TabsContainer` and `TabsContainerHost`, which `Tabs` also names and which no desktop theme defined either. The test asserts the DIFFERENCE, not the key, and for the same reason the scrollbar highlight does: `Tab.sel#` is present in every compiled theme whether or not anything styled it, because the framework seeds it. Present-and-equal IS the defect. Verified to bite by setting the selected rule back to the base colours and watching it fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Compared 150 screenshots: 150 matched. |
Its javadoc says the golden "shows the dialog centred on the window with the window's content dimmed behind it, which is only possible if both are on the same surface" -- that is the HOSTED path: the window's layered pane, the scrim, isTopmostHostedDialog, a real amount of code. The desktop themes set defaultNativeWindowModeBool, so the dialog now opens as a separate operating system window, leaves the captured raster entirely, and the golden becomes an empty host window. Correct behaviour for that mode, and no longer a test of anything. Found by scanning the reseed captures for screens that had gone uniform rather than by reading a diff: Window-Dialog-900x700 came back 99.7% a single colour, and the control run over the committed baselines showed it had not been near the top of that list before. The other Window-* screens were already sparse for their own reasons, which is why the control mattered -- high uniformity is normal in this suite and only the CHANGE is a signal. Pinned to the hosted path, so it keeps covering the code it was written for. An application can still ask for that mode, so this is a supported configuration and not a workaround. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Scoring the macOS leg after the Tab styling landed separates two things the single 35-68% number had been hiding. The DEFECT is fixed: the selected tab is now visibly selected, and the light row moved 68 -> 82. Confirmed by looking at the tiles rather than by the number -- CN1 now draws a filled "One" against a plain "Two". What remains is SHAPE, and it is not a tuning problem. CN1 draws a left-aligned row of tabs; AppKit draws a centred rounded pill inside a grey track. The dark row is still 49%, and the geometry moved FURTHER from native -- height ratio 0.53 -> 0.25 -- because the styled tab is shorter than the unstyled full-height box it replaced. Recorded as wanting a per-platform tab shape rather than more colour tuning, so the next person does not read the improved light score as the row being done. The run also reported seven sub-1% drops on TextField and GroupBox and a handful of AccentButton width drifts. Those are NOT acted on: the baseline was recorded on the CI runner and this scoring was local, which is the disagreement goldens/README.md already documents in the other direction -- a Mac-recorded baseline failed the gnome gate on eighteen pairs over a slider one pixel taller on Linux. A sub-1% delta between a local render and a runner-recorded baseline is that, not a regression. The baseline re-record itself therefore waits for the dispatched CI scoring run, from the runner that scores it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CI scoring run confirmed the Tab fix is real and large -- GNOME dark 35.73% -> 73.09%, light 64.67% -> 75.75%, with no score regressions anywhere. It also showed the geometry moving the wrong way: width ratio 1.0 -> 0.64 and the centre offset 12.5px -> 43.6px. That is a consequence of the fix rather than a separate bug. A Tab is transparent until it is selected, so once the tabs were styled the only content in the tile was the selected tab's fill and the two labels -- the comparator's bounding box shrank to the part that paints. GtkNotebook and a WinUI TabView both draw a divider under the tab strip, and neither theme had one. Adding it fixes the look and the measurement together, which is the only kind of change worth making to a geometry number: drawing the line the platform actually draws, not padding something out until a ratio improves. Aqua deliberately gets no such rule. NSTabView's pill sits on the bare window background, and for that theme a narrower bounding box is CORRECT -- its native reference is a centred pill, not a full-width strip, so the ratio moving away from 1.0 is the CN1 side getting closer to the reference rather than further. Worth recording from the same run: the seven sub-1% "regressions" my LOCAL scoring reported on TextField and GroupBox do not appear on the runner at all. That is the local-versus-runner disagreement goldens/README.md documents, measured from the other side this time, and it is why the baseline re-record has to come from the runner. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cceeded
Found by looking at the macOS reseed captures rather than at their scores. Every animation
filmstrip -- SmoothScroll, AnimateLayout, AnimateUnlayout, SheetSlideUp, three
StickyHeader variants, TabsAnimatedIndicator, TensileBounce, PullToRefreshSpinner -- came
back as a grid of empty cells in the host Form's background colour. Master's macOS leg is
green, so this is new.
The reason it has to be caught by eye is the whole point: an empty grid is still a picture.
The capture succeeded, the comparison ran, the gate reported a mismatch like any other, and
reseeding would have recorded six blank cells as the new truth.
**Root cause, reproduced in 0.06s.** Not macOS, and not the theme:
Container.layoutContainer() { if (shouldLayout) { shouldLayout = false; doLayout(); } }
These captures build a Form off-screen and size it with setWidth/setHeight, which are raw
setters that invalidate nothing. A Form sized that way is still "laid out" -- at the size
it had when it was built, which is the display size -- so layoutContainer() returns without
doing anything.
They got away with it by accident. Adding a child marks the CONTENT PANE dirty and the flag
propagates upward, but Container.setShouldLayout returns early when the value is unchanged,
so whether the FORM itself got marked depended on something else having touched it.
Attaching the Toolbar was that something else. Desktop "native" title bar mode never
attaches the Toolbar -- the title goes to the OS window, the commands to the native menu --
and with that one incidental invalidation gone the content pane kept 1080x1920 while the
form was 400x300, and every child laid out at 0x0.
Measured, native mode vs toolbar mode on the same form:
contentPane=1080x1920 content=0x0 tile=0x0 <- native, broken
contentPane=400x278 content=400x278 tile=396x40 <- toolbar, fine
**Fix:** forceRevalidate(), the public API for "things changed underneath, lay this out
again", which does not depend on anything else having marked the tree. One helper,
BaseTest.layoutOffScreen, at the choke point all nine host-form captures share.
**Guard:** a filmstrip whose every frame is a single flat colour now fails the test instead
of emitting. Validated as a discriminator against the real captures, not asserted: the
broken macOS grid has exactly 1 colour in all six cells, the good Windows grid has 7 to 32.
All six rather than any one, because a single flat frame can be legitimate at the end of an
animation and six cannot. It records the message and fails after the emit rather than
during the compose -- fail() calls done(), and finalising mid-compose would end the test
before its image is sent, which is the late-emit bug the DualAppearance gate exists for.
OffscreenFormPaintTest keeps this honest in core, and it took two tries to be a real test.
The first version passed in both modes with the bug present, because a Form only gets a
Toolbar when the global toolbar is on, so Toolbar.initMenuBar never ran and the branch
under test was never reached. It now asserts the Toolbar exists before asserting anything
about it, and carries the toolbar-mode control so a future failure says whether off-screen
painting broke generally or only in the native path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two checks failed on them and both say the same thing: a test the suite registers but no
feature maps is a test that appears in no port's report. "Validate port status contract"
fails outright, and the website build fails because every one of the twelve persisted
reports is then unusable and falls back to the checked-in copy.
Placed by what they actually exercise rather than all in one bucket, and each feature's
description updated to say so -- the descriptions are published on the port status page,
so a feature that silently grew a test would describe itself wrongly:
native-theme-controls <- DesktopWidgetsTheme (separator, group box, stepper, link,
search field, accordion header)
scrolling-and-pull <- DesktopScrollbarTheme (the scrollbar, idle/hover/dragged)
desktop-mode <- DesktopChromeTheme (context menu, tooltip, command area)
No test_scopes entry: these are plain Codename One rendering with no platform dependency,
so every port runs them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Prose only; the sentences are unchanged. The earlier edit spliced new text onto an existing line and left one paragraph broken across a phrase. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…did not have The component matrix has listed DesktopSeparator since the desktop suite was written. The row was never in fidelity-tests.yaml, so nothing scored it -- and nothing noticed, because check-fidelity-spec.py reconciles the yaml against the renderer and the three reference apps and has no reason to read a markdown table. Worth having rather than tidying the table instead: a separator is nothing but a colour and a thickness, so a theme that gets either wrong has no other symptom anywhere. It is also one of the three components this PR added, and the other two (GroupBox, Stepper) were already scored, which left the new widget with the least visible failure mode as the only one unmeasured. The tile is 24px rather than the default. A taller one would be almost entirely background on both sides and would score high whatever the rule says, which is the failure mode this row exists to catch. The matrix entry named MenuFlyoutSeparator for Windows. That is a menu primitive carrying menu insets, so it would measure the wrong thing; WinUI has no content separator, and the platform's own settings pages draw a one-pixel Border in DividerStrokeColorDefaultBrush. The table now says what the reference actually builds. AppKit and GTK have the real control -- NSBox in .separator mode and GtkSeparator -- so both take it from the stylesheet, which is the point. DesktopDialog, the other row the plan listed, is deliberately still absent: a ContentDialog, an NSAlert and an AdwMessageDialog are all windows, and these references capture through cacheDisplay / GtkWidgetPaintable, which see widgets in a view and not window-server surfaces. It would be a row with no reference on any platform -- a golden scoring 0% forever and reading as a theme bug, which is the call already made for the menus and the tooltip. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six per directory -- three tests, light and dark -- from the runs that captured them
(linux 35388615999, windows-port-x64 35388616256), seeded per each screenshots/README.md.
Reviewed rather than accepted, and the review is the reason these are worth having:
DesktopScrollbarTheme the three thumbs measure #8a8a8a / #767676 / #5d5d5d on Fluent
and #b8b4b0 / #918d88 / #6f6b66 on Adwaita -- an exact match for
each theme's DesktopScrollThumb, .selected and .pressed. This is
the change's headline behaviour and it had no capture anywhere
before.
DesktopWidgetsTheme the clamped stepper's decrement glyph measures 93 against the
enabled 26, so StepperButton.disabled really does render; both
separators appear as full-width rows at the theme's own colour.
DesktopChromeTheme the menu and tooltip are opaque over the deliberate textured
backdrop, Command.selected marks the focused row and
Command.disabled the unavailable one, and DialogButtonDefault is
distinguishable from DialogButton.
Nothing else in these three directories moved. That is worth stating: the previous commit
changed how every off-screen host Form is laid out, and if that had altered what the ten
animation filmstrips draw, their goldens would have shown it here. They are byte-identical,
on both Linux architectures and on Windows, and Android reports the same tests still
matching their stored references. The fix restores a layout these ports were already
getting; it does not change one.
Windows is seeded from windows-port-x64 alone, as before -- three legs compare against that
one directory and agree through tolerances, not byte identity.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
✅ ByteCodeTranslator Quality ReportTest & Coverage
Benchmark Results
Static Analysis
Generated automatically by the PR CI workflow. |
…s scrollbar Thirteen files from run 35388616661. Twelve are new: the JavaScript suite captures every DualAppearance test under both the default and the iOS theme, so a test contributes four goldens here rather than two -- the _ios_ pairs are not a duplicate to be trimmed. DesktopMode is the one existing golden that moves, and it is worth saying exactly what moved, because "a desktop screenshot changed on the JavaScript port" invites the wrong conclusion. The difference is a single 30-pixel-wide strip down the right edge, full height (bbox 714,134 - 744,1334) and nothing else: the desktop scrollbar that desktop.interactiveScrollbars now draws. Measured against the committed golden on two separate runs, at the old head and this one, with the same bounding box both times. That hint is in the application's own settings and applies to every port it builds, not only the three that install a native theme -- which is the intended reach. The toolbar is NOT hidden here: the capture still draws its title and hamburger, because the desktop native title bar needs a port that reports a native menu bar and the JavaScript port does not claim one. Checked for the failure mode this suite actually produces before committing: none of the twelve is degenerate. The chrome pair carries ~2500 colours, the widgets ~1100, and the scrollbar ~700 at 95% one colour, which is correct for three thin thumbs on a plain background rather than a blank capture. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Registering the tests under features was only half of it. Each golden NAME also needs a screenshot_mapping, or the contract cannot say which test produced it: "Golden screenshot DesktopChromeTheme_dark is not mapped to a test". Caught by running the validator locally before pushing rather than by CI, which is the point of it being runnable locally. Three glob patterns, matching the shape the other DualAppearance tests already use, so the _light/_dark pair and the JavaScript port's extra _ios_light/_ios_dark pair are all covered by one entry each. Golden names known to the contract: 320 -> 332. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d from CI The Android leg reports a screenshot with no stored golden as a missing reference and fails, which is right. It also writes that screenshot to artifacts/ -- "Stored PNG artifact copy at ..." appears in its own log for every one. The upload step then took artifacts/connectedAndroidTest*.log and nothing else, so those PNGs died with the runner. The effect is a dead end rather than an inconvenience: a new suite test can be added, captured correctly on Android and reported as missing there on every run afterwards, with no way to obtain its golden from CI at all. The only route left was an emulator on somebody's desk, which is why scripts/android/screenshots has no README describing a seeding protocol the way the other golden directories do. Both legs upload them now -- the default one and the JDK matrix one, which differ in what they add and should never differ in what they keep. This is the third golden set with its own shape. Linux and Windows upload every capture as a raw artifact; macOS uploads only the captures that did NOT match, because a passing one is deleted; Android uploaded none. Only the first of those is a straight overwrite on a reseed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Compared 193 screenshots: 193 matched. |
…ness
on_ready takes `probe_widget = gtk_widget_get_first_child(content)` before the capture
loop, then hands it to write_manifest afterwards. The loop builds a tile into `content`
and destroys it for every spec and every state, so by the time write_manifest calls
gtk_widget_get_pango_context on that pointer it is reading freed memory. The `? :` guard
beside it cannot help: the pointer is not null, it is stale.
That is the segfault recorded earlier in this branch as happening "once in three runs". It
was never random -- it depended on what the allocator had put back in that memory. Adding
DesktopSeparator changed the allocation pattern enough that it fired on every run, which is
the only reason it became findable instead of being re-run until it passed.
Backtrace from run 35395267870, immediately after the last tile was written:
NATIVEREF:BLOCKER fatal signal, backtrace follows
native-ref(+0x5bde)
libgtk-4.so.1(gtk_widget_get_pango_context+0x25)
native-ref(+0x5cb3) <- resolved_font
native-ref(+0x6d2b) <- write_manifest
Re-fetched at the point of use instead. If the loop left `content` empty the fetch returns
NULL and the existing fallback to the window takes over, which is what the guard was
always meant to express.
The Windows and macOS captures in the same run succeeded; only GNOME holds a widget across
its capture loop.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ride that only moved one side
From run 35395267870. Both manifests are clean and list no blockers; the GNOME leg of that
run crashed before finishing its manifest, so its tiles are deliberately not promoted here
and follow once the dangling-pointer fix has produced a clean capture.
The tiles are exactly what a separator reference should be -- 240x56, two colours, a
240-pixel rule on the window background:
macos-aqua light #e7e7e7 bg #d0d0d0 rule
macos-aqua dark #262626 bg #3b3b3b rule
fluent light #f3f3f3 bg #e5e5e5 rule
fluent dark #202020 bg #1e1e1e rule
Fluent's light rule is #e5e5e5, which is exactly what the theme's Separator UIID already
declares. Its dark rule is #1e1e1e on a #202020 background, very nearly invisible -- that
is Fluent's real dark divider and not a broken capture.
The row asked for tile_height_px: 24, on the reasoning that a taller tile is mostly
background and would score high whatever the rule says. Removed, because it would have
scored a comparison between two different things: tile_height_px is documented as a
per-component override and the CN1 side honours it, but all three reference apps mirror
only the DEFAULTS -- TILE_W and TILE_H are constants in each. CN1 would have rendered
240x24 against a 240x56 reference. No desktop row had ever used a per-component override,
so nothing had exercised that path; the proof is this row's own first capture, where the
yaml asked for 24 and all three references produced 240x56 regardless.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six from run 35395172939, the leg that reported them missing. Catalyst is not one of the three ports that install a desktop native theme, so these capture the iOS-derived theme rendering the same UIIDs -- which is the convention this suite already follows: DesktopModeScreenshotTest has a golden in all seven port directories, not only the desktop ones. Checked against the failure this suite actually produces rather than accepted: chrome ~3500 colours, widgets ~1390, scrollbar ~746 at 94% one colour, which is right for three thin thumbs on a plain background. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… for it Both loops exit as soon as the number of PNGs on disk reaches EXPECTED, and EXPECTED counts the GOLDENS in the reference directory. Any run that captures more screenshots than there are goldens therefore reaches the threshold while earlier captures are still arriving -- and that is every run that adds a test, because the new one has no golden yet. The directory is snapshotted mid-stream and everything that had not landed is reported as "Actual screenshot missing (test did not produce output)", naming tests that ran perfectly. Measured on tvOS run 35395173010: 22:34:33 [cn1ss] Test 'DesktopMode': Actual screenshot missing (test did not produce output). 22:34:33 [cn1ss] Test 'Media360Panorama': Actual screenshot missing (test did not produce output). 22:35:04 [cn1ss-ws-server] test=DesktopMode png_bytes=163304 status=ok 22:35:04 [cn1ss-ws-server] test=Media360Panorama png_bytes=403545 status=ok Both were delivered, intact, thirty seconds after being declared missing. Six new captures with no goldens had pushed the count over EXPECTED six screenshots early. This is also a bootstrap trap, which is the part worth fixing rather than working around: a new golden cannot be seeded on these two ports, because the wait ends before the capture that would seed it arrives. The count now has to have STOPPED CHANGING as well as reached EXPECTED. While captures are still streaming it keeps rising and the condition never fires; once it plateaus, the existing two confirmations give the final writes their flush window. Costs at most one more eight-second poll on a normal run. Both scripts, because both had the identical loop and a fix in one would have left the other reporting the same false missing screenshots. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six at 3840x2160 from run 35395173010. Same profile as every other port -- chrome ~4000 colours, widgets ~1650, scrollbar ~770 at 94% one colour -- so none is a blank capture. tvOS renders these through the iOS-derived theme, as Catalyst does. They are committed for the same reason: this suite gives every port a golden for every test it runs, which is why DesktopModeScreenshotTest has one in all seven directories rather than only the desktop three. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
161 captures: the 155 the run produced plus the six new desktop tests. Held back through three earlier rounds because ten animation filmstrips were capturing six empty cells, which would have been reseeded as the new truth. They are not any more: AnimateLayout [665, 823, 822, 837, 858, 866] distinct colours per cell SmoothScroll [1106, 1364, 1208, 1372, 1391, 1360] StickyHeader [1225, 1474, 1358, 1342, 1384, 1362] ... all-blank filmstrips: 0 (was 10) AnimateUnlayout ends [665, 734, 602, 542, 541, 518] rather than climbing, and its Linux and Windows goldens do the same thing -- [43, 37, 21, 9, 1, 1] and [7, 6, 5, 2, 1, 1]. An unlayout animation empties the screen, so its last frames are legitimately blank. That is why the guard added with the fix requires ALL SIX frames flat and not any one of them: a per-frame rule would fail these three tests on every port forever. Five goldens are deliberately NOT replaced. The macOS artifact carries only captures that failed comparison -- cn1ss deletes a passing one outright -- so BrowserComponent, LottieAnimated, MotionShowcase, SVGAnimated and VideoIODecodedFrames are absent because they PASSED. Each was resolved to its test in port-status-macos.json and confirmed "pass" before being left in place; absence alone is still refused. Reviewed by comparing every changed file against its predecessor for the failure this suite produces. Twelve looked suspicious and all twelve are explained, none by "it is probably fine": nine graphics-* tiles lost ~200 colours because their antialiased TITLE moved into the OS window under desktop native title-bar mode while the drawing itself is pixel-identical, and MainActivity, RichTextArea and PullToRefreshSpinner read flatter only because Aqua's background is a large flat field -- all three still carry their content, at 751-936 colours per cell in PullToRefreshSpinner's case against the old golden's 1020-1203. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six each from run 35395173010, at the two ports' own geometries -- 1179x2556 for the phone, 416x496 for the watch. Healthy on both: chrome ~4500 / ~1550 colours, widgets ~1700 / ~1100, scrollbar ~830 / ~255 at 90% one colour, which is right for three thin thumbs on a plain field and not a blank capture. These are the last two of the eight golden directories the three tests reach. Only Android is left, and only because its captures could not be retrieved from CI at all until the upload fix earlier in this branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FadeTransitionTest came back from run 35407477083 as: failed: FadeTransitionTest produced 6 frames and every one of them is a single flat colour. The animation host painted its background and none of its children. That is the guard added with the layout fix, firing on a case the fix itself missed, on the first run after it existed. Worth stating plainly: the sweep was wrong and the check caught it, which is the entire reason the check is there. The sweep was wrong in a specific and avoidable way. It looked for files calling BOTH setWidth and layoutContainer, on the assumption that a broken call site would have a layoutContainer to correct. AbstractTransitionScreenshotTest never calls layoutContainer at all -- it builds two off-screen forms, sizes both with the raw setters and relies entirely on the incidental invalidation -- so it matched neither half of the pattern and was invisible to a grep shaped that way. Enumerating every file that calls setWidth and looking at what each one does with it finds it immediately; that is the enumeration this should have been from the start. Thirteen tests share this base. Only FadeTransitionTest went fully blank because paintBookendDirectly draws the first and last frames outside the form path, so the others kept some content and stayed under the all-six-frames threshold -- they were rendering four empty middle frames and would have gone on doing it. Both forms get layoutOffScreen, after their content is built. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t reach Six at 320x640 from run 35407476926 -- the first Android run after the upload fix, and the first time these captures have ever left a runner. Before it the Android leg wrote them to artifacts/ and uploaded only the logs, so the set was unreachable from CI by construction. Healthy on the same measures as every other port: chrome ~1830 colours, widgets ~1000, scrollbar ~582 at 93% one colour. That completes ten of the eleven golden directories. The last is scripts/ios/screenshots, the OpenGL lane, which deliberately does not run on pull requests -- it costs ~40 minutes on a serialised macOS chain -- but does run on master pushes and nightly against 143 live baselines. Merging without its goldens would turn master red, so it has been dispatched explicitly rather than discovered after the fact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gate checks added and MODIFIED files, and this one had no header -- like the nine in the earlier sweep, it predates the rule. Missed locally because the check ran before git add: --base/--head compare commits, so a change still in the working tree is not in HEAD and is not examined. Run it after committing, or the answer is about the previous commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…on itself The old golden showed no Toolbar at all. The new one shows the hamburger and the title, which is the correct rendering for this port and the direct consequence of a change already made in this branch: DesktopModeScreenshotTest used to switch desktop mode on for itself and off again in done(), so its capture showed desktop chrome on every port regardless of what that port actually reports. It does not any more -- the suite-wide settings decide, and keeping the local opt-in would have hidden a regression in them rather than demonstrated a feature. Mac Catalyst answers CN.isDesktop() == false: it is an iPad application hosted on a Mac, not a desktop port, and none of the three ports that install a desktop native theme is Catalyst. So the Toolbar stays and its commands live in the side menu, which is exactly what the test's own class comment says a non-desktop port must show. Not the same change as the JavaScript port's DesktopMode, despite the shared name: there the difference was a 30-pixel strip down the right edge and nothing else, because the JavaScript port keeps its Toolbar too and only gained the desktop scrollbar. Here the whole frame moves, because the toolbar itself comes back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The twelve tests sharing AbstractTransitionScreenshotTest all moved, and only on macOS. That is the shape the whole layout bug has had throughout: the incidental invalidation these captures relied on came from attaching the Toolbar, macOS is the port that hides it under desktop native title-bar mode, and the other ports never lost it. Linux, Windows and Android all passed this same round without a single transition golden changing. What changed is content arriving where there was none. Per grid cell, before and after: FadeTransition [327, 449, 456, 455, 475, 521] -> [645, 695, 755, 768, 794, 838] SlideHorizontalTransition [327, 451, 458, 456, 477, 521] -> [645, 804, 807, 871, 881, 838] FlipTransition [327, 437, 459, 259, 475, 521] -> [645, 770, 792, 423, 794, 838] Every cell gains, and the minimum across all twelve is now 319 distinct colours with no all-blank frame anywhere. These twelve were not caught by the blank-filmstrip guard and could not have been: only FadeTransition went fully blank, because paintBookendDirectly draws the first and last frames outside the form path, so the other eleven always had two real frames and sat under the all-six threshold. They were rendering four empty middle frames and their goldens recorded it. The guard is deliberately not tightened to catch them -- a per-frame rule would fail AnimateUnlayout and its two siblings on every port forever, since an unlayout animation legitimately ends blank. Seeded from run 35409267568, the run that reported them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… last set Six at 1179x2556 from the dispatched run 35407477609. Healthy on the same measures as the other ten: chrome ~5000 colours, widgets ~1800, scrollbar ~520 at 90% one colour. This set exists only because it was gone looking for. The OpenGL lane deliberately does not run on pull requests -- it costs about 40 minutes on a serialised macOS chain and Metal is the default backend -- but it does run on master pushes and nightly, against 143 live baselines. Merging without these six would have turned master red on the first push after the merge, with nothing on the PR having ever said so. The lane reported exactly three failures, all of them these missing references, and nothing else. FadeTransitionTest passed here, which is the expected answer: the off-screen layout bug needed a port that hides its Toolbar under desktop native title-bar mode, and iOS is not one. All eleven golden directories now carry the three new tests: linux/screenshots, linux/screenshots-arm, windows/screenshots, macos/screenshots, mac-catalyst/screenshots, javascript/screenshots (12 -- two themes), ios/screenshots, ios/screenshots-metal, ios/screenshots-watch, ios/screenshots-tv, android/screenshots Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The twelve transition tests moved on tvOS too. I said earlier that only macOS moved; that
was premature -- Linux, Windows and Android had reported and tvOS had not.
It is the same fix with a different visible result, and the difference is worth recording
because "nothing appeared" makes it look like noise. The colour count per grid cell is
unchanged, in two of the three sampled tests to the exact number:
FadeTransition old [1303, 1182, 1359, 1343, 1449, 1495]
new [1303, 1182, 1359, 1343, 1449, 1495]
but ~5% of pixels differ with a delta near full range, in a band from y=42 to y=1629. Same
palette, moved content: a layout shift, not antialiasing.
That is what the fix does here. These captures size a form with the raw setters and paint it
into a frame; without the invalidation the form stayed laid out at the DISPLAY size, which on
this port is 3840x2160, and was painted into a frame a fraction of that. It now lays out at
the size it was given. macOS lost its children entirely because the whole content pane was
mis-sized; tvOS kept them and drew them to the wrong proportions. Same cause.
Checked by eye before reseeding rather than accepted on the diff numbers: the structure is
identical -- title, row, action bar, body -- with the geometry corrected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…l invalidation Same twelve tests, same cause, and now the pattern is complete enough to state: moved macOS, tvOS, Mac Catalyst unchanged Linux x64, Linux arm64, Windows, Android, JavaScript, iOS GL, watchOS The ports that moved are the ones whose off-screen host Form was not being invalidated by something else. The rest were already getting the layout by accident and forceRevalidate is a no-op there -- which is the strongest evidence available that the fix restores a layout rather than inventing one: seven golden sets did not shift by a pixel. Catalyst's shift is the smallest of the three, 2.09% of pixels on FadeTransition and 0.33% on MorphTransition, with per-cell colour counts within a few of their previous values. macOS lost its children outright, tvOS drew them at display proportions into a smaller frame, and Catalyst -- whose display and frame are closest in size -- is off by the least. The size of the symptom tracks the gap between the display and the frame, which is what a mis-sized layout predicts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…aches iOS Metal moves too. The classification in the previous two commits was built on whichever legs had reported at the time and was twice too narrow; this is what the evidence supports now, with every port that has run the fix accounted for: moved macOS, tvOS, Mac Catalyst, iOS Metal unchanged Linux x64, Linux arm64, Windows, Android, JavaScript, watchOS unknown iOS OpenGL Metal's per-cell colour counts are identical to the old ones -- 1536, 1430, 1616, 1619, 1667, 1801 on FadeTransition, the same six numbers -- while 11.19% of pixels differ. Same palette, moved content, which is the tvOS signature and not the macOS one. iOS OpenGL is listed as unknown rather than unchanged, and that is the part with a consequence. Its dispatched run finished BEFORE the transition fix existed, so its clean result says nothing about the fix; its twelve transition goldens will move on the first master push after this merges, on a lane that does not run on pull requests. It needs a second dispatch against a head that has the fix, and those goldens seeded, before this is safe to merge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Compared 154 screenshots: 154 matched. Benchmark Results
Detailed Performance Metrics
|
…fter merge Twelve, from the second dispatch (35414496010) against a head that actually has the layout fix. Exactly the twelve transition tests and nothing else, which is the answer the first dispatch could not give: it ran before the fix existed and reported clean, and clean there meant only "these goldens match the old rendering". This is the whole reason the lane was dispatched by hand. It does not run on pull requests, so its twelve would have moved on the first master push after the merge, on goldens no PR check ever looks at. The failure was predicted from the fix and confirmed by dispatching for it rather than discovered afterwards. Same signature as Metal, tvOS and Catalyst -- per-cell colour counts within a few of their old values, 12.88% of pixels moved on FadeTransition and 1.91% on MorphTransition, nothing blank. Final classification, every port now measured against a head carrying the fix: moved macOS, tvOS, Mac Catalyst, iOS Metal, iOS OpenGL unchanged Linux x64, Linux arm64, Windows, Android, JavaScript, watchOS In the same dispatch, build-ios-metal, build-ios-tv and build-ios-watch all passed, which is those three reseeds confirmed on a clean run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Compared 166 screenshots: 166 matched. Benchmark Results
Detailed Performance Metrics
|
…pture Run 35431774593 is the first GNOME reference capture since the dangling widget pointer was fixed. It completed, wrote all 104 tiles, and produced a capture-manifest.json that parses with no blockers -- where the previous run wrote its tiles and then died inside write_manifest, leaving a truncated file. That is the fix verified rather than assumed. The tiles are what a separator reference should be: 240x56, two colours, a 240-pixel rule. light #fafafa background #dddddd rule dark #242424 background #454545 rule This closes a gap with the same shape as the OpenGL one, and it is worth naming because it is not obvious: scripts-fidelity-desktop.yml scores on PUSH TO MASTER and on dispatch, never on pull requests. gnome-adwaita was the only set missing a golden for a row the yaml declares, so the first master push would have scored a declared row against nothing -- green PR, red master, on a suite no PR check runs. One measurement to act on separately: the theme declares Separator color #d8d4d0 and GTK actually draws #dddddd. Close, not equal. Left alone here deliberately -- Separator is rendered inside DesktopWidgetsTheme on the Linux screenshot set, so changing it means rebuilding the .res and reseeding screenshot goldens, which is a change with its own blast radius and does not belong in a commit whose job is to add a reference. The number is now measured and recorded, which is what the row was added for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Makes the three desktop native themes actually native. PR #5845 built, measured and gated
them and deliberately switched nothing on; this turns them on and fills what turning them on
exposed.
The two bugs this found
commandBehavior: Nativewas a silent way to lose every command.MenuBar.updateCommandshands the commands to
setNativeCommandsand RETURNS without drawing soft buttons -- correctwhere there is a real menu bar, and on a platform without one it means they go to an empty
method and are never drawn at all. Nothing downstream can tell that from "the platform handled
it". Latent until a theme asked, which the desktop themes do, and which the Windows and Linux
ports cannot honour. Fixed in
setCommandBehavior, whereBUTTON_BAR->SOFTKEYalreadynormalises an unsupportable behaviour.
COVERAGE.md's Desktop section was the iOS section.3959d6886cset out to remove ablock the merge had duplicated and removed the wrong copy, so the file has had no record of
which desktop widgets are covered since. Restored from
2ceb4f0f9cand updated.What changed
Scrolling. All three themes derived their scrollbar from the mobile one --
DesktopScrollThumb { cn1-derive: ScrollThumb; }and a transparent track, so no gutter, nominimum length and no highlight -- while the mobile themes carried the full desktop
treatment. Each theme now sizes its own gutter from the platform's figure and gives the thumb
its hover and drag colours. Two things measured rather than assumed: the highlight states are
.selectedand.pressed, never.hover(a.hoverrule there compiles and is neverpainted), and
cn1-deriveemits the whole state family by copying the base, so the old stubproduced
sel#bgColoridentical tobgColor-- a highlight present in the resource andinvisible on screen. The four UIIDs are also seeded in
resetThemePropsnow, so a theme thatturns the constant on without defining them no longer draws an invisible bar that still
reserves its gutter.
Menus.
PopupContentPane,CommandList,Command,TouchCommand,Tooltip,TooltipDialogand the dialog command area were undefined in all three themes, so each fellthrough to
UIManager's blank default -- black on white, on a dark window. Plus a realright-click menu:
Component.addContextMenuListenerhas fired for years and nothing everopened one.
Dialogs.
defaultNativeWindowModeBoolon the three themes, so a dialog opens as a realoperating system window. Anchored popups never do.
Keyboard. Tab and Shift-Tab move focus; Escape cancels. The traversal machinery is old and
could not be wired to Tab as it stood: its filter is opt-in,
preferredTabIndexdefaults to-1 and
TextAreais the only class in the framework that opts in, so Tab would have walkedbetween a form's text fields and skipped every button between them. The desktop order is its
own, filtered on focusability. Escape resolves through one
Dialog.cancel()that the windowclose control and the back gesture already meant separately.
desktopTitleBarModegained a reader. The three themes have carried the constant sincethey landed and nothing read it.
Three new components.
Separator,GroupBox,Stepper-- every desktop toolkit has allthree and Codename One had none.
The flip. Windows -> Fluent, Linux -> Adwaita, macOS -> Aqua. Java SE stays on legacy
deliberately: that default reaches every desktop application ever built rather than only ours.
Fidelity: 9 scored controls to 22. Everything that makes a window look like a desktop
window was unmeasured, including the interactive scrollbar. Three rows are not scored on every
platform, and that is the honest answer rather than a gap -- an
NSScrollerrenders nothingthrough
cacheDisplay(measured: correctusableParts, correct frame,isHidden=false, onecolour in the tile, tried detached and inside a real
NSScrollViewin both styles),NSMenubelongs to the window server, and the AppKit and GTK tooltips are separate windows.
What is still open
screenshot baselines. Both are captured from the runners that score them, never locally, so
those legs are red until the capture workflows have run and their output has been reviewed.
toolchain exists on the machine this was written on. CI is the first verifier.
setNativeCommands, sotheir commands stay in the now-properly-themed Toolbar, and
isDesktopHideToolbar()will nothide the only place they are drawn. Tracked in
COVERAGE.md.7294 core unit tests green. SpotBugs zero findings on
core-unittests; cast-semantics,control-characters, copyright, build-hint-catalog and the four fidelity gates clean. Vale and
LanguageTool clean on both guide chapters.
🤖 Generated with Claude Code