use std::collections::{HashMap, HashSet}; use std::hash::Hash; use ferese_animation::{AnimatedValue, WindowGeometry}; use ferese_core::WorkspaceId; use ferese_layout::{ColumnWidth, Rect, WindowId}; use smithay::desktop::Window; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::wayland::compositor::with_states; use smithay::wayland::shell::xdg::XdgToplevelSurfaceData; use crate::resize_transaction::ResizeTransaction; /// Metadata with exactly the lifetime of a managed window. Layouts and focus /// history reference its ID; they do not own or duplicate this metadata. #[derive(Default)] pub(crate) struct WindowRecord { pub geometry: Option, pub surface: Option, pub app_id: String, pub title: String, pub metadata_revision: u64, pub resize: Option, pub focus: Option, pub shadow: Option, pub focus_transition: Option, pub opening: Option, pub mapped_once: bool, pub world_x: Option<(WorkspaceId, AnimatedValue)>, pub coupled_width: Option<(WorkspaceId, AnimatedValue)>, pub maximized: bool, pub maximized_column_width: Option, pub natural_floating_pending: bool, pub floating_memory: Option, pub placement_anchor: Option, pub resize_anchor: Option<(bool, bool, Rect)>, pub column_width_pending: bool, pub rules_applied: bool, } /// The handle index is read-only outside this owner. Registering and removing a /// handle always updates its record as well. IDs are never reused on remapping. pub(crate) struct WindowRegistry { ids: HashMap, by_id: HashMap, surface_ids: HashMap, pub(super) records: HashMap, // Ascending IDs; overview reverses this for front-to-back traversal. overview_order: Vec, next_id: u64, } impl Default for WindowRegistry { fn default() -> Self { Self { ids: HashMap::new(), by_id: HashMap::new(), surface_ids: HashMap::new(), records: HashMap::new(), overview_order: Vec::new(), next_id: 1, } } } impl WindowRegistry { pub fn allocate_id(&mut self) -> WindowId { let id = WindowId(self.next_id); self.next_id = self.next_id.checked_add(1).expect("window IDs exhausted"); id } pub fn register(&mut self, window: W, id: WindowId) where W: Clone, { assert!(!self.ids.contains_key(&window), "window already registered"); assert!(!self.records.contains_key(&id), "window ID already registered"); let index = self.overview_order.partition_point(|existing| existing.0 < id.0); self.overview_order.insert(index, id); self.by_id.insert(id, window.clone()); self.ids.insert(window, id); self.records.insert(id, WindowRecord::default()); } pub fn ids(&self) -> &HashMap { &self.ids } /// Overview render and hit order, with the newest ID in front. Normal /// stacking changes do not affect this independent presentation order. pub fn overview_windows(&self) -> impl DoubleEndedIterator { self.overview_order.iter().rev().filter_map(|id| self.by_id.get(id)) } pub fn window(&self, id: WindowId) -> Option<&W> { self.by_id.get(&id) } pub fn id_for_surface(&self, surface: &WlSurface) -> Option { self.surface_ids.get(surface).copied() } pub fn ordered_ids(&self) -> impl DoubleEndedIterator + '_ { self.overview_order.iter().copied() } pub fn record(&self, id: WindowId) -> Option<&WindowRecord> { self.records.get(&id) } pub fn record_mut(&mut self, id: WindowId) -> Option<&mut WindowRecord> { self.records.get_mut(&id) } pub fn records(&self) -> impl Iterator { self.records.iter() } pub fn records_mut(&mut self) -> impl Iterator { self.records.iter_mut() } pub fn take_world_positions(&mut self) -> HashMap { self.records .iter_mut() .filter_map(|(id, record)| Some((*id, record.world_x.take()?))) .collect() } pub fn geometry(&self, id: &WindowId) -> Option<&WindowGeometry> { self.records.get(id)?.geometry.as_ref() } pub fn geometry_mut(&mut self, id: &WindowId) -> Option<&mut WindowGeometry> { self.records.get_mut(id)?.geometry.as_mut() } pub fn geometries(&self) -> impl Iterator { self.records .iter() .filter_map(|(id, record)| Some((id, record.geometry.as_ref()?))) } pub fn set_geometry(&mut self, id: WindowId, geometry: WindowGeometry) { self.update(id, |record| record.geometry = Some(geometry)); } pub fn transaction(&self, id: &WindowId) -> Option<&ResizeTransaction> { self.records.get(id)?.resize.as_ref() } pub fn set_transaction(&mut self, id: WindowId, transaction: ResizeTransaction) { self.update(id, |record| record.resize = Some(transaction)); } pub fn clear_transaction(&mut self, id: &WindowId) { self.update(*id, |record| record.resize = None); } pub fn resizing(&self) -> impl Iterator { self.records .iter() .filter_map(|(id, record)| record.resize.is_some().then_some(id)) } pub fn expire_transactions(&mut self, now: std::time::Duration) { for (id, record) in &mut self.records { if record.resize.is_some_and(|transaction| transaction.expired(now)) { tracing::warn!(?id, "resize presentation deadline reached"); record.resize = None; } } } pub fn update(&mut self, id: WindowId, update: impl FnOnce(&mut WindowRecord) -> R) -> Option { self.records.get_mut(&id).map(update) } pub fn remove(&mut self, window: &W) -> Option { let id = self.ids.remove(window)?; self.by_id.remove(&id); if let Some(record) = self.records.remove(&id) && let Some(surface) = record.surface { self.surface_ids.remove(&surface); } let index = self .overview_order .binary_search_by_key(&id.0, |id| id.0) .expect("registered windows have an overview entry"); self.overview_order.remove(index); Some(id) } pub fn take_column_width_requests(&mut self) -> HashSet { self.records .iter_mut() .filter_map(|(id, record)| std::mem::take(&mut record.column_width_pending).then_some(*id)) .collect() } } impl WindowRegistry { pub fn register_window(&mut self, window: Window, id: WindowId) { let surface = window.toplevel().map(|toplevel| toplevel.wl_surface().clone()); self.register(window, id); if let Some(surface) = surface { self.surface_ids.insert(surface.clone(), id); self.records.get_mut(&id).unwrap().surface = Some(surface.clone()); self.refresh_metadata(&surface); } } pub fn refresh_metadata(&mut self, surface: &WlSurface) -> bool { let Some(id) = self.id_for_surface(surface) else { return false; }; let record = self.records.get_mut(&id).unwrap(); with_states(surface, |states| { let attributes = states.data_map.get::().unwrap().lock().unwrap(); let app_id = attributes.app_id.as_deref().unwrap_or_default(); let title = attributes.title.as_deref().unwrap_or_default(); let changed = record.app_id != app_id || record.title != title; if changed { record.app_id.clear(); record.app_id.push_str(app_id); record.title.clear(); record.title.push_str(title); record.metadata_revision = record.metadata_revision.wrapping_add(1); } changed }) } } #[cfg(test)] mod tests { use super::*; #[test] fn overview_order_tracks_registration_removal_and_remapping() { let mut windows = WindowRegistry::default(); let first = windows.allocate_id(); let second = windows.allocate_id(); let third = windows.allocate_id(); // Registration need not happen in allocation order. windows.register("second", second); windows.register("first", first); windows.register("third", third); assert_eq!( windows.overview_windows().copied().collect::>(), ["third", "second", "first"] ); windows.remove(&"second"); assert_eq!( windows.overview_windows().copied().collect::>(), ["third", "first"] ); windows.remove(&"first"); let remapped = windows.allocate_id(); windows.register("first", remapped); assert_eq!( windows.overview_windows().copied().collect::>(), ["first", "third"] ); windows.remove(&"first"); windows.remove(&"third"); assert_eq!(windows.overview_windows().count(), 0); } #[test] fn overview_iteration_borrows_handles_and_removal_releases_cached_handles() { let handle = std::rc::Rc::new("surface"); let mut windows = WindowRegistry::default(); let id = windows.allocate_id(); windows.register(handle.clone(), id); assert_eq!(std::rc::Rc::strong_count(&handle), 3); for _ in 0..100 { assert!(std::rc::Rc::ptr_eq(windows.overview_windows().next().unwrap(), &handle)); assert_eq!(std::rc::Rc::strong_count(&handle), 3); } windows.remove(&handle); assert_eq!(std::rc::Rc::strong_count(&handle), 1); } #[test] fn unmap_drops_metadata_and_late_updates_cannot_resurrect_it() { let mut windows = WindowRegistry::default(); let id = windows.allocate_id(); windows.register("surface", id); windows.update(id, |record| { record.maximized = true; record.resize_anchor = Some((true, false, Rect::new(0., 0., 800., 600.))); record.column_width_pending = true; record.rules_applied = true; record.focus = Some(AnimatedValue::new(1.0)); record.opening = Some(AnimatedValue::new(0.0)); record.world_x = Some((WorkspaceId(1), AnimatedValue::new(50.0))); record.coupled_width = Some((WorkspaceId(1), AnimatedValue::new(800.0))); }); let geometry = WindowGeometry::new(Rect::new(0., 0., 800., 600.), None); let resize = ResizeTransaction::new(12.into(), std::time::Duration::ZERO); windows.set_geometry(id, geometry); windows.set_transaction(id, resize); assert_eq!(windows.remove(&"surface"), Some(id)); assert_eq!(windows.remove(&"surface"), None); assert!(windows.record(id).is_none()); assert!(windows.update(id, |record| record.maximized = true).is_none()); windows.set_geometry(id, geometry); windows.set_transaction(id, resize); assert!(windows.geometry(&id).is_none()); assert!(windows.transaction(&id).is_none()); assert_eq!(windows.resizing().count(), 0); assert!(windows.ids().is_empty()); assert!(windows.take_column_width_requests().is_empty()); } #[test] fn remap_has_new_identity_and_does_not_inherit_old_requests() { let mut windows = WindowRegistry::default(); let old = windows.allocate_id(); windows.register("surface", old); windows.update(old, |record| record.rules_applied = true); windows.remove(&"surface"); let new = windows.allocate_id(); windows.register("surface", new); assert_ne!(old, new); assert!( windows .update(old, |record| record.column_width_pending = true) .is_none() ); assert!(!windows.record(new).unwrap().rules_applied); assert!(windows.take_column_width_requests().is_empty()); } #[test] fn pending_width_changes_are_consumed_once_and_survive_other_window_removal() { let mut windows = WindowRegistry::default(); let first = windows.allocate_id(); let second = windows.allocate_id(); windows.register("first", first); windows.register("second", second); windows.update(first, |record| record.column_width_pending = true); windows.update(second, |record| record.column_width_pending = true); windows.remove(&"first"); assert_eq!(windows.take_column_width_requests(), HashSet::from([second])); assert!(windows.take_column_width_requests().is_empty()); assert!(windows.record(second).is_some()); } #[test] fn expired_resize_releases_only_its_barrier_and_preserves_geometry() { use std::time::Duration; let mut windows = WindowRegistry::default(); let first = windows.allocate_id(); let second = windows.allocate_id(); windows.register("first", first); windows.register("second", second); let geometry = WindowGeometry::new(Rect::new(0., 0., 800., 600.), None); windows.set_geometry(first, geometry); windows.set_transaction(first, ResizeTransaction::new(1.into(), Duration::ZERO)); windows.set_transaction(second, ResizeTransaction::new(2.into(), Duration::from_millis(100))); windows.expire_transactions(Duration::from_millis(300)); assert_eq!(windows.geometry(&first), Some(&geometry)); assert!(windows.transaction(&first).is_none()); assert!(windows.transaction(&second).unwrap().accepts(Some(2.into()))); assert_eq!(windows.resizing().copied().collect::>(), vec![second]); } }