diff --git a/src/app/dock/tests.rs b/src/app/dock/tests.rs index 4276df96b..653d3bfdb 100644 --- a/src/app/dock/tests.rs +++ b/src/app/dock/tests.rs @@ -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. diff --git a/src/ui/hover_zone.rs b/src/ui/hover_zone.rs index 5e9fc4ab7..c9269194e 100644 --- a/src/ui/hover_zone.rs +++ b/src/ui/hover_zone.rs @@ -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 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::(); if hovered != state.hovered { state.hovered = hovered;