Skip to main content

photon_ui/components/
container.rs

1use crate::{
2    Component,
3    InputResult,
4    RenderError,
5    Rendered,
6    events::Event,
7    layout::{
8        Rect,
9        layout::Layout,
10    },
11};
12
13/// A component that lays out its children using a `Layout`.
14///
15/// Each child is rendered into the rect assigned by the layout via
16/// [`render_rect`](Component::render_rect).
17pub struct Container {
18    layout: Layout,
19    children: Vec<Box<dyn Component>>,
20}
21
22impl Container {
23    /// Create a new container with the given layout.
24    pub fn new(layout: Layout) -> Self {
25        Self {
26            layout,
27            children: Vec::new(),
28        }
29    }
30
31    /// Add a child component.
32    pub fn push(&mut self, child: Box<dyn Component>) {
33        self.children.push(child);
34    }
35
36    /// Builder-style add.
37    pub fn with_child(mut self, child: Box<dyn Component>) -> Self {
38        self.children.push(child);
39        self
40    }
41}
42
43impl Component for Container {
44    fn render(&self, width: u16) -> Result<Rendered, RenderError> {
45        let rect = Rect::new(0, 0, width, self.children.len() as u16 * 3);
46        self.render_rect(rect)
47    }
48
49    fn render_rect(&self, rect: Rect) -> Result<Rendered, RenderError> {
50        let mut screen = Rendered::empty();
51        let areas = self.layout.split(rect);
52
53        for (child, area) in self.children.iter().zip(areas.iter()) {
54            if let Ok(rendered) = child.render_rect(*area) {
55                // Blit into the local buffer using coordinates relative to this container's
56                // origin. `layout.split()` returns areas in terminal
57                // coordinates (they include rect.x/y), but `screen` is a fresh
58                // local buffer whose origin is (0, 0).
59                let rel_area = Rect::new(
60                    area.x.saturating_sub(rect.x),
61                    area.y.saturating_sub(rect.y),
62                    area.width,
63                    area.height,
64                );
65                rendered.blit_into_rect(&mut screen, rel_area);
66            }
67        }
68
69        Ok(screen)
70    }
71
72    fn handle_input(&mut self, event: &Event) -> InputResult {
73        for child in &mut self.children {
74            let result = child.handle_input(event);
75            if result != InputResult::Ignored {
76                return result;
77            }
78        }
79        InputResult::Ignored
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::{
87        components::Text,
88        layout::Constraint,
89    };
90
91    #[test]
92    fn container_renders_children() {
93        let mut container = Container::new(Layout::horizontal([
94            Constraint::Length(10),
95            Constraint::Length(10),
96        ]));
97        container.push(Box::new(Text::new("left", 0, 0)));
98        container.push(Box::new(Text::new("right", 0, 0)));
99
100        let rendered = container.render_rect(Rect::new(0, 0, 20, 1)).unwrap();
101        assert_eq!(rendered.lines.len(), 1);
102        assert!(rendered.lines[0].contains("left"));
103        assert!(rendered.lines[0].contains("right"));
104    }
105
106    #[test]
107    fn container_vertical_split() {
108        let mut container = Container::new(Layout::vertical([
109            Constraint::Length(1),
110            Constraint::Length(1),
111        ]));
112        container.push(Box::new(Text::new("top", 0, 0)));
113        container.push(Box::new(Text::new("bottom", 0, 0)));
114
115        let rendered = container.render_rect(Rect::new(0, 0, 10, 2)).unwrap();
116        assert_eq!(rendered.lines.len(), 2);
117        assert!(rendered.lines[0].contains("top"));
118        assert!(rendered.lines[1].contains("bottom"));
119    }
120
121    /// Regression test: nested containers with non-zero rect coordinates must
122    /// not double-offset content.
123    #[test]
124    fn container_nonzero_rect_no_double_offset() {
125        let mut outer = Container::new(Layout::horizontal([
126            Constraint::Length(10),
127            Constraint::Length(10),
128        ]));
129
130        let mut inner = Container::new(Layout::vertical([
131            Constraint::Length(1),
132            Constraint::Length(1),
133        ]));
134        inner.push(Box::new(Text::new("a", 0, 0)));
135        inner.push(Box::new(Text::new("b", 0, 0)));
136
137        outer.push(Box::new(Text::new("left", 0, 0)));
138        outer.push(Box::new(inner));
139
140        // Outer rect starts at (0, 2). Inner container gets y = 2 from the layout.
141        let rendered = outer.render_rect(Rect::new(0, 2, 20, 2)).unwrap();
142        // The inner container's content should be at local rows 0 and 1,
143        // NOT shifted down by 2 due to double-offsetting.
144        assert_eq!(
145            rendered.lines.len(),
146            2,
147            "expected 2 lines, got {}",
148            rendered.lines.len()
149        );
150        assert!(rendered.lines[0].contains("left"));
151        assert!(rendered.lines[0].contains("a"));
152        assert!(rendered.lines[1].contains("b"));
153    }
154}