diff --git a/crates/ui-egui/src/page_pointer.rs b/crates/ui-egui/src/page_pointer.rs index 2fa587116..ebb81ab5f 100644 --- a/crates/ui-egui/src/page_pointer.rs +++ b/crates/ui-egui/src/page_pointer.rs @@ -1,8 +1,9 @@ //! The pointer as the pages see it, read once per frame from the page view's content widget. //! //! egui knows what is drawn over the pages (a dialog, a menu, the palette, the quick bar, the -//! chrome over a page scrolled under it) and keeps a press with the widget it began on until -//! release. egui's raw pointer state knows neither, and its hover position is gone on the frame +//! chrome over a page scrolled under it) and which widget holds a drag (a panel's tab carried +//! over them), and keeps a press with the widget it began on until release. egui's raw pointer +//! state knows none of that, and its hover position is gone on the frame //! a touch or a Windows pen lifts: egui-winit sends `PointerGone` with every touch release, and //! winit reports Windows pen input as touch. So page modules read [`PagePointer`], never egui's //! pointer positions or button states; a test below keeps it that way. @@ -26,10 +27,11 @@ const MAX_EDGES: usize = 8; /// One frame's pointer, as the pages see it. #[derive(Clone, Debug, Default, PartialEq)] pub(crate) struct PagePointer { - /// Where the pointer is, while the pages are what egui has under it, or while they hold a - /// press (until the frame of its release, past a page's edge included). With a dialog open - /// only a drag the pages already have keeps it. It is egui's `interact_pos`, which still has - /// the position on the frame a touch lifts. + /// Where the pointer is, while the pages are what egui has under it and no other widget holds + /// a drag (a panel's tab carried over them), or while they hold a press (until the frame of + /// its release, past a page's edge included). With a dialog open only a drag the pages + /// already have keeps it. It is egui's `interact_pos`, which still has the position on the + /// frame a touch lifts. pub pos: Option, /// Where the press the pages hold began, also on the frame of its release when the press /// came in that same frame (egui has forgotten it by then). `None` for a press that began on @@ -59,7 +61,8 @@ impl PagePointer { // pages (egui's interact radius), so the top layer there must be the pages' own. A // dialog covers them from the frame it opens, before egui knows its layer. let layer = ui.layer_id(); - let on_top = |p: &Pos2| !modal && resp.contains_pointer() && ui.ctx().layer_id_at(*p).is_none_or(|l| l == layer); + let on_top = + |p: &Pos2| !modal && !dragged_elsewhere(ui, resp) && resp.contains_pointer() && ui.ctx().layer_id_at(*p).is_none_or(|l| l == layer); let pos = at.filter(|p| held || on_top(p)); if !held { return Self { pos, origin: None, edges: Vec::new(), primary_down, any_down }; @@ -78,6 +81,16 @@ impl PagePointer { } } +/// Whether egui gives the drag to a widget that is not the pages' (a panel's tab, a floating panel's +/// grip): egui hovers nothing else until it stops, though it still reports the pages under the +/// pointer. A widget on the pages (a form field's editor) keeps them. +fn dragged_elsewhere(ui: &egui::Ui, resp: &egui::Response) -> bool { + let ctx = ui.ctx(); + ctx.dragged_id().is_some_and(|id| { + id != resp.id && !ctx.read_response(id).is_some_and(|r| r.layer_id == resp.layer_id && resp.interact_rect.contains_rect(r.interact_rect)) + }) +} + fn first_press(edges: &[Edge]) -> Option { edges.iter().find_map(|e| match e { Edge::Press(p) => Some(*p), diff --git a/crates/ui-egui/tests/it/pointer.rs b/crates/ui-egui/tests/it/pointer.rs index f16c53f57..145446cc1 100644 --- a/crates/ui-egui/tests/it/pointer.rs +++ b/crates/ui-egui/tests/it/pointer.rs @@ -339,6 +339,52 @@ fn link_tip_stays_off_while_the_palette_is_open() { h.get_by_label("https://example.org/"); } +/// A panel's tab carried over a link shows neither its tip nor its cursor, and the link answers +/// again once the tab is let go. +#[test] +fn a_dragged_panel_tab_passes_over_a_link_without_its_tip() { + use egui::accesskit::Role; + use egui_kittest::kittest::Queryable; + let mut h = open(link_pdf(), "150", false); + h.state_mut().set_option("panel", "comments").unwrap(); + for _ in 0..4 { + h.run_steps(2); + } + let on_link = at(&h, 100.0, 340.0); + assert_eq!(cursor_at(&mut h, on_link), egui::CursorIcon::PointingHand); + h.run_steps(2); + assert!(tip_shown(&h), "the link's tip shows over an uncovered page"); + // Away from the link, then the Inspector's tab is picked up and carried over it. + h.hover_at(at(&h, 150.0, 100.0)); + h.run_steps(4); + let tab = h.get_by(|n| n.role() == Role::Tab && n.label().as_deref() == Some("Inspector")).rect().center(); + h.hover_at(tab); + h.run_steps(1); + h.drag_at(tab); + h.run_steps(1); + for i in 1..=8 { + h.hover_at(tab + (on_link - tab) * (i as f32 / 8.0)); + h.run_steps(1); + } + for _ in 0..4 { + h.run_steps(2); + assert!(h.ctx.dragged_id().is_some(), "the tab is still being dragged"); + assert!(!tip_shown(&h), "no link tip under a dragged panel tab"); + assert_ne!(h.output().platform_output.cursor_icon, egui::CursorIcon::PointingHand); + } + // Back to the tab and let go there, which leaves the layout as it was. + for i in 1..=8 { + h.hover_at(on_link + (tab - on_link) * (i as f32 / 8.0)); + h.run_steps(1); + } + h.drop_at(tab); + h.run_steps(2); + assert!(h.ctx.dragged_id().is_none()); + assert_eq!(cursor_at(&mut h, on_link), egui::CursorIcon::PointingHand, "the link answers again"); + h.run_steps(2); + assert!(tip_shown(&h), "and shows its tip"); +} + fn wheel(h: &mut Harness<'_, PdfCraftApp>, delta: egui::Vec2) { h.hover_at(egui::pos2(800.0, 500.0)); h.event(egui::Event::MouseWheel { unit: egui::MouseWheelUnit::Point, delta, modifiers: egui::Modifiers::NONE, phase: egui::TouchPhase::Move });