From a9e6069f32a67dca7fda6e4f8570b304275f5bec Mon Sep 17 00:00:00 2001 From: bmh129 <11478682+bmh129@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:52:00 -0500 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01Wrwtfrnom6Eg1Jt7xZ7VA8 --- crates/ui-egui/src/chrome.rs | 2 +- crates/ui-egui/src/lib.rs | 2 + crates/ui-egui/src/window_controls.rs | 59 ++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index 5e8de9375..2723d16c2 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -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)); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index af9912f45..9dcee8cac 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -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() { diff --git a/crates/ui-egui/src/window_controls.rs b/crates/ui-egui/src/window_controls.rs index eacf854c2..838ce29db 100644 --- a/crates/ui-egui/src/window_controls.rs +++ b/crates/ui-egui/src/window_controls.rs @@ -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"); + } }