fix(ribbon): scrollable layer dropdown, stop mouse leak to viewport (#227)

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 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-01 00:45:23 +03:00
commit 3da81571a1
4 changed files with 46 additions and 37 deletions

View file

@ -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<Element<'_, Message>> {
@ -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<Element<'_, Message>> {
@ -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<Element<'_, Message>> {
@ -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<Element<'_, Message>> {
@ -1143,11 +1134,7 @@ impl Ribbon {
.width(Fill)
.height(Fill);
Some(
mouse_area(positioned)
.on_press(Message::CloseRibbonDropdown)
.into(),
)
Some(dropdown_backdrop(positioned.into()))
}
}