forked from 1b-agent/opencadstudio-upstream
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:
2 files changed
+87
-8
No files matched your search
@@ -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]
|
#[test]
|
||||||
fn docked_title_bar_double_click_floats_the_pallet_in_the_real_view() {
|
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.
|
// Building the real view is deeper than a default test thread's stack.
|
||||||
|
|||||||
+12
-8
@@ -3,10 +3,11 @@
|
|||||||
//! its bounds.
|
//! its bounds.
|
||||||
//!
|
//!
|
||||||
//! `mouse_area` reads such a pointer as gone: iced hands the layer below an
|
//! `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
|
//! overlay that wants the pointer an unavailable cursor, with no position. An
|
||||||
//! edge then hides as soon as one of its own dropdowns opens under the
|
//! auto-hiding dock edge then hid as soon as one of its own dropdowns opened
|
||||||
//! pointer, taking the dropdown with it. Here the cursor is landed before the
|
//! under the pointer, taking the dropdown with it. Here an unavailable cursor
|
||||||
//! bounds test, so only a pointer that really leaves the area exits it.
|
//! 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::layout::{self, Layout};
|
||||||
use iced::advanced::widget::{self, tree, Tree, Widget};
|
use iced::advanced::widget::{self, tree, Tree, Widget};
|
||||||
@@ -99,10 +100,13 @@ impl Widget<Message, Theme, Renderer> for HoverZone<'_> {
|
|||||||
shell,
|
shell,
|
||||||
viewport,
|
viewport,
|
||||||
);
|
);
|
||||||
// Unlike `mouse_area`, this runs for captured events too: a pointer
|
// Unlike `mouse_area`, this runs for captured events too, and a
|
||||||
// move over a dropdown is captured by the dropdown but must still
|
// cursor without a position (on an overlay) changes nothing.
|
||||||
// not read as leaving.
|
let hovered = match (event, cursor.land()) {
|
||||||
let hovered = cursor.land().is_over(layout.bounds());
|
(Event::Mouse(mouse::Event::CursorLeft), _) => false,
|
||||||
|
(_, mouse::Cursor::Available(p)) => layout.bounds().contains(p),
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
let state = tree.state.downcast_mut::<State>();
|
let state = tree.state.downcast_mut::<State>();
|
||||||
if hovered != state.hovered {
|
if hovered != state.hovered {
|
||||||
state.hovered = hovered;
|
state.hovered = hovered;
|
||||||
|
|||||||
Reference in new issue
Block a user