Skip to main content

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;