Select Color: review cleanups

- Split ColorEditor::view into mode_row, slider_rows, field_view,
  hex_row and hex_field; output is unchanged.
- hex_string reuses config::rgb_to_hex, and parse_hex documents why it
  stays separate (it also accepts #RGB).
- ink_is_dark uses Color::into_rgba8 instead of rounding by hand.
- Remove four translation keys the old dialog used and nothing
  references any more, from the catalog and all 22 locales.
- Eyedropper: without a main window there is no screenshot to wait for,
  so leave the eyedropper; keep the magnifier inside small windows.

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-12 19:21:38 +00:00
1 parent cc40722f70
commit fefd526c98
28 files changed
+147 -221

No files matched your search

+7 -4
View File
@@ -11039,13 +11039,16 @@ impl OpenCADStudio {
}
Some(crate::ui::color_select::Outcome::Screenshot) => {
use crate::ui::color_select::{eyedropper::EyedropperMsg, ColorPickerMsg};
self.main_window.map_or_else(Task::none, |id| {
iced::window::screenshot(id).map(|shot| {
match self.main_window {
Some(id) => iced::window::screenshot(id).map(|shot| {
Message::ColorPicker(ColorPickerMsg::Eyedropper(
EyedropperMsg::Shot(shot),
))
})
})
}),
// Nothing to capture: leave the eyedropper instead
// of waiting for a screenshot that never comes.
None => self.update(Message::ColorPicker(ColorPickerMsg::Cancel)),
}
}
None => Task::none(),
}
-4
View File
@@ -4689,8 +4689,6 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"Dismiss" => Some(("common", "dismiss")),
"?" => Some(("common", "value-8a8de8")),
"Select Color..." => Some(("common", "select-color-8dddf1")),
"No recent colors yet" => Some(("common", "no-recent-colors-yet")),
"True Color RGB {r}, {g}, {b}" => Some(("common", "true-color-rgb")),
"Index Color" => Some(("common", "index-color")),
"True Color" => Some(("common", "true-color")),
"Standard colors" => Some(("common", "standard-colors")),
@@ -4698,9 +4696,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"Nearest index" => Some(("common", "nearest-index-color")),
"Exact index match" => Some(("common", "exact-index-match")),
"CAD Color Index (ACI) 10–249" => Some(("common", "cad-color-index-aci-10-249")),
"Color family → · Shade / intensity ↓" => Some(("common", "color-family-shade-intensity")),
"Grayscale 250–255" => Some(("common", "grayscale-250-255")),
"Recently used" => Some(("common", "recently-used")),
"Isometric drafting" => Some(("common", "isometric-drafting")),
"F5 cycles the active plane." => Some(("common", "f5-cycles-the-active-plane")),
"Units…" => Some(("common", "units-5430a1")),
+129 -118
View File
@@ -138,14 +138,25 @@ impl ColorEditor {
/// `mode_end` sits at the far right of the mode buttons (the pipette);
/// `hex_end` at the far right of the hex row (the nearest index colour).
/// The field view shows the hex value beside H, S and B and leaves out
/// `hex_end`; the slider views end on a hex row.
pub fn view<'a>(
&'a self,
mode_end: Option<Element<'a, Message>>,
hex_end: Option<Element<'a, Message>>,
) -> Element<'a, Message> {
let Hsb { h, s, b } = self.hsb;
let (r, g, bl) = self.rgb;
let view = column![self.mode_row(mode_end)].spacing(12);
match self.mode {
EditorMode::Hsb | EditorMode::Rgb => view
.push(self.slider_rows())
.push(self.hex_row(hex_end))
.into(),
EditorMode::Field => view.push(self.field_view()).into(),
}
}
/// HSB, RGB and the gradient-field button, with `end` at the far right.
fn mode_row<'a>(&self, end: Option<Element<'a, Message>>) -> Element<'a, Message> {
let mode_button = |content: Element<'a, Message>, mode: EditorMode| {
let active = self.mode == mode;
button(container(content).center(Length::Fill))
@@ -189,32 +200,50 @@ impl ColorEditor {
Space::new().width(Length::Fill),
]
.spacing(6);
if let Some(end) = mode_end {
if let Some(end) = end {
modes = modes.push(end);
}
modes.into()
}
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)),
/// Three gradient sliders: hue, saturation and brightness, or red, green
/// and blue.
fn slider_rows<'a>(&self) -> Element<'a, Message> {
let Hsb { h, s, b } = self.hsb;
let (r, g, bl) = self.rgb;
let rows = if self.mode == EditorMode::Rgb {
let ends = |lo: (u8, u8, u8), hi: (u8, u8, u8)| {
vec![(0.0, rgb_color(lo)), (1.0, rgb_color(hi))]
};
let byte = |f: fn(u8) -> EditorMsg| move |v: f32| f(v.round() as u8);
column![
channel(
"R",
f32::from(r),
255.0,
None,
ends((0, g, bl), (255, g, bl)),
byte(EditorMsg::Red)
),
channel(
"G",
f32::from(g),
255.0,
None,
ends((r, 0, bl), (r, 255, bl)),
byte(EditorMsg::Green)
),
channel(
"B",
f32::from(bl),
255.0,
None,
ends((r, g, 0), (r, g, 255)),
byte(EditorMsg::Blue)
),
]
.spacing(8)
.align_y(iced::Center)
};
let field_mode = self.mode == EditorMode::Field;
let editor: Element<'a, Message> = match self.mode {
EditorMode::Hsb => column![
} else {
column![
channel("H", h, 360.0, Some('°'), hue_stops(), EditorMsg::Hue),
channel(
"S",
@@ -233,103 +262,85 @@ impl ColorEditor {
EditorMsg::Brightness,
),
]
.spacing(ROW_GAP)
.into(),
EditorMode::Rgb => {
let ends = |lo: (u8, u8, u8), hi: (u8, u8, u8)| {
vec![(0.0, rgb_color(lo)), (1.0, rgb_color(hi))]
};
let byte = |f: fn(u8) -> EditorMsg| move |v: f32| f(v.round() as u8);
column![
channel(
"R",
f32::from(r),
255.0,
None,
ends((0, g, bl), (255, g, bl)),
byte(EditorMsg::Red)
),
channel(
"G",
f32::from(g),
255.0,
None,
ends((r, 0, bl), (r, 255, bl)),
byte(EditorMsg::Green)
),
channel(
"B",
f32::from(bl),
255.0,
None,
ends((r, g, 0), (r, g, 255)),
byte(EditorMsg::Blue)
),
]
.spacing(ROW_GAP)
.into()
}
EditorMode::Field => {
let area =
ColorArea::new(hsb_color(h, 1.0, 1.0), rgb_color(self.rgb), s, b, |s, b| {
msg(EditorMsg::Field(s, b))
})
.height(Length::Fixed(FIELD_HEIGHT));
// A vertical slider grows upwards; the strip runs red at the top.
let strip = stack![
container(Space::new())
.width(STRIP_WIDTH)
.height(FIELD_HEIGHT)
.style(|_theme: &Theme| gradient_box(std::f32::consts::PI, &hue_stops())),
vertical_slider(0.0..=360.0, 360.0 - h, |v| msg(EditorMsg::Hue(360.0 - v)))
.width(STRIP_WIDTH)
.height(FIELD_HEIGHT)
.style(handle_style),
];
let value = |letter: &'static str,
v: f32,
max: f32,
unit: char,
f: fn(f32) -> EditorMsg| {
row![
text(letter).size(12).font(iced::Font::MONOSPACE),
number_box(v, max, Some(unit), f),
]
.spacing(4)
.align_y(iced::Center)
};
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),
value("S", s * 100.0, 100.0, '%', EditorMsg::Saturation),
value("B", b * 100.0, 100.0, '%', EditorMsg::Brightness),
Space::new().width(Length::Fill),
hex_field(),
]
.spacing(10)
.align_y(iced::Center),
]
// 108 + 10 matches the slider views' 3 × 26 + 2 × 14 + 12.
.spacing(10)
.into()
}
};
rows.spacing(ROW_GAP).into()
}
// The field view shows the hex value beside H, S and B and leaves out
// the nearest index colour; the slider views end on a hex row.
let mut view = column![modes, editor].spacing(12);
if !field_mode {
let mut hex_row = row![hex_field(), Space::new().width(Length::Fill)]
.spacing(8)
.align_y(iced::Center);
if let Some(end) = hex_end {
hex_row = hex_row.push(end);
}
view = view.push(hex_row);
/// The saturation/brightness field with its hue strip, and below it H, S
/// and B on the left and the hex value on the right.
fn field_view<'a>(&self) -> Element<'a, Message> {
let Hsb { h, s, b } = self.hsb;
let area = ColorArea::new(hsb_color(h, 1.0, 1.0), rgb_color(self.rgb), s, b, |s, b| {
msg(EditorMsg::Field(s, b))
})
.height(Length::Fixed(FIELD_HEIGHT));
// A vertical slider grows upwards; the strip runs red at the top.
let strip = stack![
container(Space::new())
.width(STRIP_WIDTH)
.height(FIELD_HEIGHT)
.style(|_theme: &Theme| gradient_box(std::f32::consts::PI, &hue_stops())),
vertical_slider(0.0..=360.0, 360.0 - h, |v| msg(EditorMsg::Hue(360.0 - v)))
.width(STRIP_WIDTH)
.height(FIELD_HEIGHT)
.style(handle_style),
];
let value =
|letter: &'static str, v: f32, max: f32, unit: char, f: fn(f32) -> EditorMsg| {
row![
text(letter).size(12).font(iced::Font::MONOSPACE),
number_box(v, max, Some(unit), f),
]
.spacing(4)
.align_y(iced::Center)
};
column![
row![area, strip].spacing(12),
row![
value("H", h, 360.0, '°', EditorMsg::Hue),
value("S", s * 100.0, 100.0, '%', EditorMsg::Saturation),
value("B", b * 100.0, 100.0, '%', EditorMsg::Brightness),
Space::new().width(Length::Fill),
self.hex_field(),
]
.spacing(10)
.align_y(iced::Center),
]
// 108 + 10 matches the slider views' 3 × 26 + 2 × 14 + 12.
.spacing(10)
.into()
}
/// The hex value with `end` (the nearest index colour) at the far right.
fn hex_row<'a>(&self, end: Option<Element<'a, Message>>) -> Element<'a, Message> {
let mut hex_row = row![self.hex_field(), Space::new().width(Length::Fill)]
.spacing(8)
.align_y(iced::Center);
if let Some(end) = end {
hex_row = hex_row.push(end);
}
view.into()
hex_row.into()
}
fn hex_field<'a>(&self) -> Element<'a, Message> {
let (r, g, b) = self.rgb;
let hex = self
.hex_draft
.clone()
.unwrap_or_else(|| math::hex_string(r, g, b));
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)
.into()
}
}
+5 -2
View File
@@ -119,16 +119,19 @@ impl Eyedropper {
let size = GRID as f32 * CELL + 8.0;
let window_w = shot.size.width as f32 / shot.scale_factor;
let window_h = shot.size.height as f32 / shot.scale_factor;
// In a very small window neither side fits; stay inside the top-left.
let x = if cursor.x + OFFSET + size > window_w {
cursor.x - OFFSET - size
} else {
cursor.x + OFFSET
};
}
.max(0.0);
let y = if cursor.y + OFFSET + size + 20.0 > window_h {
cursor.y - OFFSET - size - 20.0
} else {
cursor.y + OFFSET
};
}
.max(0.0);
stack![catcher, pin(loupe).x(x).y(y)].into()
}
}
+4 -3
View File
@@ -78,12 +78,13 @@ pub fn nearest_aci(r: u8, g: u8, b: u8) -> u8 {
codec::types::aci_table::nearest_aci(r, g, b)
}
/// `#RRGGBB`, upper case.
/// `#RRGGBB`, upper case — the same format the Options hex fields store.
pub fn hex_string(r: u8, g: u8, b: u8) -> String {
format!("#{r:02X}{g:02X}{b:02X}")
crate::app::config::rgb_to_hex([r, g, b])
}
/// Parse `#RRGGBB`, `RRGGBB`, `#RGB` or `RGB`.
/// Parse `#RRGGBB`, `RRGGBB`, `#RGB` or `RGB`. Unlike `config::parse_hex`,
/// which Options uses, this also accepts the short three-digit form.
pub fn parse_hex(text: &str) -> Option<(u8, u8, u8)> {
let digits = text.trim().trim_start_matches('#');
if !digits.chars().all(|c| c.is_ascii_hexdigit()) {
+2 -2
View File
@@ -510,8 +510,8 @@ fn picker_msg(msg: ColorPickerMsg) -> Message {
}
fn ink_is_dark(color: Color) -> bool {
let byte = |c: f32| (c * 255.0).round() as u8;
math::ink_is_dark(byte(color.r), byte(color.g), byte(color.b))
let [r, g, b, _] = color.into_rgba8();
math::ink_is_dark(r, g, b)
}
fn filled(color: Color) -> container::Style {