From 6845cb95f26d04f7bb7d0b5aa89766b4e01862ec Mon Sep 17 00:00:00 2001 From: Giang Nguyen <33104857+giang17@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:49:47 +0200 Subject: [PATCH 1/2] Title bar: a window move the window manager finished no longer blocks the next move or resize (#5725) --- crates/ui-egui/src/panels.rs | 2 +- crates/ui-egui/src/titlebar.rs | 61 ++++++++++++++++++++++++++++------ 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index 522c74dc15..e374217e78 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -596,7 +596,7 @@ pub fn title_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) { ui.ctx().data_mut(|d| d.insert_temp(crate::titlebar::modal_drag_area_id(), gap)); let drag = ui.interact(gap, ui.id().with("titledrag"), Sense::click_and_drag()); if drag.drag_started() { - ui.ctx().send_viewport_cmd(egui::ViewportCommand::StartDrag); + crate::titlebar::start_os_move(ui.ctx()); } if drag.double_clicked() { let max = ui.ctx().input(|i| i.viewport().maximized.unwrap_or(false)); diff --git a/crates/ui-egui/src/titlebar.rs b/crates/ui-egui/src/titlebar.rs index 25b23ee0f6..88d8714e0d 100644 --- a/crates/ui-egui/src/titlebar.rs +++ b/crates/ui-egui/src/titlebar.rs @@ -68,7 +68,7 @@ pub(crate) fn modal_drag_overlay(ctx: &egui::Context) { egui::Area::new(Id::new("titlebar-modal-drag-overlay")).order(Order::Tooltip).fixed_pos(area.min).movable(false).sense(Sense::hover()).show(ctx, |ui| { let response = ui.allocate_rect(area, Sense::click_and_drag()); if response.drag_started() { - ctx.send_viewport_cmd(ViewportCommand::StartDrag); + start_os_move(ctx); } if response.double_clicked() { let max = ctx.input(|i| i.viewport().maximized.unwrap_or(false)); @@ -266,7 +266,7 @@ pub fn caption_buttons(app: &mut PhotocraftApp, ui: &mut Ui, bar: Rect) { for (c, r, centre) in layout(&s, bar, bar.left().max(content.left()), bar.right().min(content.right())) { let resp = ui.interact(r, caption_id(c), if windows { Sense::click() } else { Sense::click_and_drag() }); if resp.drag_started_by(PointerButton::Primary) { - ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag); + start_os_move(ui.ctx()); } let pressed = resp.is_pointer_button_down_on() && resp.hovered(); if windows { @@ -468,7 +468,7 @@ fn resize_handed_off_id() -> Id { Id::new("titlebar-resize-handed-off") } -/// An OS resize started at `pass`; `released` is the pass whose input ended the press +/// An OS resize or move started at `pass`; `released` is the pass whose input ended the press /// ([`release_after_os_resize`]). #[derive(Clone, Copy, Debug)] struct HandOff { @@ -476,11 +476,26 @@ struct HandOff { released: Option, } -/// The compositor takes the button release of an OS resize (KDE Wayland), so egui kept the edge -/// pressed: the zones stopped hovering until the next click, every other resize (#1581). End the -/// press ourselves on the input after the hand-off, but only while egui still has it down and this -/// input brings no release of its own: where the OS does send it (Windows, X11), nothing is added, -/// so no second release can turn into a click. Call from `raw_input_hook`. +/// The OS took the press at this pass: [`release_after_os_resize`] ends it on the next input. +fn hand_off(ctx: &egui::Context) { + let pass = ctx.cumulative_pass_nr(); + ctx.data_mut(|d| d.insert_temp(resize_handed_off_id(), HandOff { pass, released: None })); +} + +/// Move the window from this press. The window manager takes the press of an OS move as it does +/// that of a resize and keeps its release, so egui kept the drag: every other move failed, and a +/// resize right after one (#5725). The same hand-off ends it. +pub(crate) fn start_os_move(ctx: &egui::Context) { + ctx.send_viewport_cmd(ViewportCommand::StartDrag); + hand_off(ctx); +} + +/// The compositor takes the button release of an OS resize or move (KDE Wayland, KWin on X11), so +/// egui kept the edge pressed: the zones stopped hovering until the next click, every other resize +/// (#1581, and #5725 for moves). End the press ourselves on the input after the hand-off, but only +/// while egui still has it down and this input brings no release of its own: where the OS does send +/// it (Windows), nothing is added, so no second release can turn into a click. Call from +/// `raw_input_hook`. pub fn release_after_os_resize(ctx: &egui::Context, raw: &mut egui::RawInput) { let Some(h) = ctx.data(|d| d.get_temp::(resize_handed_off_id())) else { return }; if h.released.is_some() || h.pass >= ctx.cumulative_pass_nr() { @@ -524,8 +539,7 @@ pub fn resize_zones(ui: &mut Ui) { let resp = zui.interact(r, Id::new(("titlebar-resize", i)), Sense::drag()).on_hover_cursor(cursor); if resp.drag_started_by(PointerButton::Primary) { ctx.send_viewport_cmd(ViewportCommand::BeginResize(dir)); - let pass = ctx.cumulative_pass_nr(); - ctx.data_mut(|d| d.insert_temp(resize_handed_off_id(), HandOff { pass, released: None })); + hand_off(&ctx); } } // After the zones, so its one-frame cursor wins over theirs. @@ -795,7 +809,7 @@ mod tests { assert!(w.gesture(&[p, p + vec2(6.0, 0.0)]).contains(&ViewportCommand::BeginResize(ResizeDirection::West))); } - /// Where the OS does send the release of a resize (Windows, X11), no second one is added: a + /// Where the OS does send the release of a resize (Windows), no second one is added: a /// release right after it could count as a click on what lies under the edge. #[test] fn a_release_the_os_sends_is_not_doubled() { @@ -818,6 +832,31 @@ mod tests { assert!(raw.events.is_empty(), "{:?}", raw.events); } + /// #5725: the window manager keeps the button release of an OS move just as of a resize, so + /// egui kept the drag: every other move of the window failed, and so did a resize right after one. + #[test] + fn after_an_os_move_the_bar_and_the_edges_take_the_next_press() { + /// Press and drag along `path`; the window manager takes over, so no release ever arrives. + fn handed_off(w: &mut Win, path: &[Pos2]) -> Vec { + let mut events = vec![vec![Event::PointerMoved(path[0])], vec![button(path[0], true)]]; + events.extend(path[1..].iter().map(|p| vec![Event::PointerMoved(*p)])); + events.push(vec![]); + events.into_iter().flat_map(|e| w.frame(e).viewport_output.remove(&ViewportId::ROOT).map(|v| v.commands).unwrap_or_default()).collect() + } + let moves = |cmds: &[ViewportCommand]| cmds.iter().filter(|c| matches!(c, ViewportCommand::StartDrag)).count(); + let mut w = Win::new(1440.0); + w.frame(vec![]); + let empty = pos2(980.0, 16.0); + assert!(w.bar_widgets().iter().all(|r| !r.contains(empty)), "the probe point must be empty bar space"); + let drag = [empty, empty + vec2(12.0, 4.0), empty + vec2(30.0, 8.0)]; + assert_eq!(moves(&handed_off(&mut w, &drag)), 1); + assert!(!w.ctx.input(|i| i.pointer.primary_down()), "the press ends with the hand-off"); + assert_eq!(moves(&w.gesture(&drag)), 1, "the next move"); + assert_eq!(moves(&handed_off(&mut w, &drag)), 1); + let edge = pos2(3.0, 300.0); + assert!(w.gesture(&[edge, edge + vec2(6.0, 0.0)]).contains(&ViewportCommand::BeginResize(ResizeDirection::West)), "a resize after a move"); + } + #[test] fn edge_zones_resize_unless_maximized() { use ResizeDirection::*; From ab9e32c01d16c120b3e8a7e6eef48b69c35bc989 Mon Sep 17 00:00:00 2001 From: Giang Nguyen <33104857+giang17@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:49:47 +0200 Subject: [PATCH 2/2] contributors: credit giang17 as Giang Nguyen --- contributors/people.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contributors/people.toml b/contributors/people.toml index 8694c72698..944a872f3a 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -162,3 +162,6 @@ sync_github_name = true [people.sidney] real_name = "Sidney Markowitz" sync_github_name = true + +[people.giang17] +real_name = "Giang Nguyen"