From bb925be4fd3aba7daeebb82876e24f5fd7a33fc1 Mon Sep 17 00:00:00 2001 From: "Paul D. Faria" Date: Sun, 22 Dec 2024 21:20:36 -0800 Subject: [PATCH 1/3] Move pointer button press to separate function The pointer button press is being extracted to be reused in a future commit. --- src/input/mod.rs | 336 +++++++++++++++++++++++------------------------ 1 file changed, 166 insertions(+), 170 deletions(-) diff --git a/src/input/mod.rs b/src/input/mod.rs index 8b40908e..aafc2754 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -618,176 +618,13 @@ impl State { }; self.common.idle_notifier_state.notify_activity(&seat); - let current_focus = seat.get_keyboard().unwrap().current_focus(); - let shortcuts_inhibited = current_focus.is_some_and(|f| { - f.wl_surface() - .and_then(|surface| { - seat.keyboard_shortcuts_inhibitor_for_surface(&surface) - .map(|inhibitor| inhibitor.is_active()) - }) - .unwrap_or(false) - }); - - let serial = SERIAL_COUNTER.next_serial(); - let button = event.button_code(); - let mut pass_event = !seat.supressed_buttons().remove(button); - if event.state() == ButtonState::Pressed { - // change the keyboard focus unless the pointer is grabbed - // We test for any matching surface type here but always use the root - // (in case of a window the toplevel) surface for the focus. - // see: https://gitlab.freedesktop.org/wayland/wayland/-/issues/294 - if !seat.get_pointer().unwrap().is_grabbed() { - let output = seat.active_output(); - - let global_position = - seat.get_pointer().unwrap().current_location().as_global(); - let under = { - let shell = self.common.shell.read().unwrap(); - State::element_under(global_position, &output, &shell) - }; - if let Some(target) = under { - if let Some(surface) = target.toplevel().map(Cow::into_owned) { - if seat.get_keyboard().unwrap().modifier_state().logo - && !shortcuts_inhibited - { - let seat_clone = seat.clone(); - let mouse_button = PointerButtonEvent::button(&event); - - let mut supress_button = || { - // If the logo is held then the pointer event is - // aimed at the compositor and shouldn't be passed - // to the application. - pass_event = false; - seat.supressed_buttons().add(button); - }; - - fn dispatch_grab + 'static>( - grab: Option<(G, smithay::input::pointer::Focus)>, - seat: Seat, - serial: Serial, - state: &mut State, - ) { - if let Some((target, focus)) = grab { - seat.modifiers_shortcut_queue().clear(); - - seat.get_pointer() - .unwrap() - .set_grab(state, target, serial, focus); - } - } - - if let Some(mouse_button) = mouse_button { - match mouse_button { - smithay::backend::input::MouseButton::Left => { - supress_button(); - self.common.event_loop_handle.insert_idle( - move |state| { - let mut shell = - state.common.shell.write().unwrap(); - let res = shell.move_request( - &surface, - &seat_clone, - serial, - ReleaseMode::NoMouseButtons, - false, - &state.common.config, - &state.common.event_loop_handle, - &state.common.xdg_activation_state, - false, - ); - drop(shell); - dispatch_grab( - res, seat_clone, serial, state, - ); - }, - ); - } - smithay::backend::input::MouseButton::Right => { - supress_button(); - self.common.event_loop_handle.insert_idle( - move |state| { - let mut shell = - state.common.shell.write().unwrap(); - let Some(target_elem) = - shell.element_for_surface(&surface) - else { - return; - }; - let Some(geom) = - shell.space_for(target_elem).and_then( - |f| f.element_geometry(target_elem), - ) - else { - return; - }; - let geom = geom.to_f64(); - let center = - geom.loc + geom.size.downscale(2.0); - let offset = center.to_global(&output) - - global_position; - let edge = match ( - offset.x > 0.0, - offset.y > 0.0, - ) { - (true, true) => ResizeEdge::TOP_LEFT, - (false, true) => ResizeEdge::TOP_RIGHT, - (true, false) => { - ResizeEdge::BOTTOM_LEFT - } - (false, false) => { - ResizeEdge::BOTTOM_RIGHT - } - }; - let res = shell.resize_request( - &surface, - &seat_clone, - serial, - edge, - false, - ); - drop(shell); - dispatch_grab( - res, seat_clone, serial, state, - ); - }, - ); - } - _ => {} - } - } - } - } - - Shell::set_focus(self, Some(&target), &seat, Some(serial), false); - } - } - } else { - let mut shell = self.common.shell.write().unwrap(); - if let Some(Trigger::Pointer(action_button)) = - shell.overview_mode().0.active_trigger() - { - if *action_button == button { - shell.set_overview_mode(None, self.common.event_loop_handle.clone()); - } - } - std::mem::drop(shell); - }; - - let ptr = seat.get_pointer().unwrap(); - if pass_event { - ptr.button( - self, - &ButtonEvent { - button, - state: event.state(), - serial, - time: event.time_msec(), - }, - ); - ptr.frame(self); - } else if event.state() == ButtonState::Released { - ptr.unset_grab(self, serial, event.time_msec()) - } + self.process_pointer_button( + &seat, + event.button_code(), + event.state(), + event.button(), + event.time_msec(), + ) } InputEvent::PointerAxis { event, .. } => { let scroll_factor = @@ -1414,6 +1251,165 @@ impl State { } } + /// Process a button press. + fn process_pointer_button( + &mut self, + seat: &Seat, + button: u32, + button_state: smithay::backend::input::ButtonState, + mouse_button: Option, + time: u32, + ) { + use smithay::backend::input::ButtonState; + + let current_focus = seat.get_keyboard().unwrap().current_focus(); + let shortcuts_inhibited = current_focus.is_some_and(|f| { + f.wl_surface() + .and_then(|surface| { + seat.keyboard_shortcuts_inhibitor_for_surface(&surface) + .map(|inhibitor| inhibitor.is_active()) + }) + .unwrap_or(false) + }); + + let serial = SERIAL_COUNTER.next_serial(); + let mut pass_event = !seat.supressed_buttons().remove(button); + if button_state == ButtonState::Pressed { + // change the keyboard focus unless the pointer is grabbed + // We test for any matching surface type here but always use the root + // (in case of a window the toplevel) surface for the focus. + // see: https://gitlab.freedesktop.org/wayland/wayland/-/issues/294 + if !seat.get_pointer().unwrap().is_grabbed() { + let output = seat.active_output(); + + let global_position = seat.get_pointer().unwrap().current_location().as_global(); + let under = { + let shell = self.common.shell.read().unwrap(); + State::element_under(global_position, &output, &shell) + }; + if let Some(target) = under { + if let Some(surface) = target.toplevel().map(Cow::into_owned) { + if seat.get_keyboard().unwrap().modifier_state().logo + && !shortcuts_inhibited + { + let seat_clone = seat.clone(); + let mut supress_button = || { + // If the logo is held then the pointer event is + // aimed at the compositor and shouldn't be passed + // to the application. + pass_event = false; + seat.supressed_buttons().add(button); + }; + + fn dispatch_grab + 'static>( + grab: Option<(G, smithay::input::pointer::Focus)>, + seat: Seat, + serial: Serial, + state: &mut State, + ) { + if let Some((target, focus)) = grab { + seat.modifiers_shortcut_queue().clear(); + + seat.get_pointer() + .unwrap() + .set_grab(state, target, serial, focus); + } + } + + if let Some(mouse_button) = mouse_button { + match mouse_button { + smithay::backend::input::MouseButton::Left => { + supress_button(); + self.common.event_loop_handle.insert_idle(move |state| { + let mut shell = state.common.shell.write().unwrap(); + let res = shell.move_request( + &surface, + &seat_clone, + serial, + ReleaseMode::NoMouseButtons, + false, + &state.common.config, + &state.common.event_loop_handle, + &state.common.xdg_activation_state, + false, + ); + drop(shell); + dispatch_grab(res, seat_clone, serial, state); + }); + } + smithay::backend::input::MouseButton::Right => { + supress_button(); + self.common.event_loop_handle.insert_idle(move |state| { + let mut shell = state.common.shell.write().unwrap(); + let Some(target_elem) = + shell.element_for_surface(&surface) + else { + return; + }; + let Some(geom) = shell + .space_for(target_elem) + .and_then(|f| f.element_geometry(target_elem)) + else { + return; + }; + let geom = geom.to_f64(); + let center = geom.loc + geom.size.downscale(2.0); + let offset = + center.to_global(&output) - global_position; + let edge = match (offset.x > 0.0, offset.y > 0.0) { + (true, true) => ResizeEdge::TOP_LEFT, + (false, true) => ResizeEdge::TOP_RIGHT, + (true, false) => ResizeEdge::BOTTOM_LEFT, + (false, false) => ResizeEdge::BOTTOM_RIGHT, + }; + let res = shell.resize_request( + &surface, + &seat_clone, + serial, + edge, + false, + ); + drop(shell); + dispatch_grab(res, seat_clone, serial, state); + }); + } + _ => {} + } + } + } + } + + Shell::set_focus(self, Some(&target), &seat, Some(serial), false); + } + } + } else { + let mut shell = self.common.shell.write().unwrap(); + if let Some(Trigger::Pointer(action_button)) = shell.overview_mode().0.active_trigger() + { + if *action_button == button { + shell.set_overview_mode(None, self.common.event_loop_handle.clone()); + } + } + std::mem::drop(shell); + }; + + let ptr = seat.get_pointer().unwrap(); + if pass_event { + ptr.button( + self, + &ButtonEvent { + button, + state: button_state, + serial, + time, + }, + ); + ptr.frame(self); + } else if button_state == ButtonState::Released { + ptr.unset_grab(self, serial, time) + } + } + /// Determine is key event should be intercepted as a key binding, or forwarded to surface pub fn filter_keyboard_input>( &mut self, From af6d748b345c21e8880e33b9c5c0d99d8b1f0d00 Mon Sep 17 00:00:00 2001 From: "Paul D. Faria" Date: Wed, 1 Jan 2025 22:01:49 -0800 Subject: [PATCH 2/3] Move pointer relative motion to separate function The pointer relative is being extracted to be reused in a future commit. --- src/input/mod.rs | 590 ++++++++++++++++++++++++----------------------- 1 file changed, 305 insertions(+), 285 deletions(-) diff --git a/src/input/mod.rs b/src/input/mod.rs index aafc2754..096a8bab 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -246,293 +246,23 @@ impl State { InputEvent::PointerMotion { event, .. } => { use smithay::backend::input::PointerMotionEvent; - let mut shell = self.common.shell.write().unwrap(); - if let Some(seat) = shell.seats.for_device(&event.device()).cloned() { + let maybe_seat = self + .common + .shell + .read() + .unwrap() + .seats + .for_device(&event.device()) + .cloned(); + if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); - let current_output = seat.active_output(); - - let mut position = seat.get_pointer().unwrap().current_location().as_global(); - - let under = State::surface_under(position, ¤t_output, &mut *shell) - .map(|(target, pos)| (target, pos.as_logical())); - - let ptr = seat.get_pointer().unwrap(); - - let mut pointer_locked = false; - let mut pointer_confined = false; - let mut confine_region = None; - if let Some((surface, surface_loc)) = under - .as_ref() - .and_then(|(target, l)| Some((target.wl_surface()?, l))) - { - with_pointer_constraint(&surface, &ptr, |constraint| match constraint { - Some(constraint) if constraint.is_active() => { - // Constraint does not apply if not within region - if !constraint.region().map_or(true, |x| { - x.contains( - (ptr.current_location() - *surface_loc).to_i32_round(), - ) - }) { - return; - } - match &*constraint { - PointerConstraint::Locked(_locked) => { - pointer_locked = true; - } - PointerConstraint::Confined(confine) => { - pointer_confined = true; - confine_region = confine.region().cloned(); - } - } - } - _ => {} - }); - } - let original_position = position; - position += event.delta().as_global(); - - let output = shell - .outputs() - .find(|output| output.geometry().to_f64().contains(position)) - .cloned() - .unwrap_or(current_output.clone()); - - let new_under = State::surface_under(position, &output, &mut *shell) - .map(|(target, pos)| (target, pos.as_logical())); - - std::mem::drop(shell); - ptr.relative_motion( - self, - under.clone(), - &RelativeMotionEvent { - delta: event.delta(), - delta_unaccel: event.delta_unaccel(), - utime: event.time(), - }, - ); - - if pointer_locked { - ptr.frame(self); - return; - } - - if ptr.is_grabbed() { - if seat - .user_data() - .get::() - .map(|marker| marker.get()) - .unwrap_or(false) - { - if output != current_output { - ptr.frame(self); - return; - } - } - //If the pointer isn't grabbed, we should check if the focused element should be updated - } else if self.common.config.cosmic_conf.focus_follows_cursor { - let shell = self.common.shell.read().unwrap(); - let old_keyboard_target = - State::element_under(original_position, ¤t_output, &*shell); - let new_keyboard_target = State::element_under(position, &output, &*shell); - - if old_keyboard_target != new_keyboard_target - && new_keyboard_target.is_some() - { - let create_source = if self.common.pointer_focus_state.is_none() { - true - } else { - let PointerFocusState { - originally_focused_window, - scheduled_focused_window, - token, - } = self.common.pointer_focus_state.as_ref().unwrap(); - - if &new_keyboard_target == originally_focused_window { - //if we moved to the original window, just cancel the event - self.common.event_loop_handle.remove(*token); - //clear the state - self.common.pointer_focus_state = None; - false - } else if &new_keyboard_target != scheduled_focused_window { - //if we moved to a new window, update the scheduled focus - self.common.event_loop_handle.remove(*token); - true - } else { - //the state doesn't need to be updated or cleared - false - } - }; - - if create_source { - // prevent popups from being unfocusable if there is a gap between them and their parent - let delay = calloop::timer::Timer::from_duration( - //default to 250ms - std::time::Duration::from_millis( - self.common.config.cosmic_conf.focus_follows_cursor_delay, - ), - ); - let seat = seat.clone(); - let token = self - .common - .event_loop_handle - .insert_source(delay, move |_, _, state| { - let target = state - .common - .pointer_focus_state - .as_ref() - .unwrap() - .scheduled_focused_window - .clone(); - //clear it prior in case the user twitches in the microsecond it - //takes this function to run - state.common.pointer_focus_state = None; - - Shell::set_focus( - state, - target.as_ref(), - &seat, - Some(SERIAL_COUNTER.next_serial()), - false, - ); - - TimeoutAction::Drop - }) - .ok(); - if token.is_some() { - let originally_focused_window = - if self.common.pointer_focus_state.is_none() { - old_keyboard_target - } else { - // In this case, the pointer has moved to a new window (neither original, nor scheduled) - // so we should preserve the original window for the focus state - self.common - .pointer_focus_state - .as_ref() - .unwrap() - .originally_focused_window - .clone() - }; - - self.common.pointer_focus_state = Some(PointerFocusState { - originally_focused_window, - scheduled_focused_window: new_keyboard_target, - token: token.unwrap(), - }); - } - } - } - } - - let output_geometry = output.geometry(); - - position.x = position.x.clamp( - output_geometry.loc.x as f64, - ((output_geometry.loc.x + output_geometry.size.w) as f64).next_lower(), // FIXME: Replace with f64::next_down when stable - ); - position.y = position.y.clamp( - output_geometry.loc.y as f64, - ((output_geometry.loc.y + output_geometry.size.h) as f64).next_lower(), // FIXME: Replace with f64::next_down when stable - ); - - // If confined, don't move pointer if it would go outside surface or region - if pointer_confined { - if let Some((surface, surface_loc)) = &under { - if new_under.as_ref().and_then(|(under, _)| under.wl_surface()) - != surface.wl_surface() - { - ptr.frame(self); - return; - } - if let PointerFocusTarget::WlSurface { surface, .. } = surface { - if under_from_surface_tree( - surface, - position.as_logical() - surface_loc.to_f64(), - (0, 0), - WindowSurfaceType::ALL, - ) - .is_none() - { - ptr.frame(self); - return; - } - } - if let Some(region) = confine_region { - if !region - .contains((position.as_logical() - *surface_loc).to_i32_round()) - { - ptr.frame(self); - return; - } - } - } - } - - let serial = SERIAL_COUNTER.next_serial(); - ptr.motion( - self, - under, - &MotionEvent { - location: position.as_logical(), - serial, - time: event.time_msec(), - }, + self.check_end_drag(&seat, event.time_msec()); + self.process_motion_relative( + &seat, + event.delta(), + event.delta_unaccel(), + event.time(), ); - ptr.frame(self); - - // If pointer is now in a constraint region, activate it - if let Some((under, surface_location)) = new_under - .and_then(|(target, loc)| Some((target.wl_surface()?.into_owned(), loc))) - { - with_pointer_constraint(&under, &ptr, |constraint| match constraint { - Some(constraint) if !constraint.is_active() => { - let region = match &*constraint { - PointerConstraint::Locked(locked) => locked.region(), - PointerConstraint::Confined(confined) => confined.region(), - }; - let point = - (ptr.current_location() - surface_location).to_i32_round(); - if region.map_or(true, |region| region.contains(point)) { - constraint.activate(); - } - } - _ => {} - }); - } - - let mut shell = self.common.shell.write().unwrap(); - shell.update_pointer_position(position.to_local(&output), &output); - - if output != current_output { - for session in cursor_sessions_for_output(&*shell, ¤t_output) { - session.set_cursor_pos(None); - } - seat.set_active_output(&output); - } - - for session in cursor_sessions_for_output(&shell, &output) { - if let Some((geometry, offset)) = seat.cursor_geometry( - position.as_logical().to_buffer( - output.current_scale().fractional_scale(), - output.current_transform(), - &output_geometry.size.to_f64().as_logical(), - ), - self.common.clock.now(), - ) { - if session - .current_constraints() - .map(|constraint| constraint.size != geometry.size) - .unwrap_or(true) - { - session.update_constraints(BufferConstraints { - size: geometry.size, - shm: vec![ShmFormat::Argb8888], - dma: None, - }); - } - session.set_cursor_hotspot(offset); - session.set_cursor_pos(Some(geometry.loc)); - } - } } } InputEvent::PointerMotionAbsolute { event, .. } => { @@ -1251,6 +981,296 @@ impl State { } } + /// Process relative mouse motion + fn process_motion_relative( + &mut self, + seat: &Seat, + delta: Point, + delta_unaccel: Point, + time_usec: u64, + ) { + let mut shell = self.common.shell.write().unwrap(); + let current_output = seat.active_output(); + + let mut position = seat.get_pointer().unwrap().current_location().as_global(); + + let under = State::surface_under(position, ¤t_output, &mut *shell) + .map(|(target, pos)| (target, pos.as_logical())); + + let ptr = seat.get_pointer().unwrap(); + + let mut pointer_locked = false; + let mut pointer_confined = false; + let mut confine_region = None; + if let Some((surface, surface_loc)) = under + .as_ref() + .and_then(|(target, l)| Some((target.wl_surface()?, l))) + { + with_pointer_constraint(&surface, &ptr, |constraint| match constraint { + Some(constraint) if constraint.is_active() => { + // Constraint does not apply if not within region + if !constraint.region().map_or(true, |x| { + x.contains((ptr.current_location() - *surface_loc).to_i32_round()) + }) { + return; + } + match &*constraint { + PointerConstraint::Locked(_locked) => { + pointer_locked = true; + } + PointerConstraint::Confined(confine) => { + pointer_confined = true; + confine_region = confine.region().cloned(); + } + } + } + _ => {} + }); + } + let original_position = position; + position += delta.as_global(); + + let output = shell + .outputs() + .find(|output| output.geometry().to_f64().contains(position)) + .cloned() + .unwrap_or(current_output.clone()); + + let new_under = State::surface_under(position, &output, &mut *shell) + .map(|(target, pos)| (target, pos.as_logical())); + + std::mem::drop(shell); + + ptr.relative_motion( + self, + under.clone(), + &RelativeMotionEvent { + delta, + delta_unaccel, + utime: time_usec, + }, + ); + + if pointer_locked { + ptr.frame(self); + return; + } + + if ptr.is_grabbed() { + if seat + .user_data() + .get::() + .map(|marker| marker.get()) + .unwrap_or(false) + { + if output != current_output { + ptr.frame(self); + return; + } + } + //If the pointer isn't grabbed, we should check if the focused element should be updated + } else if self.common.config.cosmic_conf.focus_follows_cursor { + let shell = self.common.shell.read().unwrap(); + let old_keyboard_target = + State::element_under(original_position, ¤t_output, &*shell); + let new_keyboard_target = State::element_under(position, &output, &*shell); + + if old_keyboard_target != new_keyboard_target && new_keyboard_target.is_some() { + let create_source = if self.common.pointer_focus_state.is_none() { + true + } else { + let PointerFocusState { + originally_focused_window, + scheduled_focused_window, + token, + } = self.common.pointer_focus_state.as_ref().unwrap(); + + if &new_keyboard_target == originally_focused_window { + //if we moved to the original window, just cancel the event + self.common.event_loop_handle.remove(*token); + //clear the state + self.common.pointer_focus_state = None; + false + } else if &new_keyboard_target != scheduled_focused_window { + //if we moved to a new window, update the scheduled focus + self.common.event_loop_handle.remove(*token); + true + } else { + //the state doesn't need to be updated or cleared + false + } + }; + + if create_source { + // prevent popups from being unfocusable if there is a gap between them and their parent + let delay = calloop::timer::Timer::from_duration( + //default to 250ms + std::time::Duration::from_millis( + self.common.config.cosmic_conf.focus_follows_cursor_delay, + ), + ); + let seat = seat.clone(); + let token = self + .common + .event_loop_handle + .insert_source(delay, move |_, _, state| { + let target = state + .common + .pointer_focus_state + .as_ref() + .unwrap() + .scheduled_focused_window + .clone(); + //clear it prior in case the user twitches in the microsecond it + //takes this function to run + state.common.pointer_focus_state = None; + + Shell::set_focus( + state, + target.as_ref(), + &seat, + Some(SERIAL_COUNTER.next_serial()), + false, + ); + + TimeoutAction::Drop + }) + .ok(); + if token.is_some() { + let originally_focused_window = if self.common.pointer_focus_state.is_none() + { + old_keyboard_target + } else { + // In this case, the pointer has moved to a new window (neither original, nor scheduled) + // so we should preserve the original window for the focus state + self.common + .pointer_focus_state + .as_ref() + .unwrap() + .originally_focused_window + .clone() + }; + + self.common.pointer_focus_state = Some(PointerFocusState { + originally_focused_window, + scheduled_focused_window: new_keyboard_target, + token: token.unwrap(), + }); + } + } + } + } + + let output_geometry = output.geometry(); + + position.x = position.x.clamp( + output_geometry.loc.x as f64, + ((output_geometry.loc.x + output_geometry.size.w) as f64).next_lower(), // FIXME: Replace with f64::next_down when stable + ); + position.y = position.y.clamp( + output_geometry.loc.y as f64, + ((output_geometry.loc.y + output_geometry.size.h) as f64).next_lower(), // FIXME: Replace with f64::next_down when stable + ); + + // If confined, don't move pointer if it would go outside surface or region + if pointer_confined { + if let Some((surface, surface_loc)) = &under { + if new_under.as_ref().and_then(|(under, _)| under.wl_surface()) + != surface.wl_surface() + { + ptr.frame(self); + return; + } + if let PointerFocusTarget::WlSurface { surface, .. } = surface { + if under_from_surface_tree( + surface, + position.as_logical() - surface_loc.to_f64(), + (0, 0), + WindowSurfaceType::ALL, + ) + .is_none() + { + ptr.frame(self); + return; + } + } + if let Some(region) = confine_region { + if !region.contains((position.as_logical() - *surface_loc).to_i32_round()) { + ptr.frame(self); + return; + } + } + } + } + + let serial = SERIAL_COUNTER.next_serial(); + + ptr.motion( + self, + under, + &MotionEvent { + location: position.as_logical(), + serial, + time: (time_usec / 1000) as u32, //: event.time_msec(), + }, + ); + ptr.frame(self); + + // If pointer is now in a constraint region, activate it + if let Some((under, surface_location)) = + new_under.and_then(|(target, loc)| Some((target.wl_surface()?.into_owned(), loc))) + { + with_pointer_constraint(&under, &ptr, |constraint| match constraint { + Some(constraint) if !constraint.is_active() => { + let region = match &*constraint { + PointerConstraint::Locked(locked) => locked.region(), + PointerConstraint::Confined(confined) => confined.region(), + }; + let point = (ptr.current_location() - surface_location).to_i32_round(); + if region.map_or(true, |region| region.contains(point)) { + constraint.activate(); + } + } + _ => {} + }); + } + + let mut shell = self.common.shell.write().unwrap(); + shell.update_pointer_position(position.to_local(&output), &output); + + if output != current_output { + for session in cursor_sessions_for_output(&*shell, ¤t_output) { + session.set_cursor_pos(None); + } + seat.set_active_output(&output); + } + + for session in cursor_sessions_for_output(&shell, &output) { + if let Some((geometry, offset)) = seat.cursor_geometry( + position.as_logical().to_buffer( + output.current_scale().fractional_scale(), + output.current_transform(), + &output_geometry.size.to_f64().as_logical(), + ), + self.common.clock.now(), + ) { + if session + .current_constraints() + .map(|constraint| constraint.size != geometry.size) + .unwrap_or(true) + { + session.update_constraints(BufferConstraints { + size: geometry.size, + shm: vec![ShmFormat::Argb8888], + dma: None, + }); + } + session.set_cursor_hotspot(offset); + session.set_cursor_pos(Some(geometry.loc)); + } + } + } + /// Process a button press. fn process_pointer_button( &mut self, From 9b5ab04022fba8594a44d91586b897781604c0fc Mon Sep 17 00:00:00 2001 From: "Paul D. Faria" Date: Sun, 22 Dec 2024 21:22:11 -0800 Subject: [PATCH 3/3] Add support for gesture-based dragging Drags are started with a configurable gesture, and can be continued even when lifting the fingers off the touchpad and starting the gesture again. If a different gesture or pointer click occurs, the drag will immediately end. The time delay for waiting for the continuation drag event is also configurable. --- cosmic-comp-config/src/input.rs | 16 ++++ src/config/input_config.rs | 2 + src/input/actions.rs | 37 ++++++- src/input/gestures/mod.rs | 10 +- src/input/mod.rs | 164 ++++++++++++++++++++++++++++++-- src/logger/mod.rs | 3 +- 6 files changed, 218 insertions(+), 14 deletions(-) diff --git a/cosmic-comp-config/src/input.rs b/cosmic-comp-config/src/input.rs index 8dc5b761..c1549d62 100644 --- a/cosmic-comp-config/src/input.rs +++ b/cosmic-comp-config/src/input.rs @@ -31,6 +31,22 @@ pub struct InputConfig { pub tap_config: Option, #[serde(skip_serializing_if = "Option::is_none", default)] pub map_to_output: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub three_finger_drag: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub three_finger_drag_delay: Option, + // #[serde(skip_serializing_if = "Option::is_none", default)] + // pub three_finger_gesture: Option, + // #[serde(skip_serializing_if = "Option::is_none", default)] + // pub four_finger_gesture: Option, + // #[serde(skip_serializing_if = "Option::is_none", default)] + // pub gesture_drag_delay: Option, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub enum Gesture { + Drag, + SwitchWorkspace, } #[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] diff --git a/src/config/input_config.rs b/src/config/input_config.rs index 6371a23b..2c09a32d 100644 --- a/src/config/input_config.rs +++ b/src/config/input_config.rs @@ -80,6 +80,8 @@ pub fn for_device(device: &InputDevice) -> InputConfig { None }, map_to_output: None, + three_finger_drag: (device.config_tap_finger_count() > 2).then_some(true), + three_finger_drag_delay: (device.config_tap_finger_count() > 2).then_some(1000), } } diff --git a/src/input/actions.rs b/src/input/actions.rs index a85cf8a0..1fccd38e 100644 --- a/src/input/actions.rs +++ b/src/input/actions.rs @@ -92,7 +92,13 @@ impl State { } } - pub fn handle_swipe_action(&mut self, action: gestures::SwipeAction, seat: &Seat) { + pub fn handle_swipe_action( + &mut self, + action: gestures::SwipeAction, + seat: &Seat, + event_time: u32, + continuation: bool, + ) { use gestures::SwipeAction; match action { @@ -112,6 +118,35 @@ impl State { &mut self.common.workspace_state.update(), ); } + SwipeAction::Drag if !continuation => { + use smithay::backend::input::{ButtonState, MouseButton}; + + let left_handed = self + .common + .config + .cosmic_conf + .input_touchpad + .left_handed + .unwrap_or(false); + let (button, mouse_button) = if left_handed { + (super::BTN_RIGHT, MouseButton::Right) + } else { + (super::BTN_LEFT, MouseButton::Left) + }; + self.process_pointer_button( + seat, + button, + ButtonState::Pressed, + Some(mouse_button), + event_time, + ); + } + SwipeAction::Drag => { + // Do not press again if this is a continuation. + } + SwipeAction::DragEnd(_) => { + // Not a user-triggered action. + } } } diff --git a/src/input/gestures/mod.rs b/src/input/gestures/mod.rs index 996d19cf..3d0f0ecd 100644 --- a/src/input/gestures/mod.rs +++ b/src/input/gestures/mod.rs @@ -1,3 +1,4 @@ +use calloop::RegistrationToken; use cosmic_settings_config::shortcuts::action::Direction; use smithay::utils::{Logical, Point}; use std::{collections::VecDeque, time::Duration}; @@ -16,6 +17,8 @@ pub struct SwipeEvent { pub enum SwipeAction { NextWorkspace, PrevWorkspace, + Drag, + DragEnd(RegistrationToken), } #[derive(Debug, Clone)] @@ -26,16 +29,19 @@ pub struct GestureState { pub delta: f64, // Delta tracking inspired by Niri (GPL-3.0) https://github.com/YaLTeR/niri/tree/v0.1.3 pub history: VecDeque, + /// `true` if this is a continuation of a previous gesture. + pub continuation: bool, } impl GestureState { - pub fn new(fingers: u32) -> Self { + pub fn new(fingers: u32, continuation: bool) -> Self { GestureState { fingers, direction: None, action: None, delta: 0.0, history: VecDeque::new(), + continuation, } } @@ -129,7 +135,7 @@ impl GestureState { impl Default for GestureState { fn default() -> Self { - GestureState::new(0) + GestureState::new(0, false) } } diff --git a/src/input/mod.rs b/src/input/mod.rs index 096a8bab..d841838e 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -40,7 +40,7 @@ use smithay::{ backend::input::{ AbsolutePositionEvent, Axis, AxisSource, Device, DeviceCapability, GestureBeginEvent, GestureEndEvent, GesturePinchUpdateEvent as _, GestureSwipeUpdateEvent as _, InputBackend, - InputEvent, KeyState, KeyboardKeyEvent, PointerAxisEvent, ProximityState, + InputEvent, KeyState, KeyboardKeyEvent, MouseButton, PointerAxisEvent, ProximityState, TabletToolButtonEvent, TabletToolEvent, TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, TouchEvent, }, @@ -60,7 +60,7 @@ use smithay::{ reexports::{ input::Device as InputDevice, wayland_server::protocol::wl_shm::Format as ShmFormat, }, - utils::{Point, Rectangle, Serial, SERIAL_COUNTER}, + utils::{Logical, Point, Rectangle, Serial, SERIAL_COUNTER}, wayland::{ keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat, pointer_constraints::{with_pointer_constraint, PointerConstraint}, @@ -159,6 +159,10 @@ impl ModifiersShortcutQueue { } } +// libinput's BTN_LEFT and BTN_RIGHT +const BTN_LEFT: u32 = 0x110; +const BTN_RIGHT: u32 = 0x111; + impl State { pub fn process_input_event(&mut self, event: InputEvent) where @@ -276,6 +280,7 @@ impl State { .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); + self.check_end_drag(&seat, event.time_msec()); let output = seat.active_output(); let geometry = output.geometry(); let position = geometry.loc.to_f64() @@ -332,9 +337,8 @@ impl State { } } InputEvent::PointerButton { event, .. } => { - use smithay::backend::input::{ButtonState, PointerButtonEvent}; + use smithay::backend::input::PointerButtonEvent; - // let Some(seat) = self .common .shell @@ -348,6 +352,7 @@ impl State { }; self.common.idle_notifier_state.notify_activity(&seat); + self.check_end_drag(&seat, event.time_msec()); self.process_pointer_button( &seat, event.button_code(), @@ -374,6 +379,7 @@ impl State { .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); + self.check_end_drag(&seat, event.time_msec()); let mut frame = AxisFrame::new(event.time_msec()).source(event.source()); if let Some(horizontal_amount) = event.amount(Axis::Horizontal) { @@ -420,9 +426,37 @@ impl State { .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); - if event.fingers() >= 3 && !workspace_overview_is_open(&seat.active_output()) { - self.common.gesture_state = Some(GestureState::new(event.fingers())); + if event.fingers() == 3 + || (event.fingers() > 3 + && !workspace_overview_is_open(&seat.active_output())) + { + // Handle the DragEnd state. + let mut continuation = false; + if let Some(GestureState { + action: Some(SwipeAction::DragEnd(ref token)), + .. + }) = self.common.gesture_state + { + if self.common.event_loop_handle.disable(token).is_ok() { + // The token was still valid. Remove the event and return true to + // signify the drag should continue. + self.common.event_loop_handle.remove(*token); + if event.fingers() == 3 { + // If this is a three-finger swipe, then it's a continuation of + // the three-finger drag + continuation = true + } else { + // This is a four-finger swipe, so end the current drag before + // starting a new swipe action + self.end_drag(&seat, event.time_msec()); + } + } + }; + + self.common.gesture_state = + Some(GestureState::new(event.fingers(), continuation)); } else { + self.check_end_drag(&seat, event.time_msec()); let serial = SERIAL_COUNTER.next_serial(); let pointer = seat.get_pointer().unwrap(); pointer.gesture_swipe_begin( @@ -448,6 +482,7 @@ impl State { if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); let mut activate_action: Option = None; + let mut continuation = false; if let Some(ref mut gesture_state) = self.common.gesture_state { let first_update = gesture_state.update( event.delta(), @@ -464,7 +499,22 @@ impl State { } } activate_action = match gesture_state.fingers { - 3 => None, // TODO: 3 finger gestures + 3 => { + // Start a drag if three-finger drag enabled, and it's not a + // continuation of a previous drag. + (self + .common + .config + .cosmic_conf + .input_touchpad + .three_finger_drag + .unwrap_or(false)) + .then(|| { + continuation = gesture_state.continuation; + SwipeAction::Drag + }) + // TODO other 3-finger gestures + } 4 => { if self.common.config.cosmic_conf.workspaces.workspace_layout == WorkspaceLayout::Horizontal @@ -519,6 +569,15 @@ impl State { gesture_state.delta, ) } + Some(SwipeAction::Drag) => { + self.process_motion_relative( + &seat, + // TODO extract acceleration scale into config + event.delta().upscale(2.0), + event.delta(), + event.time(), + ); + } _ => {} } } else { @@ -533,7 +592,7 @@ impl State { } if let Some(action) = activate_action { - self.handle_swipe_action(action, &seat); + self.handle_swipe_action(action, &seat, event.time_msec(), continuation); } } } @@ -548,7 +607,8 @@ impl State { .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); - if let Some(ref gesture_state) = self.common.gesture_state { + if let Some(ref mut gesture_state) = self.common.gesture_state { + let mut clear_state = true; match gesture_state.action { Some(SwipeAction::NextWorkspace) | Some(SwipeAction::PrevWorkspace) => { let velocity = gesture_state.velocity(); @@ -566,9 +626,52 @@ impl State { &mut self.common.workspace_state.update(), ); } + Some(SwipeAction::Drag) => { + // Start a delayed drag-end event. This will allow a user to lift + // their fingers and continue the drag event. When the event needs + // to continue or end due to a different pointer interaction, the + // token can be used to cancel the delayed end and either continue + // the drag or activate it immediately via `State::end_drag`. + // TODO reword above. + let delay_ms = self + .common + .config + .cosmic_conf + .input_touchpad + .three_finger_drag_delay + .unwrap_or(1000); + let delay = calloop::timer::Timer::from_duration( + std::time::Duration::from_millis(delay_ms as u64), + ); + let seat = seat.clone(); + let time = event.time_msec(); + let token = self + .common + .event_loop_handle + .insert_source(delay, move |_, _, state| { + if let Some(SwipeAction::DragEnd(_)) = state + .common + .gesture_state + .as_ref() + .and_then(|g| g.action) + { + state.end_drag(&seat, time); + state.common.gesture_state = None; + } + + TimeoutAction::Drop + }) + .ok(); + if let Some(token) = token { + gesture_state.action = Some(SwipeAction::DragEnd(token)); + clear_state = false; + } + } _ => {} + }; + if clear_state { + self.common.gesture_state = None; } - self.common.gesture_state = None; } else { let serial = SERIAL_COUNTER.next_serial(); let pointer = seat.get_pointer().unwrap(); @@ -1430,6 +1533,47 @@ impl State { } } + /// Check if there's an active DragEnd swipe, and if so, cancel its token + /// and trigger the DragEnd action immediately. + fn check_end_drag(&mut self, seat: &Seat, event_time_msec: u32) { + let Some(GestureState { + action: Some(SwipeAction::DragEnd(ref token)), + .. + }) = self.common.gesture_state + else { + return; + }; + + self.common.event_loop_handle.remove(*token); + self.common.gesture_state = None; + self.end_drag(seat, event_time_msec); + } + + /// End the drag event. + fn end_drag(&mut self, seat: &Seat, event_time_msec: u32) { + use smithay::backend::input::ButtonState; + + let left_handed = self + .common + .config + .cosmic_conf + .input_touchpad + .left_handed + .unwrap_or(false); + let (button, mouse_button) = if left_handed { + (BTN_RIGHT, MouseButton::Right) + } else { + (BTN_LEFT, MouseButton::Left) + }; + self.process_pointer_button( + seat, + button, + ButtonState::Released, + Some(mouse_button), + event_time_msec, + ); + } + /// Determine is key event should be intercepted as a key binding, or forwarded to surface pub fn filter_keyboard_input>( &mut self, diff --git a/src/logger/mod.rs b/src/logger/mod.rs index 1f003ccf..268e840c 100644 --- a/src/logger/mod.rs +++ b/src/logger/mod.rs @@ -12,7 +12,8 @@ pub fn init_logger() -> Result<()> { let level = if cfg!(debug_assertions) { "debug" } else { - "warn" + // "warn" + "debug" }; let filter = EnvFilter::try_from_default_env() .unwrap_or_else(|_| {