Skip to content

Wire up dark mode across all Apple frontends (RFC 0007 v2)#13

Merged
willwade merged 5 commits into
mainfrom
feature/dark-mode-v2
Jun 20, 2026
Merged

Wire up dark mode across all Apple frontends (RFC 0007 v2)#13
willwade merged 5 commits into
mainfrom
feature/dark-mode-v2

Conversation

@willwade

Copy link
Copy Markdown
Contributor

What

Wires up dark mode across all four Apple frontends using DasherCore's v2 stateful appearance model (RFC 0007, reworked in DasherCore #25).

Supersedes #12 which used the removed dasher_set_appearance API.

DasherCore bump

e8e394b5fc61344f — pulls in the stateful appearance model:

  • dasher_get/set_appearance_mode: System (default) / Light / Dark — persisted to appearance_settings.xml sidecar
  • dasher_set_system_appearance: transient OS appearance input from the frontend
  • dasher_get/set_light_palette + dasher_get/set_dark_palette: two independent persisted palette preferences
  • dasher_set_user_palette: picker convenience — sets the current-side pref and defaults the other side to the companion
  • Resolution (mode + system + prefs → active palette) lives entirely in DasherCore
  • No persistence leak: user's explicit choice is never overwritten by auto-switching

Bridge layer

Added to all 4 DasherBridge copies:

Method C API Purpose
setSystemAppearance(dark:) dasher_set_system_appearance Frontend reports OS appearance change
setAppearanceMode(_:) dasher_set_appearance_mode System / Light / Dark mode
getAppearanceMode() dasher_get_appearance_mode Read current mode
setUserPalette(_:) dasher_set_user_palette Picker convenience
getLightPalette() dasher_get_light_palette Read light preference
getDarkPalette() dasher_get_dark_palette Read dark preference

Frontend wiring

Platform How system appearance is reported
DasherApp (iOS) @Environment(\.colorScheme) + .onAppear + .onChange in ContentView
DasherMac @Environment(\.colorScheme) + .onAppear + .onChange in MacContentView
DasherVision .onAppear { setSystemAppearance(dark: true) } — visionOS is always dark
DasherKeyboard traitCollectionDidChange + viewWillAppear in KeyboardViewController

Settings UI

DasherApp's Customization tab now has:

  • Appearance segmented picker: System / Light / Dark
  • Palette picker: calls setUserPalette (works with the dual-pref model — sets the current side, defaults the other side to the companion)

When mode is System: the palette auto-switches when iOS flips between light and dark. The user's chosen palette is the light preference; its dark companion is used automatically.

When mode is Light or Dark: forces that appearance regardless of system setting.

How it works end-to-end

1. User picks "Rainbow" in the palette picker
   → bridge.setUserPalette("Rainbow")
   → DasherCore stores "Rainbow" as the light pref, defaults dark pref to "Rainbow Dark"

2. iOS switches to dark mode
   → ContentView.onChange(colorScheme) fires
   → bridge.setSystemAppearance(dark: true)
   → DasherCore resolves: mode=SYSTEM, system=DARK, darkPref="Rainbow Dark"
   → dasher_get_current_palette() now returns "Rainbow Dark"

3. iOS switches back to light
   → bridge.setSystemAppearance(dark: false)
   → DasherCore resolves: lightPref="Rainbow"
   → palette switches back to "Rainbow"

4. App restarts
   → appearance_settings.xml has: mode=SYSTEM, lightPref="Rainbow", darkPref="Rainbow Dark"
   → User's original choice preserved — never clobbered by auto-switching

Testing checklist

  • DasherApp (iOS): switch system dark/light → palette flips to companion
  • DasherApp settings: pick "Dark" mode → palette goes dark immediately
  • DasherApp settings: pick "Light" mode → palette goes light even if system is dark
  • DasherApp settings: pick "System" → follows OS
  • DasherApp: force-quit, reopen → palette preference preserved
  • DasherMac: same tests via System Settings → Appearance
  • DasherKeyboard: switch iOS dark/light → keyboard palette follows
  • Palette picker with "Yellow on Blue" (no companion) → stays regardless of mode

willwade added 5 commits June 20, 2026 06:15
Uses the stateful appearance model from DasherCore #25:

DasherCore: e8e394b5 -> fc61344f

The v2 model owns appearance state at the C API layer:
- dasher_get/set_appearance_mode: System / Light / Dark (persisted)
- dasher_set_system_appearance: transient OS input from frontend
- dasher_get/set_light_palette + dark_palette: independent prefs
- dasher_set_user_palette: picker convenience (sets current side)
- Resolution (mode + system + prefs -> active palette) in DasherCore
- Persists to appearance_settings.xml sidecar, never clobbers
  the user's explicit palette choice

Bridge methods added to all 4 platforms:
- setSystemAppearance(dark:) - frontend reports OS state
- setAppearanceMode(_:) / getAppearanceMode() - mode control
- setUserPalette(_:) / getLightPalette() / getDarkPalette()

Frontend wiring:
- DasherApp (iOS): @Environment(\.colorScheme) + onChange
- DasherMac: same @Environment(\.colorScheme) pattern
- DasherVision: forced dark on appear
- DasherKeyboard: traitCollectionDidChange + viewWillAppear

Settings UI (DasherApp):
- Appearance segmented picker: System / Light / Dark
- Palette picker calls setUserPalette (works with the new model)

Signed-off-by: will wade <willwade@gmail.com>
The single palette picker conflated 'which palette am I editing' with
'what mode am I in'. Users couldn't change their dark palette without
first forcing Dark mode, and the mode picker sometimes bounced back.

Replace with two independent palette pickers:
- 'Light Palette' — edits the light preference via setLightPalette
- 'Dark Palette'  — edits the dark preference via setDarkPalette

The mode picker (System / Light / Dark) above controls which palette
is ACTIVE on the canvas. The two palette pickers below control which
palette is USED for each appearance — independently of the current mode.

Extracted palettePickerRow() helper to avoid duplicating the swatch
rendering between the two pickers.

Signed-off-by: will wade <willwade@gmail.com>
Fixes build error: DasherSettingsView calls setLightPalette and
setDarkPalette for the dual palette pickers but those methods were
not in the bridge.

Signed-off-by: will wade <willwade@gmail.com>
sed appended the new methods inside setUserPalette's body instead
of after its closing brace. Unmangled all three (Keyboard, Mac,
Vision).

Signed-off-by: will wade <willwade@gmail.com>
…View missing onAppear

Two issues found during review:

1. MacContentView: sed inserted .onAppear/.onChange after the body's
   closing brace instead of before it. Moved them inside the body
   as modifiers on the .toolbar.

2. VisionContentView: the sed substitution silently failed (the $ and
   & characters in the pattern were interpreted by sed, not as literal
   Swift code). Added .onAppear { setSystemAppearance(dark: true) }
   manually.

Also verified: no stale references to the removed dasher_set_appearance
API anywhere in the Apple frontends.

Signed-off-by: will wade <willwade@gmail.com>
@willwade willwade force-pushed the feature/dark-mode-v2 branch from 8bb53ea to 4dcc03f Compare June 20, 2026 07:02
@willwade willwade merged commit 07c88ea into main Jun 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant