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.
This commit is contained in:
Matus Mastena committed 2026-09-17 16:54:11 +02:00
1 parent 1529a8d9c1
commit 82e410f8c0
6 files changed
+106 -53

No files matched your search

+12 -1
View File
@@ -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 {
+18
View File
@@ -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<BgTarget>,
/// 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<Option<acadrust::Preview>>,
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(),
+33
View File
@@ -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() {
+1
View File
@@ -358,6 +358,7 @@ impl OpenCADStudio {
&self.model_bg_input,
&self.paper_bg_input,
&self.desk_bg_input,
self.bg_picker,
flow,
)
},
+1 -1
View File
@@ -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(),
}
}
+41 -51
View File
@@ -146,6 +146,43 @@ impl<T> fmt::Display for Labelled<T> {
}
}
/// 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<crate::app::BgTarget>,
) -> 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<crate::app::BgTarget>,
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),