1use crate::buffer::ScreenBuffer;
4use crate::cell::Cell;
5use crate::geometry::Rect;
6use crate::style::Style;
7
8use super::Widget;
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub enum BorderStyle {
13 #[default]
15 None,
16 Single,
18 Double,
20 Rounded,
22 Heavy,
24}
25
26#[derive(Clone, Debug)]
28pub struct Container {
29 border: BorderStyle,
30 border_style: Style,
31 title: Option<String>,
32 title_style: Style,
33 padding: u16,
34}
35
36impl Container {
37 pub fn new() -> Self {
39 Self {
40 border: BorderStyle::None,
41 border_style: Style::default(),
42 title: None,
43 title_style: Style::default(),
44 padding: 0,
45 }
46 }
47
48 #[must_use]
50 pub fn border(mut self, style: BorderStyle) -> Self {
51 self.border = style;
52 self
53 }
54
55 #[must_use]
57 pub fn border_style(mut self, style: Style) -> Self {
58 self.border_style = style;
59 self
60 }
61
62 #[must_use]
64 pub fn title(mut self, title: impl Into<String>) -> Self {
65 self.title = Some(title.into());
66 self
67 }
68
69 #[must_use]
71 pub fn title_style(mut self, style: Style) -> Self {
72 self.title_style = style;
73 self
74 }
75
76 #[must_use]
78 pub fn padding(mut self, padding: u16) -> Self {
79 self.padding = padding;
80 self
81 }
82
83 pub fn inner_area(&self, area: Rect) -> Rect {
85 let border_offset = if self.border != BorderStyle::None {
86 1
87 } else {
88 0
89 };
90 let total_offset = border_offset + self.padding;
91
92 if area.size.width <= total_offset * 2 || area.size.height <= total_offset * 2 {
93 return Rect::new(
94 area.position.x + total_offset,
95 area.position.y + total_offset,
96 0,
97 0,
98 );
99 }
100
101 Rect::new(
102 area.position.x + total_offset,
103 area.position.y + total_offset,
104 area.size.width - total_offset * 2,
105 area.size.height - total_offset * 2,
106 )
107 }
108}
109
110impl Default for Container {
111 fn default() -> Self {
112 Self::new()
113 }
114}
115
116impl Widget for Container {
117 fn render(&self, area: Rect, buf: &mut ScreenBuffer) {
118 if area.size.width < 2 || area.size.height < 2 {
119 return;
120 }
121
122 let Some((tl, tr, bl, br, h, v)) = self.border.chars() else {
123 return; };
125
126 let right = area.position.x + area.size.width - 1;
127 let bottom = area.position.y + area.size.height - 1;
128
129 buf.set(
131 area.position.x,
132 area.position.y,
133 Cell::new(tl, self.border_style.clone()),
134 );
135 buf.set(
136 right,
137 area.position.y,
138 Cell::new(tr, self.border_style.clone()),
139 );
140 buf.set(
141 area.position.x,
142 bottom,
143 Cell::new(bl, self.border_style.clone()),
144 );
145 buf.set(right, bottom, Cell::new(br, self.border_style.clone()));
146
147 for x in (area.position.x + 1)..right {
149 buf.set(x, area.position.y, Cell::new(h, self.border_style.clone()));
150 buf.set(x, bottom, Cell::new(h, self.border_style.clone()));
151 }
152
153 for y in (area.position.y + 1)..bottom {
155 buf.set(area.position.x, y, Cell::new(v, self.border_style.clone()));
156 buf.set(right, y, Cell::new(v, self.border_style.clone()));
157 }
158
159 if let Some(ref title) = self.title {
161 let max_title_width = (area.size.width.saturating_sub(4)) as usize; let display_title = if title.len() > max_title_width {
163 &title[..max_title_width]
164 } else {
165 title.as_str()
166 };
167
168 let start_x = area.position.x + 2; for (i, ch) in display_title.chars().enumerate() {
170 let x = start_x + i as u16;
171 if x < right {
172 buf.set(
173 x,
174 area.position.y,
175 Cell::new(ch.to_string(), self.title_style.clone()),
176 );
177 }
178 }
179 }
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::color::{Color, NamedColor};
187 use crate::geometry::Size;
188
189 #[test]
190 fn single_border_corners() {
191 let container = Container::new().border(BorderStyle::Single);
192 let mut buf = ScreenBuffer::new(Size::new(10, 5));
193 container.render(Rect::new(0, 0, 10, 5), &mut buf);
194
195 assert_eq!(buf.get(0, 0).map(|c| c.grapheme.as_str()), Some("┌"));
196 assert_eq!(buf.get(9, 0).map(|c| c.grapheme.as_str()), Some("┐"));
197 assert_eq!(buf.get(0, 4).map(|c| c.grapheme.as_str()), Some("└"));
198 assert_eq!(buf.get(9, 4).map(|c| c.grapheme.as_str()), Some("┘"));
199 }
200
201 #[test]
202 fn single_border_edges() {
203 let container = Container::new().border(BorderStyle::Single);
204 let mut buf = ScreenBuffer::new(Size::new(10, 5));
205 container.render(Rect::new(0, 0, 10, 5), &mut buf);
206
207 assert_eq!(buf.get(1, 0).map(|c| c.grapheme.as_str()), Some("─"));
209 assert_eq!(buf.get(1, 4).map(|c| c.grapheme.as_str()), Some("─"));
211 assert_eq!(buf.get(0, 1).map(|c| c.grapheme.as_str()), Some("│"));
213 assert_eq!(buf.get(9, 1).map(|c| c.grapheme.as_str()), Some("│"));
215 }
216
217 #[test]
218 fn double_border() {
219 let container = Container::new().border(BorderStyle::Double);
220 let mut buf = ScreenBuffer::new(Size::new(10, 5));
221 container.render(Rect::new(0, 0, 10, 5), &mut buf);
222
223 assert_eq!(buf.get(0, 0).map(|c| c.grapheme.as_str()), Some("╔"));
224 assert_eq!(buf.get(9, 0).map(|c| c.grapheme.as_str()), Some("╗"));
225 }
226
227 #[test]
228 fn rounded_border() {
229 let container = Container::new().border(BorderStyle::Rounded);
230 let mut buf = ScreenBuffer::new(Size::new(10, 5));
231 container.render(Rect::new(0, 0, 10, 5), &mut buf);
232
233 assert_eq!(buf.get(0, 0).map(|c| c.grapheme.as_str()), Some("╭"));
234 assert_eq!(buf.get(9, 0).map(|c| c.grapheme.as_str()), Some("╮"));
235 }
236
237 #[test]
238 fn heavy_border() {
239 let container = Container::new().border(BorderStyle::Heavy);
240 let mut buf = ScreenBuffer::new(Size::new(10, 5));
241 container.render(Rect::new(0, 0, 10, 5), &mut buf);
242
243 assert_eq!(buf.get(0, 0).map(|c| c.grapheme.as_str()), Some("┏"));
244 assert_eq!(buf.get(1, 0).map(|c| c.grapheme.as_str()), Some("━"));
245 }
246
247 #[test]
248 fn border_with_title() {
249 let container = Container::new().border(BorderStyle::Single).title("Test");
250 let mut buf = ScreenBuffer::new(Size::new(20, 5));
251 container.render(Rect::new(0, 0, 20, 5), &mut buf);
252
253 assert_eq!(buf.get(2, 0).map(|c| c.grapheme.as_str()), Some("T"));
254 assert_eq!(buf.get(3, 0).map(|c| c.grapheme.as_str()), Some("e"));
255 assert_eq!(buf.get(4, 0).map(|c| c.grapheme.as_str()), Some("s"));
256 assert_eq!(buf.get(5, 0).map(|c| c.grapheme.as_str()), Some("t"));
257 }
258
259 #[test]
260 fn border_with_style() {
261 let style = Style::new().fg(Color::Named(NamedColor::Cyan));
262 let container = Container::new()
263 .border(BorderStyle::Single)
264 .border_style(style.clone());
265 let mut buf = ScreenBuffer::new(Size::new(10, 5));
266 container.render(Rect::new(0, 0, 10, 5), &mut buf);
267
268 assert_eq!(buf.get(0, 0).map(|c| &c.style), Some(&style));
269 }
270
271 #[test]
272 fn inner_area_with_border() {
273 let container = Container::new().border(BorderStyle::Single);
274 let inner = container.inner_area(Rect::new(0, 0, 20, 10));
275 assert_eq!(inner, Rect::new(1, 1, 18, 8));
276 }
277
278 #[test]
279 fn inner_area_with_border_and_padding() {
280 let container = Container::new().border(BorderStyle::Single).padding(1);
281 let inner = container.inner_area(Rect::new(0, 0, 20, 10));
282 assert_eq!(inner, Rect::new(2, 2, 16, 6));
283 }
284
285 #[test]
286 fn inner_area_no_border() {
287 let container = Container::new();
288 let inner = container.inner_area(Rect::new(0, 0, 20, 10));
289 assert_eq!(inner, Rect::new(0, 0, 20, 10));
290 }
291
292 #[test]
293 fn no_border_renders_nothing() {
294 let container = Container::new();
295 let mut buf = ScreenBuffer::new(Size::new(10, 5));
296 container.render(Rect::new(0, 0, 10, 5), &mut buf);
297 assert!(buf.get(0, 0).is_some_and(|c| c.is_blank()));
299 }
300}