1use crate::{
2 Component,
3 InputResult,
4 RenderError,
5 Rendered,
6 events::Event,
7 layout::{
8 Layout,
9 Rect,
10 },
11};
12
13pub struct Container {
18 layout: Layout,
19 children: Vec<Box<dyn Component>>,
20}
21
22impl Container {
23 pub fn new(layout: Layout) -> Self {
25 Self {
26 layout,
27 children: Vec::new(),
28 }
29 }
30
31 pub fn push(&mut self, child: Box<dyn Component>) {
33 self.children.push(child);
34 }
35
36 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 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_with_child_builder() {
108 let container = Container::new(Layout::horizontal([
109 Constraint::Length(10),
110 Constraint::Length(10),
111 ]))
112 .with_child(Box::new(Text::new("left", 0, 0)))
113 .with_child(Box::new(Text::new("right", 0, 0)));
114
115 let rendered = container.render_rect(Rect::new(0, 0, 20, 1)).unwrap();
116 assert!(rendered.lines[0].contains("left"));
117 assert!(rendered.lines[0].contains("right"));
118 }
119
120 #[test]
121 fn container_render_uses_child_count_height() {
122 let mut container = Container::new(Layout::vertical([
123 Constraint::Length(1),
124 Constraint::Length(1),
125 ]));
126 container.push(Box::new(Text::new("top", 0, 0)));
127 container.push(Box::new(Text::new("bottom", 0, 0)));
128
129 let rendered = container.render(10).unwrap();
130 assert_eq!(rendered.lines.len(), 2);
131 }
132
133 #[test]
134 fn container_handle_input_falls_through() {
135 use crossterm::event::{
136 KeyCode,
137 KeyEvent,
138 KeyEventKind,
139 KeyModifiers,
140 };
141
142 let mut container = Container::new(Layout::horizontal([Constraint::Length(10)]));
143 container.push(Box::new(Text::new("text", 0, 0)));
144
145 let event = Event::Key(KeyEvent {
146 code: KeyCode::Char('x'),
147 modifiers: KeyModifiers::empty(),
148 kind: KeyEventKind::Press,
149 state: crossterm::event::KeyEventState::empty(),
150 });
151 let result = container.handle_input(&event);
152 assert_eq!(result, InputResult::Ignored);
153 }
154
155 #[test]
156 fn container_vertical_split() {
157 let mut container = Container::new(Layout::vertical([
158 Constraint::Length(1),
159 Constraint::Length(1),
160 ]));
161 container.push(Box::new(Text::new("top", 0, 0)));
162 container.push(Box::new(Text::new("bottom", 0, 0)));
163
164 let rendered = container.render_rect(Rect::new(0, 0, 10, 2)).unwrap();
165 assert_eq!(rendered.lines.len(), 2);
166 assert!(rendered.lines[0].contains("top"));
167 assert!(rendered.lines[1].contains("bottom"));
168 }
169
170 #[test]
173 fn container_nonzero_rect_no_double_offset() {
174 let mut outer = Container::new(Layout::horizontal([
175 Constraint::Length(10),
176 Constraint::Length(10),
177 ]));
178
179 let mut inner = Container::new(Layout::vertical([
180 Constraint::Length(1),
181 Constraint::Length(1),
182 ]));
183 inner.push(Box::new(Text::new("a", 0, 0)));
184 inner.push(Box::new(Text::new("b", 0, 0)));
185
186 outer.push(Box::new(Text::new("left", 0, 0)));
187 outer.push(Box::new(inner));
188
189 let rendered = outer.render_rect(Rect::new(0, 2, 20, 2)).unwrap();
191 assert_eq!(
194 rendered.lines.len(),
195 2,
196 "expected 2 lines, got {}",
197 rendered.lines.len()
198 );
199 assert!(rendered.lines[0].contains("left"));
200 assert!(rendered.lines[0].contains("a"));
201 assert!(rendered.lines[1].contains("b"));
202 }
203}