From 96a5a3273591872b3f90e4f8fa4ccf97c3922d8c Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 14 Jun 2026 10:47:20 +0300 Subject: [PATCH] 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 --- src/ui/color_select.rs | 284 +++++++++++++++++++++++------------------ 1 file changed, 162 insertions(+), 122 deletions(-) diff --git a/src/ui/color_select.rs b/src/ui/color_select.rs index cac732c68..ba734dd0d 100644 --- a/src/ui/color_select.rs +++ b/src/ui/color_select.rs @@ -1,14 +1,15 @@ -//! Shared colour selector: a main-colour dropdown plus a "more colours" button -//! that expands the full ACI palette. Used by the properties panel and every -//! style editor so colour selection looks and behaves the same everywhere. +//! Shared colour selector: a dropdown-style button that opens a list of named +//! colours (each shown with its swatch) plus the full ACI palette. Used by the +//! properties panel and every style editor so colour selection looks and +//! behaves the same everywhere. use crate::app::Message; use crate::ui::properties::acad_color_display; use acadrust::types::Color as AcadColor; -use iced::widget::{button, column, container, pick_list, row, scrollable, text}; -use iced::{Background, Border, Color, Element, Theme}; +use iced::widget::{button, column, container, row, scrollable, text}; +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. #[derive(Clone, Copy, Default)] pub struct ColorExtras { @@ -16,6 +17,25 @@ pub struct ColorExtras { 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 /// (ByBlock=0, ByLayer=256, indexed 1-255; RGB has no ACI slot → ByLayer). pub fn color_to_aci_string(c: AcadColor) -> String { @@ -37,135 +57,155 @@ pub fn aci_string_to_color(s: &str) -> AcadColor { } } -/// A dropdown option wrapping an `AcadColor`, displayed by its colour name. -#[derive(Clone, PartialEq)] -struct ColorChoice(AcadColor); - -impl std::fmt::Display for ColorChoice { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let (_, name) = acad_color_display(self.0); - write!(f, "{name}") - } +/// A small colour square. +fn swatch<'a>(bg: Color) -> Element<'a, Message> { + container(text("").width(13).height(13)) + .style(move |_: &Theme| container::Style { + background: Some(Background::Color(bg)), + border: Border { + color: Color { + 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. /// -/// * `current` — the currently selected colour (shown in the dropdown). -/// * `palette_open` — whether the expanded 255-colour grid is shown. -/// * `extras` — whether ByLayer / ByBlock appear in the dropdown. -/// * `on_select` — called with the chosen colour (dropdown item or grid swatch). -/// * `on_more` — toggles the expanded palette. +/// * `current` — the currently selected colour (shown on the button). +/// * `open` — whether the colour list / palette is expanded. +/// * `extras` — whether ByLayer / ByBlock appear in the list. +/// * `on_select` — called with the chosen colour. +/// * `on_toggle` — opens / closes the list. pub fn color_selector<'a>( current: AcadColor, - palette_open: bool, + open: bool, extras: ColorExtras, on_select: impl Fn(AcadColor) -> Message + 'a, - on_more: Message, + on_toggle: Message, ) -> Element<'a, Message> { - // Standard ACI 1-7 plus the requested extras, and the current colour if it - // isn't already one of them (so a non-standard index still shows). - let mut opts: Vec = Vec::new(); - if extras.by_layer { - opts.push(ColorChoice(AcadColor::ByLayer)); - } - if extras.by_block { - opts.push(ColorChoice(AcadColor::ByBlock)); - } - for i in 1u8..=7 { - opts.push(ColorChoice(AcadColor::Index(i))); - } - let cur_choice = ColorChoice(current); - if !opts.contains(&cur_choice) { - opts.push(cur_choice.clone()); + let (cur_bg, cur_name) = acad_color_display(current); + + // Closed button: current swatch + name + caret. + let head = button( + row![ + swatch(cur_bg), + text(cur_name).size(11).color(TEXT), + text(if open { " ▲" } else { " ▾" }).size(9).color(TEXT), + ] + .spacing(5) + .align_y(iced::Center), + ) + .on_press(on_toggle) + .padding([3, 6]) + .width(170); + + if !open { + return head.into(); } - // Expanded ACI palette. Built before the dropdown closure so it can borrow - // `on_select`; the dropdown then moves it. - let grid: Option> = if palette_open { - const COLS: u16 = 16; - let mut rows = column![].spacing(1); - let mut idx: u16 = 1; - while idx <= 255 { - let mut r = row![].spacing(1); - for _ in 0..COLS { - if idx > 255 { - break; - } - let ci = idx as u8; - let (bg, _) = acad_color_display(AcadColor::Index(ci)); - let msg = on_select(AcadColor::Index(ci)); - r = r.push( - button(text("").width(12).height(12)) - .on_press(msg) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(bg)), - border: Border { - color: if matches!(status, button::Status::Hovered) { - Color::WHITE - } else { - Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.4, - } - }, - width: if matches!(status, button::Status::Hovered) { - 1.5 - } else { - 1.0 - }, - radius: 1.0.into(), - }, - ..Default::default() - }) - .padding(0), - ); - idx += 1; - } - rows = rows.push(r); - } - Some( - container(scrollable(rows).height(150)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PICKER_BG)), - border: Border { - color: BORDER, - width: 1.0, - radius: 0.0.into(), - }, - ..Default::default() - }) - .padding(4) - .into(), + // One named-colour row (swatch + name), selectable. + let named_row = |color: AcadColor| -> Element<'a, Message> { + let (bg, name) = acad_color_display(color); + button( + row![swatch(bg), text(name).size(11).color(TEXT)] + .spacing(5) + .align_y(iced::Center), ) - } else { - None + .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 dropdown = pick_list(opts, Some(cur_choice), move |c| on_select(c.0)) - .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(), + 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; + let mut grid = column![].spacing(1); + let mut idx: u16 = 1; + while idx <= 255 { + let mut r = row![].spacing(1); + for _ in 0..COLS { + if idx > 255 { + break; + } + let ci = idx as u8; + let (bg, _) = acad_color_display(AcadColor::Index(ci)); + r = r.push( + button(text("").width(12).height(12)) + .on_press(on_select(AcadColor::Index(ci))) + .style(move |_: &Theme, status| button::Style { + background: Some(Background::Color(bg)), + border: Border { + color: if matches!(status, button::Status::Hovered) { + Color::WHITE + } else { + Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.4, + } + }, + width: if matches!(status, button::Status::Hovered) { + 1.5 + } else { + 1.0 + }, + radius: 1.0.into(), + }, + ..Default::default() + }) + .padding(0), + ); + idx += 1; + } + grid = grid.push(r); + } + + let popup = container( + column![list, scrollable(grid).height(120)].spacing(4), + ) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(PICKER_BG)), + border: Border { + color: BORDER, + width: 1.0, + radius: 2.0.into(), + }, + ..Default::default() + }) + .padding(5) + .width(220); + + column![head, popup].spacing(2).into() }