M3: resize and move the window again right after a resize or move (Wayland)

ViewportCommand::BeginResize and StartDrag hand the pointer to the
compositor, which keeps the button's release on Wayland; winit reports
only that the pointer left, and egui doesn't count that as a release. egui
then still thought the primary button held, so the next press on an edge
or the tab strip didn't start a drag until a click inside the window.

window_controls::desktop_grab notes each hand-over, and
release_after_desktop_grab (called first in raw_input_hook) adds the
missed release to the next input, once, unless that input brings one.
Ported from WordCraft 1581c288, confirmed by hand there for repeated edge
resizes on GNOME/Wayland.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrwtfrnom6Eg1Jt7xZ7VA8
This commit is contained in:
bmh129
2026-10-24 00:46:06 -05:00
co-authored by Claude Opus 5.5
parent 0b670caf4a
commit a9e6069f32
3 changed files with 61 additions and 2 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
let full = ui.max_rect();
let drag = ui.interact(full, ui.id().with("titledrag"), Sense::click_and_drag());
if drag.drag_started() {
ui.ctx().send_viewport_cmd(egui::ViewportCommand::StartDrag);
window_controls::desktop_grab(ui.ctx(), egui::ViewportCommand::StartDrag);
}
if drag.double_clicked() {
let max = ui.ctx().input(|i| i.viewport().maximized.unwrap_or(false));
+2
View File
@@ -3125,6 +3125,8 @@ impl eframe::App for PdfCraftApp {
/// would keep the place the pointer entered at: tell it where the pointer really is, so the
/// page grid can show (and use) the gap under it.
fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) {
// The button's release a window move or resize kept from egui.
window_controls::release_after_desktop_grab(ctx, raw);
// Before egui reads this frame's wheel input.
self.scroll_speed.apply(ctx, raw);
if raw.hovered_files.is_empty() && raw.dropped_files.is_empty() {
+58 -1
View File
@@ -217,7 +217,7 @@ pub fn resize_edges(ctx: &egui::Context) {
let response = ui.interact(rect, ui.id().with(("edge", n)), Sense::drag()).on_hover_cursor(cursor(direction));
// Sensing only drags, a drag starts on the press itself.
if response.drag_started() {
ui.ctx().send_viewport_cmd(egui::ViewportCommand::BeginResize(direction));
desktop_grab(ui.ctx(), egui::ViewportCommand::BeginResize(direction));
}
}
// Without the desktop's frame or shadow, a hairline keeps the window apart from
@@ -231,6 +231,33 @@ pub fn resize_edges(ctx: &egui::Context) {
});
}
fn grab_id() -> egui::Id {
egui::Id::new("window_controls_desktop_grab")
}
/// Hand the pointer to the desktop to move or resize the window. On Wayland the compositor keeps
/// the button's release and winit reports only that the pointer left, which egui doesn't count as
/// a release; egui would think the button still held, and the next press on an edge or the title
/// bar wouldn't start a drag until a click inside the window. The release comes back on the next
/// input (`release_after_desktop_grab`).
pub fn desktop_grab(ctx: &egui::Context, command: egui::ViewportCommand) {
ctx.send_viewport_cmd(command);
ctx.data_mut(|d| d.insert_temp(grab_id(), ()));
ctx.request_repaint();
}
/// After `desktop_grab`, add the primary button's release to the next input, unless it brings one.
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 released = raw.events.iter().any(|e| matches!(e, egui::Event::PointerButton { button: egui::PointerButton::Primary, pressed: false, .. }));
if !released {
let (pos, modifiers) = ctx.input(|i| (i.pointer.interact_pos().unwrap_or_default(), i.modifiers));
raw.events.push(egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, modifiers });
}
}
#[cfg(test)]
mod tests {
use super::{Button, Layout, ResizeDirection, zones};
@@ -285,4 +312,34 @@ mod tests {
assert!(r.width() >= 0.0 && r.height() >= 0.0);
}
}
#[test]
fn a_desktop_grab_gives_the_button_release_back() {
let ctx = egui::Context::default();
let releases = |raw: &egui::RawInput| {
raw.events.iter().filter(|e| matches!(e, egui::Event::PointerButton { button: egui::PointerButton::Primary, pressed: false, .. })).count()
};
let mut raw = egui::RawInput::default();
super::release_after_desktop_grab(&ctx, &mut raw);
assert_eq!(releases(&raw), 0, "no grab, no release");
super::desktop_grab(&ctx, egui::ViewportCommand::StartDrag);
super::release_after_desktop_grab(&ctx, &mut raw);
assert_eq!(releases(&raw), 1, "the release the desktop kept");
let mut raw = egui::RawInput::default();
super::release_after_desktop_grab(&ctx, &mut raw);
assert_eq!(releases(&raw), 0, "only once");
super::desktop_grab(&ctx, egui::ViewportCommand::BeginResize(ResizeDirection::East));
let mut raw = egui::RawInput::default();
raw.events.push(egui::Event::PointerButton {
pos: Pos2::ZERO,
button: egui::PointerButton::Primary,
pressed: false,
modifiers: egui::Modifiers::NONE,
});
super::release_after_desktop_grab(&ctx, &mut raw);
assert_eq!(releases(&raw), 1, "not doubled when the input brings it");
}
}