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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rGmDUNr1hUf4zo1X3dak8
This commit is contained in:
Claude committed 2026-10-12 18:33:23 +00:00
1 parent 0a28163ef5
commit c8c5f667f7
6 files changed
+102 -125

No files matched your search

+12 -7
View File
@@ -1577,12 +1577,17 @@ pub struct SaveOutcome {
refreshed_preview: Option<Option<codec::Preview>>,
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
+1 -1
View File
@@ -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);
}
+12 -7
View File
@@ -1136,7 +1136,7 @@ impl OpenCADStudio {
}
pub(super) fn on_color_window_pick(&mut self, color: codec::types::Color) -> Task<Message> {
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,
};
+52 -31
View File
@@ -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);
}
+3 -3
View File
@@ -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,
+22 -76
View File
@@ -157,13 +157,12 @@ impl<T> fmt::Display for Labelled<T> {
}
}
/// 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),