Files
OneCAD-Draft/src/ui/color_select.rs
T

442 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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, row, text};
use iced::{Background, Border, Color, Element, Length, Theme};
use crate::t;
/// 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,
}
/// Encode a colour as the ACI integer string the style editors store
/// (ByBlock=0, ByLayer=256, indexed 1-255). True colours are mapped to the
/// closest ACI entry because these fields cannot store RGB values.
pub fn color_to_aci_string(c: AcadColor) -> String {
match c {
AcadColor::ByBlock => "0".to_string(),
AcadColor::ByLayer => "256".to_string(),
AcadColor::None => "257".to_string(),
AcadColor::Index(i) => i.to_string(),
AcadColor::Rgb { r, g, b } => nearest_aci(r, g, b).to_string(),
}
}
/// Convert an Iced colour chosen by `iced_aw::ColorPicker` into a DWG true
/// colour. ACI-only destinations map it to their closest indexed colour later.
pub fn iced_to_acad_color(color: Color) -> AcadColor {
let [r, g, b, _] = color.into_rgba8();
AcadColor::Rgb { r, g, b }
}
/// Return the closest AutoCAD Color Index for an RGB colour.
pub fn nearest_aci(r: u8, g: u8, b: u8) -> u8 {
let mut best = 7;
let mut best_distance = u32::MAX;
for index in 1..=255 {
let Some((ar, ag, ab)) = acadrust::types::aci_table::aci_to_rgb(index) else {
continue;
};
let dr = i32::from(r) - i32::from(ar);
let dg = i32::from(g) - i32::from(ag);
let db = i32::from(b) - i32::from(ab);
let distance = (dr * dr + dg * dg + db * db) as u32;
if distance < best_distance {
best = index;
best_distance = distance;
}
}
best
}
/// 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,
257 => AcadColor::None,
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 {
t!(label).into_owned()
}
}
/// A small colour square.
fn swatch<'a>(bg: Color) -> Element<'a, Message> {
container(text("").width(13).height(13))
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(bg)),
border: Border {
color: theme.palette().background.neutral.color,
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);
let on_dismiss = on_toggle.clone();
// Closed button: current swatch + name + caret.
let head = button(
row![
swatch(cur_bg),
text(cur_name).size(11),
crate::ui::icons::themed_arrow_toggle(open, 9.0),
]
.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: &Theme| {
let palette = theme.palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border {
color: palette.background.neutral.color,
width: 1.0,
radius: 2.0.into(),
},
..Default::default()
}
})
.padding(5)
.width(220);
// `DropDown` keeps the popup outside the surrounding form layout and
// handles viewport placement, Escape, and outside-click dismissal.
iced_aw::DropDown::new(head, popup, true)
.width(220)
.alignment(iced_aw::drop_down::Alignment::Bottom)
.offset(2.0)
.on_dismiss(on_dismiss)
.into()
}
fn list_row_style(theme: &Theme, status: button::Status) -> button::Style {
let palette = theme.palette();
let hovered = matches!(status, button::Status::Hovered);
let text_color = if hovered {
palette.background.strong.text
} else {
palette.background.base.text
};
button::Style {
background: hovered.then_some(Background::Color(palette.background.strong.color)),
text_color,
..Default::default()
}
}
/// 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(t!(name)).size(11)]
.spacing(5)
.align_y(iced::Center),
)
.on_press(on_select(color))
.style(list_row_style)
.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(t!("More…")).size(11))
.on_press(on_more)
.style(list_row_style)
.padding([2, 4])
.width(Length::Fill),
);
list.into()
}
/// Render `base` inline with `popup` in an `iced_aw` dropdown.
pub fn drop_down_below<'a>(
base: Element<'a, Message>,
popup: Element<'a, Message>,
popup_width: Length,
popup_height: Length,
on_dismiss: Message,
) -> Element<'a, Message> {
iced_aw::DropDown::new(base, popup, true)
.width(popup_width)
.height(popup_height)
.alignment(iced_aw::drop_down::Alignment::Bottom)
.offset(2.0)
.on_dismiss(on_dismiss)
.into()
}
/// Full CAD indexed-colour page used by the standalone Select Color dialog.
///
/// This only edits the pending colour. The caller commits it with
/// `Message::ColorWindowPick`, so choosing a swatch does not immediately close
/// the dialog.
pub fn index_color_page<'a>(
current: AcadColor,
recent_colors: &'a [AcadColor],
) -> Element<'a, Message> {
let swatch_button = |color: AcadColor, size: f32| -> Element<'a, Message> {
let (bg, _) = acad_color_display(color);
let selected = current == color;
button(
text("")
.width(Length::Fixed(size))
.height(Length::Fixed(size)),
)
.on_press(Message::ColorPickerColorChanged(color))
.style(move |theme: &Theme, status| {
let hovered = matches!(
status,
button::Status::Hovered | button::Status::Pressed
);
button::Style {
background: Some(Background::Color(bg)),
border: Border {
color: if selected || hovered {
theme.palette().primary.base.color
} else {
theme.palette().background.neutral.color
},
width: if selected {
2.5
} else if hovered {
1.5
} else {
1.0
},
radius: 1.0.into(),
},
..Default::default()
}
})
.padding(0)
.into()
};
// AutoCAD's first standard indexed colours.
let mut standard = row![].spacing(4);
for idx in 1u8..=9 {
standard = standard.push(swatch_button(AcadColor::Index(idx), 22.0));
}
// ACI 10-249.
//
// Each group of ten indices belongs together. Arrange the 24 colour
// families horizontally and their ten shade/intensity variants vertically.
// This makes the palette read as a colour spectrum instead of a raw numeric
// sequence.
let mut palette_rows = column![].spacing(2);
for variant in 0u8..10 {
let mut palette_row = row![].spacing(2);
for family in 1u8..=24 {
let idx = family * 10 + variant;
palette_row = palette_row.push(swatch_button(AcadColor::Index(idx), 16.0));
}
palette_rows = palette_rows.push(palette_row);
}
// ACI 250-255 are the grayscale entries and are shown separately.
let mut grayscale = row![].spacing(4);
for idx in 250u8..=255 {
grayscale =
grayscale.push(swatch_button(AcadColor::Index(idx), 22.0));
}
let recent: Element<'a, Message> = if recent_colors.is_empty() {
text("No recent colors yet")
.size(10)
.style(|theme: &Theme| iced::widget::text::Style {
color: Some(
theme
.palette()
.background
.base
.text
.scale_alpha(0.55),
),
})
.into()
} else {
let mut recent_row = row![].spacing(4);
for &color in recent_colors.iter().take(12) {
recent_row = recent_row.push(swatch_button(color, 22.0));
}
recent_row.into()
};
let selected_text = match current {
AcadColor::Index(i) => {
let (r, g, b) =
acadrust::types::aci_table::aci_to_rgb(i).unwrap_or((128, 128, 128));
format!("ACI {i} RGB {r}, {g}, {b}")
}
AcadColor::Rgb { r, g, b } => {
format!("True Color RGB {r}, {g}, {b}")
}
AcadColor::ByLayer => "ByLayer".to_string(),
AcadColor::ByBlock => "ByBlock".to_string(),
AcadColor::None => "None".to_string(),
};
let tabs = row![
button(text("Index Color").size(12))
.style(button::primary)
.padding([5, 14]),
button(text("True Color").size(12))
.on_press(Message::ColorPickerTabChanged(
crate::app::ColorPickerTab::TrueColor
))
.style(button::secondary)
.padding([5, 14]),
]
.spacing(4);
let actions = container(
row![
button(text("Cancel").size(11))
.on_press(Message::CloseColorPicker)
.style(button::secondary)
.padding([5, 14]),
button(text("OK").size(11))
.on_press(Message::ColorWindowPick(current))
.style(button::primary)
.padding([5, 18]),
]
.spacing(8),
)
.width(Length::Fill)
.align_x(iced::alignment::Horizontal::Right);
container(
column![
tabs,
text("Standard colors").size(11),
standard,
text("AutoCAD Color Index (ACI) 10–249").size(11),
text("Color family → · Shade / intensity ↓")
.size(9)
.style(|theme: &Theme| iced::widget::text::Style {
color: Some(theme.palette().background.base.text.scale_alpha(0.65)),
}),
container(palette_rows)
.padding(6)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.weakest.color
)),
border: Border {
color: theme.palette().background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
}),
text("Grayscale 250–255").size(11),
grayscale,
text("Recently used").size(11),
recent,
text(selected_text).size(11),
actions,
]
.spacing(9),
)
.padding(10)
.width(Length::Fixed(470.0))
.into()
}