Select Color: bottom-aligned field, list view, name sorting, book names

- True Color opens in RGB; the gradient field, hex and nearest index sit
  at the bottom so CMYK's fourth slider doesn't move them; slider rows
  closer together.
- Document tab: grid or list view (list drawn like the colour-book bars),
  and Name A–Z / Z–A sorting (translated in all locales). Swatches of
  colour-book colours show the colour's name.
- A colour-book colour picked as the current colour (nothing selected)
  keeps its BOOK$NAME on the objects drawn with it.

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-14 13:13:00 +00:00
1 parent ecdead20c7
commit b039e3dc48
33 files changed
+221 -53

No files matched your search

+10 -2
View File
@@ -358,7 +358,11 @@ impl OpenCADStudio {
if layer != "0" || entity.as_entity().layer().is_empty() {
entity.as_entity_mut().set_layer(layer);
}
crate::scene::view::dispatch::apply_color(&mut entity, self.ribbon.active_color);
crate::scene::view::dispatch::apply_named_color(
&mut entity,
self.ribbon.active_color,
self.ribbon.active_color_name(),
);
crate::scene::view::dispatch::apply_common_prop(
&mut entity,
"linetype",
@@ -476,7 +480,11 @@ impl OpenCADStudio {
if layer != "0" || entity.as_entity().layer().is_empty() {
entity.as_entity_mut().set_layer(layer);
}
crate::scene::view::dispatch::apply_color(&mut entity, self.ribbon.active_color);
crate::scene::view::dispatch::apply_named_color(
&mut entity,
self.ribbon.active_color,
self.ribbon.active_color_name(),
);
crate::scene::view::dispatch::apply_common_prop(
&mut entity,
"linetype",
+5 -1
View File
@@ -3149,7 +3149,11 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
}
if !preserve_entity_style {
crate::scene::view::dispatch::apply_color(&mut entity, self.ribbon.active_color);
crate::scene::view::dispatch::apply_named_color(
&mut entity,
self.ribbon.active_color,
self.ribbon.active_color_name(),
);
crate::scene::view::dispatch::apply_common_prop(
&mut entity,
"linetype",
+4
View File
@@ -11699,6 +11699,9 @@ impl OpenCADStudio {
};
match target {
crate::app::ColorPickTarget::Properties => {}
crate::app::ColorPickTarget::Ribbon => {
return (color, false, self.ribbon.active_color_name());
}
crate::app::ColorPickTarget::Layer(layer) => {
let name = document.layers.get(layer).and_then(|layer| {
match (&layer.book_name, &layer.color_name) {
@@ -11767,6 +11770,7 @@ impl OpenCADStudio {
let handles = self.property_target_handles(i);
if handles.is_empty() {
let task = self.on_ribbon_color_changed(color);
self.ribbon.active_color_book = name.map(|name| (color, name));
self.refresh_properties();
return task;
}
+5 -1
View File
@@ -1215,7 +1215,11 @@ impl OpenCADStudio {
Some(Message::PropColorFieldChanged { field, color })
}
Some(crate::app::ColorPickTarget::MText) => Some(Message::MTextColorChanged(color)),
Some(crate::app::ColorPickTarget::Ribbon) => Some(Message::RibbonColorChanged(color)),
Some(crate::app::ColorPickTarget::Ribbon) => {
let task = self.update(Message::RibbonColorChanged(color));
self.ribbon.active_color_book = name.map(|name| (color, name));
return task;
}
Some(crate::app::ColorPickTarget::Layer(layer)) => {
let i = self.active_tab;
match self.layer_panel_index(i, &layer) {
+2
View File
@@ -4723,6 +4723,8 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"Recently used" => Some(("common", "color-recently-used")),
"Most used" => Some(("common", "color-most-used")),
"Color number" => Some(("common", "color-number")),
"Name A–Z" => Some(("common", "color-name-az")),
"Name Z–A" => Some(("common", "color-name-za")),
"Colors in this drawing" => Some(("common", "color-in-drawing")),
"Sort" => Some(("common", "color-sort")),
"No colors in this drawing yet" => Some(("common", "color-no-drawing-colors")),
+35 -11
View File
@@ -21,21 +21,35 @@ pub struct DocColor {
}
impl DocColor {
/// The few characters drawn on the swatch: the index number, `RGB`, or
/// the first word of the colour book.
/// The text drawn on the swatch: the index number, `RGB`, or the
/// colour-book colour's own name.
pub fn tag(&self) -> String {
match (&self.name, self.color) {
(Some(name), _) => name
.split(['$', ' '])
.next()
.unwrap_or_default()
.chars()
.take(5)
.collect(),
(Some(_), _) => self.label(),
(None, AcadColor::Index(i)) => i.to_string(),
_ => "RGB".to_string(),
}
}
/// The name in the list view: the colour-book colour's own name (without
/// the book), `ACI n` or `#RRGGBB`.
pub fn label(&self) -> String {
match &self.name {
Some(name) => name
.split_once('$')
.map_or(name.as_str(), |(_, name)| name)
.to_string(),
None => math::short_name(self.color),
}
}
}
/// Swatches or a list of named bars.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DocView {
#[default]
Grid,
List,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
@@ -45,14 +59,18 @@ pub enum DocSort {
Usage,
Hue,
Number,
NameAsc,
NameDesc,
}
impl DocSort {
pub const ALL: [DocSort; 4] = [
pub const ALL: [DocSort; 6] = [
DocSort::Recent,
DocSort::Usage,
DocSort::Hue,
DocSort::Number,
DocSort::NameAsc,
DocSort::NameDesc,
];
}
@@ -63,6 +81,8 @@ impl std::fmt::Display for DocSort {
DocSort::Usage => crate::t!("Most used"),
DocSort::Hue => crate::t!("Color"),
DocSort::Number => crate::t!("Color number"),
DocSort::NameAsc => crate::t!("Name A–Z"),
DocSort::NameDesc => crate::t!("Name Z–A"),
})
}
}
@@ -163,6 +183,10 @@ pub fn sort(colors: &mut [DocColor], by: DocSort, recent: &[AcadColor]) {
.then_with(|| a.name.cmp(&b.name))
.then_with(|| key(a.color).cmp(&key(b.color)))
}),
DocSort::NameAsc => colors.sort_by_cached_key(|c| c.label().to_lowercase()),
DocSort::NameDesc => {
colors.sort_by_cached_key(|c| std::cmp::Reverse(c.label().to_lowercase()))
}
}
}
@@ -201,7 +225,7 @@ mod tests {
assert_eq!(colors[0].uses, 3);
assert_eq!(colors[0].newest, Handle::new(3));
assert_eq!(colors[3].name, None, "an empty name is no name");
assert_eq!(colors[2].tag(), "RAL");
assert_eq!(colors[2].tag(), "RAL 5015");
assert_eq!(colors[0].tag(), "1");
assert_eq!(colors[1].tag(), "RGB");
}
+10 -10
View File
@@ -15,15 +15,15 @@ use iced::{Background, Border, Color, Element, Gradient, Length, Radians, Theme}
const ROW_HEIGHT: f32 = 26.0;
const TRACK: f32 = 18.0;
const STRIP_WIDTH: f32 = 22.0;
const ROW_GAP: f32 = 8.0;
const ROW_GAP: f32 = 6.0;
const FIELD_HEIGHT: f32 = 96.0;
const MODE_WIDTH: f32 = 48.0;
const MODE_HEIGHT: f32 = 24.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EditorMode {
Rgb,
#[default]
Rgb,
Hsb,
Cmyk,
}
@@ -151,19 +151,19 @@ 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).
/// With `fill` the field and hex row sit at the bottom of the space, so
/// they stay put when CMYK adds a fourth slider.
pub fn view<'a>(
&'a self,
mode_end: Option<Element<'a, Message>>,
hex_end: Option<Element<'a, Message>>,
fill: bool,
) -> Element<'a, Message> {
column![
self.mode_row(mode_end),
self.slider_rows(),
self.field(),
self.hex_row(hex_end),
]
.spacing(12)
.into()
let mut view = column![self.mode_row(mode_end), self.slider_rows()].spacing(12);
if fill {
view = view.push(Space::new().height(Length::Fill));
}
view.push(self.field()).push(self.hex_row(hex_end)).into()
}
/// RGB, HSB and CMYK, with `end` at the far right.
+91 -28
View File
@@ -6,7 +6,7 @@
//! colour editor: no tabs, no index colours.
use super::book::ColorBook;
use super::document::{self, DocColor, DocSort};
use super::document::{self, DocColor, DocSort, DocView};
use super::editor::{rgb_color, ColorEditor, EditorMsg};
use super::eyedropper::{Eyedropper, EyedropperMsg};
use super::math;
@@ -54,6 +54,7 @@ pub enum ColorPickerMsg {
/// A colour of the Document tab, by its place in the list.
PickDocument(usize),
DocSort(DocSort),
DocView(DocView),
/// Show another loaded colour book.
Book(usize),
BookPage(usize),
@@ -94,6 +95,7 @@ pub struct ColorPicker {
eyedropper: Option<Eyedropper>,
doc: Vec<DocColor>,
doc_sort: DocSort,
doc_view: DocView,
/// This drawing's colours picked this session, newest first.
recent: Vec<AcadColor>,
books: Arc<Vec<ColorBook>>,
@@ -118,6 +120,7 @@ impl ColorPicker {
eyedropper: None,
doc: Vec::new(),
doc_sort: DocSort::default(),
doc_view: DocView::default(),
recent: Vec::new(),
books: Arc::default(),
book: 0,
@@ -284,6 +287,7 @@ impl ColorPicker {
self.pending_name = name;
}
}
ColorPickerMsg::DocView(view) => self.doc_view = view,
ColorPickerMsg::DocSort(by) => {
self.doc_sort = by;
document::sort(&mut self.doc, by, &self.recent);
@@ -358,8 +362,8 @@ impl ColorPicker {
// height across its tabs, with the info line at the bottom.
let mut body = column![body].spacing(8);
if !editor_only {
// Document and Color books fill the height themselves.
if matches!(self.tab, ColorPickerTab::Index | ColorPickerTab::TrueColor) {
// The other tabs fill the height themselves.
if self.tab == ColorPickerTab::Index {
body = body.push(Space::new().height(Length::Fill));
}
let fill =
@@ -578,6 +582,11 @@ impl ColorPicker {
.on_select(|by| picker_msg(ColorPickerMsg::DocSort(by)))
.text_size(12)
.padding([3, 6]),
row![
self.view_button(crate::ui::icons::GRID, DocView::Grid),
self.view_button(crate::ui::icons::LIST, DocView::List),
]
.spacing(2),
]
.spacing(6)
.align_y(iced::Center);
@@ -589,9 +598,59 @@ impl ColorPicker {
.spacing(10)
.into();
}
let grid = self.doc.chunks(DOC_COLUMNS).enumerate().fold(
column![].spacing(4),
|grid, (r, chunk)| {
let list: Element<'_, Message> = if self.doc_view == DocView::List {
self.doc
.iter()
.enumerate()
.fold(column![].spacing(2), |list, (i, entry)| {
let fill = math::color_rgb(entry.color)
.map_or(Color::from_rgb8(128, 128, 128), rgb_color);
let selected = self.pending == entry.color && self.pending_name == entry.name;
list.push(
color_bar(
entry.label(),
fill,
selected,
ColorPickerMsg::PickDocument(i),
)
.height(26),
)
})
.into()
} else {
self.doc_grid()
};
column![
header,
scrollable(list).height(Length::Fill).spacing(4).direction(
scrollable::Direction::Vertical(
scrollable::Scrollbar::new().width(4).scroller_width(4)
)
),
]
.spacing(10)
.into()
}
/// Grid or list toggle for the Document tab.
fn view_button(&self, icon: &'static [u8], view: DocView) -> Element<'_, Message> {
let active = self.doc_view == view;
button(
svg(crate::ui::icons::themed_handle(icon))
.width(14)
.height(14),
)
.on_press(picker_msg(ColorPickerMsg::DocView(view)))
.padding(5)
.style(move |theme: &Theme, status| toggle_style(theme, active, status))
.into()
}
fn doc_grid(&self) -> Element<'_, Message> {
self.doc
.chunks(DOC_COLUMNS)
.enumerate()
.fold(column![].spacing(4), |grid, (r, chunk)| {
let line = chunk
.iter()
.enumerate()
@@ -601,18 +660,8 @@ impl ColorPicker {
let line = (chunk.len()..DOC_COLUMNS)
.fold(line, |line, _| line.push(Space::new().width(Length::Fill)));
grid.push(line)
},
);
column![
header,
scrollable(grid).height(Length::Fill).spacing(4).direction(
scrollable::Direction::Vertical(
scrollable::Scrollbar::new().width(4).scroller_width(4)
)
),
]
.spacing(10)
.into()
})
.into()
}
fn doc_swatch<'a>(&self, i: usize, entry: &'a DocColor) -> Element<'a, Message> {
@@ -627,7 +676,8 @@ impl ColorPicker {
)
.align_y(iced::Bottom)
.padding([1, 4])
.height(Length::Fill),
.height(Length::Fill)
.clip(true),
)
.on_press(picker_msg(ColorPickerMsg::PickDocument(i)))
.padding(0)
@@ -863,7 +913,7 @@ impl ColorPicker {
)
};
let nearest = (!self.editor_only()).then(|| self.nearest_button());
self.editor.view(pipette, nearest)
self.editor.view(pipette, nearest, !self.editor_only())
}
fn nearest_button(&self) -> Element<'_, Message> {
@@ -985,24 +1035,37 @@ fn book_bar(
color: &super::book::BookColor,
selected: Option<usize>,
) -> Element<'_, Message> {
let fill = rgb_color(color.rgb);
color_bar(
&color.name,
rgb_color(color.rgb),
selected == Some(i),
ColorPickerMsg::PickBook(i),
)
.height(Length::Fill)
.into()
}
/// A full-width colour bar with a name: the Color books pages and the
/// Document tab's list.
fn color_bar<'a>(
name: impl text::IntoFragment<'a>,
fill: Color,
selected: bool,
on_press: ColorPickerMsg,
) -> button::Button<'a, Message> {
button(
container(
text(&color.name)
text(name)
.size(12)
.font(iced::Font::MONOSPACE)
.color(ink(fill)),
)
.center(Length::Fill),
)
.on_press(picker_msg(ColorPickerMsg::PickBook(i)))
.on_press(picker_msg(on_press))
.padding(0)
.width(Length::Fill)
.height(Length::Fill)
.style(move |theme: &Theme, status| {
swatch_button_style(theme, status, fill, selected == Some(i))
})
.into()
.style(move |theme: &Theme, status| swatch_button_style(theme, status, fill, selected))
}
/// A short gray note in the middle of the free space.
+1
View File
@@ -81,6 +81,7 @@ pub static PIN_ACTIVE: &[u8] = include_bytes!("../../assets/icons/ui/pin_active.
pub static SPLIT_V: &[u8] = include_bytes!("../../assets/icons/ui/split_v.svg");
pub static SPLIT_H: &[u8] = include_bytes!("../../assets/icons/ui/split_h.svg");
pub static GRID: &[u8] = include_bytes!("../../assets/icons/ui/grid.svg");
pub static LIST: &[u8] = include_bytes!("../../assets/icons/ui/list.svg");
pub static SNAP: &[u8] = include_bytes!("../../assets/icons/ui/snap.svg");
pub static DOC_NEW: &[u8] = include_bytes!("../../assets/icons/ui/doc_new.svg");
pub static DOC: &[u8] = include_bytes!("../../assets/icons/ui/doc.svg");
+13
View File
@@ -82,6 +82,9 @@ pub struct Ribbon {
pub layer_filter: String,
/// Active object color — ACI / ByLayer / ByBlock.
pub active_color: AcadColor,
/// Colour-book identity (`BOOK$NAME`) of a colour picked from a colour
/// book, with the colour it was picked as; see `active_color_name`.
pub active_color_book: Option<(AcadColor, String)>,
/// Active linetype override ("ByLayer", "Continuous", …).
pub active_linetype: String,
/// Active lineweight.
@@ -171,6 +174,15 @@ fn position_ribbon_dropdown<'a>(
}
impl Ribbon {
/// The colour-book identity of the active colour, while the active colour
/// is still the one picked from the book.
pub fn active_color_name(&self) -> Option<String> {
self.active_color_book
.as_ref()
.filter(|(color, _)| *color == self.active_color)
.map(|(_, name)| name.clone())
}
pub fn new() -> Self {
Self {
modules: all_ribbon_modules(),
@@ -195,6 +207,7 @@ impl Ribbon {
layer_infos: vec![],
layer_filter: String::new(),
active_color: AcadColor::ByLayer,
active_color_book: None,
active_linetype: "ByLayer".to_string(),
active_lineweight: LineWeight::ByLayer,
available_linetypes: vec![LinetypeItem {