From 6bf84d8f5ead3afa3f2502f3729431e1e9d78b09 Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Sat, 29 Aug 2026 12:31:03 +0100 Subject: [PATCH] feat(ui): show color swatch and layer in selection cycling popup Improve the selection-cycling list box so overlapping objects can be identified at a glance: - Add a color swatch per row showing the object's effective drawn color - Show the layer name right-aligned next to the element type - Size the popup to fit its content with consistent padding Widen render_style_for_viewport visibility from scene-internal to crate-wide so the view layer resolves the same color the renderer uses. --- src/app/view/mod.rs | 20 ++++++--- src/scene/view/render.rs | 2 +- src/ui/popup/cycle_popup.rs | 87 ++++++++++++++++++++++++++++++++----- 3 files changed, 93 insertions(+), 16 deletions(-) diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 7a073e03d..f59c33cec 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1435,13 +1435,23 @@ impl OpenCADStudio { // Selection-cycling list box: pick among overlapping objects. if let Some((pt, cands)) = &self.cycle_candidates { if !tab.is_start { - let items: Vec<(acadrust::Handle, String)> = cands + let items: Vec = cands .iter() .filter_map(|&h| { - tab.scene - .document - .get_entity(h) - .map(|e| (h, crate::entities::traits::entity_type_name(e).to_string())) + tab.scene.document.get_entity(h).map(|e| { + let style = crate::scene::view::render::render_style_for_viewport( + &tab.scene.document, + e, + None, + ); + crate::ui::popup::cycle_popup::CycleCandidate { + handle: h, + type_name: crate::entities::traits::entity_type_name(e) + .to_string(), + layer: e.common().layer.clone(), + color: style.0, + } + }) }) .collect(); if !items.is_empty() { diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index feeff457d..0a4d074cd 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -2802,7 +2802,7 @@ fn viewport_override( .find_map(|(handle, value)| (handle == viewport).then_some(value)) } -pub(in crate::scene) fn render_style_for_viewport( +pub(crate) fn render_style_for_viewport( document: &CadDocument, e: &EntityType, viewport: Option, diff --git a/src/ui/popup/cycle_popup.rs b/src/ui/popup/cycle_popup.rs index 080669842..9cbf9df46 100644 --- a/src/ui/popup/cycle_popup.rs +++ b/src/ui/popup/cycle_popup.rs @@ -1,26 +1,43 @@ //! Selection-cycling list box — shown at the cursor when a click lands on -//! two or more overlapping objects. Each row names a candidate; clicking it -//! adds that object to the current selection. Clicking outside dismisses it. +//! two or more overlapping objects. Each row shows the object's drawn color +//! swatch, its type, and (right-aligned) its layer; clicking a row adds that +//! object to the current selection. Clicking outside dismisses it. -use iced::widget::{button, column, container, mouse_area, opaque, text}; -use iced::{Element, Fill, Length}; +use iced::alignment::Horizontal; +use iced::widget::{button, column, container, mouse_area, opaque, row, rule, space, text}; +use iced::{border::Radius, color, Element, Fill, Length, Theme}; use crate::app::Message; +/// Row content for one overlapping candidate. +pub struct CycleCandidate { + pub handle: acadrust::Handle, + pub type_name: String, + pub layer: String, + /// The color the object is drawn with (layer-inherited when ByLayer). + pub color: [f32; 4], +} + /// Full-canvas overlay: the list box anchored at `anchor` (canvas /// coordinates) plus a transparent click-catcher that cancels. pub fn cycle_popup_overlay( anchor: iced::Point, - items: Vec<(acadrust::Handle, String)>, + items: Vec, ) -> Element<'static, Message> { + // The type column and the layer column get one shared width each (the + // widest entry of their kind, clamped) so the separators line up. + let type_w = clamp_col_width(items.iter().map(|c| c.type_name.chars().count()), 11.0, 40.0, 96.0); + let layer_w = + clamp_col_width(items.iter().map(|c| c.layer.chars().count()), 10.0, 28.0, 84.0); + let rows: Vec> = items .into_iter() - .map(|(handle, label)| item_row(handle, label)) + .map(|c| item_row(c.handle, c.type_name, c.layer, c.color, type_w, layer_w)) .collect(); let panel = container(column(rows)) .style(container::bordered_box) - .width(Length::Fixed(150.0)); + .width(Length::Fixed(type_w + layer_w + 42.0)); let positioned = iced::widget::pin(opaque(panel)) .position(iced::Point::new(anchor.x.max(0.0), anchor.y.max(0.0))); @@ -28,13 +45,63 @@ pub fn cycle_popup_overlay( mouse_area(positioned).on_press(Message::CycleCancel).into() } -fn item_row(handle: acadrust::Handle, label: String) -> Element<'static, Message> { - let content = text(label).size(11).align_y(iced::Center); +/// Column width for the widest label, using a per-glyph estimate with a +/// minimum so short labels don't collapse and a maximum so long ones clip. +fn clamp_col_width(labels: impl Iterator, glyph_w: f32, min: f32, max: f32) -> f32 { + let widest = labels.max().unwrap_or(0) as f32 * glyph_w; + widest.clamp(min, max) +} + +fn item_row( + handle: acadrust::Handle, + type_name: String, + layer: String, + color: [f32; 4], + type_w: f32, + layer_w: f32, +) -> Element<'static, Message> { + let swatch = container( + space::Space::new() + .width(Length::Fixed(9.0)) + .height(Length::Fixed(9.0)), + ) + .style(move |_| container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgba( + color[0], color[1], color[2], color[3], + ))), + border: iced::Border { + color: color!(0x6b6b6b), + width: 1.0, + radius: Radius::from(2.0), + }, + ..Default::default() + }); + + let content = row![ + swatch, + container(text(type_name).size(11)) + .width(Length::Fixed(type_w)) + .align_y(iced::alignment::Vertical::Center), + container(rule::vertical(1)).height(Length::Fixed(12.0)), + container( + text(layer) + .size(10) + .align_x(Horizontal::Right) + .style(|theme: &Theme| iced::widget::text::Style { + color: Some(theme.palette().background.base.text.scale_alpha(0.72)), + }) + ) + .width(Length::Fixed(layer_w)) + .align_y(iced::alignment::Vertical::Center), + ] + .spacing(6) + .align_y(iced::Center); + let btn = button(content) .on_press(Message::CycleSelect(handle)) .style(button::subtle) .width(Fill) - .padding([4, 10]); + .padding([3, 8]); // Highlight the underlying object while the cursor is over this row. mouse_area(btn) .on_enter(Message::CycleHover(Some(handle)))