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
This commit is contained in:
Hakan Seven committed 2026-08-04 17:32:06 +03:00
1 parent 5ad95d1d2e
commit c9550b48f9
9 files changed
+75 -57

No files matched your search

+3
View File
@@ -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,
+3
View File
@@ -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(),
+2
View File
@@ -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() {
+12
View File
@@ -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) => {
+4
View File
@@ -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<Handle> = self.tabs[i]
.scene
+19 -16
View File
@@ -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,
+21 -4
View File
@@ -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<Message, Theme, iced::Renderer> for ClampedPin<'_> {
@@ -799,8 +816,8 @@ impl Widget<Message, Theme, iced::Renderer> 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<Message, Theme, iced::Renderer> 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 {
+5 -33
View File
@@ -125,41 +125,13 @@ pub fn visualization_section(entity: &EntityType) -> Option<PropSection> {
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<acadrust::Handle>| {
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()),
}],
})
}
+6 -4
View File
@@ -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<Element<'_, Message>> {
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 {