reovim_driver_layout/compositor.rs
1//! Root compositor and window layer compositor traits.
2//!
3//! This module defines the compositor architecture for window layout management.
4//! The design follows a Wayland-inspired model where the runner is a pure event
5//! loop and compositors handle all window geometry concerns.
6//!
7//! # Architecture
8//!
9//! ```text
10//! RootCompositor (manages multiple layers)
11//! │
12//! ├── Layer 1 ("main", z_base=100)
13//! │ └── WindowLayerCompositor
14//! │ ├── Tiled Zone
15//! │ ├── Float Zone
16//! │ └── Overlay Zone
17//! │
18//! └── Layer 2 ("float-term", z_base=200)
19//! └── WindowLayerCompositor
20//! ├── Tiled Zone
21//! ├── Float Zone
22//! └── Overlay Zone
23//! ```
24//!
25//! # Responsibilities
26//!
27//! - **`RootCompositor`**: Layer lifecycle, focus routing between layers
28//! - **`WindowLayerCompositor`**: Window management within a single layer
29
30use {
31 super::layer::{Layer, LayerConfig, LayerId, OverlayConstraints, WindowPlacement, Zone},
32 crate::{NavigateDirection, Rect, SplitDirection, WindowId},
33};
34
35/// Result of compositing all layers.
36///
37/// Contains all information needed to render the complete window layout.
38#[derive(Debug, Clone)]
39pub struct CompositeResult {
40 /// All windows in z-order (lowest first).
41 pub placements: Vec<WindowPlacement>,
42 /// Currently focused window (if any).
43 pub focused: Option<WindowId>,
44 /// Active layer (receives keyboard input).
45 pub active_layer: Option<LayerId>,
46 /// Total screen bounds.
47 pub screen: Rect,
48}
49
50impl CompositeResult {
51 /// Create an empty composite result.
52 #[must_use]
53 #[allow(clippy::missing_const_for_fn)] // Vec::new() is not const
54 pub fn empty(screen: Rect) -> Self {
55 Self {
56 placements: Vec::new(),
57 focused: None,
58 active_layer: None,
59 screen,
60 }
61 }
62
63 /// Get placement for a specific window.
64 #[must_use]
65 pub fn get_placement(&self, window: WindowId) -> Option<&WindowPlacement> {
66 self.placements.iter().find(|p| p.window_id == window)
67 }
68}
69
70/// Root compositor manages multiple layers.
71///
72/// Each layer is a mini-compositor. The root handles:
73/// - Layer creation/destruction
74/// - Layer stacking (z-order)
75/// - Focus routing between layers
76/// - Click-through to lower layers
77///
78/// # Thread Safety
79///
80/// Implementations must be `Send + Sync` to allow compositors to be
81/// shared across async tasks.
82pub trait RootCompositor: Send + Sync {
83 /// Compute final layout for all layers.
84 ///
85 /// Returns all window placements sorted by z-order (lowest first).
86 fn composite(&self, screen: Rect) -> CompositeResult;
87
88 // ========================================================================
89 // Layer Management
90 // ========================================================================
91
92 /// Create a new layer.
93 ///
94 /// Returns the ID of the newly created layer.
95 fn create_layer(&mut self, config: LayerConfig) -> LayerId;
96
97 /// Remove a layer (closes all windows in it).
98 fn remove_layer(&mut self, layer: LayerId);
99
100 /// Get layer by label (for direct focus shortcuts like `<C-w>term`).
101 fn layer_by_label(&self, label: &str) -> Option<LayerId>;
102
103 /// Get all layers in z-order (lowest first).
104 fn layers(&self) -> Vec<&Layer>;
105
106 /// Set layer visibility.
107 fn set_layer_visible(&mut self, layer: LayerId, visible: bool);
108
109 /// Set layer opacity (future: transparency support).
110 fn set_layer_opacity(&mut self, layer: LayerId, opacity: f32);
111
112 /// Move layer in z-order (up/down in stack).
113 fn reorder_layer(&mut self, layer: LayerId, new_z: u16);
114
115 // ========================================================================
116 // Focus Management
117 // ========================================================================
118
119 /// Set active layer (receives keyboard input).
120 fn set_active_layer(&mut self, layer: LayerId);
121
122 /// Get active layer.
123 fn active_layer(&self) -> Option<LayerId>;
124
125 /// Set focus to window (also activates its layer).
126 fn set_focus(&mut self, window: WindowId);
127
128 /// Get focused window in active layer.
129 fn focused(&self) -> Option<WindowId>;
130
131 /// Focus window by clicking at position (handles click-through).
132 ///
133 /// Iterates layers top-to-bottom. Layers with opacity below
134 /// [`CLICK_THROUGH_THRESHOLD`](super::layer::CLICK_THROUGH_THRESHOLD)
135 /// are skipped, allowing clicks to pass through to lower layers.
136 ///
137 /// Returns the window that was focused, or None if clicking empty space.
138 fn focus_at(&mut self, x: u16, y: u16) -> Option<WindowId>;
139
140 // ========================================================================
141 // Per-Layer Operations (delegate to WindowLayerCompositor)
142 // ========================================================================
143
144 /// Get the compositor for a specific layer.
145 ///
146 /// Returns a trait object reference (`&dyn WindowLayerCompositor`) to allow
147 /// polymorphic access to layer-specific operations. Callers can invoke
148 /// any `WindowLayerCompositor` method on the returned reference.
149 ///
150 /// # Example
151 /// ```ignore
152 /// let layer = compositor.layer_compositor(id)?;
153 /// let windows = layer.navigate_tiled(from, Direction::Right);
154 /// ```
155 fn layer_compositor(&self, layer: LayerId) -> Option<&dyn WindowLayerCompositor>;
156
157 /// Get mutable compositor for a layer.
158 ///
159 /// Required for operations that modify layer state (split, close, resize).
160 fn layer_compositor_mut(&mut self, layer: LayerId) -> Option<&mut dyn WindowLayerCompositor>;
161
162 /// Window count across all layers.
163 fn window_count(&self) -> usize;
164
165 /// Update screen dimensions.
166 ///
167 /// Called on terminal resize to update cached screen size.
168 fn set_screen(&mut self, screen: Rect);
169
170 /// Find which layer contains a window.
171 fn layer_of(&self, window: WindowId) -> Option<LayerId>;
172
173 /// Clone this compositor into a new boxed instance.
174 ///
175 /// This allows extracting a fresh owned compositor from shared storage
176 /// (like `ServiceRegistry`'s `Arc<dyn RootCompositor>`). Implementations
177 /// that support `Clone` can simply use `Box::new(self.clone())`.
178 ///
179 /// # Example
180 ///
181 /// ```ignore
182 /// // Get compositor from ServiceRegistry (Arc)
183 /// let arc_compositor = registry.get(&CompositorKey::Root)?;
184 ///
185 /// // Convert to owned Box for DriverSession
186 /// let box_compositor = arc_compositor.boxed_clone();
187 /// driver_session.set_compositor(box_compositor);
188 /// ```
189 fn boxed_clone(&self) -> Box<dyn RootCompositor>;
190}
191
192/// Window layer compositor manages windows within a single layer.
193///
194/// Each layer has three zones: Tiled, Float, Overlay.
195/// This is the mini-compositor for one layer.
196///
197/// # Thread Safety
198///
199/// Implementations must be `Send + Sync` to allow compositors to be
200/// shared across async tasks.
201pub trait WindowLayerCompositor: Send + Sync {
202 /// Get layer ID.
203 fn id(&self) -> LayerId;
204
205 /// Arrange all windows in this layer.
206 ///
207 /// Returns window placements sorted by z-order.
208 fn arrange(&self, bounds: Rect) -> Vec<WindowPlacement>;
209
210 // ========================================================================
211 // Tiled Zone
212 // ========================================================================
213
214 /// Add first tiled window.
215 ///
216 /// Called when the layer has no tiled windows yet.
217 fn add_tiled(&mut self) -> WindowId;
218
219 /// Split a tiled window.
220 ///
221 /// Creates a new window adjacent to `from` in the specified direction.
222 /// Returns the new window's ID, or None if split not possible.
223 fn split_tiled(&mut self, from: WindowId, direction: SplitDirection) -> Option<WindowId>;
224
225 /// Navigate within tiled zone.
226 ///
227 /// Returns the window in the specified direction from `from`,
228 /// or None if there is no neighbor in that direction.
229 fn navigate_tiled(&self, from: WindowId, direction: NavigateDirection) -> Option<WindowId>;
230
231 /// Resize tiled window.
232 ///
233 /// Moves the edge of `window` in `direction` by `delta` units.
234 /// Positive delta expands, negative contracts.
235 fn resize_tiled(&mut self, window: WindowId, direction: NavigateDirection, delta: i16);
236
237 /// Close tiled window.
238 ///
239 /// Returns the window to focus next, or None if this was the last window.
240 fn close_tiled(&mut self, window: WindowId) -> Option<WindowId>;
241
242 /// Equalize tiled windows.
243 ///
244 /// Distributes space equally among all tiled windows.
245 fn equalize_tiled(&mut self);
246
247 /// Cycle through tiled windows.
248 ///
249 /// Returns the next (forward=true) or previous (forward=false) window
250 /// in winnr order.
251 fn cycle_tiled(&self, from: WindowId, forward: bool) -> Option<WindowId>;
252
253 // ========================================================================
254 // Float Zone
255 // ========================================================================
256
257 /// Create floating window.
258 ///
259 /// Creates a new floating window with the specified bounds.
260 fn create_float(&mut self, bounds: Rect) -> WindowId;
261
262 /// Move floating window.
263 fn move_float(&mut self, window: WindowId, x: u16, y: u16);
264
265 /// Resize floating window.
266 fn resize_float(&mut self, window: WindowId, width: u16, height: u16);
267
268 /// Bring to front within float zone.
269 fn raise_float(&mut self, window: WindowId);
270
271 /// Send to back within float zone.
272 fn lower_float(&mut self, window: WindowId);
273
274 /// Close floating window.
275 fn close_float(&mut self, window: WindowId);
276
277 /// Toggle between tiled and floating.
278 ///
279 /// If window is tiled, removes from tiled zone and creates float.
280 /// If window is floating, removes from float zone and adds to tiled.
281 fn toggle_float(&mut self, window: WindowId);
282
283 // ========================================================================
284 // Overlay Zone (#399)
285 // ========================================================================
286
287 /// Show overlay with constraints.
288 ///
289 /// Creates a new overlay positioned according to the constraints.
290 /// Overlays do NOT auto-focus (they are temporary UI above content).
291 fn show_overlay(&mut self, constraints: OverlayConstraints) -> WindowId;
292
293 /// Hide overlay.
294 fn hide_overlay(&mut self, window: WindowId);
295
296 /// Update overlay size.
297 fn resize_overlay(&mut self, window: WindowId, width: u16, height: u16);
298
299 /// Hide all overlays in this layer.
300 fn hide_all_overlays(&mut self);
301
302 // ========================================================================
303 // Focus
304 // ========================================================================
305
306 /// Set focused window within this layer.
307 fn set_focus(&mut self, window: WindowId);
308
309 /// Get focused window.
310 fn focused(&self) -> Option<WindowId>;
311
312 /// Get all windows in a zone.
313 fn windows_in_zone(&self, zone: Zone) -> Vec<WindowId>;
314
315 /// Get window's zone.
316 fn zone_of(&self, window: WindowId) -> Option<Zone>;
317}
318
319/// Error type for compositor operations.
320///
321/// Maps to vim-compatible error codes for user feedback.
322#[derive(Debug, Clone, PartialEq, Eq)]
323pub enum WindowError {
324 /// E444: Cannot close last window.
325 CannotCloseLastWindow,
326 /// E36: No neighbor in direction.
327 NoNeighbor(NavigateDirection),
328 /// E94: Not enough room to split.
329 NotEnoughRoom,
330 /// E36: Cannot resize at edge.
331 CannotResizeAtEdge,
332 /// Generic window not found.
333 WindowNotFound(WindowId),
334 /// Layer not found.
335 LayerNotFound(LayerId),
336}
337
338impl WindowError {
339 /// Get vim-compatible error code.
340 #[must_use]
341 pub const fn vim_code(&self) -> &'static str {
342 match self {
343 Self::CannotCloseLastWindow => "E444",
344 Self::NotEnoughRoom => "E94",
345 Self::NoNeighbor(_)
346 | Self::CannotResizeAtEdge
347 | Self::WindowNotFound(_)
348 | Self::LayerNotFound(_) => "E36",
349 }
350 }
351
352 /// Get user-facing error message.
353 #[must_use]
354 pub fn message(&self) -> String {
355 match self {
356 Self::CannotCloseLastWindow => "Cannot close last window".to_string(),
357 Self::NoNeighbor(dir) => format!("No window in direction: {dir:?}"),
358 Self::NotEnoughRoom => "Not enough room to split".to_string(),
359 Self::CannotResizeAtEdge => "Cannot resize at edge".to_string(),
360 Self::WindowNotFound(id) => format!("Window not found: {}", id.as_usize()),
361 Self::LayerNotFound(id) => format!("Layer not found: {}", id.as_u16()),
362 }
363 }
364}
365
366impl std::fmt::Display for WindowError {
367 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368 write!(f, "{}: {}", self.vim_code(), self.message())
369 }
370}
371
372impl std::error::Error for WindowError {}
373
374#[cfg(test)]
375#[path = "compositor_tests.rs"]
376mod tests;