From 82e410f8c0835e9aca5e2460bfd46927dceedbee Mon Sep 17 00:00:00 2001 From: Matus Mastena Date: Sat, 12 Sep 2026 21:00:29 +0200 Subject: [PATCH] Add a colour wheel for the viewport backgrounds The Display page could only take a typed #RRGGBB, which is precise but no way to choose a colour by eye. The swatches are now buttons that open a wheel. The wheel does not replace the hex field, it sits beside it: typing an exact value stays the quickest way to reproduce a known colour. On submit it hands a hex string to the existing ModelSpaceBgChanged and friends rather than writing the config itself, so the two routes cannot drift apart on validation, persistence, or the MatchTheme fallback an empty field triggers. Also stops the Browser shipping in the default dock layout. It was added to the right edge, which shifted the block palette to index 1 and broke four dock tests that encode the existing arrangement. Those tests were right and the change was wrong: the earlier commit claimed to leave the layout alone. BROWSER now docks the panel on first use, so nobody's edges rearrange on upgrade for a panel they never asked for. --- src/app/commands/layerprops.rs | 13 ++++- src/app/mod.rs | 18 +++++++ src/app/update/mod.rs | 33 ++++++++++++ src/app/view/modal.rs | 1 + src/ui/dock.rs | 2 +- src/ui/window/options.rs | 92 +++++++++++++++------------------- 6 files changed, 106 insertions(+), 53 deletions(-) diff --git a/src/app/commands/layerprops.rs b/src/app/commands/layerprops.rs index bb39b3a15..3943e0a33 100644 --- a/src/app/commands/layerprops.rs +++ b/src/app/commands/layerprops.rs @@ -673,9 +673,20 @@ impl OpenCADStudio { "BROWSER" => { self.show_browser ^= true; if self.show_browser { + // Dock it on first use rather than shipping it in the + // default layout: an existing user's edges should not + // rearrange themselves on upgrade for a panel they have + // not asked for. + let id = crate::ui::dock::PanelId::Browser; + if self.dock.location(id).is_none() { + // dock() clamps an out-of-range index to the end of + // the edge, so this appends below whatever is there. + self.dock + .dock(id, crate::app::config::DockSide::Right, usize::MAX); + } // Open expanded so it is usable straight away; the pin // button still collapses it. - self.dock_expanded = Some(crate::ui::dock::PanelId::Browser); + self.dock_expanded = Some(id); self.command_line .push_output(crate::t!("Browser opened.").as_ref()); } else { diff --git a/src/app/mod.rs b/src/app/mod.rs index 9204daedb..281daca5b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -736,6 +736,9 @@ pub(super) struct OpenCADStudio { /// Whether the Browser panel is shown. Off until BROWSER opens it, so /// the default layout is unchanged for existing users. pub(crate) show_browser: bool, + /// Which viewport background the colour wheel is editing, or `None` when + /// it is closed. One slot, because only one wheel can be open at a time. + pub(crate) bg_picker: Option, /// General edge-stack dock layout for the side panels. pub(crate) dock: crate::ui::dock::DockState, /// Which panel is currently floated at full height (hovered, or a pinned @@ -1444,6 +1447,14 @@ pub struct SaveOutcome { refreshed_preview: Option>, result: Result<(), crate::io::SaveFailure>, } +/// Which viewport background a colour-wheel session is editing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BgTarget { + Model, + Paper, + Desk, +} + /// Active page in the shared CAD colour picker. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum ColorPickerTab { @@ -2129,6 +2140,12 @@ pub enum Message { ModelSpaceModeChanged(config::ModelSpaceMode), /// Change Model Space custom background color as hex or empty for default. ModelSpaceBgChanged(String), + /// Open the colour wheel on one of the viewport backgrounds. + BgPickerOpen(BgTarget), + /// Dismiss the wheel, changing nothing. + BgPickerCancel, + /// Accept the wheel's colour for whichever background it was opened on. + BgPickerSubmit(iced::Color), /// Change Paper Space custom sheet background color as hex or empty for default. PaperSpaceBgChanged(String), /// Change Paper Space desk surround background (#RRGGBB). @@ -3933,6 +3950,7 @@ impl OpenCADStudio { show_block_palette: false, show_external_references: false, show_browser: false, + bg_picker: None, block_palette: Default::default(), xref_manager: Default::default(), dock: Default::default(), diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index ee962d958..3573234d5 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -7068,6 +7068,39 @@ impl OpenCADStudio { Task::none() } + Message::BgPickerOpen(target) => { + self.bg_picker = Some(target); + Task::none() + } + + Message::BgPickerCancel => { + self.bg_picker = None; + Task::none() + } + + Message::BgPickerSubmit(color) => { + // Hand the result to the same handler the typed hex field + // uses, so the wheel and the field cannot drift apart on + // validation, persistence or the MatchTheme fallback. + let hex = crate::app::config::rgb_to_hex([ + (color.r * 255.0).round() as u8, + (color.g * 255.0).round() as u8, + (color.b * 255.0).round() as u8, + ]); + match self.bg_picker.take() { + Some(crate::app::BgTarget::Model) => { + Task::done(Message::ModelSpaceBgChanged(hex)) + } + Some(crate::app::BgTarget::Paper) => { + Task::done(Message::PaperSpaceBgChanged(hex)) + } + Some(crate::app::BgTarget::Desk) => { + Task::done(Message::DeskSpaceBgChanged(hex)) + } + None => Task::none(), + } + } + Message::ModelSpaceBgChanged(hex) => { self.model_bg_input = hex.clone(); if hex.trim().is_empty() { diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index d3bc809ab..80e96ac1d 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -358,6 +358,7 @@ impl OpenCADStudio { &self.model_bg_input, &self.paper_bg_input, &self.desk_bg_input, + self.bg_picker, flow, ) }, diff --git a/src/ui/dock.rs b/src/ui/dock.rs index 68b49c015..2be4829a2 100644 --- a/src/ui/dock.rs +++ b/src/ui/dock.rs @@ -121,7 +121,7 @@ impl Default for DockState { fn default() -> Self { Self { left: vec![PanelId::Properties], - right: vec![PanelId::Browser, PanelId::BlockPalette], + right: vec![PanelId::BlockPalette], panels: BTreeMap::new(), } } diff --git a/src/ui/window/options.rs b/src/ui/window/options.rs index a6664e345..1c1f35322 100644 --- a/src/ui/window/options.rs +++ b/src/ui/window/options.rs @@ -146,6 +146,43 @@ impl fmt::Display for Labelled { } } +/// A background swatch that opens the colour wheel when clicked. +/// +/// The wheel sits alongside the hex field rather than replacing it: typing +/// `#1E1E1E` stays the fastest way to reproduce an exact colour, while the +/// wheel is for choosing one by eye. +fn bg_swatch_picker<'a>( + target: crate::app::BgTarget, + rgb: [u8; 3], + open: Option, +) -> Element<'a, Message> { + let colour = iced::Color::from_rgb8(rgb[0], rgb[1], rgb[2]); + let swatch = container(Space::new()) + .width(28) + .height(22) + .style(move |theme: &Theme| container::Style { + background: Some(Background::Color(colour)), + border: Border { + color: theme.palette().background.strong.color, + width: 1.0, + radius: 3.0.into(), + }, + ..Default::default() + }); + let underlay = button(swatch) + .on_press(Message::BgPickerOpen(target)) + .padding(0) + .style(button::text); + iced_aw::ColorPicker::new( + open == Some(target), + colour, + underlay, + Message::BgPickerCancel, + Message::BgPickerSubmit, + ) + .into() +} + #[allow(clippy::too_many_arguments)] pub fn view_window<'a>( default_save_format: &'a str, @@ -172,6 +209,7 @@ pub fn view_window<'a>( model_bg_input: &'a str, paper_bg_input: &'a str, desk_bg_input: &'a str, + bg_picker: Option, sizing: crate::ui::modal::ModalSizing, ) -> Element<'a, Message> { let selected_format = crate::io::SAVE_FORMAT_OPTIONS @@ -455,58 +493,10 @@ pub fn view_window<'a>( }); let model_bg_rgb = model_space.custom_bg.unwrap_or(crate::app::config::CLASSIC_CAD_DARK_BG); - let model_bg_swatch = container(Space::new()) - .width(28) - .height(22) - .style(move |theme: &Theme| container::Style { - background: Some(Background::Color(iced::Color::from_rgb8( - model_bg_rgb[0], - model_bg_rgb[1], - model_bg_rgb[2], - ))), - border: Border { - color: theme.palette().background.strong.color, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() - }); let paper_bg_rgb = model_space.custom_paper_bg.unwrap_or(crate::app::config::DEFAULT_PAPER_BG); - let paper_bg_swatch = container(Space::new()) - .width(28) - .height(22) - .style(move |theme: &Theme| container::Style { - background: Some(Background::Color(iced::Color::from_rgb8( - paper_bg_rgb[0], - paper_bg_rgb[1], - paper_bg_rgb[2], - ))), - border: Border { - color: theme.palette().background.strong.color, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() - }); let desk_bg_rgb = model_space.custom_desk_bg.unwrap_or(crate::app::config::DEFAULT_DESK_BG); - let desk_bg_swatch = container(Space::new()) - .width(28) - .height(22) - .style(move |theme: &Theme| container::Style { - background: Some(Background::Color(iced::Color::from_rgb8( - desk_bg_rgb[0], - desk_bg_rgb[1], - desk_bg_rgb[2], - ))), - border: Border { - color: theme.palette().background.strong.color, - width: 1.0, - radius: 3.0.into(), - }, - ..Default::default() - }); let mode_options = crate::app::config::ModelSpaceMode::ALL .into_iter() @@ -641,7 +631,7 @@ pub fn view_window<'a>( display = display.push(Space::new().height(10)).push( row![ text(crate::t!("Model background")).size(12).width(140), - model_bg_swatch, + bg_swatch_picker(crate::app::BgTarget::Model, model_bg_rgb, bg_picker), text_input("#RRGGBB", model_bg_input) .on_input(Message::ModelSpaceBgChanged) .width(150), @@ -654,7 +644,7 @@ pub fn view_window<'a>( display = display.push(Space::new().height(10)).push( row![ text(crate::t!("Paper background")).size(12).width(140), - paper_bg_swatch, + bg_swatch_picker(crate::app::BgTarget::Paper, paper_bg_rgb, bg_picker), text_input("#RRGGBB", paper_bg_input) .on_input(Message::PaperSpaceBgChanged) .width(150), @@ -666,7 +656,7 @@ pub fn view_window<'a>( display = display.push(Space::new().height(10)).push( row![ text(crate::t!("Desk surround")).size(12).width(140), - desk_bg_swatch, + bg_swatch_picker(crate::app::BgTarget::Desk, desk_bg_rgb, bg_picker), text_input("#RRGGBB", desk_bg_input) .on_input(Message::DeskSpaceBgChanged) .width(150),