fix(dock): hold the hover state while the pointer is on a dropdown

At this iced revision the layer under an overlay that wants the pointer
gets an unavailable cursor, not a levitating one, so hover_zone still
read a pointer on an open linetype list as leaving the edge. Keep the
last state on an unavailable cursor and exit only on a position outside
the bounds or the pointer leaving the window. A test drives a real open
dropdown through iced's UserInterface for both mouse_area and the zone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ihuqzxdUhd9UbioPn5MGu
This commit is contained in:
Claude committed 2026-10-15 18:54:40 +00:00
1 parent e69b1bc207
commit f17dc11e33
2 files changed
+87 -8

No files matched your search

+75
View File
@@ -582,6 +582,81 @@ fn drive_view(app: &mut OpenCADStudio, at: iced::Point, events: &[iced::Event])
}
}
/// The hover messages an open dropdown inside a hover zone (`zone`) or a
/// `mouse_area` publishes as the pointer goes from the field onto the
/// dropdown's list, then out of the area, each tagged with where the pointer
/// was.
fn hover_messages_over_an_open_dropdown(zone: bool) -> Vec<(&'static str, &'static str)> {
use crate::ui::dock::DockMsg;
use iced::widget::{button, column, container, mouse_area, text};
use iced_test::core::{mouse, shell, window};
use iced_test::runtime::user_interface::{Cache, UserInterface};
let view = || -> iced::Element<'static, Message> {
let field = button(text("ByLayer")).width(120).height(20);
// A pointer-wanting list, like a linetype combo's, under the field.
let list = button(column![text("Continuous"), text("Dashed")])
.on_press(Message::Dock(DockMsg::HoverEnd(crate::ui::dock::PanelId::Layers)))
.width(120)
.height(100);
let dd = iced_aw::DropDown::new(field, list, true)
.alignment(iced_aw::drop_down::Alignment::Bottom);
let area = container(dd).width(300).height(400);
let (enter, exit) = (
Message::Dock(DockMsg::HoverStay),
Message::Dock(DockMsg::HoverExit),
);
if zone {
crate::ui::hover_zone::hover_zone(area, enter, exit)
} else {
mouse_area(area).on_enter(enter).on_exit(exit).into()
}
};
let mut renderer = headless_renderer();
let mut cache = Cache::default();
let mut seen = Vec::new();
// On the field, on the open list (both inside the area), then outside.
for (place, x, y) in [
("field", 60.0, 10.0),
("list", 60.0, 70.0),
("list", 60.0, 90.0),
("outside", 500.0, 70.0),
] {
let at = iced::Point::new(x, y);
let mut messages = Vec::new();
let mut ui = UserInterface::build(view(), VIEW_SIZE, cache, &mut renderer);
let _ = ui.update(
&window::Headless,
&shell::Waker::noop(),
&[iced::Event::Mouse(mouse::Event::CursorMoved { position: at })],
mouse::Cursor::Available(at),
&mut renderer,
&mut messages,
);
cache = ui.into_cache();
seen.extend(messages.into_iter().filter_map(|m| match m {
Message::Dock(DockMsg::HoverStay) => Some(("enter", place)),
Message::Dock(DockMsg::HoverExit) => Some(("exit", place)),
_ => None,
}));
}
seen
}
#[test]
fn an_open_dropdown_in_a_hover_zone_does_not_read_as_leaving() {
with_stack(|| {
// The bug: a mouse area reads the pointer on the list as gone.
assert_eq!(
hover_messages_over_an_open_dropdown(false),
[("enter", "field"), ("exit", "list")]
);
assert_eq!(
hover_messages_over_an_open_dropdown(true),
[("enter", "field"), ("exit", "outside")]
);
});
}
#[test]
fn docked_title_bar_double_click_floats_the_pallet_in_the_real_view() {
// Building the real view is deeper than a default test thread's stack.
+12 -8
View File
@@ -3,10 +3,11 @@
//! its bounds.
//!
//! `mouse_area` reads such a pointer as gone: iced hands the layer below an
//! overlay a levitating cursor, which has no position. An auto-hiding dock
//! edge then hides as soon as one of its own dropdowns opens under the
//! pointer, taking the dropdown with it. Here the cursor is landed before the
//! bounds test, so only a pointer that really leaves the area exits it.
//! overlay that wants the pointer an unavailable cursor, with no position. An
//! auto-hiding dock edge then hid as soon as one of its own dropdowns opened
//! under the pointer, taking the dropdown with it. Here an unavailable cursor
//! keeps the last state, so only a pointer seen outside the bounds, or one
//! that leaves the window, exits the area.
use iced::advanced::layout::{self, Layout};
use iced::advanced::widget::{self, tree, Tree, Widget};
@@ -99,10 +100,13 @@ impl Widget<Message, Theme, Renderer> for HoverZone<'_> {
shell,
viewport,
);
// Unlike `mouse_area`, this runs for captured events too: a pointer
// move over a dropdown is captured by the dropdown but must still
// not read as leaving.
let hovered = cursor.land().is_over(layout.bounds());
// Unlike `mouse_area`, this runs for captured events too, and a
// cursor without a position (on an overlay) changes nothing.
let hovered = match (event, cursor.land()) {
(Event::Mouse(mouse::Event::CursorLeft), _) => false,
(_, mouse::Cursor::Available(p)) => layout.bounds().contains(p),
_ => return,
};
let state = tree.state.downcast_mut::<State>();
if hovered != state.hovered {
state.hovered = hovered;