diff --git a/compositor/src/grabs/move_grab.rs b/compositor/src/grabs/move_grab.rs index d3abf6f..c1f08c5 100644 --- a/compositor/src/grabs/move_grab.rs +++ b/compositor/src/grabs/move_grab.rs @@ -16,6 +16,7 @@ pub struct MoveSurfaceGrab { pub start_data: GrabStartData, pub window: Window, pub initial_location: Point, + pub finished: bool, } impl PointerGrab for MoveSurfaceGrab { @@ -28,8 +29,11 @@ impl PointerGrab for MoveSurfaceGrab { ) { handle.motion(data, None, event); let location = self.initial_location.to_f64() + (event.location - self.start_data.location); - data.space - .map_element(self.window.clone(), location.to_i32_round(), true); + let location = location.to_i32_round(); + let size = self.window.geometry().size; + + data.set_floating_window_geometry(&self.window, location, size); + data.space.map_element(self.window.clone(), location, true); } fn relative_motion( @@ -50,6 +54,7 @@ impl PointerGrab for MoveSurfaceGrab { ) { handle.button(data, event); if handle.current_pressed().is_empty() { + self.finished = true; handle.unset_grab(self, data, event.serial, event.time, true); } } @@ -143,5 +148,14 @@ impl PointerGrab for MoveSurfaceGrab { &self.start_data } - fn unset(&mut self, _data: &mut Ferese) {} + fn unset(&mut self, data: &mut Ferese) { + if self.finished { + return; + } + + let size = self.window.geometry().size; + data.set_floating_window_geometry(&self.window, self.initial_location, size); + data.space + .map_element(self.window.clone(), self.initial_location, true); + } } diff --git a/compositor/src/grabs/resize_grab.rs b/compositor/src/grabs/resize_grab.rs index 2617764..528053c 100644 --- a/compositor/src/grabs/resize_grab.rs +++ b/compositor/src/grabs/resize_grab.rs @@ -71,6 +71,7 @@ pub struct ResizeSurfaceGrab { edges: ResizeEdge, initial_rect: Rectangle, last_size: Size, + finished: bool, } impl ResizeSurfaceGrab { @@ -98,6 +99,7 @@ impl ResizeSurfaceGrab { edges, initial_rect, last_size: initial_rect.size, + finished: false, } } } @@ -123,6 +125,8 @@ impl PointerGrab for ResizeSurfaceGrab { }); self.last_size = constrained_size(self.initial_rect.size, delta, self.edges, minimum, maximum); + let rect = resized_rect(self.initial_rect, self.last_size, self.edges); + data.set_floating_window_geometry(&self.window, rect.loc, rect.size); surface.with_pending_state(|state| { state.states.set(xdg_toplevel::State::Resizing); @@ -149,6 +153,7 @@ impl PointerGrab for ResizeSurfaceGrab { ) { handle.button(data, event); if handle.current_pressed().is_empty() { + self.finished = true; handle.unset_grab(self, data, event.serial, event.time, true); let surface = self .window @@ -250,7 +255,29 @@ impl PointerGrab for ResizeSurfaceGrab { &self.start_data } - fn unset(&mut self, _data: &mut Ferese) {} + fn unset(&mut self, data: &mut Ferese) { + if self.finished { + return; + } + + let surface = self + .window + .toplevel() + .expect("managed window has a toplevel"); + surface.with_pending_state(|state| { + state.states.unset(xdg_toplevel::State::Resizing); + state.size = Some(self.initial_rect.size); + }); + surface.send_pending_configure(); + ResizeState::with(surface.wl_surface(), |state| *state = ResizeState::Idle); + data.set_floating_window_geometry( + &self.window, + self.initial_rect.loc, + self.initial_rect.size, + ); + data.space + .map_element(self.window.clone(), self.initial_rect.loc, true); + } } #[derive(Clone, Copy, Debug, Default)] @@ -359,6 +386,23 @@ fn constrained_size( .into() } +fn resized_rect( + initial: Rectangle, + size: Size, + edges: ResizeEdge, +) -> Rectangle { + let mut location = initial.loc; + + if edges.left() { + location.x += initial.size.w - size.w; + } + if edges.top() { + location.y += initial.size.h - size.h; + } + + Rectangle::new(location, size) +} + #[cfg(test)] mod tests { use super::*; @@ -398,4 +442,17 @@ mod tests { ); assert_eq!(size, (1_024, 768).into()); } + + #[test] + fn resized_rect_keeps_the_opposite_edge_fixed() { + let initial = Rectangle::new((100, 80).into(), (800, 600).into()); + let resized = resized_rect( + initial, + (700, 550).into(), + ResizeEdge(xdg_toplevel::ResizeEdge::TopLeft), + ); + + assert_eq!(resized.loc, (200, 130).into()); + assert_eq!(resized.size, (700, 550).into()); + } } diff --git a/compositor/src/handlers/xdg_shell.rs b/compositor/src/handlers/xdg_shell.rs index d30f860..09c9e95 100644 --- a/compositor/src/handlers/xdg_shell.rs +++ b/compositor/src/handlers/xdg_shell.rs @@ -100,12 +100,16 @@ impl XdgShellHandler for Ferese { let Some(initial_location) = self.space.element_location(&window) else { return; }; + if !self.is_floating_window(&window) { + return; + } pointer.set_grab( self, MoveSurfaceGrab { start_data, window, initial_location, + finished: false, }, serial, Focus::Clear, @@ -143,6 +147,9 @@ impl XdgShellHandler for Ferese { let Some(location) = self.space.element_location(&window) else { return; }; + if !self.is_floating_window(&window) { + return; + } let rect = Rectangle::new(location, window.geometry().size); pointer.set_grab( self, diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 18a84f9..1aa3e8f 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -21,7 +21,7 @@ use smithay::{ protocol::wl_surface::WlSurface, }, }, - utils::{Logical, Point}, + utils::{Logical, Point, Size}, wayland::{ compositor::{CompositorClientState, CompositorState}, fractional_scale::FractionalScaleManagerState, @@ -440,6 +440,34 @@ impl Ferese { } } + pub fn is_floating_window(&self, window: &Window) -> bool { + self.window_ids + .get(window) + .and_then(|id| self.workspaces.placement(*id)) + .is_some_and(|placement| matches!(placement, WindowPlacement::Floating { .. })) + } + + pub fn set_floating_window_geometry( + &mut self, + window: &Window, + location: Point, + size: Size, + ) { + let Some(id) = self.window_ids.get(window).copied() else { + return; + }; + let rect = Rect::new( + location.x as f64, + location.y as f64, + size.w.max(1) as f64, + size.h.max(1) as f64, + ); + + if let Err(error) = self.workspaces.set_floating_rect(id, rect) { + tracing::error!(%error, ?id, "failed to update floating window geometry"); + } + } + pub fn switch_workspace(&mut self, index: u32) { let focus = match self.workspaces.switch_to_numeric(index) { Ok(focus) => focus, diff --git a/crates/ferese-core/src/workspace.rs b/crates/ferese-core/src/workspace.rs index c3186f4..1908097 100644 --- a/crates/ferese-core/src/workspace.rs +++ b/crates/ferese-core/src/workspace.rs @@ -127,6 +127,28 @@ impl WorkspaceSet { self.placements.get(&window).copied() } + pub fn set_floating_rect( + &mut self, + window: WindowId, + rect: Rect, + ) -> Result<(), WorkspaceError> { + let placement = self + .placements + .get_mut(&window) + .ok_or(LayoutError::UnknownWindow(window))?; + + if !matches!(placement, WindowPlacement::Floating { .. }) { + return Err(WorkspaceError::InvalidState("window is not floating")); + } + + *placement = WindowPlacement::Floating { + rect: normalized_floating_rect(rect), + }; + + debug_assert!(self.validate().is_ok()); + Ok(()) + } + pub fn focus_window(&mut self, window: WindowId) -> Result<(), WorkspaceError> { if self.workspace_for_window(window) != Some(self.active) { return Err(WorkspaceError::InvalidState( @@ -621,6 +643,38 @@ mod tests { assert!(workspaces.validate().is_ok()); } + #[test] + fn floating_geometry_updates_are_authoritative_and_validated() { + let mut workspaces = WorkspaceSet::default(); + workspaces + .insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); + assert_eq!( + workspaces.set_floating_rect(WindowId(1), Rect::default()), + Err(WorkspaceError::InvalidState("window is not floating")) + ); + + workspaces + .toggle_floating( + WindowId(1), + Rect::new(10.0, 20.0, 640.0, 480.0), + Axis::Horizontal, + 0.5, + ) + .unwrap(); + workspaces + .set_floating_rect(WindowId(1), Rect::new(30.0, 40.0, f64::NAN, -2.0)) + .unwrap(); + + assert_eq!( + workspaces.placement(WindowId(1)), + Some(WindowPlacement::Floating { + rect: Rect::new(30.0, 40.0, 1.0, 1.0) + }) + ); + assert!(workspaces.validate().is_ok()); + } + #[test] fn fullscreen_preserves_underlying_placement() { let mut workspaces = WorkspaceSet::default();