2021-12-21 18:57:09 +01:00
|
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
|
|
2022-07-08 19:15:56 +02:00
|
|
|
use crate::{
|
cursor: Add idle-hide timeout
Adds a cursor_hide_timeout config key (Option<u32> seconds) to
CosmicCompConfig. When set, the cursor is hidden after the configured
period of pointer inactivity and revealed again by any pointer event.
Touch input does not count as activity (no visible cursor to surface).
Implementation:
- Per-seat hidden flag, calloop timer token, and last-armed Instant on
CursorStateInner.
- notify_cursor_activity called from each pointer-related input branch
(motion, button, axis, tablet) resets the flag and reschedules the
timer; rapid successive calls are coalesced behind a 100ms throttle
so high-frequency mice don't churn the calloop timer source.
- On timer fire, the hidden flag is set, draw_cursor short-circuits to
an empty element list, and a render is scheduled. Active pointer
grabs (drags, resizes) suppress the hide.
- Config reload arms or cancels the timer immediately; None as the
configured value collapses the cancel path into the same function.
Closes #2231.
Drafted with Claude (Anthropic); reviewed and tested by the committer.
2026-05-20 23:52:45 +02:00
|
|
|
backend::render::{ElementFilter, cursor::notify_cursor_activity},
|
2024-04-03 16:02:27 +02:00
|
|
|
config::{
|
2025-10-16 18:53:57 +02:00
|
|
|
Action, Config, PrivateAction,
|
2024-04-03 16:02:27 +02:00
|
|
|
key_bindings::{
|
|
|
|
|
cosmic_keystate_from_smithay, cosmic_modifiers_eq_smithay,
|
|
|
|
|
cosmic_modifiers_from_smithay,
|
|
|
|
|
},
|
|
|
|
|
},
|
2024-03-07 13:14:53 -06:00
|
|
|
input::gestures::{GestureState, SwipeAction},
|
2022-11-10 18:42:11 +01:00
|
|
|
shell::{
|
2026-06-15 16:23:31 -07:00
|
|
|
LastModifierChange, SeatExt, Trigger,
|
2024-10-10 23:18:04 +02:00
|
|
|
focus::{
|
2025-10-16 18:53:57 +02:00
|
|
|
Stage, render_input_order,
|
2024-10-10 23:18:04 +02:00
|
|
|
target::{KeyboardFocusTarget, PointerFocusTarget},
|
|
|
|
|
},
|
2024-04-05 13:53:35 +02:00
|
|
|
grabs::{ReleaseMode, ResizeEdge},
|
2024-09-16 13:00:31 -07:00
|
|
|
layout::{
|
|
|
|
|
floating::ResizeGrabMarker,
|
2025-01-17 21:25:43 +07:00
|
|
|
tiling::{NodeDesc, SwapWindowGrab, TilingLayout},
|
2024-09-16 13:00:31 -07:00
|
|
|
},
|
2025-02-13 21:09:13 +01:00
|
|
|
zoom::ZoomState,
|
2023-07-31 17:36:32 +02:00
|
|
|
},
|
2025-12-19 18:56:56 +01:00
|
|
|
utils::{prelude::*, quirks::workspace_overview_is_open},
|
2026-01-13 18:26:26 -08:00
|
|
|
wayland::handlers::{
|
2026-06-15 16:23:31 -07:00
|
|
|
image_copy_capture::{SessionHolder, cursor_capture_constraints},
|
|
|
|
|
xwayland_keyboard_grab::XWaylandGrabSeat,
|
2023-11-07 18:46:25 +01:00
|
|
|
},
|
2022-07-08 19:15:56 +02:00
|
|
|
};
|
2024-09-04 11:13:59 -05:00
|
|
|
use calloop::{
|
|
|
|
|
RegistrationToken,
|
2025-10-16 18:53:57 +02:00
|
|
|
timer::{TimeoutAction, Timer},
|
2024-09-04 11:13:59 -05:00
|
|
|
};
|
2025-10-16 18:53:57 +02:00
|
|
|
use cosmic_comp_config::{NumlockState, workspace::WorkspaceLayout};
|
2025-06-13 12:52:49 -04:00
|
|
|
use cosmic_settings_config::shortcuts;
|
2024-09-09 16:38:52 +02:00
|
|
|
use cosmic_settings_config::shortcuts::action::{Direction, ResizeDirection};
|
2026-07-16 11:40:58 +02:00
|
|
|
#[cfg(feature = "logind")]
|
2026-05-22 16:50:35 +02:00
|
|
|
use smithay::backend::input::{Switch, SwitchState, SwitchToggleEvent};
|
2021-12-21 18:57:09 +01:00
|
|
|
use smithay::{
|
2023-01-27 19:51:23 +01:00
|
|
|
backend::input::{
|
2026-05-15 15:51:24 -06:00
|
|
|
AbsolutePositionEvent, Axis, AxisRelativeDirection, AxisSource, Device, DeviceCapability,
|
|
|
|
|
GestureBeginEvent, GestureEndEvent, GesturePinchUpdateEvent as _,
|
|
|
|
|
GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState, KeyboardKeyEvent,
|
2026-05-22 16:50:35 +02:00
|
|
|
PointerAxisEvent, ProximityState, TabletToolButtonEvent, TabletToolEvent,
|
|
|
|
|
TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, TouchEvent,
|
2022-08-31 13:01:23 +02:00
|
|
|
},
|
2025-10-16 18:53:57 +02:00
|
|
|
desktop::{PopupKeyboardGrab, WindowSurfaceType, utils::under_from_surface_tree},
|
2022-08-31 13:01:23 +02:00
|
|
|
input::{
|
2025-10-16 18:53:57 +02:00
|
|
|
Seat,
|
2024-09-13 10:07:46 -07:00
|
|
|
keyboard::{FilterResult, KeysymHandle, ModifiersState},
|
2023-09-05 13:41:21 -07:00
|
|
|
pointer::{
|
2024-04-05 13:53:35 +02:00
|
|
|
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent,
|
2023-09-05 13:41:21 -07:00
|
|
|
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent,
|
|
|
|
|
GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, MotionEvent,
|
2026-05-13 19:53:07 +08:00
|
|
|
PointerGrab, PointerHandle, RelativeMotionEvent,
|
2023-09-05 13:41:21 -07:00
|
|
|
},
|
2024-03-26 16:45:30 +01:00
|
|
|
touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent},
|
2021-12-21 18:57:09 +01:00
|
|
|
},
|
2022-09-09 20:00:00 -07:00
|
|
|
output::Output,
|
2024-03-12 19:42:48 +01:00
|
|
|
reexports::{
|
2026-05-13 19:53:07 +08:00
|
|
|
input::Device as InputDevice,
|
2026-06-15 16:23:31 -07:00
|
|
|
wayland_server::{Resource as _, protocol::wl_surface::WlSurface},
|
2024-03-12 19:42:48 +01:00
|
|
|
},
|
2026-06-15 16:23:31 -07:00
|
|
|
utils::{Logical, Point, Rectangle, SERIAL_COUNTER, Serial},
|
2022-09-09 20:17:40 -07:00
|
|
|
wayland::{
|
2026-05-13 19:53:07 +08:00
|
|
|
compositor::CompositorHandler,
|
2026-06-15 16:23:31 -07:00
|
|
|
image_copy_capture::CursorSessionRef,
|
2023-09-13 20:52:10 -07:00
|
|
|
keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat,
|
2025-10-16 18:53:57 +02:00
|
|
|
pointer_constraints::{PointerConstraint, with_pointer_constraint},
|
2023-09-13 20:52:10 -07:00
|
|
|
seat::WaylandFocus,
|
2023-12-28 13:36:59 -08:00
|
|
|
tablet_manager::{TabletDescriptor, TabletSeatTrait},
|
2022-09-09 20:17:40 -07:00
|
|
|
},
|
2021-12-21 18:57:09 +01:00
|
|
|
};
|
2026-05-22 16:50:35 +02:00
|
|
|
use tracing::{error, trace};
|
2023-09-29 21:33:16 +02:00
|
|
|
use xkbcommon::xkb::{Keycode, Keysym};
|
2022-08-31 13:01:23 +02:00
|
|
|
|
2023-07-06 18:20:10 +02:00
|
|
|
use std::{
|
2023-09-01 12:33:55 -07:00
|
|
|
any::Any,
|
2024-05-13 14:16:21 -07:00
|
|
|
borrow::Cow,
|
2023-07-06 18:20:10 +02:00
|
|
|
cell::RefCell,
|
2024-08-21 21:29:23 +01:00
|
|
|
collections::HashSet,
|
2024-10-10 23:18:04 +02:00
|
|
|
ops::ControlFlow,
|
2023-07-06 18:20:10 +02:00
|
|
|
time::{Duration, Instant},
|
|
|
|
|
};
|
2021-12-21 18:57:09 +01:00
|
|
|
|
2024-09-09 16:38:52 +02:00
|
|
|
pub mod actions;
|
2024-03-07 13:14:53 -06:00
|
|
|
pub mod gestures;
|
|
|
|
|
|
2024-09-04 11:13:59 -05:00
|
|
|
/// Used for debouncing focus updates due to pointer motion, if after the focus change is
|
|
|
|
|
/// triggered the event will cancel if the pointer moves to the original target
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct PointerFocusState {
|
|
|
|
|
//the window under the cursor prior to it's movement
|
|
|
|
|
originally_focused_window: Option<KeyboardFocusTarget>,
|
|
|
|
|
//the window under the cursor after it's movement
|
|
|
|
|
scheduled_focused_window: Option<KeyboardFocusTarget>,
|
|
|
|
|
token: RegistrationToken,
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-08 14:25:26 +02:00
|
|
|
#[derive(Default)]
|
2023-09-29 21:33:16 +02:00
|
|
|
pub struct SupressedKeys(RefCell<Vec<(Keycode, Option<RegistrationToken>)>>);
|
2024-08-21 21:29:23 +01:00
|
|
|
#[derive(Default)]
|
|
|
|
|
pub struct SupressedButtons(RefCell<HashSet<u32>>);
|
2023-09-30 08:42:42 -05:00
|
|
|
#[derive(Default, Debug)]
|
2024-04-03 16:02:27 +02:00
|
|
|
pub struct ModifiersShortcutQueue(RefCell<Option<shortcuts::Binding>>);
|
2022-07-08 14:00:13 +02:00
|
|
|
|
2022-01-11 17:22:23 +01:00
|
|
|
impl SupressedKeys {
|
2023-07-06 18:20:10 +02:00
|
|
|
fn add(&self, keysym: &KeysymHandle, token: impl Into<Option<RegistrationToken>>) {
|
|
|
|
|
self.0.borrow_mut().push((keysym.raw_code(), token.into()));
|
2022-01-11 17:22:23 +01:00
|
|
|
}
|
|
|
|
|
|
2023-07-06 18:20:10 +02:00
|
|
|
fn filter(&self, keysym: &KeysymHandle) -> Option<Vec<RegistrationToken>> {
|
2022-01-11 17:22:23 +01:00
|
|
|
let mut keys = self.0.borrow_mut();
|
2023-07-06 18:20:10 +02:00
|
|
|
let (removed, remaining) = keys
|
|
|
|
|
.drain(..)
|
|
|
|
|
.partition(|(key, _)| *key == keysym.raw_code());
|
|
|
|
|
*keys = remaining;
|
|
|
|
|
|
|
|
|
|
if removed.is_empty() {
|
2024-07-17 21:07:38 +02:00
|
|
|
return None;
|
2022-01-11 17:22:23 +01:00
|
|
|
}
|
2024-07-17 21:07:38 +02:00
|
|
|
|
|
|
|
|
Some(
|
|
|
|
|
removed
|
|
|
|
|
.into_iter()
|
2025-10-16 13:50:32 +02:00
|
|
|
.filter_map(|(_, token)| token)
|
2024-07-17 21:07:38 +02:00
|
|
|
.collect::<Vec<_>>(),
|
|
|
|
|
)
|
2022-01-11 17:22:23 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-21 21:29:23 +01:00
|
|
|
impl SupressedButtons {
|
|
|
|
|
fn add(&self, button: u32) {
|
|
|
|
|
self.0.borrow_mut().insert(button);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn remove(&self, button: u32) -> bool {
|
|
|
|
|
self.0.borrow_mut().remove(&button)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-30 08:42:42 -05:00
|
|
|
impl ModifiersShortcutQueue {
|
2024-04-03 16:02:27 +02:00
|
|
|
pub fn set(&self, binding: shortcuts::Binding) {
|
2023-09-30 08:42:42 -05:00
|
|
|
let mut set = self.0.borrow_mut();
|
|
|
|
|
*set = Some(binding);
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-03 16:02:27 +02:00
|
|
|
pub fn take(&self, binding: &shortcuts::Binding) -> bool {
|
2023-09-30 08:42:42 -05:00
|
|
|
let mut set = self.0.borrow_mut();
|
|
|
|
|
if set.is_some() && set.as_ref().unwrap() == binding {
|
|
|
|
|
*set = None;
|
|
|
|
|
true
|
|
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn clear(&self) {
|
|
|
|
|
let mut set = self.0.borrow_mut();
|
|
|
|
|
*set = None;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-25 12:35:55 +02:00
|
|
|
impl State {
|
2025-05-20 17:54:12 +02:00
|
|
|
#[profiling::function]
|
2024-09-18 13:20:17 -07:00
|
|
|
pub fn process_input_event<B: InputBackend>(&mut self, event: InputEvent<B>)
|
|
|
|
|
where
|
2023-12-28 13:36:59 -08:00
|
|
|
<B as InputBackend>::Device: 'static,
|
2023-09-01 12:33:55 -07:00
|
|
|
{
|
2024-08-16 20:49:59 -07:00
|
|
|
crate::wayland::handlers::output_power::set_all_surfaces_dpms_on(self);
|
|
|
|
|
|
2021-12-22 20:14:09 +01:00
|
|
|
use smithay::backend::input::Event;
|
|
|
|
|
match event {
|
|
|
|
|
InputEvent::DeviceAdded { device } => {
|
2025-05-20 17:41:27 +02:00
|
|
|
let shell = self.common.shell.read();
|
2024-04-10 15:49:08 +02:00
|
|
|
let seat = shell.seats.last_active();
|
2025-01-01 08:48:32 -08:00
|
|
|
let led_state = seat.get_keyboard().unwrap().led_state();
|
|
|
|
|
seat.devices().add_device(&device, led_state);
|
2024-05-14 18:48:21 -07:00
|
|
|
if device.has_capability(DeviceCapability::TabletTool) {
|
|
|
|
|
seat.tablet_seat().add_tablet::<Self>(
|
|
|
|
|
&self.common.display_handle,
|
|
|
|
|
&TabletDescriptor::from(&device),
|
|
|
|
|
);
|
2021-12-22 20:14:09 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
InputEvent::DeviceRemoved { device } => {
|
2025-05-20 17:41:27 +02:00
|
|
|
for seat in &mut self.common.shell.read().seats.iter() {
|
2024-04-05 13:53:35 +02:00
|
|
|
let devices = seat.devices();
|
2021-12-22 20:14:09 +01:00
|
|
|
if devices.has_device(&device) {
|
2024-05-14 18:48:21 -07:00
|
|
|
devices.remove_device(&device);
|
|
|
|
|
if device.has_capability(DeviceCapability::TabletTool) {
|
|
|
|
|
seat.tablet_seat()
|
|
|
|
|
.remove_tablet(&TabletDescriptor::from(&device));
|
|
|
|
|
if seat.tablet_seat().count_tablets() == 0 {
|
|
|
|
|
seat.tablet_seat().clear_tools();
|
2021-12-22 20:14:09 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-09-09 16:38:52 +02:00
|
|
|
|
2021-12-22 20:14:09 +01:00
|
|
|
InputEvent::Keyboard { event, .. } => {
|
|
|
|
|
use smithay::backend::input::KeyboardKeyEvent;
|
|
|
|
|
|
2024-04-10 15:49:08 +02:00
|
|
|
let maybe_seat = self
|
|
|
|
|
.common
|
|
|
|
|
.shell
|
|
|
|
|
.read()
|
|
|
|
|
.seats
|
|
|
|
|
.for_device(&event.device())
|
|
|
|
|
.cloned();
|
|
|
|
|
if let Some(seat) = maybe_seat {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
2024-09-04 11:13:59 -05:00
|
|
|
|
2023-09-05 13:41:21 -07:00
|
|
|
let keycode = event.key_code();
|
|
|
|
|
let state = event.state();
|
|
|
|
|
trace!(?keycode, ?state, "key");
|
2021-12-22 20:14:09 +01:00
|
|
|
|
2023-09-05 13:41:21 -07:00
|
|
|
let serial = SERIAL_COUNTER.next_serial();
|
|
|
|
|
let time = Event::time_msec(&event);
|
2023-08-11 18:15:22 +02:00
|
|
|
let keyboard = seat.get_keyboard().unwrap();
|
2025-02-24 21:43:16 +01:00
|
|
|
let previous_modifiers = keyboard.modifier_state();
|
2023-08-11 18:15:22 +02:00
|
|
|
if let Some((action, pattern)) = keyboard
|
2024-09-13 10:07:46 -07:00
|
|
|
.input(
|
|
|
|
|
self,
|
|
|
|
|
keycode,
|
|
|
|
|
state,
|
|
|
|
|
serial,
|
|
|
|
|
time,
|
|
|
|
|
|data, modifiers, handle| {
|
2025-02-24 21:43:16 +01:00
|
|
|
if previous_modifiers != *modifiers {
|
|
|
|
|
*seat
|
|
|
|
|
.user_data()
|
|
|
|
|
.get::<LastModifierChange>()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.0
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap() = Some(serial);
|
|
|
|
|
}
|
2025-03-28 17:45:28 +01:00
|
|
|
|
|
|
|
|
let current_focus = seat.get_keyboard().unwrap().current_focus();
|
|
|
|
|
let shortcuts_inhibited = current_focus.as_ref().is_some_and(|f| {
|
|
|
|
|
f.wl_surface()
|
2025-08-15 13:14:13 +02:00
|
|
|
.map(|surface| {
|
2025-03-28 17:45:28 +01:00
|
|
|
seat.keyboard_shortcuts_inhibitor_for_surface(&surface)
|
|
|
|
|
.map(|inhibitor| inhibitor.is_active())
|
2025-08-15 13:14:13 +02:00
|
|
|
.unwrap_or(false)
|
|
|
|
|
|| seat.has_active_xwayland_grab(&surface)
|
2025-03-28 17:45:28 +01:00
|
|
|
})
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
});
|
|
|
|
|
let sym = handle.modified_sym();
|
|
|
|
|
|
|
|
|
|
let result = Self::filter_keyboard_input(
|
2024-09-18 13:20:17 -07:00
|
|
|
data, &event, &seat, modifiers, handle, serial,
|
2025-03-28 17:45:28 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (matches!(result, FilterResult::Forward)
|
|
|
|
|
&& !seat.get_keyboard().unwrap().is_grabbed()
|
|
|
|
|
&& !shortcuts_inhibited
|
|
|
|
|
&& !matches!(
|
|
|
|
|
current_focus,
|
|
|
|
|
Some(KeyboardFocusTarget::LockSurface(_))
|
|
|
|
|
))
|
|
|
|
|
// we don't want to accidentally leave any keys pressed
|
|
|
|
|
// and do more filtering in `xwayland_notify_key_event`
|
|
|
|
|
// for released keys
|
|
|
|
|
|| state == KeyState::Released
|
|
|
|
|
{
|
|
|
|
|
data.common.xwayland_notify_key_event(
|
|
|
|
|
sym, keycode, state, serial, time,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result
|
2024-09-13 10:07:46 -07:00
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.flatten()
|
|
|
|
|
{
|
|
|
|
|
if pattern.key.is_none() && state == KeyState::Released {
|
|
|
|
|
// we still want to send release-events and not have apps stuck on some modifiers.
|
|
|
|
|
keyboard.input(self, keycode, state, serial, time, |_, _, _| {
|
|
|
|
|
FilterResult::<()>::Forward
|
|
|
|
|
});
|
2022-03-30 23:24:29 +02:00
|
|
|
}
|
2025-02-26 17:13:31 +01:00
|
|
|
self.handle_action(action, &seat, serial, time, pattern, None)
|
2024-09-13 10:07:46 -07:00
|
|
|
}
|
2025-02-12 05:35:22 -08:00
|
|
|
|
|
|
|
|
// If we want to track numlock state so it can be reused on the next boot...
|
|
|
|
|
if let NumlockState::LastBoot =
|
|
|
|
|
self.common.config.cosmic_conf.keyboard_config.numlock_state
|
|
|
|
|
{
|
|
|
|
|
// .. and the state has been updated ...
|
|
|
|
|
if self.common.config.dynamic_conf.numlock().last_state
|
|
|
|
|
!= keyboard.modifier_state().num_lock
|
|
|
|
|
{
|
|
|
|
|
// ... then record the updated state.
|
|
|
|
|
// The call to `numlock_mut` will generate a `PersistenceGuard`. The
|
|
|
|
|
// `PersistenceGuard` will write to a file when it's dropped here.
|
|
|
|
|
self.common.config.dynamic_conf.numlock_mut().last_state =
|
|
|
|
|
keyboard.modifier_state().num_lock;
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-12-22 20:14:09 +01:00
|
|
|
}
|
|
|
|
|
}
|
2024-09-09 16:38:52 +02:00
|
|
|
|
2021-12-22 20:14:09 +01:00
|
|
|
InputEvent::PointerMotion { event, .. } => {
|
|
|
|
|
use smithay::backend::input::PointerMotionEvent;
|
|
|
|
|
|
2025-10-16 15:46:54 +02:00
|
|
|
let shell = self.common.shell.write();
|
2024-04-10 15:49:08 +02:00
|
|
|
if let Some(seat) = shell.seats.for_device(&event.device()).cloned() {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
cursor: Add idle-hide timeout
Adds a cursor_hide_timeout config key (Option<u32> seconds) to
CosmicCompConfig. When set, the cursor is hidden after the configured
period of pointer inactivity and revealed again by any pointer event.
Touch input does not count as activity (no visible cursor to surface).
Implementation:
- Per-seat hidden flag, calloop timer token, and last-armed Instant on
CursorStateInner.
- notify_cursor_activity called from each pointer-related input branch
(motion, button, axis, tablet) resets the flag and reschedules the
timer; rapid successive calls are coalesced behind a 100ms throttle
so high-frequency mice don't churn the calloop timer source.
- On timer fire, the hidden flag is set, draw_cursor short-circuits to
an empty element list, and a render is scheduled. Active pointer
grabs (drags, resizes) suppress the hide.
- Config reload arms or cancels the timer immediately; None as the
configured value collapses the cancel path into the same function.
Closes #2231.
Drafted with Claude (Anthropic); reviewed and tested by the committer.
2026-05-20 23:52:45 +02:00
|
|
|
notify_cursor_activity(self, &seat);
|
2023-09-05 13:41:21 -07:00
|
|
|
let current_output = seat.active_output();
|
2022-11-03 18:51:27 +01:00
|
|
|
|
2023-10-25 19:24:51 +02:00
|
|
|
let mut position = seat.get_pointer().unwrap().current_location().as_global();
|
2023-09-13 20:52:10 -07:00
|
|
|
|
2025-10-16 15:46:54 +02:00
|
|
|
let under = State::surface_under(position, ¤t_output, &shell)
|
2024-04-10 15:49:08 +02:00
|
|
|
.map(|(target, pos)| (target, pos.as_logical()));
|
2023-09-13 20:52:10 -07:00
|
|
|
|
|
|
|
|
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
|
2025-10-16 13:50:32 +02:00
|
|
|
if !constraint.region().is_none_or(|x| {
|
2024-04-03 16:02:27 +02:00
|
|
|
x.contains(
|
2026-07-21 10:32:09 +02:00
|
|
|
(ptr.current_location() - *surface_loc).to_i32_floor(),
|
2024-04-03 16:02:27 +02:00
|
|
|
)
|
2023-09-13 20:52:10 -07:00
|
|
|
}) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
match &*constraint {
|
|
|
|
|
PointerConstraint::Locked(_locked) => {
|
|
|
|
|
pointer_locked = true;
|
|
|
|
|
}
|
|
|
|
|
PointerConstraint::Confined(confine) => {
|
|
|
|
|
pointer_confined = true;
|
|
|
|
|
confine_region = confine.region().cloned();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
});
|
|
|
|
|
}
|
2024-04-10 15:49:08 +02:00
|
|
|
|
|
|
|
|
std::mem::drop(shell);
|
2023-09-13 20:52:10 -07:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 20:05:39 +08:00
|
|
|
let original_position = position;
|
|
|
|
|
position += event.delta().as_global();
|
|
|
|
|
let shell = self.common.shell.read();
|
|
|
|
|
let output = shell
|
|
|
|
|
.outputs()
|
|
|
|
|
.find(|output| output.geometry().to_f64().contains(position))
|
|
|
|
|
.cloned()
|
|
|
|
|
.unwrap_or(current_output.clone());
|
|
|
|
|
drop(shell);
|
|
|
|
|
let output_geometry = output.geometry();
|
2026-07-21 10:32:09 +02:00
|
|
|
|
|
|
|
|
let scale = output.current_scale().fractional_scale();
|
|
|
|
|
let physical = output
|
|
|
|
|
.current_mode()
|
|
|
|
|
.map(|mode| output.current_transform().transform_size(mode.size))
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
let logical = physical.to_f64().to_logical(scale);
|
|
|
|
|
let output_geometry_loc = output_geometry.loc.to_f64();
|
|
|
|
|
// output_geometry.size is a rounded value and may undershoot/overshoot the accurate logical size
|
|
|
|
|
// We constrain the position with:
|
|
|
|
|
// - output_geometry.size so that we don't send leave events to a fullscreen app
|
|
|
|
|
// - logical size so that the position doesn't end up outside the actual size of the output
|
|
|
|
|
// See https://github.com/pop-os/cosmic-comp/pull/2568
|
|
|
|
|
let max_x = output_geometry_loc.x
|
|
|
|
|
+ logical.w.min(output_geometry.size.w as f64).next_down();
|
|
|
|
|
let max_y = output_geometry_loc.y
|
|
|
|
|
+ logical.h.min(output_geometry.size.h as f64).next_down();
|
|
|
|
|
position.x = position.x.clamp(output_geometry_loc.x, max_x);
|
|
|
|
|
position.y = position.y.clamp(output_geometry_loc.y, max_y);
|
2026-05-13 20:05:39 +08:00
|
|
|
|
2024-09-04 11:13:59 -05:00
|
|
|
if ptr.is_grabbed() {
|
|
|
|
|
if seat
|
2023-10-23 21:09:38 +02:00
|
|
|
.user_data()
|
|
|
|
|
.get::<ResizeGrabMarker>()
|
|
|
|
|
.map(|marker| marker.get())
|
2025-10-16 15:46:54 +02:00
|
|
|
.unwrap_or(false)
|
|
|
|
|
&& output != current_output
|
|
|
|
|
{
|
2025-10-16 13:50:32 +02:00
|
|
|
ptr.frame(self);
|
|
|
|
|
return;
|
2024-09-04 11:13:59 -05:00
|
|
|
}
|
|
|
|
|
//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 {
|
2025-05-20 17:41:27 +02:00
|
|
|
let shell = self.common.shell.read();
|
2025-10-16 15:46:54 +02:00
|
|
|
let old_keyboard_target =
|
|
|
|
|
State::element_under(original_position, ¤t_output, &shell, &seat);
|
2025-06-25 17:54:27 +02:00
|
|
|
let new_keyboard_target =
|
2025-10-16 13:50:32 +02:00
|
|
|
State::element_under(position, &output, &shell, &seat);
|
2024-09-04 11:13:59 -05:00
|
|
|
|
|
|
|
|
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(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-10-23 21:09:38 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 20:05:39 +08:00
|
|
|
// If confined, help user to update valid coordinates can improve user experience
|
|
|
|
|
let shell = self.common.shell.read();
|
|
|
|
|
let new_under = if pointer_confined && let Some((surface, surface_loc)) = &under
|
|
|
|
|
{
|
|
|
|
|
let is_legal = |pos: Point<f64, Global>, shell: &Shell| {
|
|
|
|
|
let new_under = State::surface_under(pos, &output, shell)
|
|
|
|
|
.map(|(target, pos)| (target, pos.as_logical()));
|
|
|
|
|
|
2026-05-13 20:08:21 +08:00
|
|
|
// TODO: We might need a solution that allows constraints to bypass the surface without affecting the constraints themselves
|
|
|
|
|
if new_under.as_ref().and_then(|(under, _)| under.wl_surface())
|
|
|
|
|
!= surface.wl_surface()
|
|
|
|
|
{
|
|
|
|
|
return (false, None);
|
|
|
|
|
}
|
2026-05-13 20:05:39 +08:00
|
|
|
|
|
|
|
|
match surface {
|
|
|
|
|
PointerFocusTarget::WlSurface { surface, .. } => {
|
|
|
|
|
if under_from_surface_tree(
|
|
|
|
|
surface,
|
2025-11-10 18:32:34 +01:00
|
|
|
position.as_logical() - surface_loc.to_f64(),
|
|
|
|
|
(0, 0),
|
|
|
|
|
WindowSurfaceType::ALL,
|
|
|
|
|
)
|
|
|
|
|
.is_none()
|
2026-05-13 20:05:39 +08:00
|
|
|
{
|
|
|
|
|
return (false, None);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
PointerFocusTarget::X11Surface { surface, .. } => {
|
|
|
|
|
if surface
|
|
|
|
|
.surface_under(
|
|
|
|
|
position.as_logical() - surface_loc.to_f64(),
|
|
|
|
|
(0, 0),
|
|
|
|
|
WindowSurfaceType::ALL,
|
|
|
|
|
)
|
|
|
|
|
.is_none()
|
|
|
|
|
{
|
|
|
|
|
return (false, None);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(region) = &confine_region
|
|
|
|
|
&& !region
|
2026-07-21 10:32:09 +02:00
|
|
|
.contains((pos.as_logical() - *surface_loc).to_i32_floor())
|
2026-05-13 20:05:39 +08:00
|
|
|
{
|
|
|
|
|
return (false, None);
|
|
|
|
|
}
|
|
|
|
|
(true, new_under)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match is_legal(position, &shell) {
|
|
|
|
|
(true, under) => under,
|
|
|
|
|
_ => {
|
|
|
|
|
let y_only_pos = Point::new(original_position.x, position.y);
|
|
|
|
|
let x_only_pos = Point::new(position.x, original_position.y);
|
|
|
|
|
match is_legal(y_only_pos, &shell) {
|
|
|
|
|
(true, under) => {
|
|
|
|
|
position = y_only_pos;
|
|
|
|
|
under
|
|
|
|
|
}
|
|
|
|
|
_ => match is_legal(x_only_pos, &shell) {
|
|
|
|
|
(true, under) => {
|
|
|
|
|
position = x_only_pos;
|
|
|
|
|
under
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
position = original_position;
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
},
|
2023-09-13 20:52:10 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-13 20:05:39 +08:00
|
|
|
} else {
|
|
|
|
|
State::surface_under(position, &output, &shell)
|
|
|
|
|
.map(|(target, pos)| (target, pos.as_logical()))
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
drop(shell);
|
|
|
|
|
if pointer_confined && new_under.is_none() {
|
|
|
|
|
ptr.frame(self);
|
|
|
|
|
return;
|
2023-09-13 20:52:10 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let serial = SERIAL_COUNTER.next_serial();
|
|
|
|
|
ptr.motion(
|
|
|
|
|
self,
|
|
|
|
|
under,
|
|
|
|
|
&MotionEvent {
|
2023-10-25 19:24:51 +02:00
|
|
|
location: position.as_logical(),
|
2023-09-13 20:52:10 -07:00
|
|
|
serial,
|
|
|
|
|
time: event.time_msec(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
ptr.frame(self);
|
|
|
|
|
|
2026-05-13 20:05:39 +08:00
|
|
|
// If pointer is now in a constraint region and window is in focused, activate it
|
2024-05-13 14:16:21 -07:00
|
|
|
if let Some((under, surface_location)) = new_under
|
|
|
|
|
.and_then(|(target, loc)| Some((target.wl_surface()?.into_owned(), loc)))
|
2023-09-13 20:52:10 -07:00
|
|
|
{
|
2026-05-13 20:05:39 +08:00
|
|
|
let shell = self.common.shell.read();
|
|
|
|
|
let is_focused = seat
|
|
|
|
|
.get_keyboard()
|
|
|
|
|
.and_then(|k| k.current_focus())
|
|
|
|
|
.is_some_and(|f| f.has_surface(&shell, &under));
|
|
|
|
|
|
|
|
|
|
if is_focused {
|
|
|
|
|
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 =
|
2026-07-21 10:32:09 +02:00
|
|
|
(ptr.current_location() - surface_location).to_i32_floor();
|
2026-05-13 20:05:39 +08:00
|
|
|
if region.is_none_or(|region| region.contains(point)) {
|
|
|
|
|
constraint.activate();
|
|
|
|
|
}
|
2023-09-13 20:52:10 -07:00
|
|
|
}
|
2026-05-13 20:05:39 +08:00
|
|
|
_ => {}
|
|
|
|
|
});
|
|
|
|
|
}
|
2023-09-13 20:52:10 -07:00
|
|
|
}
|
|
|
|
|
|
2025-05-20 17:41:27 +02:00
|
|
|
let mut shell = self.common.shell.write();
|
2024-10-10 23:18:04 +02:00
|
|
|
shell.update_pointer_position(position.to_local(&output), &output);
|
2025-01-24 18:07:33 +01:00
|
|
|
shell.update_focal_point(
|
|
|
|
|
&seat,
|
|
|
|
|
original_position,
|
|
|
|
|
self.common.config.cosmic_conf.accessibility_zoom.view_moves,
|
|
|
|
|
);
|
2024-04-10 15:49:08 +02:00
|
|
|
|
2023-09-05 13:41:21 -07:00
|
|
|
if output != current_output {
|
2025-10-16 13:50:32 +02:00
|
|
|
for session in cursor_sessions_for_output(&shell, ¤t_output) {
|
2024-03-12 19:42:48 +01:00
|
|
|
session.set_cursor_pos(None);
|
2022-11-03 18:51:27 +01:00
|
|
|
}
|
2023-09-05 13:41:21 -07:00
|
|
|
seat.set_active_output(&output);
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-10 15:49:08 +02:00
|
|
|
for session in cursor_sessions_for_output(&shell, &output) {
|
2026-06-15 16:23:31 -07:00
|
|
|
if let Some(cursor_geometry) = seat.cursor_geometry(
|
2026-05-05 18:44:26 -07:00
|
|
|
(position - output_geometry.loc.to_f64())
|
|
|
|
|
.as_logical()
|
|
|
|
|
.to_buffer(
|
|
|
|
|
output.current_scale().fractional_scale(),
|
|
|
|
|
output.current_transform(),
|
|
|
|
|
&output_geometry.size.to_f64().as_logical(),
|
|
|
|
|
),
|
2023-09-05 13:41:21 -07:00
|
|
|
self.common.clock.now(),
|
|
|
|
|
) {
|
2026-06-15 16:23:31 -07:00
|
|
|
let constraints = cursor_capture_constraints(Some(cursor_geometry));
|
2024-03-12 19:42:48 +01:00
|
|
|
if session
|
|
|
|
|
.current_constraints()
|
2026-06-15 16:23:31 -07:00
|
|
|
.map(|current_constraints| {
|
|
|
|
|
current_constraints.size != constraints.size
|
|
|
|
|
})
|
2024-03-12 19:42:48 +01:00
|
|
|
.unwrap_or(true)
|
|
|
|
|
{
|
2026-06-15 16:23:31 -07:00
|
|
|
session.update_constraints(constraints);
|
2024-03-12 19:42:48 +01:00
|
|
|
}
|
2026-06-15 16:23:31 -07:00
|
|
|
session.set_cursor_hotspot(cursor_geometry.hotspot);
|
|
|
|
|
session.set_cursor_pos(Some(cursor_geometry.geometry.loc));
|
2023-03-06 18:48:52 +01:00
|
|
|
}
|
2023-09-05 13:41:21 -07:00
|
|
|
}
|
2021-12-22 20:14:09 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
InputEvent::PointerMotionAbsolute { event, .. } => {
|
2024-04-10 15:49:08 +02:00
|
|
|
let maybe_seat = self
|
|
|
|
|
.common
|
|
|
|
|
.shell
|
|
|
|
|
.read()
|
|
|
|
|
.seats
|
|
|
|
|
.for_device(&event.device())
|
|
|
|
|
.cloned();
|
|
|
|
|
if let Some(seat) = maybe_seat {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
cursor: Add idle-hide timeout
Adds a cursor_hide_timeout config key (Option<u32> seconds) to
CosmicCompConfig. When set, the cursor is hidden after the configured
period of pointer inactivity and revealed again by any pointer event.
Touch input does not count as activity (no visible cursor to surface).
Implementation:
- Per-seat hidden flag, calloop timer token, and last-armed Instant on
CursorStateInner.
- notify_cursor_activity called from each pointer-related input branch
(motion, button, axis, tablet) resets the flag and reschedules the
timer; rapid successive calls are coalesced behind a 100ms throttle
so high-frequency mice don't churn the calloop timer source.
- On timer fire, the hidden flag is set, draw_cursor short-circuits to
an empty element list, and a render is scheduled. Active pointer
grabs (drags, resizes) suppress the hide.
- Config reload arms or cancels the timer immediately; None as the
configured value collapses the cancel path into the same function.
Closes #2231.
Drafted with Claude (Anthropic); reviewed and tested by the committer.
2026-05-20 23:52:45 +02:00
|
|
|
notify_cursor_activity(self, &seat);
|
2023-09-05 13:41:21 -07:00
|
|
|
let output = seat.active_output();
|
2026-05-05 18:44:26 -07:00
|
|
|
let output_geometry = output.geometry();
|
|
|
|
|
let position = output_geometry.loc.to_f64()
|
2023-09-05 13:41:21 -07:00
|
|
|
+ smithay::backend::input::AbsolutePositionEvent::position_transformed(
|
|
|
|
|
&event,
|
2026-05-05 18:44:26 -07:00
|
|
|
output_geometry.size.as_logical(),
|
2023-10-25 19:24:51 +02:00
|
|
|
)
|
|
|
|
|
.as_global();
|
2023-09-05 13:41:21 -07:00
|
|
|
let serial = SERIAL_COUNTER.next_serial();
|
2025-10-16 15:46:54 +02:00
|
|
|
let under = State::surface_under(position, &output, &self.common.shell.write())
|
|
|
|
|
.map(|(target, pos)| (target, pos.as_logical()));
|
2024-04-10 15:49:08 +02:00
|
|
|
|
|
|
|
|
let ptr = seat.get_pointer().unwrap();
|
|
|
|
|
ptr.motion(
|
|
|
|
|
self,
|
|
|
|
|
under,
|
|
|
|
|
&MotionEvent {
|
|
|
|
|
location: position.as_logical(),
|
|
|
|
|
serial,
|
|
|
|
|
time: event.time_msec(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
ptr.frame(self);
|
2023-09-05 13:41:21 -07:00
|
|
|
|
2025-05-20 17:41:27 +02:00
|
|
|
let shell = self.common.shell.read();
|
2025-10-16 13:50:32 +02:00
|
|
|
for session in cursor_sessions_for_output(&shell, &output) {
|
2026-06-15 16:23:31 -07:00
|
|
|
if let Some(cursor_geometry) = seat.cursor_geometry(
|
2026-05-05 18:44:26 -07:00
|
|
|
(position - output_geometry.loc.to_f64())
|
|
|
|
|
.as_logical()
|
|
|
|
|
.to_buffer(
|
|
|
|
|
output.current_scale().fractional_scale(),
|
|
|
|
|
output.current_transform(),
|
|
|
|
|
&output_geometry.size.to_f64().as_logical(),
|
|
|
|
|
),
|
2023-09-05 13:41:21 -07:00
|
|
|
self.common.clock.now(),
|
|
|
|
|
) {
|
2026-06-15 16:23:31 -07:00
|
|
|
let constraints = cursor_capture_constraints(Some(cursor_geometry));
|
2024-03-12 19:42:48 +01:00
|
|
|
if session
|
|
|
|
|
.current_constraints()
|
2026-06-15 16:23:31 -07:00
|
|
|
.map(|current_constraints| {
|
|
|
|
|
current_constraints.size != constraints.size
|
|
|
|
|
})
|
2024-03-12 19:42:48 +01:00
|
|
|
.unwrap_or(true)
|
|
|
|
|
{
|
2026-06-15 16:23:31 -07:00
|
|
|
session.update_constraints(constraints);
|
2024-03-12 19:42:48 +01:00
|
|
|
}
|
2026-06-15 16:23:31 -07:00
|
|
|
session.set_cursor_hotspot(cursor_geometry.hotspot);
|
|
|
|
|
session.set_cursor_pos(Some(cursor_geometry.geometry.loc));
|
2023-03-06 18:48:52 +01:00
|
|
|
}
|
2023-09-05 13:41:21 -07:00
|
|
|
}
|
2021-12-22 20:14:09 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
InputEvent::PointerButton { event, .. } => {
|
2022-08-31 13:01:23 +02:00
|
|
|
use smithay::backend::input::{ButtonState, PointerButtonEvent};
|
2021-12-22 20:14:09 +01:00
|
|
|
|
2024-09-04 11:13:59 -05:00
|
|
|
//
|
|
|
|
|
let Some(seat) = self
|
|
|
|
|
.common
|
|
|
|
|
.shell
|
|
|
|
|
.read()
|
|
|
|
|
.seats
|
|
|
|
|
.for_device(&event.device())
|
|
|
|
|
.cloned()
|
|
|
|
|
else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
cursor: Add idle-hide timeout
Adds a cursor_hide_timeout config key (Option<u32> seconds) to
CosmicCompConfig. When set, the cursor is hidden after the configured
period of pointer inactivity and revealed again by any pointer event.
Touch input does not count as activity (no visible cursor to surface).
Implementation:
- Per-seat hidden flag, calloop timer token, and last-armed Instant on
CursorStateInner.
- notify_cursor_activity called from each pointer-related input branch
(motion, button, axis, tablet) resets the flag and reschedules the
timer; rapid successive calls are coalesced behind a 100ms throttle
so high-frequency mice don't churn the calloop timer source.
- On timer fire, the hidden flag is set, draw_cursor short-circuits to
an empty element list, and a render is scheduled. Active pointer
grabs (drags, resizes) suppress the hide.
- Config reload arms or cancels the timer immediately; None as the
configured value collapses the cancel path into the same function.
Closes #2231.
Drafted with Claude (Anthropic); reviewed and tested by the committer.
2026-05-20 23:52:45 +02:00
|
|
|
notify_cursor_activity(self, &seat);
|
2024-09-04 11:13:59 -05:00
|
|
|
|
2024-09-18 13:57:10 +02:00
|
|
|
let current_focus = seat.get_keyboard().unwrap().current_focus();
|
2025-03-28 17:45:28 +01:00
|
|
|
let shortcuts_inhibited = current_focus.as_ref().is_some_and(|f| {
|
2024-09-18 13:57:10 +02:00
|
|
|
f.wl_surface()
|
2025-08-15 13:14:13 +02:00
|
|
|
.map(|surface| {
|
2024-09-18 13:57:10 +02:00
|
|
|
seat.keyboard_shortcuts_inhibitor_for_surface(&surface)
|
|
|
|
|
.map(|inhibitor| inhibitor.is_active())
|
2025-08-15 13:14:13 +02:00
|
|
|
.unwrap_or(false)
|
|
|
|
|
|| seat.has_active_xwayland_grab(&surface)
|
2024-09-18 13:57:10 +02:00
|
|
|
})
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
});
|
|
|
|
|
|
2024-09-04 11:13:59 -05:00
|
|
|
let serial = SERIAL_COUNTER.next_serial();
|
|
|
|
|
let button = event.button_code();
|
2025-03-28 17:45:28 +01:00
|
|
|
|
2024-09-04 11:13:59 -05:00
|
|
|
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();
|
2024-10-10 23:18:04 +02:00
|
|
|
let under = {
|
2025-05-20 17:41:27 +02:00
|
|
|
let shell = self.common.shell.read();
|
2025-06-25 17:54:27 +02:00
|
|
|
State::element_under(global_position, &output, &shell, &seat)
|
2024-10-10 23:18:04 +02:00
|
|
|
};
|
2026-07-10 18:50:45 -06:00
|
|
|
// Grabbing a tiling resize handle (the gap between tiles) must not change keyboard focus
|
|
|
|
|
let on_resize_fork = matches!(
|
|
|
|
|
seat.get_pointer().unwrap().current_focus(),
|
|
|
|
|
Some(PointerFocusTarget::ResizeFork(_))
|
|
|
|
|
);
|
|
|
|
|
if let Some(target) = under.filter(|_| !on_resize_fork) {
|
2026-02-23 16:25:06 +01:00
|
|
|
if let Some(surface) = target.toplevel().map(Cow::into_owned)
|
|
|
|
|
&& 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);
|
|
|
|
|
};
|
2024-09-09 17:50:19 +02:00
|
|
|
|
2026-02-23 16:25:06 +01:00
|
|
|
fn dispatch_grab<G: PointerGrab<State> + 'static>(
|
|
|
|
|
grab: Option<(G, smithay::input::pointer::Focus)>,
|
|
|
|
|
seat: Seat<State>,
|
|
|
|
|
serial: Serial,
|
|
|
|
|
state: &mut State,
|
|
|
|
|
) {
|
|
|
|
|
if let Some((target, focus)) = grab {
|
|
|
|
|
seat.modifiers_shortcut_queue().clear();
|
2024-09-09 17:50:19 +02:00
|
|
|
|
2026-02-23 16:25:06 +01:00
|
|
|
seat.get_pointer()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.set_grab(state, target, serial, focus);
|
2024-09-09 17:50:19 +02:00
|
|
|
}
|
2026-02-23 16:25:06 +01:00
|
|
|
}
|
2024-08-21 21:29:23 +01:00
|
|
|
|
2026-02-23 16:25:06 +01:00
|
|
|
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();
|
|
|
|
|
let res = shell.move_request(
|
|
|
|
|
&surface,
|
|
|
|
|
&seat_clone,
|
|
|
|
|
serial,
|
|
|
|
|
ReleaseMode::NoMouseButtons,
|
|
|
|
|
false,
|
|
|
|
|
&state.common.config,
|
|
|
|
|
&state.common.event_loop_handle,
|
|
|
|
|
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();
|
|
|
|
|
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)
|
|
|
|
|
})
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
shell
|
|
|
|
|
.workspaces
|
|
|
|
|
.sets
|
|
|
|
|
.get(&output)
|
|
|
|
|
.and_then(|set| {
|
|
|
|
|
set.sticky_layer
|
|
|
|
|
.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) {
|
2024-09-09 17:50:19 +02:00
|
|
|
(true, true) => ResizeEdge::TOP_LEFT,
|
|
|
|
|
(false, true) => ResizeEdge::TOP_RIGHT,
|
|
|
|
|
(true, false) => {
|
|
|
|
|
ResizeEdge::BOTTOM_LEFT
|
|
|
|
|
}
|
|
|
|
|
(false, false) => {
|
|
|
|
|
ResizeEdge::BOTTOM_RIGHT
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-02-23 16:25:06 +01:00
|
|
|
let res = shell.resize_request(
|
|
|
|
|
&surface,
|
|
|
|
|
&seat_clone,
|
|
|
|
|
serial,
|
|
|
|
|
edge,
|
|
|
|
|
state
|
|
|
|
|
.common
|
|
|
|
|
.config
|
|
|
|
|
.cosmic_conf
|
|
|
|
|
.edge_snap_threshold,
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
drop(shell);
|
|
|
|
|
dispatch_grab(res, seat_clone, serial, state);
|
|
|
|
|
},
|
|
|
|
|
);
|
2024-03-27 18:35:04 +01:00
|
|
|
}
|
2026-02-23 16:25:06 +01:00
|
|
|
_ => {}
|
2023-07-21 16:08:55 +02:00
|
|
|
}
|
2021-12-22 20:14:09 +01:00
|
|
|
}
|
|
|
|
|
}
|
2024-08-21 21:29:23 +01:00
|
|
|
|
2024-10-10 23:18:04 +02:00
|
|
|
Shell::set_focus(self, Some(&target), &seat, Some(serial), false);
|
|
|
|
|
}
|
2024-08-21 21:29:23 +01:00
|
|
|
}
|
2024-09-04 11:13:59 -05:00
|
|
|
} else {
|
2025-05-20 17:41:27 +02:00
|
|
|
let mut shell = self.common.shell.write();
|
2024-09-04 11:13:59 -05:00
|
|
|
if let Some(Trigger::Pointer(action_button)) =
|
|
|
|
|
shell.overview_mode().0.active_trigger()
|
2026-02-23 16:25:06 +01:00
|
|
|
&& *action_button == button
|
2024-09-04 11:13:59 -05:00
|
|
|
{
|
2026-02-23 16:25:06 +01:00
|
|
|
shell.set_overview_mode(None, self.common.event_loop_handle.clone());
|
2024-09-04 11:13:59 -05:00
|
|
|
}
|
|
|
|
|
std::mem::drop(shell);
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-28 17:45:28 +01:00
|
|
|
if pass_event
|
|
|
|
|
&& !matches!(current_focus, Some(KeyboardFocusTarget::LockSurface(_)))
|
|
|
|
|
&& !shortcuts_inhibited
|
|
|
|
|
{
|
|
|
|
|
self.common.xwayland_notify_pointer_button_event(
|
|
|
|
|
button,
|
|
|
|
|
event.state(),
|
|
|
|
|
serial,
|
|
|
|
|
event.time_msec(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-04 11:13:59 -05:00
|
|
|
let ptr = seat.get_pointer().unwrap();
|
2024-09-09 20:01:59 +02:00
|
|
|
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())
|
|
|
|
|
}
|
2021-12-22 20:14:09 +01:00
|
|
|
}
|
|
|
|
|
InputEvent::PointerAxis { event, .. } => {
|
2023-12-28 13:36:59 -08:00
|
|
|
let scroll_factor =
|
|
|
|
|
if let Some(device) = <dyn Any>::downcast_ref::<InputDevice>(&event.device()) {
|
|
|
|
|
self.common.config.scroll_factor(device)
|
|
|
|
|
} else {
|
|
|
|
|
1.0
|
|
|
|
|
};
|
2023-09-01 12:33:55 -07:00
|
|
|
|
2024-04-10 15:49:08 +02:00
|
|
|
let maybe_seat = self
|
|
|
|
|
.common
|
|
|
|
|
.shell
|
|
|
|
|
.read()
|
|
|
|
|
.seats
|
|
|
|
|
.for_device(&event.device())
|
|
|
|
|
.cloned();
|
|
|
|
|
if let Some(seat) = maybe_seat {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
cursor: Add idle-hide timeout
Adds a cursor_hide_timeout config key (Option<u32> seconds) to
CosmicCompConfig. When set, the cursor is hidden after the configured
period of pointer inactivity and revealed again by any pointer event.
Touch input does not count as activity (no visible cursor to surface).
Implementation:
- Per-seat hidden flag, calloop timer token, and last-armed Instant on
CursorStateInner.
- notify_cursor_activity called from each pointer-related input branch
(motion, button, axis, tablet) resets the flag and reschedules the
timer; rapid successive calls are coalesced behind a 100ms throttle
so high-frequency mice don't churn the calloop timer source.
- On timer fire, the hidden flag is set, draw_cursor short-circuits to
an empty element list, and a render is scheduled. Active pointer
grabs (drags, resizes) suppress the hide.
- Config reload arms or cancels the timer immediately; None as the
configured value collapses the cancel path into the same function.
Closes #2231.
Drafted with Claude (Anthropic); reviewed and tested by the committer.
2026-05-20 23:52:45 +02:00
|
|
|
notify_cursor_activity(self, &seat);
|
2023-03-06 18:48:52 +01:00
|
|
|
|
2025-03-25 14:42:20 +01:00
|
|
|
if seat.get_keyboard().unwrap().modifier_state().logo
|
|
|
|
|
&& self
|
|
|
|
|
.common
|
|
|
|
|
.config
|
|
|
|
|
.cosmic_conf
|
|
|
|
|
.accessibility_zoom
|
|
|
|
|
.enable_mouse_zoom_shortcuts
|
|
|
|
|
{
|
2025-03-25 18:30:55 +01:00
|
|
|
seat.modifiers_shortcut_queue().clear();
|
2025-02-05 18:05:08 +01:00
|
|
|
if let Some(mut percentage) = event
|
|
|
|
|
.amount_v120(Axis::Vertical)
|
|
|
|
|
.map(|val| val / 120.)
|
|
|
|
|
.or_else(|| event.amount(Axis::Vertical))
|
|
|
|
|
.map(|val| val * scroll_factor)
|
|
|
|
|
{
|
2026-05-15 15:51:24 -06:00
|
|
|
if event.relative_direction(Axis::Vertical)
|
|
|
|
|
== AxisRelativeDirection::Inverted
|
|
|
|
|
{
|
|
|
|
|
percentage *= -1.;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-05 18:05:08 +01:00
|
|
|
if event.source() == AxisSource::Wheel {
|
|
|
|
|
percentage *= 5.;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-16 13:50:32 +02:00
|
|
|
let change = -(percentage / 100.);
|
2026-05-19 16:30:46 -06:00
|
|
|
self.update_zoom(&seat, change, event.source() == AxisSource::Wheel);
|
2023-09-05 13:41:21 -07:00
|
|
|
}
|
2025-02-05 18:05:08 +01:00
|
|
|
} else {
|
|
|
|
|
let mut frame = AxisFrame::new(event.time_msec()).source(event.source());
|
|
|
|
|
if let Some(horizontal_amount) = event.amount(Axis::Horizontal) {
|
|
|
|
|
if horizontal_amount != 0.0 {
|
|
|
|
|
frame = frame
|
2026-06-14 02:54:22 +06:00
|
|
|
.relative_direction(
|
|
|
|
|
Axis::Horizontal,
|
|
|
|
|
event.relative_direction(Axis::Horizontal),
|
|
|
|
|
)
|
2025-02-05 18:05:08 +01:00
|
|
|
.value(Axis::Horizontal, scroll_factor * horizontal_amount);
|
|
|
|
|
if let Some(discrete) = event.amount_v120(Axis::Horizontal) {
|
|
|
|
|
frame = frame.v120(
|
|
|
|
|
Axis::Horizontal,
|
|
|
|
|
(discrete * scroll_factor).round() as i32,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else if event.source() == AxisSource::Finger {
|
|
|
|
|
frame = frame.stop(Axis::Horizontal);
|
2021-12-22 20:14:09 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-02-05 18:05:08 +01:00
|
|
|
if let Some(vertical_amount) = event.amount(Axis::Vertical) {
|
|
|
|
|
if vertical_amount != 0.0 {
|
2026-06-14 02:54:22 +06:00
|
|
|
frame = frame
|
|
|
|
|
.relative_direction(
|
|
|
|
|
Axis::Vertical,
|
|
|
|
|
event.relative_direction(Axis::Vertical),
|
|
|
|
|
)
|
|
|
|
|
.value(Axis::Vertical, scroll_factor * vertical_amount);
|
2025-02-05 18:05:08 +01:00
|
|
|
if let Some(discrete) = event.amount_v120(Axis::Vertical) {
|
|
|
|
|
frame = frame.v120(
|
|
|
|
|
Axis::Vertical,
|
|
|
|
|
(discrete * scroll_factor).round() as i32,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else if event.source() == AxisSource::Finger {
|
|
|
|
|
frame = frame.stop(Axis::Vertical);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let ptr = seat.get_pointer().unwrap();
|
|
|
|
|
ptr.axis(self, frame);
|
|
|
|
|
ptr.frame(self);
|
2021-12-22 20:14:09 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-09-09 16:38:52 +02:00
|
|
|
|
2023-09-05 13:41:21 -07:00
|
|
|
InputEvent::GestureSwipeBegin { event, .. } => {
|
2024-04-10 15:49:08 +02:00
|
|
|
let maybe_seat = self
|
|
|
|
|
.common
|
|
|
|
|
.shell
|
|
|
|
|
.read()
|
|
|
|
|
.seats
|
|
|
|
|
.for_device(&event.device())
|
|
|
|
|
.cloned();
|
|
|
|
|
if let Some(seat) = maybe_seat {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
2024-07-12 18:30:41 -07:00
|
|
|
if event.fingers() >= 3 && !workspace_overview_is_open(&seat.active_output()) {
|
2024-03-07 13:14:53 -06:00
|
|
|
self.common.gesture_state = Some(GestureState::new(event.fingers()));
|
|
|
|
|
} else {
|
|
|
|
|
let serial = SERIAL_COUNTER.next_serial();
|
|
|
|
|
let pointer = seat.get_pointer().unwrap();
|
|
|
|
|
pointer.gesture_swipe_begin(
|
|
|
|
|
self,
|
|
|
|
|
&GestureSwipeBeginEvent {
|
|
|
|
|
serial,
|
|
|
|
|
time: event.time_msec(),
|
|
|
|
|
fingers: event.fingers(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
2023-09-05 13:41:21 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
InputEvent::GestureSwipeUpdate { event, .. } => {
|
2024-04-10 15:49:08 +02:00
|
|
|
let maybe_seat = self
|
|
|
|
|
.common
|
|
|
|
|
.shell
|
|
|
|
|
.read()
|
|
|
|
|
.seats
|
|
|
|
|
.for_device(&event.device())
|
|
|
|
|
.cloned();
|
|
|
|
|
if let Some(seat) = maybe_seat {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
2024-03-07 13:14:53 -06:00
|
|
|
let mut activate_action: Option<SwipeAction> = None;
|
|
|
|
|
if let Some(ref mut gesture_state) = self.common.gesture_state {
|
|
|
|
|
let first_update = gesture_state.update(
|
|
|
|
|
event.delta(),
|
|
|
|
|
Duration::from_millis(event.time_msec() as u64),
|
|
|
|
|
);
|
|
|
|
|
// Decide on action if first update
|
|
|
|
|
if first_update {
|
2024-08-29 23:33:35 -04:00
|
|
|
let mut natural_scroll = false;
|
2024-09-04 11:13:59 -05:00
|
|
|
if let Some(scroll_config) =
|
|
|
|
|
&self.common.config.cosmic_conf.input_touchpad.scroll_config
|
2026-02-23 16:25:06 +01:00
|
|
|
&& let Some(natural) = scroll_config.natural_scroll
|
2024-09-04 11:13:59 -05:00
|
|
|
{
|
2026-02-23 16:25:06 +01:00
|
|
|
natural_scroll = natural;
|
2024-08-29 23:33:35 -04:00
|
|
|
}
|
2024-03-07 13:14:53 -06:00
|
|
|
activate_action = match gesture_state.fingers {
|
|
|
|
|
3 => None, // TODO: 3 finger gestures
|
|
|
|
|
4 => {
|
|
|
|
|
if self.common.config.cosmic_conf.workspaces.workspace_layout
|
|
|
|
|
== WorkspaceLayout::Horizontal
|
|
|
|
|
{
|
|
|
|
|
match gesture_state.direction {
|
|
|
|
|
Some(Direction::Left) => {
|
2024-08-29 23:33:35 -04:00
|
|
|
if natural_scroll {
|
|
|
|
|
Some(SwipeAction::NextWorkspace)
|
|
|
|
|
} else {
|
|
|
|
|
Some(SwipeAction::PrevWorkspace)
|
|
|
|
|
}
|
2024-03-07 13:14:53 -06:00
|
|
|
}
|
|
|
|
|
Some(Direction::Right) => {
|
2024-08-29 23:33:35 -04:00
|
|
|
if natural_scroll {
|
|
|
|
|
Some(SwipeAction::PrevWorkspace)
|
|
|
|
|
} else {
|
|
|
|
|
Some(SwipeAction::NextWorkspace)
|
|
|
|
|
}
|
2024-03-07 13:14:53 -06:00
|
|
|
}
|
|
|
|
|
_ => None, // TODO: Other actions
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
match gesture_state.direction {
|
2024-08-29 23:33:35 -04:00
|
|
|
Some(Direction::Up) => {
|
|
|
|
|
if natural_scroll {
|
|
|
|
|
Some(SwipeAction::NextWorkspace)
|
|
|
|
|
} else {
|
|
|
|
|
Some(SwipeAction::PrevWorkspace)
|
|
|
|
|
}
|
2024-09-04 11:13:59 -05:00
|
|
|
}
|
2024-03-07 13:14:53 -06:00
|
|
|
Some(Direction::Down) => {
|
2024-08-29 23:33:35 -04:00
|
|
|
if natural_scroll {
|
|
|
|
|
Some(SwipeAction::PrevWorkspace)
|
|
|
|
|
} else {
|
|
|
|
|
Some(SwipeAction::NextWorkspace)
|
|
|
|
|
}
|
2024-03-07 13:14:53 -06:00
|
|
|
}
|
|
|
|
|
_ => None, // TODO: Other actions
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
gesture_state.action = activate_action;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match gesture_state.action {
|
2025-11-18 18:21:24 +01:00
|
|
|
Some(x @ SwipeAction::NextWorkspace)
|
|
|
|
|
| Some(x @ SwipeAction::PrevWorkspace) => {
|
2025-05-20 17:41:27 +02:00
|
|
|
self.common.shell.write().update_workspace_delta(
|
2024-03-07 13:14:53 -06:00
|
|
|
&seat.active_output(),
|
|
|
|
|
gesture_state.delta,
|
2025-11-18 18:21:24 +01:00
|
|
|
matches!(x, SwipeAction::NextWorkspace),
|
2024-03-07 13:14:53 -06:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
let pointer = seat.get_pointer().unwrap();
|
|
|
|
|
pointer.gesture_swipe_update(
|
|
|
|
|
self,
|
|
|
|
|
&GestureSwipeUpdateEvent {
|
|
|
|
|
time: event.time_msec(),
|
|
|
|
|
delta: event.delta(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-09-09 16:38:52 +02:00
|
|
|
|
|
|
|
|
if let Some(action) = activate_action {
|
|
|
|
|
self.handle_swipe_action(action, &seat);
|
2024-03-07 13:14:53 -06:00
|
|
|
}
|
2023-09-05 13:41:21 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
InputEvent::GestureSwipeEnd { event, .. } => {
|
2024-04-10 15:49:08 +02:00
|
|
|
let maybe_seat = self
|
|
|
|
|
.common
|
|
|
|
|
.shell
|
|
|
|
|
.read()
|
|
|
|
|
.seats
|
|
|
|
|
.for_device(&event.device())
|
|
|
|
|
.cloned();
|
|
|
|
|
if let Some(seat) = maybe_seat {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
2024-03-07 13:14:53 -06:00
|
|
|
if let Some(ref gesture_state) = self.common.gesture_state {
|
|
|
|
|
match gesture_state.action {
|
|
|
|
|
Some(SwipeAction::NextWorkspace) | Some(SwipeAction::PrevWorkspace) => {
|
|
|
|
|
let velocity = gesture_state.velocity();
|
|
|
|
|
let norm_velocity =
|
|
|
|
|
if self.common.config.cosmic_conf.workspaces.workspace_layout
|
|
|
|
|
== WorkspaceLayout::Horizontal
|
|
|
|
|
{
|
|
|
|
|
velocity / seat.active_output().geometry().size.w as f64
|
|
|
|
|
} else {
|
|
|
|
|
velocity / seat.active_output().geometry().size.h as f64
|
|
|
|
|
};
|
2025-05-20 17:41:27 +02:00
|
|
|
let _ = self.common.shell.write().end_workspace_swipe(
|
2024-04-10 15:49:08 +02:00
|
|
|
&seat.active_output(),
|
|
|
|
|
norm_velocity,
|
|
|
|
|
&mut self.common.workspace_state.update(),
|
|
|
|
|
);
|
2024-03-07 13:14:53 -06:00
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
self.common.gesture_state = None;
|
|
|
|
|
} else {
|
|
|
|
|
let serial = SERIAL_COUNTER.next_serial();
|
|
|
|
|
let pointer = seat.get_pointer().unwrap();
|
|
|
|
|
pointer.gesture_swipe_end(
|
|
|
|
|
self,
|
|
|
|
|
&GestureSwipeEndEvent {
|
|
|
|
|
serial,
|
|
|
|
|
time: event.time_msec(),
|
|
|
|
|
cancelled: event.cancelled(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
2023-09-05 13:41:21 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
InputEvent::GesturePinchBegin { event, .. } => {
|
2024-04-10 15:49:08 +02:00
|
|
|
let maybe_seat = self
|
|
|
|
|
.common
|
|
|
|
|
.shell
|
|
|
|
|
.read()
|
|
|
|
|
.seats
|
|
|
|
|
.for_device(&event.device())
|
|
|
|
|
.cloned();
|
|
|
|
|
if let Some(seat) = maybe_seat {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
2023-09-05 13:41:21 -07:00
|
|
|
let serial = SERIAL_COUNTER.next_serial();
|
|
|
|
|
let pointer = seat.get_pointer().unwrap();
|
|
|
|
|
pointer.gesture_pinch_begin(
|
|
|
|
|
self,
|
|
|
|
|
&GesturePinchBeginEvent {
|
|
|
|
|
serial,
|
|
|
|
|
time: event.time_msec(),
|
|
|
|
|
fingers: event.fingers(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
InputEvent::GesturePinchUpdate { event, .. } => {
|
2024-04-10 15:49:08 +02:00
|
|
|
let maybe_seat = self
|
|
|
|
|
.common
|
|
|
|
|
.shell
|
|
|
|
|
.read()
|
|
|
|
|
.seats
|
|
|
|
|
.for_device(&event.device())
|
|
|
|
|
.cloned();
|
|
|
|
|
if let Some(seat) = maybe_seat {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
2023-09-05 13:41:21 -07:00
|
|
|
let pointer = seat.get_pointer().unwrap();
|
|
|
|
|
pointer.gesture_pinch_update(
|
|
|
|
|
self,
|
|
|
|
|
&GesturePinchUpdateEvent {
|
|
|
|
|
time: event.time_msec(),
|
|
|
|
|
delta: event.delta(),
|
|
|
|
|
scale: event.scale(),
|
|
|
|
|
rotation: event.rotation(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
InputEvent::GesturePinchEnd { event, .. } => {
|
2024-04-10 15:49:08 +02:00
|
|
|
let maybe_seat = self
|
|
|
|
|
.common
|
|
|
|
|
.shell
|
|
|
|
|
.read()
|
|
|
|
|
.seats
|
|
|
|
|
.for_device(&event.device())
|
|
|
|
|
.cloned();
|
|
|
|
|
if let Some(seat) = maybe_seat {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
2023-09-05 13:41:21 -07:00
|
|
|
let serial = SERIAL_COUNTER.next_serial();
|
|
|
|
|
let pointer = seat.get_pointer().unwrap();
|
|
|
|
|
pointer.gesture_pinch_end(
|
|
|
|
|
self,
|
|
|
|
|
&GesturePinchEndEvent {
|
|
|
|
|
serial,
|
|
|
|
|
time: event.time_msec(),
|
|
|
|
|
cancelled: event.cancelled(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
InputEvent::GestureHoldBegin { event, .. } => {
|
2024-04-10 15:49:08 +02:00
|
|
|
let maybe_seat = self
|
|
|
|
|
.common
|
|
|
|
|
.shell
|
|
|
|
|
.read()
|
|
|
|
|
.seats
|
|
|
|
|
.for_device(&event.device())
|
|
|
|
|
.cloned();
|
|
|
|
|
if let Some(seat) = maybe_seat {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
2023-09-05 13:41:21 -07:00
|
|
|
let serial = SERIAL_COUNTER.next_serial();
|
|
|
|
|
let pointer = seat.get_pointer().unwrap();
|
|
|
|
|
pointer.gesture_hold_begin(
|
|
|
|
|
self,
|
|
|
|
|
&GestureHoldBeginEvent {
|
|
|
|
|
serial,
|
|
|
|
|
time: event.time_msec(),
|
|
|
|
|
fingers: event.fingers(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
InputEvent::GestureHoldEnd { event, .. } => {
|
2024-04-10 15:49:08 +02:00
|
|
|
let maybe_seat = self
|
|
|
|
|
.common
|
|
|
|
|
.shell
|
|
|
|
|
.read()
|
|
|
|
|
.seats
|
|
|
|
|
.for_device(&event.device())
|
|
|
|
|
.cloned();
|
|
|
|
|
if let Some(seat) = maybe_seat {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
2023-09-05 13:41:21 -07:00
|
|
|
let serial = SERIAL_COUNTER.next_serial();
|
|
|
|
|
let pointer = seat.get_pointer().unwrap();
|
|
|
|
|
pointer.gesture_hold_end(
|
|
|
|
|
self,
|
|
|
|
|
&GestureHoldEndEvent {
|
|
|
|
|
serial,
|
|
|
|
|
time: event.time_msec(),
|
|
|
|
|
cancelled: event.cancelled(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-09-09 16:38:52 +02:00
|
|
|
|
2023-11-07 13:56:31 -08:00
|
|
|
InputEvent::TouchDown { event, .. } => {
|
2025-10-16 15:46:54 +02:00
|
|
|
let shell = self.common.shell.write();
|
2024-04-10 15:49:08 +02:00
|
|
|
if let Some(seat) = shell.seats.for_device(&event.device()).cloned() {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
2024-02-21 13:24:56 -08:00
|
|
|
let Some(output) =
|
2025-10-16 13:50:32 +02:00
|
|
|
mapped_output_for_device(&self.common.config, &shell, &event.device())
|
2024-04-10 15:49:08 +02:00
|
|
|
.cloned()
|
2024-02-21 13:24:56 -08:00
|
|
|
else {
|
2023-11-07 13:56:31 -08:00
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
2025-02-13 21:09:13 +01:00
|
|
|
let position =
|
|
|
|
|
transform_output_mapped_position(&output, &event, shell.zoom_state());
|
2025-10-16 15:46:54 +02:00
|
|
|
let under = State::surface_under(position, &output, &shell)
|
2024-01-17 13:12:46 -08:00
|
|
|
.map(|(target, pos)| (target, pos.as_logical()));
|
2023-11-07 13:56:31 -08:00
|
|
|
|
2024-04-10 15:49:08 +02:00
|
|
|
std::mem::drop(shell);
|
|
|
|
|
|
2024-03-26 16:45:30 +01:00
|
|
|
let serial = SERIAL_COUNTER.next_serial();
|
|
|
|
|
let touch = seat.get_touch().unwrap();
|
|
|
|
|
touch.down(
|
|
|
|
|
self,
|
|
|
|
|
under,
|
|
|
|
|
&DownEvent {
|
|
|
|
|
slot: event.slot(),
|
|
|
|
|
location: position.as_logical(),
|
|
|
|
|
serial,
|
|
|
|
|
time: event.time_msec(),
|
|
|
|
|
},
|
|
|
|
|
);
|
2023-11-07 13:56:31 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
InputEvent::TouchMotion { event, .. } => {
|
2025-10-16 15:46:54 +02:00
|
|
|
let shell = self.common.shell.write();
|
2024-04-10 15:49:08 +02:00
|
|
|
if let Some(seat) = shell.seats.for_device(&event.device()).cloned() {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
2024-02-21 13:24:56 -08:00
|
|
|
let Some(output) =
|
2025-10-16 13:50:32 +02:00
|
|
|
mapped_output_for_device(&self.common.config, &shell, &event.device())
|
2024-04-10 15:49:08 +02:00
|
|
|
.cloned()
|
2024-02-21 13:24:56 -08:00
|
|
|
else {
|
2023-11-07 13:56:31 -08:00
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
2025-02-13 21:09:13 +01:00
|
|
|
let position =
|
|
|
|
|
transform_output_mapped_position(&output, &event, shell.zoom_state());
|
2025-10-16 15:46:54 +02:00
|
|
|
let under = State::surface_under(position, &output, &shell)
|
2024-01-17 13:12:46 -08:00
|
|
|
.map(|(target, pos)| (target, pos.as_logical()));
|
2023-11-07 13:56:31 -08:00
|
|
|
|
2024-04-10 15:49:08 +02:00
|
|
|
std::mem::drop(shell);
|
|
|
|
|
|
2024-03-26 16:45:30 +01:00
|
|
|
let touch = seat.get_touch().unwrap();
|
|
|
|
|
touch.motion(
|
|
|
|
|
self,
|
|
|
|
|
under,
|
|
|
|
|
&TouchMotionEvent {
|
|
|
|
|
slot: event.slot(),
|
|
|
|
|
location: position.as_logical(),
|
|
|
|
|
time: event.time_msec(),
|
|
|
|
|
},
|
|
|
|
|
);
|
2023-11-07 13:56:31 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
InputEvent::TouchUp { event, .. } => {
|
2025-05-20 17:41:27 +02:00
|
|
|
let mut shell = self.common.shell.write();
|
2026-02-23 16:25:06 +01:00
|
|
|
if let Some(Trigger::Touch(slot)) = shell.overview_mode().0.active_trigger()
|
|
|
|
|
&& *slot == event.slot()
|
|
|
|
|
{
|
|
|
|
|
shell.set_overview_mode(None, self.common.event_loop_handle.clone());
|
2024-04-04 19:17:03 -07:00
|
|
|
}
|
|
|
|
|
|
2024-04-10 15:49:08 +02:00
|
|
|
let maybe_seat = shell.seats.for_device(&event.device()).cloned();
|
|
|
|
|
if let Some(seat) = maybe_seat {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
2024-04-10 15:49:08 +02:00
|
|
|
std::mem::drop(shell);
|
2023-11-07 13:56:31 -08:00
|
|
|
let serial = SERIAL_COUNTER.next_serial();
|
|
|
|
|
let touch = seat.get_touch().unwrap();
|
2024-03-26 16:45:30 +01:00
|
|
|
touch.up(
|
|
|
|
|
self,
|
|
|
|
|
&UpEvent {
|
|
|
|
|
slot: event.slot(),
|
|
|
|
|
time: event.time_msec(),
|
|
|
|
|
serial,
|
|
|
|
|
},
|
|
|
|
|
);
|
2023-11-07 13:56:31 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
InputEvent::TouchCancel { event, .. } => {
|
2024-04-10 15:49:08 +02:00
|
|
|
let maybe_seat = self
|
|
|
|
|
.common
|
|
|
|
|
.shell
|
|
|
|
|
.read()
|
|
|
|
|
.seats
|
|
|
|
|
.for_device(&event.device())
|
|
|
|
|
.cloned();
|
|
|
|
|
if let Some(seat) = maybe_seat {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
2023-11-07 13:56:31 -08:00
|
|
|
let touch = seat.get_touch().unwrap();
|
2024-03-26 16:45:30 +01:00
|
|
|
touch.cancel(self);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
InputEvent::TouchFrame { event, .. } => {
|
2024-04-10 15:49:08 +02:00
|
|
|
let maybe_seat = self
|
|
|
|
|
.common
|
|
|
|
|
.shell
|
|
|
|
|
.read()
|
|
|
|
|
.seats
|
|
|
|
|
.for_device(&event.device())
|
|
|
|
|
.cloned();
|
|
|
|
|
if let Some(seat) = maybe_seat {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
2024-03-26 16:45:30 +01:00
|
|
|
let touch = seat.get_touch().unwrap();
|
|
|
|
|
touch.frame(self);
|
2023-11-07 13:56:31 -08:00
|
|
|
}
|
|
|
|
|
}
|
2024-09-09 16:38:52 +02:00
|
|
|
|
2023-12-28 13:36:59 -08:00
|
|
|
InputEvent::TabletToolAxis { event, .. } => {
|
2025-10-16 15:46:54 +02:00
|
|
|
let shell = self.common.shell.write();
|
2024-04-10 15:49:08 +02:00
|
|
|
if let Some(seat) = shell.seats.for_device(&event.device()).cloned() {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
cursor: Add idle-hide timeout
Adds a cursor_hide_timeout config key (Option<u32> seconds) to
CosmicCompConfig. When set, the cursor is hidden after the configured
period of pointer inactivity and revealed again by any pointer event.
Touch input does not count as activity (no visible cursor to surface).
Implementation:
- Per-seat hidden flag, calloop timer token, and last-armed Instant on
CursorStateInner.
- notify_cursor_activity called from each pointer-related input branch
(motion, button, axis, tablet) resets the flag and reschedules the
timer; rapid successive calls are coalesced behind a 100ms throttle
so high-frequency mice don't churn the calloop timer source.
- On timer fire, the hidden flag is set, draw_cursor short-circuits to
an empty element list, and a render is scheduled. Active pointer
grabs (drags, resizes) suppress the hide.
- Config reload arms or cancels the timer immediately; None as the
configured value collapses the cancel path into the same function.
Closes #2231.
Drafted with Claude (Anthropic); reviewed and tested by the committer.
2026-05-20 23:52:45 +02:00
|
|
|
notify_cursor_activity(self, &seat);
|
2024-02-21 13:24:56 -08:00
|
|
|
let Some(output) =
|
2024-04-10 15:49:08 +02:00
|
|
|
mapped_output_for_device(&self.common.config, &shell, &event.device())
|
|
|
|
|
.cloned()
|
2024-02-21 13:24:56 -08:00
|
|
|
else {
|
2023-12-28 13:36:59 -08:00
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
2025-02-13 21:09:13 +01:00
|
|
|
let position =
|
|
|
|
|
transform_output_mapped_position(&output, &event, shell.zoom_state());
|
2025-10-16 15:46:54 +02:00
|
|
|
let under = State::surface_under(position, &output, &shell)
|
2024-01-17 13:12:46 -08:00
|
|
|
.map(|(target, pos)| (target, pos.as_logical()));
|
2023-12-28 13:36:59 -08:00
|
|
|
|
2024-04-10 15:49:08 +02:00
|
|
|
std::mem::drop(shell);
|
|
|
|
|
|
2023-12-28 13:36:59 -08:00
|
|
|
let pointer = seat.get_pointer().unwrap();
|
|
|
|
|
pointer.motion(
|
|
|
|
|
self,
|
|
|
|
|
under.clone(),
|
|
|
|
|
&MotionEvent {
|
|
|
|
|
location: position.as_logical(),
|
|
|
|
|
serial: SERIAL_COUNTER.next_serial(),
|
2024-08-21 13:49:49 -07:00
|
|
|
time: self.common.clock.now().as_millis(),
|
2023-12-28 13:36:59 -08:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let tablet_seat = seat.tablet_seat();
|
|
|
|
|
|
|
|
|
|
let tablet = tablet_seat.get_tablet(&TabletDescriptor::from(&event.device()));
|
|
|
|
|
let tool = tablet_seat.get_tool(&event.tool());
|
|
|
|
|
|
|
|
|
|
if let (Some(tablet), Some(tool)) = (tablet, tool) {
|
|
|
|
|
if event.pressure_has_changed() {
|
|
|
|
|
tool.pressure(event.pressure());
|
|
|
|
|
}
|
|
|
|
|
if event.distance_has_changed() {
|
|
|
|
|
tool.distance(event.distance());
|
|
|
|
|
}
|
|
|
|
|
if event.tilt_has_changed() {
|
|
|
|
|
tool.tilt(event.tilt());
|
|
|
|
|
}
|
|
|
|
|
if event.slider_has_changed() {
|
|
|
|
|
tool.slider_position(event.slider_position());
|
|
|
|
|
}
|
|
|
|
|
if event.rotation_has_changed() {
|
|
|
|
|
tool.rotation(event.rotation());
|
|
|
|
|
}
|
|
|
|
|
if event.wheel_has_changed() {
|
|
|
|
|
tool.wheel(event.wheel_delta(), event.wheel_delta_discrete());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tool.motion(
|
|
|
|
|
position.as_logical(),
|
2024-05-13 14:16:21 -07:00
|
|
|
under
|
|
|
|
|
.and_then(|(f, loc)| f.wl_surface().map(|s| (s.into_owned(), loc))),
|
2023-12-28 13:36:59 -08:00
|
|
|
&tablet,
|
|
|
|
|
SERIAL_COUNTER.next_serial(),
|
|
|
|
|
event.time_msec(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
InputEvent::TabletToolProximity { event, .. } => {
|
2025-10-16 15:46:54 +02:00
|
|
|
let shell = self.common.shell.write();
|
2024-04-10 15:49:08 +02:00
|
|
|
if let Some(seat) = shell.seats.for_device(&event.device()).cloned() {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
cursor: Add idle-hide timeout
Adds a cursor_hide_timeout config key (Option<u32> seconds) to
CosmicCompConfig. When set, the cursor is hidden after the configured
period of pointer inactivity and revealed again by any pointer event.
Touch input does not count as activity (no visible cursor to surface).
Implementation:
- Per-seat hidden flag, calloop timer token, and last-armed Instant on
CursorStateInner.
- notify_cursor_activity called from each pointer-related input branch
(motion, button, axis, tablet) resets the flag and reschedules the
timer; rapid successive calls are coalesced behind a 100ms throttle
so high-frequency mice don't churn the calloop timer source.
- On timer fire, the hidden flag is set, draw_cursor short-circuits to
an empty element list, and a render is scheduled. Active pointer
grabs (drags, resizes) suppress the hide.
- Config reload arms or cancels the timer immediately; None as the
configured value collapses the cancel path into the same function.
Closes #2231.
Drafted with Claude (Anthropic); reviewed and tested by the committer.
2026-05-20 23:52:45 +02:00
|
|
|
notify_cursor_activity(self, &seat);
|
2024-02-21 13:24:56 -08:00
|
|
|
let Some(output) =
|
2024-04-10 15:49:08 +02:00
|
|
|
mapped_output_for_device(&self.common.config, &shell, &event.device())
|
|
|
|
|
.cloned()
|
2024-02-21 13:24:56 -08:00
|
|
|
else {
|
2023-12-28 13:36:59 -08:00
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
2025-02-13 21:09:13 +01:00
|
|
|
let position =
|
|
|
|
|
transform_output_mapped_position(&output, &event, shell.zoom_state());
|
2025-10-16 15:46:54 +02:00
|
|
|
let under = State::surface_under(position, &output, &shell)
|
2024-01-17 13:12:46 -08:00
|
|
|
.map(|(target, pos)| (target, pos.as_logical()));
|
2023-12-28 13:36:59 -08:00
|
|
|
|
2024-04-10 15:49:08 +02:00
|
|
|
std::mem::drop(shell);
|
|
|
|
|
|
2023-12-28 13:36:59 -08:00
|
|
|
let pointer = seat.get_pointer().unwrap();
|
|
|
|
|
pointer.motion(
|
|
|
|
|
self,
|
|
|
|
|
under.clone(),
|
|
|
|
|
&MotionEvent {
|
|
|
|
|
location: position.as_logical(),
|
|
|
|
|
serial: SERIAL_COUNTER.next_serial(),
|
2024-08-21 13:49:49 -07:00
|
|
|
time: self.common.clock.now().as_millis(),
|
2023-12-28 13:36:59 -08:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let tablet_seat = seat.tablet_seat();
|
|
|
|
|
|
|
|
|
|
let tablet = tablet_seat.get_tablet(&TabletDescriptor::from(&event.device()));
|
2024-08-22 16:24:27 +02:00
|
|
|
let dh = self.common.display_handle.clone();
|
|
|
|
|
let tool = tablet_seat.add_tool::<Self>(self, &dh, &event.tool());
|
2023-12-28 13:36:59 -08:00
|
|
|
|
2024-05-14 18:48:21 -07:00
|
|
|
if let Some(tablet) = tablet {
|
2023-12-28 13:36:59 -08:00
|
|
|
match event.state() {
|
|
|
|
|
ProximityState::In => {
|
2024-05-13 14:16:21 -07:00
|
|
|
if let Some(under) = under.and_then(|(f, loc)| {
|
|
|
|
|
f.wl_surface().map(|s| (s.into_owned(), loc))
|
|
|
|
|
}) {
|
2023-12-28 13:36:59 -08:00
|
|
|
tool.proximity_in(
|
|
|
|
|
position.as_logical(),
|
|
|
|
|
under,
|
|
|
|
|
&tablet,
|
|
|
|
|
SERIAL_COUNTER.next_serial(),
|
|
|
|
|
event.time_msec(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ProximityState::Out => tool.proximity_out(event.time_msec()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
InputEvent::TabletToolTip { event, .. } => {
|
2024-04-10 15:49:08 +02:00
|
|
|
let maybe_seat = self
|
|
|
|
|
.common
|
|
|
|
|
.shell
|
|
|
|
|
.read()
|
|
|
|
|
.seats
|
|
|
|
|
.for_device(&event.device())
|
|
|
|
|
.cloned();
|
|
|
|
|
if let Some(seat) = maybe_seat {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
cursor: Add idle-hide timeout
Adds a cursor_hide_timeout config key (Option<u32> seconds) to
CosmicCompConfig. When set, the cursor is hidden after the configured
period of pointer inactivity and revealed again by any pointer event.
Touch input does not count as activity (no visible cursor to surface).
Implementation:
- Per-seat hidden flag, calloop timer token, and last-armed Instant on
CursorStateInner.
- notify_cursor_activity called from each pointer-related input branch
(motion, button, axis, tablet) resets the flag and reschedules the
timer; rapid successive calls are coalesced behind a 100ms throttle
so high-frequency mice don't churn the calloop timer source.
- On timer fire, the hidden flag is set, draw_cursor short-circuits to
an empty element list, and a render is scheduled. Active pointer
grabs (drags, resizes) suppress the hide.
- Config reload arms or cancels the timer immediately; None as the
configured value collapses the cancel path into the same function.
Closes #2231.
Drafted with Claude (Anthropic); reviewed and tested by the committer.
2026-05-20 23:52:45 +02:00
|
|
|
notify_cursor_activity(self, &seat);
|
2023-12-28 13:36:59 -08:00
|
|
|
if let Some(tool) = seat.tablet_seat().get_tool(&event.tool()) {
|
|
|
|
|
match event.tip_state() {
|
|
|
|
|
TabletToolTipState::Down => {
|
|
|
|
|
tool.tip_down(SERIAL_COUNTER.next_serial(), event.time_msec());
|
|
|
|
|
}
|
|
|
|
|
TabletToolTipState::Up => {
|
|
|
|
|
tool.tip_up(event.time_msec());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
InputEvent::TabletToolButton { event, .. } => {
|
2024-04-10 15:49:08 +02:00
|
|
|
let maybe_seat = self
|
|
|
|
|
.common
|
|
|
|
|
.shell
|
|
|
|
|
.read()
|
|
|
|
|
.seats
|
|
|
|
|
.for_device(&event.device())
|
|
|
|
|
.cloned();
|
|
|
|
|
if let Some(seat) = maybe_seat {
|
2024-04-19 14:57:17 +02:00
|
|
|
self.common.idle_notifier_state.notify_activity(&seat);
|
cursor: Add idle-hide timeout
Adds a cursor_hide_timeout config key (Option<u32> seconds) to
CosmicCompConfig. When set, the cursor is hidden after the configured
period of pointer inactivity and revealed again by any pointer event.
Touch input does not count as activity (no visible cursor to surface).
Implementation:
- Per-seat hidden flag, calloop timer token, and last-armed Instant on
CursorStateInner.
- notify_cursor_activity called from each pointer-related input branch
(motion, button, axis, tablet) resets the flag and reschedules the
timer; rapid successive calls are coalesced behind a 100ms throttle
so high-frequency mice don't churn the calloop timer source.
- On timer fire, the hidden flag is set, draw_cursor short-circuits to
an empty element list, and a render is scheduled. Active pointer
grabs (drags, resizes) suppress the hide.
- Config reload arms or cancels the timer immediately; None as the
configured value collapses the cancel path into the same function.
Closes #2231.
Drafted with Claude (Anthropic); reviewed and tested by the committer.
2026-05-20 23:52:45 +02:00
|
|
|
notify_cursor_activity(self, &seat);
|
2023-12-28 13:36:59 -08:00
|
|
|
if let Some(tool) = seat.tablet_seat().get_tool(&event.tool()) {
|
|
|
|
|
tool.button(
|
|
|
|
|
event.button(),
|
|
|
|
|
event.button_state(),
|
|
|
|
|
SERIAL_COUNTER.next_serial(),
|
|
|
|
|
event.time_msec(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
InputEvent::Special(_) => {}
|
2026-05-22 16:50:35 +02:00
|
|
|
#[allow(unused_variables)]
|
2025-07-29 15:58:07 +02:00
|
|
|
InputEvent::SwitchToggle { event } => {
|
2026-06-15 00:30:35 -05:00
|
|
|
#[cfg(feature = "logind")]
|
2025-08-11 16:51:57 +02:00
|
|
|
if event.switch() == Some(Switch::Lid) && self.common.inhibit_lid_fd.is_some() {
|
2025-09-10 18:25:33 +02:00
|
|
|
let backend = self.backend.lock();
|
|
|
|
|
let output = backend
|
|
|
|
|
.all_outputs()
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|o| o.is_internal())
|
|
|
|
|
.cloned();
|
|
|
|
|
let closed = event.state() == SwitchState::On;
|
|
|
|
|
|
|
|
|
|
if closed {
|
|
|
|
|
backend
|
2025-08-11 16:51:57 +02:00
|
|
|
.disable_internal_output(&mut self.common.output_configuration_state);
|
|
|
|
|
} else {
|
2025-09-10 18:25:33 +02:00
|
|
|
backend.enable_internal_output(&mut self.common.output_configuration_state);
|
2025-07-29 15:58:07 +02:00
|
|
|
}
|
2025-09-10 18:25:33 +02:00
|
|
|
std::mem::drop(backend);
|
2025-08-11 16:51:57 +02:00
|
|
|
|
2025-09-10 18:25:33 +02:00
|
|
|
if let Err(err) = self.refresh_output_config() {
|
|
|
|
|
if !closed {
|
2026-05-22 16:50:35 +02:00
|
|
|
tracing::warn!(?err, "Failed to re-enable internal connector");
|
2025-09-10 18:25:33 +02:00
|
|
|
if let Some(output) = output {
|
|
|
|
|
use cosmic_comp_config::output::comp::OutputState;
|
|
|
|
|
|
|
|
|
|
output.config_mut().enabled = OutputState::Disabled;
|
|
|
|
|
if let Err(err) = self.refresh_output_config() {
|
|
|
|
|
error!("Unrecoverable output configuration error: {}", err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Disabling an output should never fail.
|
|
|
|
|
error!("Unrecoverable output configuration error: {}", err);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-29 15:58:07 +02:00
|
|
|
}
|
|
|
|
|
}
|
2021-12-22 20:14:09 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-13 10:07:46 -07:00
|
|
|
/// Determine is key event should be intercepted as a key binding, or forwarded to surface
|
2025-05-20 17:54:12 +02:00
|
|
|
#[profiling::function]
|
2024-09-13 10:07:46 -07:00
|
|
|
pub fn filter_keyboard_input<B: InputBackend, E: KeyboardKeyEvent<B>>(
|
|
|
|
|
&mut self,
|
|
|
|
|
event: &E,
|
|
|
|
|
seat: &Seat<Self>,
|
|
|
|
|
modifiers: &ModifiersState,
|
|
|
|
|
handle: KeysymHandle<'_>,
|
|
|
|
|
serial: Serial,
|
|
|
|
|
) -> FilterResult<Option<(Action, shortcuts::Binding)>> {
|
2026-02-07 17:26:12 +01:00
|
|
|
// Pre-compute for layout-agnostic shortcut matching
|
|
|
|
|
let raw_syms = handle.raw_syms();
|
|
|
|
|
let latin_sym = handle.raw_latin_sym_or_raw_current_sym();
|
|
|
|
|
let key_matches = |binding_key: Keysym| -> bool {
|
|
|
|
|
raw_syms.contains(&binding_key) || latin_sym.is_some_and(|sym| sym == binding_key)
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-20 17:41:27 +02:00
|
|
|
let mut shell = self.common.shell.write();
|
2024-09-13 10:07:46 -07:00
|
|
|
|
|
|
|
|
let keyboard = seat.get_keyboard().unwrap();
|
|
|
|
|
let pointer = seat.get_pointer().unwrap();
|
2025-01-17 21:25:43 +07:00
|
|
|
// We're only interested in filtering keyboard grabs if we initiated them.
|
|
|
|
|
// The easiest way to check that is to check the type of the grab.
|
|
|
|
|
let keyboard_grabbed = keyboard.with_grab(|_serial, grab| {
|
|
|
|
|
grab.is::<SwapWindowGrab>() || grab.is::<PopupKeyboardGrab<State>>()
|
|
|
|
|
}) == Some(true);
|
|
|
|
|
let is_grabbed = keyboard_grabbed || pointer.is_grabbed();
|
2024-09-13 10:07:46 -07:00
|
|
|
|
|
|
|
|
let current_focus = keyboard.current_focus();
|
|
|
|
|
//this should fall back to active output since there may not be a focused output
|
|
|
|
|
let focused_output = seat.focused_or_active_output();
|
|
|
|
|
|
|
|
|
|
let shortcuts_inhibited = current_focus.as_ref().is_some_and(|f| {
|
|
|
|
|
f.wl_surface()
|
2025-08-15 13:14:13 +02:00
|
|
|
.map(|surface| {
|
2024-09-13 10:07:46 -07:00
|
|
|
seat.keyboard_shortcuts_inhibitor_for_surface(&surface)
|
|
|
|
|
.map(|inhibitor| inhibitor.is_active())
|
2025-08-15 13:14:13 +02:00
|
|
|
.unwrap_or(false)
|
|
|
|
|
|| seat.has_active_xwayland_grab(&surface)
|
2024-09-13 10:07:46 -07:00
|
|
|
})
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-05 15:56:06 -08:00
|
|
|
if let Some(a11y_keyboard_monitor) = self.common.dbus_state.a11y_keyboard_monitor() {
|
|
|
|
|
a11y_keyboard_monitor.key_event(modifiers, &handle, event.state());
|
|
|
|
|
}
|
2024-08-21 20:20:27 -07:00
|
|
|
|
2024-09-13 10:07:46 -07:00
|
|
|
// Leave move overview mode, if any modifier was released
|
|
|
|
|
if let Some(Trigger::KeyboardMove(action_modifiers)) =
|
|
|
|
|
shell.overview_mode().0.active_trigger()
|
2026-02-23 16:25:06 +01:00
|
|
|
&& ((action_modifiers.ctrl && !modifiers.ctrl)
|
2024-09-13 10:07:46 -07:00
|
|
|
|| (action_modifiers.alt && !modifiers.alt)
|
|
|
|
|
|| (action_modifiers.logo && !modifiers.logo)
|
2026-02-23 16:25:06 +01:00
|
|
|
|| (action_modifiers.shift && !modifiers.shift))
|
|
|
|
|
{
|
|
|
|
|
shell.set_overview_mode(None, self.common.event_loop_handle.clone());
|
2024-09-13 10:07:46 -07:00
|
|
|
}
|
|
|
|
|
// Leave swap overview mode, if any key was released
|
|
|
|
|
if let Some(Trigger::KeyboardSwap(action_pattern, old_descriptor)) =
|
|
|
|
|
shell.overview_mode().0.active_trigger()
|
2026-02-23 16:25:06 +01:00
|
|
|
&& ((action_pattern.modifiers.ctrl && !modifiers.ctrl)
|
2024-09-13 10:07:46 -07:00
|
|
|
|| (action_pattern.modifiers.alt && !modifiers.alt)
|
|
|
|
|
|| (action_pattern.modifiers.logo && !modifiers.logo)
|
|
|
|
|
|| (action_pattern.modifiers.shift && !modifiers.shift)
|
|
|
|
|
|| (action_pattern.key.is_some()
|
2026-02-07 17:26:12 +01:00
|
|
|
&& key_matches(action_pattern.key.unwrap())
|
2026-02-23 16:25:06 +01:00
|
|
|
&& event.state() == KeyState::Released))
|
|
|
|
|
{
|
|
|
|
|
shell.set_overview_mode(None, self.common.event_loop_handle.clone());
|
|
|
|
|
|
|
|
|
|
self.keyboard_swap(
|
|
|
|
|
seat,
|
|
|
|
|
&mut shell,
|
|
|
|
|
old_descriptor,
|
|
|
|
|
current_focus,
|
|
|
|
|
&focused_output,
|
|
|
|
|
);
|
2024-09-13 10:07:46 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Leave or update resize mode, if modifiers changed or initial key was released
|
|
|
|
|
if let Some(action_pattern) = shell.resize_mode().0.active_binding() {
|
|
|
|
|
if action_pattern.key.is_some()
|
|
|
|
|
&& event.state() == KeyState::Released
|
2026-02-07 17:26:12 +01:00
|
|
|
&& key_matches(action_pattern.key.unwrap())
|
2024-09-13 10:07:46 -07:00
|
|
|
{
|
|
|
|
|
shell.set_resize_mode(
|
|
|
|
|
None,
|
|
|
|
|
&self.common.config,
|
|
|
|
|
self.common.event_loop_handle.clone(),
|
|
|
|
|
);
|
|
|
|
|
} else if !cosmic_modifiers_eq_smithay(&action_pattern.modifiers, modifiers) {
|
|
|
|
|
let mut new_pattern = action_pattern.clone();
|
2025-10-16 13:50:32 +02:00
|
|
|
new_pattern.modifiers = cosmic_modifiers_from_smithay(*modifiers);
|
2024-09-13 10:07:46 -07:00
|
|
|
let enabled =
|
|
|
|
|
self.common
|
|
|
|
|
.config
|
|
|
|
|
.shortcuts
|
|
|
|
|
.iter()
|
|
|
|
|
.find_map(move |(binding, action)| {
|
|
|
|
|
if binding == &new_pattern
|
|
|
|
|
&& matches!(action, shortcuts::Action::Resizing(_))
|
|
|
|
|
{
|
|
|
|
|
let shortcuts::Action::Resizing(direction) = action else {
|
|
|
|
|
unreachable!()
|
|
|
|
|
};
|
|
|
|
|
Some((new_pattern.clone(), *direction))
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
shell.set_resize_mode(
|
|
|
|
|
enabled,
|
|
|
|
|
&self.common.config,
|
|
|
|
|
self.common.event_loop_handle.clone(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Special case resizing with regards to arrow keys
|
|
|
|
|
if let Some(direction) = shell.resize_mode().0.active_direction() {
|
|
|
|
|
let resize_edge = match handle.modified_sym() {
|
|
|
|
|
Keysym::Left | Keysym::h | Keysym::H => Some(ResizeEdge::LEFT),
|
|
|
|
|
Keysym::Down | Keysym::j | Keysym::J => Some(ResizeEdge::BOTTOM),
|
|
|
|
|
Keysym::Up | Keysym::k | Keysym::K => Some(ResizeEdge::TOP),
|
|
|
|
|
Keysym::Right | Keysym::l | Keysym::L => Some(ResizeEdge::RIGHT),
|
|
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Some(mut edge) = resize_edge {
|
|
|
|
|
if direction == ResizeDirection::Inwards {
|
|
|
|
|
edge.flip_direction();
|
|
|
|
|
}
|
|
|
|
|
let action = Action::Private(PrivateAction::Resizing(
|
|
|
|
|
direction,
|
|
|
|
|
edge.into(),
|
|
|
|
|
cosmic_keystate_from_smithay(event.state()),
|
|
|
|
|
));
|
|
|
|
|
let key_pattern = shortcuts::Binding {
|
2025-10-16 13:50:32 +02:00
|
|
|
modifiers: cosmic_modifiers_from_smithay(*modifiers),
|
2025-09-19 09:33:10 -04:00
|
|
|
keycode: None,
|
2024-09-13 10:07:46 -07:00
|
|
|
key: Some(handle.modified_sym()),
|
|
|
|
|
description: None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if event.state() == KeyState::Released {
|
|
|
|
|
if let Some(tokens) = seat.supressed_keys().filter(&handle) {
|
|
|
|
|
for token in tokens {
|
|
|
|
|
self.common.event_loop_handle.remove(token);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2024-09-18 13:20:17 -07:00
|
|
|
let seat_clone = seat.clone();
|
|
|
|
|
let action_clone = action.clone();
|
|
|
|
|
let key_pattern_clone = key_pattern.clone();
|
|
|
|
|
let start = Instant::now();
|
|
|
|
|
let time = event.time_msec();
|
|
|
|
|
let token = self
|
|
|
|
|
.common
|
|
|
|
|
.event_loop_handle
|
|
|
|
|
.insert_source(
|
|
|
|
|
Timer::from_duration(Duration::from_millis(200)),
|
|
|
|
|
move |current, _, state| {
|
|
|
|
|
let duration = current.duration_since(start).as_millis();
|
|
|
|
|
state.handle_action(
|
|
|
|
|
action_clone.clone(),
|
|
|
|
|
&seat_clone,
|
|
|
|
|
serial,
|
|
|
|
|
time.overflowing_add(duration as u32).0,
|
|
|
|
|
key_pattern_clone.clone(),
|
|
|
|
|
None,
|
|
|
|
|
);
|
|
|
|
|
calloop::timer::TimeoutAction::ToDuration(Duration::from_millis(25))
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.ok();
|
2024-09-13 10:07:46 -07:00
|
|
|
|
|
|
|
|
seat.supressed_keys().add(&handle, token);
|
|
|
|
|
}
|
|
|
|
|
return FilterResult::Intercept(Some((action, key_pattern)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::mem::drop(shell);
|
|
|
|
|
|
|
|
|
|
// cancel grabs
|
|
|
|
|
if is_grabbed
|
|
|
|
|
&& handle.modified_sym() == Keysym::Escape
|
|
|
|
|
&& event.state() == KeyState::Pressed
|
|
|
|
|
&& !modifiers.alt
|
|
|
|
|
&& !modifiers.ctrl
|
|
|
|
|
&& !modifiers.logo
|
|
|
|
|
&& !modifiers.shift
|
|
|
|
|
{
|
|
|
|
|
seat.supressed_keys().add(&handle, None);
|
|
|
|
|
return FilterResult::Intercept(Some((
|
|
|
|
|
Action::Private(PrivateAction::Escape),
|
|
|
|
|
shortcuts::Binding {
|
|
|
|
|
modifiers: shortcuts::Modifiers::default(),
|
2025-09-19 09:33:10 -04:00
|
|
|
keycode: None,
|
2024-09-13 10:07:46 -07:00
|
|
|
key: Some(Keysym::Escape),
|
|
|
|
|
description: None,
|
|
|
|
|
},
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 15:56:06 -08:00
|
|
|
if let Some(mut a11y_keyboard_monitor) = self.common.dbus_state.a11y_keyboard_monitor() {
|
|
|
|
|
if event.state() == KeyState::Released {
|
|
|
|
|
let removed =
|
|
|
|
|
a11y_keyboard_monitor.remove_active_virtual_mod(handle.modified_sym());
|
|
|
|
|
// If `Caps_Lock` is a virtual modifier, and is in locked state, clear it
|
|
|
|
|
if removed
|
|
|
|
|
&& handle.modified_sym() == Keysym::Caps_Lock
|
|
|
|
|
&& (modifiers.serialized.locked & 2) != 0
|
|
|
|
|
{
|
|
|
|
|
let seat = seat.clone();
|
|
|
|
|
let key_code = event.key_code();
|
|
|
|
|
self.common.event_loop_handle.insert_idle(move |state| {
|
|
|
|
|
if let Some(keyboard) = seat.get_keyboard() {
|
|
|
|
|
let serial = SERIAL_COUNTER.next_serial();
|
|
|
|
|
let time = state.common.clock.now().as_millis();
|
|
|
|
|
keyboard.input(
|
|
|
|
|
state,
|
|
|
|
|
key_code,
|
|
|
|
|
KeyState::Pressed,
|
|
|
|
|
serial,
|
|
|
|
|
time,
|
|
|
|
|
|_, _, _| FilterResult::<()>::Forward,
|
|
|
|
|
);
|
|
|
|
|
let serial = SERIAL_COUNTER.next_serial();
|
|
|
|
|
keyboard.input(
|
|
|
|
|
state,
|
|
|
|
|
key_code,
|
|
|
|
|
KeyState::Released,
|
|
|
|
|
serial,
|
|
|
|
|
time,
|
|
|
|
|
|_, _, _| FilterResult::<()>::Forward,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} else if event.state() == KeyState::Pressed
|
|
|
|
|
&& a11y_keyboard_monitor.has_virtual_mod(handle.modified_sym())
|
2025-10-16 15:46:54 +02:00
|
|
|
{
|
2026-03-05 15:56:06 -08:00
|
|
|
a11y_keyboard_monitor.add_active_virtual_mod(handle.modified_sym());
|
2024-08-21 20:20:27 -07:00
|
|
|
|
2026-03-05 15:56:06 -08:00
|
|
|
tracing::debug!(
|
|
|
|
|
"active virtual mods: {:?}",
|
|
|
|
|
a11y_keyboard_monitor.active_virtual_mods()
|
|
|
|
|
);
|
|
|
|
|
seat.supressed_keys().add(&handle, None);
|
|
|
|
|
|
|
|
|
|
return FilterResult::Intercept(None);
|
|
|
|
|
}
|
2024-08-21 20:20:27 -07:00
|
|
|
}
|
|
|
|
|
|
2024-09-13 10:07:46 -07:00
|
|
|
// Skip released events for initially surpressed keys
|
2026-02-23 16:25:06 +01:00
|
|
|
if event.state() == KeyState::Released
|
|
|
|
|
&& let Some(tokens) = seat.supressed_keys().filter(&handle)
|
|
|
|
|
{
|
|
|
|
|
for token in tokens {
|
|
|
|
|
self.common.event_loop_handle.remove(token);
|
2024-09-13 10:07:46 -07:00
|
|
|
}
|
2026-02-23 16:25:06 +01:00
|
|
|
return FilterResult::Intercept(None);
|
2024-09-13 10:07:46 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle VT switches
|
|
|
|
|
if event.state() == KeyState::Pressed
|
|
|
|
|
&& (Keysym::XF86_Switch_VT_1.raw()..=Keysym::XF86_Switch_VT_12.raw())
|
|
|
|
|
.contains(&handle.modified_sym().raw())
|
|
|
|
|
{
|
|
|
|
|
if let Err(err) = self.backend.kms().switch_vt(
|
|
|
|
|
(handle.modified_sym().raw() - Keysym::XF86_Switch_VT_1.raw() + 1) as i32,
|
|
|
|
|
) {
|
|
|
|
|
error!(?err, "Failed switching virtual terminal.");
|
|
|
|
|
}
|
|
|
|
|
seat.supressed_keys().add(&handle, None);
|
|
|
|
|
return FilterResult::Intercept(None);
|
2024-08-21 20:20:27 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-05 15:56:06 -08:00
|
|
|
if let Some(a11y_keyboard_monitor) = self.common.dbus_state.a11y_keyboard_monitor()
|
|
|
|
|
&& event.state() == KeyState::Pressed
|
|
|
|
|
&& (a11y_keyboard_monitor.has_keyboard_grab()
|
|
|
|
|
|| a11y_keyboard_monitor.has_key_grab(modifiers, handle.modified_sym()))
|
2024-08-21 20:20:27 -07:00
|
|
|
{
|
2025-11-21 16:34:15 -08:00
|
|
|
let modifiers_queue = seat.modifiers_shortcut_queue();
|
|
|
|
|
modifiers_queue.clear();
|
2025-11-20 13:03:59 -08:00
|
|
|
seat.supressed_keys().add(&handle, None);
|
2024-08-21 20:20:27 -07:00
|
|
|
return FilterResult::Intercept(None);
|
2024-09-13 10:07:46 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handle the rest of the global shortcuts
|
|
|
|
|
let mut clear_queue = true;
|
|
|
|
|
if !shortcuts_inhibited {
|
|
|
|
|
let modifiers_queue = seat.modifiers_shortcut_queue();
|
|
|
|
|
|
|
|
|
|
for (binding, action) in self.common.config.shortcuts.iter() {
|
|
|
|
|
if *action == shortcuts::Action::Disable {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// is this a released (triggered) modifier-only binding?
|
|
|
|
|
if binding.key.is_none()
|
|
|
|
|
&& event.state() == KeyState::Released
|
|
|
|
|
&& !cosmic_modifiers_eq_smithay(&binding.modifiers, modifiers)
|
|
|
|
|
&& modifiers_queue.take(binding)
|
|
|
|
|
{
|
|
|
|
|
modifiers_queue.clear();
|
|
|
|
|
return FilterResult::Intercept(Some((
|
|
|
|
|
Action::Shortcut(action.clone()),
|
|
|
|
|
binding.clone(),
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// could this potentially become a modifier-only binding?
|
|
|
|
|
if binding.key.is_none()
|
|
|
|
|
&& event.state() == KeyState::Pressed
|
|
|
|
|
&& cosmic_modifiers_eq_smithay(&binding.modifiers, modifiers)
|
|
|
|
|
{
|
|
|
|
|
modifiers_queue.set(binding.clone());
|
|
|
|
|
clear_queue = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// is this a normal binding?
|
|
|
|
|
if binding.key.is_some()
|
|
|
|
|
&& event.state() == KeyState::Pressed
|
2026-02-07 17:26:12 +01:00
|
|
|
&& key_matches(binding.key.unwrap())
|
2024-09-13 10:07:46 -07:00
|
|
|
&& cosmic_modifiers_eq_smithay(&binding.modifiers, modifiers)
|
|
|
|
|
{
|
|
|
|
|
modifiers_queue.clear();
|
|
|
|
|
seat.supressed_keys().add(&handle, None);
|
|
|
|
|
return FilterResult::Intercept(Some((
|
|
|
|
|
Action::Shortcut(action.clone()),
|
|
|
|
|
binding.clone(),
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// no binding
|
|
|
|
|
if clear_queue {
|
|
|
|
|
seat.modifiers_shortcut_queue().clear();
|
|
|
|
|
}
|
|
|
|
|
// keys are passed through to apps
|
|
|
|
|
FilterResult::Forward
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-16 13:00:31 -07:00
|
|
|
fn keyboard_swap(
|
|
|
|
|
&self,
|
|
|
|
|
seat: &Seat<Self>,
|
|
|
|
|
shell: &mut Shell,
|
|
|
|
|
old_descriptor: &NodeDesc,
|
|
|
|
|
current_focus: Option<KeyboardFocusTarget>,
|
|
|
|
|
focused_output: &Output,
|
|
|
|
|
) {
|
|
|
|
|
if let Some(focus) = current_focus {
|
2025-01-06 19:23:06 +01:00
|
|
|
if let Some(new_descriptor) = shell
|
|
|
|
|
.workspaces
|
2025-10-16 13:50:32 +02:00
|
|
|
.active(focused_output)
|
2025-01-06 19:23:06 +01:00
|
|
|
.unwrap()
|
|
|
|
|
.1
|
|
|
|
|
.node_desc(focus)
|
2024-09-16 13:00:31 -07:00
|
|
|
{
|
|
|
|
|
let mut spaces = shell.workspaces.spaces_mut();
|
|
|
|
|
if old_descriptor.handle != new_descriptor.handle {
|
|
|
|
|
let (mut old_w, mut other_w) =
|
|
|
|
|
spaces.partition::<Vec<_>, _>(|w| w.handle == old_descriptor.handle);
|
2026-02-23 16:25:06 +01:00
|
|
|
if let Some(old_workspace) = old_w.get_mut(0)
|
|
|
|
|
&& let Some(new_workspace) = other_w
|
2024-09-16 13:00:31 -07:00
|
|
|
.iter_mut()
|
|
|
|
|
.find(|w| w.handle == new_descriptor.handle)
|
2026-02-23 16:25:06 +01:00
|
|
|
{
|
2024-09-16 13:00:31 -07:00
|
|
|
{
|
2026-02-23 16:25:06 +01:00
|
|
|
let mut stack = new_workspace.focus_stack.get_mut(seat);
|
|
|
|
|
for elem in old_descriptor.focus_stack.iter().flat_map(|node_id| {
|
|
|
|
|
old_workspace.tiling_layer.element_for_node(node_id)
|
|
|
|
|
}) {
|
|
|
|
|
stack.append(elem.clone());
|
2024-09-16 13:00:31 -07:00
|
|
|
}
|
2026-02-23 16:25:06 +01:00
|
|
|
}
|
|
|
|
|
{
|
|
|
|
|
let mut stack = old_workspace.focus_stack.get_mut(seat);
|
|
|
|
|
for elem in new_descriptor.focus_stack.iter().flat_map(|node_id| {
|
|
|
|
|
new_workspace.tiling_layer.element_for_node(node_id)
|
|
|
|
|
}) {
|
|
|
|
|
stack.append(elem.clone());
|
2024-09-16 13:00:31 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-23 16:25:06 +01:00
|
|
|
if let Some(focus) = TilingLayout::swap_trees(
|
|
|
|
|
&mut old_workspace.tiling_layer,
|
|
|
|
|
Some(&mut new_workspace.tiling_layer),
|
|
|
|
|
old_descriptor,
|
|
|
|
|
&new_descriptor,
|
|
|
|
|
) {
|
|
|
|
|
let seat = seat.clone();
|
|
|
|
|
self.common.event_loop_handle.insert_idle(move |state| {
|
|
|
|
|
Shell::set_focus(state, Some(&focus), &seat, None, true);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
old_workspace.refresh_focus_stack();
|
|
|
|
|
new_workspace.refresh_focus_stack();
|
2024-09-16 13:00:31 -07:00
|
|
|
}
|
2025-10-16 13:50:32 +02:00
|
|
|
} else if let Some(workspace) = spaces.find(|w| w.handle == new_descriptor.handle) {
|
|
|
|
|
if let Some(focus) = TilingLayout::swap_trees(
|
|
|
|
|
&mut workspace.tiling_layer,
|
|
|
|
|
None,
|
|
|
|
|
old_descriptor,
|
|
|
|
|
&new_descriptor,
|
|
|
|
|
) {
|
|
|
|
|
std::mem::drop(spaces);
|
|
|
|
|
let seat = seat.clone();
|
|
|
|
|
self.common.event_loop_handle.insert_idle(move |state| {
|
|
|
|
|
Shell::set_focus(state, Some(&focus), &seat, None, true);
|
|
|
|
|
});
|
2024-09-16 13:00:31 -07:00
|
|
|
}
|
2025-10-16 13:50:32 +02:00
|
|
|
workspace.refresh_focus_stack();
|
2024-09-16 13:00:31 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2025-10-16 13:50:32 +02:00
|
|
|
let new_workspace = shell.workspaces.active(focused_output).unwrap().1.handle;
|
2024-09-16 13:00:31 -07:00
|
|
|
if new_workspace != old_descriptor.handle {
|
|
|
|
|
let spaces = shell.workspaces.spaces_mut();
|
|
|
|
|
let (mut old_w, mut other_w) =
|
|
|
|
|
spaces.partition::<Vec<_>, _>(|w| w.handle == old_descriptor.handle);
|
2026-02-23 16:25:06 +01:00
|
|
|
if let Some(old_workspace) = old_w.get_mut(0)
|
|
|
|
|
&& let Some(new_workspace) =
|
2024-09-16 13:00:31 -07:00
|
|
|
other_w.iter_mut().find(|w| w.handle == new_workspace)
|
2026-02-23 16:25:06 +01:00
|
|
|
&& new_workspace.tiling_layer.windows().next().is_none()
|
|
|
|
|
{
|
2024-09-16 13:00:31 -07:00
|
|
|
{
|
2026-02-23 16:25:06 +01:00
|
|
|
let mut stack = new_workspace.focus_stack.get_mut(seat);
|
|
|
|
|
for elem in old_descriptor.focus_stack.iter().flat_map(|node_id| {
|
|
|
|
|
old_workspace.tiling_layer.element_for_node(node_id)
|
|
|
|
|
}) {
|
|
|
|
|
stack.append(elem.clone());
|
2024-09-16 13:00:31 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-23 16:25:06 +01:00
|
|
|
if let Some(focus) = TilingLayout::move_tree(
|
|
|
|
|
&mut old_workspace.tiling_layer,
|
|
|
|
|
&mut new_workspace.tiling_layer,
|
|
|
|
|
&new_workspace.handle,
|
|
|
|
|
seat,
|
|
|
|
|
new_workspace.focus_stack.get(seat).iter(),
|
|
|
|
|
old_descriptor.clone(),
|
|
|
|
|
None,
|
|
|
|
|
) {
|
|
|
|
|
let seat = seat.clone();
|
|
|
|
|
self.common.event_loop_handle.insert_idle(move |state| {
|
|
|
|
|
Shell::set_focus(state, Some(&focus), &seat, None, true);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
old_workspace.refresh_focus_stack();
|
2024-09-16 13:00:31 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-20 17:54:12 +02:00
|
|
|
#[profiling::function]
|
2024-10-10 23:18:04 +02:00
|
|
|
pub fn element_under(
|
|
|
|
|
global_pos: Point<f64, Global>,
|
|
|
|
|
output: &Output,
|
|
|
|
|
shell: &Shell,
|
2025-06-25 17:54:27 +02:00
|
|
|
seat: &Seat<State>,
|
2024-10-10 23:18:04 +02:00
|
|
|
) -> Option<KeyboardFocusTarget> {
|
2025-01-06 19:23:06 +01:00
|
|
|
let (previous_workspace, workspace) = shell.workspaces.active(output)?;
|
2024-10-10 23:18:04 +02:00
|
|
|
let (previous_idx, idx) = shell.workspaces.active_num(output);
|
|
|
|
|
let previous_workspace = previous_workspace
|
|
|
|
|
.zip(previous_idx)
|
|
|
|
|
.map(|((w, start), idx)| (w.handle, idx, start));
|
|
|
|
|
let workspace = (workspace.handle, idx);
|
|
|
|
|
let element_filter = if workspace_overview_is_open(output) {
|
|
|
|
|
ElementFilter::LayerShellOnly
|
|
|
|
|
} else {
|
|
|
|
|
ElementFilter::All
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
render_input_order(
|
|
|
|
|
shell,
|
|
|
|
|
output,
|
|
|
|
|
previous_workspace,
|
|
|
|
|
workspace,
|
|
|
|
|
element_filter,
|
|
|
|
|
|stage| {
|
|
|
|
|
match stage {
|
2025-02-13 21:09:13 +01:00
|
|
|
Stage::ZoomUI => {}
|
2024-10-10 23:18:04 +02:00
|
|
|
Stage::SessionLock(lock_surface) => {
|
|
|
|
|
return ControlFlow::Break(Ok(lock_surface
|
|
|
|
|
.cloned()
|
2025-07-21 14:56:53 +02:00
|
|
|
.map(KeyboardFocusTarget::LockSurface)));
|
2024-10-10 23:18:04 +02:00
|
|
|
}
|
|
|
|
|
Stage::LayerPopup {
|
|
|
|
|
layer,
|
|
|
|
|
popup,
|
|
|
|
|
location,
|
2026-03-06 17:19:12 +01:00
|
|
|
..
|
2024-10-10 23:18:04 +02:00
|
|
|
} => {
|
|
|
|
|
if layer.can_receive_keyboard_focus() {
|
|
|
|
|
let surface = popup.wl_surface();
|
|
|
|
|
if under_from_surface_tree(
|
|
|
|
|
surface,
|
|
|
|
|
global_pos.as_logical(),
|
|
|
|
|
location.as_logical(),
|
|
|
|
|
WindowSurfaceType::POPUP | WindowSurfaceType::SUBSURFACE,
|
|
|
|
|
)
|
|
|
|
|
.is_some()
|
|
|
|
|
{
|
|
|
|
|
return ControlFlow::Break(Ok(Some(
|
|
|
|
|
KeyboardFocusTarget::LayerSurface(layer),
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-06 17:19:12 +01:00
|
|
|
Stage::LayerSurface {
|
|
|
|
|
layer, location, ..
|
|
|
|
|
} => {
|
2025-05-21 09:30:42 -07:00
|
|
|
if under_from_surface_tree(
|
|
|
|
|
layer.wl_surface(),
|
|
|
|
|
global_pos.as_logical(),
|
|
|
|
|
location.as_logical(),
|
|
|
|
|
WindowSurfaceType::TOPLEVEL | WindowSurfaceType::SUBSURFACE,
|
|
|
|
|
)
|
|
|
|
|
.is_some()
|
|
|
|
|
{
|
|
|
|
|
return ControlFlow::Break(Ok(if layer.can_receive_keyboard_focus() {
|
|
|
|
|
Some(KeyboardFocusTarget::LayerSurface(layer))
|
|
|
|
|
} else {
|
|
|
|
|
// Don't change keyboard focus if in input region of layer shell
|
|
|
|
|
// surface, but surface doesn't have keyboard interactivity.
|
|
|
|
|
None
|
|
|
|
|
}));
|
2024-10-10 23:18:04 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Stage::OverrideRedirect { .. } => {
|
|
|
|
|
// Override redirect windows take a grab on their own via
|
|
|
|
|
// the Xwayland keyboard grab protocol. Don't focus them via click.
|
|
|
|
|
}
|
|
|
|
|
Stage::StickyPopups(layout) => {
|
|
|
|
|
if let Some(element) =
|
2026-05-27 14:17:01 +08:00
|
|
|
layout.popup_element_under(global_pos.to_local(output), seat)
|
2024-10-10 23:18:04 +02:00
|
|
|
{
|
|
|
|
|
return ControlFlow::Break(Ok(Some(element)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Stage::Sticky(layout) => {
|
|
|
|
|
if let Some(element) =
|
2026-05-27 14:17:01 +08:00
|
|
|
layout.toplevel_element_under(global_pos.to_local(output), seat)
|
2024-10-10 23:18:04 +02:00
|
|
|
{
|
|
|
|
|
return ControlFlow::Break(Ok(Some(element)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Stage::WorkspacePopups { workspace, offset } => {
|
|
|
|
|
let location = global_pos + offset.as_global().to_f64();
|
|
|
|
|
let output = workspace.output();
|
|
|
|
|
let output_geo = output.geometry().to_local(output);
|
2024-12-26 18:18:35 -08:00
|
|
|
if Rectangle::new(offset.as_local(), output_geo.size)
|
2024-10-10 23:18:04 +02:00
|
|
|
.intersection(output_geo)
|
|
|
|
|
.is_some_and(|geometry| {
|
2026-07-21 10:32:09 +02:00
|
|
|
geometry.contains(global_pos.to_local(output).to_i32_floor())
|
2024-10-10 23:18:04 +02:00
|
|
|
})
|
2026-02-23 16:25:06 +01:00
|
|
|
&& let Some(element) = workspace.popup_element_under(location, seat)
|
2024-10-10 23:18:04 +02:00
|
|
|
{
|
2026-02-23 16:25:06 +01:00
|
|
|
return ControlFlow::Break(Ok(Some(element)));
|
2024-10-10 23:18:04 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Stage::Workspace { workspace, offset } => {
|
|
|
|
|
let location = global_pos + offset.as_global().to_f64();
|
|
|
|
|
let output = workspace.output();
|
|
|
|
|
let output_geo = output.geometry().to_local(output);
|
2024-12-26 18:18:35 -08:00
|
|
|
if Rectangle::new(offset.as_local(), output_geo.size)
|
2024-10-10 23:18:04 +02:00
|
|
|
.intersection(output_geo)
|
|
|
|
|
.is_some_and(|geometry| {
|
2026-07-21 10:32:09 +02:00
|
|
|
geometry.contains(global_pos.to_local(output).to_i32_floor())
|
2024-10-10 23:18:04 +02:00
|
|
|
})
|
2026-02-23 16:25:06 +01:00
|
|
|
&& let Some(element) = workspace.toplevel_element_under(location, seat)
|
2024-10-10 23:18:04 +02:00
|
|
|
{
|
2026-02-23 16:25:06 +01:00
|
|
|
return ControlFlow::Break(Ok(Some(element)));
|
2024-10-10 23:18:04 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ControlFlow::Continue(())
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.ok()
|
|
|
|
|
.flatten()
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-20 17:54:12 +02:00
|
|
|
#[profiling::function]
|
2021-12-22 20:14:09 +01:00
|
|
|
pub fn surface_under(
|
2023-10-25 19:24:51 +02:00
|
|
|
global_pos: Point<f64, Global>,
|
2021-12-22 20:14:09 +01:00
|
|
|
output: &Output,
|
2024-10-10 23:18:04 +02:00
|
|
|
shell: &Shell,
|
2024-06-18 19:23:16 -07:00
|
|
|
) -> Option<(PointerFocusTarget, Point<f64, Global>)> {
|
2025-01-06 19:23:06 +01:00
|
|
|
let (previous_workspace, workspace) = shell.workspaces.active(output)?;
|
2024-10-10 23:18:04 +02:00
|
|
|
let (previous_idx, idx) = shell.workspaces.active_num(output);
|
|
|
|
|
let previous_workspace = previous_workspace
|
|
|
|
|
.zip(previous_idx)
|
|
|
|
|
.map(|((w, start), idx)| (w.handle, idx, start));
|
|
|
|
|
let workspace = (workspace.handle, idx);
|
|
|
|
|
|
|
|
|
|
let element_filter = if workspace_overview_is_open(output) {
|
|
|
|
|
ElementFilter::LayerShellOnly
|
|
|
|
|
} else {
|
|
|
|
|
ElementFilter::All
|
|
|
|
|
};
|
|
|
|
|
|
2023-10-25 19:24:51 +02:00
|
|
|
let relative_pos = global_pos.to_local(output);
|
|
|
|
|
let output_geo = output.geometry();
|
2024-10-10 23:18:04 +02:00
|
|
|
let overview = shell.overview_mode().0;
|
2025-06-25 17:54:27 +02:00
|
|
|
let seat = shell.seats.last_active();
|
2024-10-10 23:18:04 +02:00
|
|
|
|
|
|
|
|
render_input_order(
|
|
|
|
|
shell,
|
|
|
|
|
output,
|
|
|
|
|
previous_workspace,
|
|
|
|
|
workspace,
|
|
|
|
|
element_filter,
|
|
|
|
|
|stage| {
|
|
|
|
|
match stage {
|
2025-02-13 21:09:13 +01:00
|
|
|
Stage::ZoomUI => {
|
2026-02-23 16:25:06 +01:00
|
|
|
if let Some(zoom_state) = shell.zoom_state()
|
|
|
|
|
&& let Some((target, loc)) =
|
2025-02-13 21:09:13 +01:00
|
|
|
zoom_state.surface_under(output, global_pos)
|
2026-02-23 16:25:06 +01:00
|
|
|
{
|
|
|
|
|
return ControlFlow::Break(Ok(Some((target, loc))));
|
2025-02-13 21:09:13 +01:00
|
|
|
}
|
|
|
|
|
}
|
2024-10-10 23:18:04 +02:00
|
|
|
Stage::SessionLock(lock_surface) => {
|
2025-03-12 15:29:17 +01:00
|
|
|
return ControlFlow::Break(Ok(lock_surface.and_then(|surface| {
|
|
|
|
|
let location = output_geo.loc;
|
|
|
|
|
if let Some((surface, surface_loc)) = under_from_surface_tree(
|
|
|
|
|
surface.wl_surface(),
|
|
|
|
|
global_pos.as_logical(),
|
|
|
|
|
location.as_logical(),
|
|
|
|
|
WindowSurfaceType::ALL,
|
|
|
|
|
) {
|
|
|
|
|
Some((
|
|
|
|
|
PointerFocusTarget::WlSurface {
|
|
|
|
|
surface,
|
|
|
|
|
toplevel: None,
|
|
|
|
|
},
|
|
|
|
|
surface_loc.as_global().to_f64(),
|
|
|
|
|
))
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
2024-10-10 23:18:04 +02:00
|
|
|
})));
|
2023-07-21 16:08:55 +02:00
|
|
|
}
|
2024-10-10 23:18:04 +02:00
|
|
|
Stage::LayerPopup {
|
|
|
|
|
popup, location, ..
|
|
|
|
|
} => {
|
|
|
|
|
let surface = popup.wl_surface();
|
|
|
|
|
if let Some((surface, surface_loc)) = under_from_surface_tree(
|
|
|
|
|
surface,
|
|
|
|
|
global_pos.as_logical(),
|
|
|
|
|
location.as_logical(),
|
|
|
|
|
WindowSurfaceType::ALL,
|
|
|
|
|
) {
|
|
|
|
|
return ControlFlow::Break(Ok(Some((
|
|
|
|
|
PointerFocusTarget::WlSurface {
|
|
|
|
|
surface,
|
|
|
|
|
toplevel: None,
|
|
|
|
|
},
|
|
|
|
|
surface_loc.as_global().to_f64(),
|
|
|
|
|
))));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-06 17:19:12 +01:00
|
|
|
Stage::LayerSurface {
|
|
|
|
|
layer, location, ..
|
|
|
|
|
} => {
|
2024-10-10 23:18:04 +02:00
|
|
|
let surface = layer.wl_surface();
|
|
|
|
|
if let Some((surface, surface_loc)) = under_from_surface_tree(
|
|
|
|
|
surface,
|
|
|
|
|
global_pos.as_logical(),
|
|
|
|
|
location.as_logical(),
|
|
|
|
|
WindowSurfaceType::ALL,
|
|
|
|
|
) {
|
|
|
|
|
return ControlFlow::Break(Ok(Some((
|
|
|
|
|
PointerFocusTarget::WlSurface {
|
|
|
|
|
surface,
|
|
|
|
|
toplevel: None,
|
|
|
|
|
},
|
|
|
|
|
surface_loc.as_global().to_f64(),
|
|
|
|
|
))));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Stage::OverrideRedirect { surface, location } => {
|
2025-11-10 18:32:34 +01:00
|
|
|
if let Some((_, surface_loc)) = surface.surface_under(
|
|
|
|
|
global_pos.as_logical(),
|
|
|
|
|
location.as_logical(),
|
|
|
|
|
WindowSurfaceType::ALL,
|
|
|
|
|
) {
|
|
|
|
|
return ControlFlow::Break(Ok(Some((
|
|
|
|
|
PointerFocusTarget::X11Surface {
|
|
|
|
|
surface: surface.clone(),
|
|
|
|
|
toplevel: None,
|
|
|
|
|
},
|
|
|
|
|
surface_loc.as_global().to_f64(),
|
|
|
|
|
))));
|
2024-10-10 23:18:04 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Stage::StickyPopups(floating_layer) => {
|
|
|
|
|
if let Some(under) = floating_layer
|
2026-05-27 14:17:01 +08:00
|
|
|
.popup_surface_under(relative_pos, seat)
|
2024-10-10 23:18:04 +02:00
|
|
|
.map(|(target, point)| (target, point.to_global(output)))
|
|
|
|
|
{
|
|
|
|
|
return ControlFlow::Break(Ok(Some(under)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Stage::Sticky(floating_layer) => {
|
|
|
|
|
if let Some(under) = floating_layer
|
2026-05-27 14:17:01 +08:00
|
|
|
.toplevel_surface_under(relative_pos, seat)
|
2024-10-10 23:18:04 +02:00
|
|
|
.map(|(target, point)| (target, point.to_global(output)))
|
|
|
|
|
{
|
|
|
|
|
return ControlFlow::Break(Ok(Some(under)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Stage::WorkspacePopups { workspace, offset } => {
|
|
|
|
|
let global_pos = global_pos + offset.to_f64().as_global();
|
|
|
|
|
if let Some(under) =
|
2025-06-25 17:54:27 +02:00
|
|
|
workspace.popup_surface_under(global_pos, overview.clone(), seat)
|
2024-10-10 23:18:04 +02:00
|
|
|
{
|
|
|
|
|
return ControlFlow::Break(Ok(Some(under)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Stage::Workspace { workspace, offset } => {
|
|
|
|
|
let global_pos = global_pos + offset.to_f64().as_global();
|
|
|
|
|
if let Some(under) =
|
2025-06-25 17:54:27 +02:00
|
|
|
workspace.toplevel_surface_under(global_pos, overview.clone(), seat)
|
2024-10-10 23:18:04 +02:00
|
|
|
{
|
|
|
|
|
return ControlFlow::Break(Ok(Some(under)));
|
|
|
|
|
}
|
2023-07-21 16:08:55 +02:00
|
|
|
}
|
2022-11-11 23:23:38 +01:00
|
|
|
}
|
2024-10-10 23:18:04 +02:00
|
|
|
|
|
|
|
|
ControlFlow::Continue(())
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.ok()
|
|
|
|
|
.flatten()
|
2021-12-22 20:14:09 +01:00
|
|
|
}
|
2026-05-13 19:53:07 +08:00
|
|
|
|
|
|
|
|
pub fn apply_cursor_hint(
|
|
|
|
|
&mut self,
|
|
|
|
|
surface: &WlSurface,
|
|
|
|
|
pointer: &PointerHandle<Self>,
|
|
|
|
|
mut location: Point<f64, Logical>,
|
|
|
|
|
) {
|
2026-05-13 20:08:21 +08:00
|
|
|
let Some(client) = surface.client() else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
location = location.downscale(self.client_compositor_state(&client).client_scale());
|
2026-05-13 19:53:07 +08:00
|
|
|
|
|
|
|
|
let point_and_output = {
|
|
|
|
|
let shell = self.common.shell.read();
|
|
|
|
|
let found = shell.workspaces.sets.iter().find_map(|(out, set)| {
|
|
|
|
|
set.surface_geometry_offset_from_toplevel(surface)
|
|
|
|
|
.map(|(geometry, surface_offset)| (out, geometry, surface_offset))
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if let Some((output, geometry, surface_offset)) = found {
|
2026-06-05 23:35:29 +08:00
|
|
|
let pos_in_element = location + surface_offset.to_f64();
|
2026-05-13 19:53:07 +08:00
|
|
|
let window_size = geometry.size.to_f64();
|
|
|
|
|
|
|
|
|
|
let is_legal = |p: Point<f64, Logical>| {
|
2026-05-27 14:17:01 +08:00
|
|
|
let in_window =
|
|
|
|
|
p.x >= 0.0 && p.y >= 0.0 && p.x < window_size.w && p.y < window_size.h;
|
2026-05-13 19:53:07 +08:00
|
|
|
if !in_window {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
with_pointer_constraint(surface, pointer, |constraint| {
|
|
|
|
|
if let Some(constraint) = constraint
|
|
|
|
|
&& let Some(region) = constraint.region()
|
|
|
|
|
{
|
2026-07-21 10:32:09 +02:00
|
|
|
let point_in_surface = (p - surface_offset.to_f64()).to_i32_floor();
|
2026-05-13 19:53:07 +08:00
|
|
|
return region.contains(point_in_surface);
|
|
|
|
|
}
|
|
|
|
|
true
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let workspace_origin = output.geometry().loc.to_f64();
|
|
|
|
|
let origin = geometry.loc.to_f64();
|
|
|
|
|
|
2026-06-05 23:35:29 +08:00
|
|
|
if is_legal(pos_in_element) {
|
|
|
|
|
let x = workspace_origin.x + origin.x + pos_in_element.x;
|
|
|
|
|
let y = workspace_origin.y + origin.y + pos_in_element.y;
|
2026-07-14 15:04:53 -07:00
|
|
|
Some((Point::<_, Global>::new(x, y), output.clone()))
|
2026-06-05 23:35:29 +08:00
|
|
|
} else {
|
|
|
|
|
None
|
2026-05-13 19:53:07 +08:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Some((point, output)) = point_and_output {
|
|
|
|
|
let original_position = pointer.current_location();
|
2026-07-14 15:04:53 -07:00
|
|
|
pointer.set_location(point.as_logical());
|
2026-05-13 19:53:07 +08:00
|
|
|
|
|
|
|
|
let mut shell = self.common.shell.write();
|
2026-07-14 15:04:53 -07:00
|
|
|
shell.update_pointer_position(point.to_local(&output), &output);
|
2026-05-13 19:53:07 +08:00
|
|
|
|
|
|
|
|
let seat = shell
|
|
|
|
|
.seats
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|s| s.get_pointer().as_ref() == Some(pointer))
|
|
|
|
|
.cloned();
|
|
|
|
|
|
|
|
|
|
if let Some(seat) = seat {
|
|
|
|
|
shell.update_focal_point(
|
|
|
|
|
&seat,
|
|
|
|
|
original_position.as_global(),
|
|
|
|
|
self.common.config.cosmic_conf.accessibility_zoom.view_moves,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let output_geometry = output.geometry();
|
|
|
|
|
for session in cursor_sessions_for_output(&shell, &output) {
|
2026-06-15 16:23:31 -07:00
|
|
|
if let Some(cursor_geometry) = seat.cursor_geometry(
|
2026-07-14 15:04:53 -07:00
|
|
|
point.as_logical().to_buffer(
|
2026-05-13 19:53:07 +08:00
|
|
|
output.current_scale().fractional_scale(),
|
|
|
|
|
output.current_transform(),
|
|
|
|
|
&output_geometry.size.to_f64().as_logical(),
|
|
|
|
|
),
|
|
|
|
|
self.common.clock.now(),
|
|
|
|
|
) {
|
2026-06-15 16:23:31 -07:00
|
|
|
let constraints = cursor_capture_constraints(Some(cursor_geometry));
|
2026-05-13 19:53:07 +08:00
|
|
|
if session
|
|
|
|
|
.current_constraints()
|
2026-06-15 16:23:31 -07:00
|
|
|
.map(|current_constraints| current_constraints.size != constraints.size)
|
2026-05-13 19:53:07 +08:00
|
|
|
.unwrap_or(true)
|
|
|
|
|
{
|
2026-06-15 16:23:31 -07:00
|
|
|
session.update_constraints(constraints);
|
2026-05-13 19:53:07 +08:00
|
|
|
}
|
2026-06-15 16:23:31 -07:00
|
|
|
session.set_cursor_hotspot(cursor_geometry.hotspot);
|
|
|
|
|
session.set_cursor_pos(Some(cursor_geometry.geometry.loc));
|
2026-05-13 19:53:07 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-04-10 15:49:08 +02:00
|
|
|
}
|
2024-03-07 13:14:53 -06:00
|
|
|
|
2026-05-07 10:16:35 -07:00
|
|
|
// Output and workspace sessions for the given output
|
2025-01-06 19:23:06 +01:00
|
|
|
fn cursor_sessions_for_output<'a>(
|
|
|
|
|
shell: &'a Shell,
|
|
|
|
|
output: &'a Output,
|
protocols/screencopy: Make frame/session send stopped/fail on drop
Previously, `Frame` was stored in KMS frame udata, but in some cases the
udata was dropped without a capture happening, and `Frame` did not
implement `Drop`, so `fail` was never sent.
Instead, rename `DropableFrame` to `Frame` and `Frame` to `FrameRef`, so
we can have a single instance of `Frame`, that will send `fail` on drop.
This guarantees either `.success` or `.fail` are send, as long as its
not leaked.
This seems to fix https://github.com/pop-os/cosmic-comp/issues/1305.
xdg-desktop-portal-cosmic prints an error, buy retries (as it should for
an `Unknown` error; though maybe there should be a retry limit) and the
session continues working.
(Not sure if it should be sending `failed`, or queing it with the next
frame so it can send `success` to the client, but this works and is
desirable as a failsafe anyway.)
`Session` and `CursorSession` are similiarly updated.
`.fail()`, `.success()`, and `.stop()` now consume
`Frame`/`Session`/`CursorSession`. So to stop a session, it is now
necessary to call `.remove_session()`, but then simply dropping with
send `.stop()`.
Factoring out some `Request::Capture` handling into a `capture_frame`
function seems to clean up error handling and such a bit.
2025-04-30 14:32:33 -07:00
|
|
|
) -> impl Iterator<Item = CursorSessionRef> + 'a {
|
2025-01-06 19:23:06 +01:00
|
|
|
shell
|
2025-10-16 13:50:32 +02:00
|
|
|
.active_space(output)
|
2024-03-12 19:42:48 +01:00
|
|
|
.into_iter()
|
2025-01-06 19:23:06 +01:00
|
|
|
.flat_map(|workspace| {
|
|
|
|
|
workspace
|
|
|
|
|
.cursor_sessions()
|
2022-11-03 18:51:27 +01:00
|
|
|
.into_iter()
|
2025-10-16 13:50:32 +02:00
|
|
|
.chain(output.cursor_sessions())
|
2025-01-06 19:23:06 +01:00
|
|
|
})
|
2022-11-03 18:51:27 +01:00
|
|
|
}
|
2023-12-28 13:36:59 -08:00
|
|
|
|
2025-10-16 15:46:54 +02:00
|
|
|
fn transform_output_mapped_position<B, E>(
|
2025-02-13 21:09:13 +01:00
|
|
|
output: &Output,
|
|
|
|
|
event: &E,
|
|
|
|
|
zoom_state: Option<&ZoomState>,
|
|
|
|
|
) -> Point<f64, Global>
|
2024-11-20 13:13:07 -08:00
|
|
|
where
|
|
|
|
|
B: InputBackend,
|
|
|
|
|
E: AbsolutePositionEvent<B>,
|
|
|
|
|
B::Device: 'static,
|
|
|
|
|
{
|
2025-02-13 21:09:13 +01:00
|
|
|
let geometry = zoom_state
|
2025-03-25 17:31:48 +01:00
|
|
|
.and_then(|_| output.zoomed_geometry())
|
2025-02-13 21:09:13 +01:00
|
|
|
.unwrap_or_else(|| output.geometry());
|
2024-11-20 13:21:09 -08:00
|
|
|
let transform = output.current_transform();
|
|
|
|
|
let size = transform
|
|
|
|
|
.invert()
|
|
|
|
|
.transform_size(geometry.size.as_logical());
|
|
|
|
|
geometry.loc.to_f64()
|
|
|
|
|
+ transform
|
|
|
|
|
.transform_point_in(event.position_transformed(size), &size.to_f64())
|
|
|
|
|
.as_global()
|
2024-11-20 13:13:07 -08:00
|
|
|
}
|
|
|
|
|
|
2023-12-28 13:36:59 -08:00
|
|
|
// TODO Is it possible to determine mapping for external touchscreen?
|
|
|
|
|
// Support map_to_region like sway?
|
|
|
|
|
fn mapped_output_for_device<'a, D: Device + 'static>(
|
2024-04-10 15:49:08 +02:00
|
|
|
config: &Config,
|
|
|
|
|
shell: &'a Shell,
|
2023-12-28 13:36:59 -08:00
|
|
|
device: &D,
|
|
|
|
|
) -> Option<&'a Output> {
|
|
|
|
|
let map_to_output = if let Some(device) = <dyn Any>::downcast_ref::<InputDevice>(device) {
|
2024-04-10 15:49:08 +02:00
|
|
|
config
|
2023-12-28 13:36:59 -08:00
|
|
|
.map_to_output(device)
|
2024-04-10 15:49:08 +02:00
|
|
|
.and_then(|name| shell.outputs().find(|output| output.name() == name))
|
2023-12-28 13:36:59 -08:00
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
2024-04-10 15:49:08 +02:00
|
|
|
map_to_output.or_else(|| shell.builtin_output())
|
2023-12-28 13:36:59 -08:00
|
|
|
}
|