From c9c3d3b1bff73ab9f65f12d0e94fce0d5ec99f5b Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 30 Jul 2026 15:19:58 +0300 Subject: [PATCH] refactor(ui): adopt iced color picker Remove local Pin/PickList wrappers and the ACI grid.\nBlock viewport input behind the picker and load iced_aw glyphs. --- Cargo.toml | 2 +- src/app/mod.rs | 14 ++-- src/app/update/mod.rs | 10 ++- src/app/update/style.rs | 14 ++-- src/app/update/viewport.rs | 6 +- src/app/view/controls.rs | 7 +- src/app/view/mod.rs | 78 ++++++++++--------- src/app/view/modal.rs | 9 ++- src/app/view/overlay.rs | 55 +++++++++----- src/app/view/viewcube.rs | 7 +- src/ui/color_select.rs | 110 ++++++++------------------- src/ui/mod.rs | 28 ------- src/ui/modal.rs | 37 +++++++-- src/ui/popup/cycle_popup.rs | 3 +- src/ui/properties.rs | 10 ++- src/ui/ribbon/mod.rs | 5 +- src/ui/style/dimstyle.rs | 13 ++-- src/ui/style/mleaderstyle.rs | 22 +++--- src/ui/style/tablestyle.rs | 30 +++++--- src/ui/window/attribute_editor.rs | 8 +- src/ui/window/layer_state_manager.rs | 35 ++++++--- src/ui/window/layers.rs | 5 +- src/ui/window/options.rs | 14 ++-- src/ui/window/plot.rs | 17 +++-- src/ui/window/plugin_manager.rs | 5 +- 25 files changed, 289 insertions(+), 255 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7fb1059c..07d72770 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ iced = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00 iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } # iced_aw 0.15 compatibility is pending upstream in PR #432. Pin the exact # reviewed pair of commits so Iced and its additional widgets cannot drift. -iced_aw = { git = "https://github.com/tsuza/iced_aw.git", rev = "a9301708a4d008dc17f63cbbbb2b2aa06e0e7352", features = ["menu", "context_menu"] } +iced_aw = { git = "https://github.com/tsuza/iced_aw.git", rev = "a9301708a4d008dc17f63cbbbb2b2aa06e0e7352", features = ["menu", "context_menu", "color_picker"] } bytemuck = { version = "1.25", features = ["derive"] } glam = { version = "0.33", features = ["bytemuck"] } truck-modeling = "0.6" diff --git a/src/app/mod.rs b/src/app/mod.rs index f8015a4c..d28355a4 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -465,8 +465,8 @@ pub(super) struct OpenCADStudio { /// OS window Id of the primary application window. main_window: Option, // ── Floating panel windows ──────────────────────────────────────────── - /// Standalone "Select Color" palette window + the field it targets. - color_pick_target: Option, + /// Active `iced_aw` colour picker: destination plus its initial colour. + color_pick_target: Option<(ColorPickTarget, AcadColor)>, /// The open in-canvas modal dialog, if any (Plan B: shared overlay instead /// of OS windows). active_modal: Option, @@ -2503,11 +2503,11 @@ pub enum Message { DsToggle(DsField), /// Toggle the expanded colour palette for a DimStyle colour field. DsColorMore(DsField), - /// Open the standalone palette window targeting the given field. - OpenColorWindow(ColorPickTarget), - /// Close the nested colour-picker modal without choosing (Plan B). + /// Open the `iced_aw` colour picker for a field and its current colour. + OpenColorWindow(ColorPickTarget, AcadColor), + /// Close the colour picker without choosing. CloseColorPicker, - /// A colour was chosen in the standalone palette window. + /// A colour was chosen in the `iced_aw` picker. ColorWindowPick(acadrust::types::Color), /// Set a block/linetype Handle field on the selected dim style from a /// dropdown of available block-records / linetypes (by name). @@ -3177,6 +3177,7 @@ pub fn run() -> iced::Result { } }) .theme(|state: &OpenCADStudio, _| state.active_theme.clone()) + .font(iced_aw::ICED_AW_FONT_BYTES) .run() } @@ -3206,5 +3207,6 @@ pub fn run_web() -> iced::Result { .subscription(OpenCADStudio::subscription) .title(|_state: &OpenCADStudio| "Open CAD Studio".to_string()) .theme(|state: &OpenCADStudio| state.active_theme.clone()) + .font(iced_aw::ICED_AW_FONT_BYTES) .run() } diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 546d8d3a..9a4e0006 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -2120,6 +2120,9 @@ impl OpenCADStudio { Message::PaneClicked(pane) => self.on_pane_clicked(pane), Message::PaneDragged(ev) => self.on_pane_dragged(ev), Message::PaneMove(idx, local) => { + if self.color_pick_target.is_some() { + return Task::none(); + } let p = self.pane_canvas_point(idx, local); // While dragging a pane, just track the cursor (no focus swap or // snap) so the drop target reads cleanly. @@ -5744,8 +5747,8 @@ impl OpenCADStudio { }; Task::none() } - Message::OpenColorWindow(target) => { - self.color_pick_target = Some(target); + Message::OpenColorWindow(target, color) => { + self.color_pick_target = Some((target, color)); self.ds_color_open = None; self.mls_color_open = None; self.ts_color_open = None; @@ -5753,8 +5756,7 @@ impl OpenCADStudio { let i = self.active_tab; self.tabs[i].properties.color_picker_open = false; self.tabs[i].layers.color_picker_row = None; - // Shown as a nested modal over the active dialog (Plan B): - // `color_pick_target.is_some()` drives the overlay in view_main. + // `color_pick_target.is_some()` drives the iced_aw overlay. Task::none() } Message::CloseColorPicker => { diff --git a/src/app/update/style.rs b/src/app/update/style.rs index 057b272b..46e9e0d1 100644 --- a/src/app/update/style.rs +++ b/src/app/update/style.rs @@ -960,7 +960,7 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task { pub(super) fn on_color_window_pick(&mut self, color: acadrust::types::Color) -> Task { let s = crate::ui::color_select::color_to_aci_string(color); - let edit = match self.color_pick_target.take() { + let edit = match self.color_pick_target.take().map(|(target, _)| target) { Some(crate::app::ColorPickTarget::DimStyle(f)) => Some(Message::DsEdit(f, s)), Some(crate::app::ColorPickTarget::MLeader(f)) => { Some(Message::MLeaderStyleEdit { field: f, value: s }) @@ -986,10 +986,14 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task { } Some(crate::app::ColorPickTarget::Layer(idx)) => { self.tabs[self.active_tab].layers.selected = Some(idx); - match color { - acadrust::types::Color::Index(i) => Some(Message::LayerColorSet(i)), - _ => None, - } + let index = match color { + acadrust::types::Color::Index(i) => i, + acadrust::types::Color::Rgb { r, g, b } => { + crate::ui::color_select::nearest_aci(r, g, b) + } + _ => 7, + }; + Some(Message::LayerColorSet(index)) } Some(crate::app::ColorPickTarget::LayerState(idx)) => { Some(Message::LayerStateEditorLayerColor(idx, color)) diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 9d7b2abf..232aecdc 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -435,6 +435,10 @@ impl OpenCADStudio { } pub(super) fn on_cursor_moved(&mut self, p: Point) -> Task { + if self.color_pick_target.is_some() { + return Task::none(); + } + // `p` is relative to the ViewCube hit area's top-left. Map // it back to full-canvas coordinates so ViewportClick's // hit-test lines up. The hit area sits in the top-right of @@ -498,7 +502,7 @@ impl OpenCADStudio { // leaks through the stack to the pane mouse_area beneath and // would track the crosshair over the dropdown's empty areas. // Drop the move here instead. (#227) - if self.ribbon.open_dropdown.is_some() { + if self.ribbon.open_dropdown.is_some() || self.color_pick_target.is_some() { return Task::none(); } let i = self.active_tab; diff --git a/src/app/view/controls.rs b/src/app/view/controls.rs index 572e2c24..2fbae28f 100644 --- a/src/app/view/controls.rs +++ b/src/app/view/controls.rs @@ -79,11 +79,12 @@ pub(super) fn viewport_controls<'a>( }; // Render-mode picker, restyled borderless so the outer chip frames it. - let picker = crate::ui::pick_list( - render_modes, + let picker = iced::widget::pick_list( Some(RenderModeChoice(render_mode)), - |c| Message::SetRenderMode(c.0), + render_modes, + |value| value.to_string(), ) + .on_select(|c| Message::SetRenderMode(c.0)) .text_size(11) .padding([4, 6]) .style(move |theme: &Theme, _| { diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 42f71419..8051e6f8 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -788,10 +788,10 @@ impl OpenCADStudio { .into(); // Pin the bar to the active model tile's top-left corner so it // follows the active panel in a tiled layout. - let bar_layer = crate::ui::pin_at( - iced::Point::new(rect.x, rect.y), + let bar_layer = iced::widget::pin( container(adaptive).width(iced::Length::Fixed(rect.width.max(1.0))), - ); + ) + .position(iced::Point::new(rect.x.max(0.0), rect.y.max(0.0))); viewport_stack = viewport_stack.push(bar_layer); } @@ -840,7 +840,8 @@ impl OpenCADStudio { }, ..Default::default() }); - let border_layer = crate::ui::pin_at(iced::Point::new(x, y), border_frame); + let border_layer = + iced::widget::pin(border_frame).position(iced::Point::new(x, y)); viewport_stack = viewport_stack.push(border_layer); let vp_mode = tab @@ -866,10 +867,10 @@ impl OpenCADStudio { ]) .report_width0(self.render_bar_w.clone()) .into(); - let picker_layer = crate::ui::pin_at( - iced::Point::new(x + 4.0, y + 4.0), + let picker_layer = iced::widget::pin( container(adaptive).width(iced::Length::Fixed(rect.width.max(1.0))), - ); + ) + .position(iced::Point::new(x + 4.0, y + 4.0)); viewport_stack = viewport_stack.push(picker_layer); // Hide the ViewCube first — before the render bar — when they collide. @@ -877,10 +878,8 @@ impl OpenCADStudio { let cube_x = (rect.x + rect.width - VIEWCUBE_HIT_SIZE - VIEWCUBE_PAD).max(0.0); let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0); - let controls = crate::ui::pin_at( - iced::Point::new(cube_x, cube_y), - viewcube_nav_controls(), - ); + let controls = iced::widget::pin(viewcube_nav_controls()) + .position(iced::Point::new(cube_x, cube_y)); viewport_stack = viewport_stack.push(controls); let ucs_current = tab @@ -896,13 +895,14 @@ impl OpenCADStudio { .map(|u| u.name.clone()) .filter(|n| !n.is_empty()) .collect(); - let picker = crate::ui::pin_at( - iced::Point::new( + let picker = iced::widget::pin(iced::widget::opaque(viewcube_ucs_picker( + ucs_current, + ucs_names, + ))) + .position(iced::Point::new( cube_x + VIEWCUBE_HIT_SIZE * 0.5 - UCS_PICKER_W * 0.5, cube_y + VIEWCUBE_HIT_SIZE + 6.0, - ), - iced::widget::opaque(viewcube_ucs_picker(ucs_current, ucs_names)), - ); + )); viewport_stack = viewport_stack.push(picker); } } @@ -917,10 +917,8 @@ impl OpenCADStudio { let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0); // Cube hit area + nav controls (home / roll / nudge) as one layer. - let controls = crate::ui::pin_at( - iced::Point::new(cube_x, cube_y), - viewcube_nav_controls(), - ); + let controls = iced::widget::pin(viewcube_nav_controls()) + .position(iced::Point::new(cube_x, cube_y)); viewport_stack = viewport_stack.push(controls); // WCS / named-UCS selector under the cube. @@ -937,13 +935,14 @@ impl OpenCADStudio { .map(|u| u.name.clone()) .filter(|n| !n.is_empty()) .collect(); - let picker = crate::ui::pin_at( - iced::Point::new( + let picker = iced::widget::pin(iced::widget::opaque(viewcube_ucs_picker( + ucs_current, + ucs_names, + ))) + .position(iced::Point::new( cube_x + VIEWCUBE_HIT_SIZE * 0.5 - UCS_PICKER_W * 0.5, cube_y + VIEWCUBE_HIT_SIZE + 6.0, - ), - iced::widget::opaque(viewcube_ucs_picker(ucs_current, ucs_names)), - ); + )); viewport_stack = viewport_stack.push(picker); } @@ -1554,21 +1553,24 @@ impl OpenCADStudio { } None => composed.into(), }; - // The colour picker is a nested modal: it stacks over whichever dialog - // (style editor, properties, …) requested it. - if self.color_pick_target.is_some() { - crate::ui::modal::modal( - base, - "Select Color", - iced::widget::container(crate::ui::color_select::color_grid_window( - Message::ColorWindowPick, - )) - .width(iced::Length::Fit.max(420.0)) - .height(iced::Length::Fit.max(470.0)), + // iced_aw owns the colour-picker overlay and keeps it above whichever + // application modal requested it. + if let Some((_, current)) = self.color_pick_target.as_ref() { + 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, Message::CloseColorPicker, - iced::Vector::ZERO, - crate::ui::modal::ModalOptions::MOVABLE_FIXED, + |color| { + Message::ColorWindowPick( + crate::ui::color_select::iced_to_acad_color(color), + ) + }, ) + .into() } else { base } diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index bb84ee9e..c5213cd2 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -982,9 +982,12 @@ fn save_as_dialog_window<'a>(filename: &'a str, format: &'a str) -> Element<'a, items.push( row![ label("Format:").width(70), - crate::ui::pick_list(crate::io::SAVE_FORMAT_OPTIONS, sel_fmt, |s: &str| { - Message::SaveDialogFormatChanged(s.to_string()) - }) + iced::widget::pick_list( + sel_fmt, + crate::io::SAVE_FORMAT_OPTIONS, + |value| value.to_string(), + ) + .on_select(|s: &str| Message::SaveDialogFormatChanged(s.to_string())) .width(Fit), ] .align_y(iced::Alignment::Center) diff --git a/src/app/view/overlay.rs b/src/app/view/overlay.rs index 2f1b977f..252ec23b 100644 --- a/src/app/view/overlay.rs +++ b/src/app/view/overlay.rs @@ -9,7 +9,9 @@ pub(super) fn position_canvas_overlay<'a>( anchor: iced::Point, panel: Element<'a, Message>, ) -> Element<'a, Message> { - crate::ui::pin_at(anchor, iced::widget::opaque(panel)) + iced::widget::pin(iced::widget::opaque(panel)) + .position(iced::Point::new(anchor.x.max(0.0), anchor.y.max(0.0))) + .into() } // ── In-place MText editor overlay ─────────────────────────────────────────── @@ -369,7 +371,12 @@ pub(super) fn mtext_editor_overlay<'a>( } else { styles }; - let style_pl = crate::ui::pick_list(style_opts, Some(ed.style.clone()), Message::MTextStyle) + let style_pl = iced::widget::pick_list( + Some(ed.style.clone()), + style_opts, + |value| value.to_string(), + ) + .on_select(Message::MTextStyle) .text_size(11) .width(iced::Length::Fixed(96.0)); let font_sel = if ed.font.trim().is_empty() { @@ -377,14 +384,15 @@ pub(super) fn mtext_editor_overlay<'a>( } else { ed.font.clone() }; - let font_pl = crate::ui::pick_list( + let font_pl = iced::widget::pick_list( + Some(font_sel), MTEXT_FONTS .iter() .map(|s| s.to_string()) .collect::>(), - Some(font_sel), - Message::MTextFont, + |value| value.to_string(), ) + .on_select(Message::MTextFont) .text_size(11) .width(iced::Length::Fixed(120.0)); // Same colour picker as the Properties panel (named swatches + "More…" full @@ -398,7 +406,10 @@ pub(super) fn mtext_editor_overlay<'a>( }, Message::MTextColorChanged, Message::MTextColorPickerToggle, - Message::OpenColorWindow(crate::app::ColorPickTarget::MText), + Message::OpenColorWindow( + crate::app::ColorPickTarget::MText, + acadrust::types::Color::from_index(ed.color_aci as i16), + ), )) .width(iced::Length::Fixed(150.0)); @@ -443,11 +454,12 @@ pub(super) fn mtext_editor_overlay<'a>( .width(width); // ── Row 2: oblique / width / char-spacing · align · line spacing · OK ─ - let justify = crate::ui::pick_list( - JustifyChoice::ALL, + let justify = iced::widget::pick_list( Some(JustifyChoice(ed.attachment)), - |c| Message::MTextJustify(c.0), + JustifyChoice::ALL, + |value| value.to_string(), ) + .on_select(|c| Message::MTextJustify(c.0)) .text_size(11) .width(iced::Length::Fixed(112.0)); let row2 = row![ @@ -1008,7 +1020,12 @@ pub(super) fn qselect_overlay<'a>( Space::new().height(10), row![ label("Object type:"), - crate::ui::pick_list(type_options, Some(type_sel), |s: String| { + iced::widget::pick_list( + Some(type_sel), + type_options, + |value| value.to_string(), + ) + .on_select(|s: String| { if s == QSELECT_ANY_TYPE { Message::QSelectSetType(None) } else { @@ -1022,17 +1039,18 @@ pub(super) fn qselect_overlay<'a>( Space::new().height(6), row![ label("Property:"), - crate::ui::pick_list( - prop_options, + iced::widget::pick_list( Some(prop_sel), - |p: crate::app::QSelectPropertyChoice| { + prop_options, + |value| value.to_string(), + ) + .on_select(|p: crate::app::QSelectPropertyChoice| { if p.field.is_empty() { Message::QSelectSetProperty(None) } else { Message::QSelectSetProperty(Some(p)) } - } - ) + }) .width(Fill), ] .align_y(iced::Alignment::Center) @@ -1040,11 +1058,12 @@ pub(super) fn qselect_overlay<'a>( Space::new().height(6), row![ label("Operator:"), - crate::ui::pick_list( - op_options, + iced::widget::pick_list( Some(state.operator), - Message::QSelectSetOperator + op_options, + |value| value.to_string(), ) + .on_select(Message::QSelectSetOperator) .width(Fill), ] .align_y(iced::Alignment::Center) diff --git a/src/app/view/viewcube.rs b/src/app/view/viewcube.rs index fd37d96d..4d63fa51 100644 --- a/src/app/view/viewcube.rs +++ b/src/app/view/viewcube.rs @@ -16,7 +16,9 @@ use iced::{Background, Border, Element, Theme}; /// Place `el` at pixel offset (x, y) inside a fill layer (top-left origin). fn vc_place<'a>(x: f32, y: f32, el: Element<'a, Message>) -> Element<'a, Message> { - crate::ui::pin_at(iced::Point::new(x, y), el) + iced::widget::pin(el) + .position(iced::Point::new(x.max(0.0), y.max(0.0))) + .into() } /// Borderless square icon button used by the ViewCube nav controls. @@ -155,7 +157,8 @@ pub(super) fn viewcube_ucs_picker<'a>(current: String, names: Vec) -> El } else { current }; - crate::ui::pick_list(options, Some(selected), Message::SetViewcubeUcs) + iced::widget::pick_list(Some(selected), options, |value| value.to_string()) + .on_select(Message::SetViewcubeUcs) .text_size(11) .padding([2, 6]) // Fixed width so the caller can centre it under the cube centre with a diff --git a/src/ui/color_select.rs b/src/ui/color_select.rs index 4d0a6103..1d48214d 100644 --- a/src/ui/color_select.rs +++ b/src/ui/color_select.rs @@ -9,7 +9,7 @@ use acadrust::types::Color as AcadColor; use iced::advanced::layout::{self, Layout}; use iced::advanced::widget::{self, Widget}; use iced::advanced::{mouse, overlay, renderer, Shell}; -use iced::widget::{button, column, container, row, scrollable, text}; +use iced::widget::{button, column, container, row, 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 @@ -21,17 +21,47 @@ pub struct ColorExtras { } /// 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). +/// (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 { .. } => "256".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::().unwrap_or(256) { @@ -197,80 +227,6 @@ pub fn color_list<'a>( 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)] - .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: &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; - } - grid = grid.push(r); - } - - container( - column![ - text("Select Color").size(13), - row![chip(AcadColor::ByLayer, "ByLayer"), chip(AcadColor::ByBlock, "ByBlock")].spacing(6), - scrollable(grid).height(Length::Fit), - ] - .spacing(8), - ) - .style(|theme: &Theme| container::Style { - background: Some(Background::Color( - theme.palette().background.weak.color - )), - ..Default::default() - }) - .padding(10) - .width(Length::Fit) - .height(Length::Fit) - .into() -} - /// Render `base` inline with `popup` floating just below it — the shared /// dropdown mechanic for the panel's custom dropdowns (colour picker, block /// Name). Unlike iced's menu overlay it always opens downward. diff --git a/src/ui/mod.rs b/src/ui/mod.rs index db6b164d..38640b8d 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -2,34 +2,6 @@ /// Change this to scale the ribbon, layer manager rows, and property panel rows uniformly. pub const ROW_H: f32 = 26.0; -/// Iced 0.15 separates pick-list construction from the selection callback. -/// Keep the application's established call shape while the rest of the UI -/// migrates independently. -pub fn pick_list<'a, T, L, V, Message>( - options: L, - selected: Option, - on_select: impl Fn(T) -> Message + 'a, -) -> iced::widget::PickList<'a, T, L, V, Message> -where - T: PartialEq + Clone + ToString + 'a, - L: std::borrow::Borrow<[T]> + 'a, - V: std::borrow::Borrow + 'a, - Message: Clone + 'a, -{ - iced::widget::pick_list(selected, options, |value| value.to_string()).on_select(on_select) -} - -/// Place `content` at fixed top-left coordinates inside a fill-sized layer. -/// Negative coordinates clamp to the layer edge. -pub fn pin_at<'a, Message: 'a>( - position: iced::Point, - content: impl Into>, -) -> iced::Element<'a, Message> { - iced::widget::pin(content) - .position(iced::Point::new(position.x.max(0.0), position.y.max(0.0))) - .into() -} - pub mod color_select; pub mod command_line; pub mod icons; diff --git a/src/ui/modal.rs b/src/ui/modal.rs index da18ad36..e0852d99 100644 --- a/src/ui/modal.rs +++ b/src/ui/modal.rs @@ -210,12 +210,6 @@ impl ModalOptions { neutral_close: false, }; - pub const MOVABLE_FIXED: Self = Self { - movable: true, - resizable: false, - neutral_close: false, - }; - pub const NOTICE: Self = Self { movable: true, resizable: false, @@ -223,6 +217,37 @@ impl ModalOptions { }; } +/// Dim and block the application beneath an overlay-owned dialog. +/// +/// Some third-party overlays ignore pointer movement outside their interactive +/// controls. The shield owns the cursor and pointer presses across the window, +/// making such an overlay behave modally. +pub fn backdrop<'a>( + base: impl Into>, + on_close: Message, +) -> Element<'a, Message> { + let shield = mouse_area( + container(Space::new()) + .width(Length::Fill) + .height(Length::Fill) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme + .palette() + .background + .strongest + .color + .scale_alpha(0.55), + )), + ..Default::default() + }), + ) + .on_press(on_close) + .interaction(iced::mouse::Interaction::Idle); + + stack![base.into(), opaque(shield)].into() +} + /// Stack `content` over `base` behind a dimmed backdrop, framed with a title bar /// (the ✕ close button at its right end). The backdrop only dims and blocks /// clicks from reaching the view beneath — it does **not** dismiss the dialog; diff --git a/src/ui/popup/cycle_popup.rs b/src/ui/popup/cycle_popup.rs index 979a913c..08066984 100644 --- a/src/ui/popup/cycle_popup.rs +++ b/src/ui/popup/cycle_popup.rs @@ -22,7 +22,8 @@ pub fn cycle_popup_overlay( .style(container::bordered_box) .width(Length::Fixed(150.0)); - let positioned = crate::ui::pin_at(anchor, opaque(panel)); + let positioned = iced::widget::pin(opaque(panel)) + .position(iced::Point::new(anchor.x.max(0.0), anchor.y.max(0.0))); mouse_area(positioned).on_press(Message::CycleCancel).into() } diff --git a/src/ui/properties.rs b/src/ui/properties.rs index 9ca65354..f1b52090 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -611,7 +611,10 @@ impl PropertiesPanel { // "More Colors…" opens the full palette window targeting the // background colour — this used to just close the picker // (#415). - Message::OpenColorWindow(crate::app::ColorPickTarget::PropertiesBg), + Message::OpenColorWindow( + crate::app::ColorPickTarget::PropertiesBg, + color, + ), ); return prop_row_widget(label, selector); } @@ -655,7 +658,10 @@ impl PropertiesPanel { }, Message::PropColorChanged, Message::PropColorPickerToggle, - Message::OpenColorWindow(crate::app::ColorPickTarget::Properties), + Message::OpenColorWindow( + crate::app::ColorPickTarget::Properties, + color, + ), ); prop_row_widget(label, selector) } diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index 79f74166..a70442ac 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -1108,7 +1108,10 @@ impl Ribbon { by_block: true, }, Message::RibbonColorChanged, - Message::OpenColorWindow(crate::app::ColorPickTarget::Ribbon), + Message::OpenColorWindow( + crate::app::ColorPickTarget::Ribbon, + self.active_color, + ), ); let panel = container(picker) diff --git a/src/ui/style/dimstyle.rs b/src/ui/style/dimstyle.rs index bfa0a8a5..31f9e5b2 100644 --- a/src/ui/style/dimstyle.rs +++ b/src/ui/style/dimstyle.rs @@ -235,7 +235,8 @@ pub fn view_window<'a>( .unwrap_or_else(|| val.to_string()); row![ lbl(label), - crate::ui::pick_list(labels, Some(cur), move |chosen| { + iced::widget::pick_list(Some(cur), labels, |value| value.to_string()) + .on_select(move |chosen| { let code = opts .iter() .find(|(_, l)| *l == chosen.as_str()) @@ -287,7 +288,10 @@ pub fn view_window<'a>( }, move |c| Message::DsEdit(f_sel.clone(), crate::ui::color_select::color_to_aci_string(c)), Message::DsColorMore(fld.clone()), - Message::OpenColorWindow(ColorPickTarget::DimStyle(fld.clone())), + Message::OpenColorWindow( + ColorPickTarget::DimStyle(fld.clone()), + cur, + ), ); row![lbl(label), container(selector).width(150)] .spacing(8) @@ -304,9 +308,8 @@ pub fn view_window<'a>( -> Element<'a, Message> { row![ lbl(label), - crate::ui::pick_list(options, Some(selected), move |value| { - Message::DsSetHandle { field, value } - }) + iced::widget::pick_list(Some(selected), options, |value| value.to_string()) + .on_select(move |value| Message::DsSetHandle { field, value }) .text_size(11) .width(150), ] diff --git a/src/ui/style/mleaderstyle.rs b/src/ui/style/mleaderstyle.rs index ffb255d7..354eb047 100644 --- a/src/ui/style/mleaderstyle.rs +++ b/src/ui/style/mleaderstyle.rs @@ -126,7 +126,10 @@ fn color_row<'a>( value: crate::ui::color_select::color_to_aci_string(c), }, Message::MLeaderColorMore(field), - Message::OpenColorWindow(crate::app::ColorPickTarget::MLeader(field)), + Message::OpenColorWindow( + crate::app::ColorPickTarget::MLeader(field), + cur, + ), ); row![text(label).size(11).style(muted_style).width(150), selector] .spacing(8) @@ -142,9 +145,8 @@ fn enum_row<'a>( ) -> Element<'a, Message> { row![ text(label).size(11).style(muted_style).width(150), - crate::ui::pick_list(options, Some(selected), move |value| { - Message::MLeaderStyleSetEnum { field, value } - }) + iced::widget::pick_list(Some(selected), options, |value| value.to_string()) + .on_select(move |value| Message::MLeaderStyleSetEnum { field, value }) .text_size(11) .width(190), ] @@ -157,11 +159,12 @@ fn lineweight_row<'a>(selected: acadrust::types::LineWeight) -> Element<'a, Mess let selected = crate::ui::properties::LwItem(selected); row![ text("Line weight:").size(11).style(muted_style).width(150), - crate::ui::pick_list( - crate::ui::properties::lw_options(), + iced::widget::pick_list( Some(selected), - |item| Message::MLeaderStyleLineWeightChanged(item.0) + crate::ui::properties::lw_options(), + |value| value.to_string(), ) + .on_select(|item| Message::MLeaderStyleLineWeightChanged(item.0)) .text_size(11) .width(190), ] @@ -199,9 +202,8 @@ fn handle_row<'a>( ) -> Element<'a, Message> { row![ text(label).size(11).style(muted_style).width(150), - crate::ui::pick_list(options, Some(selected), move |value| { - Message::MLeaderStyleSetHandle { field, value } - }) + iced::widget::pick_list(Some(selected), options, |value| value.to_string()) + .on_select(move |value| Message::MLeaderStyleSetHandle { field, value }) .text_size(11) .width(190), ] diff --git a/src/ui/style/tablestyle.rs b/src/ui/style/tablestyle.rs index 9f4c3265..ebf4406e 100644 --- a/src/ui/style/tablestyle.rs +++ b/src/ui/style/tablestyle.rs @@ -119,7 +119,10 @@ pub fn view_window<'a>( value: crate::ui::color_select::color_to_aci_string(c), }, Message::TableColorMore(row, field), - Message::OpenColorWindow(crate::app::ColorPickTarget::Table(row, field)), + Message::OpenColorWindow( + crate::app::ColorPickTarget::Table(row, field), + cur, + ), ); row![text(label).size(11).style(muted_style).width(150), selector] .spacing(8) @@ -141,7 +144,8 @@ pub fn view_window<'a>( .push( row![ text(" Alignment:").size(11).style(muted_style).width(150), - crate::ui::pick_list( + iced::widget::pick_list( + Some(format!("{:?}", rs.alignment)), [ "TopLeft", "TopCenter", @@ -156,9 +160,9 @@ pub fn view_window<'a>( .iter() .map(|s| s.to_string()) .collect::>(), - Some(format!("{:?}", rs.alignment)), - move |value| Message::TableStyleCellSetAlign { row, value }, + |value| value.to_string(), ) + .on_select(move |value| Message::TableStyleCellSetAlign { row, value }) .text_size(11) .width(140), ] @@ -194,18 +198,19 @@ pub fn view_window<'a>( col = col.push( row![ text(format!(" {bname}")).size(11).style(muted_style).width(28), - crate::ui::pick_list( + iced::widget::pick_list( + Some(format!("{:?}", bd.border_type)), ["Single", "Double"] .iter() .map(|s| s.to_string()) .collect::>(), - Some(format!("{:?}", bd.border_type)), - move |value| Message::TableStyleBorderSetType { + |value| value.to_string(), + ) + .on_select(move |value| Message::TableStyleBorderSetType { cell: row, border: bu, value - }, - ) + }) .text_size(10) .width(74), text_input("wt", &border_lw[r][b]) @@ -274,14 +279,15 @@ pub fn view_window<'a>( .align_y(iced::Center), row![ text("Flow direction:").size(11).style(muted_style).width(160), - crate::ui::pick_list( + iced::widget::pick_list( + Some(format!("{:?}", s.flow_direction)), ["Down", "Up"] .iter() .map(|s| s.to_string()) .collect::>(), - Some(format!("{:?}", s.flow_direction)), - Message::TableStyleSetFlow, + |value| value.to_string(), ) + .on_select(Message::TableStyleSetFlow) .text_size(11) .width(100), ] diff --git a/src/ui/window/attribute_editor.rs b/src/ui/window/attribute_editor.rs index 7a807814..ee48c0e4 100644 --- a/src/ui/window/attribute_editor.rs +++ b/src/ui/window/attribute_editor.rs @@ -210,7 +210,8 @@ fn pick_field<'a>( on_select: impl Fn(String) -> Message + 'a, width: Length, ) -> Element<'a, Message> { - let pl = crate::ui::pick_list(options, selected, on_select) + let pl = iced::widget::pick_list(selected, options, |value| value.to_string()) + .on_select(on_select) .text_size(13) .padding([3, 6]) .width(width); @@ -475,9 +476,8 @@ fn properties_tab<'a>( let lw_opts = lw_options(); let lw_sel = LwItem(r.line_weight); - let lw = crate::ui::pick_list(lw_opts, Some(lw_sel), |it: LwItem| { - Message::AttrEditorLineweight(it.0) - }) + let lw = iced::widget::pick_list(Some(lw_sel), lw_opts, |value| value.to_string()) + .on_select(|it: LwItem| Message::AttrEditorLineweight(it.0)) .text_size(13) .padding([3, 6]) .width(width); diff --git a/src/ui/window/layer_state_manager.rs b/src/ui/window/layer_state_manager.rs index f1a608c0..b9e39bb7 100644 --- a/src/ui/window/layer_state_manager.rs +++ b/src/ui/window/layer_state_manager.rs @@ -413,7 +413,10 @@ fn editor_layer_row<'a>( }, move |color| Message::LayerStateEditorLayerColor(index, color), Message::LayerStateEditorLayerColorToggle(index), - Message::OpenColorWindow(crate::app::ColorPickTarget::LayerState(index)), + Message::OpenColorWindow( + crate::app::ColorPickTarget::LayerState(index), + layer.color, + ), ); container( @@ -432,13 +435,21 @@ fn editor_layer_row<'a>( 54.0 ), container(color).width(Length::Fixed(135.0)), - crate::ui::pick_list(linetypes, current_linetype, move |value| { - Message::LayerStateEditorLayerLinetype(index, value) - }) + iced::widget::pick_list( + current_linetype, + linetypes, + |value| value.to_string(), + ) + .on_select(move |value| Message::LayerStateEditorLayerLinetype(index, value)) .text_size(11) .padding([3, 5]) .width(Length::Fixed(150.0)), - crate::ui::pick_list(lw_options(), current_lineweight, move |item: LwItem| { + iced::widget::pick_list( + current_lineweight, + lw_options(), + |value| value.to_string(), + ) + .on_select(move |item: LwItem| { Message::LayerStateEditorLayerLineweight(index, item.0) }) .text_size(11) @@ -449,11 +460,12 @@ fn editor_layer_row<'a>( .size(11) .padding([3, 5]) .width(Length::Fixed(135.0)), - crate::ui::pick_list( - transparency_options(layer.transparency), + iced::widget::pick_list( Some(TransparencyItem(layer.transparency)), - move |item| Message::LayerStateEditorLayerTransparency(index, item.0), + transparency_options(layer.transparency), + |value| value.to_string(), ) + .on_select(move |item| Message::LayerStateEditorLayerTransparency(index, item.0)) .text_size(11) .padding([3, 5]) .width(Length::Fixed(105.0)), @@ -543,11 +555,12 @@ pub fn view_editor<'a>( .style(muted), Space::new().width(sizing.width), text("Current layer").size(10).style(muted), - crate::ui::pick_list( - layer_names, + iced::widget::pick_list( Some(state.current_layer.clone()), - Message::LayerStateEditorCurrentLayer, + layer_names, + |value| value.to_string(), ) + .on_select(Message::LayerStateEditorCurrentLayer) .text_size(11) .padding([3, 6]) .width(Length::Fixed(180.0)), diff --git a/src/ui/window/layers.rs b/src/ui/window/layers.rs index 1c637893..09d2a728 100644 --- a/src/ui/window/layers.rs +++ b/src/ui/window/layers.rs @@ -780,7 +780,10 @@ fn layer_row<'a>( _ => Message::LayerColorSet(7), }, Message::LayerColorPickerToggle(index), - Message::OpenColorWindow(crate::app::ColorPickTarget::Layer(index)), + Message::OpenColorWindow( + crate::app::ColorPickTarget::Layer(index), + acadrust::types::Color::Index(aci), + ), )) .width(Length::Fixed(COL_COLOR)) .into(); diff --git a/src/ui/window/options.rs b/src/ui/window/options.rs index 5022c9c2..865a54a5 100644 --- a/src/ui/window/options.rs +++ b/src/ui/window/options.rs @@ -70,11 +70,12 @@ pub fn view_window<'a>( Space::new().height(10), row![ text("Default save format:").size(12).width(150), - crate::ui::pick_list( - crate::io::SAVE_FORMAT_OPTIONS, + iced::widget::pick_list( selected_format, - |format: &str| Message::DefaultSaveFormatChanged(format.to_string()) + crate::io::SAVE_FORMAT_OPTIONS, + |value| value.to_string(), ) + .on_select(|format: &str| Message::DefaultSaveFormatChanged(format.to_string())) .width(sizing.width), ] .spacing(12) @@ -90,11 +91,12 @@ pub fn view_window<'a>( Space::new().height(10), row![ text("Iced theme:").size(12).width(150), - crate::ui::pick_list( - theme_options, + iced::widget::pick_list( selected_theme, - Message::OptionsThemeChanged, + theme_options, + |value| value.to_string(), ) + .on_select(Message::OptionsThemeChanged) .width(sizing.width), ] .spacing(12) diff --git a/src/ui/window/plot.rs b/src/ui/window/plot.rs index b7876da1..44b368fc 100644 --- a/src/ui/window/plot.rs +++ b/src/ui/window/plot.rs @@ -332,7 +332,8 @@ fn drop_row<'a>( ctor: fn(String) -> PlotDlgMsg, width: Length, ) -> Element<'a, Message> { - let pl = crate::ui::pick_list(options, selected, move |s| Message::PlotDlg(ctor(s))) + let pl = iced::widget::pick_list(selected, options, |value| value.to_string()) + .on_select(move |s| Message::PlotDlg(ctor(s))) .text_size(12) .padding([3, 6]) .width(width); @@ -536,11 +537,12 @@ pub fn view_window( section_label("Plot area"), row![ container( - crate::ui::pick_list( - strs(&["Layout", "Extents", "Display", "Window"]), + iced::widget::pick_list( Some(s.area.clone()), - move |v| Message::PlotDlg(PlotDlgMsg::Area(v)), + strs(&["Layout", "Extents", "Display", "Window"]), + |value| value.to_string(), ) + .on_select(move |v| Message::PlotDlg(PlotDlgMsg::Area(v))) .text_size(12) .padding([3, 6]) .width(width) @@ -608,11 +610,12 @@ pub fn view_window( section_label("Quality"), row![ container( - crate::ui::pick_list( - strs(&["Draft", "Normal", "High", "Maximum"]), + iced::widget::pick_list( Some(s.quality.clone()), - move |v| Message::PlotDlg(PlotDlgMsg::Quality(v)), + strs(&["Draft", "Normal", "High", "Maximum"]), + |value| value.to_string(), ) + .on_select(move |v| Message::PlotDlg(PlotDlgMsg::Quality(v))) .text_size(12) .padding([3, 6]) .width(width) diff --git a/src/ui/window/plugin_manager.rs b/src/ui/window/plugin_manager.rs index 3151cf7a..b0c1c2a3 100644 --- a/src/ui/window/plugin_manager.rs +++ b/src/ui/window/plugin_manager.rs @@ -325,9 +325,8 @@ fn install_controls<'a>( text("no releases").size(11).style(muted_style).into() } else { let r = repo_s.clone(); - crate::ui::pick_list(tags, selected, move |tag| { - Message::PluginReleaseSelect(r.clone(), tag) - }) + iced::widget::pick_list(selected, tags, |value| value.to_string()) + .on_select(move |tag| Message::PluginReleaseSelect(r.clone(), tag)) .text_size(12) .into() };