From 9a2ff16849b41f3e27626e95067994b7b9733d28 Mon Sep 17 00:00:00 2001 From: kirillDevPro <113171057+kirillDevPro@users.noreply.github.com> Date: Tue, 1 Sep 2026 19:55:06 +0200 Subject: [PATCH] feat(badges): give the badge colour pickers the full palette and a hex field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A user with 23 detect types could reach only nine usable swatches, two of them white: the Badges tab built its pickers without passing a palette, so they fell back to MoonUI's ten theme colours. design::picker_palette already offers 65 swatches and the chart and strategy pickers already use it — the settings tab was simply never wired to it. Pass it here too, and let the hex readout be typed into, so a colour outside any palette is still reachable. Colours entered that way are remembered per theme and offered back at the front of the picker, which is what makes them usable across 23 badges rather than one. The MoonUI pin moves to the merged MoonColorPicker change the typed-hex field needs; nothing but MoonUI moved in the lock. --- Cargo.lock | 74 ++++++------ crates/moon-core/src/config/badges.rs | 52 +++++++- crates/moon-core/src/config/badges/tests.rs | 121 +++++++++++++++++++ crates/moon-ui-gpui/src/chart_tabs/mod.rs | 7 +- crates/moon-ui-gpui/src/design.rs | 8 ++ crates/moon-ui-gpui/src/settings/badges.rs | 104 +++++++++++++--- crates/moon-ui-gpui/src/settings/common.rs | 6 +- crates/moon-ui-gpui/src/settings/mod.rs | 2 +- crates/moon-ui-gpui/src/settings/share.rs | 9 +- crates/moon-ui-gpui/src/strategies/fields.rs | 6 +- 10 files changed, 322 insertions(+), 67 deletions(-) create mode 100644 crates/moon-core/src/config/badges/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 232b08ba..12c21f5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -255,7 +255,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -266,7 +266,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1673,7 +1673,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1905,7 +1905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2805,7 +2805,7 @@ dependencies = [ "hyper", "libc", "pin-project-lite", - "socket2 0.6.5", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -3771,7 +3771,7 @@ dependencies = [ [[package]] name = "moon-collections" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "indexmap", "moon-gpui-util", @@ -3818,7 +3818,7 @@ dependencies = [ [[package]] name = "moon-derive-refineable" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "proc-macro2", "quote", @@ -3828,7 +3828,7 @@ dependencies = [ [[package]] name = "moon-gpui" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "accesskit", "anyhow", @@ -3912,7 +3912,7 @@ dependencies = [ [[package]] name = "moon-gpui-linux" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "accesskit", "accesskit_unix", @@ -3963,7 +3963,7 @@ dependencies = [ [[package]] name = "moon-gpui-macos" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "accesskit", "accesskit_macos", @@ -4010,7 +4010,7 @@ dependencies = [ [[package]] name = "moon-gpui-macros" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -4021,7 +4021,7 @@ dependencies = [ [[package]] name = "moon-gpui-platform" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "console_error_panic_hook", "moon-gpui", @@ -4034,7 +4034,7 @@ dependencies = [ [[package]] name = "moon-gpui-shared-string" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "schemars", "serde", @@ -4044,7 +4044,7 @@ dependencies = [ [[package]] name = "moon-gpui-util" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "anyhow", "log", @@ -4054,7 +4054,7 @@ dependencies = [ [[package]] name = "moon-gpui-web" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "anyhow", "console_error_panic_hook", @@ -4078,7 +4078,7 @@ dependencies = [ [[package]] name = "moon-gpui-wgpu" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "anyhow", "bytemuck", @@ -4107,7 +4107,7 @@ dependencies = [ [[package]] name = "moon-gpui-windows" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "accesskit", "accesskit_windows", @@ -4135,7 +4135,7 @@ dependencies = [ [[package]] name = "moon-http-client" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "anyhow", "async-compression", @@ -4155,7 +4155,7 @@ dependencies = [ [[package]] name = "moon-media" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "anyhow", "bindgen", @@ -4170,7 +4170,7 @@ dependencies = [ [[package]] name = "moon-perf" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "moon-collections", "serde", @@ -4180,7 +4180,7 @@ dependencies = [ [[package]] name = "moon-refineable" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "moon-derive-refineable", ] @@ -4188,7 +4188,7 @@ dependencies = [ [[package]] name = "moon-scheduler" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "async-task", "backtrace", @@ -4203,7 +4203,7 @@ dependencies = [ [[package]] name = "moon-sum-tree" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "heapless", "log", @@ -4214,7 +4214,7 @@ dependencies = [ [[package]] name = "moon-ui" version = "0.1.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "moon-ui-components", "moon-ui-components-assets", @@ -4223,7 +4223,7 @@ dependencies = [ [[package]] name = "moon-ui-components" version = "0.1.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "aho-corasick", "anyhow", @@ -4273,7 +4273,7 @@ dependencies = [ [[package]] name = "moon-ui-components-assets" version = "0.1.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "anyhow", "log", @@ -4287,7 +4287,7 @@ dependencies = [ [[package]] name = "moon-ui-components-macros" version = "0.1.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "proc-macro2", "quote", @@ -4338,7 +4338,7 @@ dependencies = [ [[package]] name = "moon-util-macros" version = "0.0.0" -source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#3a005c158689f9ed27f1247975c8368be4654736" +source = "git+https://github.com/Moonbot-Tech/MoonUI?branch=master#a262ff8f2fa832a245859add9d1db5622c8e2afe" dependencies = [ "moon-perf", "quote", @@ -5631,7 +5631,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.3", "rustls", - "socket2 0.6.5", + "socket2 0.5.10", "thiserror 2.0.20", "tokio", "tracing", @@ -5669,9 +5669,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.5", + "socket2 0.5.10", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6233,7 +6233,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6830,7 +6830,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6888,7 +6888,7 @@ dependencies = [ "cfg-if", "libc", "psm", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7231,7 +7231,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7681,7 +7681,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -8601,7 +8601,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] diff --git a/crates/moon-core/src/config/badges.rs b/crates/moon-core/src/config/badges.rs index 0e34348c..353b02b1 100644 --- a/crates/moon-core/src/config/badges.rs +++ b/crates/moon-core/src/config/badges.rs @@ -110,12 +110,21 @@ impl BadgeEntry { } } +/// Maximum remembered custom colours. Matches the MoonUI colour-picker widget's own +/// `MAX_CUSTOM_COLORS`; a larger stored/seeded list here would just be silently trimmed the next +/// time a picker seeds itself from it. +const CUSTOM_COLORS_MAX: usize = 20; + /// Detection-badge configuration (portable `badges.json`). #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(default)] pub struct BadgesConfig { /// Entries by strategy kind (ordinal → code/colors/outline). pub entries: Vec, + /// Colours typed into a badge colour picker's hex field, most recent first. A picker-wide + /// reuse palette for the whole Badges tab (shared by every row's colour/outline pickers), not + /// per-row data — hence not a field on `BadgeEntry`. + pub custom_colors: Vec<[u8; 3]>, } /// Default 24 kinds: `(ordinal, name, code, dark_color, light_color)`. Colors come from @@ -171,6 +180,7 @@ impl Default for BadgesConfig { ..BadgeEntry::default() }) .collect(), + custom_colors: Vec::new(), } } } @@ -218,10 +228,45 @@ impl BadgesConfig { /// Parses badges.json text (clipboard paste / file contents). Requires an `entries` /// array; otherwise any JSON would silently produce the default (`serde(default)`). /// `None` means the text is not a badge configuration. - pub fn parse_share(text: &str) -> Option { + /// + /// `custom_colors` is always kept from `current` rather than the pasted text: it is local + /// reuse history, not a shared setting, and `to_share_string` writes the same struct back out + /// — so without this, pasting a colleague's `badges.json` (whose own history is unrelated, or + /// simply absent under `#[serde(default)]`) would silently wipe the local user's remembered + /// custom colours. + /// + /// Args: + /// text: Pasted `badges.json` text to validate and deserialize. + /// current: Local configuration whose custom-colour reuse history must survive the paste. + /// + /// Returns: + /// Parsed shared badge settings with `current.custom_colors`, or `None` when `text` does + /// not contain a badge `entries` array. + pub fn parse_share(text: &str, current: &Self) -> Option { let v: serde_json::Value = serde_json::from_str(text).ok()?; v.get("entries")?.as_array()?; - serde_json::from_str(text).ok() + let mut parsed: Self = serde_json::from_str(text).ok()?; + parsed.custom_colors = current.custom_colors.clone(); + Some(parsed) + } + + /// Remember a colour typed into a badge colour picker's hex field, most-recent-first, capped + /// at `CUSTOM_COLORS_MAX` (dropping the oldest). Mirrors the MoonUI widget's own dedupe/cap so + /// the persisted list and a freshly-seeded picker never disagree on order. + /// + /// Args: + /// color: RGB value the user committed through a picker hex field. + /// + /// Returns: + /// Whether the list actually changed — `false` when `color` was already the front entry. + pub fn remember_custom_color(&mut self, color: [u8; 3]) -> bool { + if self.custom_colors.first() == Some(&color) { + return false; + } + self.custom_colors.retain(|c| *c != color); + self.custom_colors.insert(0, color); + self.custom_colors.truncate(CUSTOM_COLORS_MAX); + true } /// Entry by ordinal (first match). @@ -254,3 +299,6 @@ impl BadgesConfig { .and_then(|e| e.outline_color(is_short, is_light)) } } + +#[cfg(test)] +mod tests; diff --git a/crates/moon-core/src/config/badges/tests.rs b/crates/moon-core/src/config/badges/tests.rs new file mode 100644 index 00000000..2c17d43c --- /dev/null +++ b/crates/moon-core/src/config/badges/tests.rs @@ -0,0 +1,121 @@ +//! Regression coverage for the badges custom-colour reuse palette and sharing boundary. + +use super::*; + +/// Regression target: removing the front-entry guard in `BadgesConfig::remember_custom_color` +/// duplicates the currently selected colour, making the Settings reuse palette grow on a no-op. +#[test] +fn remember_custom_color_prepends_new_values_and_leaves_the_current_front_unchanged() { + let initial = vec![[12, 34, 56], [78, 90, 12]]; + let new_color = [210, 45, 67]; + let mut config = BadgesConfig { + custom_colors: initial.clone(), + ..BadgesConfig::default() + }; + + assert!(config.remember_custom_color(new_color)); + assert_eq!( + config.custom_colors, + vec![new_color, initial[0], initial[1]], + "a newly typed colour must be the first reusable swatch" + ); + + let unchanged = config.custom_colors.clone(); + assert!(!config.remember_custom_color(unchanged[0])); + assert_eq!( + config.custom_colors, unchanged, + "reselecting the current swatch must not duplicate or reorder the palette" + ); +} + +/// Regression target: removing the existing-colour removal in `BadgesConfig::remember_custom_color` +/// leaves duplicate swatches after a previously used colour is selected again. +#[test] +fn remember_custom_color_moves_an_older_entry_to_the_front_without_a_duplicate() { + let mut config = BadgesConfig { + custom_colors: vec![[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]], + ..BadgesConfig::default() + }; + + assert!(config.remember_custom_color([7, 8, 9])); + assert_eq!( + config.custom_colors, + vec![[7, 8, 9], [1, 2, 3], [4, 5, 6], [10, 11, 12]], + "selecting an older swatch must promote that one value instead of cloning it" + ); +} + +/// Regression target: removing `truncate(CUSTOM_COLORS_MAX)` in +/// `BadgesConfig::remember_custom_color` lets the persisted reuse palette exceed the picker cap. +#[test] +fn remember_custom_color_evicts_only_the_oldest_values_at_the_picker_limit() { + let colors: Vec<[u8; 3]> = (0..=CUSTOM_COLORS_MAX as u8) + .map(|n| [n, 255 - n, n.wrapping_mul(7)]) + .collect(); + let mut config = BadgesConfig::default(); + + for color in colors.iter().copied() { + assert!(config.remember_custom_color(color)); + } + + let expected: Vec<[u8; 3]> = colors[1..].iter().rev().copied().collect(); + assert_eq!( + config.custom_colors, expected, + "the newest twenty swatches must remain in recency order after the oldest is evicted" + ); + assert_eq!(config.custom_colors.len(), CUSTOM_COLORS_MAX); + assert!(config.custom_colors.contains(&colors[10])); + assert!(!config.custom_colors.contains(&colors[0])); +} + +/// Regression target: deleting `parsed.custom_colors = current.custom_colors.clone();` in +/// `BadgesConfig::parse_share` imports a colleague's palette and replaces the local reuse history. +#[test] +fn parse_share_keeps_local_palette_when_pasted_text_carries_a_different_palette() { + let local_palette = vec![[21, 22, 23], [24, 25, 26]]; + let current = BadgesConfig { + custom_colors: local_palette.clone(), + ..BadgesConfig::default() + }; + let pasted = r#"{"entries": [], "custom_colors": [[201, 202, 203]]}"#; + + let parsed = BadgesConfig::parse_share(pasted, ¤t).expect("entries array is badges JSON"); + + assert_eq!( + parsed.custom_colors, local_palette, + "pasting another user's badges file must not replace this user's reusable colours" + ); +} + +/// Regression target: deleting `parsed.custom_colors = current.custom_colors.clone();` in +/// `BadgesConfig::parse_share` makes an older badges file silently clear the local reuse palette. +#[test] +fn parse_share_keeps_local_palette_when_pasted_text_has_no_palette_field() { + let local_palette = vec![[31, 32, 33], [34, 35, 36]]; + let current = BadgesConfig { + custom_colors: local_palette.clone(), + ..BadgesConfig::default() + }; + let pasted = r#"{"entries": []}"#; + + let parsed = BadgesConfig::parse_share(pasted, ¤t).expect("entries array is badges JSON"); + + assert_eq!( + parsed.custom_colors, local_palette, + "pasting an older badges file must not erase this user's reusable colours" + ); +} + +/// Regression target: removing the `entries` array validation in `BadgesConfig::parse_share` +/// accepts unrelated JSON as a badge configuration and resets Settings to serde defaults. +#[test] +fn parse_share_requires_an_entries_array_before_deserializing_badges() { + let current = BadgesConfig::default(); + + for text in ["{}", r#"{"entries": {}}"#, r#"{"other": []}"#, "not json"] { + assert!( + BadgesConfig::parse_share(text, ¤t).is_none(), + "{text:?} is not a badges configuration because it has no entries array" + ); + } +} diff --git a/crates/moon-ui-gpui/src/chart_tabs/mod.rs b/crates/moon-ui-gpui/src/chart_tabs/mod.rs index 6d0a98be..5ba51255 100644 --- a/crates/moon-ui-gpui/src/chart_tabs/mod.rs +++ b/crates/moon-ui-gpui/src/chart_tabs/mod.rs @@ -671,14 +671,15 @@ impl ChartTabs { let fig_color_picker = { let b = backend.read(cx); let init = b.fig_style(b.fig_tool).color; - let hsla: Hsla = - gpui::rgb(crate::design::rgb_to_u32([init[0], init[1], init[2]])).into(); + let hsla: Hsla = crate::design::rgb_bytes_to_hsla([init[0], init[1], init[2]]); cx.new(|cx| MoonColorPickerState::new(window, cx).default_value(hsla)) }; cx.subscribe( &fig_color_picker, |this, _st, ev: &MoonColorPickerEvent, cx| { - let MoonColorPickerEvent::Change(h) = ev; + let MoonColorPickerEvent::Change(h) = ev else { + return; + }; let c = crate::design::hsla_to_rgb8(*h); this.backend.update(cx, |b, bcx| { let tool = b.fig_tool; diff --git a/crates/moon-ui-gpui/src/design.rs b/crates/moon-ui-gpui/src/design.rs index 19a688d4..af02128e 100644 --- a/crates/moon-ui-gpui/src/design.rs +++ b/crates/moon-ui-gpui/src/design.rs @@ -279,6 +279,14 @@ pub fn rgb_to_u32(c: [u8; 3]) -> u32 { (c[0] as u32) << 16 | (c[1] as u32) << 8 | c[2] as u32 } +/// Convert palette/config `[u8; 3]` RGB bytes directly to a `MoonColorPicker` `Hsla` value. +/// +/// The one-liner every `MoonColorPickerState` seed/init site repeats (`rgb(rgb_to_u32(c)).into()`) +/// — kept here beside `rgb_to_u32` rather than re-derived at each call site. +pub fn rgb_bytes_to_hsla(c: [u8; 3]) -> Hsla { + rgb(rgb_to_u32(c)).into() +} + /// Build the explicit palette supplied to `MoonColorPicker::colors`. /// /// It contains five saturation/lightness variants for each of 12 hues plus five grays, for 65 diff --git a/crates/moon-ui-gpui/src/settings/badges.rs b/crates/moon-ui-gpui/src/settings/badges.rs index 7e5a857e..1d35e030 100644 --- a/crates/moon-ui-gpui/src/settings/badges.rs +++ b/crates/moon-ui-gpui/src/settings/badges.rs @@ -10,8 +10,8 @@ use gpui::prelude::FluentBuilder; use gpui::*; use moon_ui::{ MoonBadge, MoonBadgeSize, MoonBadgeVariant, MoonButton, MoonButtonSize, MoonCheckboxSize, - MoonColorPicker, MoonColorPickerState, MoonInput, MoonInputEvent, MoonInputState, MoonPalette, - MoonTooltipView, StyledExt, h_flex, rgba_from, v_flex, + MoonColorPicker, MoonColorPickerEvent, MoonColorPickerState, MoonInput, MoonInputEvent, + MoonInputState, MoonPalette, MoonTooltipView, StyledExt, h_flex, rgba_from, v_flex, }; use rust_i18n::t; @@ -67,7 +67,28 @@ fn badge_input( st } +/// Convert the draft's persisted custom-colour history to the widget's `Hsla` seed list. +/// +/// Every picker receives this same conversion, so a custom colour stays reusable across badge rows +/// after Settings rebuilds the editor state instead of surviving only in the picker that added it. +/// +/// Args: +/// cfg: Live configuration or the Settings draft that owns the persisted reuse history. +/// +/// Returns: +/// Custom colours in their persisted most-recent-first order, converted for MoonUI. +fn custom_colors_seed(cfg: &moon_core::config::AppConfig) -> Vec { + cfg.badges + .custom_colors + .iter() + .map(|c| design::rgb_bytes_to_hsla(*c)) + .collect() +} + /// Build a color picker bound through `BadgeEntry` accessors to draft `badges.entries[idx]`. +/// +/// Shared by all three per-row pickers (main colour, outline-long, outline-short), so every one +/// of them seeds from — and can add to — the same tab-wide custom-colour reuse list. fn entry_color( backend: &Entity, window: &mut Window, @@ -81,17 +102,19 @@ fn entry_color( // saved when this editor was built. The entry itself cannot answer that, so the mode is // resolved here, where the draft is in scope, and handed down: in the getter from the same // snapshot the entry is read out of, in the setter from the draft being written. - let init = { + let (init, custom_seed) = { let b = backend.read(cx); let cfg = b.preview.as_ref().unwrap_or(&b.config); let is_light = cfg.ui_theme_mode == UiThemeMode::Light; - cfg.badges + let init = cfg + .badges .entries .get(idx) .map(|e| get(e, is_light)) - .unwrap_or([0x97, 0x92, 0x8A]) + .unwrap_or([0x97, 0x92, 0x8A]); + (init, custom_colors_seed(cfg)) }; - super::draft_color(window, cx, init, move |p, cc| { + let st = super::draft_color(window, cx, init, move |p, cc| { let is_light = p.ui_theme_mode == UiThemeMode::Light; if let Some(e) = p.badges.entries.get_mut(idx) { if get(e, is_light) != cc { @@ -100,7 +123,54 @@ fn entry_color( } } false + }); + st.update(cx, |s, c| s.set_custom_colors(custom_seed, c)); + + // A colour committed through ANY of the tab's pickers is persisted once and fanned out to + // every OTHER already-built row so it is immediately reusable — never through `build()` again, + // which would tear down every row's `MoonInputState` (including whichever hex field the user + // is mid-edit in) and has no `Window` available here to rebuild with anyway. + cx.subscribe(&st, move |this, _emitter, ev: &MoonColorPickerEvent, cx| { + let MoonColorPickerEvent::CustomAdded(h) = ev else { + return; + }; + let c = super::hsla_u8(*h); + let changed = this.backend.update(cx, |b, bcx| { + let Some(p) = b.preview.as_mut() else { + return false; + }; + if !p.badges.remember_custom_color(c) { + return false; + } + bcx.notify(); + true + }); + if !changed { + return; + } + let list = { + let b = this.backend.read(cx); + custom_colors_seed(b.preview.as_ref().unwrap_or(&b.config)) + }; + let siblings: Vec> = this + .badges + .rows + .iter() + .flat_map(|row| { + [ + row.color.clone(), + row.outline_long.clone(), + row.outline_short.clone(), + ] + }) + .collect(); + for sibling in siblings { + sibling.update(cx, |s, c| s.set_custom_colors(list.clone(), c)); + } }) + .detach(); + + st } /// Build badge editor state from the current draft. @@ -390,7 +460,11 @@ impl SettingsView { ), ) }) - .child(div().flex_none().child(MoonColorPicker::new(&row.color))) + .child( + div() + .flex_none() + .child(MoonColorPicker::new(&row.color).colors(design::picker_palette())), + ) .child( div().flex_none().w(px(42.0)).flex().justify_center().child( MoonBadge::new(code) @@ -404,17 +478,13 @@ impl SettingsView { .child(outline_chk) .when(outline, |el| { el.child(cap("L")) - .child( - div() - .flex_none() - .child(MoonColorPicker::new(&row.outline_long)), - ) + .child(div().flex_none().child( + MoonColorPicker::new(&row.outline_long).colors(design::picker_palette()), + )) .child(cap("S")) - .child( - div() - .flex_none() - .child(MoonColorPicker::new(&row.outline_short)), - ) + .child(div().flex_none().child( + MoonColorPicker::new(&row.outline_short).colors(design::picker_palette()), + )) }) .child( MoonButton::new(SharedString::from(format!("badge-del-{idx}"))) diff --git a/crates/moon-ui-gpui/src/settings/common.rs b/crates/moon-ui-gpui/src/settings/common.rs index 818c1e0f..ed144666 100644 --- a/crates/moon-ui-gpui/src/settings/common.rs +++ b/crates/moon-ui-gpui/src/settings/common.rs @@ -141,10 +141,12 @@ pub(super) fn draft_color( apply: impl Fn(&mut AppConfig, [u8; 3]) -> bool + 'static, ) -> Entity { let st = cx.new(|cx| { - MoonColorPickerState::new(window, cx).default_value(rgb(design::rgb_to_u32(init)).into()) + MoonColorPickerState::new(window, cx).default_value(design::rgb_bytes_to_hsla(init)) }); cx.subscribe(&st, move |this, _emitter, ev: &MoonColorPickerEvent, cx| { - let MoonColorPickerEvent::Change(h) = ev; + let MoonColorPickerEvent::Change(h) = ev else { + return; + }; let c = hsla_u8(*h); this.backend.update(cx, |b, bcx| { if let Some(p) = b.preview.as_mut() { diff --git a/crates/moon-ui-gpui/src/settings/mod.rs b/crates/moon-ui-gpui/src/settings/mod.rs index 7e7da666..44f1cafa 100644 --- a/crates/moon-ui-gpui/src/settings/mod.rs +++ b/crates/moon-ui-gpui/src/settings/mod.rs @@ -45,7 +45,7 @@ use moon_core::session::CoreId; use badges::BadgesEd; use common::{ - collapse_block, color_row, draft_color, draft_slider, section, separator, slider_row, + collapse_block, color_row, draft_color, draft_slider, hsla_u8, section, separator, slider_row, }; use connections::{ConnEntry, ConnRow}; use interface::Iface; diff --git a/crates/moon-ui-gpui/src/settings/share.rs b/crates/moon-ui-gpui/src/settings/share.rs index 6b4e5b24..18eee51c 100644 --- a/crates/moon-ui-gpui/src/settings/share.rs +++ b/crates/moon-ui-gpui/src/settings/share.rs @@ -69,9 +69,12 @@ impl SettingsView { Tab::Hotkeys => HotkeysConfig::parse_share(&text) .map(|h| self.apply_draft(cx, move |d| d.hotkeys = h)) .is_some(), - Tab::Badges => BadgesConfig::parse_share(&text) - .map(|b| self.apply_draft(cx, move |d| d.badges = b)) - .is_some(), + Tab::Badges => { + let cur = self.draft_snapshot(cx, |d| d.badges.clone()); + BadgesConfig::parse_share(&text, &cur) + .map(|b| self.apply_draft(cx, move |d| d.badges = b)) + .is_some() + } _ => false, }; diff --git a/crates/moon-ui-gpui/src/strategies/fields.rs b/crates/moon-ui-gpui/src/strategies/fields.rs index e7b7b0c9..4524d771 100644 --- a/crates/moon-ui-gpui/src/strategies/fields.rs +++ b/crates/moon-ui-gpui/src/strategies/fields.rs @@ -85,11 +85,13 @@ impl StrategiesView { return state.clone(); } } - let init: Hsla = gpui::rgb(design::rgb_to_u32(rgb_val)).into(); + let init: Hsla = design::rgb_bytes_to_hsla(rgb_val); let state = cx.new(|cx| MoonColorPickerState::new(window, cx).default_value(init)); let prefix = hex_alpha_prefix(hex); cx.subscribe(&state, move |this, _st, ev: &MoonColorPickerEvent, cx| { - let MoonColorPickerEvent::Change(h) = ev; + let MoonColorPickerEvent::Change(h) = ev else { + return; + }; let c = design::hsla_to_rgb8(*h); let hexv = format!("{prefix}{:02X}{:02X}{:02X}", c[0], c[1], c[2]); this.stage_field_value(keys.as_ref(), &field, hexv, cx);