Redesign Select Color panel: index and true color tabs

Replace the old index page and the iced_aw ColorPicker popup with one
Select Color panel:

- Index Color tab: standard colors, ACI 10-249 grid, grays; arrow keys
  move through the palette.
- True Color tab: HSB, RGB and gradient-field modes built on two new
  widgets (GradientSlider, ColorArea) and iced_aw NumberInput boxes,
  an editable hex field and a nearest/exact index color button.
- Current/New swatch: clicking Current reverts. A mixed selection opens
  on the color of the most recently drawn object with *VARIES* on top.
- Color field accepts an index, a color name, r,g,b or hex.
- Enter confirms, Esc cancels, other keys stay in the panel.
- New labels translated in all 22 locales.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013rGmDUNr1hUf4zo1X3dak8
This commit is contained in:
Claude committed 2026-10-11 20:29:46 +00:00
1 parent cff240ef71
commit 00830f8714
36 files changed
+2173 -276

No files matched your search

+83 -14
View File
@@ -377,6 +377,37 @@ impl OpenCADStudio {
// closes it and every other keystroke-derived message is swallowed;
// the modal's own text fields keep working because they emit their own
// (non-blocked) messages. (#126)
// The Select Color panel can open on top of another dialog, so it
// takes the keyboard first: Esc cancels, Enter confirms, the arrows
// walk the index palette, and every other keystroke is swallowed.
if self.color_picker.is_some() {
use crate::ui::color_select::ColorPickerMsg;
let last_key = |key: &str| key.rsplit('+').next().map(str::to_owned);
match &msg {
Message::CommandEscape => {
return self.update(Message::ColorPicker(ColorPickerMsg::Cancel));
}
Message::CommandFinalize => {
return self.update(Message::ColorPicker(ColorPickerMsg::Ok));
}
Message::ShortcutPressed(key) => match last_key(key).as_deref() {
Some("ESCAPE") => {
return self.update(Message::ColorPicker(ColorPickerMsg::Cancel));
}
Some("ENTER" | "RETURN") => {
return self.update(Message::ColorPicker(ColorPickerMsg::Ok));
}
_ => {}
},
Message::ArrowKeyPressed { direction, .. } => {
return self.update(Message::ColorPicker(ColorPickerMsg::Arrow(*direction)));
}
_ => {}
}
if is_modal_blocked_key_msg(&msg) {
return Task::none();
}
}
if self.active_modal.is_some() {
if matches!(msg, Message::CommandEscape)
|| matches!(&msg, Message::ShortcutPressed(key) if key.rsplit('+').next() == Some("ESCAPE"))
@@ -3806,7 +3837,7 @@ impl OpenCADStudio {
Message::PaneClicked(pane) => self.on_pane_clicked(pane),
Message::PaneDragged(ev) => self.on_pane_dragged(ev),
Message::PaneMove(idx, local) => {
if self.color_pick_target.is_some() {
if self.color_picker.is_some() {
return Task::none();
}
let p = self.pane_canvas_point(idx, local);
@@ -11005,10 +11036,10 @@ impl OpenCADStudio {
Task::none()
}
Message::OpenColorWindow(target, color) => {
self.color_pick_target = Some((target, color));
// Always open the shared CAD colour picker on the indexed ACI page.
self.color_picker_tab = crate::app::ColorPickerTab::Index;
let (current, varies) = self.color_picker_current(&target, color);
self.color_picker = Some(crate::ui::color_select::ColorPicker::new(
target, current, varies,
));
self.modal_offset = iced::Vector::ZERO;
self.ds_color_open = None;
self.mls_color_open = None;
@@ -11023,18 +11054,23 @@ impl OpenCADStudio {
Task::none()
}
Message::ColorPickerTabChanged(tab) => {
self.color_picker_tab = tab;
Task::none()
}
Message::ColorPickerColorChanged(color) => {
if let Some((_, current)) = self.color_pick_target.as_mut() {
*current = color;
Message::ColorPicker(msg) => {
let Some(picker) = self.color_picker.as_mut() else {
return Task::none();
};
match picker.update(msg) {
Some(crate::ui::color_select::Outcome::Commit(color)) => {
self.update(Message::ColorWindowPick(color))
}
Some(crate::ui::color_select::Outcome::Cancel) => {
self.color_picker = None;
Task::none()
}
None => Task::none(),
}
Task::none()
}
Message::CloseColorPicker => {
self.color_pick_target = None;
self.color_picker = None;
Task::none()
}
@@ -11046,6 +11082,39 @@ impl OpenCADStudio {
}
}
/// The colour the Select Color panel shows as "Current", and whether the
/// selection it edits has mixed colours. A mixed selection starts from the
/// colour of its most recently drawn object (the highest handle).
fn color_picker_current(
&self,
target: &crate::app::ColorPickTarget,
color: codec::types::Color,
) -> (codec::types::Color, bool) {
if !matches!(target, crate::app::ColorPickTarget::Properties) {
return (color, false);
}
let i = self.active_tab;
let document = &self.tabs[i].scene.document;
let mut colors = self
.property_target_handles(i)
.into_iter()
.filter_map(|handle| {
document
.get_entity(handle)
.map(|entity| (handle, entity.common().color))
})
.collect::<Vec<_>>();
let Some(&(_, first)) = colors.first() else {
return (color, false);
};
if colors.iter().all(|&(_, c)| c == first) {
return (first, false);
}
colors.sort_by_key(|&(handle, _)| handle);
let newest = colors.last().map_or(color, |&(_, c)| c);
(newest, true)
}
pub(crate) fn note_recent_color(&mut self, color: codec::types::Color) {
// Keep only real colours in the recent list. ByLayer / ByBlock / None
// are logical CAD states rather than reusable colours.
+3 -3
View File
@@ -1137,10 +1137,10 @@ impl OpenCADStudio {
pub(super) fn on_color_window_pick(&mut self, color: codec::types::Color) -> Task<Message> {
if matches!(
self.color_pick_target.as_ref().map(|(target, _)| target),
self.color_picker.as_ref().map(|picker| &picker.target),
Some(crate::app::ColorPickTarget::PlotStyle)
) {
self.color_pick_target = None;
self.color_picker = None;
let rgb = match color {
codec::types::Color::Rgb { r, g, b } => Some((r, g, b)),
@@ -1155,7 +1155,7 @@ impl OpenCADStudio {
return self.on_plot_style_panel_apply();
}
let s = crate::ui::color_select::color_to_aci_string(color);
let edit = match self.color_pick_target.take().map(|(target, _)| target) {
let edit = match self.color_picker.take().map(|picker| picker.target) {
Some(crate::app::ColorPickTarget::DimStyle(f)) => Some(Message::DsEdit(f, s)),
Some(crate::app::ColorPickTarget::MLeader(f)) => {
Some(Message::MLeaderStyleEdit { field: f, value: s })
+2 -2
View File
@@ -1119,7 +1119,7 @@ impl OpenCADStudio {
p: Point,
expected_viewport: Option<codec::Handle>,
) -> Task<Message> {
if self.color_pick_target.is_some() {
if self.color_picker.is_some() {
return Task::none();
}
@@ -1207,7 +1207,7 @@ impl OpenCADStudio {
// leaks through the stack to the pane mouse_area beneath and
// would track the crosshair over the dropdown's empty areas.
// Drop the move here instead. (#227)
if self.ribbon.open_dropdown.is_some() || self.color_pick_target.is_some() {
if self.ribbon.open_dropdown.is_some() || self.color_picker.is_some() {
return Task::none();
}
let i = self.active_tab;