From d8365ee544527d19ce18cd51440db3244a9e3e24 Mon Sep 17 00:00:00 2001 From: will wade Date: Sat, 20 Jun 2026 06:15:24 +0100 Subject: [PATCH 1/5] Wire up dark mode across all Apple frontends (RFC 0007 v2) 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 --- DasherApp/Sources/ContentView.swift | 5 ++++ DasherApp/Sources/DasherBridge.swift | 28 +++++++++++++++++++ DasherApp/Sources/DasherSettingsView.swift | 13 ++++++++- DasherCore | 2 +- DasherKeyboard/Sources/DasherBridge.swift | 28 +++++++++++++++++++ .../Sources/KeyboardViewController.swift | 7 +++++ DasherMac/Sources/DasherBridge.swift | 28 +++++++++++++++++++ DasherMac/Sources/MacContentView.swift | 5 ++++ DasherVision/Sources/DasherBridge.swift | 28 +++++++++++++++++++ 9 files changed, 142 insertions(+), 2 deletions(-) diff --git a/DasherApp/Sources/ContentView.swift b/DasherApp/Sources/ContentView.swift index 40abbdc..a5276c4 100644 --- a/DasherApp/Sources/ContentView.swift +++ b/DasherApp/Sources/ContentView.swift @@ -11,6 +11,7 @@ struct ContentView: View { @State private var showOpenFile = false @State private var outputPaneFraction: CGFloat = 2.0 / 9.0 @State private var showAlphabetPopover = false + @Environment(\.colorScheme) private var colorScheme var body: some View { GeometryReader { geometry in @@ -67,6 +68,10 @@ struct ContentView: View { .padding(.top, 8) } } + .onAppear { viewModel.bridge.setSystemAppearance(dark: colorScheme == .dark) } + .onChange(of: colorScheme) { _, newScheme in + viewModel.bridge.setSystemAppearance(dark: newScheme == .dark) + } } // MARK: - Layouts diff --git a/DasherApp/Sources/DasherBridge.swift b/DasherApp/Sources/DasherBridge.swift index ce768b2..7b60673 100644 --- a/DasherApp/Sources/DasherBridge.swift +++ b/DasherApp/Sources/DasherBridge.swift @@ -350,6 +350,34 @@ class DasherBridge: InputMethodBridge { return String(cString: cStr) } + + // MARK: - Appearance / dark mode (RFC 0007) + + func setSystemAppearance(dark: Bool) { + guard let ctx = ctx else { return } + dasher_set_system_appearance(ctx, dark ? 2 : 1) + } + func setAppearanceMode(_ mode: Int) { + guard let ctx = ctx else { return } + dasher_set_appearance_mode(ctx, Int32(mode)) + } + func getAppearanceMode() -> Int { + guard let ctx = ctx else { return 0 } + return Int(dasher_get_appearance_mode(ctx)) + } + func setUserPalette(_ name: String) { + guard let ctx = ctx else { return } + dasher_set_user_palette(ctx, name) + } + func getLightPalette() -> String { + guard let ctx = ctx, let cStr = dasher_get_light_palette(ctx) else { return "" } + return String(cString: cStr) + } + func getDarkPalette() -> String { + guard let ctx = ctx, let cStr = dasher_get_dark_palette(ctx) else { return "" } + return String(cString: cStr) + } + // MARK: - Alphabets var alphabetCount: Int { diff --git a/DasherApp/Sources/DasherSettingsView.swift b/DasherApp/Sources/DasherSettingsView.swift index 8a629c7..bf0e37d 100644 --- a/DasherApp/Sources/DasherSettingsView.swift +++ b/DasherApp/Sources/DasherSettingsView.swift @@ -169,6 +169,17 @@ struct DasherSettingsView: View { private func customizationSection(_ params: [DasherParameterInfo]) -> some View { Section { + // Appearance mode: System / Light / Dark + Picker("Appearance", selection: Binding( + get: { viewModel.bridge.getAppearanceMode() }, + set: { viewModel.bridge.setAppearanceMode($0) } + )) { + Text("System").tag(0) + Text("Light").tag(1) + Text("Dark").tag(2) + } + .pickerStyle(.segmented) + let palettes = viewModel.bridge.allPalettes if !palettes.isEmpty { VStack(alignment: .leading, spacing: 8) { @@ -179,7 +190,7 @@ struct DasherSettingsView: View { ForEach(0.. Int { + guard let ctx = ctx else { return 0 } + return Int(dasher_get_appearance_mode(ctx)) + } + func setUserPalette(_ name: String) { + guard let ctx = ctx else { return } + dasher_set_user_palette(ctx, name) + } + func getLightPalette() -> String { + guard let ctx = ctx, let cStr = dasher_get_light_palette(ctx) else { return "" } + return String(cString: cStr) + } + func getDarkPalette() -> String { + guard let ctx = ctx, let cStr = dasher_get_dark_palette(ctx) else { return "" } + return String(cString: cStr) + } + // MARK: - Alphabets var alphabetCount: Int { diff --git a/DasherKeyboard/Sources/KeyboardViewController.swift b/DasherKeyboard/Sources/KeyboardViewController.swift index f0c1a36..95bfe9b 100644 --- a/DasherKeyboard/Sources/KeyboardViewController.swift +++ b/DasherKeyboard/Sources/KeyboardViewController.swift @@ -118,6 +118,7 @@ class KeyboardViewController: UIInputViewController { } @objc private func resetTapped() { + viewModel?.bridge.setSystemAppearance(dark: traitCollection.userInterfaceStyle == .dark) viewModel?.bridge.reset() } @@ -133,10 +134,16 @@ class KeyboardViewController: UIInputViewController { super.viewWillAppear(animated) // The bridge is a singleton so its model state survives across open/close // cycles. Reset to the root so each keyboard session starts fresh. + viewModel?.bridge.setSystemAppearance(dark: traitCollection.userInterfaceStyle == .dark) viewModel?.bridge.reset() canvas?.requestRedraw() } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + viewModel?.bridge.setSystemAppearance(dark: traitCollection.userInterfaceStyle == .dark) + } + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() viewModel?.setCanvasSize(canvas?.bounds.size ?? view.bounds.size) diff --git a/DasherMac/Sources/DasherBridge.swift b/DasherMac/Sources/DasherBridge.swift index 64c914a..a0cc36f 100644 --- a/DasherMac/Sources/DasherBridge.swift +++ b/DasherMac/Sources/DasherBridge.swift @@ -353,6 +353,34 @@ class DasherBridge: InputMethodBridge, DasherBridgeProtocol { return String(cString: cStr) } + + // MARK: - Appearance / dark mode (RFC 0007) + + func setSystemAppearance(dark: Bool) { + guard let ctx = ctx else { return } + dasher_set_system_appearance(ctx, dark ? 2 : 1) + } + func setAppearanceMode(_ mode: Int) { + guard let ctx = ctx else { return } + dasher_set_appearance_mode(ctx, Int32(mode)) + } + func getAppearanceMode() -> Int { + guard let ctx = ctx else { return 0 } + return Int(dasher_get_appearance_mode(ctx)) + } + func setUserPalette(_ name: String) { + guard let ctx = ctx else { return } + dasher_set_user_palette(ctx, name) + } + func getLightPalette() -> String { + guard let ctx = ctx, let cStr = dasher_get_light_palette(ctx) else { return "" } + return String(cString: cStr) + } + func getDarkPalette() -> String { + guard let ctx = ctx, let cStr = dasher_get_dark_palette(ctx) else { return "" } + return String(cString: cStr) + } + // MARK: - Alphabets var alphabetCount: Int { diff --git a/DasherMac/Sources/MacContentView.swift b/DasherMac/Sources/MacContentView.swift index 20633a0..b7c7ca1 100644 --- a/DasherMac/Sources/MacContentView.swift +++ b/DasherMac/Sources/MacContentView.swift @@ -8,6 +8,7 @@ struct MacContentView: View { @State private var showSettings = false @State private var currentLayoutPosition = "Right" @State private var outputPaneFraction: CGFloat = 2.0 / 9.0 + @Environment(\.colorScheme) private var colorScheme var body: some View { VStack(spacing: 0) { @@ -71,6 +72,10 @@ struct MacContentView: View { } } + .onAppear { viewModel.bridge.setSystemAppearance(dark: colorScheme == .dark) } + .onChange(of: colorScheme) { _, newScheme in + viewModel.bridge.setSystemAppearance(dark: newScheme == .dark) + } private var layoutPickerMenu: some View { Menu { Button("Right side") { currentLayoutPosition = "Right" } diff --git a/DasherVision/Sources/DasherBridge.swift b/DasherVision/Sources/DasherBridge.swift index 1fe7438..98486be 100644 --- a/DasherVision/Sources/DasherBridge.swift +++ b/DasherVision/Sources/DasherBridge.swift @@ -315,6 +315,34 @@ class DasherBridge: InputMethodBridge { return String(cString: cStr) } + + // MARK: - Appearance / dark mode (RFC 0007) + + func setSystemAppearance(dark: Bool) { + guard let ctx = ctx else { return } + dasher_set_system_appearance(ctx, dark ? 2 : 1) + } + func setAppearanceMode(_ mode: Int) { + guard let ctx = ctx else { return } + dasher_set_appearance_mode(ctx, Int32(mode)) + } + func getAppearanceMode() -> Int { + guard let ctx = ctx else { return 0 } + return Int(dasher_get_appearance_mode(ctx)) + } + func setUserPalette(_ name: String) { + guard let ctx = ctx else { return } + dasher_set_user_palette(ctx, name) + } + func getLightPalette() -> String { + guard let ctx = ctx, let cStr = dasher_get_light_palette(ctx) else { return "" } + return String(cString: cStr) + } + func getDarkPalette() -> String { + guard let ctx = ctx, let cStr = dasher_get_dark_palette(ctx) else { return "" } + return String(cString: cStr) + } + // MARK: - Alphabets var alphabetCount: Int { From 7ab92f6f381c2affcabd65a06c71748f8e95f0b6 Mon Sep 17 00:00:00 2001 From: will wade Date: Sat, 20 Jun 2026 06:53:36 +0100 Subject: [PATCH 2/5] Rework appearance settings: separate light/dark palette pickers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- DasherApp/Sources/DasherSettingsView.swift | 90 ++++++++++++++-------- 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/DasherApp/Sources/DasherSettingsView.swift b/DasherApp/Sources/DasherSettingsView.swift index bf0e37d..a4b48a5 100644 --- a/DasherApp/Sources/DasherSettingsView.swift +++ b/DasherApp/Sources/DasherSettingsView.swift @@ -169,7 +169,6 @@ struct DasherSettingsView: View { private func customizationSection(_ params: [DasherParameterInfo]) -> some View { Section { - // Appearance mode: System / Light / Dark Picker("Appearance", selection: Binding( get: { viewModel.bridge.getAppearanceMode() }, set: { viewModel.bridge.setAppearanceMode($0) } @@ -182,39 +181,19 @@ struct DasherSettingsView: View { let palettes = viewModel.bridge.allPalettes if !palettes.isEmpty { - VStack(alignment: .leading, spacing: 8) { - Text("Colour Theme") - .font(.subheadline) - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 10) { - ForEach(0.. Void + ) -> some View { + VStack(alignment: .leading, spacing: 8) { + Text(label) + .font(.subheadline) + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 10) { + ForEach(0.. some View { From 7612d765890d5dc371de30586240034b10804f40 Mon Sep 17 00:00:00 2001 From: will wade Date: Sat, 20 Jun 2026 07:22:15 +0100 Subject: [PATCH 3/5] Add missing setLightPalette/setDarkPalette to all 4 bridges 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 --- DasherApp/Sources/DasherBridge.swift | 8 ++++++++ DasherKeyboard/Sources/DasherBridge.swift | 8 ++++++++ DasherMac/Sources/DasherBridge.swift | 8 ++++++++ DasherVision/Sources/DasherBridge.swift | 8 ++++++++ 4 files changed, 32 insertions(+) diff --git a/DasherApp/Sources/DasherBridge.swift b/DasherApp/Sources/DasherBridge.swift index 7b60673..850b517 100644 --- a/DasherApp/Sources/DasherBridge.swift +++ b/DasherApp/Sources/DasherBridge.swift @@ -369,6 +369,14 @@ class DasherBridge: InputMethodBridge { guard let ctx = ctx else { return } dasher_set_user_palette(ctx, name) } + func setLightPalette(_ name: String) { + guard let ctx = ctx else { return } + dasher_set_light_palette(ctx, name) + } + func setDarkPalette(_ name: String) { + guard let ctx = ctx else { return } + dasher_set_dark_palette(ctx, name) + } func getLightPalette() -> String { guard let ctx = ctx, let cStr = dasher_get_light_palette(ctx) else { return "" } return String(cString: cStr) diff --git a/DasherKeyboard/Sources/DasherBridge.swift b/DasherKeyboard/Sources/DasherBridge.swift index 54aecff..ee18653 100644 --- a/DasherKeyboard/Sources/DasherBridge.swift +++ b/DasherKeyboard/Sources/DasherBridge.swift @@ -358,6 +358,14 @@ class DasherBridge: InputMethodBridge { } func setUserPalette(_ name: String) { guard let ctx = ctx else { return } + func setLightPalette(_ name: String) { + guard let ctx = ctx else { return } + dasher_set_light_palette(ctx, name) + } + func setDarkPalette(_ name: String) { + guard let ctx = ctx else { return } + dasher_set_dark_palette(ctx, name) + } dasher_set_user_palette(ctx, name) } func getLightPalette() -> String { diff --git a/DasherMac/Sources/DasherBridge.swift b/DasherMac/Sources/DasherBridge.swift index a0cc36f..5b14b7d 100644 --- a/DasherMac/Sources/DasherBridge.swift +++ b/DasherMac/Sources/DasherBridge.swift @@ -370,6 +370,14 @@ class DasherBridge: InputMethodBridge, DasherBridgeProtocol { } func setUserPalette(_ name: String) { guard let ctx = ctx else { return } + func setLightPalette(_ name: String) { + guard let ctx = ctx else { return } + dasher_set_light_palette(ctx, name) + } + func setDarkPalette(_ name: String) { + guard let ctx = ctx else { return } + dasher_set_dark_palette(ctx, name) + } dasher_set_user_palette(ctx, name) } func getLightPalette() -> String { diff --git a/DasherVision/Sources/DasherBridge.swift b/DasherVision/Sources/DasherBridge.swift index 98486be..67107fd 100644 --- a/DasherVision/Sources/DasherBridge.swift +++ b/DasherVision/Sources/DasherBridge.swift @@ -332,6 +332,14 @@ class DasherBridge: InputMethodBridge { } func setUserPalette(_ name: String) { guard let ctx = ctx else { return } + func setLightPalette(_ name: String) { + guard let ctx = ctx else { return } + dasher_set_light_palette(ctx, name) + } + func setDarkPalette(_ name: String) { + guard let ctx = ctx else { return } + dasher_set_dark_palette(ctx, name) + } dasher_set_user_palette(ctx, name) } func getLightPalette() -> String { From fa475afa72c5cd160d33440a191d3cc7bf0e7d57 Mon Sep 17 00:00:00 2001 From: will wade Date: Sat, 20 Jun 2026 07:29:24 +0100 Subject: [PATCH 4/5] Fix mangled setLightPalette/setDarkPalette in 3 bridges 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 --- DasherKeyboard/Sources/DasherBridge.swift | 4 ++-- DasherMac/Sources/DasherBridge.swift | 4 ++-- DasherVision/Sources/DasherBridge.swift | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DasherKeyboard/Sources/DasherBridge.swift b/DasherKeyboard/Sources/DasherBridge.swift index ee18653..02f389e 100644 --- a/DasherKeyboard/Sources/DasherBridge.swift +++ b/DasherKeyboard/Sources/DasherBridge.swift @@ -358,6 +358,8 @@ class DasherBridge: InputMethodBridge { } func setUserPalette(_ name: String) { guard let ctx = ctx else { return } + dasher_set_user_palette(ctx, name) + } func setLightPalette(_ name: String) { guard let ctx = ctx else { return } dasher_set_light_palette(ctx, name) @@ -365,8 +367,6 @@ class DasherBridge: InputMethodBridge { func setDarkPalette(_ name: String) { guard let ctx = ctx else { return } dasher_set_dark_palette(ctx, name) - } - dasher_set_user_palette(ctx, name) } func getLightPalette() -> String { guard let ctx = ctx, let cStr = dasher_get_light_palette(ctx) else { return "" } diff --git a/DasherMac/Sources/DasherBridge.swift b/DasherMac/Sources/DasherBridge.swift index 5b14b7d..04a9712 100644 --- a/DasherMac/Sources/DasherBridge.swift +++ b/DasherMac/Sources/DasherBridge.swift @@ -370,6 +370,8 @@ class DasherBridge: InputMethodBridge, DasherBridgeProtocol { } func setUserPalette(_ name: String) { guard let ctx = ctx else { return } + dasher_set_user_palette(ctx, name) + } func setLightPalette(_ name: String) { guard let ctx = ctx else { return } dasher_set_light_palette(ctx, name) @@ -377,8 +379,6 @@ class DasherBridge: InputMethodBridge, DasherBridgeProtocol { func setDarkPalette(_ name: String) { guard let ctx = ctx else { return } dasher_set_dark_palette(ctx, name) - } - dasher_set_user_palette(ctx, name) } func getLightPalette() -> String { guard let ctx = ctx, let cStr = dasher_get_light_palette(ctx) else { return "" } diff --git a/DasherVision/Sources/DasherBridge.swift b/DasherVision/Sources/DasherBridge.swift index 67107fd..e0d72fd 100644 --- a/DasherVision/Sources/DasherBridge.swift +++ b/DasherVision/Sources/DasherBridge.swift @@ -332,6 +332,8 @@ class DasherBridge: InputMethodBridge { } func setUserPalette(_ name: String) { guard let ctx = ctx else { return } + dasher_set_user_palette(ctx, name) + } func setLightPalette(_ name: String) { guard let ctx = ctx else { return } dasher_set_light_palette(ctx, name) @@ -339,8 +341,6 @@ class DasherBridge: InputMethodBridge { func setDarkPalette(_ name: String) { guard let ctx = ctx else { return } dasher_set_dark_palette(ctx, name) - } - dasher_set_user_palette(ctx, name) } func getLightPalette() -> String { guard let ctx = ctx, let cStr = dasher_get_light_palette(ctx) else { return "" } From 4dcc03fcc4982c153046c385a9b18eebacb61f73 Mon Sep 17 00:00:00 2001 From: will wade Date: Sat, 20 Jun 2026 07:51:54 +0100 Subject: [PATCH 5/5] Fix code review: MacContentView modifiers outside body, VisionContentView 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 --- DasherCore | 2 +- DasherMac/Sources/MacContentView.swift | 4 ++-- DasherVision/Sources/VisionContentView.swift | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/DasherCore b/DasherCore index fc61344..5d6cfa0 160000 --- a/DasherCore +++ b/DasherCore @@ -1 +1 @@ -Subproject commit fc61344f76e8b5d850ca9d1cb8b44dbcc5bfc31d +Subproject commit 5d6cfa0d4a65becda69af14eec9d679f42717456 diff --git a/DasherMac/Sources/MacContentView.swift b/DasherMac/Sources/MacContentView.swift index b7c7ca1..7b7e027 100644 --- a/DasherMac/Sources/MacContentView.swift +++ b/DasherMac/Sources/MacContentView.swift @@ -70,12 +70,12 @@ struct MacContentView: View { } } } - } - .onAppear { viewModel.bridge.setSystemAppearance(dark: colorScheme == .dark) } .onChange(of: colorScheme) { _, newScheme in viewModel.bridge.setSystemAppearance(dark: newScheme == .dark) } + } + private var layoutPickerMenu: some View { Menu { Button("Right side") { currentLayoutPosition = "Right" } diff --git a/DasherVision/Sources/VisionContentView.swift b/DasherVision/Sources/VisionContentView.swift index 3ee94ea..0c43136 100644 --- a/DasherVision/Sources/VisionContentView.swift +++ b/DasherVision/Sources/VisionContentView.swift @@ -73,6 +73,7 @@ struct VisionContentView: View { .sheet(isPresented: $showSettings) { VisionSettingsView(viewModel: viewModel) } + .onAppear { viewModel.bridge.setSystemAppearance(dark: true) } } private func toolbarButton(_ icon: String, isAccent: Bool = false, _ action: @escaping () -> Void) -> some View {