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);