Apple renderer: Toggle, Spacer, ProgressBar and ActivityIndicator (phase 2) #9

Open
opened 2026-09-29 10:07:28 -07:00 by rleibman · 1 comment
Owner

Phase 2 added four widgets to the contract. GTK and Android implement them; the Apple renderer throws for all four, so docs/12 still counts the catalogue as 9 of 32 rather than 13.

Why it throws rather than falls back. sui_create's default: branch returns a separator. Letting a new kind through would silently render a progress bar as a hairline rule — a wrong widget that looks like a layout bug and sends whoever hits it looking in the wrong place entirely. AppleRenderer.kindCode therefore raises UnsupportedOperationException naming the missing widget, and reserves the codes below.

Kind codes, already reserved in AppleRenderer

Code Widget AppKit UIKit
9 Toggle NSSwitch UISwitch
10 Spacer empty NSView empty UIView
11 ProgressBar NSProgressIndicator, .bar UIProgressView
12 ActivityIndicator NSProgressIndicator, .spinning UIActivityIndicatorView

What to do

  1. Abi.scala in tools/shim-gen: extend the sui_create doc comment with codes 9-12 and regenerate the header, so the kind table stays in one place. The consistency suite runs on any machine and will tell you if the four artefacts drift.
  2. AppleRenderer.kindCode: replace the four unimplemented(...) calls with the codes.
  3. Both shims' sui_create: add cases 9-12.
  4. Prop.Progress is currently a documented no-op in AppleRenderer.update. Implement it: Some(f) sets the fraction (clamp 0.0-1.0; NSProgressIndicator is 0-100, UIProgressView is 0.0-1.0), None means indeterminate and is deliberately not the same as Some(0.0) — isIndeterminate = true / an animating bar.
  5. ActivityIndicator has no "running" prop. It spins while mounted; Show is what stops it. So start animating at create (startAnimation, startAnimating) and do not add a prop for it.
  6. Toggle carries no label — Prop.Text is not sent to it. NSSwitch and UISwitch have nowhere to put one; the caption is a sibling. Do not add one.
  7. Spacer must be allowed to expand: it takes the room its siblings do not, through Prop.Grow. On a stack view that means a low content-hugging priority on the stack axis, the same thing sui_set_grow already does.

How to verify. The shared TodoApp now has a settings row (Label, Spacer, Toggle) plus a ProgressBar and a Show-gated Spinner, and both other hosts check them. Mirror those checks in the macOS/iOS self-test:

  • the switch starts off, writing the signal flips it, and flipping it filters the list;
  • the progress bar shows the model's fraction — drive it 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. GTK and Android both measure 0.750;
  • no spinner when idle, exactly one when model.busy is set, none again after.

Record the numbers in the report, and add a screenshot to docs/screenshots/.

Also worth checking while you are there: 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. See docs/screenshots/android-catalogue.png. If AppKit/UIKit have the same split, say so in the report; it is a theming gap recorded in docs/12 rather than part of this issue.

See docs/12-component-status.md and docs/13-phases.md phase 2.

Phase 2 added four widgets to the contract. GTK and Android implement them; the Apple renderer **throws** for all four, so `docs/12` still counts the catalogue as 9 of 32 rather than 13. **Why it throws rather than falls back.** `sui_create`'s `default:` branch returns a *separator*. Letting a new kind through would silently render a progress bar as a hairline rule — a wrong widget that looks like a layout bug and sends whoever hits it looking in the wrong place entirely. `AppleRenderer.kindCode` therefore raises `UnsupportedOperationException` naming the missing widget, and reserves the codes below. **Kind codes, already reserved in `AppleRenderer`** | Code | Widget | AppKit | UIKit | |---|---|---|---| | 9 | `Toggle` | `NSSwitch` | `UISwitch` | | 10 | `Spacer` | empty `NSView` | empty `UIView` | | 11 | `ProgressBar` | `NSProgressIndicator`, `.bar` | `UIProgressView` | | 12 | `ActivityIndicator` | `NSProgressIndicator`, `.spinning` | `UIActivityIndicatorView` | **What to do** 1. `Abi.scala` in `tools/shim-gen`: extend the `sui_create` doc comment with codes 9-12 and regenerate the header, so the kind table stays in one place. The consistency suite runs on any machine and will tell you if the four artefacts drift. 2. `AppleRenderer.kindCode`: replace the four `unimplemented(...)` calls with the codes. 3. Both shims' `sui_create`: add cases 9-12. 4. **`Prop.Progress`** is currently a documented no-op in `AppleRenderer.update`. Implement it: `Some(f)` sets the fraction (clamp 0.0-1.0; `NSProgressIndicator` is 0-100, `UIProgressView` is 0.0-1.0), **`None` means indeterminate** and is deliberately not the same as `Some(0.0)` — `isIndeterminate = true` / an animating bar. 5. **`ActivityIndicator` has no "running" prop.** It spins while mounted; `Show` is what stops it. So start animating at create (`startAnimation`, `startAnimating`) and do not add a prop for it. 6. **`Toggle` carries no label** — `Prop.Text` is not sent to it. `NSSwitch` and `UISwitch` have nowhere to put one; the caption is a sibling. Do not add one. 7. **`Spacer`** must be allowed to expand: it takes the room its siblings do not, through `Prop.Grow`. On a stack view that means a low content-hugging priority on the stack axis, the same thing `sui_set_grow` already does. **How to verify.** The shared `TodoApp` now has a settings row (`Label`, `Spacer`, `Toggle`) plus a `ProgressBar` and a `Show`-gated `Spinner`, and both other hosts check them. Mirror those checks in the macOS/iOS self-test: - the switch starts off, writing the signal flips it, and flipping it filters the list; - the progress bar shows the model's fraction — **drive it 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. GTK and Android both measure **0.750**; - no spinner when idle, exactly one when `model.busy` is set, none again after. Record the numbers in the report, and add a screenshot to `docs/screenshots/`. **Also worth checking while you are there:** 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. See `docs/screenshots/android-catalogue.png`. If AppKit/UIKit have the same split, say so in the report; it is a theming gap recorded in `docs/12` rather than part of this issue. See `docs/12-component-status.md` and `docs/13-phases.md` phase 2.
Author
Owner

Slice 2 adds two more, so this issue now covers six widgets. Same rule as before: AppleRenderer.kindCode throws, naming the widget, rather than letting sui_create's default: render it as a separator.

Code Widget AppKit UIKit
13 Slider NSSlider UISlider
14 SecureField NSSecureTextField UITextField with isSecureTextEntry = true

Three more props are documented no-ops in AppleRenderer.update and need implementing with them:

  • Prop.Range(min, max) - NSSlider.minValue/maxValue, UISlider.minimumValue/maximumValue.
  • Prop.Value(v) - in the app's own units, not a fraction. Both Apple controls already take app units, so this is near-direct; there is no conversion to write, unlike Android's SeekBar. Apply the same don't-disturb rule the text field uses: only write when the value actually differs, or the write fights the user's drag.
  • Prop.OnValueChange - target/action on NSSlider, .valueChanged on UISlider, reporting in app units.

Range always arrives before Value; you can rely on that (it is asserted in CatalogueSuite).

SecureField shares everything but construction with TextField - Prop.Text, Prop.Placeholder and Prop.OnTextChange must all work on it. On GTK it is literally the same GtkEntry with visibility off; only AppKit needs the separate class, which is why this is a kind at all.

How to verify. The shared TodoApp now has a Slider bound to model.volume (0-11, starting at 7.0) and a SecureField bound to model.secret. Mirror the checks both other hosts run:

  • the slider starts at 7.0 and follows a signal write to 3.5 - in app units, so a renderer that stored a fraction fails here;
  • exactly one of the two text fields masks its input, and the masked one still holds its value ("hunter2" round-trips even though it is not drawn).

One trap, found the hard way on Android: SeekBar extends ProgressBar, so a naive "find the determinate progress bar" predicate matched the slider too and the self-test reported two bars. Check whether the Apple slider and progress indicator share a superclass before writing the equivalent check - on AppKit they may well both be NSControl.

Also note docs/12 section 12.2a: Radio and Stepper were in phase 2 and were reclassified rather than built - UIKit has no radio control and Android has no stepper. Please do not add kind codes for them.

**Slice 2 adds two more**, so this issue now covers six widgets. Same rule as before: `AppleRenderer.kindCode` throws, naming the widget, rather than letting `sui_create`'s `default:` render it as a separator. | Code | Widget | AppKit | UIKit | |---|---|---|---| | 13 | `Slider` | `NSSlider` | `UISlider` | | 14 | `SecureField` | `NSSecureTextField` | `UITextField` with `isSecureTextEntry = true` | Three more props are documented no-ops in `AppleRenderer.update` and need implementing with them: - **`Prop.Range(min, max)`** - `NSSlider.minValue`/`maxValue`, `UISlider.minimumValue`/`maximumValue`. - **`Prop.Value(v)`** - **in the app's own units, not a fraction.** Both Apple controls already take app units, so this is near-direct; there is no conversion to write, unlike Android's `SeekBar`. Apply the same don't-disturb rule the text field uses: only write when the value actually differs, or the write fights the user's drag. - **`Prop.OnValueChange`** - target/action on `NSSlider`, `.valueChanged` on `UISlider`, reporting in app units. `Range` always arrives before `Value`; you can rely on that (it is asserted in `CatalogueSuite`). **`SecureField` shares everything but construction with `TextField`** - `Prop.Text`, `Prop.Placeholder` and `Prop.OnTextChange` must all work on it. On GTK it is literally the same `GtkEntry` with visibility off; only AppKit needs the separate class, which is why this is a kind at all. **How to verify.** The shared `TodoApp` now has a `Slider` bound to `model.volume` (0-11, starting at 7.0) and a `SecureField` bound to `model.secret`. Mirror the checks both other hosts run: - the slider starts at **7.0** and follows a signal write to 3.5 - in app units, so a renderer that stored a fraction fails here; - exactly **one** of the two text fields masks its input, and the masked one still *holds* its value ("hunter2" round-trips even though it is not drawn). **One trap, found the hard way on Android:** `SeekBar` extends `ProgressBar`, so a naive "find the determinate progress bar" predicate matched the slider too and the self-test reported two bars. Check whether the Apple slider and progress indicator share a superclass before writing the equivalent check - on AppKit they may well both be `NSControl`. Also note `docs/12` section 12.2a: `Radio` and `Stepper` were in phase 2 and were **reclassified rather than built** - UIKit has no radio control and Android has no stepper. Please do not add kind codes for them.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
rleibman/scala-ui#9
No description provided.