M3: give the kept button release back before the pointer leaves (Wayland) (#4500)

* M3: give the kept button release back before the pointer leaves (Wayland)

After a desktop grab the next input on Wayland often brings only
PointerGone. The synthetic release went after it, and a button event
sets egui's pointer position, so the gone pointer came back and hover
stuck at the old place until the pointer moved. The release now goes
before the first PointerGone as well as before the first press; a
release that follows PointerGone is still the kept one and not doubled.
Found by CodeRabbit in review of #3970.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rPQFoRmQbw8vWhGgGVrRs

* M3: move a release that follows the pointer leaving in front of it

A release the input brings after PointerGone is the one the desktop kept.
Left after PointerGone, it brought the gone pointer back at the release
position; it now moves in front, still not doubled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rPQFoRmQbw8vWhGgGVrRs

---------

Co-authored-by: bmh129 <11478682+bmh129@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bmh129
2026-10-25 10:01:56 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 bmh129
parent 3f5d8d06fe
commit f0eccde407
3 changed files with 81 additions and 10 deletions
+1
View File
@@ -199,6 +199,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
## Progress log
- **2026-10-24 (M3, window move on Wayland, #3970 follow-up):** After a move or resize by PdfCraft's own title bar or edges, the button release given back to egui goes before the pointer leaving the window, so hover no longer sticks at the old place until the pointer moves. Found by CodeRabbit in review of #3970; a unit test covers it. Estimates unchanged.
- **2026-10-24 (M14, #4253):** The annotation quick bar remembers Keep tool selected across restarts, including unpinning. Old and malformed settings keep safe defaults; the opt-in UI control channel reads and changes the same preference. Estimates unchanged.
- **2026-10-23 (M7, #4280):** Edit text names each standard stand-in font after its face (`PCEdTimesBold`, ...) instead of one shared `PCEdHelv`. Rewriting a bold paragraph whose subset lacks an accent no longer repoints the paragraphs set earlier in a regular stand-in at Times-Bold, and the editor's frame stays on the paragraph being edited. Pages saved by older builds keep their shared name and are not repaired. Estimates unchanged.
+79 -10
View File
@@ -248,20 +248,41 @@ pub fn desktop_grab(ctx: &egui::Context, command: egui::ViewportCommand) {
/// After `desktop_grab`, add the primary button's release to the next input, unless it brings one.
/// The release goes before the input's first press (the next resize or move, or a click), which
/// egui would otherwise end straight away; a release after that press is the press's own.
/// egui would otherwise end straight away; a release after that press is the press's own. It also
/// goes before the pointer leaving, as a button event would bring the gone pointer back; a release
/// the input brings after the pointer left is the grab's own, and moves there instead.
pub fn release_after_desktop_grab(ctx: &egui::Context, raw: &mut egui::RawInput) {
if ctx.data_mut(|d| d.remove_temp::<()>(grab_id())).is_none() {
return;
}
let first = raw.events.iter().enumerate().find_map(|(n, e)| match e {
egui::Event::PointerButton { button: egui::PointerButton::Primary, pressed, .. } => Some((n, *pressed)),
_ => None,
});
let at = match first {
Some((_, false)) => return,
Some((n, true)) => n,
None => raw.events.len(),
};
let mut at = raw.events.len();
let mut gone = None;
let mut brought = None;
for (n, e) in raw.events.iter().enumerate() {
match e {
egui::Event::PointerButton { button: egui::PointerButton::Primary, pressed: false, .. } => {
brought = Some(n);
break;
}
egui::Event::PointerButton { button: egui::PointerButton::Primary, pressed: true, .. } => {
at = n;
break;
}
egui::Event::PointerGone => {
gone.get_or_insert(n);
}
_ => {}
}
}
if let Some(n) = brought {
// `gone` was found before `n`, so both are in range.
if let Some(g) = gone {
let release = raw.events.remove(n);
raw.events.insert(g, release);
}
return;
}
let at = gone.unwrap_or(at);
let (pos, modifiers) = ctx.input(|i| (i.pointer.interact_pos().unwrap_or_default(), i.modifiers));
raw.events.insert(at, egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, modifiers });
}
@@ -389,4 +410,52 @@ mod tests {
frame(vec![], Some(egui::ViewportCommand::StartDrag));
assert_eq!(frame(vec![button(true), button(false)], None), [false, true, false]);
}
#[test]
fn the_release_goes_before_the_pointer_leaving() {
let ctx = egui::Context::default();
let frame = |events: Vec<egui::Event>, grab: bool| {
let mut raw = egui::RawInput { events, ..Default::default() };
super::release_after_desktop_grab(&ctx, &mut raw);
let order = raw.events.clone();
ctx.run_ui(raw, |ui| {
if grab {
super::desktop_grab(ui.ctx(), egui::ViewportCommand::StartDrag);
}
})
.textures_delta
.clear();
order
};
let release = |e: &egui::Event| matches!(e, egui::Event::PointerButton { button: egui::PointerButton::Primary, pressed: false, .. });
let press = egui::Event::PointerButton {
pos: Pos2::new(5.0, 5.0),
button: egui::PointerButton::Primary,
pressed: true,
modifiers: egui::Modifiers::NONE,
};
// On Wayland the input after a move brings only that the pointer left.
frame(vec![egui::Event::PointerMoved(Pos2::new(5.0, 5.0)), press], true);
let order = frame(vec![egui::Event::PointerGone], false);
assert!(matches!(order.as_slice(), [r, egui::Event::PointerGone] if release(r)), "{order:?}");
assert_eq!(ctx.input(|i| i.pointer.latest_pos()), None, "the pointer stays gone");
assert!(!ctx.input(|i| i.pointer.primary_down()));
// A release after the pointer left is the one the desktop kept, not to be doubled.
frame(vec![], true);
let order = frame(
vec![
egui::Event::PointerGone,
egui::Event::PointerButton {
pos: Pos2::new(5.0, 5.0),
button: egui::PointerButton::Primary,
pressed: false,
modifiers: egui::Modifiers::NONE,
},
],
false,
);
assert!(matches!(order.as_slice(), [r, egui::Event::PointerGone] if release(r)), "moved, not doubled: {order:?}");
assert_eq!(ctx.input(|i| i.pointer.latest_pos()), None, "the pointer stays gone");
}
}
+1
View File
@@ -252,6 +252,7 @@ Estimates unchanged.
| Date | Change | Summary |
|---|---|---|
| 2026-10-24 | minor | M3, #3970 follow-up: the button release given back after a window move or resize goes before the pointer leaving, so hover doesn't stick. Estimates unchanged. |
| 2026-10-23 | minor | M8, #3205 follow-up: phone-pattern redaction includes the +39 country code on labelled Italian geographic numbers with a slash; dates, protocol identifiers and whole-run guards remain. Estimates unchanged. |
| 2026-10-23 | minor | M10: Word table cells use the grid columns they span, including cells after a merged cell; synthetic export regression and CLI reproduction. Estimates unchanged. |
| 2026-10-23 | minor | M10: covered table slots retain their text in paragraph flow across Office exporters; emitted superscripts remain in their cells. Synthetic source and CLI regressions; estimates unchanged. |