From b111cbf531ac3225ad8bd685ea367dc8d92582c1 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Thu, 24 Sep 2026 11:40:41 +0100 Subject: [PATCH] feat: animate compositor geometry targets --- Cargo.lock | 1 + compositor/Cargo.toml | 1 + compositor/src/handlers/compositor.rs | 18 +++-- compositor/src/state.rs | 99 +++++++++++++++++++++++---- compositor/src/winit.rs | 6 +- 5 files changed, 106 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8fc1a54..b9c2437 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -368,6 +368,7 @@ checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" name = "ferese" version = "0.1.0" dependencies = [ + "ferese-animation", "ferese-core", "ferese-layout", "libc", diff --git a/compositor/Cargo.toml b/compositor/Cargo.toml index d08db16..4d91b2c 100644 --- a/compositor/Cargo.toml +++ b/compositor/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true rust-version.workspace = true [dependencies] +ferese-animation = { path = "../crates/ferese-animation" } ferese-core = { path = "../crates/ferese-core" } ferese-layout = { path = "../crates/ferese-layout" } libc.workspace = true diff --git a/compositor/src/handlers/compositor.rs b/compositor/src/handlers/compositor.rs index bc86224..224c1bb 100644 --- a/compositor/src/handlers/compositor.rs +++ b/compositor/src/handlers/compositor.rs @@ -36,12 +36,20 @@ impl CompositorHandler for Ferese { while let Some(parent) = get_parent(&root) { root = parent; } - if let Some(window) = self.space.elements().find(|window| { - window - .toplevel() - .is_some_and(|toplevel| toplevel.wl_surface() == &root) - }) { + let window = { + self.space + .elements() + .find(|window| { + window + .toplevel() + .is_some_and(|toplevel| toplevel.wl_surface() == &root) + }) + .cloned() + }; + + if let Some(window) = window { window.on_commit(); + self.record_client_commit(&window); } } xdg_shell::handle_commit(&mut self.popups, &mut self.space, surface); diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 7e0a67f..ccdbb12 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -6,6 +6,7 @@ use std::{ time::Instant, }; +use ferese_animation::{ClientSize, SpringConfig, WindowGeometry}; use ferese_core::{WindowPlacement, WorkspaceSet}; use ferese_layout::{Axis, Direction, GapConfig, LayoutResult, Rect, SizeConstraints, WindowId}; @@ -47,9 +48,11 @@ pub struct Ferese { pub space: Space, pub workspaces: WorkspaceSet, pub window_ids: HashMap, + pub window_geometry: HashMap, pub focused_window: Option, pub intercepted_keys: HashSet, next_window_id: u64, + last_animation_tick: Instant, pub popups: PopupManager, pub seat: Seat, pub compositor_state: CompositorState, @@ -94,17 +97,21 @@ impl Ferese { seat.add_pointer(); let socket_name = Self::init_wayland_listener(display, event_loop)?; + let start_time = Instant::now(); + Ok(Self { - start_time: Instant::now(), + start_time, socket_name, display_handle, loop_signal: event_loop.get_signal(), space: Space::default(), workspaces: WorkspaceSet::default(), window_ids: HashMap::new(), + window_geometry: HashMap::new(), focused_window: None, intercepted_keys: HashSet::new(), next_window_id: 1, + last_animation_tick: start_time, popups: PopupManager::default(), seat, compositor_state, @@ -228,6 +235,7 @@ impl Ferese { }; self.space.unmap_elem(window); + self.window_geometry.remove(&id); if let Err(error) = self.workspaces.remove_window(id) { tracing::error!(%error, ?id, "failed to remove window from layout"); } @@ -298,33 +306,89 @@ impl Ferese { } }; - Some((window.clone(), rect, fullscreen == Some(*id))) + Some((window.clone(), *id, rect, fullscreen == Some(*id))) }) .collect::>(); - for (window, rect, is_fullscreen) in placements { - let location = (rect.x.round() as i32, rect.y.round() as i32); - let size = ( - (rect.width.round() as i32).max(1), - (rect.height.round() as i32).max(1), - ); + let now = self.start_time.elapsed(); + + for (window, id, rect, is_fullscreen) in placements { + let committed_size = client_size(&window); + let geometry = self + .window_geometry + .entry(id) + .or_insert_with(|| WindowGeometry::new(rect, committed_size)); + let requested_size = geometry.set_logical_target(rect, now); + let visual = geometry.visual.current; + let location = (visual.x.round() as i32, visual.y.round() as i32); self.space.map_element(window.clone(), location, false); if let Some(toplevel) = window.toplevel() { - toplevel.with_pending_state(|state| { - state.size = Some(size.into()); + let state_changed = toplevel.with_pending_state(|state| { + if let Some(size) = requested_size { + state.size = Some((size.width, size.height).into()); + } if is_fullscreen { - state.states.set(xdg_toplevel::State::Fullscreen); + state.states.set(xdg_toplevel::State::Fullscreen) } else { - state.states.unset(xdg_toplevel::State::Fullscreen); + state.states.unset(xdg_toplevel::State::Fullscreen) } }); - toplevel.send_pending_configure(); + + if requested_size.is_some() || state_changed { + toplevel.send_pending_configure(); + } } } } + pub fn advance_animations(&mut self, now: Instant) -> bool { + let delta = now.saturating_duration_since(self.last_animation_tick); + self.last_animation_tick = now; + let windows = self + .space + .elements() + .filter_map(|window| { + self.window_ids + .get(window) + .copied() + .map(|id| (window.clone(), id)) + }) + .collect::>(); + let mut active_animation = false; + + for (window, id) in windows { + let Some(geometry) = self.window_geometry.get_mut(&id) else { + continue; + }; + + active_animation |= geometry.advance(delta, SpringConfig::default(), true); + let visual = geometry.visual.current; + self.space.map_element( + window, + (visual.x.round() as i32, visual.y.round() as i32), + false, + ); + } + + active_animation + } + + pub fn record_client_commit(&mut self, window: &Window) { + let Some(id) = self.window_ids.get(window) else { + return; + }; + let Some(size) = client_size(window) else { + return; + }; + let Some(geometry) = self.window_geometry.get_mut(id) else { + return; + }; + + geometry.client.commit(size); + } + pub fn focus_direction(&mut self, direction: Direction) { let Some(current) = self.focused_window else { return; @@ -655,6 +719,15 @@ fn centered_transient_rect(parent: Rect) -> Rect { ) } +fn client_size(window: &Window) -> Option { + let size = window.geometry().size; + + (size.w > 0 && size.h > 0).then_some(ClientSize { + width: size.w, + height: size.h, + }) +} + #[derive(Default)] pub struct ClientState { pub compositor_state: CompositorClientState, diff --git a/compositor/src/winit.rs b/compositor/src/winit.rs index 36c2a13..22761cd 100644 --- a/compositor/src/winit.rs +++ b/compositor/src/winit.rs @@ -1,4 +1,7 @@ -use std::{error::Error, time::Duration}; +use std::{ + error::Error, + time::{Duration, Instant}, +}; use smithay::{ backend::{ @@ -74,6 +77,7 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() } WinitEvent::Input(event) => state.process_input_event(event), WinitEvent::Redraw => { + state.advance_animations(Instant::now()); let damage = Rectangle::from_size(backend.window_size()); let mut presentation = OutputPresentationFeedback::new(&output); state.space.elements().for_each(|window| {