forked from 1b-agent/opencadstudio-upstream
Picking a colour from the full palette showed a generic "Index" label. Add color_display_name: standard names for ACI 1-9 / ByLayer / ByBlock, "R,G,B" (0-255) for every other indexed or true colour. Used by the shared colour selector button and the ribbon colour combo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
520 lines
15 KiB
Rust
520 lines
15 KiB
Rust
//! 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::advanced::layout::{self, Layout};
|
|
use iced::advanced::widget::{self, Widget};
|
|
use iced::advanced::{mouse, overlay, renderer, Clipboard, Shell};
|
|
use iced::widget::{button, column, container, row, scrollable, text};
|
|
use iced::{Background, Border, Color, Element, Event, Length, Point, Rectangle, Renderer, Size, Theme, Vector};
|
|
|
|
/// Which "logical" entries the colour list offers besides the standard ACI
|
|
/// colours.
|
|
#[derive(Clone, Copy, Default)]
|
|
pub struct ColorExtras {
|
|
pub by_layer: 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
|
|
/// (ByBlock=0, ByLayer=256, indexed 1-255; RGB has no ACI slot → ByLayer).
|
|
pub fn color_to_aci_string(c: AcadColor) -> String {
|
|
match c {
|
|
AcadColor::ByBlock => "0".to_string(),
|
|
AcadColor::ByLayer => "256".to_string(),
|
|
AcadColor::Index(i) => i.to_string(),
|
|
AcadColor::Rgb { .. } => "256".to_string(),
|
|
}
|
|
}
|
|
|
|
/// Decode an ACI integer string back into an `AcadColor`.
|
|
pub fn aci_string_to_color(s: &str) -> AcadColor {
|
|
match s.trim().parse::<i16>().unwrap_or(256) {
|
|
0 => AcadColor::ByBlock,
|
|
256 => AcadColor::ByLayer,
|
|
n if (1..=255).contains(&n) => AcadColor::Index(n as u8),
|
|
_ => AcadColor::ByLayer,
|
|
}
|
|
}
|
|
|
|
/// Display name for a colour: the standard name for ACI 1-9 / ByLayer /
|
|
/// ByBlock, otherwise the "R,G,B" values (0-255) so unnamed palette colours
|
|
/// read meaningfully.
|
|
pub fn color_display_name(c: AcadColor) -> String {
|
|
let (_, label) = acad_color_display(c);
|
|
if label == "Index" || label == "Custom" {
|
|
match c {
|
|
AcadColor::Index(i) => {
|
|
let (r, g, b) =
|
|
acadrust::types::aci_table::aci_to_rgb(i).unwrap_or((128, 128, 128));
|
|
format!("{r},{g},{b}")
|
|
}
|
|
AcadColor::Rgb { r, g, b } => format!("{r},{g},{b}"),
|
|
_ => label.to_string(),
|
|
}
|
|
} else {
|
|
label.to_string()
|
|
}
|
|
}
|
|
|
|
/// 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()
|
|
}
|
|
|
|
/// Build a colour selector.
|
|
///
|
|
/// * `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,
|
|
open: bool,
|
|
extras: ColorExtras,
|
|
on_select: impl Fn(AcadColor) -> Message + 'a,
|
|
on_toggle: Message,
|
|
on_more: Message,
|
|
) -> Element<'a, Message> {
|
|
let (cur_bg, _) = acad_color_display(current);
|
|
let cur_name = color_display_name(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(Length::Fill);
|
|
|
|
if !open {
|
|
return head.into();
|
|
}
|
|
|
|
let popup = container(color_list(extras, on_select, on_more))
|
|
.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);
|
|
|
|
// The popup is shown as a floating overlay (anchored below the button) so
|
|
// it doesn't push the surrounding form down.
|
|
Element::new(Floating {
|
|
base: head.into(),
|
|
popup: popup.into(),
|
|
})
|
|
}
|
|
|
|
/// The colour list shown inside a picker popup: named ACI colours (with
|
|
/// swatches) plus a "More…" entry that opens the full palette window. Shared by
|
|
/// `color_selector` and the ribbon's colour overlay.
|
|
pub fn color_list<'a>(
|
|
extras: ColorExtras,
|
|
on_select: impl Fn(AcadColor) -> Message + 'a,
|
|
on_more: Message,
|
|
) -> Element<'a, Message> {
|
|
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),
|
|
)
|
|
.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)));
|
|
}
|
|
list = list.push(
|
|
button(text("More…").size(11).color(TEXT))
|
|
.on_press(on_more)
|
|
.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),
|
|
);
|
|
list.into()
|
|
}
|
|
|
|
/// Full ACI palette as a standalone window body: ByLayer / ByBlock plus the
|
|
/// 256-colour grid. `on_pick` is called with the chosen colour.
|
|
pub fn color_grid_window(on_pick: impl Fn(AcadColor) -> Message) -> Element<'static, Message> {
|
|
let chip = |color: AcadColor, label: &'static str| -> Element<'static, Message> {
|
|
let (bg, _) = acad_color_display(color);
|
|
button(
|
|
row![swatch(bg), text(label).size(11).color(TEXT)]
|
|
.spacing(5)
|
|
.align_y(iced::Center),
|
|
)
|
|
.on_press(on_pick(color))
|
|
.padding([3, 6])
|
|
.into()
|
|
};
|
|
|
|
const COLS: u16 = 16;
|
|
let mut grid = column![].spacing(2);
|
|
let mut idx: u16 = 1;
|
|
while idx <= 255 {
|
|
let mut r = row![].spacing(2);
|
|
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(18).height(18))
|
|
.on_press(on_pick(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);
|
|
}
|
|
|
|
container(
|
|
column![
|
|
text("Select Color").size(13).color(TEXT),
|
|
row![chip(AcadColor::ByLayer, "ByLayer"), chip(AcadColor::ByBlock, "ByBlock")].spacing(6),
|
|
scrollable(grid).height(Length::Fill),
|
|
]
|
|
.spacing(8),
|
|
)
|
|
.style(|_: &Theme| container::Style {
|
|
background: Some(Background::Color(PICKER_BG)),
|
|
..Default::default()
|
|
})
|
|
.padding(10)
|
|
.width(Length::Fill)
|
|
.height(Length::Fill)
|
|
.into()
|
|
}
|
|
|
|
/// A widget that renders `base` inline and `popup` as a floating overlay
|
|
/// anchored just below it.
|
|
struct Floating<'a> {
|
|
base: Element<'a, Message>,
|
|
popup: Element<'a, Message>,
|
|
}
|
|
|
|
impl<'a> Widget<Message, Theme, Renderer> for Floating<'a> {
|
|
fn children(&self) -> Vec<widget::Tree> {
|
|
vec![widget::Tree::new(&self.base), widget::Tree::new(&self.popup)]
|
|
}
|
|
|
|
fn diff(&self, tree: &mut widget::Tree) {
|
|
tree.diff_children(&[self.base.as_widget(), self.popup.as_widget()]);
|
|
}
|
|
|
|
fn size(&self) -> Size<Length> {
|
|
self.base.as_widget().size()
|
|
}
|
|
|
|
fn size_hint(&self) -> Size<Length> {
|
|
self.base.as_widget().size_hint()
|
|
}
|
|
|
|
fn layout(
|
|
&mut self,
|
|
tree: &mut widget::Tree,
|
|
renderer: &Renderer,
|
|
limits: &layout::Limits,
|
|
) -> layout::Node {
|
|
self.base
|
|
.as_widget_mut()
|
|
.layout(&mut tree.children[0], renderer, limits)
|
|
}
|
|
|
|
fn update(
|
|
&mut self,
|
|
tree: &mut widget::Tree,
|
|
event: &Event,
|
|
layout: Layout<'_>,
|
|
cursor: mouse::Cursor,
|
|
renderer: &Renderer,
|
|
clipboard: &mut dyn Clipboard,
|
|
shell: &mut Shell<'_, Message>,
|
|
viewport: &Rectangle,
|
|
) {
|
|
self.base.as_widget_mut().update(
|
|
&mut tree.children[0],
|
|
event,
|
|
layout,
|
|
cursor,
|
|
renderer,
|
|
clipboard,
|
|
shell,
|
|
viewport,
|
|
);
|
|
}
|
|
|
|
fn mouse_interaction(
|
|
&self,
|
|
tree: &widget::Tree,
|
|
layout: Layout<'_>,
|
|
cursor: mouse::Cursor,
|
|
viewport: &Rectangle,
|
|
renderer: &Renderer,
|
|
) -> mouse::Interaction {
|
|
self.base.as_widget().mouse_interaction(
|
|
&tree.children[0],
|
|
layout,
|
|
cursor,
|
|
viewport,
|
|
renderer,
|
|
)
|
|
}
|
|
|
|
fn operate(
|
|
&mut self,
|
|
tree: &mut widget::Tree,
|
|
layout: Layout<'_>,
|
|
renderer: &Renderer,
|
|
operation: &mut dyn widget::Operation,
|
|
) {
|
|
self.base
|
|
.as_widget_mut()
|
|
.operate(&mut tree.children[0], layout, renderer, operation);
|
|
}
|
|
|
|
fn draw(
|
|
&self,
|
|
tree: &widget::Tree,
|
|
renderer: &mut Renderer,
|
|
theme: &Theme,
|
|
style: &renderer::Style,
|
|
layout: Layout<'_>,
|
|
cursor: mouse::Cursor,
|
|
viewport: &Rectangle,
|
|
) {
|
|
self.base.as_widget().draw(
|
|
&tree.children[0],
|
|
renderer,
|
|
theme,
|
|
style,
|
|
layout,
|
|
cursor,
|
|
viewport,
|
|
);
|
|
}
|
|
|
|
fn overlay<'b>(
|
|
&'b mut self,
|
|
tree: &'b mut widget::Tree,
|
|
layout: Layout<'b>,
|
|
renderer: &Renderer,
|
|
_viewport: &Rectangle,
|
|
translation: Vector,
|
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
|
let bounds = layout.bounds();
|
|
let anchor = Point::new(
|
|
bounds.x + translation.x,
|
|
bounds.y + bounds.height + translation.y + 2.0,
|
|
);
|
|
Some(overlay::Element::new(Box::new(FloatingOverlay {
|
|
popup: &mut self.popup,
|
|
tree: &mut tree.children[1],
|
|
anchor,
|
|
})))
|
|
}
|
|
}
|
|
|
|
impl<'a> From<Floating<'a>> for Element<'a, Message> {
|
|
fn from(f: Floating<'a>) -> Self {
|
|
Element::new(f)
|
|
}
|
|
}
|
|
|
|
struct FloatingOverlay<'a, 'b> {
|
|
popup: &'b mut Element<'a, Message>,
|
|
tree: &'b mut widget::Tree,
|
|
anchor: Point,
|
|
}
|
|
|
|
impl overlay::Overlay<Message, Theme, Renderer> for FloatingOverlay<'_, '_> {
|
|
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
|
|
let viewport = Rectangle::with_size(bounds);
|
|
let limits = layout::Limits::new(Size::ZERO, viewport.size());
|
|
let node = self
|
|
.popup
|
|
.as_widget_mut()
|
|
.layout(self.tree, renderer, &limits);
|
|
let size = node.size();
|
|
let mut x = self.anchor.x;
|
|
let mut y = self.anchor.y;
|
|
if x + size.width > viewport.width {
|
|
x = (viewport.width - size.width).max(0.0);
|
|
}
|
|
if y + size.height > viewport.height {
|
|
// Not enough room below — flip above the anchor.
|
|
y = (self.anchor.y - bounds.height.min(0.0) - size.height).max(0.0);
|
|
}
|
|
layout::Node::with_children(size, vec![node]).translate(Vector::new(x, y))
|
|
}
|
|
|
|
fn draw(
|
|
&self,
|
|
renderer: &mut Renderer,
|
|
theme: &Theme,
|
|
style: &renderer::Style,
|
|
layout: Layout<'_>,
|
|
cursor: mouse::Cursor,
|
|
) {
|
|
let child = layout.children().next().unwrap();
|
|
self.popup.as_widget().draw(
|
|
self.tree,
|
|
renderer,
|
|
theme,
|
|
style,
|
|
child,
|
|
cursor,
|
|
&child.bounds(),
|
|
);
|
|
}
|
|
|
|
fn update(
|
|
&mut self,
|
|
event: &Event,
|
|
layout: Layout<'_>,
|
|
cursor: mouse::Cursor,
|
|
renderer: &Renderer,
|
|
clipboard: &mut dyn Clipboard,
|
|
shell: &mut Shell<'_, Message>,
|
|
) {
|
|
let child = layout.children().next().unwrap();
|
|
let vp = child.bounds();
|
|
self.popup.as_widget_mut().update(
|
|
self.tree, event, child, cursor, renderer, clipboard, shell, &vp,
|
|
);
|
|
}
|
|
|
|
fn operate(
|
|
&mut self,
|
|
layout: Layout<'_>,
|
|
renderer: &Renderer,
|
|
operation: &mut dyn widget::Operation,
|
|
) {
|
|
let child = layout.children().next().unwrap();
|
|
self.popup
|
|
.as_widget_mut()
|
|
.operate(self.tree, child, renderer, operation);
|
|
}
|
|
|
|
fn mouse_interaction(
|
|
&self,
|
|
layout: Layout<'_>,
|
|
cursor: mouse::Cursor,
|
|
renderer: &Renderer,
|
|
) -> mouse::Interaction {
|
|
let child = layout.children().next().unwrap();
|
|
self.popup
|
|
.as_widget()
|
|
.mouse_interaction(self.tree, child, cursor, &child.bounds(), renderer)
|
|
}
|
|
}
|