From ce1091cea5e84a3c8161b6ef7635d62094f7e961 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 28 Jul 2026 01:25:39 +0300 Subject: [PATCH] refactor(ui): use pin for anchored overlays --- src/app/view/mod.rs | 97 +++++++++++++------------------------ src/app/view/overlay.rs | 13 +---- src/app/view/viewcube.rs | 16 ++---- src/ui/mod.rs | 11 +++++ src/ui/popup/cycle_popup.rs | 12 +---- 5 files changed, 51 insertions(+), 98 deletions(-) diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 0704495f8..18f585c64 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -742,18 +742,12 @@ impl OpenCADStudio { ]) .report_width0(self.render_bar_w.clone()) .into(); - // Position the bar at the active model tile's top-left corner so it - // follows the active panel in a tiled layout (full canvas when a - // single tile fills the window). Leading Spaces offset it. - let bar_layer = column![ - Space::new().height(iced::Length::Fixed(rect.y.max(0.0))), - row![ - Space::new().width(iced::Length::Fixed(rect.x.max(0.0))), - container(adaptive).width(iced::Length::Fixed(rect.width.max(1.0))), - ], - ] - .width(Fill) - .height(Fill); + // 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), + container(adaptive).width(iced::Length::Fixed(rect.width.max(1.0))), + ); viewport_stack = viewport_stack.push(bar_layer); } @@ -802,12 +796,7 @@ impl OpenCADStudio { }, ..Default::default() }); - let border_layer = column![ - Space::new().height(iced::Length::Fixed(y)), - row![Space::new().width(iced::Length::Fixed(x)), border_frame,], - ] - .width(Fill) - .height(Fill); + let border_layer = crate::ui::pin_at(iced::Point::new(x, y), border_frame); viewport_stack = viewport_stack.push(border_layer); let vp_mode = tab @@ -833,15 +822,10 @@ impl OpenCADStudio { ]) .report_width0(self.render_bar_w.clone()) .into(); - let picker_layer = column![ - Space::new().height(iced::Length::Fixed(y + 4.0)), - row![ - Space::new().width(iced::Length::Fixed(x + 4.0)), - container(adaptive).width(iced::Length::Fixed(rect.width.max(1.0))), - ], - ] - .width(Fill) - .height(Fill); + let picker_layer = crate::ui::pin_at( + iced::Point::new(x + 4.0, y + 4.0), + container(adaptive).width(iced::Length::Fixed(rect.width.max(1.0))), + ); viewport_stack = viewport_stack.push(picker_layer); // Hide the ViewCube first — before the render bar — when they collide. @@ -849,15 +833,10 @@ 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 = column![ - Space::new().height(iced::Length::Fixed(cube_y)), - row![ - Space::new().width(iced::Length::Fixed(cube_x)), - viewcube_nav_controls(), - ], - ] - .width(Fill) - .height(Fill); + let controls = crate::ui::pin_at( + iced::Point::new(cube_x, cube_y), + viewcube_nav_controls(), + ); viewport_stack = viewport_stack.push(controls); let ucs_current = tab @@ -873,16 +852,13 @@ impl OpenCADStudio { .map(|u| u.name.clone()) .filter(|n| !n.is_empty()) .collect(); - let picker = column![ - Space::new().height(iced::Length::Fixed(cube_y + VIEWCUBE_HIT_SIZE + 6.0)), - row![ - Space::new() - .width(iced::Length::Fixed(cube_x + VIEWCUBE_HIT_SIZE * 0.5 - UCS_PICKER_W * 0.5)), - iced::widget::opaque(viewcube_ucs_picker(ucs_current, ucs_names)), - ], - ] - .width(Fill) - .height(Fill); + let picker = crate::ui::pin_at( + 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); } } @@ -897,15 +873,10 @@ 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 = column![ - Space::new().height(iced::Length::Fixed(cube_y)), - row![ - Space::new().width(iced::Length::Fixed(cube_x)), - viewcube_nav_controls(), - ], - ] - .width(Fill) - .height(Fill); + let controls = crate::ui::pin_at( + iced::Point::new(cube_x, cube_y), + viewcube_nav_controls(), + ); viewport_stack = viewport_stack.push(controls); // WCS / named-UCS selector under the cube. @@ -922,15 +893,13 @@ impl OpenCADStudio { .map(|u| u.name.clone()) .filter(|n| !n.is_empty()) .collect(); - let picker = column![ - Space::new().height(iced::Length::Fixed(cube_y + VIEWCUBE_HIT_SIZE + 6.0)), - row![ - Space::new().width(iced::Length::Fixed(cube_x + VIEWCUBE_HIT_SIZE * 0.5 - UCS_PICKER_W * 0.5)), - iced::widget::opaque(viewcube_ucs_picker(ucs_current, ucs_names)), - ], - ] - .width(Fill) - .height(Fill); + let picker = crate::ui::pin_at( + 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); } diff --git a/src/app/view/overlay.rs b/src/app/view/overlay.rs index c96449016..660b5a8b3 100644 --- a/src/app/view/overlay.rs +++ b/src/app/view/overlay.rs @@ -9,18 +9,7 @@ pub(super) fn position_canvas_overlay<'a>( anchor: iced::Point, panel: Element<'a, Message>, ) -> Element<'a, Message> { - let ax = anchor.x.max(0.0); - let ay = anchor.y.max(0.0); - column![ - Space::new().height(iced::Length::Fixed(ay)), - row![ - Space::new().width(iced::Length::Fixed(ax)), - iced::widget::opaque(panel), - ], - ] - .width(Fill) - .height(Fill) - .into() + crate::ui::pin_at(anchor, iced::widget::opaque(panel)) } // ── In-place MText editor overlay ─────────────────────────────────────────── diff --git a/src/app/view/viewcube.rs b/src/app/view/viewcube.rs index 495519fd5..6f1c686b0 100644 --- a/src/app/view/viewcube.rs +++ b/src/app/view/viewcube.rs @@ -1,10 +1,9 @@ use super::super::Message; use crate::scene::{VIEWCUBE_PX, VIEWCUBE_REGION_PX}; use iced::widget::{ - button, column, container, mouse_area, pick_list, row, stack, - Space, + button, container, mouse_area, pick_list, stack, Space, }; -use iced::{Background, Border, Color, Element, Fill, Theme}; +use iced::{Background, Border, Color, Element, Theme}; // ── Render-mode picker ────────────────────────────────────────────────────── @@ -15,15 +14,9 @@ use iced::{Background, Border, Color, Element, Fill, Theme}; /// state and emit `ToggleGrid` / `ToggleGridSnap`. // ── ViewCube navigation controls (home / roll / nudge / UCS) ─────────────── -/// Place `el` at pixel offset (x, y) inside a Fill layer (top-left origin). +/// 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> { - column![ - Space::new().height(iced::Length::Fixed(y.max(0.0))), - row![Space::new().width(iced::Length::Fixed(x.max(0.0))), el], - ] - .width(Fill) - .height(Fill) - .into() + crate::ui::pin_at(iced::Point::new(x, y), el) } /// Borderless square icon button used by the ViewCube nav controls. @@ -198,4 +191,3 @@ pub(super) fn viewcube_ucs_picker<'a>(current: String, names: Vec) -> El }) .into() } - diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 38640b8d1..d6ce49f54 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -2,6 +2,17 @@ /// Change this to scale the ribbon, layer manager rows, and property panel rows uniformly. pub const ROW_H: f32 = 26.0; +/// 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/popup/cycle_popup.rs b/src/ui/popup/cycle_popup.rs index 2f0f0f273..e04440baa 100644 --- a/src/ui/popup/cycle_popup.rs +++ b/src/ui/popup/cycle_popup.rs @@ -2,7 +2,7 @@ //! two or more overlapping objects. Each row names a candidate; clicking it //! adds that object to the current selection. Clicking outside dismisses it. -use iced::widget::{button, column, container, mouse_area, opaque, row, text, Space}; +use iced::widget::{button, column, container, mouse_area, opaque, text}; use iced::{Background, Border, Color, Element, Fill, Length, Theme}; use crate::app::Message; @@ -30,15 +30,7 @@ pub fn cycle_popup_overlay( }) .width(Length::Fixed(150.0)); - let positioned = column![ - Space::new().height(Length::Fixed(anchor.y.max(0.0))), - row![ - Space::new().width(Length::Fixed(anchor.x.max(0.0))), - opaque(panel), - ], - ] - .width(Fill) - .height(Fill); + let positioned = crate::ui::pin_at(anchor, opaque(panel)); mouse_area(positioned).on_press(Message::CycleCancel).into() }