diff --git a/.cargo/config.toml b/.cargo/config.toml index de96c3e4a..876a223e7 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -22,6 +22,7 @@ rustflags = [ [target.x86_64-pc-windows-gnu] rustflags = [ "-Clink-arg=-Wl,--stack,16777216", + "-Clink-arg=-Wl,--exclude-all-symbols", ] [target.x86_64-pc-windows-msvc] diff --git a/src/app/automation.rs b/src/app/automation.rs index 3b53ce982..8012361d7 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -1669,4 +1669,15 @@ mod tests { Some("MTP") ); } + + #[test] + fn test_open_color_dropdown() { + use crate::app::Message; + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + let wid = app.main_window.unwrap_or_else(iced::window::Id::unique); + let _ = app.view(wid); + let _ = app.update(Message::ToggleRibbonDropdown("PROP_COLOR".to_string())); + let _ = app.view(wid); + } } diff --git a/src/app/update/command.rs b/src/app/update/command.rs index fd58e70d5..8a3c081c4 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1999,10 +1999,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } pub(super) fn on_ribbon_color_changed(&mut self, color: AcadColor) -> Task { - let i = self.active_tab; - self.ribbon.prop_color_palette_open = false; - self.ribbon.close_dropdown(); - let handles = self.property_target_handles(i); + let i = self.active_tab; + self.ribbon.prop_color_palette_open = false; + self.ribbon.close_dropdown(); + self.note_recent_color(color); + let handles = self.property_target_handles(i); if handles.is_empty() { if self.has_property_selection(i) { return Task::none(); diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 299d060ac..1f999afd0 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -9131,27 +9131,30 @@ impl OpenCADStudio { } 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.note_recent_color(color); self.on_color_window_pick(color) } Message::DsSetHandle { field, value } => self.on_ds_set_handle(field, value), } } + pub(crate) fn note_recent_color(&mut self, color: acadrust::types::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); + self.recent_colors.truncate(12); + } + } + /// Load a named scale into the scale-manager editor buffers (name + the /// paper / drawing units); blank ratios when the scale isn't found. fn load_scale_editor(&mut self, name: &str) { diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 943119b3d..0e3c7fdae 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -2218,6 +2218,7 @@ bg={bg_ms:.1}ms n={view_count}" &history_dropdown_labels(&self.tabs[self.active_tab].history.redo_stack), self.win_size, self.tabs[self.active_tab].is_start, + &self.recent_colors, ) .unwrap_or_else(|| iced::widget::Space::new().width(0).height(0).into()); diff --git a/src/ui/ribbon/color_dropdown.rs b/src/ui/ribbon/color_dropdown.rs new file mode 100644 index 000000000..8ec7393b7 --- /dev/null +++ b/src/ui/ribbon/color_dropdown.rs @@ -0,0 +1,333 @@ +//! Object Color Dropdown panel. +//! +//! Provides a 5-section flyout ribbon color control: +//! 1. ByLayer and ByBlock logical color entries +//! 2. 45-swatch Quick Pick Grid (5 rows × 9 columns) +//! 3. Standard Index Colors (ACI 1–9) +//! 4. Recent Colors row (up to 9 swatches) +//! 5. "Select Color..." action to open the full Select Color dialog + +use super::widgets::{muted_text_style, popup_panel_style, popup_row_style}; +use crate::app::Message; +use crate::t; +use crate::ui::properties::acad_color_display; +use acadrust::types::Color as AcadColor; +use iced::widget::{button, column, container, row, text, Space}; +use iced::{Background, Border, Element, Length, Padding, Theme}; + +pub const PANEL_W: f32 = 202.0; +const SWATCH_SIZE: f32 = 18.0; +const SWATCH_GAP: f32 = 2.0; + +/// Curated 45-swatch quick pick grid measured from the AutoCAD ribbon control. +pub const QUICK_PICK_GRID: [[(u8, u8, u8); 9]; 5] = [ + [ + (0x00, 0x00, 0x00), + (0xF8, 0xD7, 0x31), + (0xCD, 0x69, 0x28), + (0x99, 0x1B, 0x1E), + (0x64, 0x21, 0x65), + (0x29, 0x31, 0x89), + (0x22, 0x34, 0x6E), + (0x13, 0x67, 0x34), + (0x72, 0x74, 0x30), + ], + [ + (0x63, 0x64, 0x66), + (0xF1, 0xEB, 0x1F), + (0xF2, 0x67, 0x22), + (0xCD, 0x20, 0x27), + (0x93, 0x27, 0x8F), + (0x45, 0x54, 0xA5), + (0x10, 0x56, 0x89), + (0x13, 0x9B, 0x48), + (0x99, 0x9B, 0x37), + ], + [ + (0x93, 0x95, 0x98), + (0xF4, 0xEE, 0x51), + (0xF8, 0x99, 0x1E), + (0xED, 0x1F, 0x24), + (0xA9, 0x53, 0xA0), + (0x5E, 0x67, 0xAF), + (0x27, 0x76, 0xBB), + (0x58, 0xBA, 0x48), + (0xC8, 0xC9, 0x2D), + ], + [ + (0xC7, 0xC8, 0xCA), + (0xF7, 0xF2, 0x81), + (0xFE, 0xCC, 0x66), + (0xF2, 0x71, 0x72), + (0xD1, 0x9C, 0xC7), + (0x98, 0x98, 0xCC), + (0x7A, 0xAF, 0xDF), + (0xAF, 0xD9, 0xAA), + (0xE5, 0xE8, 0x78), + ], + [ + (0xFF, 0xFF, 0xFF), + (0xF8, 0xF6, 0xB0), + (0xFE, 0xEA, 0xB9), + (0xF7, 0xAB, 0xAE), + (0xDD, 0xB3, 0xD4), + (0xCE, 0xCC, 0xE6), + (0xAD, 0xDD, 0xF7), + (0xD7, 0xEB, 0xD2), + (0xED, 0xEE, 0x99), + ], +]; + +/// The standard ACI 1..=9 index colors pulled directly from the ACI color table. +#[allow(dead_code)] +pub fn aci_1_9() -> [(u8, u8, u8); 9] { + std::array::from_fn(|i| { + acadrust::types::aci_table::aci_to_rgb((i + 1) as u8).unwrap_or((128, 128, 128)) + }) +} + +fn swatch_btn<'a>(color: AcadColor, cur_color: AcadColor) -> Element<'a, Message> { + let (bg, _) = acad_color_display(color); + let selected = color == cur_color; + + button( + Space::new() + .width(Length::Fixed(SWATCH_SIZE)) + .height(Length::Fixed(SWATCH_SIZE)), + ) + .on_press(Message::RibbonColorChanged(color)) + .style(move |theme: &Theme, status| { + let palette = theme.palette(); + let hovered = matches!(status, button::Status::Hovered | button::Status::Pressed); + button::Style { + background: Some(Background::Color(bg)), + border: Border { + color: if selected { + palette.primary.base.color + } else if hovered { + palette.background.base.text + } else { + palette.background.neutral.color + }, + width: if selected { + 2.0 + } else if hovered { + 1.5 + } else { + 1.0 + }, + radius: 1.0.into(), + }, + ..Default::default() + } + }) + .padding(0) + .into() +} + +fn placeholder_swatch<'a>() -> Element<'a, Message> { + container( + Space::new() + .width(Length::Fixed(SWATCH_SIZE)) + .height(Length::Fixed(SWATCH_SIZE)), + ) + .style(|theme: &Theme| { + let palette = theme.palette(); + container::Style { + background: Some(Background::Color(palette.background.weak.color.scale_alpha(0.5))), + border: Border { + color: palette.background.neutral.color.scale_alpha(0.5), + width: 1.0, + radius: 1.0.into(), + }, + ..Default::default() + } + }) + .into() +} + +fn divider<'a>() -> Element<'a, Message> { + container( + Space::new() + .width(Length::Fill) + .height(Length::Fixed(1.0)), + ) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().background.neutral.color)), + ..Default::default() + }) + .into() +} + +fn logical_row<'a>( + color: AcadColor, + cur_color: AcadColor, + label: &'static str, +) -> Element<'a, Message> { + let (bg, _) = acad_color_display(color); + let selected = color == cur_color; + + let swatch = container( + Space::new() + .width(Length::Fixed(SWATCH_SIZE)) + .height(Length::Fixed(SWATCH_SIZE)), + ) + .style(move |theme: &Theme| { + let palette = theme.palette(); + container::Style { + background: Some(Background::Color(bg)), + border: Border { + color: if selected { + palette.primary.base.color + } else { + palette.background.neutral.color + }, + width: if selected { 2.0 } else { 1.0 }, + radius: 1.0.into(), + }, + ..Default::default() + } + }); + + button( + row![ + swatch, + text(t!(label)).size(11), + ] + .spacing(8) + .align_y(iced::Center), + ) + .on_press(Message::RibbonColorChanged(color)) + .style(popup_row_style) + .padding([3, 4]) + .width(Length::Fill) + .into() +} + +fn more_colors_row<'a>(active_color: AcadColor) -> Element<'a, Message> { + button( + row![text(t!("Select Color...")).size(11)] + .align_y(iced::Center), + ) + .on_press(Message::OpenColorWindow( + crate::app::ColorPickTarget::Ribbon, + active_color, + )) + .style(popup_row_style) + .padding([4, 4]) + .width(Length::Fill) + .into() +} + +/// Builds the complete 5-section color dropdown panel element. +pub fn color_dropdown_panel<'a>( + active_color: AcadColor, + recent_colors: &[AcadColor], +) -> Element<'a, Message> { + // 1. ByLayer and ByBlock + let by_layer = logical_row(AcadColor::ByLayer, active_color, "ByLayer"); + let by_block = logical_row(AcadColor::ByBlock, active_color, "ByBlock"); + let section1 = column![by_layer, by_block].spacing(2); + + // 2. 45-swatch Quick Pick Grid (5 rows × 9 columns) + let mut grid_rows = column![].spacing(SWATCH_GAP); + for row_colors in QUICK_PICK_GRID { + let mut r = row![].spacing(SWATCH_GAP); + for (red, green, blue) in row_colors { + let color = AcadColor::Rgb { + r: red, + g: green, + b: blue, + }; + r = r.push(swatch_btn(color, active_color)); + } + grid_rows = grid_rows.push(r); + } + + // 3. Standard Index Colors (ACI 1..=9) + let index_header = text(t!("Index Color")).size(10).style(muted_text_style); + let mut standard_row = row![].spacing(SWATCH_GAP); + for idx in 1u8..=9 { + let color = AcadColor::Index(idx); + standard_row = standard_row.push(swatch_btn(color, active_color)); + } + let section3 = column![index_header, standard_row].spacing(4); + + // 4. Recent Colors row (up to 9 swatches; pad with placeholders if fewer) + let recent_header = text(t!("Recent Colors")).size(10).style(muted_text_style); + let mut recent_row = row![].spacing(SWATCH_GAP); + let mut count = 0; + for &color in recent_colors.iter().take(9) { + recent_row = recent_row.push(swatch_btn(color, active_color)); + count += 1; + } + while count < 9 { + recent_row = recent_row.push(placeholder_swatch()); + count += 1; + } + let section4 = column![recent_header, recent_row].spacing(4); + + // 5. Select Color... + let section5 = more_colors_row(active_color); + + let content = column![ + section1, + divider(), + grid_rows, + divider(), + section3, + divider(), + section4, + divider(), + section5, + ] + .spacing(6); + + container(content) + .padding(Padding { + top: 6.0, + bottom: 6.0, + left: 12.0, + right: 12.0, + }) + .width(Length::Fixed(PANEL_W)) + .style(popup_panel_style) + .into() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_quick_pick_grid_dimensions() { + assert_eq!(QUICK_PICK_GRID.len(), 5); + for row in &QUICK_PICK_GRID { + assert_eq!(row.len(), 9); + } + } + + #[test] + fn test_aci_1_9() { + let colors = aci_1_9(); + assert_eq!(colors.len(), 9); + assert_eq!(colors[0], (255, 0, 0)); // Red + assert_eq!(colors[1], (255, 255, 0)); // Yellow + assert_eq!(colors[2], (0, 255, 0)); // Green + assert_eq!(colors[3], (0, 255, 255)); // Cyan + assert_eq!(colors[4], (0, 0, 255)); // Blue + assert_eq!(colors[5], (255, 0, 255)); // Magenta + assert_eq!(colors[6], (255, 255, 255)); // White + assert_eq!(colors[7], (128, 128, 128)); // Dark Gray + assert_eq!(colors[8], (192, 192, 192)); // Light Gray + } + + #[test] + fn test_panel_geometry() { + assert_eq!(PANEL_W, 202.0); + let grid_width = 9.0 * SWATCH_SIZE + 8.0 * SWATCH_GAP; + assert_eq!(grid_width, 178.0); + let padding_h = (PANEL_W - grid_width) / 2.0; + assert_eq!(padding_h, 12.0); + } +} diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index a5eb2ffe3..7a71921f0 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -23,6 +23,7 @@ use crate::ui::properties::{linetype_display_name, lw_options, LinetypeItem}; mod widgets; mod draw_panel; mod modify_panel; +mod color_dropdown; use widgets::{StyleContext, *}; mod collapse; use collapse::{CollapsePanels, Panel}; @@ -739,6 +740,7 @@ impl Ribbon { redo_labels: &[String], win: (f32, f32), is_start: bool, + recent_colors: &[AcadColor], ) -> Option> { if is_start { return None; @@ -820,7 +822,7 @@ impl Ribbon { } if open_id == PROP_COLOR_ID { - return self.prop_color_overlay(win); + return self.prop_color_overlay(win, recent_colors); } if open_id == PROP_LINETYPE_ID { return self.prop_linetype_overlay(win); @@ -1169,27 +1171,17 @@ impl Ribbon { ))) } - fn prop_color_overlay(&self, win: (f32, f32)) -> Option> { - let picker = crate::ui::color_select::color_list( - crate::ui::color_select::ColorExtras { - by_layer: true, - by_block: true, - ..Default::default() - }, - Message::RibbonColorChanged, - Message::OpenColorWindow( - crate::app::ColorPickTarget::Ribbon, - self.active_color, - ), - ); + fn prop_color_overlay( + &self, + win: (f32, f32), + recent_colors: &[AcadColor], + ) -> Option> { + let panel = color_dropdown::color_dropdown_panel(self.active_color, recent_colors); - let panel = container(picker) - .style(popup_panel_style) - .width(Length::Fixed(200.0)); - - let (align_right, h_pad, top) = self.dd_anchor(PROP_COLOR_ID, 200.0, win.0); + let (align_right, h_pad, top) = + self.dd_anchor(PROP_COLOR_ID, color_dropdown::PANEL_W, win.0); Some(dropdown_backdrop(position_ribbon_dropdown( - panel.into(), + panel, align_right, h_pad, top,