From 3da81571a13cf9f94e5cb1ad7b6ee2ad107c4732 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 1 Jul 2026 00:45:23 +0300 Subject: [PATCH] fix(ribbon): scrollable layer dropdown, stop mouse leak to viewport (#227) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ribbon layer dropdown grew unbounded so a long layer list ran off the bottom of the screen with no way to scroll. Cap the panel height and make the list scrollable; short lists still shrink to fit. While a dropdown was open, the 3D viewport beneath it kept tracking the cursor over the panel's empty areas, and the OS cursor vanished there. In iced 0.14 mouse_area/opaque only capture button presses, never CursorMoved, so a higher stack layer can't swallow motion — gate it state-side instead: - ignore viewport motion in on_viewport_move while ribbon.open_dropdown is set - suppress the crosshair overlay's Hidden cursor (and crosshair draw) so the normal OS cursor shows over an open dropdown - route all ribbon dropdown backdrops through a shared dropdown_backdrop() Co-Authored-By: Claude Opus 4.8 --- src/app/update/viewport.rs | 9 ++++++ src/app/view/mod.rs | 1 + src/ui/overlay.rs | 14 ++++++++- src/ui/ribbon/mod.rs | 59 +++++++++++++++----------------------- 4 files changed, 46 insertions(+), 37 deletions(-) diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 78df28c52..ecf63bdf7 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -482,6 +482,15 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { } pub(super) fn on_viewport_move(&mut self, p: Point) -> Task { + // A ribbon dropdown is open over the viewport. Its backdrop + // cannot swallow cursor motion — in iced 0.14 mouse_area/opaque + // capture only button presses, never CursorMoved — so the move + // 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() { + return Task::none(); + } let i = self.active_tab; // UCS icon grip drag: map the cursor onto the UCS plane and diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index c4e6e9b5f..963a12666 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -401,6 +401,7 @@ impl OpenCADStudio { pane_move_rect, pane_drop_rect, tab.pan_mode, + self.ribbon.open_dropdown.is_some(), ) }; diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 324b13f5d..6debb659b 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -189,6 +189,7 @@ pub fn selection_overlay<'a>( pane_move_rect: Option, pane_drop_rect: Option, pan_mode: bool, + suppressed: bool, ) -> Element<'a, Message> { canvas(SelectionCanvas { selection, @@ -202,6 +203,7 @@ pub fn selection_overlay<'a>( pane_move_rect, pane_drop_rect, pan_mode, + suppressed, }) .width(Length::Fill) .height(Length::Fill) @@ -230,6 +232,10 @@ struct SelectionCanvas { /// Interactive PAN mode: the crosshair is hidden and the cursor becomes a /// hand so the viewport reads as a draggable surface. pan_mode: bool, + /// A ribbon dropdown (or similar overlay) is open over the viewport. The + /// crosshair is not drawn and the OS cursor is shown normally so the panel + /// is usable instead of the cursor vanishing over it. (#227) + suppressed: bool, } impl SelectionCanvas { @@ -259,6 +265,12 @@ impl canvas::Program for SelectionCanvas { bounds: iced::Rectangle, cursor: mouse::Cursor, ) -> mouse::Interaction { + // A dropdown/overlay is open over the viewport — show the normal OS + // cursor over the whole canvas instead of hiding it for the crosshair, + // so the cursor doesn't vanish while using the panel. (#227) + if self.suppressed { + return mouse::Interaction::default(); + } // PAN mode owns the whole viewport: an open hand when hovering, a // closed hand while dragging. if self.pan_mode && cursor.is_over(bounds) { @@ -826,7 +838,7 @@ impl canvas::Program for SelectionCanvas { // top of it would double up the visual feedback. let over_divider = self.divider_under(cursor, bounds); // PAN mode replaces the crosshair with a hand cursor. - if !over_viewcube && !over_divider && !self.pan_mode { + if !over_viewcube && !over_divider && !self.pan_mode && !self.suppressed { if let Some(cp) = self.selection.last_move_pos { let color = Color { r: 0.85, diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index cfb71ac01..a13fb35d4 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -64,6 +64,16 @@ pub struct LayerInfo { pub locked: bool, } +/// Full-screen backdrop for an open ribbon dropdown: closes the dropdown when +/// the user clicks outside the panel. (Cursor motion leaking to the viewport +/// beneath is blocked in `on_viewport_move`, not here — iced 0.14's mouse_area +/// can only capture button presses, never CursorMoved. #227.) +fn dropdown_backdrop<'a>(positioned: Element<'a, Message>) -> Element<'a, Message> { + mouse_area(positioned) + .on_press(Message::CloseRibbonDropdown) + .into() +} + impl Ribbon { pub fn new() -> Self { Self { @@ -547,11 +557,7 @@ impl Ribbon { .width(Fill) .height(Fill); - return Some( - mouse_area(positioned) - .on_press(Message::CloseRibbonDropdown) - .into(), - ); + return Some(dropdown_backdrop(positioned.into())); } if open_id == LAYER_COMBO_ID { @@ -677,11 +683,7 @@ impl Ribbon { .width(Fill) .height(Fill); - Some( - mouse_area(positioned) - .on_press(Message::CloseRibbonDropdown) - .into(), - ) + Some(dropdown_backdrop(positioned.into())) } fn layer_combo_overlay(&self) -> Option> { @@ -774,7 +776,12 @@ impl Ribbon { }) .collect(); - let panel = container(column(rows)) + // Cap the panel height and make the list scrollable so a long layer + // list stays reachable instead of running off the bottom of the + // screen (#227). Short lists shrink to fit; longer ones scroll. + let row_count = self.layer_infos.len().max(1); + let list_h = (row_count as f32 * 26.0).min(420.0); + let panel = container(scrollable(column(rows)).height(Length::Fixed(list_h))) .style(|_: &Theme| container::Style { background: Some(Background::Color(PANEL_BG)), border: Border { @@ -800,11 +807,7 @@ impl Ribbon { .width(Fill) .height(Fill); - Some( - mouse_area(positioned) - .on_press(Message::CloseRibbonDropdown) - .into(), - ) + Some(dropdown_backdrop(positioned.into())) } /// Floating popup for an annotate-tab style combo (text / dimension / @@ -927,11 +930,7 @@ impl Ribbon { .width(Fill) .height(Fill); - Some( - mouse_area(positioned) - .on_press(Message::CloseRibbonDropdown) - .into(), - ) + Some(dropdown_backdrop(positioned.into())) } fn prop_color_overlay(&self) -> Option> { @@ -970,11 +969,7 @@ impl Ribbon { .width(Fill) .height(Fill); - Some( - mouse_area(positioned) - .on_press(Message::CloseRibbonDropdown) - .into(), - ) + Some(dropdown_backdrop(positioned.into())) } fn prop_linetype_overlay(&self) -> Option> { @@ -1063,11 +1058,7 @@ impl Ribbon { .width(Fill) .height(Fill); - Some( - mouse_area(positioned) - .on_press(Message::CloseRibbonDropdown) - .into(), - ) + Some(dropdown_backdrop(positioned.into())) } fn prop_lw_overlay(&self) -> Option> { @@ -1143,11 +1134,7 @@ impl Ribbon { .width(Fill) .height(Fill); - Some( - mouse_area(positioned) - .on_press(Message::CloseRibbonDropdown) - .into(), - ) + Some(dropdown_backdrop(positioned.into())) } }