From 7b6e592cb45911a8d6a975159530cc18aaa0d06e Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Tue, 29 Sep 2026 13:50:12 +0100 Subject: [PATCH] Keep drag source focused until pointer leaves release window --- compositor/src/input.rs | 52 +++++++++++++++++++++++++++++++++++++++-- compositor/src/state.rs | 20 +++++++++++++--- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/compositor/src/input.rs b/compositor/src/input.rs index 12d83b8..3b6c66b 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -25,6 +25,17 @@ use smithay::{ }; use crate::{Ferese, config::BindingAction}; +use ferese_layout::WindowId; + +fn hover_blocked_after_drag(blocked: &mut Option, target: Option) -> bool { + if let Some(window) = *blocked { + if target == Some(window) { + return true; + } + *blocked = None; + } + false +} impl Ferese { pub fn process_input_event(&mut self, event: InputEvent) { @@ -279,6 +290,12 @@ impl Ferese { InputEvent::PointerButton { event, .. } => { let pointer = self.seat.get_pointer().expect("seat has a pointer"); let serial = SERIAL_COUNTER.next_serial(); + let released_over_another_window = (event.state() == ButtonState::Released + && pointer.is_grabbed()) + .then(|| self.window_under_visual(pointer.current_location())) + .flatten() + .and_then(|window| self.window_ids.get(&window).copied()) + .filter(|window| Some(*window) != self.focused_window); // Native popup_done destroys Iced's window immediately. For // effects-capable shell popups, release input now but defer @@ -337,6 +354,7 @@ impl Ferese { } if event.state() == ButtonState::Pressed && !pointer.is_grabbed() { + self.hover_focus_blocked = None; self.focus_window_at(pointer.current_location(), serial, true); } @@ -350,6 +368,9 @@ impl Ferese { }, ); pointer.frame(self); + if event.state() == ButtonState::Released { + self.hover_focus_blocked = released_over_another_window; + } } InputEvent::PointerAxis { event, .. } => { let source = event.source(); @@ -624,8 +645,19 @@ impl Ferese { { return; } + if self.layer_under(position).is_some() { + self.hover_focus_blocked = None; + return; + } - let Some(window) = self.window_under_visual(position) else { + let window = self.window_under_visual(position); + let target = window + .as_ref() + .and_then(|window| self.window_ids.get(window).copied()); + if hover_blocked_after_drag(&mut self.hover_focus_blocked, target) { + return; + } + let Some(window) = window else { return; }; if self.window_ids.get(&window).copied() == self.focused_window { @@ -797,7 +829,23 @@ fn overview_escape(symbol: u32, overview_active: bool) -> bool { #[cfg(test)] mod tests { - use super::{emergency_shortcut_escape, overview_escape, virtual_terminal}; + use super::{ + WindowId, emergency_shortcut_escape, hover_blocked_after_drag, overview_escape, + virtual_terminal, + }; + + #[test] + fn drag_release_does_not_turn_motion_inside_the_same_window_into_hover_focus() { + let selected = WindowId(1); + let other = WindowId(2); + let mut blocked = Some(other); + + assert!(hover_blocked_after_drag(&mut blocked, Some(other))); + assert!(hover_blocked_after_drag(&mut blocked, Some(other))); + assert!(!hover_blocked_after_drag(&mut blocked, Some(selected))); + assert_eq!(blocked, None); + assert!(!hover_blocked_after_drag(&mut blocked, Some(other))); + } #[test] fn escape_closes_only_an_active_overview() { diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 947ce56..a764f25 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -147,6 +147,7 @@ pub struct Ferese { viewport_coupled_widths: HashMap, pending_column_width_cycles: HashSet, pub focused_window: Option, + pub(crate) hover_focus_blocked: Option, column_width_presets: Vec, gap_config: GapConfig, pub(crate) input_settings: InputSettings, @@ -360,6 +361,7 @@ impl Ferese { viewport_coupled_widths: HashMap::new(), pending_column_width_cycles: HashSet::new(), focused_window: None, + hover_focus_blocked: None, column_width_presets: config.column_width_presets, gap_config: config.gap_config, input_settings: config.input_settings, @@ -862,11 +864,23 @@ impl Ferese { let visual = self.presented_window_rect(*id)?; let caption_height = if overview_active { 34.0 } else { 0.0 }; - (position.x >= visual.x + let inside_visual = position.x >= visual.x && position.y >= visual.y && position.x < visual.x + visual.width - && position.y < visual.y + visual.height + caption_height) - .then_some(window.clone()) + && position.y < visual.y + visual.height + caption_height; + if !inside_visual { + return None; + } + + if !overview_active { + let (source_x, source_y) = + self.inverse_presented_window_point(*id, position.x, position.y)?; + let source_point = + Point::from((source_x, source_y)) + window.geometry().loc.to_f64(); + window.surface_under(source_point, WindowSurfaceType::ALL)?; + } + + Some(window.clone()) }; if overview_active {