From 93b441d4b802a2255ea900abd5825092694f6583 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 2 Sep 2026 21:28:15 +0300 Subject: [PATCH] Unify mixed color selection Use the shared color selector for mixed property values and remove the legacy inline palette and its state. --- src/app/mod.rs | 2 - src/app/properties.rs | 3 - src/app/update/mod.rs | 11 -- src/app/update/viewport.rs | 1 - src/ui/color_select.rs | 63 ++++++++- src/ui/properties.rs | 280 ++----------------------------------- 6 files changed, 76 insertions(+), 284 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index bdf4d6c62..8262c8bd0 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2523,8 +2523,6 @@ pub enum Message { /// properties-panel dropdown (a combo_box) opens, so at most one panel /// dropdown is open at a time and they can't overlap. (#235) PropColorPickerClose, - /// Toggle the full ACI colour palette expansion. - PropColorPaletteToggle, /// Enter the model-space editing mode inside the given viewport (MSPACE). EnterViewport(acadrust::Handle), /// Exit MSPACE and return to paper-space editing (PSPACE). diff --git a/src/app/properties.rs b/src/app/properties.rs index 8c95cfa38..2afb21660 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -42,7 +42,6 @@ impl OpenCADStudio { // Note: the color-picker dropdown is intentionally NOT carried over — a // rebuild means the selection (or a property) changed, so the dropdown // closes, matching the deselect / reselect / click-away expectation. - let color_palette_open = self.tabs[i].properties.color_palette_open; let edit_buf = std::mem::take(&mut self.tabs[i].properties.edit_buf); let active_field = std::mem::take(&mut self.tabs[i].properties.active_field); // Expanded coordinate groups persist across rebuilds AND selection @@ -2224,7 +2223,6 @@ impl OpenCADStudio { // Precompute the focused-id → field-key map for O(1) lookups on // `PropSyncActive`; derived from `sections`, so rebuild it here. panel.field_key_by_id = crate::ui::properties::build_field_key_map(&panel.sections); - panel.color_palette_open = color_palette_open; let new_handles: Vec = selected.iter().map(|(h, _)| *h).collect(); // Carry the in-progress edits only when the selection is unchanged // (a commit-triggered rebuild); a genuine selection change starts @@ -2265,7 +2263,6 @@ impl OpenCADStudio { panel.edit_buf.clear(); panel.active_field = None; panel.color_picker_open = false; - panel.color_palette_open = false; panel.bg_color_picker_open = false; panel.open_color_field = None; panel.hatch_pattern_picker_open = false; diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index c41f99477..e2d7e0c3c 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -4653,7 +4653,6 @@ impl OpenCADStudio { let i = self.active_tab; let handles = self.property_target_handles(i); if handles.is_empty() { - self.tabs[i].properties.color_palette_open = false; let task = self.on_ribbon_color_changed(color); self.refresh_properties(); return task; @@ -4666,7 +4665,6 @@ impl OpenCADStudio { } }); self.tabs[i].properties.color_picker_open = false; - self.tabs[i].properties.color_palette_open = false; Task::none() } @@ -4751,7 +4749,6 @@ impl OpenCADStudio { panel.hatch_pattern_picker_open = !panel.hatch_pattern_picker_open; if panel.hatch_pattern_picker_open { panel.color_picker_open = false; - panel.color_palette_open = false; panel.open_color_field = None; panel.edit_choice_open = false; panel.hatch_pattern_focus = @@ -5069,7 +5066,6 @@ impl OpenCADStudio { self.tabs[i].properties.color_picker_open = !self.tabs[i].properties.color_picker_open; if self.tabs[i].properties.color_picker_open { - self.tabs[i].properties.color_palette_open = false; self.tabs[i].properties.hatch_pattern_picker_open = false; self.tabs[i].properties.hatch_pattern_search.clear(); } @@ -5311,18 +5307,11 @@ impl OpenCADStudio { Message::PropColorPickerClose => { let i = self.active_tab; self.tabs[i].properties.color_picker_open = false; - self.tabs[i].properties.color_palette_open = false; self.tabs[i].properties.hatch_pattern_picker_open = false; self.tabs[i].properties.hatch_pattern_search.clear(); Task::none() } - Message::PropColorPaletteToggle => { - self.tabs[self.active_tab].properties.color_palette_open = - !self.tabs[self.active_tab].properties.color_palette_open; - Task::none() - } - Message::LayoutSwitch(name) => { self.layout_list_open = false; self.on_layout_switch(name) diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 84206993f..6c152735d 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -2657,7 +2657,6 @@ impl OpenCADStudio { // press starts a box selection, so without this they'd only // close on the second click (issue #104). self.tabs[i].properties.color_picker_open = false; - self.tabs[i].properties.color_palette_open = false; // Click anywhere outside the popup dismisses it. The // menu's own buttons live above this mouse_area, so a // press that reaches here means the cursor is not on diff --git a/src/ui/color_select.rs b/src/ui/color_select.rs index d7f49715f..3181e9ac1 100644 --- a/src/ui/color_select.rs +++ b/src/ui/color_select.rs @@ -149,13 +149,72 @@ pub fn color_selector_with_name<'a>( let cur_name = display_name .map(str::to_string) .unwrap_or_else(|| color_display_name(current)); + + color_selector_with_indicator( + swatch(cur_bg), + cur_name, + open, + extras, + on_select, + on_toggle, + on_more, + ) +} + +/// Build the shared colour selector for a mixed selection. +pub fn color_selector_varies<'a>( + open: bool, + extras: ColorExtras, + on_select: impl Fn(AcadColor) -> Message + 'a, + on_toggle: Message, + on_more: Message, +) -> Element<'a, Message> { + let indicator = container(text("?").size(10)) + .style(|theme: &Theme| { + let palette = theme.palette(); + container::Style { + background: Some(Background::Color(palette.background.strong.color)), + border: Border { + color: palette.background.neutral.color, + width: 1.0, + radius: 2.0.into(), + }, + text_color: Some(palette.background.strong.text), + ..Default::default() + } + }) + .width(13) + .height(13) + .align_x(iced::Center) + .align_y(iced::Center); + + color_selector_with_indicator( + indicator.into(), + "*VARIES*".to_string(), + open, + extras, + on_select, + on_toggle, + on_more, + ) +} + +fn color_selector_with_indicator<'a>( + indicator: Element<'a, Message>, + name: String, + open: bool, + extras: ColorExtras, + on_select: impl Fn(AcadColor) -> Message + 'a, + on_toggle: Message, + on_more: Message, +) -> Element<'a, Message> { let on_dismiss = on_toggle.clone(); // Closed button: current swatch + name + caret. let head = button( row![ - swatch(cur_bg), - text(cur_name).size(11), + indicator, + text(name).size(11), iced::widget::Space::new().width(Length::Fill), crate::ui::icons::themed_arrow_toggle(open, 9.0), ] diff --git a/src/ui/properties.rs b/src/ui/properties.rs index 5c54d877d..fb0660c52 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -25,7 +25,6 @@ use crate::t; // ── Row-height-derived constants ───────────────────────────────────────── const FONT_SZ: f32 = ROW_H * 0.42; // ≈11 px const COMBO_PAD_V: f32 = (ROW_H - FONT_SZ * 1.3 - 2.0) / 2.0; // fills combo to ROW_H -const SWATCH_SZ: f32 = ROW_H * 0.54; // ≈14 px color swatch const PATTERN_CARD_W: f32 = 158.0; const PATTERN_PREVIEW_H: f32 = 58.0; const PATTERN_PICKER_W: f32 = 348.0; @@ -430,8 +429,6 @@ pub struct PropertiesPanel { pub source_handles: Vec, /// Whether the quick color picker dropdown is open. pub color_picker_open: bool, - /// Whether the full 16×16 ACI palette is expanded inside the color picker. - pub color_palette_open: bool, /// Whether the MTEXT background-colour picker dropdown is open. Separate /// from `color_picker_open` so the entity colour and the background colour /// pickers are independent. @@ -483,7 +480,6 @@ impl Default for PropertiesPanel { edit_buf: HashMap::default(), source_handles: vec![], color_picker_open: false, - color_palette_open: false, bg_color_picker_open: false, open_color_field: None, prop_vertex: 0, @@ -955,59 +951,21 @@ impl PropertiesPanel { } fn render_color_varies_row<'a>(&'a self, label: &'a str) -> Element<'a, Message> { - let color_btn = button( - row![ - container(text("?").size(10)) - .style(move |theme: &Theme| { - let palette = theme.palette(); - container::Style { - background: Some(Background::Color(palette.background.strong.color)), - border: Border { - color: palette.background.neutral.color, - width: 1.0, - radius: 2.0.into() - }, - text_color: Some(palette.background.strong.text), - ..Default::default() - } - }) - .width(SWATCH_SZ) - .height(SWATCH_SZ) - .align_x(iced::Center) - .align_y(iced::Center), - text(VARIES_LABEL).size(FONT_SZ), - ] - .spacing(4) - .align_y(iced::Center), - ) - .on_press(Message::PropColorPickerToggle) - .style(combo_btn_style) - .padding(Padding { - top: COMBO_PAD_V, - bottom: COMBO_PAD_V, - left: 6.0, - right: 6.0, - }) - .width(Length::Fill); - - let color_row = prop_row_widget(label, color_btn.into()); - if self.color_picker_open { - column![color_row, self.render_color_picker()] - .spacing(0) - .into() - } else { - color_row - } - } - - fn render_color_picker(&self) -> Element<'_, Message> { - color_picker_dropdown( - self.color_palette_open, - Message::PropColorPaletteToggle, - Some(Message::PropColorChanged(AcadColor::ByLayer)), - Some(Message::PropColorChanged(AcadColor::ByBlock)), - |aci| Message::PropColorChanged(AcadColor::Index(aci)), - ) + let selector = crate::ui::color_select::color_selector_varies( + self.color_picker_open, + crate::ui::color_select::ColorExtras { + by_layer: true, + by_block: true, + ..Default::default() + }, + Message::PropColorChanged, + Message::PropColorPickerToggle, + Message::OpenColorWindow( + crate::app::ColorPickTarget::Properties, + AcadColor::ByLayer, + ), + ); + prop_row_widget(label, selector) } // ── Lineweight row (combo_box) ──────────────────────────────────────── @@ -1527,187 +1485,6 @@ impl PropertiesPanel { } } -// ── Shared color picker widget ──────────────────────────────────────────── - -/// Builds the color picker dropdown content (standard swatches + optional -/// ByLayer/ByBlock + "More Colors…" expanding to full ACI palette). -/// Use this from both the Properties panel and the Layer Manager. -pub fn color_picker_dropdown<'a>( - palette_open: bool, - palette_toggle_msg: Message, - by_layer_msg: Option, - by_block_msg: Option, - on_aci: impl Fn(u8) -> Message + 'a, -) -> Element<'a, Message> { - // ByLayer / ByBlock row (optional) - let extras: Option> = match (by_layer_msg, by_block_msg) { - (Some(bl), Some(bb)) => Some( - row![ - picker_text_btn("ByLayer", bl), - picker_text_btn("ByBlock", bb) - ] - .spacing(4) - .into(), - ), - (Some(bl), None) => Some(picker_text_btn("ByLayer", bl)), - (None, Some(bb)) => Some(picker_text_btn("ByBlock", bb)), - (None, None) => None, - }; - - // 9 standard ACI swatches (1-9) - let standard: Element<'a, Message> = (1u8..=9u8) - .fold(row![].spacing(2).width(Length::Fill), |r, idx| { - let c = AcadColor::Index(idx); - let (bg, _) = acad_color_display(c); - let msg = on_aci(idx); - r.push( - button(text("").width(18).height(18)) - .width(Length::FillPortion(1)) - .on_press(msg) - .style(move |theme: &Theme, status| button::Style { - background: Some(Background::Color(bg)), - border: Border { - color: if matches!(status, button::Status::Hovered) { - theme.palette().primary.base.color - } else { - theme.palette().background.neutral.color - }, - width: if matches!(status, button::Status::Hovered) { - 1.5 - } else { - 1.0 - }, - radius: 2.0.into(), - }, - text_color: theme.palette().background.base.text, - ..Default::default() - }) - .padding(0), - ) - }) - .into(); - - // "More Colors…" toggle button - let more_btn = button( - row![ - if palette_open { - crate::ui::icons::themed_arrow_up(9.0) - } else { - crate::ui::icons::themed_arrow_down(9.0) - }, - text(if palette_open { - t!("Less").into_owned() - } else { - t!("More Colors…").into_owned() - }) - .size(10) - .style(hint_text_style), - ] - .spacing(4) - .align_y(iced::Center), - ) - .on_press(palette_toggle_msg) - .style(button::subtle) - .padding([2, 6]) - .width(Length::Fill); - - let inner = if let Some(e) = extras { - column![e, standard, more_btn].spacing(4) - } else { - column![standard, more_btn].spacing(4) - }; - - let mut col = column![container(inner) - .style(|theme: &Theme| { - let palette = theme.palette(); - container::Style { - background: Some(Background::Color(palette.background.base.color)), - border: Border { - color: palette.background.neutral.color, - width: 1.0, - radius: 0.0.into() - }, - ..Default::default() - } - }) - .padding([6, 8]) - .width(Length::Fill)] - .spacing(0); - - // Full ACI palette (expanded) - if palette_open { - const COLS: u16 = 16; - let mut rows = column![].spacing(1).width(Length::Fill); - let mut idx: u16 = 1; - while idx <= 255 { - let mut r = row![].spacing(1).width(Length::Fill); - for _ in 0..COLS { - if idx <= 255 { - let ci = idx as u8; - let (bg, _) = acad_color_display(AcadColor::Index(ci)); - let msg = on_aci(ci); - r = r.push( - button(text("").width(12).height(12)) - .width(Length::FillPortion(1)) - .on_press(msg) - .style(move |theme: &Theme, status| button::Style { - background: Some(Background::Color(bg)), - border: Border { - color: if matches!(status, button::Status::Hovered) { - theme.palette().primary.base.color - } else { - theme.palette().background.neutral.color - }, - width: if matches!(status, button::Status::Hovered) { - 1.5 - } else { - 1.0 - }, - radius: 1.0.into(), - }, - text_color: theme.palette().background.base.text, - ..Default::default() - }) - .padding(0), - ); - idx += 1; - } else { - r = r.push( - Space::new() - .width(Length::FillPortion(1)) - .height(12), - ); - } - } - rows = rows.push(r); - } - col = col.push( - container( - scrollable(rows) - .height(160) - .width(Length::Fill) - .spacing(2), - ) - .style(|theme: &Theme| { - let palette = theme.palette(); - container::Style { - background: Some(Background::Color(palette.background.base.color)), - border: Border { - color: palette.background.neutral.color, - width: 1.0, - radius: 0.0.into(), - }, - ..Default::default() - } - }) - .padding([4, 6]) - .width(Length::Fill), - ); - } - - col.into() -} - // ── Standalone helpers ──────────────────────────────────────────────────── /// A boolean toggle button row (for "Invisible" etc.). @@ -2066,15 +1843,6 @@ fn prop_row_with_active<'a>( .into() } -/// A plain text button used inside the color picker for ByLayer / ByBlock. -fn picker_text_btn(label: &str, msg: Message) -> Element<'_, Message> { - button(text(t!(label)).size(FONT_SZ)) - .on_press(msg) - .style(button::secondary) - .padding([2, 8]) - .into() -} - // ── Color display helper ────────────────────────────────────────────────── /// Returns an (iced::Color swatch_bg, display_label) pair for an AcadColor. @@ -2130,24 +1898,6 @@ fn aci_label(idx: u8) -> &'static str { // ── Widget style helpers ────────────────────────────────────────────────── -fn combo_btn_style(theme: &Theme, status: button::Status) -> button::Style { - let palette = theme.palette(); - let pair = match status { - button::Status::Hovered | button::Status::Pressed => palette.background.weak, - _ => palette.background.base, - }; - button::Style { - background: Some(Background::Color(pair.color)), - border: Border { - color: palette.background.neutral.color, - width: 1.0, - radius: 2.0.into(), - }, - text_color: pair.text, - ..Default::default() - } -} - fn text_input_style(theme: &Theme, status: text_input::Status) -> text_input::Style { let palette = theme.palette(); let border_color = match status {