From c8c5f667f7452adde56658da1b229d76253ccf52 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 18:33:23 +0000 Subject: [PATCH] Select Color: frameless gradient button, hex beside H/S/B, all Options swatches - The gradient mode button is just its gradient, without a frame. - In the gradient view H, S and B sit on the left and Hex on the right of one row; the nearest index colour moves to the row below. - Every colour swatch in Options opens the Select Color panel: the six theme colours, crosshair, snap marker and command line text join the three viewport backgrounds. BgTarget becomes UiColor, and one color_swatch helper replaces the four hand-built swatches. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013rGmDUNr1hUf4zo1X3dak8 --- src/app/mod.rs | 19 ++++--- src/app/update/mod.rs | 2 +- src/app/update/style.rs | 19 ++++--- src/ui/color_select/editor.rs | 83 ++++++++++++++++++----------- src/ui/color_select/picker.rs | 6 +-- src/ui/window/options.rs | 98 ++++++++--------------------------- 6 files changed, 102 insertions(+), 125 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 1c9cacc71..917215db4 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1577,12 +1577,17 @@ pub struct SaveOutcome { refreshed_preview: Option>, result: Result<(), crate::io::SaveFailure>, } -/// Which viewport background the interface colour picker is editing. +/// Which Options colour the interface colour picker is editing. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BgTarget { - Model, - Paper, - Desk, +pub enum UiColor { + ModelBackground, + PaperBackground, + DeskBackground, + /// One of the six theme colours, by its row in Options. + Theme(usize), + Crosshair, + SnapMarker, + CommandText, } /// Where a colour chosen in the standalone palette window should be applied. @@ -1607,8 +1612,8 @@ pub enum ColorPickTarget { LayerState(usize), /// The MText editor's selection (or global) colour. MText, - /// An Options viewport background (interface colour, true colour only). - Background(BgTarget), + /// An Options interface colour (true colour only). + Interface(UiColor), } /// Table records the clipboard entities depend on, snapshotted from the source diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index eddd93911..2dbc0c81a 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -11061,7 +11061,7 @@ impl OpenCADStudio { // the recent list. if !matches!( self.color_picker.as_ref().map(|picker| &picker.target), - Some(crate::app::ColorPickTarget::Background(_)) + Some(crate::app::ColorPickTarget::Interface(_)) ) { self.note_recent_color(color); } diff --git a/src/app/update/style.rs b/src/app/update/style.rs index 0db169c56..62953b487 100644 --- a/src/app/update/style.rs +++ b/src/app/update/style.rs @@ -1136,7 +1136,7 @@ impl OpenCADStudio { } pub(super) fn on_color_window_pick(&mut self, color: codec::types::Color) -> Task { - if let Some(crate::app::ColorPickTarget::Background(target)) = self + if let Some(crate::app::ColorPickTarget::Interface(target)) = self .color_picker .as_ref() .map(|picker| picker.target.clone()) @@ -1146,13 +1146,18 @@ impl OpenCADStudio { return Task::none(); }; // Hand the result to the same handler the typed hex field uses, so - // the picker and the field cannot drift apart on validation, - // persistence or the MatchTheme fallback. + // the picker and the field cannot drift apart on validation or + // persistence. let hex = crate::app::config::rgb_to_hex([r, g, b]); + use crate::app::UiColor; return Task::done(match target { - crate::app::BgTarget::Model => Message::ModelSpaceBgChanged(hex), - crate::app::BgTarget::Paper => Message::PaperSpaceBgChanged(hex), - crate::app::BgTarget::Desk => Message::DeskSpaceBgChanged(hex), + UiColor::ModelBackground => Message::ModelSpaceBgChanged(hex), + UiColor::PaperBackground => Message::PaperSpaceBgChanged(hex), + UiColor::DeskBackground => Message::DeskSpaceBgChanged(hex), + UiColor::Theme(index) => Message::OptionsThemeColorChanged(index, hex), + UiColor::Crosshair => Message::CrosshairColorChanged(hex), + UiColor::SnapMarker => Message::SnapMarkerColorChanged(hex), + UiColor::CommandText => Message::CommandTextColorChanged(hex), }); } if matches!( @@ -1208,7 +1213,7 @@ impl OpenCADStudio { } // Handled before this match. Some( - crate::app::ColorPickTarget::PlotStyle | crate::app::ColorPickTarget::Background(_), + crate::app::ColorPickTarget::PlotStyle | crate::app::ColorPickTarget::Interface(_), ) => None, None => None, }; diff --git a/src/ui/color_select/editor.rs b/src/ui/color_select/editor.rs index d3c3b1df2..12f286632 100644 --- a/src/ui/color_select/editor.rs +++ b/src/ui/color_select/editor.rs @@ -145,37 +145,46 @@ impl ColorEditor { let Hsb { h, s, b } = self.hsb; let (r, g, bl) = self.rgb; - let mode_button = |content: Element<'a, Message>, mode: EditorMode, padding: u16| { + let mode_button = |content: Element<'a, Message>, mode: EditorMode| { let active = self.mode == mode; button(container(content).center(Length::Fill)) .on_press(msg(EditorMsg::Mode(mode))) .width(52) .height(28) - .padding(padding) + .padding(0) .style(move |theme: &Theme, status| { super::picker::toggle_style(theme, active, status) }) }; - // The field button is all gradient; it only takes colour when active. + // The field button is only its gradient, without a frame; it takes + // colour when its mode is active and stays gray otherwise. let field_active = self.mode == EditorMode::Field; - let field_icon = container(Space::new()) - .width(Length::Fill) - .height(Length::Fill) - .style(move |_theme: &Theme| { + let field_button = button(Space::new().width(52).height(28)) + .on_press(msg(EditorMsg::Mode(EditorMode::Field))) + .padding(0) + .style(move |_theme: &Theme, _status| { let middle = if field_active { Color::from_rgb8(0x3A, 0xA0, 0xFF) } else { Color::from_rgb8(0x80, 0x80, 0x80) }; - gradient_box( + let icon = gradient_box( 3.0 * std::f32::consts::FRAC_PI_4, &[(0.0, Color::WHITE), (0.5, middle), (1.0, Color::BLACK)], - ) + ); + button::Style { + background: icon.background, + border: Border { + radius: 5.0.into(), + ..Border::default() + }, + ..Default::default() + } }); let mut modes = row![ - mode_button(text("HSB").size(12).into(), EditorMode::Hsb, 0), - mode_button(text("RGB").size(12).into(), EditorMode::Rgb, 0), - mode_button(field_icon.into(), EditorMode::Field, 3), + mode_button(text("HSB").size(12).into(), EditorMode::Hsb), + mode_button(text("RGB").size(12).into(), EditorMode::Rgb), + field_button, Space::new().width(Length::Fill), ] .spacing(6); @@ -183,6 +192,26 @@ impl ColorEditor { modes = modes.push(end); } + let hex = self + .hex_draft + .clone() + .unwrap_or_else(|| math::hex_string(r, g, bl)); + let hex_field = move || { + row![ + text(t!("Hex")).size(11).style(super::picker::muted_text), + text_input("", &hex) + .on_input(|value| msg(EditorMsg::HexInput(value))) + .on_submit(msg(EditorMsg::HexSubmit)) + .size(12) + .font(iced::Font::MONOSPACE) + .padding([5, 6]) + .width(Length::Fixed(74.0)), + ] + .spacing(8) + .align_y(iced::Center) + }; + let field_mode = self.mode == EditorMode::Field; + let editor: Element<'a, Message> = match self.mode { EditorMode::Hsb => column![ channel("H", h, 360.0, Some('°'), hue_stops(), EditorMsg::Hue), @@ -270,13 +299,15 @@ impl ColorEditor { }; column![ row![area, strip].spacing(12), + // H, S and B on the left, the hex value on the right. row![ value("H", h, 360.0, '°', EditorMsg::Hue), - Space::new().width(Length::Fill), value("S", s * 100.0, 100.0, '%', EditorMsg::Saturation), - Space::new().width(Length::Fill), value("B", b * 100.0, 100.0, '%', EditorMsg::Brightness), + Space::new().width(Length::Fill), + hex_field(), ] + .spacing(10) .align_y(iced::Center), ] .spacing(10) @@ -284,23 +315,13 @@ impl ColorEditor { } }; - let hex = self - .hex_draft - .clone() - .unwrap_or_else(|| math::hex_string(r, g, bl)); - let mut hex_row = row![ - text(t!("Hex")).size(11).style(super::picker::muted_text), - text_input("", &hex) - .on_input(|value| msg(EditorMsg::HexInput(value))) - .on_submit(msg(EditorMsg::HexSubmit)) - .size(12) - .font(iced::Font::MONOSPACE) - .padding([5, 6]) - .width(Length::Fixed(74.0)), - Space::new().width(Length::Fill), - ] - .spacing(8) - .align_y(iced::Center); + // Below the editor: the hex value (unless the field view already + // shows it) and, on the right, the nearest index colour. + let mut hex_row = row![].spacing(8).align_y(iced::Center); + if !field_mode { + hex_row = hex_row.push(hex_field()); + } + hex_row = hex_row.push(Space::new().width(Length::Fill)); if let Some(end) = hex_end { hex_row = hex_row.push(end); } diff --git a/src/ui/color_select/picker.rs b/src/ui/color_select/picker.rs index 5d9668c42..2043267cb 100644 --- a/src/ui/color_select/picker.rs +++ b/src/ui/color_select/picker.rs @@ -95,7 +95,7 @@ impl ColorPicker { /// Interface colours (Options backgrounds) only get the colour editor. fn editor_only(&self) -> bool { - matches!(self.target, ColorPickTarget::Background(_)) + matches!(self.target, ColorPickTarget::Interface(_)) } fn set_pending(&mut self, color: AcadColor) { @@ -763,7 +763,7 @@ mod tests { #[test] fn interface_colours_show_only_the_editor() { let p = ColorPicker::new( - ColorPickTarget::Background(crate::app::BgTarget::Model), + ColorPickTarget::Interface(crate::app::UiColor::ModelBackground), AcadColor::Rgb { r: 30, g: 30, @@ -843,7 +843,7 @@ mod tests { ), ( "interface", - ColorPickTarget::Background(crate::app::BgTarget::Model), + ColorPickTarget::Interface(crate::app::UiColor::ModelBackground), AcadColor::Rgb { r: 33, g: 40, diff --git a/src/ui/window/options.rs b/src/ui/window/options.rs index 99d116c9d..5b1ea49f9 100644 --- a/src/ui/window/options.rs +++ b/src/ui/window/options.rs @@ -157,13 +157,12 @@ impl fmt::Display for Labelled { } } -/// A background swatch that opens the colour picker when clicked. +/// A colour swatch that opens the Select Color panel when clicked. /// -/// The picker sits alongside the hex field rather than replacing it: typing +/// The swatch sits beside the hex field rather than replacing it: typing /// `#1E1E1E` stays the fastest way to reproduce an exact colour, while the -/// picker is for choosing one by eye. -fn bg_swatch_picker<'a>(target: crate::app::BgTarget, rgb: [u8; 3]) -> Element<'a, Message> { - let [r, g, b] = rgb; +/// panel is for choosing one by eye. +fn color_swatch<'a>(target: crate::app::UiColor, [r, g, b]: [u8; 3]) -> Element<'a, Message> { let swatch = container(Space::new()) .width(28) .height(22) @@ -178,7 +177,7 @@ fn bg_swatch_picker<'a>(target: crate::app::BgTarget, rgb: [u8; 3]) -> Element<' }); button(swatch) .on_press(Message::OpenColorWindow( - crate::app::ColorPickTarget::Background(target), + crate::app::ColorPickTarget::Interface(target), codec::types::Color::Rgb { r, g, b }, )) .padding(0) @@ -298,18 +297,8 @@ pub fn view_window<'a>( let mut color_controls = column![].spacing(8); for (index, (label, color)) in colors.into_iter().enumerate() { - let swatch = container(Space::new()) - .width(28) - .height(22) - .style(move |theme: &Theme| container::Style { - background: Some(Background::Color(color)), - border: Border { - color: theme.palette().background.strong.color, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() - }); + let [r, g, bl, _] = color.into_rgba8(); + let swatch = color_swatch(crate::app::UiColor::Theme(index), [r, g, bl]); color_controls = color_controls.push( row![ text(label).size(12).width(110), @@ -519,61 +508,18 @@ pub fn view_window<'a>( .spacing(0) .width(sizing.width); - let crosshair_rgb = crosshair_color.unwrap_or([255, 255, 255]); - let crosshair_swatch = - container(Space::new()) - .width(28) - .height(22) - .style(move |theme: &Theme| container::Style { - background: Some(Background::Color(iced::Color::from_rgb8( - crosshair_rgb[0], - crosshair_rgb[1], - crosshair_rgb[2], - ))), - border: Border { - color: theme.palette().background.strong.color, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() - }); - - let command_rgb = command_text_color.unwrap_or([220, 220, 220]); - let command_swatch = - container(Space::new()) - .width(28) - .height(22) - .style(move |theme: &Theme| container::Style { - background: Some(Background::Color(iced::Color::from_rgb8( - command_rgb[0], - command_rgb[1], - command_rgb[2], - ))), - border: Border { - color: theme.palette().background.strong.color, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() - }); - - let snap_rgb = snap_marker_color.unwrap_or([255, 230, 26]); - let snap_swatch = container(Space::new()) - .width(28) - .height(22) - .style(move |theme: &Theme| container::Style { - background: Some(Background::Color(iced::Color::from_rgb8( - snap_rgb[0], - snap_rgb[1], - snap_rgb[2], - ))), - border: Border { - color: theme.palette().background.strong.color, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() - }); + let crosshair_swatch = color_swatch( + crate::app::UiColor::Crosshair, + crosshair_color.unwrap_or([255, 255, 255]), + ); + let command_swatch = color_swatch( + crate::app::UiColor::CommandText, + command_text_color.unwrap_or([220, 220, 220]), + ); + let snap_swatch = color_swatch( + crate::app::UiColor::SnapMarker, + snap_marker_color.unwrap_or([255, 230, 26]), + ); let model_bg_rgb = model_space .custom_bg @@ -733,7 +679,7 @@ pub fn view_window<'a>( display = display.push(Space::new().height(10)).push( row![ text(crate::t!("Model background")).size(12).width(140), - bg_swatch_picker(crate::app::BgTarget::Model, model_bg_rgb), + color_swatch(crate::app::UiColor::ModelBackground, model_bg_rgb), text_input("#RRGGBB", model_bg_input) .on_input(Message::ModelSpaceBgChanged) .width(150), @@ -746,7 +692,7 @@ pub fn view_window<'a>( display = display.push(Space::new().height(10)).push( row![ text(crate::t!("Paper background")).size(12).width(140), - bg_swatch_picker(crate::app::BgTarget::Paper, paper_bg_rgb), + color_swatch(crate::app::UiColor::PaperBackground, paper_bg_rgb), text_input("#RRGGBB", paper_bg_input) .on_input(Message::PaperSpaceBgChanged) .width(150), @@ -758,7 +704,7 @@ pub fn view_window<'a>( display = display.push(Space::new().height(10)).push( row![ text(crate::t!("Desk surround")).size(12).width(140), - bg_swatch_picker(crate::app::BgTarget::Desk, desk_bg_rgb), + color_swatch(crate::app::UiColor::DeskBackground, desk_bg_rgb), text_input("#RRGGBB", desk_bg_input) .on_input(Message::DeskSpaceBgChanged) .width(150),