forked from 1b-agent/opencadstudio-upstream
feat(color): add CAD indexed color picker
This commit is contained in:
1 parent
60f5bca0d4
commit
4697e8cc33
4 files changed
+293
-21
No files matched your search
+20
-3
@@ -599,6 +599,11 @@ pub(super) struct OpenCADStudio {
|
|||||||
// ── Floating panel windows ────────────────────────────────────────────
|
// ── Floating panel windows ────────────────────────────────────────────
|
||||||
/// Active `iced_aw` colour picker: destination plus its initial colour.
|
/// Active `iced_aw` colour picker: destination plus its initial colour.
|
||||||
color_pick_target: Option<(ColorPickTarget, AcadColor)>,
|
color_pick_target: Option<(ColorPickTarget, AcadColor)>,
|
||||||
|
/// Visible page in the shared CAD colour picker.
|
||||||
|
color_picker_tab: ColorPickerTab,
|
||||||
|
/// Recently committed real colours, newest first.
|
||||||
|
/// ACI and True Color remain semantically distinct even when their RGB matches.
|
||||||
|
recent_colors: Vec<AcadColor>,
|
||||||
/// The open in-canvas modal dialog, if any (Plan B: shared overlay instead
|
/// The open in-canvas modal dialog, if any (Plan B: shared overlay instead
|
||||||
/// of OS windows).
|
/// of OS windows).
|
||||||
active_modal: Option<ModalKind>,
|
active_modal: Option<ModalKind>,
|
||||||
@@ -1162,7 +1167,13 @@ pub struct SaveOutcome {
|
|||||||
refreshed_preview: Option<Option<acadrust::Preview>>,
|
refreshed_preview: Option<Option<acadrust::Preview>>,
|
||||||
result: Result<(), crate::io::SaveFailure>,
|
result: Result<(), crate::io::SaveFailure>,
|
||||||
}
|
}
|
||||||
|
/// Active page in the shared CAD colour picker.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum ColorPickerTab {
|
||||||
|
#[default]
|
||||||
|
Index,
|
||||||
|
TrueColor,
|
||||||
|
}
|
||||||
/// Where a colour chosen in the standalone palette window should be applied.
|
/// Where a colour chosen in the standalone palette window should be applied.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum ColorPickTarget {
|
pub enum ColorPickTarget {
|
||||||
@@ -2904,11 +2915,15 @@ pub enum Message {
|
|||||||
DsCenterMarkMode(String),
|
DsCenterMarkMode(String),
|
||||||
/// Toggle the expanded colour palette for a DimStyle colour field.
|
/// Toggle the expanded colour palette for a DimStyle colour field.
|
||||||
DsColorMore(DsField),
|
DsColorMore(DsField),
|
||||||
/// Open the `iced_aw` colour picker for a field and its current colour.
|
/// Open the shared CAD colour picker for a field and its current colour.
|
||||||
OpenColorWindow(ColorPickTarget, AcadColor),
|
OpenColorWindow(ColorPickTarget, AcadColor),
|
||||||
|
/// Switch between Index Color and True Color.
|
||||||
|
ColorPickerTabChanged(ColorPickerTab),
|
||||||
|
/// Change the pending colour without committing it yet.
|
||||||
|
ColorPickerColorChanged(AcadColor),
|
||||||
/// Close the colour picker without choosing.
|
/// Close the colour picker without choosing.
|
||||||
CloseColorPicker,
|
CloseColorPicker,
|
||||||
/// A colour was chosen in the `iced_aw` picker.
|
/// Commit the colour chosen in the shared picker.
|
||||||
ColorWindowPick(acadrust::types::Color),
|
ColorWindowPick(acadrust::types::Color),
|
||||||
/// Set a block/linetype Handle field on the selected dim style from a
|
/// Set a block/linetype Handle field on the selected dim style from a
|
||||||
/// dropdown of available block-records / linetypes (by name).
|
/// dropdown of available block-records / linetypes (by name).
|
||||||
@@ -3127,6 +3142,8 @@ impl OpenCADStudio {
|
|||||||
last_point: None,
|
last_point: None,
|
||||||
main_window: None,
|
main_window: None,
|
||||||
color_pick_target: None,
|
color_pick_target: None,
|
||||||
|
color_picker_tab: ColorPickerTab::Index,
|
||||||
|
recent_colors: Vec::new(),
|
||||||
active_modal: None,
|
active_modal: None,
|
||||||
find_replace: FindReplaceState::default(),
|
find_replace: FindReplaceState::default(),
|
||||||
aec_drop_acknowledged: false,
|
aec_drop_acknowledged: false,
|
||||||
|
|||||||
+37
-2
@@ -7321,21 +7321,56 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
Message::OpenColorWindow(target, color) => {
|
Message::OpenColorWindow(target, color) => {
|
||||||
self.color_pick_target = Some((target, color));
|
self.color_pick_target = Some((target, color));
|
||||||
|
|
||||||
|
// Always open the shared CAD colour picker on the indexed ACI page.
|
||||||
|
self.color_picker_tab = crate::app::ColorPickerTab::Index;
|
||||||
|
self.modal_offset = iced::Vector::ZERO;
|
||||||
self.ds_color_open = None;
|
self.ds_color_open = None;
|
||||||
self.mls_color_open = None;
|
self.mls_color_open = None;
|
||||||
self.ts_color_open = None;
|
self.ts_color_open = None;
|
||||||
self.ribbon.close_dropdown();
|
self.ribbon.close_dropdown();
|
||||||
|
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.tabs[i].properties.color_picker_open = false;
|
self.tabs[i].properties.color_picker_open = false;
|
||||||
self.tabs[i].layers.color_picker_row = None;
|
self.tabs[i].layers.color_picker_row = None;
|
||||||
// `color_pick_target.is_some()` drives the iced_aw overlay.
|
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::ColorPickerTabChanged(tab) => {
|
||||||
|
self.color_picker_tab = tab;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::ColorPickerColorChanged(color) => {
|
||||||
|
if let Some((_, current)) = self.color_pick_target.as_mut() {
|
||||||
|
*current = color;
|
||||||
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::CloseColorPicker => {
|
Message::CloseColorPicker => {
|
||||||
self.color_pick_target = None;
|
self.color_pick_target = None;
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::ColorWindowPick(color) => self.on_color_window_pick(color),
|
|
||||||
|
Message::ColorWindowPick(color) => {
|
||||||
|
// Keep only real colours in the recent list. ByLayer / ByBlock / None
|
||||||
|
// are logical CAD states rather than reusable colours.
|
||||||
|
if matches!(
|
||||||
|
&color,
|
||||||
|
acadrust::types::Color::Index(_)
|
||||||
|
| acadrust::types::Color::Rgb { .. }
|
||||||
|
) {
|
||||||
|
// No duplicates: selecting an existing colour moves it to the front.
|
||||||
|
if let Some(pos) = self.recent_colors.iter().position(|c| c == &color) {
|
||||||
|
self.recent_colors.remove(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.recent_colors.insert(0, color.clone());
|
||||||
|
self.recent_colors.truncate(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.on_color_window_pick(color)
|
||||||
|
}
|
||||||
Message::DsSetHandle { field, value } => self.on_ds_set_handle(field, value),
|
Message::DsSetHandle { field, value } => self.on_ds_set_handle(field, value),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-16
@@ -1850,24 +1850,49 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
None => composed.into(),
|
None => composed.into(),
|
||||||
};
|
};
|
||||||
// iced_aw owns the colour-picker overlay and keeps it above whichever
|
// Shared CAD colour picker. Indexed ACI colours use OpenCADStudio's own
|
||||||
// application modal requested it.
|
// dialog; True Color keeps the existing iced_aw gradient picker.
|
||||||
if let Some((_, current)) = self.color_pick_target.as_ref() {
|
if let Some((_, current)) = self.color_pick_target.as_ref() {
|
||||||
let initial = crate::ui::properties::acad_color_display(*current).0;
|
match self.color_picker_tab {
|
||||||
let modal_base =
|
super::ColorPickerTab::Index => {
|
||||||
crate::ui::modal::backdrop(base, Message::CloseColorPicker);
|
let content = crate::ui::color_select::index_color_page(
|
||||||
iced_aw::ColorPicker::new(
|
*current,
|
||||||
true,
|
&self.recent_colors,
|
||||||
initial,
|
);
|
||||||
modal_base,
|
|
||||||
Message::CloseColorPicker,
|
crate::ui::modal::modal(
|
||||||
|color| {
|
base,
|
||||||
Message::ColorWindowPick(
|
"Select Color",
|
||||||
crate::ui::color_select::iced_to_acad_color(color),
|
content,
|
||||||
|
Message::CloseColorPicker,
|
||||||
|
self.modal_offset,
|
||||||
|
crate::ui::modal::ModalOptions::NOTICE,
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
)
|
|
||||||
.into()
|
super::ColorPickerTab::TrueColor => {
|
||||||
|
let initial = crate::ui::properties::acad_color_display(*current).0;
|
||||||
|
let modal_base =
|
||||||
|
crate::ui::modal::backdrop(base, Message::CloseColorPicker);
|
||||||
|
|
||||||
|
iced_aw::ColorPicker::new(
|
||||||
|
true,
|
||||||
|
initial,
|
||||||
|
modal_base,
|
||||||
|
|
||||||
|
// Cancel inside True Color returns to the indexed page instead
|
||||||
|
// of closing the whole CAD colour picker.
|
||||||
|
Message::ColorPickerTabChanged(super::ColorPickerTab::Index),
|
||||||
|
|
||||||
|
|color| {
|
||||||
|
Message::ColorWindowPick(
|
||||||
|
crate::ui::color_select::iced_to_acad_color(color),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
base
|
base
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,3 +244,198 @@ pub fn drop_down_below<'a>(
|
|||||||
.on_dismiss(on_dismiss)
|
.on_dismiss(on_dismiss)
|
||||||
.into()
|
.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()
|
||||||
|
}
|
||||||
Reference in new issue
Block a user