cosmic-comp/src/shell/grabs/moving.rs

1087 lines
38 KiB
Rust
Raw Normal View History

// SPDX-License-Identifier: GPL-3.0-only
use crate::{
2023-07-18 12:21:16 +02:00
backend::render::{
2025-10-16 18:53:57 +02:00
BackdropShader, IndicatorShader, Key, Usage, cursor::CursorState, element::AsGlowRenderer,
2023-07-18 12:21:16 +02:00
},
shell::{
2025-10-16 18:53:57 +02:00
CosmicMapped, CosmicSurface, Direction, ManagedLayer,
element::{CosmicMappedRenderElement, stack_hover::StackHover},
focus::target::{KeyboardFocusTarget, PointerFocusTarget},
2026-08-11 18:18:02 +02:00
grabs::GrabType,
2024-03-18 00:12:58 -05:00
layout::floating::TiledCorners,
},
utils::prelude::*,
2025-09-25 11:47:53 -04:00
wayland::protocols::toplevel_info::{toplevel_enter_output, toplevel_enter_workspace},
};
use calloop::LoopHandle;
use cosmic::theme::CosmicTheme;
use smallvec::SmallVec;
use smithay::{
backend::{
drm::DrmNode,
2026-08-11 18:18:02 +02:00
input::{ButtonState, InputTime, TabletToolDescriptor},
renderer::{
ImportAll, ImportMem,
element::{RenderElement, utils::RescaleRenderElement},
},
2023-07-18 12:21:31 +02:00
},
2025-10-16 18:53:57 +02:00
desktop::{WindowSurfaceType, layer_map_for_output, space::SpaceElement},
input::{
2025-10-16 18:53:57 +02:00
Seat,
pointer::{
2026-08-11 18:18:02 +02:00
AxisFrame as PointerAxisFrame, ButtonEvent as PointerButtonEvent, CursorIcon,
GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent,
GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
MotionEvent as PointerMotionEvent, PointerGrab, PointerInnerHandle,
RelativeMotionEvent,
},
2026-08-11 18:18:02 +02:00
tablet::{
TabletSeatHandler,
tool::{
AxisFrame as TabletAxisFrame, ButtonEvent as TabletButtonEvent,
DownEvent as TabletDownEvent, GrabStartData as TabletGrabStartData,
MotionEvent as TabletMotionEvent, ProximityInEvent, ProximityOutEvent,
TabletToolGrab, TabletToolInnerHandle, UpEvent as TabletUpEvent,
},
},
touch::{
DownEvent as TouchDownEvent, GrabStartData as TouchGrabStartData,
MotionEvent as TouchMotionEvent, OrientationEvent, ShapeEvent, TouchGrab,
TouchInnerHandle, UpEvent as TouchUpEvent,
},
},
output::Output,
2026-06-29 17:20:02 +02:00
utils::{IsAlive, Logical, Point, Rectangle, SERIAL_COUNTER, Scale},
2023-07-18 12:21:31 +02:00
};
use std::{
collections::HashSet,
2025-10-16 18:53:57 +02:00
sync::{Mutex, atomic::Ordering},
time::Instant,
};
use super::{GrabStartData, ReleaseMode};
pub type SeatMoveGrabState = Mutex<Option<MoveGrabState>>;
2023-07-18 12:21:31 +02:00
const RESCALE_ANIMATION_DURATION: f64 = 150.0;
pub struct MoveGrabState {
window: CosmicMapped,
2023-03-06 18:50:59 +01:00
window_offset: Point<i32, Logical>,
indicator_thickness: u8,
2023-07-18 12:21:31 +02:00
start: Instant,
previous: ManagedLayer,
2024-03-18 00:12:58 -05:00
snapping_zone: Option<SnappingZone>,
2023-07-24 19:31:31 +02:00
stacking_indicator: Option<(StackHover, Point<i32, Logical>)>,
location: Point<f64, Logical>,
cursor_output: Output,
}
impl MoveGrabState {
2023-10-07 19:15:44 -07:00
#[profiling::function]
pub fn render<R>(
&self,
renderer: &mut R,
output: &Output,
theme: &CosmicTheme,
scanout_node: Option<DrmNode>,
push: &mut dyn FnMut(CosmicMappedRenderElement<R>),
) where
R: AsGlowRenderer + ImportAll + ImportMem,
2025-03-11 19:14:49 +01:00
R::TextureId: Send + Clone + 'static,
CosmicMappedRenderElement<R>: RenderElement<R>,
{
let scale = if self.previous == ManagedLayer::Tiling {
2023-07-25 16:46:08 +02:00
0.6 + ((1.0
2023-07-18 12:21:31 +02:00
- (Instant::now().duration_since(self.start).as_millis() as f64
/ RESCALE_ANIMATION_DURATION)
.min(1.0))
2023-07-25 16:46:08 +02:00
* 0.4)
} else {
1.0
};
let alpha = if &self.cursor_output == output {
1.0
} else {
0.4
};
2023-07-18 12:21:31 +02:00
let mut window_geo = self.window.geometry();
window_geo.loc += self.location.to_i32_round() + self.window_offset;
2025-10-16 13:50:32 +02:00
if output
.geometry()
.as_logical()
2025-10-16 16:10:35 +02:00
.intersection(window_geo)
.is_none()
{
return;
}
let output_scale: Scale<f64> = output.current_scale().fractional_scale().into();
2023-07-18 12:21:31 +02:00
let scaling_offset =
self.window_offset - self.window_offset.to_f64().upscale(scale).to_i32_round();
let render_location = self.location.to_i32_round() - output.geometry().loc.as_logical()
+ self.window_offset
- scaling_offset;
2023-03-06 18:50:59 +01:00
for (indicator, location) in self.stacking_indicator.iter() {
indicator.push_render_elements(
renderer,
location.to_physical_precise_round(output_scale),
output_scale,
1.0,
&mut |elem| push(elem.into()),
None,
);
}
self.window.push_popup_render_elements::<R>(
renderer,
(render_location - self.window.geometry().loc).to_physical_precise_round(output_scale),
output_scale,
alpha,
scanout_node,
push,
);
let active_window_hint = crate::theme::active_window_hint(theme);
2025-09-24 15:11:16 -04:00
let radius = self
2025-10-03 12:28:57 -04:00
.element()
.corner_radius(window_geo.size, self.indicator_thickness);
2025-09-24 15:11:16 -04:00
if self.indicator_thickness > 0 {
push(
2025-10-16 16:10:35 +02:00
IndicatorShader::focus_element(
renderer,
Key::Window(Usage::MoveGrabIndicator, self.window.key()),
Rectangle::new(
2023-07-18 12:21:31 +02:00
render_location,
self.window
.geometry()
.size
.to_f64()
.upscale(scale)
.to_i32_round(),
)
.as_local(),
self.indicator_thickness,
2025-09-24 15:11:16 -04:00
radius,
alpha,
output_scale.x,
[
active_window_hint.red,
active_window_hint.green,
active_window_hint.blue,
],
)
.into(),
)
}
2024-04-19 20:38:14 -05:00
let map_window_element = |elem| match elem {
CosmicMappedRenderElement::Stack(stack) => {
CosmicMappedRenderElement::GrabbedStack(RescaleRenderElement::from_element(
stack,
render_location
.to_physical_precise_round(output.current_scale().fractional_scale()),
scale,
))
}
CosmicMappedRenderElement::Window(window) => {
CosmicMappedRenderElement::GrabbedWindow(RescaleRenderElement::from_element(
window,
render_location
.to_physical_precise_round(output.current_scale().fractional_scale()),
scale,
))
}
x => x,
2024-03-18 00:12:58 -05:00
};
let mut lower_elements = SmallVec::<[CosmicMappedRenderElement<R>; 4]>::new_const();
self.window.push_render_elements(
renderer,
(render_location - self.window.geometry().loc).to_physical_precise_round(output_scale),
None,
output_scale,
alpha,
Some(false),
scanout_node,
&mut |elem| push(map_window_element(elem)),
&mut |elem| lower_elements.push(map_window_element(elem)),
);
if let Some(shadow_element) = self.window.shadow_render_element(
renderer,
(render_location - self.window.geometry().loc).to_physical_precise_round(output_scale),
None,
output_scale,
scale,
alpha,
) {
push(shadow_element);
}
for elem in lower_elements.into_iter() {
push(elem);
}
let non_exclusive_geometry = {
let layers = layer_map_for_output(output);
layers.non_exclusive_zone()
};
let gaps = (theme.gaps.0 as i32, theme.gaps.1 as i32);
let thickness = self.indicator_thickness.max(1);
if let Some(t) = &self.snapping_zone
&& &self.cursor_output == output
{
let base_color = theme.palette.neutral_9;
let overlay_geometry = t.overlay_geometry(non_exclusive_geometry, gaps);
push(
IndicatorShader::element(
2023-07-24 19:31:31 +02:00
renderer,
Key::Window(Usage::SnappingIndicator, self.window.key()),
overlay_geometry,
thickness,
[
theme.radius_s()[0] as u8,
theme.radius_s()[1] as u8,
theme.radius_s()[2] as u8,
theme.radius_s()[3] as u8,
],
2023-07-24 19:31:31 +02:00
1.0,
output_scale.x,
[
active_window_hint.red,
active_window_hint.green,
active_window_hint.blue,
],
2023-07-24 19:31:31 +02:00
)
.into(),
);
push(
BackdropShader::element(
renderer,
Key::Window(Usage::SnappingIndicator, self.window.key()),
t.overlay_geometry(non_exclusive_geometry, gaps),
theme.radius_s()[0], // TODO: Fix once shaders support 4 corner radii customization
0.4,
[base_color.red, base_color.green, base_color.blue],
)
.into(),
)
}
}
pub fn element(&self) -> CosmicMapped {
self.window.clone()
}
2023-03-31 13:57:37 +02:00
pub fn window(&self) -> CosmicSurface {
self.window.active_window()
}
}
struct NotSend<T>(pub T);
unsafe impl<T> Send for NotSend<T> {}
2024-03-18 00:12:58 -05:00
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SnappingZone {
Maximize,
Top,
TopLeft,
2024-03-18 00:12:58 -05:00
Left,
BottomLeft,
2024-03-18 00:12:58 -05:00
Bottom,
BottomRight,
Right,
TopRight,
2024-03-18 00:12:58 -05:00
}
const SNAP_RANGE: i32 = 32;
2024-04-09 17:50:52 -05:00
const SNAP_RANGE_MAXIMIZE: i32 = 22;
const SNAP_RANGE_TOP: i32 = 16;
2024-03-18 00:12:58 -05:00
impl SnappingZone {
pub fn contains(
&self,
point: Point<i32, Local>,
output_geometry: Rectangle<i32, Local>,
2024-03-18 00:12:58 -05:00
) -> bool {
if !output_geometry.contains(point) {
return false;
}
2024-04-09 17:50:52 -05:00
let top_zone_32 = point.y < output_geometry.loc.y + SNAP_RANGE_MAXIMIZE;
let top_zone_56 = point.y < output_geometry.loc.y + SNAP_RANGE_MAXIMIZE + SNAP_RANGE_TOP;
let left_zone = point.x < output_geometry.loc.x + SNAP_RANGE;
let right_zone = point.x > output_geometry.loc.x + output_geometry.size.w - SNAP_RANGE;
let bottom_zone = point.y > output_geometry.loc.y + output_geometry.size.h - SNAP_RANGE;
let left_6th = point.x < output_geometry.loc.x + (output_geometry.size.w / 6);
let right_6th = point.x > output_geometry.loc.x + (output_geometry.size.w * 5 / 6);
let top_4th = point.y < output_geometry.loc.y + (output_geometry.size.h / 4);
let bottom_4th = point.y > output_geometry.loc.y + (output_geometry.size.h * 3 / 4);
2024-03-18 00:12:58 -05:00
match self {
SnappingZone::Maximize => top_zone_32 && !left_6th && !right_6th,
SnappingZone::Top => top_zone_56 && !top_zone_32 && !left_6th && !right_6th,
SnappingZone::TopLeft => (top_zone_56 && left_6th) || (left_zone && top_4th),
SnappingZone::Left => left_zone && !top_4th && !bottom_4th,
SnappingZone::BottomLeft => (bottom_zone && left_6th) || (left_zone && bottom_4th),
SnappingZone::Bottom => bottom_zone && !left_6th && !right_6th,
SnappingZone::BottomRight => (bottom_zone && right_6th) || (right_zone && bottom_4th),
SnappingZone::Right => right_zone && !top_4th && !bottom_4th,
SnappingZone::TopRight => (top_zone_56 && right_6th) || (right_zone && top_4th),
2024-03-18 00:12:58 -05:00
}
}
pub fn overlay_geometry(
&self,
non_exclusive_geometry: Rectangle<i32, Logical>,
2024-04-19 20:38:14 -05:00
gaps: (i32, i32),
2024-03-18 00:12:58 -05:00
) -> Rectangle<i32, Local> {
match self {
SnappingZone::Maximize => non_exclusive_geometry.as_local(),
2024-04-19 20:38:14 -05:00
SnappingZone::Top => TiledCorners::Top.relative_geometry(non_exclusive_geometry, gaps),
SnappingZone::TopLeft => {
2024-04-19 20:38:14 -05:00
TiledCorners::TopLeft.relative_geometry(non_exclusive_geometry, gaps)
}
SnappingZone::Left => {
TiledCorners::Left.relative_geometry(non_exclusive_geometry, gaps)
}
SnappingZone::BottomLeft => {
2024-04-19 20:38:14 -05:00
TiledCorners::BottomLeft.relative_geometry(non_exclusive_geometry, gaps)
}
SnappingZone::Bottom => {
TiledCorners::Bottom.relative_geometry(non_exclusive_geometry, gaps)
}
SnappingZone::BottomRight => {
2024-04-19 20:38:14 -05:00
TiledCorners::BottomRight.relative_geometry(non_exclusive_geometry, gaps)
}
SnappingZone::Right => {
TiledCorners::Right.relative_geometry(non_exclusive_geometry, gaps)
}
SnappingZone::TopRight => {
2024-04-19 20:38:14 -05:00
TiledCorners::TopRight.relative_geometry(non_exclusive_geometry, gaps)
}
2024-03-18 00:12:58 -05:00
}
}
}
2023-07-17 21:11:23 +02:00
pub struct MoveGrab {
window: CosmicMapped,
start_data: GrabStartData,
seat: Seat<State>,
cursor_output: Output,
window_outputs: HashSet<Output>,
previous: ManagedLayer,
release: ReleaseMode,
2025-02-14 22:17:17 +11:00
edge_snap_threshold: f64,
// SAFETY: This is only used on drop which will always be on the main thread
evlh: NotSend<LoopHandle<'static, State>>,
}
impl MoveGrab {
fn update_location(&mut self, state: &mut State, location: Point<f64, Logical>) {
let mut shell = state.common.shell.write();
2024-04-10 15:49:08 +02:00
let Some(current_output) = shell
.outputs()
.find(|output| {
output
.geometry()
.as_logical()
.overlaps_or_touches(Rectangle::new(location.to_i32_floor(), (0, 0).into()))
})
.cloned()
else {
return;
};
if self.cursor_output != current_output {
2024-04-10 15:49:08 +02:00
shell
.workspaces
.active_mut(&self.cursor_output)
.unwrap()
.tiling_layer
2023-10-25 19:40:26 +02:00
.cleanup_drag();
self.cursor_output = current_output.clone();
}
2023-07-24 19:31:31 +02:00
let mut borrow = self
2023-03-06 18:50:59 +01:00
.seat
.user_data()
.get::<SeatMoveGrabState>()
.map(|s| s.lock().unwrap());
2023-07-24 19:31:31 +02:00
if let Some(grab_state) = borrow.as_mut().and_then(|s| s.as_mut()) {
grab_state.location = location;
grab_state.cursor_output = self.cursor_output.clone();
2023-03-06 18:50:59 +01:00
let mut window_geo = self.window.geometry();
window_geo.loc += location.to_i32_round() + grab_state.window_offset;
if matches!(self.previous, ManagedLayer::Floating | ManagedLayer::Sticky) {
let loc = grab_state.window_offset.to_f64() + grab_state.location;
let size = window_geo.size.to_f64();
let output_geom = self.cursor_output.geometry().to_f64().as_logical();
let output_loc = output_geom.loc;
let output_size = output_geom.size;
2025-02-14 22:17:17 +11:00
grab_state.location.x = if (loc.x - output_loc.x).abs() < self.edge_snap_threshold {
output_loc.x - grab_state.window_offset.x as f64
} else if ((loc.x + size.w) - (output_loc.x + output_size.w)).abs()
2025-02-14 22:17:17 +11:00
< self.edge_snap_threshold
{
output_loc.x + output_size.w - grab_state.window_offset.x as f64 - size.w
} else {
grab_state.location.x
};
2025-02-14 22:17:17 +11:00
grab_state.location.y = if (loc.y - output_loc.y).abs() < self.edge_snap_threshold {
output_loc.y - grab_state.window_offset.y as f64
} else if ((loc.y + size.h) - (output_loc.y + output_size.h)).abs()
2025-02-14 22:17:17 +11:00
< self.edge_snap_threshold
{
output_loc.y + output_size.h - grab_state.window_offset.y as f64 - size.h
} else {
grab_state.location.y
};
}
2024-04-10 15:49:08 +02:00
for output in shell.outputs() {
if let Some(overlap) = output.geometry().as_logical().intersection(window_geo) {
if self.window_outputs.insert(output.clone()) {
2023-03-06 18:50:59 +01:00
self.window.output_enter(output, overlap);
2023-07-24 19:31:31 +02:00
if let Some(indicator) =
grab_state.stacking_indicator.as_ref().map(|x| &x.0)
{
indicator.output_enter(output);
2023-07-24 19:31:31 +02:00
}
2023-03-06 18:50:59 +01:00
}
2025-10-16 13:50:32 +02:00
} else if self.window_outputs.remove(output) {
2023-03-06 18:50:59 +01:00
self.window.output_leave(output);
2023-07-24 19:31:31 +02:00
if let Some(indicator) = grab_state.stacking_indicator.as_ref().map(|x| &x.0) {
indicator.output_leave(output);
}
}
}
2025-10-16 16:10:35 +02:00
let indicator_location = shell.stacking_indicator(&current_output, self.previous);
if indicator_location.is_some() != grab_state.stacking_indicator.is_some() {
grab_state.stacking_indicator = indicator_location.map(|geo| {
let size = geo.size.as_logical();
let element = StackHover::new(
state.common.event_loop_handle.clone(),
size,
state.common.theme.clone(),
);
for output in &self.window_outputs {
element.output_enter(output);
}
(element, geo.loc.as_logical())
});
2023-03-06 18:50:59 +01:00
}
2024-03-18 00:12:58 -05:00
// Check for overlapping with zones
if grab_state.previous == ManagedLayer::Floating {
let output_geometry = current_output.geometry().to_local(&current_output);
grab_state.snapping_zone = [
SnappingZone::Maximize,
SnappingZone::Top,
SnappingZone::TopLeft,
2024-03-18 00:12:58 -05:00
SnappingZone::Left,
SnappingZone::BottomLeft,
2024-03-18 00:12:58 -05:00
SnappingZone::Bottom,
SnappingZone::BottomRight,
SnappingZone::Right,
SnappingZone::TopRight,
2024-03-18 00:12:58 -05:00
]
.iter()
.find(|&x| {
x.contains(
location
.as_global()
.to_local(&current_output)
.to_i32_floor(),
output_geometry,
2024-03-18 00:12:58 -05:00
)
})
.cloned();
}
2023-03-06 18:50:59 +01:00
}
drop(borrow);
}
}
impl PointerGrab<State> for MoveGrab {
fn motion(
&mut self,
state: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
2024-06-18 19:23:16 -07:00
_focus: Option<(PointerFocusTarget, Point<f64, Logical>)>,
2026-08-11 18:18:02 +02:00
event: &PointerMotionEvent,
) {
self.update_location(state, event.location);
2023-03-06 18:50:59 +01:00
// While the grab is active, no client has pointer focus
handle.motion(state, None, event);
if !self.window.alive() {
handle.unset_grab(self, state, event.serial, event.time, true);
}
}
fn relative_motion(
&mut self,
state: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
2024-06-18 19:23:16 -07:00
_focus: Option<(PointerFocusTarget, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
// While the grab is active, no client has pointer focus
handle.relative_motion(state, None, event);
}
fn button(
&mut self,
state: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
2026-08-11 18:18:02 +02:00
event: &PointerButtonEvent,
) {
handle.button(state, event);
match self.release {
ReleaseMode::NoMouseButtons => {
if handle.current_pressed().is_empty() {
handle.unset_grab(self, state, event.serial, event.time, true);
}
}
ReleaseMode::Click => {
if event.state == ButtonState::Pressed {
handle.unset_grab(self, state, event.serial, event.time, true);
}
}
}
}
fn axis(
&mut self,
state: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
2026-08-11 18:18:02 +02:00
details: PointerAxisFrame,
) {
handle.axis(state, details);
}
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
handle.frame(data)
}
fn gesture_swipe_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeBeginEvent,
) {
handle.gesture_swipe_begin(data, event)
}
fn gesture_swipe_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeUpdateEvent,
) {
handle.gesture_swipe_update(data, event)
}
fn gesture_swipe_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureSwipeEndEvent,
) {
handle.gesture_swipe_end(data, event)
}
fn gesture_pinch_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchBeginEvent,
) {
handle.gesture_pinch_begin(data, event)
}
fn gesture_pinch_update(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchUpdateEvent,
) {
handle.gesture_pinch_update(data, event)
}
fn gesture_pinch_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GesturePinchEndEvent,
) {
handle.gesture_pinch_end(data, event)
}
fn gesture_hold_begin(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldBeginEvent,
) {
handle.gesture_hold_begin(data, event)
}
fn gesture_hold_end(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &GestureHoldEndEvent,
) {
handle.gesture_hold_end(data, event)
}
fn start_data(&self) -> &PointerGrabStartData<State> {
match &self.start_data {
GrabStartData::Pointer(start_data) => start_data,
_ => unreachable!(),
}
}
fn unset(&mut self, _data: &mut State) {}
}
impl TouchGrab<State> for MoveGrab {
fn down(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
2024-06-18 19:23:16 -07:00
_focus: Option<(PointerFocusTarget, Point<f64, Logical>)>,
2026-08-11 18:18:02 +02:00
event: &TouchDownEvent,
) {
2026-06-29 17:20:02 +02:00
handle.down(data, None, event)
}
fn up(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
2026-08-11 18:18:02 +02:00
event: &TouchUpEvent,
) {
if event.slot == <Self as TouchGrab<State>>::start_data(self).slot {
handle.unset_grab(self, data);
}
2026-06-29 17:20:02 +02:00
handle.up(data, event);
}
fn motion(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
2024-06-18 19:23:16 -07:00
_focus: Option<(PointerFocusTarget, Point<f64, Logical>)>,
2026-08-11 18:18:02 +02:00
event: &TouchMotionEvent,
) {
if event.slot == <Self as TouchGrab<State>>::start_data(self).slot {
self.update_location(data, event.location);
}
2026-06-29 17:20:02 +02:00
handle.motion(data, None, event);
}
2026-06-29 17:20:02 +02:00
fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>) {
handle.frame(data)
}
2026-06-29 17:20:02 +02:00
fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>) {
handle.unset_grab(self, data);
}
fn shape(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
2026-08-11 18:18:02 +02:00
event: &ShapeEvent,
) {
2026-06-29 17:20:02 +02:00
handle.shape(data, event)
}
fn orientation(
&mut self,
data: &mut State,
handle: &mut TouchInnerHandle<'_, State>,
2026-08-11 18:18:02 +02:00
event: &OrientationEvent,
) {
2026-06-29 17:20:02 +02:00
handle.orientation(data, event)
}
fn start_data(&self) -> &TouchGrabStartData<State> {
match &self.start_data {
GrabStartData::Touch(start_data) => start_data,
_ => unreachable!(),
}
}
fn unset(&mut self, _data: &mut State) {}
}
2026-08-11 18:18:02 +02:00
impl TabletToolGrab<State> for MoveGrab {
fn start_data(&self) -> &TabletGrabStartData<State> {
match &self.start_data {
GrabStartData::TabletTool { data, .. } => data,
_ => unreachable!(),
}
}
fn proximity_out(
&mut self,
data: &mut State,
handle: &mut TabletToolInnerHandle<'_, State>,
event: &ProximityOutEvent,
) {
handle.proximity_out(data, event);
handle.unset_grab(self, data, event.serial, event.time, false);
2026-08-11 18:18:02 +02:00
}
fn motion(
&mut self,
data: &mut State,
handle: &mut TabletToolInnerHandle<'_, State>,
_focus: Option<(<State as TabletSeatHandler>::ToolFocus, Point<f64, Logical>)>,
event: &TabletMotionEvent,
) {
handle.motion(data, None, event);
self.update_location(data, event.location);
if !self.window.alive() {
handle.unset_grab(self, data, event.serial, event.time, true);
}
}
fn down(
&mut self,
data: &mut State,
handle: &mut TabletToolInnerHandle<'_, State>,
event: &TabletDownEvent,
) {
handle.down(data, event)
}
fn up(
&mut self,
data: &mut State,
handle: &mut TabletToolInnerHandle<'_, State>,
event: &TabletUpEvent,
) {
handle.up(data, event);
handle.unset_grab(self, data, event.serial, event.time, false);
2026-08-11 18:18:02 +02:00
}
fn button(
&mut self,
data: &mut State,
handle: &mut TabletToolInnerHandle<'_, State>,
event: &TabletButtonEvent,
) {
handle.button(data, event)
}
fn axis(
&mut self,
data: &mut State,
handle: &mut TabletToolInnerHandle<'_, State>,
frame: TabletAxisFrame,
) {
handle.axis(data, frame)
}
fn frame(
&mut self,
data: &mut State,
handle: &mut TabletToolInnerHandle<'_, State>,
time: InputTime,
) {
handle.frame(data, time)
}
fn proximity_in(
&mut self,
data: &mut State,
handle: &mut TabletToolInnerHandle<'_, State>,
focus: Option<(<State as TabletSeatHandler>::ToolFocus, Point<f64, Logical>)>,
event: &ProximityInEvent,
) {
handle.proximity_in(data, focus, event);
}
fn unset(&mut self, _data: &mut State) {}
}
2023-07-17 21:11:23 +02:00
impl MoveGrab {
pub fn new(
start_data: GrabStartData,
window: CosmicMapped,
seat: &Seat<State>,
initial_window_location: Point<i32, Global>,
cursor_output: Output,
indicator_thickness: u8,
2025-02-14 22:17:17 +11:00
edge_snap_threshold: f64,
previous_layer: ManagedLayer,
release: ReleaseMode,
evlh: LoopHandle<'static, State>,
2023-07-17 21:11:23 +02:00
) -> MoveGrab {
2026-03-20 13:40:06 +01:00
// false-positive: `Output`s hash is based on it's inner ptr
#[allow(clippy::mutable_key_type)]
2023-03-06 18:50:59 +01:00
let mut outputs = HashSet::new();
outputs.insert(cursor_output.clone());
window.output_enter(&cursor_output, window.geometry()); // not accurate but...
2023-11-08 22:59:46 +01:00
window.moved_since_mapped.store(true, Ordering::SeqCst);
2023-03-06 18:50:59 +01:00
let grab_state = MoveGrabState {
window: window.clone(),
window_offset: (initial_window_location
- start_data.location().as_global().to_i32_round())
.as_logical(),
indicator_thickness,
2023-07-18 12:21:31 +02:00
start: Instant::now(),
2023-07-24 19:31:31 +02:00
stacking_indicator: None,
2024-03-18 00:12:58 -05:00
snapping_zone: None,
2025-10-16 13:50:32 +02:00
previous: previous_layer,
location: start_data.location(),
cursor_output: cursor_output.clone(),
};
*seat
.user_data()
.get::<SeatMoveGrabState>()
.unwrap()
.lock()
.unwrap() = Some(grab_state);
2023-07-18 12:21:16 +02:00
{
let cursor_state = seat.user_data().get::<CursorState>().unwrap();
cursor_state.lock().unwrap().set_shape(CursorIcon::Grabbing);
2023-07-18 12:21:16 +02:00
}
2023-07-17 21:11:23 +02:00
MoveGrab {
window,
start_data,
seat: seat.clone(),
cursor_output,
window_outputs: outputs,
previous: previous_layer,
release,
2025-02-14 22:17:17 +11:00
edge_snap_threshold,
evlh: NotSend(evlh),
}
}
2023-07-17 21:11:23 +02:00
pub fn is_tiling_grab(&self) -> bool {
self.previous == ManagedLayer::Tiling
2023-07-17 21:11:23 +02:00
}
2024-04-10 15:49:08 +02:00
2026-08-11 18:18:02 +02:00
pub fn grab_type(&self) -> GrabType {
self.start_data.type_()
}
pub fn tool(&self) -> Option<&TabletToolDescriptor> {
if let GrabStartData::TabletTool { tool, .. } = &self.start_data {
Some(tool)
} else {
None
2024-04-10 15:49:08 +02:00
}
}
}
2023-07-17 21:11:23 +02:00
impl Drop for MoveGrab {
fn drop(&mut self) {
// No more buttons are pressed, release the grab.
let output = self.cursor_output.clone();
let seat = self.seat.clone();
2026-03-20 13:40:06 +01:00
// false-positive: `Output`s hash is based on it's inner ptr
#[allow(clippy::mutable_key_type)]
let window_outputs = self.window_outputs.drain().collect::<HashSet<_>>();
2025-10-16 13:50:32 +02:00
let previous = self.previous;
let window = self.window.clone();
let is_touch_grab = matches!(self.start_data, GrabStartData::Touch(_));
let cursor_output = self.cursor_output.clone();
let _ = self.evlh.0.insert_idle(move |state| {
let position: Option<(CosmicMapped, Point<i32, Global>)> = if let Some(grab_state) =
seat.user_data()
.get::<SeatMoveGrabState>()
.and_then(|s| s.lock().unwrap().take())
{
if grab_state.window.alive() {
let window_location =
(grab_state.location.to_i32_round() + grab_state.window_offset).as_global();
let mut shell = state.common.shell.write();
let workspace_handle = shell.active_space(&output).unwrap().handle;
for old_output in window_outputs.iter().filter(|o| *o != &output) {
grab_state.window.output_leave(old_output);
}
2024-04-10 15:49:08 +02:00
for (window, _) in grab_state.window.windows() {
2024-04-10 15:49:08 +02:00
toplevel_enter_output(&window, &output);
if previous != ManagedLayer::Sticky {
2024-04-10 15:49:08 +02:00
toplevel_enter_workspace(&window, &workspace_handle);
}
}
match previous {
ManagedLayer::Sticky => {
grab_state.window.set_geometry(Rectangle::new(
window_location,
grab_state.window.geometry().size.as_global(),
));
2024-04-10 15:49:08 +02:00
let set = shell.workspaces.sets.get_mut(&output).unwrap();
let (window, location) = set
.sticky_layer
.drop_window(grab_state.window, window_location.to_local(&output));
Some((window, location.to_global(&output)))
}
ManagedLayer::Tiling
if shell.active_space(&output).unwrap().tiling_enabled =>
{
2024-04-10 15:49:08 +02:00
let (window, location) = shell
.active_space_mut(&output)
.unwrap()
.tiling_layer
.drop_window(grab_state.window);
Some((window, location.to_global(&output)))
}
_ => {
grab_state.window.set_geometry(Rectangle::new(
window_location,
grab_state.window.geometry().size.as_global(),
));
2024-04-10 15:49:08 +02:00
let theme = shell.theme.clone();
let workspace = shell.active_space_mut(&output).unwrap();
let (window, location) = workspace.floating_layer.drop_window(
grab_state.window,
window_location.to_local(&workspace.output),
);
2024-03-18 00:12:58 -05:00
2026-02-23 16:25:06 +01:00
if matches!(previous, ManagedLayer::Floating)
&& let Some(sz) = grab_state.snapping_zone
{
// `last_geometry` was set to the pre-drag geometry(in FloatingLayout::unmap).
// Snapshot it here and restore it after so "restore-to-floating" goes back to where the user had the window.
let pre_drag_geometry = *window.last_geometry.lock().unwrap();
2026-02-23 16:25:06 +01:00
if sz == SnappingZone::Maximize {
shell.maximize_toggle(
&window,
&seat,
&state.common.event_loop_handle,
);
if let Some(geo) = pre_drag_geometry
&& let Some(state) =
window.maximized_state.lock().unwrap().as_mut()
{
state.original_geometry = geo;
}
2026-02-23 16:25:06 +01:00
} else {
let directions = match sz {
SnappingZone::Maximize => vec![],
SnappingZone::Top => vec![Direction::Up],
SnappingZone::TopLeft => {
vec![Direction::Up, Direction::Left]
}
SnappingZone::Left => vec![Direction::Left],
SnappingZone::BottomLeft => {
vec![Direction::Down, Direction::Left]
}
SnappingZone::Bottom => vec![Direction::Down],
SnappingZone::BottomRight => {
vec![Direction::Down, Direction::Right]
}
SnappingZone::Right => vec![Direction::Right],
SnappingZone::TopRight => {
vec![Direction::Up, Direction::Right]
}
};
for direction in directions {
workspace.floating_layer.move_element(
direction,
&seat,
2026-02-23 16:25:06 +01:00
ManagedLayer::Floating,
&theme,
&window,
);
2024-03-18 00:12:58 -05:00
}
if let Some(geo) = pre_drag_geometry {
*window.last_geometry.lock().unwrap() = Some(geo);
}
2024-03-18 00:12:58 -05:00
}
}
Some((window, location.to_global(&output)))
}
}
} else {
None
2023-07-17 21:11:23 +02:00
}
2023-04-19 11:42:46 +02:00
} else {
None
};
2023-07-18 12:21:16 +02:00
let mut shell = state.common.shell.write();
shell
.workspaces
.active_mut(&cursor_output)
.unwrap()
.tiling_layer
.cleanup_drag();
shell.set_overview_mode(None, state.common.event_loop_handle.clone());
drop(shell);
{
let cursor_state = seat.user_data().get::<CursorState>().unwrap();
2024-09-09 16:21:27 +02:00
cursor_state.lock().unwrap().unset_shape();
}
2023-07-18 12:21:16 +02:00
if let Some((mapped, position)) = position {
let serial = SERIAL_COUNTER.next_serial();
if !is_touch_grab {
let pointer = seat.get_pointer().unwrap();
let current_location = pointer.current_location();
2024-11-14 13:19:47 -08:00
if let Some((target, offset)) = mapped.focus_under(
current_location - position.as_logical().to_f64(),
WindowSurfaceType::ALL,
&seat,
2024-11-14 13:19:47 -08:00
) {
pointer.motion(
state,
Some((
target,
2024-04-03 16:02:27 +02:00
position.as_logical().to_f64() - window.geometry().loc.to_f64()
+ offset,
)),
2026-08-11 18:18:02 +02:00
&PointerMotionEvent {
location: pointer.current_location(),
serial,
2026-08-19 12:40:05 +02:00
time: InputTime::now(),
},
);
}
}
2024-04-05 13:53:35 +02:00
Shell::set_focus(
state,
Some(&KeyboardFocusTarget::from(mapped)),
&seat,
Some(serial),
false,
)
}
});
}
}