forked from 1b-agent/opencadstudio-upstream
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:
33 files changed
+221
-53
No files matched your search
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user