Phase 2: widget breadth #10
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "phase/2-widget-breadth"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 2 of
docs/13-phases.md. Slice 1 has landed; slice 2 is in progress on this branch, so expect further commits before this is ready to merge.Slice 1 —
Toggle,Spacer,ProgressBar,ActivityIndicatorFour widgets on GTK and Android, each with unit tests and a self-test check that reads the value back out of the real toolkit rather than out of the signal that drove it.
ToggleGtkSwitchSwitchSpacerGtkBoxViewwith weightProgressBarGtkProgressBarProgressBar, horizontalSpinnerGtkSpinnerProgressBar, indeterminateTwo decisions that shape the catalogue
Both are the same rule: do not promise an API some platforms cannot keep.
Togglecarries no label. AGtkSwitchand aUISwitchhave nowhere to put one; only Android'sSwitchdoes. A caption that two of four renderers would silently drop is worse than no caption, so it is a sibling —Row()(Label("Hide completed"), Spacer(), Toggle(hide)(hide.set)). That is what a settings row looks like on all four, and it is whySpaceris in the same slice.ActivityIndicatorhas no "running" prop. It spins while mounted, soShow(busy)(Spinner())is what stops it. The reconciler already does this; a second mechanism for the same thing is one more to keep in step.Toggleis a separateWidgetKindrather than a style flag onCheckboxbecause the platforms disagree about which control a given setting is — that is the app's judgement, not the framework's.Prop.ProgressisOption[Double], whereNoneis indeterminate and deliberately notSome(0.0): every toolkit here renders those differently, so collapsing them would make "nothing has happened yet" and "we cannot say" look identical.Measured
busyThe bar is driven to a genuine fraction first. Everything is done by that point in the self-test, and a 1.0-against-1.0 comparison would also pass if the renderer clamped every value to full — the first version of this check had exactly that hole.
114 unit tests pass.
Not papered over
docs/12still reports 9 of 32, not 13. Its own rule is that a widget is done when every renderer has it. The Apple half is #9.sui_create'sdefault:branch returns a separator, so letting a new kind through would render a progress bar as a hairline rule — a wrong widget that looks like a layout bug and sends whoever hits it looking somewhere else entirely.ColorRole.Accentdoes not reachProgressBar. On Android the bar renders in the platform's default blue while the buttons are the app's branded green. Visible indocs/screenshots/android-catalogue.png, recorded indocs/12, flagged in #9, and not fixed here — per-subtree theming is phase 4.Slice 2 (in progress)
Slider,SecureField,Radio,Stepper.IconButtonandLinkare deferred out of phase 2 on purpose: the first needs an icon/resource system and the second needs platform URL opening, and neither should be improvised inside a widget.🤖 Generated with Claude Code
Four widgets, on GTK and Android, each with unit tests and a check in both self-tests that reads the value back out of the real toolkit. Two catalogue-shaping decisions, both the same rule - do not promise an API some platforms cannot keep: Toggle carries no label. A GtkSwitch and a UISwitch have nowhere to put one; only Android's Switch does. Promising a caption that two of four renderers would silently drop is worse than not having it, so it is a sibling: Row()(Label("Dark mode"), Spacer(), Toggle(dark)(dark.set)). That is what a settings row looks like on all of them, and it is why Spacer is in the same slice. ActivityIndicator has no "running" prop. It spins while it is mounted, so Show(loading)(Spinner()) stops it - the reconciler already does this, and a second mechanism for the same thing is one to keep in step. Toggle is a separate WidgetKind rather than a style flag on Checkbox because the platforms disagree about which control a given setting *is*, and that is the app's judgement, not the framework's. Prop.Progress is Option[Double]: None is indeterminate and is deliberately not Some(0.0). Every toolkit here renders them differently - an indeterminate bar animates, a determinate one at zero does not - so collapsing them would make "nothing has happened yet" and "we cannot say" look identical. Renderers: - GTK: GtkSwitch, empty GtkBox, GtkProgressBar, GtkSpinner. A switch has no 2-argument "changed" signal - state-set also carries the new state and activate is a keybinding a mouse never fires - so it binds notify::active, which carries a GParamSpec and needed a third trampoline arity. - Android: Switch is a CompoundButton exactly as CheckBox is, so Checked and OnCheckedChange needed no special case. A horizontal bar and a circular spinner are the same class with different styles, fixed at construction, which is why they are two kinds rather than one kind with a prop. - Apple: throws, naming the widget, with codes 9-12 reserved. sui_create's default branch returns a separator, so letting a new kind through would render a progress bar as a hairline rule - a wrong widget that looks like a layout bug and sends whoever hits it looking somewhere else. Forgejo #9. Measured, both toolkits, read back out of the widget rather than the signal: the switch flips when the signal is written and filtering the list follows it; the progress bar reads 0.750. The bar is driven to a genuine fraction first - everything is done by that point in the self-test, and 1.0-against-1.0 would also pass if the renderer clamped every value to full. docs/12 still says 9 of 32, not 13, because its own rule is that a widget is done when every renderer has it. Recorded honestly: on Android the ProgressBar renders in the platform's default blue while the buttons are the app's branded green - ColorRole.Accent does not reach it. Visible in the new screenshot, noted in docs/12, not fixed here; per-subtree theming is phase 4. 114 tests pass; GTK self-test 24 checks, Android 33, both green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>Slider and SecureField on GTK and Android, with unit tests and self-test checks on both. But the more useful result is what did not get built. docs/07 section 7.10 listed the v1 catalogue before any renderer existed. Phase 2 is the first time each entry has been checked against four real toolkits, and slice 2's other two candidates do not survive it. Verified against the installed GTK4 headers and android-36/android.jar: Radio - UIKit has no radio control at all. And mutual exclusion is a sibling *relationship*: Android expresses it structurally, since RadioGroup is a container, and GTK as a pointer to a sibling. Neither is a prop, and the contract has no grouping concept. Stepper - Android has none. NumberPicker is a scrolling wheel, a different control, and the Android idiom is a row of buttons the app composes - which the framework can already express. Neither is built. Both are reclassified as app-level composition or .platform escape hatches, and "32 widgets" is retired as the denominator: a widget earns a place in the catalogue by existing on every platform we render to, and forcing one in would mean two renderers faking it. Every remaining section 7.10 entry gets this question asked before it is built, not after. docs/12 section 12.2a. The two that do survive: Slider - GtkScale / SeekBar. Prop.Value is in the app's units, never a fraction: an app choosing a volume between 0 and 11 says 7, not 0.636. Android's SeekBar is integral, so that renderer converts, at 1000 steps - it is the only side that knows its own resolution. Prop.Range is sent before Prop.Value, because a value outside its bounds is meaningless and every toolkit clamps silently; the ordering is asserted rather than left to how the DSL reads. SecureField - a widget kind rather than a flag on TextField, because NSSecureTextField is a separate class and an Apple widget is chosen at create. On GTK it is the same GtkEntry with visibility off; on Android the same EditText with a password input type. Both take the don't-disturb rule the text field already had: only write when the value differs, or the write fights the user's drag. One real bug caught by the Android self-test: SeekBar extends ProgressBar, so "find the determinate progress bar" matched the slider too and reported two bars. Excluded explicitly, with the reason recorded - the Apple issue is warned about the equivalent. Measured on both toolkits, read back out of the widget: progress bar 0.750, slider 7.0 of 0-11 following a write to 3.5, exactly one of two text fields masked and still holding its value. 117 tests; GTK self-test 29 checks, Android 38, both green. Screenshot refreshed. Apple is #9, now covering six widgets and two reserved codes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>