From 0ae6e3805e9913bfb60efa203d51e6735b757c8a Mon Sep 17 00:00:00 2001 From: Victoria Brekenfeld Date: Tue, 28 Dec 2021 16:23:12 +0100 Subject: [PATCH] shell: Add resize and move grabs --- Cargo.lock | 5 +- src/input/mod.rs | 30 +++- src/shell/grabs.rs | 435 +++++++++++++++++++++++++++++++++++++++++++++ src/shell/mod.rs | 163 ++++++++++++++++- 4 files changed, 620 insertions(+), 13 deletions(-) create mode 100644 src/shell/grabs.rs diff --git a/Cargo.lock b/Cargo.lock index ba98972c..2d9a09de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,9 +81,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "calloop" @@ -135,6 +135,7 @@ name = "cosmic-comp" version = "0.1.0" dependencies = [ "anyhow", + "bitflags", "slog", "slog-async", "slog-scope", diff --git a/src/input/mod.rs b/src/input/mod.rs index 7e65db3c..468d90a9 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -207,11 +207,12 @@ impl State { .to_f64() .contains(position) }) - .unwrap_or(¤t_output); - if output != ¤t_output { - set_active_output(seat, output); + .cloned() + .unwrap_or(current_output.clone()); + if output != current_output { + set_active_output(seat, &output); } - let output_geometry = self.spaces.output_geometry(output); + let output_geometry = self.spaces.output_geometry(&output); position.x = 0.0f64 .max(position.x) @@ -221,11 +222,13 @@ impl State { .min((output_geometry.loc.y + output_geometry.size.h) as f64); let serial = SERIAL_COUNTER.next_serial(); - let space = self.spaces.active_space(&output); - let under = State::surface_under(position, output, space); + let space = self.spaces.active_space_mut(&output); + let under = State::surface_under(position, &output, space); + handle_window_movement(under.as_ref().map(|(s, _)| s), space); seat.get_pointer() .unwrap() .motion(position, under, serial, event.time()); + break; } } @@ -239,12 +242,13 @@ impl State { let devices = userdata.get::().unwrap(); if devices.has_device(&device) { let output = active_output(seat, &self); - let space = self.spaces.active_space(&output); let geometry = self.spaces.output_geometry(&output); + let space = self.spaces.active_space_mut(&output); let position = geometry.loc.to_f64() + event.position_transformed(geometry.size); let serial = SERIAL_COUNTER.next_serial(); let under = State::surface_under(position, &output, space); + handle_window_movement(under.as_ref().map(|(s, _)| s), space); seat.get_pointer() .unwrap() .motion(position, under, serial, event.time()); @@ -420,3 +424,15 @@ impl State { } } } + +pub fn handle_window_movement(surface: Option<&WlSurface>, space: &mut Space) { + if let Some(surface) = surface { + if let Some(window) = space.window_for_surface(&surface).cloned() { + if let Some(new_position) = + crate::shell::grabs::MoveSurfaceGrab::apply_move_state(&window) + { + space.map_window(&window, new_position); + } + } + } +} diff --git a/src/shell/grabs.rs b/src/shell/grabs.rs new file mode 100644 index 00000000..de12b1e2 --- /dev/null +++ b/src/shell/grabs.rs @@ -0,0 +1,435 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use smithay::{ + desktop::{Kind, Window}, + reexports::{ + wayland_protocols::xdg_shell::server::xdg_toplevel, + wayland_server::protocol::{wl_pointer::ButtonState, wl_surface}, + }, + utils::{Logical, Point, Rectangle, Size}, + wayland::{ + compositor::with_states, + seat::{AxisFrame, GrabStartData, PointerGrab, PointerInnerHandle}, + shell::xdg::{SurfaceCachedState, ToplevelConfigure, XdgToplevelSurfaceRoleAttributes}, + Serial, + }, +}; +use std::{cell::RefCell, sync::Mutex}; + +#[derive(Debug, Default)] +struct MoveData { + new_location: Point, +} + +pub struct MoveSurfaceGrab { + start_data: GrabStartData, + window: Window, + initial_window_location: Point, +} + +impl PointerGrab for MoveSurfaceGrab { + fn motion( + &mut self, + _handle: &mut PointerInnerHandle<'_>, + location: Point, + _focus: Option<(wl_surface::WlSurface, Point)>, + _serial: Serial, + _time: u32, + ) { + let delta = location - self.start_data.location; + let new_location = self.initial_window_location.to_f64() + delta; + self.window + .user_data() + .insert_if_missing(|| RefCell::>::new(None)); + let data = self + .window + .user_data() + .get::>>() + .unwrap(); + *data.borrow_mut() = Some(MoveData { + new_location: new_location.to_i32_round(), + }); + } + + fn button( + &mut self, + handle: &mut PointerInnerHandle<'_>, + button: u32, + state: ButtonState, + serial: Serial, + time: u32, + ) { + handle.button(button, state, serial, time); + if handle.current_pressed().is_empty() { + // No more buttons are pressed, release the grab. + handle.unset_grab(serial, time); + } + } + + fn axis(&mut self, handle: &mut PointerInnerHandle<'_>, details: AxisFrame) { + handle.axis(details) + } + + fn start_data(&self) -> &GrabStartData { + &self.start_data + } +} + +impl MoveSurfaceGrab { + pub fn new( + start_data: GrabStartData, + window: Window, + initial_window_location: Point, + ) -> MoveSurfaceGrab { + MoveSurfaceGrab { + start_data, + window, + initial_window_location, + } + } + + pub fn apply_move_state(window: &Window) -> Option> { + window + .user_data() + .get::>>() + .and_then(|opt| { + opt.borrow_mut() + .take() + .map(|move_data| move_data.new_location) + }) + } +} + +bitflags::bitflags! { + struct ResizeEdge: u32 { + const NONE = 0; + const TOP = 1; + const BOTTOM = 2; + const LEFT = 4; + const TOP_LEFT = 5; + const BOTTOM_LEFT = 6; + const RIGHT = 8; + const TOP_RIGHT = 9; + const BOTTOM_RIGHT = 10; + } +} + +impl From for ResizeEdge { + #[inline] + fn from(x: xdg_toplevel::ResizeEdge) -> Self { + Self::from_bits(x.to_raw()).unwrap() + } +} + +impl From for xdg_toplevel::ResizeEdge { + #[inline] + fn from(x: ResizeEdge) -> Self { + Self::from_raw(x.bits()).unwrap() + } +} + +/// Information about the resize operation. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +struct ResizeData { + /// The edges the surface is being resized with. + edges: ResizeEdge, + /// The initial window location. + initial_window_location: Point, + /// The initial window size (geometry width and height). + initial_window_size: Size, +} + +/// State of the resize operation. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum ResizeState { + /// The surface is not being resized. + NotResizing, + /// The surface is currently being resized. + Resizing(ResizeData), + /// The resize has finished, and the surface needs to ack the final configure. + WaitingForFinalAck(ResizeData, Serial), + /// The resize has finished, and the surface needs to commit its final state. + WaitingForCommit(ResizeData), +} + +impl Default for ResizeState { + fn default() -> Self { + ResizeState::NotResizing + } +} + +pub struct ResizeSurfaceGrab { + start_data: GrabStartData, + window: Window, + edges: ResizeEdge, + initial_window_size: Size, + last_window_size: Size, +} + +impl PointerGrab for ResizeSurfaceGrab { + fn motion( + &mut self, + handle: &mut PointerInnerHandle<'_>, + location: Point, + _focus: Option<(wl_surface::WlSurface, Point)>, + serial: Serial, + time: u32, + ) { + // It is impossible to get `min_size` and `max_size` of dead toplevel, so we return early. + if !self.window.toplevel().alive() | self.window.toplevel().get_surface().is_none() { + handle.unset_grab(serial, time); + return; + } + + let (mut dx, mut dy) = (location - self.start_data.location).into(); + + let mut new_window_width = self.initial_window_size.w; + let mut new_window_height = self.initial_window_size.h; + + let left_right = ResizeEdge::LEFT | ResizeEdge::RIGHT; + let top_bottom = ResizeEdge::TOP | ResizeEdge::BOTTOM; + + if self.edges.intersects(left_right) { + if self.edges.intersects(ResizeEdge::LEFT) { + dx = -dx; + } + + new_window_width = (self.initial_window_size.w as f64 + dx) as i32; + } + + if self.edges.intersects(top_bottom) { + if self.edges.intersects(ResizeEdge::TOP) { + dy = -dy; + } + + new_window_height = (self.initial_window_size.h as f64 + dy) as i32; + } + + let (min_size, max_size) = + with_states(self.window.toplevel().get_surface().unwrap(), |states| { + let data = states.cached_state.current::(); + (data.min_size, data.max_size) + }) + .unwrap(); + + let min_width = min_size.w.max(1); + let min_height = min_size.h.max(1); + let max_width = if max_size.w == 0 { + i32::max_value() + } else { + max_size.w + }; + let max_height = if max_size.h == 0 { + i32::max_value() + } else { + max_size.h + }; + + new_window_width = new_window_width.max(min_width).min(max_width); + new_window_height = new_window_height.max(min_height).min(max_height); + + self.last_window_size = (new_window_width, new_window_height).into(); + + match &self.window.toplevel() { + Kind::Xdg(xdg) => { + let ret = xdg.with_pending_state(|state| { + state.states.set(xdg_toplevel::State::Resizing); + state.size = Some(self.last_window_size); + }); + if ret.is_ok() { + xdg.send_configure(); + } + } + } + } + + fn button( + &mut self, + handle: &mut PointerInnerHandle<'_>, + button: u32, + state: ButtonState, + serial: Serial, + time: u32, + ) { + handle.button(button, state, serial, time); + if handle.current_pressed().is_empty() { + // No more buttons are pressed, release the grab. + handle.unset_grab(serial, time); + + // If toplevel is dead, we can't resize it, so we return early. + if !self.window.toplevel().alive() | self.window.toplevel().get_surface().is_none() { + return; + } + + #[allow(irrefutable_let_patterns)] + if let Kind::Xdg(xdg) = &self.window.toplevel() { + let ret = xdg.with_pending_state(|state| { + state.states.unset(xdg_toplevel::State::Resizing); + state.size = Some(self.last_window_size); + }); + if ret.is_ok() { + xdg.send_configure(); + } + } + + let mut resize_state = self + .window + .user_data() + .get::>() + .unwrap() + .borrow_mut(); + if let ResizeState::Resizing(resize_data) = *resize_state { + *resize_state = ResizeState::WaitingForFinalAck(resize_data, serial); + } else { + panic!("invalid resize state: {:?}", resize_state); + } + } + } + + fn axis(&mut self, handle: &mut PointerInnerHandle<'_>, details: AxisFrame) { + handle.axis(details) + } + + fn start_data(&self) -> &GrabStartData { + &self.start_data + } +} + +impl ResizeSurfaceGrab { + pub fn new( + start_data: GrabStartData, + window: Window, + edges: xdg_toplevel::ResizeEdge, + initial_window_geometry: Rectangle, + ) -> ResizeSurfaceGrab { + let (initial_window_location, initial_window_size) = + (initial_window_geometry.loc, initial_window_geometry.size); + let resize_state = ResizeState::Resizing(ResizeData { + edges: edges.into(), + initial_window_location, + initial_window_size, + }); + + window + .user_data() + .insert_if_missing(|| RefCell::new(ResizeState::default())); + *window + .user_data() + .get::>() + .unwrap() + .borrow_mut() = resize_state; + + ResizeSurfaceGrab { + start_data, + window, + edges: edges.into(), + initial_window_size, + last_window_size: initial_window_size, + } + } + + pub fn ack_configure(window: &Window, configure: ToplevelConfigure) { + let surface = if let Some(surface) = window.toplevel().get_surface() { + surface + } else { + return; + }; + + let waiting_for_serial = + if let Some(data) = window.user_data().get::>() { + if let ResizeState::WaitingForFinalAck(_, serial) = *data.borrow() { + Some(serial) + } else { + None + } + } else { + None + }; + + if let Some(serial) = waiting_for_serial { + // When the resize grab is released the surface + // resize state will be set to WaitingForFinalAck + // and the client will receive a configure request + // without the resize state to inform the client + // resizing has finished. Here we will wait for + // the client to acknowledge the end of the + // resizing. To check if the surface was resizing + // before sending the configure we need to use + // the current state as the received acknowledge + // will no longer have the resize state set + let is_resizing = with_states(&surface, |states| { + states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap() + .current + .states + .contains(xdg_toplevel::State::Resizing) + }) + .unwrap(); + + if configure.serial >= serial && is_resizing { + let mut resize_state = window + .user_data() + .get::>() + .unwrap() + .borrow_mut(); + if let ResizeState::WaitingForFinalAck(resize_data, _) = *resize_state { + *resize_state = ResizeState::WaitingForCommit(resize_data); + } else { + unreachable!() + } + } + } + } + + pub fn apply_resize_state( + window: &Window, + geometry: Rectangle, + ) -> Option> { + let mut new_location = None; + + if let Some(resize_state) = window.user_data().get::>() { + let mut resize_state = resize_state.borrow_mut(); + + // If the window is being resized by top or left, its location must be adjusted + // accordingly. + match *resize_state { + ResizeState::Resizing(resize_data) + | ResizeState::WaitingForFinalAck(resize_data, _) + | ResizeState::WaitingForCommit(resize_data) => { + let ResizeData { + edges, + initial_window_location, + initial_window_size, + } = resize_data; + + if edges.intersects(ResizeEdge::TOP_LEFT) { + let mut location = geometry.loc; + + if edges.intersects(ResizeEdge::LEFT) { + location.x = initial_window_location.x + + (initial_window_size.w - geometry.size.w); + } + if edges.intersects(ResizeEdge::TOP) { + location.y = initial_window_location.y + + (initial_window_size.h - geometry.size.h); + } + + new_location = Some(location); + } + } + ResizeState::NotResizing => (), + } + + // Finish resizing. + if let ResizeState::WaitingForCommit(_) = *resize_state { + *resize_state = ResizeState::NotResizing; + } + } + + new_location + } +} diff --git a/src/shell/mod.rs b/src/shell/mod.rs index 1371fe4d..a556e9b8 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -11,6 +11,7 @@ use smithay::{ wayland::{ compositor::{compositor_init, with_states}, output::Output, + seat::{GrabStartData, PointerHandle, Seat}, shell::{ wlr_layer::{ wlr_layer_shell_init, LayerShellRequest, LayerShellState, LayerSurfaceAttributes, @@ -20,10 +21,12 @@ use smithay::{ XdgPopupSurfaceRoleAttributes, XdgRequest, XdgToplevelSurfaceRoleAttributes, }, }, + Serial, }, }; use std::sync::{Arc, Mutex}; +pub mod grabs; pub mod workspaces; pub struct ShellStates { @@ -87,11 +90,105 @@ pub fn init_shell(display: &mut Display) -> ShellStates { surface.send_repositioned(token); } } - /* - XdgRequest::AckConfigure { surface, configure: Configure::Toplevel(configure) } => { + XdgRequest::Move { + surface, + seat, + serial, + } => { + let seat = Seat::from_resource(&seat).unwrap(); + if let Some((pointer, start_data)) = + check_grab_preconditions(&seat, surface.get_surface(), serial) + { + let space = state + .spaces + .space_for_surface(surface.get_surface().unwrap()) + .unwrap(); + let window = space + .window_for_surface(surface.get_surface().unwrap()) + .unwrap() + .clone(); + let mut initial_window_location = + space.window_geometry(&window).unwrap().loc; - }, - */ + // If surface is maximized then unmaximize it + if let Some(current_state) = surface.current_state() { + if current_state + .states + .contains(xdg_toplevel::State::Maximized) + { + let fs_changed = surface.with_pending_state(|state| { + state.states.unset(xdg_toplevel::State::Maximized); + state.size = None; + }); + + if fs_changed.is_ok() { + surface.send_configure(); + + // NOTE: In real compositor mouse location should be mapped to a new window size + // For example, you could: + // 1) transform mouse pointer position from compositor space to window space (location relative) + // 2) divide the x coordinate by width of the window to get the percentage + // - 0.0 would be on the far left of the window + // - 0.5 would be in middle of the window + // - 1.0 would be on the far right of the window + // 3) multiply the percentage by new window width + // 4) by doing that, drag will look a lot more natural + // + // but for anvil needs setting location to pointer location is fine + let pos = pointer.current_location(); + initial_window_location = (pos.x as i32, pos.y as i32).into(); + } + } + } + + let grab = grabs::MoveSurfaceGrab::new( + start_data, + window, + initial_window_location, + ); + + pointer.set_grab(grab, serial); + } + } + + XdgRequest::Resize { + surface, + seat, + serial, + edges, + } => { + let seat = Seat::from_resource(&seat).unwrap(); + if let Some((pointer, start_data)) = + check_grab_preconditions(&seat, surface.get_surface(), serial) + { + let space = state + .spaces + .space_for_surface(surface.get_surface().unwrap()) + .unwrap(); + let window = space + .window_for_surface(surface.get_surface().unwrap()) + .unwrap() + .clone(); + let geometry = space.window_geometry(&window).unwrap(); + + let grab = + grabs::ResizeSurfaceGrab::new(start_data, window, edges, geometry); + + pointer.set_grab(grab, serial); + } + } + XdgRequest::AckConfigure { + surface, + configure: Configure::Toplevel(configure), + } => { + let window = state + .spaces + .space_for_surface(&surface) + .unwrap() + .window_for_surface(&surface) + .unwrap(); + grabs::ResizeSurfaceGrab::ack_configure(window, configure) + } XdgRequest::Maximize { surface } => { let seat = &state.last_active_seat; let output = active_output(seat, &state); @@ -161,6 +258,43 @@ pub fn init_shell(display: &mut Display) -> ShellStates { } } +fn check_grab_preconditions( + seat: &Seat, + surface: Option<&WlSurface>, + serial: Serial, +) -> Option<(PointerHandle, GrabStartData)> { + let surface = if let Some(surface) = surface { + surface + } else { + return None; + }; + + // TODO: touch resize. + let pointer = seat.get_pointer().unwrap(); + + // Check that this surface has a click grab. + if !pointer.has_grab(serial) { + return None; + } + + let start_data = pointer.grab_start_data().unwrap(); + + // If the focus was for a different surface, ignore the request. + if start_data.focus.is_none() + || !start_data + .focus + .as_ref() + .unwrap() + .0 + .as_ref() + .same_client_as(surface.as_ref()) + { + return None; + } + + Some((pointer, start_data)) +} + fn commit(surface: &WlSurface, state: &mut State) { if let Some(toplevel) = state.pending_toplevels.iter().find(|toplevel| { toplevel @@ -216,6 +350,27 @@ fn commit(surface: &WlSurface, state: &mut State) { return; } + if let Some((space, window)) = state + .spaces + .space_for_surface_mut(surface) + .and_then(|space| { + space + .window_for_surface(surface) + .cloned() + .map(|window| (space, window)) + }) + { + let new_location = grabs::ResizeSurfaceGrab::apply_resize_state( + &window, + space.window_geometry(&window).unwrap(), + ); + if let Some(location) = new_location { + space.map_window(&window, location); + } + + return; + } + if let Some(popup) = state.shell.popups.find_popup(surface) { let PopupKind::Xdg(ref popup) = popup; let initial_configure_sent = with_states(surface, |states| {