feat(ui): show colour swatches beside names in the colour selector (#94)

Reworked the shared colour selector from a plain text pick_list into a
dropdown button that opens a list where each named colour (ByLayer,
ByBlock, ACI 1-9) shows its swatch next to the name, with the full ACI
palette grid below. The dimension/multileader/table style editors pick
it up unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-14 10:47:20 +03:00
commit 96a5a32735

View file

@ -1,14 +1,15 @@
//! Shared colour selector: a main-colour dropdown plus a "more colours" button //! Shared colour selector: a dropdown-style button that opens a list of named
//! that expands the full ACI palette. Used by the properties panel and every //! colours (each shown with its swatch) plus the full ACI palette. Used by the
//! style editor so colour selection looks and behaves the same everywhere. //! properties panel and every style editor so colour selection looks and
//! behaves the same everywhere.
use crate::app::Message; use crate::app::Message;
use crate::ui::properties::acad_color_display; use crate::ui::properties::acad_color_display;
use acadrust::types::Color as AcadColor; use acadrust::types::Color as AcadColor;
use iced::widget::{button, column, container, pick_list, row, scrollable, text}; use iced::widget::{button, column, container, row, scrollable, text};
use iced::{Background, Border, Color, Element, Theme}; use iced::{Background, Border, Color, Element, Length, Theme};
/// Which "logical" entries the main dropdown offers besides the standard ACI /// Which "logical" entries the colour list offers besides the standard ACI
/// colours. /// colours.
#[derive(Clone, Copy, Default)] #[derive(Clone, Copy, Default)]
pub struct ColorExtras { pub struct ColorExtras {
@ -16,6 +17,25 @@ pub struct ColorExtras {
pub by_block: bool, pub by_block: bool,
} }
const PICKER_BG: Color = Color {
r: 0.12,
g: 0.12,
b: 0.12,
a: 1.0,
};
const BORDER: Color = Color {
r: 0.35,
g: 0.35,
b: 0.35,
a: 1.0,
};
const TEXT: Color = Color {
r: 0.88,
g: 0.88,
b: 0.88,
a: 1.0,
};
/// Encode a colour as the ACI integer string the style editors store /// Encode a colour as the ACI integer string the style editors store
/// (ByBlock=0, ByLayer=256, indexed 1-255; RGB has no ACI slot → ByLayer). /// (ByBlock=0, ByLayer=256, indexed 1-255; RGB has no ACI slot → ByLayer).
pub fn color_to_aci_string(c: AcadColor) -> String { pub fn color_to_aci_string(c: AcadColor) -> String {
@ -37,66 +57,100 @@ pub fn aci_string_to_color(s: &str) -> AcadColor {
} }
} }
/// A dropdown option wrapping an `AcadColor`, displayed by its colour name. /// A small colour square.
#[derive(Clone, PartialEq)] fn swatch<'a>(bg: Color) -> Element<'a, Message> {
struct ColorChoice(AcadColor); container(text("").width(13).height(13))
.style(move |_: &Theme| container::Style {
impl std::fmt::Display for ColorChoice { background: Some(Background::Color(bg)),
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { border: Border {
let (_, name) = acad_color_display(self.0); color: Color {
write!(f, "{name}") r: 0.0,
} g: 0.0,
b: 0.0,
a: 0.5,
},
width: 1.0,
radius: 2.0.into(),
},
..Default::default()
})
.width(13)
.height(13)
.into()
} }
const PICKER_BG: Color = Color {
r: 0.12,
g: 0.12,
b: 0.12,
a: 1.0,
};
const BORDER: Color = Color {
r: 0.35,
g: 0.35,
b: 0.35,
a: 1.0,
};
/// Build a colour selector. /// Build a colour selector.
/// ///
/// * `current` — the currently selected colour (shown in the dropdown). /// * `current` — the currently selected colour (shown on the button).
/// * `palette_open` — whether the expanded 255-colour grid is shown. /// * `open` — whether the colour list / palette is expanded.
/// * `extras` — whether ByLayer / ByBlock appear in the dropdown. /// * `extras` — whether ByLayer / ByBlock appear in the list.
/// * `on_select` — called with the chosen colour (dropdown item or grid swatch). /// * `on_select` — called with the chosen colour.
/// * `on_more` — toggles the expanded palette. /// * `on_toggle` — opens / closes the list.
pub fn color_selector<'a>( pub fn color_selector<'a>(
current: AcadColor, current: AcadColor,
palette_open: bool, open: bool,
extras: ColorExtras, extras: ColorExtras,
on_select: impl Fn(AcadColor) -> Message + 'a, on_select: impl Fn(AcadColor) -> Message + 'a,
on_more: Message, on_toggle: Message,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
// Standard ACI 1-7 plus the requested extras, and the current colour if it let (cur_bg, cur_name) = acad_color_display(current);
// isn't already one of them (so a non-standard index still shows).
let mut opts: Vec<ColorChoice> = Vec::new(); // Closed button: current swatch + name + caret.
if extras.by_layer { let head = button(
opts.push(ColorChoice(AcadColor::ByLayer)); row![
} swatch(cur_bg),
if extras.by_block { text(cur_name).size(11).color(TEXT),
opts.push(ColorChoice(AcadColor::ByBlock)); text(if open { " ▲" } else { " ▾" }).size(9).color(TEXT),
} ]
for i in 1u8..=7 { .spacing(5)
opts.push(ColorChoice(AcadColor::Index(i))); .align_y(iced::Center),
} )
let cur_choice = ColorChoice(current); .on_press(on_toggle)
if !opts.contains(&cur_choice) { .padding([3, 6])
opts.push(cur_choice.clone()); .width(170);
if !open {
return head.into();
} }
// Expanded ACI palette. Built before the dropdown closure so it can borrow // One named-colour row (swatch + name), selectable.
// `on_select`; the dropdown then moves it. let named_row = |color: AcadColor| -> Element<'a, Message> {
let grid: Option<Element<'a, Message>> = if palette_open { let (bg, name) = acad_color_display(color);
button(
row![swatch(bg), text(name).size(11).color(TEXT)]
.spacing(5)
.align_y(iced::Center),
)
.on_press(on_select(color))
.style(|_: &Theme, status| button::Style {
background: matches!(status, button::Status::Hovered)
.then_some(Background::Color(Color {
r: 0.25,
g: 0.25,
b: 0.30,
a: 1.0,
})),
..Default::default()
})
.padding([2, 4])
.width(Length::Fill)
.into()
};
let mut list = column![].spacing(1);
if extras.by_layer {
list = list.push(named_row(AcadColor::ByLayer));
}
if extras.by_block {
list = list.push(named_row(AcadColor::ByBlock));
}
for i in 1u8..=9 {
list = list.push(named_row(AcadColor::Index(i)));
}
// Full ACI palette grid (10-255) below the named colours.
const COLS: u16 = 16; const COLS: u16 = 16;
let mut rows = column![].spacing(1); let mut grid = column![].spacing(1);
let mut idx: u16 = 1; let mut idx: u16 = 1;
while idx <= 255 { while idx <= 255 {
let mut r = row![].spacing(1); let mut r = row![].spacing(1);
@ -106,10 +160,9 @@ pub fn color_selector<'a>(
} }
let ci = idx as u8; let ci = idx as u8;
let (bg, _) = acad_color_display(AcadColor::Index(ci)); let (bg, _) = acad_color_display(AcadColor::Index(ci));
let msg = on_select(AcadColor::Index(ci));
r = r.push( r = r.push(
button(text("").width(12).height(12)) button(text("").width(12).height(12))
.on_press(msg) .on_press(on_select(AcadColor::Index(ci)))
.style(move |_: &Theme, status| button::Style { .style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(bg)), background: Some(Background::Color(bg)),
border: Border { border: Border {
@ -136,36 +189,23 @@ pub fn color_selector<'a>(
); );
idx += 1; idx += 1;
} }
rows = rows.push(r); grid = grid.push(r);
} }
Some(
container(scrollable(rows).height(150)) let popup = container(
column![list, scrollable(grid).height(120)].spacing(4),
)
.style(|_: &Theme| container::Style { .style(|_: &Theme| container::Style {
background: Some(Background::Color(PICKER_BG)), background: Some(Background::Color(PICKER_BG)),
border: Border { border: Border {
color: BORDER, color: BORDER,
width: 1.0, width: 1.0,
radius: 0.0.into(), radius: 2.0.into(),
}, },
..Default::default() ..Default::default()
}) })
.padding(4) .padding(5)
.into(), .width(220);
)
} else {
None
};
let dropdown = pick_list(opts, Some(cur_choice), move |c| on_select(c.0)) column![head, popup].spacing(2).into()
.text_size(11)
.width(150);
let more = button(text(if palette_open { "×" } else { "⋯" }).size(11))
.on_press(on_more)
.padding([2, 7]);
let top = row![dropdown, more].spacing(4).align_y(iced::Center);
match grid {
Some(g) => column![top, g].spacing(2).into(),
None => top.into(),
}
} }