Skip to main content

photon_ui/
layer.rs

1use crate::{
2    Component,
3    layout::{
4        Layout,
5        Rect,
6    },
7    renderer::Rendered,
8};
9
10/// Visual effect applied to lower layers to make stacking obvious.
11#[derive(Debug, Clone, PartialEq, Default)]
12pub enum Shadow {
13    /// No shadow effect.
14    #[default]
15    None,
16    /// Dim the entire screen behind this layer except for the layer's own
17    /// opaque content. The `style` string is an ANSI prefix such as
18    /// `"\x1b[2m"` for faint text.
19    Dim {
20        /// ANSI style prefix applied to lower-layer cells visible behind this
21        /// layer.
22        style: String,
23    },
24    /// Render a drop-shadow offset from this layer's opaque bounding box.
25    Drop {
26        /// ANSI style prefix applied to shadow cells.
27        style: String,
28        /// Horizontal offset in columns.
29        offset_x: i16,
30        /// Vertical offset in rows.
31        offset_y: i16,
32    },
33}
34
35/// Classification of a layer for backward-compatible TUI APIs.
36#[derive(Debug, Clone, Copy, PartialEq, Default)]
37pub enum LayerKind {
38    /// The bottom layer that receives mounted components.
39    #[default]
40    Base,
41    /// A floating layer created by `TUI::add_overlay`.
42    Overlay,
43    /// A capture layer created by `TUI::show_modal`.
44    Modal,
45}
46
47/// A full-terminal-size surface that holds its own components and layout.
48///
49/// Layers stack from index `0` (bottom/floor) to `N` (top/front). The TUI
50/// renders each layer independently and then composites them front-to-back,
51/// stripping cells that are hidden by higher layers.
52#[derive(Default)]
53pub struct Layer {
54    /// Components owned by this layer.
55    pub components: Vec<Box<dyn Component>>,
56    /// Optional layout for splitting the layer among its components.
57    pub layout: Option<Layout>,
58    /// Visual effect applied to lower layers behind this layer.
59    pub shadow: Shadow,
60    /// Whether this layer participates in rendering.
61    pub visible: bool,
62    /// TUI classification of this layer.
63    pub kind: LayerKind,
64}
65
66impl Layer {
67    /// Create a new empty layer.
68    pub fn new() -> Self {
69        Self {
70            components: Vec::new(),
71            layout: None,
72            shadow: Shadow::None,
73            visible: true,
74            kind: LayerKind::Base,
75        }
76    }
77
78    /// Create a layer that already contains a single component.
79    pub fn with_component(component: Box<dyn Component>) -> Self {
80        Self {
81            components: vec![component],
82            layout: None,
83            shadow: Shadow::None,
84            visible: true,
85            kind: LayerKind::Base,
86        }
87    }
88
89    /// Assign a layout to this layer.
90    pub fn set_layout(&mut self, layout: Layout) {
91        self.layout = Some(layout);
92    }
93
94    /// Append a component to this layer.
95    pub fn mount(&mut self, component: Box<dyn Component>) {
96        self.components.push(component);
97    }
98
99    /// Set the layer kind.
100    pub fn with_kind(mut self, kind: LayerKind) -> Self {
101        self.kind = kind;
102        self
103    }
104
105    /// Render this layer to a full-terminal-size buffer.
106    ///
107    /// The returned [`Rendered`] has exactly `height` lines. `focused_index`
108    /// identifies which of this layer's components currently has focus so that
109    /// its cursor can be translated into screen coordinates.
110    pub fn render(&self, width: u16, height: u16, focused_index: Option<usize>) -> Rendered {
111        if !self.visible {
112            let mut rendered = Rendered::empty();
113            while rendered.lines.len() < height as usize {
114                rendered.lines.push(String::new());
115            }
116            return rendered;
117        }
118
119        let mut rendered = Rendered::empty();
120        let term_rect = Rect::new(0, 0, width, height);
121
122        if let Some(ref layout) = self.layout {
123            let areas = layout.split(term_rect);
124            for (i, (child, area)) in self.components.iter().zip(areas.iter()).enumerate() {
125                let child_rendered = match child.render_rect(*area) {
126                    | Ok(r) => r,
127                    | Err(_) => continue,
128                };
129                child_rendered.blit_into_rect(&mut rendered, *area);
130                if Some(i) == focused_index &&
131                    let Some((r_local, c_local)) = child_rendered.cursor
132                {
133                    rendered.cursor = Some((area.y as usize + r_local, area.x as usize + c_local));
134                }
135            }
136        } else {
137            let mut row = 0usize;
138            for (i, child) in self.components.iter().enumerate() {
139                if row >= height as usize {
140                    break;
141                }
142                let child_rendered = match child.render(width) {
143                    | Ok(r) => r,
144                    | Err(_) => continue,
145                };
146                let start_row = row;
147                for line in &child_rendered.lines {
148                    if row < height as usize {
149                        rendered.lines.push(line.clone());
150                        row += 1;
151                    }
152                }
153                if Some(i) == focused_index &&
154                    let Some((r_local, c_local)) = child_rendered.cursor
155                {
156                    rendered.cursor = Some((start_row + r_local, c_local));
157                }
158                for image in &child_rendered.images {
159                    rendered.images.push(crate::renderer::ImageCommand {
160                        id: image.id,
161                        data: image.data.clone(),
162                        row: start_row as u16 + image.row,
163                        col: image.col,
164                    });
165                }
166            }
167        }
168
169        while rendered.lines.len() < height as usize {
170            rendered.lines.push(String::new());
171        }
172
173        rendered
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use crate::{
181        RenderError,
182        components::Text,
183        layout::{
184            Constraint,
185            Layout,
186        },
187        renderer::ImageCommand,
188    };
189
190    struct CursorComponent;
191
192    impl Component for CursorComponent {
193        fn render(&self, _width: u16) -> Result<Rendered, RenderError> {
194            Ok(Rendered {
195                lines: vec!["cursor".into()],
196                cursor: Some((0, 3)),
197                images: Vec::new(),
198            })
199        }
200    }
201
202    #[test]
203    fn layer_new_is_empty() {
204        let layer = Layer::new();
205        assert!(layer.components.is_empty());
206        assert!(layer.layout.is_none());
207        assert_eq!(layer.shadow, Shadow::None);
208        assert!(layer.visible);
209    }
210
211    #[test]
212    fn layer_render_empty_pads_to_terminal_size() {
213        let layer = Layer::new();
214        let rendered = layer.render(80, 24, None);
215        assert_eq!(rendered.lines.len(), 24);
216        assert!(rendered.cursor.is_none());
217        assert!(rendered.images.is_empty());
218    }
219
220    #[test]
221    fn layer_render_single_component_vertical_stack() {
222        let mut layer = Layer::new();
223        layer.mount(Box::new(Text::new("hello", 0, 0)));
224        let rendered = layer.render(80, 24, None);
225        assert!(rendered.lines[0].starts_with("hello"));
226        assert_eq!(crate::utils::visible_width(&rendered.lines[0]), 80);
227        assert_eq!(rendered.lines.len(), 24);
228    }
229
230    #[test]
231    fn layer_render_multiple_components_vertical_stack() {
232        let mut layer = Layer::new();
233        layer.mount(Box::new(Text::new("first", 0, 0)));
234        layer.mount(Box::new(Text::new("second", 0, 0)));
235        let rendered = layer.render(80, 24, None);
236        assert!(rendered.lines[0].starts_with("first"));
237        assert!(rendered.lines[1].starts_with("second"));
238        assert_eq!(rendered.lines.len(), 24);
239    }
240
241    #[test]
242    fn layer_render_clips_component_to_size() {
243        let mut layer = Layer::new();
244        layer.mount(Box::new(Text::new("hello", 0, 0)));
245        let rendered = layer.render(4, 2, None);
246        assert!(rendered.lines[0].starts_with("hel"));
247        assert_eq!(rendered.lines.len(), 2);
248    }
249
250    #[test]
251    fn layer_render_focused_component_cursor() {
252        let mut layer = Layer::new();
253        layer.mount(Box::new(Text::new("a", 0, 0)));
254        layer.mount(Box::new(CursorComponent));
255        let rendered = layer.render(80, 24, Some(1));
256        assert_eq!(rendered.cursor, Some((1, 3)));
257    }
258
259    #[test]
260    fn layer_render_preserves_images() {
261        struct ImageComponent;
262
263        impl Component for ImageComponent {
264            fn render(&self, _width: u16) -> Result<Rendered, RenderError> {
265                Ok(Rendered {
266                    lines: vec!["img".into()],
267                    cursor: None,
268                    images: vec![ImageCommand {
269                        id: 7,
270                        data: "data".into(),
271                        row: 0,
272                        col: 0,
273                    }],
274                })
275            }
276        }
277
278        let mut layer = Layer::new();
279        layer.mount(Box::new(ImageComponent));
280        let rendered = layer.render(80, 24, None);
281        assert_eq!(rendered.images.len(), 1);
282        assert_eq!(rendered.images[0].id, 7);
283    }
284
285    #[test]
286    fn layer_shadow_variants_clone_and_equal() {
287        let dim = Shadow::Dim {
288            style: "\x1b[2m".into(),
289        };
290        let cloned = dim.clone();
291        assert_eq!(dim, cloned);
292
293        let drop_shadow = Shadow::Drop {
294            style: "\x1b[2m".into(),
295            offset_x: 1,
296            offset_y: 1,
297        };
298        assert_ne!(dim, drop_shadow);
299    }
300
301    #[test]
302    fn layer_visible_false_skips_render() {
303        let mut layer = Layer::new();
304        layer.mount(Box::new(Text::new("hello", 0, 0)));
305        layer.visible = false;
306        let rendered = layer.render(80, 24, None);
307        assert_eq!(rendered.lines[0], "");
308        assert_eq!(rendered.lines.len(), 24);
309    }
310
311    #[test]
312    fn shadow_default_is_none() {
313        assert_eq!(Shadow::default(), Shadow::None);
314    }
315
316    #[test]
317    fn layer_kind_default_is_base() {
318        assert_eq!(LayerKind::default(), LayerKind::Base);
319    }
320
321    #[test]
322    fn layer_with_component() {
323        let layer = Layer::with_component(Box::new(Text::new("hi", 0, 0)));
324        assert_eq!(layer.components.len(), 1);
325        assert!(layer.layout.is_none());
326        assert_eq!(layer.shadow, Shadow::None);
327        assert_eq!(layer.kind, LayerKind::Base);
328        assert!(layer.visible);
329    }
330
331    #[test]
332    fn layer_with_kind() {
333        let layer = Layer::new().with_kind(LayerKind::Overlay);
334        assert_eq!(layer.kind, LayerKind::Overlay);
335    }
336
337    #[test]
338    fn layer_render_with_layout_focuses_cursor() {
339        let mut layer = Layer::new();
340        layer.set_layout(Layout::horizontal([
341            Constraint::Percentage(50),
342            Constraint::Percentage(50),
343        ]));
344        layer.mount(Box::new(Text::new("a", 0, 0)));
345        layer.mount(Box::new(CursorComponent));
346        let rendered = layer.render(80, 24, Some(1));
347        assert!(rendered.cursor.is_some());
348        let (_, col) = rendered.cursor.unwrap();
349        assert!(col >= 40, "expected cursor in second half, got col {}", col);
350    }
351
352    #[test]
353    fn layer_render_clips_to_height() {
354        let mut layer = Layer::new();
355        layer.mount(Box::new(Text::new("a", 0, 0)));
356        layer.mount(Box::new(Text::new("b", 0, 0)));
357        layer.mount(Box::new(Text::new("c", 0, 0)));
358        layer.mount(Box::new(Text::new("d", 0, 0)));
359        let rendered = layer.render(80, 2, None);
360        assert_eq!(rendered.lines.len(), 2);
361        assert!(rendered.lines[0].starts_with("a"));
362        assert!(rendered.lines[1].starts_with("b"));
363    }
364
365    #[test]
366    fn layer_render_ignores_failing_component() {
367        struct Fail;
368
369        impl Component for Fail {
370            fn render(&self, _width: u16) -> Result<Rendered, RenderError> {
371                Err(RenderError::WidthOverflow {
372                    line: String::new(),
373                    width: 0,
374                    actual: 0,
375                })
376            }
377        }
378
379        let mut layer = Layer::new();
380        layer.mount(Box::new(Text::new("ok", 0, 0)));
381        layer.mount(Box::new(Fail));
382        let rendered = layer.render(80, 24, None);
383        assert!(rendered.lines[0].starts_with("ok"));
384    }
385}