fix(ui): the page ignores the pointer while a panel is being dragged (#4581)

This commit is contained in:
aiongg
2026-10-24 22:12:46 -04:00
committed by GitHub
parent 24edde6563
commit 418ac6a2af
2 changed files with 66 additions and 7 deletions
+20 -7
View File
@@ -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<Pos2>,
/// 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<Pos2> {
edges.iter().find_map(|e| match e {
Edge::Press(p) => Some(*p),
+46
View File
@@ -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 });