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:
parent
8cdb17a6b4
commit
96a5a32735
1 changed files with 158 additions and 118 deletions
|
|
@ -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,135 +57,155 @@ 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);
|
||||||
const COLS: u16 = 16;
|
button(
|
||||||
let mut rows = column![].spacing(1);
|
row![swatch(bg), text(name).size(11).color(TEXT)]
|
||||||
let mut idx: u16 = 1;
|
.spacing(5)
|
||||||
while idx <= 255 {
|
.align_y(iced::Center),
|
||||||
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(),
|
|
||||||
)
|
)
|
||||||
} else {
|
.on_press(on_select(color))
|
||||||
None
|
.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))
|
let mut list = column![].spacing(1);
|
||||||
.text_size(11)
|
if extras.by_layer {
|
||||||
.width(150);
|
list = list.push(named_row(AcadColor::ByLayer));
|
||||||
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(),
|
|
||||||
}
|
}
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue