reovim_driver_session/api/compositor.rs
1//! Compositor API for window layout operations.
2//!
3//! This module provides the `CompositorApi` trait, which offers high-level
4//! window management operations that delegate to the nested compositor.
5//!
6//! # Architecture
7//!
8//! ```text
9//! Command (e.g., FocusLeft)
10//! │
11//! ↓ calls
12//! CompositorApi.navigate(Left)
13//! │
14//! ↓ delegates to
15//! RootCompositor.layer_compositor(active_layer)
16//! │
17//! ↓ then
18//! WindowLayerCompositor.navigate_tiled(from, Left)
19//! ```
20//!
21//! # Overlay Operations (#399)
22//!
23//! Overlays are temporary UI elements (popups, menus, tooltips) that appear
24//! above all other windows. They do NOT auto-focus when shown - focus remains
25//! on the underlying tiled/float window.
26//!
27use {
28 reovim_driver_layout::{
29 LayerId, NavigateDirection, OverlayConstraints, Rect, SplitDirection, WindowId,
30 WindowPlacement,
31 },
32 reovim_kernel::api::v1::TabId,
33};
34
35/// Errors from compositor operations.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum CompositorError {
38 /// Cannot close the last window.
39 CannotCloseLastWindow,
40 /// No window in the specified direction.
41 NoNeighbor(NavigateDirection),
42 /// Not enough room to split.
43 NotEnoughRoom,
44 /// Cannot resize at screen edge.
45 CannotResizeAtEdge,
46 /// No active layer to operate on.
47 NoActiveLayer,
48 /// No focused window in the active layer.
49 NoFocusedWindow,
50 /// Window not found.
51 WindowNotFound(WindowId),
52 /// Layer not found.
53 LayerNotFound(LayerId),
54 /// No tab pages available.
55 NoTabPages,
56 /// Cannot close the last tab page.
57 CannotCloseLastTab,
58 /// Tab page not found.
59 TabNotFound(TabId),
60}
61
62impl std::fmt::Display for CompositorError {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 match self {
65 Self::CannotCloseLastWindow => write!(f, "cannot close last window"),
66 Self::NoNeighbor(dir) => write!(f, "no window {dir:?}"),
67 Self::NotEnoughRoom => write!(f, "not enough room to split"),
68 Self::CannotResizeAtEdge => write!(f, "cannot resize at edge"),
69 Self::NoActiveLayer => write!(f, "no active layer"),
70 Self::NoFocusedWindow => write!(f, "no focused window"),
71 Self::WindowNotFound(id) => write!(f, "window {} not found", id.as_usize()),
72 Self::LayerNotFound(id) => write!(f, "layer {} not found", id.as_u16()),
73 Self::NoTabPages => write!(f, "no tab pages"),
74 Self::CannotCloseLastTab => write!(f, "cannot close last tab"),
75 Self::TabNotFound(id) => write!(f, "tab {} not found", id.as_usize()),
76 }
77 }
78}
79
80impl std::error::Error for CompositorError {}
81
82/// Compositor operations for window management.
83///
84/// This trait provides high-level operations that commands use to manage
85/// windows. It abstracts over the compositor implementation, allowing
86/// commands to work with any compositor.
87///
88/// # Design
89///
90/// Operations work with the **currently focused window** in the **active layer**.
91/// This matches vim's window model where `<C-w>` commands operate on the
92/// current window without requiring explicit window arguments.
93///
94/// # Examples
95///
96/// ```ignore
97/// // In a command handler:
98/// fn execute(&self, runtime: &mut SessionRuntime<'_>, _ctx: &CommandContext) -> CommandResult {
99/// match runtime.navigate(NavigateDirection::Left) {
100/// Ok(window) => {
101/// runtime.focus(window)?;
102/// CommandResult::Success
103/// }
104/// Err(e) => CommandResult::Error(e.to_string()),
105/// }
106/// }
107/// ```
108pub trait CompositorApi {
109 /// Navigate in direction from the current window.
110 ///
111 /// Returns the window ID of the neighbor in that direction, or an error
112 /// if there is no neighbor (at screen edge).
113 ///
114 /// # Errors
115 ///
116 /// - `NoActiveLayer` - No layer is active
117 /// - `NoFocusedWindow` - No window is focused
118 /// - `NoNeighbor` - No window in the specified direction
119 fn navigate(&self, direction: NavigateDirection) -> Result<WindowId, CompositorError>;
120
121 /// Split the current window.
122 ///
123 /// Creates a new window adjacent to the current window in the specified
124 /// direction. The new window is focused after creation.
125 ///
126 /// # Errors
127 ///
128 /// - `NoActiveLayer` - No layer is active
129 /// - `NoFocusedWindow` - No window is focused
130 /// - `NotEnoughRoom` - Window is too small to split
131 fn split(&mut self, direction: SplitDirection) -> Result<WindowId, CompositorError>;
132
133 /// Close the current window.
134 ///
135 /// Returns the window that will receive focus after closing.
136 /// Named `close_current_window` to avoid conflict with `WindowApi::close_window`.
137 ///
138 /// # Errors
139 ///
140 /// - `CannotCloseLastWindow` - This is the only window
141 /// - `NoActiveLayer` - No layer is active
142 /// - `NoFocusedWindow` - No window is focused
143 fn close_current_window(&mut self) -> Result<WindowId, CompositorError>;
144
145 /// Close all windows except the current one.
146 ///
147 /// # Errors
148 ///
149 /// - `NoActiveLayer` - No layer is active
150 /// - `NoFocusedWindow` - No window is focused
151 fn close_others(&mut self) -> Result<(), CompositorError>;
152
153 /// Resize the current window.
154 ///
155 /// Moves the edge of the current window in the specified direction.
156 /// Positive delta expands in that direction, negative contracts.
157 ///
158 /// # Arguments
159 ///
160 /// * `direction` - Which edge to move (Left/Right/Up/Down)
161 /// * `delta` - Amount to move (positive = expand, negative = contract)
162 ///
163 /// # Errors
164 ///
165 /// - `NoActiveLayer` - No layer is active
166 /// - `NoFocusedWindow` - No window is focused
167 /// - `CannotResizeAtEdge` - Window is at screen edge in that direction
168 fn resize(&mut self, direction: NavigateDirection, delta: i16) -> Result<(), CompositorError>;
169
170 /// Equalize all windows in the current layer.
171 ///
172 /// Distributes space equally among all tiled windows.
173 ///
174 /// # Errors
175 ///
176 /// - `NoActiveLayer` - No layer is active
177 fn equalize(&mut self) -> Result<(), CompositorError>;
178
179 /// Cycle to next/previous window.
180 ///
181 /// Windows are ordered by winnr (top-to-bottom, left-to-right).
182 ///
183 /// # Arguments
184 ///
185 /// * `forward` - `true` for next window, `false` for previous
186 ///
187 /// # Errors
188 ///
189 /// - `NoActiveLayer` - No layer is active
190 /// - `NoFocusedWindow` - No window is focused
191 fn cycle(&self, forward: bool) -> Result<WindowId, CompositorError>;
192
193 /// Set focus to a specific window.
194 ///
195 /// Also activates the layer containing the window.
196 ///
197 /// # Errors
198 ///
199 /// - `WindowNotFound` - Window does not exist
200 fn focus(&mut self, window: WindowId) -> Result<(), CompositorError>;
201
202 /// Get the currently focused window.
203 ///
204 /// Returns `None` if no window is focused.
205 fn focused_window(&self) -> Option<WindowId>;
206
207 /// Get window count in the active layer.
208 fn compositor_window_count(&self) -> usize;
209
210 /// Get all window placements (for rendering).
211 ///
212 /// Returns windows in z-order (lowest first) for proper rendering.
213 fn arrange(&self, screen: Rect) -> Vec<WindowPlacement>;
214
215 /// Get the active layer ID.
216 fn active_layer(&self) -> Option<LayerId>;
217
218 /// Update screen size (on terminal resize).
219 fn set_screen(&mut self, screen: Rect);
220
221 // =========================================================================
222 // Float Zone Operations (#398)
223 // =========================================================================
224
225 /// Toggle the current window between tiled and float zones.
226 ///
227 /// If the window is tiled, it becomes a floating window (80% centered).
228 /// If the window is floating, it returns to the tiled zone.
229 ///
230 /// # Errors
231 ///
232 /// - `NoActiveLayer` - No layer is active
233 /// - `NoFocusedWindow` - No window is focused
234 fn toggle_float(&mut self) -> Result<(), CompositorError>;
235
236 /// Raise the current float window to the front.
237 ///
238 /// No-op if the current window is not a float.
239 ///
240 /// # Errors
241 ///
242 /// - `NoActiveLayer` - No layer is active
243 /// - `NoFocusedWindow` - No window is focused
244 fn raise_float(&mut self) -> Result<(), CompositorError>;
245
246 /// Lower the current float window to the back.
247 ///
248 /// No-op if the current window is not a float.
249 ///
250 /// # Errors
251 ///
252 /// - `NoActiveLayer` - No layer is active
253 /// - `NoFocusedWindow` - No window is focused
254 fn lower_float(&mut self) -> Result<(), CompositorError>;
255
256 // =========================================================================
257 // Opacity Operations (#400)
258 // =========================================================================
259
260 /// Set the opacity of the active layer.
261 ///
262 /// # Arguments
263 ///
264 /// * `opacity` - Value clamped to 0.0..=1.0
265 ///
266 /// # Errors
267 ///
268 /// - `NoActiveLayer` - No layer is active
269 fn set_active_layer_opacity(&mut self, opacity: f32) -> Result<(), CompositorError>;
270
271 /// Get the opacity of the active layer.
272 ///
273 /// # Errors
274 ///
275 /// - `NoActiveLayer` - No layer is active
276 fn active_layer_opacity(&self) -> Result<f32, CompositorError>;
277
278 /// Adjust the active layer's opacity by a delta.
279 ///
280 /// The result is clamped to 0.0..=1.0.
281 ///
282 /// # Arguments
283 ///
284 /// * `delta` - Amount to add (positive = more opaque, negative = more transparent)
285 ///
286 /// # Returns
287 ///
288 /// The new opacity value after adjustment.
289 ///
290 /// # Errors
291 ///
292 /// - `NoActiveLayer` - No layer is active
293 fn adjust_active_layer_opacity(&mut self, delta: f32) -> Result<f32, CompositorError>;
294
295 // =========================================================================
296 // Overlay Zone Operations (#399)
297 // =========================================================================
298
299 /// Show an overlay with constraints.
300 ///
301 /// Creates a new overlay positioned according to the constraints.
302 /// Overlays do NOT auto-focus - they are temporary UI elements that
303 /// appear above content without stealing keyboard input.
304 ///
305 /// # Arguments
306 ///
307 /// * `constraints` - Positioning constraints (anchor, preferred size, max size)
308 ///
309 /// # Returns
310 ///
311 /// The window ID of the new overlay.
312 ///
313 /// # Errors
314 ///
315 /// - `NoActiveLayer` - No layer is active
316 fn show_overlay(
317 &mut self,
318 constraints: OverlayConstraints,
319 ) -> Result<WindowId, CompositorError>;
320
321 /// Hide (remove) an overlay.
322 ///
323 /// # Arguments
324 ///
325 /// * `window` - The overlay window ID to hide
326 ///
327 /// # Errors
328 ///
329 /// - `NoActiveLayer` - No layer is active
330 fn hide_overlay(&mut self, window: WindowId) -> Result<(), CompositorError>;
331
332 /// Resize an overlay.
333 ///
334 /// Updates the preferred size of the overlay.
335 ///
336 /// # Errors
337 ///
338 /// - `NoActiveLayer` - No layer is active
339 fn resize_overlay(
340 &mut self,
341 window: WindowId,
342 width: u16,
343 height: u16,
344 ) -> Result<(), CompositorError>;
345
346 /// Hide all overlays in the active layer.
347 ///
348 /// Useful for commands like "dismiss all popups" (Escape key behavior).
349 ///
350 /// # Errors
351 ///
352 /// - `NoActiveLayer` - No layer is active
353 fn hide_all_overlays(&mut self) -> Result<(), CompositorError>;
354
355 // =========================================================================
356 // Tab Page Operations (#401)
357 // =========================================================================
358
359 /// Create a new tab page after the active tab.
360 ///
361 /// The new tab becomes active.
362 ///
363 /// # Errors
364 ///
365 /// - `NoTabPages` - Tab system not initialized
366 fn tab_new(&mut self) -> Result<TabId, CompositorError>;
367
368 /// Close the active tab page.
369 ///
370 /// # Errors
371 ///
372 /// - `NoTabPages` - Tab system not initialized
373 /// - `CannotCloseLastTab` - Cannot close the only tab
374 fn tab_close(&mut self) -> Result<(), CompositorError>;
375
376 /// Switch to the next tab page (wraps around).
377 ///
378 /// # Errors
379 ///
380 /// - `NoTabPages` - Tab system not initialized
381 fn tab_next(&mut self) -> Result<TabId, CompositorError>;
382
383 /// Switch to the previous tab page (wraps around).
384 ///
385 /// # Errors
386 ///
387 /// - `NoTabPages` - Tab system not initialized
388 fn tab_prev(&mut self) -> Result<TabId, CompositorError>;
389
390 /// Switch to a tab page by 0-based index.
391 ///
392 /// # Errors
393 ///
394 /// - `NoTabPages` - Tab system not initialized
395 /// - `TabNotFound` - Index out of bounds
396 fn tab_goto(&mut self, index: usize) -> Result<TabId, CompositorError>;
397
398 /// Get the number of tab pages.
399 fn tab_count(&self) -> usize;
400
401 /// Get the active tab page's ID.
402 fn active_tab_id(&self) -> Option<TabId>;
403}
404#[cfg(test)]
405#[path = "tests/compositor.rs"]
406mod tests;