Repository navigation
refactor: share code between the styles in lib, and fix 18 bugs found on the way - #215
Merged
Merged
Conversation
The four painted switches repeated the same shell. It now lives once in the internal settings_switch_base.dart: - SettingsSwitchState: the Space/Enter actions, reduced motion, text direction, focus highlight, drag-down position, and the semantics, focus and gesture widgets around the painter (buildSwitch). - SettingsSwitchKnobDrag: the drag of the macOS, Windows and GNOME switches (the knob follows the pointer, letting go past the middle picks a side). Each style keeps its own settle step, because they differ. - SettingsSwitchPainter: mirrors the canvas in right-to-left layouts. - debugFillSwitchProperties and switchBrightnessOf. The Fluent _SwitchColors class becomes a record, which has the same value equality without the hand-written == and hashCode. No visible change: widget trees, callbacks, animation calls and paint order are the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The press, release and position springs were three copies of the same SpringSimulation call. They now go through _spring(). - _paintLens no longer takes the press value, which it never read. - The focus ring color is only computed while the ring shows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The iOS, Windows and GNOME painters built the same track rectangle in the middle of the painted size. It is now SettingsSwitchPainter.centerTrack. The macOS painter keeps its own (it draws from the top left corner). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing in fluent_settings_switch.dart needs flutter/services.dart: the mouse cursors come with widgets.dart, and now live in the shared State. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Android and web row The dispatcher built six 17-20 argument calls and each platform tile repeated the same constructor and fields. They now take one SettingsTileData. The Android and web tiles become two configurations of MaterialTileRow; the web menu item shares their semantics node and the selected/disabled colors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
buildTileContent was about 210 lines and 14 levels deep. The title and value column, the tap handler and the state colors are now separate, the nested color ternaries are flat, and the value uses the shared tileValue. Drops a Material wrapper that could never be built (the tile only exists in the iOS style). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ross desktop tiles The Fluent and GNOME tiles tracked the pressing pointer with the same code (TilePressTracking), and the macOS, Fluent and GNOME tiles built the same Space/Enter actions and the same title-over-subtitles column. Their 200+ line build methods are split into named parts and the nested color ternaries are flat. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The padded heading node (Padding > Semantics header > DefaultTextStyle) was written out in seven places across the six section styles. The iOS card-corner rules use one _hasFooter helper, and the iOS and web sections import the files they use instead of the package barrel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he device-platform error SettingsList looked for the first shown section again for every item it built; it now does so once per build, and its three loops over shown sections share one predicate. The Android and web themes share their six common ColorScheme colors, and the five copies of the DevicePlatform.device exception become throwUnresolvedPlatform. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaterialTileRow builds its end (switch, trailing widget, chevron) in its own method and creates the Switch only for switch tiles. The macOS row separates its content from the box around it (padding, pressed tint, focus ring), so no method there is longer than about 115 lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
resolveBrightness runs for every Android and web tile and every iOS switch. MediaQuery.of made them depend on the whole MediaQueryData, so they rebuilt on any change of it (keyboard insets, padding). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The only place that builds a SettingsSplitListScope passes `sidebar`, so the `sidebar ?? isSplit` default was dead. The flag's doc comment now says what it really holds (true with two panes in every style), and SettingsSplitScope.shownId says why it is there although nothing reads it: it makes widgets that got the controller from SettingsSplitView.of rebuild when the page changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Android bar and the collapsing title drew the same toolbar row twice, and the arrow back button and the 22px title existed three and four times. They are now SettingsArrowBackButton, SettingsHeaderTitle and one toolbar row, which reads the tablet inset and the back label itself, so the collapsing delegate loses two fields. settingsHeaderOverBody is the "header over a body without the top safe area" column of every page. showTitle is only ever false for the iOS bar: the macOS, Windows, GNOME and Android branches no longer handle it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A move only, no code changes. split_view_routes.dart holds the pages of the two navigators and their observers; split_view_focus.dart holds the pane focus action and the state's four focus methods, as an extension on the state so their names and call sites stay the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both observers of the split view kept their navigator's top route with the same four overrides. A shared _TopRouteObserver does it; the stack and detail observers add what is theirs, in the same order as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The state's fields are grouped by what they describe (the pick, the last layout, the two navigators, the list pane), and the one-pane navigation flags say what each one means. Three notification fields become one record. build() reads as three steps: _readDestinations, _computeGeometry (with _findHinge) and _buildLayout, whose bookkeeping is _recordLayout. Every side effect keeps its place in the order. The Tab actions are created once, not on every build, so the Actions widget no longer swaps its listeners on each rebuild. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_buildListPane was 278 lines with eight switches on the style family, and _buildTwoPanes had two more, so the generic view knew the macOS, Windows, GNOME, web, iOS and Android details. Each family now has one small SplitPaneStyle class (split_pane_style.dart) that answers what differs: whether rows are sidebar rows, the list padding, the separators and the iOS large title among the sections, the header over the list with its keyboard and window wrappers, the line between the panes, Android's hidden icons and the web detail column. The view builds the same list pane for every style, in 55 lines, and no longer imports the macOS and GNOME sidebars. The widgets and their order are the same, except that the macOS list's MediaQuery now sits above its extend-up layout instead of below it. The list still gets its MediaQuery from the view's context, as before. Still in the view: the Windows compact rail (FluentCompactPaneLayout and its open flag), which picking a page closes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A move only: the six methods that build the panes (two panes, the stack navigator, the host page, the detail navigator and its root page, the list pane) become an extension on the state in a part file, with the same names and call sites. The state class keeps what the view tracks and does: the pick, back handling, the layout and its notifications. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The state's back section listed actions and listeners in turn. They are now two sections: "Back" (what back may do, in order, and the list pane's leave) and "What the navigators report" (the listeners and observer callbacks that keep the navigation flags current). Methods only moved; a comment over each section says how the pieces fit. The pane parts call the context they carry viewContext, since it is the split view's and not the pane's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lay and controls fluent_split.dart mixed the navigation pane, the page header with its breadcrumb, the compact rail's overlay and the subtle button. The page header, the overlay layout and the shared controls move to files of their own; fluent_split.dart re-exports the symbols it had. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The macOS capsule halves, the GNOME flat button, the Windows subtle button and the breadcrumb crumbs each carried the same semantics, keyboard activation and state tracking. SidebarButton holds it once and each style only draws its states. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The macOS, Windows and GNOME sidebar rows repeated their field list and their focus / activate plumbing, and the Windows and GNOME rows their press tracking, which had drifted. SidebarRow holds the fields, SidebarRowState the focus node, the Enter/Space actions and the tap handling, and SidebarRowPress follows the pressed pointer; each row still decides which pointer presses it. SidebarSection holds the header-over-rows column that the three section widgets shared. Also moves each row's content line out of its build method. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A builder put one more element between each button and its semantics, and made a breadcrumb crumb rebuild (and its header relayout) on every press, which it had not done. The buttons now extend SidebarButton and draw themselves in buildButton, so their element trees are as before, and a button opts out of the states it does not draw. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each row laid out its icon, one-line title, trailing widget and switch in its own Row. SidebarRowState.buildLine builds that line from the style's icon slot, text styles, spacing and switch; the Windows rail asks for the icon only. The Windows item's color resolution moves out of its build method. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FluentPageHeader builds the breadcrumb under its own LayoutBuilder, which answers intrinsic size and dry layout queries without asking its child, so the render object's computeDryLayout and intrinsic overrides never ran (no hit in the unit tests or the pixel harness). Its paint, hit test and semantics visits walk the children without copying them into a list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The back button and the item margins were written out for each of the pane's layouts; the Column's children are now one collection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FluentNavigationItem.build was 256 lines; with its colors, line, pointer handling and rail tooltip in named methods it is 140. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s item The shared row line no longer knows about the rail: the item builds its icon-only Row itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… sidebar rows and switches The sidebar rows had their own copy of the tiles' press tracking (SidebarRowPress) and of the Enter/Space actions, and so had the switch base. They now use TilePressTracking and tileActivateActions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…chIfSeparate The sidebar rows were the other callers of labelTileSwitch. With them on SettingsTileData, the function had one caller, which now does the labelling itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…red lib List the shared files the refactor created (tile_*.dart, section_header.dart, settings_switch_base.dart, unresolved_platform.dart, sidebar_*.dart, split_pane_style.dart, split_view_*.dart, fluent_*.dart) with what each holds, and correct the names that moved (SidebarRowFocus, labelTileSwitch). The behavior and design notes are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pane-style refactor passed the app's list straight to the pane's lazy list in some styles. An app that removes sections in place without rebuilding the view then got a RangeError on scroll, which the copy made before the refactor prevented. Restore the copy and cover it with a test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…essed
A pointer that went down on the switch keeps reporting to its Listener
after the switch has left the tree. Its release then animated the press
controller, which was already disposed ("AnimationController.animateTo()
called after AnimationController.dispose()").
The same scenarios (removed while held, mid-drag and while settling) are
tested on the Cupertino, macOS and GNOME switches, which were fine.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pointer that went down on a row keeps reporting to its Listener after
the row has left the tree. TilePressTracking then asked the unmounted
State for its render box ("This widget has been unmounted, so the State
no longer has a context").
Windows tiles and sidebar rows threw on every move. GNOME ones threw only
when the row went away before the tap was recognized as down (a touch,
in the first 100 ms): after that, the cancelled tap released the press.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Given more room than its 36x16 or 44x20 track (tight constraints), the switch painted the track from the top-left corner of its box. It now paints it in the middle, like the other three switches, in left-to-right and right-to-left layouts. At its own size nothing changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…diagnostics debugFillProperties left the two out. They show when set and stay hidden at their defaults, so a switch that sets neither prints as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
titleTextStyle said titleTextColor applies only when the style has no color. Only the macOS style works that way: the other five always draw the section title in titleTextColor (their default when it is null). The comments of both fields now say what each style does, in lists and in a split view's list pane, and a test pins it. No behavior changes. Other field comments checked against the code and corrected: inactiveTitleColor, inactiveSubtitleColor, inactiveSwitchColor, tileDescriptionTextStyle, selectedTileIconColor and listPaneBackground. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…able A switch tile with onPressed but no onToggle kept the row's ink well enabled: it showed hover, splash and focus and had a semantics tap action, but a tap did nothing (these styles toggle on a row tap and never call onPressed for switch tiles). The row now has no tap handler, like a switch tile with neither callback already had. The disabled switch is drawn as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The arrow keys already select only rows with a destination: a row with
onPressed and no destination ("Sign out"), a switch row and a navigation
tile that pushes its own route take the focus and nothing is called. No
code change; the test keeps it that way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ocus The glass back button only reacted to taps. Tab now reaches it, Enter and Space press it, and it shows the Cupertino focus ring (3.5pt, inside the circle) while the keyboard has the focus. Its look at rest, its semantics node and taps are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SidebarButton (the macOS toolbar buttons, the GNOME flat button, the Windows subtle button and the breadcrumb crumbs) handled ActivateIntent only. On the web Enter is ButtonActivateIntent, so it did nothing there. It now handles both, like the tiles and the sidebar rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tics nodes In the macOS, Windows and GNOME sidebars, CustomSettingsTiles next to each other merged into one semantics node, which a screen reader read as one row. The sidebar sections now wrap them with tileSemanticsNode, like the list sections of the other styles. No pixels change. The iOS and web list sections are left as they are: their inner list already makes each row a node (the tests cover them too). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An item of the compact rail took its semantics label from the title's text and ignored Text.semanticsLabel, so the rail read "Wi-Fi" where the open pane read the label the app gave. It now uses tileTitleLabel, the rule of the tiles: the semantics label wins, and a RichText title is read too. The tooltip and the first-letter icon still show the title's text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n action A GNOME sidebar row told screen readers it was enabled even when it had nothing to do (no onPressed, or a switch row without onToggle). It now reports an enabled state only when it has an action, as the GNOME list tile does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…S and GNOME sidebars A disabled switch in a macOS or GNOME sidebar row kept its active color and ignored SettingsThemeData.inactiveSwitchColor, which the list tiles of both styles use. The sidebar rows now follow the same rule. Without an inactiveSwitchColor nothing changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FluentNavigationItem put the focus ring and the rail's tooltip around its content only while they were needed, so the leading widget, the switch and the fill's fade were recreated whenever the ring showed or hid, or the rail opened or closed. The ring's CustomPaint now stays in the tree (without a painter it draws nothing), and the content sits under a GlobalKey, so it moves in and out of the tooltip with its State. No pixels change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… in paint The selection pill's painter called back into the item's State, which looked Directionality up while painting (and after a frame, when another item asked where its pill was). The item now reads the direction in build, keeps it for those later calls and hands it to the painter, which repaints when it changes. Nothing observable changes, so there is no new test: the existing pill tests (left to right, right to left, the slide) cover it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ew's menu The web style put a separator between every two sections of the menu, also around a SettingsSection without tiles, which shows nothing: the line doubled, or showed before the first group or after the last. It now leaves empty sections out first, like the macOS, Windows and GNOME sidebars. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With two panes the list pane's list still got the window's safe-area padding of the end side (a phone in landscape with the cutout at the right), which only the detail pane touches: the list read its MediaQuery from above the pane. Only the Android style with a title was right. The pane's content is now built in the pane, so its MediaQuery has no end-side padding, as the detail pane's has none at the start side. Right to left it is mirrored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A SettingsSplitView whose platform went from iOS to another style and back kept showing the title in the bar although its list, built anew, was at the top with the large title on screen: the view remembered that the large title had scrolled away. It now forgets that when the style changes, so the view looks like one built in the iOS style. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
openSettingsDestination runs from a tile's onPressed and looked up the split view's list scope, the theme and the style scope with calls that make the tile depend on them, outside of a build. It now reads them without listening, as it already read the page trail. Nothing observable changes, so there is no new test: the tests that open pages from tiles, in lists and in split views, cover it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…S.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 8, 2026
Closed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Connection with issue(s)
No issue. This is maintenance: a cleanup of
lib/, plus the bugs found while doing it.What changed
Refactor (38
refactor:commits): no change to the public API, the look or the behaviorsettings_switch_base.dart.SettingsTileDatainstead of forwarding 17 to 20 arguments six times. The Android and web tiles are two configurations ofMaterialTileRow. Press tracking, Enter/Space activation and the title column are shared.sidebar_row.dart,sidebar_button.dartandsidebar_section.dart.fluent_split.dartis split into pane, page header, compact pane and controls.settings_split_view.dartwent from 1,669 to 784 lines. Routes, focus and pane builders are part files, and what differs per style is behindSplitPaneStyle.lib/11,941 → 10,902. Largest file 1,780 → 895 lines.Bug fixes (18
fix:commits)Each fix has a test that failed before it. The full list is in
CHANGELOG.mdunder[Unreleased]. In short:FluentSettingsSwitchleaves the tree while pressed.onToggle,MacosSettingsSwitchcentering,SettingsThemeDatadoc comments corrected to match the code.Testing and Review Notes
dart formatandflutter analyze .: clean, also inexample/.flutter test: 876 pass. The 733 tests that existed before are unchanged; 143 are new.example/integration_teston an iOS simulator: 21 gallery tests and 30 split view flow tests pass.sectionslist without copying it). It is fixed and covered by a test.Not checked: real macOS, Windows and Linux desktop builds, a real screen reader, release mode.
Heads-up for reviewers:
package:settings_ui/src/moved and changed (for example, the platform tile widgets now take oneSettingsTileData). They were never public API, but apps that import them will need updates.refactor:commits come first, then thefix:commits, so the two halves can be read separately.To Do
[Unreleased]section of the changelog.🤖 Generated with Claude Code