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;