From 58d91363d6ef00689ec3d3564a143376928019bf Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Thu, 24 Sep 2026 06:19:06 +0100 Subject: [PATCH] feat: tile managed windows with layout tree --- Cargo.lock | 1 + compositor/Cargo.toml | 1 + compositor/src/handlers/activation.rs | 1 + compositor/src/handlers/xdg_shell.rs | 20 +++++- compositor/src/input.rs | 3 + compositor/src/state.rs | 91 ++++++++++++++++++++++++++- compositor/src/winit.rs | 2 + 7 files changed, 116 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 83d5512..6ae350f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -368,6 +368,7 @@ checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" name = "ferese" version = "0.1.0" dependencies = [ + "ferese-layout", "libc", "smithay", "tracing", diff --git a/compositor/Cargo.toml b/compositor/Cargo.toml index 2a7fd02..1d729c2 100644 --- a/compositor/Cargo.toml +++ b/compositor/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true rust-version.workspace = true [dependencies] +ferese-layout = { path = "../crates/ferese-layout" } libc.workspace = true smithay.workspace = true tracing.workspace = true diff --git a/compositor/src/handlers/activation.rs b/compositor/src/handlers/activation.rs index a321869..fe3fb28 100644 --- a/compositor/src/handlers/activation.rs +++ b/compositor/src/handlers/activation.rs @@ -47,6 +47,7 @@ impl XdgActivationHandler for Ferese { return; }; + self.focused_window = self.window_ids.get(&window).copied(); self.space.raise_element(&window, true); let keyboard = self.seat.get_keyboard().expect("seat has a keyboard"); keyboard.set_focus(self, Some(surface), SERIAL_COUNTER.next_serial()); diff --git a/compositor/src/handlers/xdg_shell.rs b/compositor/src/handlers/xdg_shell.rs index cc4b698..c0beab7 100644 --- a/compositor/src/handlers/xdg_shell.rs +++ b/compositor/src/handlers/xdg_shell.rs @@ -34,8 +34,24 @@ impl XdgShellHandler for Ferese { } fn new_toplevel(&mut self, surface: ToplevelSurface) { - self.space - .map_element(Window::new_wayland_window(surface), (0, 0), true); + self.add_tiled_window(Window::new_wayland_window(surface)); + } + + fn toplevel_destroyed(&mut self, surface: ToplevelSurface) { + let Some(window) = self + .space + .elements() + .find(|window| { + window + .toplevel() + .is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface()) + }) + .cloned() + else { + return; + }; + + self.remove_tiled_window(&window); } fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) { diff --git a/compositor/src/input.rs b/compositor/src/input.rs index a68fd8a..7233ec7 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -132,6 +132,8 @@ impl Ferese { if let Some((window, _)) = self.space.element_under(pointer.current_location()) { let window = window.clone(); + + self.focused_window = self.window_ids.get(&window).copied(); self.space.raise_element(&window, true); let surface = window .toplevel() @@ -140,6 +142,7 @@ impl Ferese { .clone(); keyboard.set_focus(self, Some(surface), serial); } else { + self.focused_window = None; keyboard.set_focus(self, Option::::None, serial); } diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 0837830..b462e52 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -1,4 +1,6 @@ -use std::{error::Error, ffi::OsString, sync::Arc, time::Instant}; +use std::{collections::HashMap, error::Error, ffi::OsString, sync::Arc, time::Instant}; + +use ferese_layout::{Axis, LayoutTree, Rect, WindowId}; use smithay::{ desktop::{PopupManager, Space, Window, WindowSurfaceType}, @@ -35,6 +37,10 @@ pub struct Ferese { pub display_handle: DisplayHandle, pub loop_signal: LoopSignal, pub space: Space, + pub layout: LayoutTree, + pub window_ids: HashMap, + pub focused_window: Option, + next_window_id: u64, pub popups: PopupManager, pub seat: Seat, pub compositor_state: CompositorState, @@ -85,6 +91,10 @@ impl Ferese { display_handle, loop_signal: event_loop.get_signal(), space: Space::default(), + layout: LayoutTree::default(), + window_ids: HashMap::new(), + focused_window: None, + next_window_id: 1, popups: PopupManager::default(), seat, compositor_state, @@ -145,6 +155,85 @@ impl Ferese { }) }) } + + pub fn add_tiled_window(&mut self, window: Window) { + let id = WindowId(self.next_window_id); + self.next_window_id += 1; + + if let Err(error) = self + .layout + .insert(id, self.focused_window, Axis::Horizontal, 0.5) + { + tracing::error!(%error, ?id, "failed to insert window into layout"); + return; + } + + self.window_ids.insert(window.clone(), id); + self.focused_window = Some(id); + self.space.map_element(window, (0, 0), true); + self.relayout(); + } + + pub fn remove_tiled_window(&mut self, window: &Window) { + let Some(id) = self.window_ids.remove(window) else { + return; + }; + + self.space.unmap_elem(window); + if let Err(error) = self.layout.remove(id) { + tracing::error!(%error, ?id, "failed to remove window from layout"); + } + if self.focused_window == Some(id) { + self.focused_window = self + .space + .elements() + .rev() + .find_map(|window| self.window_ids.get(window).copied()); + } + + self.relayout(); + } + + pub fn relayout(&mut self) { + let Some(output) = self.space.outputs().next() else { + return; + }; + let Some(output_geometry) = self.space.output_geometry(output) else { + return; + }; + let bounds = Rect::new( + output_geometry.loc.x as f64, + output_geometry.loc.y as f64, + output_geometry.size.w as f64, + output_geometry.size.h as f64, + ); + let geometry = match self.layout.geometry(bounds) { + Ok(geometry) => geometry, + Err(error) => { + tracing::error!(%error, "failed to compute tiled geometry"); + return; + } + }; + let placements = self + .window_ids + .iter() + .filter_map(|(window, id)| geometry.get(id).map(|rect| (window.clone(), *rect))) + .collect::>(); + + for (window, rect) 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), + ); + + self.space.map_element(window.clone(), location, false); + if let Some(toplevel) = window.toplevel() { + toplevel.with_pending_state(|state| state.size = Some(size.into())); + toplevel.send_pending_configure(); + } + } + } } #[derive(Default)] diff --git a/compositor/src/winit.rs b/compositor/src/winit.rs index 7d37aa0..36c2a13 100644 --- a/compositor/src/winit.rs +++ b/compositor/src/winit.rs @@ -69,6 +69,8 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() output_scale = scale; state.update_fractional_scale(scale); } + + state.relayout(); } WinitEvent::Input(event) => state.process_input_event(event), WinitEvent::Redraw => {