From c9550b48f99834f5dcb729c5c1814ff4e15fa608 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 4 Aug 2026 17:32:06 +0300 Subject: [PATCH] feat(properties): refine quick properties - Keep the floating panel near the selection and inside the viewport - Persist the toggle and hide the panel without a selection - Show only Material in 3D Visualization --- src/app/mod.rs | 3 +++ src/app/settings.rs | 3 +++ src/app/update/file.rs | 2 ++ src/app/update/mod.rs | 12 +++++++++++ src/app/update/viewport.rs | 4 ++++ src/app/view/mod.rs | 35 +++++++++++++++++--------------- src/app/view/overlay.rs | 25 +++++++++++++++++++---- src/scene/cache/properties.rs | 38 +++++------------------------------ src/ui/properties.rs | 10 +++++---- 9 files changed, 75 insertions(+), 57 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 12717ecb3..5463f3d88 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -352,6 +352,8 @@ pub(super) struct OpenCADStudio { clean_screen: bool, /// Quick Properties: show a compact floating property panel on selection. quick_properties: bool, + /// Canvas-space cursor position where Quick Properties was last opened. + quick_properties_anchor: Point, /// Selection cycling: clicking where objects overlap opens a list box /// to pick which one; the pick is added to the current selection. selection_cycling: bool, @@ -2869,6 +2871,7 @@ impl OpenCADStudio { otrack_active: None, clean_screen: false, quick_properties: false, + quick_properties_anchor: Point::new(12.0, 12.0), selection_cycling: false, pick_add: true, pick_drag_rect: false, diff --git a/src/app/settings.rs b/src/app/settings.rs index 0d34a864b..13906d3d4 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -147,6 +147,8 @@ pub struct UserSettings { /// PICKDRAG (#226): `false` (default) = press-drag draws the freeform /// lasso; `true` = press-drag draws a rectangle marquee instead. pub pick_drag_rect: bool, + /// Show the floating Quick Properties panel when objects are selected. + pub quick_properties: bool, /// Persisted viewport background colours (0–255 RGB); `None` = app default /// (dark grey model / off-white paper). Applied to every drawing tab on /// launch and to tabs opened later, so a chosen background survives restarts @@ -181,6 +183,7 @@ impl Default for UserSettings { default_save_format: crate::io::DEFAULT_SAVE_FORMAT.to_string(), pick_add: true, pick_drag_rect: false, + quick_properties: false, bg_color: None, paper_bg_color: None, language: crate::i18n::Language::default(), diff --git a/src/app/update/file.rs b/src/app/update/file.rs index b202c694e..b2ccc45dc 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -287,6 +287,7 @@ impl OpenCADStudio { default_save_format: self.default_save_format.clone(), pick_add: self.pick_add, pick_drag_rect: self.pick_drag_rect, + quick_properties: self.quick_properties, bg_color: self.default_bg_color.map(f4_to_u3), paper_bg_color: self.default_paper_bg_color.map(f4_to_u3), language: self.language, @@ -325,6 +326,7 @@ impl OpenCADStudio { crate::io::canonical_save_format(&s.default_save_format).to_string(); self.pick_add = s.pick_add; self.pick_drag_rect = s.pick_drag_rect; + self.quick_properties = s.quick_properties; self.default_bg_color = s.bg_color.map(u3_to_f4); self.default_paper_bg_color = s.paper_bg_color.map(u3_to_f4); if crate::i18n::set_language(s.language).is_ok() { diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 45fb6862a..aaf98fbc2 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -3216,6 +3216,11 @@ impl OpenCADStudio { } Message::ToggleQuickProperties => { self.quick_properties ^= true; + if self.quick_properties { + self.quick_properties_anchor = + self.tabs[self.active_tab].last_cursor_screen; + } + self.save_config(); Task::none() } Message::ToggleSelectionCycling => { @@ -3226,12 +3231,19 @@ impl OpenCADStudio { } Message::CycleSelect(handle) => { // Add the picked object to the current selection (accumulate). + let quick_properties_anchor = self + .cycle_candidates + .as_ref() + .map(|(point, _)| *point); self.cycle_candidates = None; let i = self.active_tab; self.tabs[i].scene.set_hover_highlight(None); self.tabs[i].scene.select_entity(handle, false); self.tabs[i].scene.expand_selection_for_groups(&[handle]); self.refresh_properties(); + if let Some(point) = quick_properties_anchor { + self.quick_properties_anchor = point; + } Task::none() } Message::CycleHover(handle) => { diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 33e0ba221..03923bba5 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -3530,6 +3530,10 @@ impl OpenCADStudio { sel.left_dragging = false; } + if selection_just_completed { + self.quick_properties_anchor = p_full; + } + if is_gathering && selection_just_completed { let handles: Vec = self.tabs[i] .scene diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 26af5a82f..9c478428d 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -23,8 +23,8 @@ mod viewcube; use controls::{dyn_component_value, viewport_controls}; use overlay::{ - mtext_editor_overlay, position_canvas_overlay, qselect_overlay, text_inline_overlay, - viewport_context_menu_overlay, + mtext_editor_overlay, position_canvas_overlay, position_canvas_overlay_near_cursor, + qselect_overlay, text_inline_overlay, viewport_context_menu_overlay, }; use viewcube::{viewcube_nav_controls, viewcube_ucs_picker, UCS_PICKER_W}; @@ -1208,12 +1208,25 @@ impl OpenCADStudio { } } - // Quick Properties: compact floating property panel on selection, - // anchored at the canvas top-left so it doesn't track the cursor. + // Reserve the overlaid command line when placing cursor-anchored panels. + let command_line_inset = if self.command_line.history_open { + self.command_line.history_height.clamp( + crate::ui::command_line::HISTORY_HEIGHT_MIN, + crate::ui::command_line::history_max_height(self.win_size.1), + ) + 72.0 + } else { + 34.0 + }; + + // Quick Properties: stay near the selection cursor, flipping around + // it as needed to remain inside the visible drawing area. if self.quick_properties && !tab.is_start { if let Some(panel) = tab.properties.quick_view() { - viewport_stack = viewport_stack - .push(position_canvas_overlay(iced::Point::new(12.0, 12.0), panel)); + viewport_stack = viewport_stack.push(position_canvas_overlay_near_cursor( + self.quick_properties_anchor, + command_line_inset, + panel, + )); } } @@ -1353,16 +1366,6 @@ impl OpenCADStudio { .take(3) .cloned() .collect(); - // The command line overlays the bottom of the drawing stack. - // Keep context menus above its interactive input/history area. - let command_line_inset = if self.command_line.history_open { - self.command_line.history_height.clamp( - crate::ui::command_line::HISTORY_HEIGHT_MIN, - crate::ui::command_line::history_max_height(self.win_size.1), - ) + 72.0 - } else { - 34.0 - }; viewport_stack = viewport_stack.push(viewport_context_menu_overlay( p, command_line_inset, diff --git a/src/app/view/overlay.rs b/src/app/view/overlay.rs index c6f01d65a..30a27ea6e 100644 --- a/src/app/view/overlay.rs +++ b/src/app/view/overlay.rs @@ -752,6 +752,22 @@ fn position_canvas_overlay_clamped<'a>( content: iced::widget::opaque(panel), anchor, bottom_inset: bottom_inset.max(0.0), + gap: 0.0, + }) +} + +/// Keep a floating panel close to the cursor without letting it cover the +/// pointer or leave the drawing's safe rectangle. +pub(super) fn position_canvas_overlay_near_cursor<'a>( + cursor: iced::Point, + bottom_inset: f32, + panel: Element<'a, Message>, +) -> Element<'a, Message> { + Element::new(ClampedPin { + content: iced::widget::opaque(panel), + anchor: cursor, + bottom_inset: bottom_inset.max(0.0), + gap: 12.0, }) } @@ -759,6 +775,7 @@ struct ClampedPin<'a> { content: Element<'a, Message>, anchor: iced::Point, bottom_inset: f32, + gap: f32, } impl Widget for ClampedPin<'_> { @@ -799,8 +816,8 @@ impl Widget for ClampedPin<'_> { let content = node.size(); let max_x = (max.width - MARGIN - content.width).max(MARGIN); - let right = self.anchor.x; - let left = self.anchor.x - content.width; + let right = self.anchor.x + self.gap; + let left = self.anchor.x - self.gap - content.width; let x = if right <= max_x { right.max(MARGIN) } else if left >= MARGIN { @@ -811,8 +828,8 @@ impl Widget for ClampedPin<'_> { let max_y = (max.height - self.bottom_inset - MARGIN - content.height).max(MARGIN); - let below = self.anchor.y; - let above = self.anchor.y - content.height; + let below = self.anchor.y + self.gap; + let above = self.anchor.y - self.gap - content.height; let y = if below <= max_y { below.max(MARGIN) } else if above >= MARGIN { diff --git a/src/scene/cache/properties.rs b/src/scene/cache/properties.rs index 70b7337c3..5bad362b1 100644 --- a/src/scene/cache/properties.rs +++ b/src/scene/cache/properties.rs @@ -125,41 +125,13 @@ pub fn visualization_section(entity: &EntityType) -> Option { 1 => "ByBlock", _ => "Custom", }; - let mut props = vec![Property { - label: t!("Material").into_owned(), - field: "material", - value: PropValue::ReadOnly(material.into()), - }]; - let handle_text = |handle: Option| { - handle - .filter(|handle| handle.is_valid()) - .map_or_else(|| "None".to_string(), |handle| format!("{:X}", handle.value())) - }; - props.extend([ - Property { - label: t!("Visual style").into_owned(), - field: "visual_style", - value: PropValue::ReadOnly(handle_text(common.full_visual_style_handle)), - }, - Property { - label: t!("Face style").into_owned(), - field: "face_visual_style", - value: PropValue::ReadOnly(handle_text(common.face_visual_style_handle)), - }, - Property { - label: t!("Edge style").into_owned(), - field: "edge_visual_style", - value: PropValue::ReadOnly(handle_text(common.edge_visual_style_handle)), - }, - Property { - label: t!("Shadow flags").into_owned(), - field: "shadow_flags", - value: PropValue::ReadOnly(common.shadow_flags.to_string()), - }, - ]); Some(PropSection { title: t!("3D Visualization").into_owned(), - props, + props: vec![Property { + label: t!("Material").into_owned(), + field: "material", + value: PropValue::ReadOnly(material.into()), + }], }) } diff --git a/src/ui/properties.rs b/src/ui/properties.rs index f07ea46cb..b87c02367 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -504,7 +504,7 @@ impl PropertiesPanel { /// editable section rows as the docked panel, sized to its content. /// Returns `None` when nothing is selected. pub fn quick_view(&self) -> Option> { - if self.sections.is_empty() { + if self.source_handles.is_empty() || self.sections.is_empty() { return None; } let title = container( @@ -527,13 +527,15 @@ impl PropertiesPanel { .width(Length::Fill) .padding([4, 10]); - let mut col = column![title].spacing(0); + let mut sections = column![].spacing(0); for section in &self.sections { - col = col.push(self.render_section(section)); + sections = sections.push(self.render_section(section)); } + let content = scrollable(sections).height(Length::Shrink); + Some( - container(col) + container(column![title, content].spacing(0)) .style(|theme: &Theme| { let palette = theme.palette(); container::Style {