Browse / github.com/storytold/soundcraft / patch
Give each edit mode its own colour when it is active
Give each edit mode its own colour when it is active ## Problem and resulting behavior Shuffle, Spot, Slip, Grid and Relative Grid all lit up the same green, so the active mode could only be told apart by reading its label. Each now has its own colour: | Mode | Colour | |---|---| | Shuffle | red | | Spot | orange | | Slip | green | | Grid | blue | | Relative Grid | violet | ## Change One active-fill token per mode, in both themes (`theme.rs`, read through `Tokens::edit_mode_on`). - The Grid key lights for both Grid and Relative Grid, in the colour of whichever is active. - Labels keep `mode_on_text` and reach at least 4.5:1 contrast on ever
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- lucasegen (imported from pull request #969, open)
- Base
0a95b6dadf50377cac5891bfe27fd82c5668a5f0- Size
- +73 −3 in 3 files
- Review
- pending
- Shared
- 2026-10-28
- Id
f0b2f38e109601cd8b973e0a
How it went
No other operator has reported using it yet.
Diff
raw · crates/ui-egui/src/theme.rs crates/ui-egui/src/toolbar.rs crates/ui-egui/src/widgets.rs
@@ -1,6 +1,7 @@ //! Design tokens. Colours were chosen by eye to give a dark, studio-style look; they are ours. use egui::{Color32, FontFamily, FontId};+use soundcraft_model::EditMode; use std::sync::atomic::{AtomicBool, Ordering}; /// Whether the Light palette is active. Dark is the default (the incumbent is dark-only).@@ -165,6 +166,13 @@ pub struct Tokens { pub group_border: Color32, /// Edit-mode cell, not selected. pub mode_off: Color32,+ /// Edit-mode cell, selected: one colour per mode so the active mode reads at a glance+ /// (see [`Tokens::edit_mode_on`]). The label on them is `mode_on_text`.+ pub mode_shuffle: Color32,+ pub mode_spot: Color32,+ pub mode_slip: Color32,+ pub mode_grid: Color32,+ pub mode_grid_relative: Color32, /// Counter-panel toggle (Grid, Count Off), off. pub counter_off: Color32, /// Outline of the counter panels.@@ -272,6 +280,11 @@ impl Tokens { menu_bar: Color32::from_rgb(22, 22, 23), group_border: Color32::from_rgb(8, 8, 8), mode_off: Color32::from_rgb(20, 36, 22),+ mode_shuffle: Color32::from_rgb(246, 112, 102),+ mode_spot: Color32::from_rgb(236, 148, 38),+ mode_slip: Color32::from_rgb(92, 196, 108),+ mode_grid: Color32::from_rgb(86, 162, 238),+ mode_grid_relative: Color32::from_rgb(176, 134, 238), counter_off: Color32::from_rgb(30, 50, 32), counter_border: Color32::from_rgb(50, 50, 50), counter_rule: Color32::from_rgb(40, 40, 40),@@ -357,6 +370,11 @@ impl Tokens { menu_bar: Color32::from_rgb(204, 206, 210), group_border: Color32::from_rgb(118, 121, 127), mode_off: Color32::from_rgb(202, 212, 204),+ mode_shuffle: Color32::from_rgb(188, 48, 42),+ mode_spot: Color32::from_rgb(176, 94, 4),+ mode_slip: Color32::from_rgb(44, 128, 62),+ mode_grid: Color32::from_rgb(62, 116, 180),+ mode_grid_relative: Color32::from_rgb(124, 82, 178), counter_off: Color32::from_rgb(192, 212, 198), counter_border: Color32::from_rgb(118, 121, 127), counter_rule: Color32::from_rgb(188, 191, 196),@@ -386,6 +404,19 @@ impl Tokens { pub fn current() -> Tokens { if is_light() { Self::LIGHT } else { Self::DARK } }++ /// Fill of the edit-mode key while `mode` is the active one.+ // ponytail: one label colour (`mode_on_text`) serves the five fills; Light needs fills dark enough for+ // white text, so Spot is a deep orange. A per-mode label token is the way out if a fill must go lighter.+ pub fn edit_mode_on(&self, mode: EditMode) -> Color32 {+ match mode {+ EditMode::Shuffle => self.mode_shuffle,+ EditMode::Spot => self.mode_spot,+ EditMode::Slip => self.mode_slip,+ EditMode::Grid => self.mode_grid,+ EditMode::GridRelative => self.mode_grid_relative,+ }+ } } /// Whether the Light palette is the active one.@@ -620,4 +651,36 @@ mod tests { } } }++ #[test]+ fn each_edit_mode_has_its_own_readable_active_colour() {+ // WCAG relative luminance and contrast ratio.+ fn lum(c: Color32) -> f64 {+ let lin = |v: u8| {+ let v = f64::from(v) / 255.0;+ if v <= 0.04045 { v / 12.92 } else { ((v + 0.055) / 1.055).powf(2.4) }+ };+ 0.2126 * lin(c.r()) + 0.7152 * lin(c.g()) + 0.0722 * lin(c.b())+ }+ fn contrast(a: Color32, b: Color32) -> f64 {+ let (a, b) = (lum(a), lum(b));+ (a.max(b) + 0.05) / (a.min(b) + 0.05)+ }+ let modes = [EditMode::Shuffle, EditMode::Spot, EditMode::Slip, EditMode::Grid, EditMode::GridRelative];+ for (name, t) in [("dark", Tokens::DARK), ("light", Tokens::LIGHT)] {+ for (i, a) in modes.into_iter().enumerate() {+ let fill = t.edit_mode_on(a);+ assert_ne!(fill, t.mode_off, "{name}: {a:?} active must differ from inactive");+ // Small bold label: WCAG AA wants 4.5:1, at rest and at both ends of the pressed gradient.+ let pressed = crate::widgets::PRESSED_SHADE.map(|k| crate::widgets::shade(fill, k));+ for (state, face) in [("active", fill), ("pressed top", pressed[0]), ("pressed bottom", pressed[1])] {+ let ratio = contrast(face, t.mode_on_text);+ assert!(ratio >= 4.5, "{name}: {a:?} {state} label contrast {ratio:.2} is below 4.5");+ }+ for b in &modes[i + 1..] {+ assert_ne!(fill, t.edit_mode_on(*b), "{name}: {a:?} and {b:?} share an active colour");+ }+ }+ }+ } }@@ -60,10 +60,13 @@ fn edit_mode_tooltip(m: EditMode, locked: bool, cmd: &str, fkey: &str) -> String fn draw_mode_cell(p: &egui::Painter, cell: Rect, resp: &egui::Response, t: &Tokens, m: EditMode, state: &ModeCellState, label: &str) { let on = state.cur == m || (m == EditMode::Grid && state.cur == EditMode::GridRelative);- key(p, cell, resp, if on { t.mode_on } else { t.mode_off });+ // `on` means `cur == m`, or Grid showing Relative Grid: the active colour is always cur's.+ key(p, cell, resp, if on { t.edit_mode_on(state.cur) } else { t.mode_off }); let fg = if on { t.mode_on_text } else { t.mode_off_text }; if m == EditMode::Grid && state.snap {- p.rect_stroke(cell, 1.0, Stroke::new(1.5, t.mode_on), StrokeKind::Inside);+ // The Snap to Grid outline wears the Grid colour, so it never reads as the green of Slip.+ let grid = t.edit_mode_on(if state.cur.is_grid() { state.cur } else { EditMode::Grid });+ p.rect_stroke(cell, 1.0, Stroke::new(1.5, grid), StrokeKind::Inside); } if m == EditMode::Shuffle && state.locked { icons::draw(p, Rect::from_center_size(cell.center(), vec2(15.0, 15.0)), "lock", fg);@@ -69,11 +69,15 @@ pub fn key_fill(resp: &Response) -> Color32 { if resp.hovered() { t.button_hi } else { t.button } } +/// The [`shade`] of a pressed key's face, top and bottom of its gradient.+pub const PRESSED_SHADE: [f32; 2] = [-0.14, 0.02];+ /// A key in `fill`, softly lit from above; it sinks while pressed. pub fn key(p: &Painter, r: Rect, resp: &Response, fill: Color32) { let face = r.shrink(1.0); if resp.is_pointer_button_down_on() {- vgradient(p, face, shade(fill, -0.14), shade(fill, 0.02));+ let [top, bottom] = PRESSED_SHADE;+ vgradient(p, face, shade(fill, top), shade(fill, bottom)); } else { p.rect_filled(face, 0.0, fill); key_light(p, face);