Skip to main content

reovim_driver_layout/
layer.rs

1//! Nested layer system for compositing windows.
2//!
3//! This module provides types for the Hyprland-inspired nested compositor
4//! architecture. Each layer is a self-contained mini-compositor with
5//! three zones: Tiled, Float, and Overlay.
6//!
7//! # Layer Model
8//!
9//! ```text
10//! Layer (Self-contained compositor)
11//! ├── Overlay Zone (z: layer_z + 50-99) - Popups, tooltips, menus
12//! ├── Float Zone   (z: layer_z + 10-49) - Floating windows
13//! └── Tiled Zone   (z: layer_z + 0-9)   - Split windows (binary tree)
14//! ```
15
16use crate::{Rect, WindowId};
17
18use super::view::{ColIndex, LineIndex};
19
20/// Opacity threshold below which mouse clicks pass through to the layer below.
21///
22/// Layers with opacity below this value are considered "click-through" —
23/// they are still rendered (dimmed) but do not capture mouse input.
24pub const CLICK_THROUGH_THRESHOLD: f32 = 0.1;
25
26/// Unique identifier for a layer.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub struct LayerId(pub u16);
29
30impl LayerId {
31    /// Create a new layer ID.
32    #[must_use]
33    pub const fn new(id: u16) -> Self {
34        Self(id)
35    }
36
37    /// Get the raw ID value.
38    #[must_use]
39    pub const fn as_u16(self) -> u16 {
40        self.0
41    }
42}
43
44/// Zone within a layer (each layer has three zones).
45///
46/// Zones determine rendering order within a layer:
47/// - Tiled windows are rendered first (background)
48/// - Floating windows are rendered above tiled
49/// - Overlays are rendered on top of everything
50#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
51pub enum Zone {
52    /// Tiled windows (z: `layer_base` + 0-9). Binary split tree.
53    Tiled,
54    /// Floating windows (z: `layer_base` + 10-49). Free positioning.
55    Float,
56    /// Overlay windows (z: `layer_base` + 50-99). Popups, menus.
57    Overlay,
58}
59
60impl Zone {
61    /// Z-order offset within layer's z-range.
62    ///
63    /// # Z-Order Computation
64    ///
65    /// ```text
66    /// z_order = layer.z_base + zone.z_offset() + window_index
67    ///
68    /// Example for Layer 1 (z_base=100):
69    ///   Tiled windows:   z=100, 101, 102...
70    ///   Float windows:   z=110, 111, 112...
71    ///   Overlay windows: z=150, 151, 152...
72    /// ```
73    #[must_use]
74    pub const fn z_offset(self) -> u16 {
75        match self {
76            Self::Tiled => 0,
77            Self::Float => 10,
78            Self::Overlay => 50,
79        }
80    }
81}
82
83/// Z-order value for window stacking.
84///
85/// Lower values are rendered first (background), higher values on top.
86/// Each layer has a base z-order, and zones/windows add offsets:
87///
88/// ```text
89/// Layer 0 (z_base=0):   Tiled=0-9, Float=10-49, Overlay=50-99
90/// Layer 1 (z_base=100): Tiled=100-109, Float=110-149, Overlay=150-199
91/// ```
92///
93/// # Type Safety
94///
95/// Using a newtype prevents accidentally mixing z-order values with
96/// other `u16` values like dimensions or positions.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
98pub struct ZOrder(u16);
99
100impl ZOrder {
101    /// Create a new z-order value.
102    #[must_use]
103    pub const fn new(value: u16) -> Self {
104        Self(value)
105    }
106
107    /// Get the raw z-order value.
108    #[must_use]
109    pub const fn as_u16(self) -> u16 {
110        self.0
111    }
112
113    /// Compute the base z-order for a layer.
114    ///
115    /// Each layer gets 100 z-order slots (0-99, 100-199, etc.).
116    #[must_use]
117    pub const fn layer_base(layer_id: LayerId) -> Self {
118        Self(layer_id.as_u16() * 100)
119    }
120
121    /// Compute the final z-order for a window.
122    ///
123    /// # Arguments
124    ///
125    /// * `layer_base` - Base z-order for the layer
126    /// * `zone` - Which zone the window is in
127    /// * `index` - Window index within the zone (for stacking order)
128    #[must_use]
129    pub const fn for_window(layer_base: Self, zone: Zone, index: u16) -> Self {
130        Self(layer_base.0 + zone.z_offset() + index)
131    }
132
133    /// Add an offset to the z-order.
134    #[must_use]
135    pub const fn offset(self, delta: u16) -> Self {
136        Self(self.0 + delta)
137    }
138}
139
140/// A Layer is a self-contained mini-compositor.
141///
142/// Each layer contains three zones (Tiled, Float, Overlay) and can be
143/// stacked with other layers. Think: Hyprland workspaces that can overlay.
144#[derive(Debug, Clone)]
145pub struct Layer {
146    /// Unique identifier.
147    pub id: LayerId,
148    /// Human-readable label for shortcuts (e.g., "main", "term", "1").
149    pub label: String,
150    /// Base z-order (layers stack: 0, 100, 200...).
151    pub z_base: ZOrder,
152    /// Screen bounds this layer occupies.
153    pub bounds: Rect,
154    /// Opacity (0.0 = transparent, 1.0 = opaque). Future use.
155    pub opacity: f32,
156    /// Whether this layer is visible.
157    pub visible: bool,
158}
159
160impl Layer {
161    /// Create a new layer with default settings.
162    #[must_use]
163    pub fn new(id: LayerId, label: impl Into<String>, z_base: ZOrder) -> Self {
164        Self {
165            id,
166            label: label.into(),
167            z_base,
168            bounds: Rect::default(),
169            opacity: 1.0,
170            visible: true,
171        }
172    }
173
174    /// Check if this layer is click-through.
175    ///
176    /// A layer is click-through when its opacity is below
177    /// [`CLICK_THROUGH_THRESHOLD`]. Click-through layers are still
178    /// rendered (dimmed) but do not capture mouse input — clicks
179    /// pass through to the layer below.
180    ///
181    /// Keyboard input is NOT affected by click-through; it always
182    /// goes to the focused layer regardless of opacity.
183    #[must_use]
184    pub fn is_click_through(&self) -> bool {
185        self.opacity < CLICK_THROUGH_THRESHOLD
186    }
187
188    /// Calculate z-order for a window in a specific zone.
189    ///
190    /// # Arguments
191    ///
192    /// * `zone` - The zone the window belongs to
193    /// * `index` - Window index within the zone (0, 1, 2...)
194    ///
195    /// # Returns
196    ///
197    /// Absolute z-order value for rendering.
198    #[must_use]
199    pub const fn z_for(&self, zone: Zone, index: u16) -> ZOrder {
200        ZOrder::for_window(self.z_base, zone, index)
201    }
202}
203
204/// Positioned window ready for rendering.
205///
206/// This is the output of the compositor's `arrange()` method.
207/// Contains all information needed to render a window.
208#[derive(Debug, Clone)]
209pub struct WindowPlacement {
210    /// The window being placed.
211    pub window_id: WindowId,
212    /// Layer containing this window.
213    pub layer_id: LayerId,
214    /// Zone within the layer.
215    pub zone: Zone,
216    /// Computed screen bounds.
217    pub bounds: Rect,
218    /// Computed z-order for rendering.
219    pub z_order: ZOrder,
220    /// Whether the window is currently visible.
221    pub visible: bool,
222    /// Whether the window can receive focus.
223    pub focusable: bool,
224    /// Layer opacity (0.0 = fully transparent, 1.0 = fully opaque).
225    pub opacity: f32,
226}
227
228impl WindowPlacement {
229    /// Create a new window placement.
230    #[must_use]
231    pub const fn new(
232        window_id: WindowId,
233        layer_id: LayerId,
234        zone: Zone,
235        bounds: Rect,
236        z_order: ZOrder,
237    ) -> Self {
238        Self {
239            window_id,
240            layer_id,
241            zone,
242            bounds,
243            z_order,
244            visible: true,
245            focusable: true,
246            opacity: 1.0,
247        }
248    }
249
250    /// Check if this placement is click-through.
251    ///
252    /// A window placement inherits click-through from its layer's opacity.
253    /// When click-through, mouse clicks pass through to windows below.
254    #[must_use]
255    pub fn is_click_through(&self) -> bool {
256        self.opacity < CLICK_THROUGH_THRESHOLD
257    }
258}
259
260/// Anchor point for positioning overlays.
261///
262/// Overlays can be anchored to various reference points.
263#[derive(Debug, Clone, Copy)]
264#[allow(clippy::doc_markdown)] // Code blocks in doc comments
265pub enum Anchor {
266    /// Relative to cursor position in a window.
267    Cursor {
268        /// Window containing the cursor.
269        window: WindowId,
270        /// Line number (0-indexed).
271        line: LineIndex,
272        /// Column number (0-indexed).
273        col: ColIndex,
274    },
275    /// Relative to a screen position.
276    Screen {
277        /// X coordinate (column).
278        x: u16,
279        /// Y coordinate (row).
280        y: u16,
281    },
282    /// Centered on screen.
283    Center,
284    /// Below another window.
285    Below(WindowId),
286}
287
288/// Constraints for overlay positioning.
289///
290/// Used when creating overlays to specify size preferences.
291#[derive(Debug, Clone)]
292pub struct OverlayConstraints {
293    /// Anchor point for positioning.
294    pub anchor: Anchor,
295    /// Preferred width (if any).
296    pub preferred_width: Option<u16>,
297    /// Preferred height (if any).
298    pub preferred_height: Option<u16>,
299    /// Maximum allowed width.
300    pub max_width: Option<u16>,
301    /// Maximum allowed height.
302    pub max_height: Option<u16>,
303}
304
305impl OverlayConstraints {
306    /// Create constraints anchored to cursor position.
307    #[must_use]
308    pub const fn at_cursor(window: WindowId, line: LineIndex, col: ColIndex) -> Self {
309        Self {
310            anchor: Anchor::Cursor { window, line, col },
311            preferred_width: None,
312            preferred_height: None,
313            max_width: None,
314            max_height: None,
315        }
316    }
317
318    /// Create constraints anchored to cursor position from raw values.
319    #[must_use]
320    pub const fn at_cursor_raw(window: WindowId, line: usize, col: usize) -> Self {
321        Self {
322            anchor: Anchor::Cursor {
323                window,
324                line: LineIndex::new(line),
325                col: ColIndex::new(col),
326            },
327            preferred_width: None,
328            preferred_height: None,
329            max_width: None,
330            max_height: None,
331        }
332    }
333
334    /// Create constraints anchored to screen position.
335    #[must_use]
336    pub const fn at_position(x: u16, y: u16) -> Self {
337        Self {
338            anchor: Anchor::Screen { x, y },
339            preferred_width: None,
340            preferred_height: None,
341            max_width: None,
342            max_height: None,
343        }
344    }
345
346    /// Create centered constraints.
347    #[must_use]
348    pub const fn centered() -> Self {
349        Self {
350            anchor: Anchor::Center,
351            preferred_width: None,
352            preferred_height: None,
353            max_width: None,
354            max_height: None,
355        }
356    }
357
358    /// Set preferred dimensions.
359    #[must_use]
360    pub const fn with_size(mut self, width: u16, height: u16) -> Self {
361        self.preferred_width = Some(width);
362        self.preferred_height = Some(height);
363        self
364    }
365
366    /// Set maximum dimensions.
367    #[must_use]
368    pub const fn with_max_size(mut self, width: u16, height: u16) -> Self {
369        self.max_width = Some(width);
370        self.max_height = Some(height);
371        self
372    }
373}
374
375/// Layer creation parameters.
376#[derive(Debug, Clone)]
377pub struct LayerConfig {
378    /// Human-readable label for shortcuts.
379    pub label: String,
380    /// Screen bounds (None = fullscreen).
381    pub bounds: Option<Rect>,
382    /// Initial opacity (0.0-1.0).
383    pub opacity: f32,
384}
385
386impl LayerConfig {
387    /// Create a fullscreen layer configuration.
388    #[must_use]
389    pub fn fullscreen(label: impl Into<String>) -> Self {
390        Self {
391            label: label.into(),
392            bounds: None,
393            opacity: 1.0,
394        }
395    }
396
397    /// Create a layer with specific bounds.
398    #[must_use]
399    pub fn with_bounds(label: impl Into<String>, bounds: Rect) -> Self {
400        Self {
401            label: label.into(),
402            bounds: Some(bounds),
403            opacity: 1.0,
404        }
405    }
406}
407
408#[cfg(test)]
409#[path = "layer_tests.rs"]
410mod tests;