1#![forbid(unsafe_code)]
2
3use crate::block::Alignment;
10use crate::{StatefulWidget, Widget};
11use ftui_core::geometry::Rect;
12use ftui_render::frame::Frame;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum VerticalAlignment {
17 #[default]
19 Top,
20 Middle,
22 Bottom,
24}
25
26#[derive(Debug, Clone)]
45pub struct Align<W> {
46 inner: W,
47 horizontal: Alignment,
48 vertical: VerticalAlignment,
49 child_width: Option<u16>,
50 child_height: Option<u16>,
51}
52
53impl<W> Align<W> {
54 pub fn new(inner: W) -> Self {
56 Self {
57 inner,
58 horizontal: Alignment::Left,
59 vertical: VerticalAlignment::Top,
60 child_width: None,
61 child_height: None,
62 }
63 }
64
65 pub fn horizontal(mut self, alignment: Alignment) -> Self {
67 self.horizontal = alignment;
68 self
69 }
70
71 pub fn vertical(mut self, alignment: VerticalAlignment) -> Self {
73 self.vertical = alignment;
74 self
75 }
76
77 pub fn child_width(mut self, width: u16) -> Self {
79 self.child_width = Some(width);
80 self
81 }
82
83 pub fn child_height(mut self, height: u16) -> Self {
85 self.child_height = Some(height);
86 self
87 }
88
89 pub fn aligned_area(&self, area: Rect) -> Rect {
91 let w = self.child_width.unwrap_or(area.width).min(area.width);
92 let h = self.child_height.unwrap_or(area.height).min(area.height);
93
94 let x = match self.horizontal {
95 Alignment::Left => area.x,
96 Alignment::Center => area.x.saturating_add((area.width.saturating_sub(w)) / 2),
97 Alignment::Right => area.x.saturating_add(area.width.saturating_sub(w)),
98 };
99
100 let y = match self.vertical {
101 VerticalAlignment::Top => area.y,
102 VerticalAlignment::Middle => area.y.saturating_add((area.height.saturating_sub(h)) / 2),
103 VerticalAlignment::Bottom => area.y.saturating_add(area.height.saturating_sub(h)),
104 };
105
106 Rect::new(x, y, w, h)
107 }
108
109 pub const fn inner(&self) -> &W {
111 &self.inner
112 }
113
114 pub fn inner_mut(&mut self) -> &mut W {
116 &mut self.inner
117 }
118
119 pub fn into_inner(self) -> W {
121 self.inner
122 }
123}
124
125impl<W: Widget> Widget for Align<W> {
126 fn render(&self, area: Rect, frame: &mut Frame) {
127 if area.is_empty() {
128 return;
129 }
130
131 let child_area = self.aligned_area(area);
132 if child_area.is_empty() {
133 return;
134 }
135
136 self.inner.render(child_area, frame);
137 }
138
139 fn is_essential(&self) -> bool {
140 self.inner.is_essential()
141 }
142}
143
144impl<W: StatefulWidget> StatefulWidget for Align<W> {
145 type State = W::State;
146
147 fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
148 if area.is_empty() {
149 return;
150 }
151
152 let child_area = self.aligned_area(area);
153 if child_area.is_empty() {
154 return;
155 }
156
157 self.inner.render(child_area, frame, state);
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use ftui_render::cell::Cell;
165 use ftui_render::grapheme_pool::GraphemePool;
166
167 fn buf_to_lines(buf: &ftui_render::buffer::Buffer) -> Vec<String> {
168 let mut lines = Vec::new();
169 for y in 0..buf.height() {
170 let mut row = String::with_capacity(buf.width() as usize);
171 for x in 0..buf.width() {
172 let ch = buf
173 .get(x, y)
174 .and_then(|c| c.content.as_char())
175 .unwrap_or(' ');
176 row.push(ch);
177 }
178 lines.push(row);
179 }
180 lines
181 }
182
183 #[derive(Debug, Clone, Copy)]
185 struct Fill(char);
186
187 impl Widget for Fill {
188 fn render(&self, area: Rect, frame: &mut Frame) {
189 for y in area.y..area.bottom() {
190 for x in area.x..area.right() {
191 frame.buffer.set(x, y, Cell::from_char(self.0));
192 }
193 }
194 }
195 }
196
197 #[test]
198 fn default_alignment_uses_full_area() {
199 let align = Align::new(Fill('X'));
200 let area = Rect::new(0, 0, 5, 3);
201 let mut pool = GraphemePool::new();
202 let mut frame = Frame::new(5, 3, &mut pool);
203 align.render(area, &mut frame);
204
205 for line in buf_to_lines(&frame.buffer) {
206 assert_eq!(line, "XXXXX");
207 }
208 }
209
210 #[test]
211 fn center_horizontal() {
212 let align = Align::new(Fill('X'))
213 .horizontal(Alignment::Center)
214 .child_width(3);
215 let area = Rect::new(0, 0, 7, 1);
216 let mut pool = GraphemePool::new();
217 let mut frame = Frame::new(7, 1, &mut pool);
218 align.render(area, &mut frame);
219
220 assert_eq!(buf_to_lines(&frame.buffer), vec![" XXX "]);
221 }
222
223 #[test]
224 fn right_horizontal() {
225 let align = Align::new(Fill('X'))
226 .horizontal(Alignment::Right)
227 .child_width(3);
228 let area = Rect::new(0, 0, 7, 1);
229 let mut pool = GraphemePool::new();
230 let mut frame = Frame::new(7, 1, &mut pool);
231 align.render(area, &mut frame);
232
233 assert_eq!(buf_to_lines(&frame.buffer), vec![" XXX"]);
234 }
235
236 #[test]
237 fn left_horizontal() {
238 let align = Align::new(Fill('X'))
239 .horizontal(Alignment::Left)
240 .child_width(3);
241 let area = Rect::new(0, 0, 7, 1);
242 let mut pool = GraphemePool::new();
243 let mut frame = Frame::new(7, 1, &mut pool);
244 align.render(area, &mut frame);
245
246 assert_eq!(buf_to_lines(&frame.buffer), vec!["XXX "]);
247 }
248
249 #[test]
250 fn center_vertical() {
251 let align = Align::new(Fill('X'))
252 .vertical(VerticalAlignment::Middle)
253 .child_height(1);
254 let area = Rect::new(0, 0, 3, 5);
255 let mut pool = GraphemePool::new();
256 let mut frame = Frame::new(3, 5, &mut pool);
257 align.render(area, &mut frame);
258
259 assert_eq!(
260 buf_to_lines(&frame.buffer),
261 vec![" ", " ", "XXX", " ", " "]
262 );
263 }
264
265 #[test]
266 fn bottom_vertical() {
267 let align = Align::new(Fill('X'))
268 .vertical(VerticalAlignment::Bottom)
269 .child_height(2);
270 let area = Rect::new(0, 0, 3, 4);
271 let mut pool = GraphemePool::new();
272 let mut frame = Frame::new(3, 4, &mut pool);
273 align.render(area, &mut frame);
274
275 assert_eq!(
276 buf_to_lines(&frame.buffer),
277 vec![" ", " ", "XXX", "XXX"]
278 );
279 }
280
281 #[test]
282 fn center_both_axes() {
283 let align = Align::new(Fill('O'))
284 .horizontal(Alignment::Center)
285 .vertical(VerticalAlignment::Middle)
286 .child_width(1)
287 .child_height(1);
288 let area = Rect::new(0, 0, 5, 5);
289 let mut pool = GraphemePool::new();
290 let mut frame = Frame::new(5, 5, &mut pool);
291 align.render(area, &mut frame);
292
293 assert_eq!(
294 buf_to_lines(&frame.buffer),
295 vec![" ", " ", " O ", " ", " "]
296 );
297 }
298
299 #[test]
300 fn child_larger_than_area_is_clamped() {
301 let align = Align::new(Fill('X'))
302 .horizontal(Alignment::Center)
303 .child_width(20)
304 .child_height(10);
305 let area = Rect::new(0, 0, 5, 3);
306
307 let child_area = align.aligned_area(area);
308 assert_eq!(child_area.width, 5);
309 assert_eq!(child_area.height, 3);
310 }
311
312 #[test]
313 fn zero_size_area_is_noop() {
314 let align = Align::new(Fill('X'))
315 .horizontal(Alignment::Center)
316 .child_width(3);
317 let area = Rect::new(0, 0, 0, 0);
318 let mut pool = GraphemePool::new();
319 let mut frame = Frame::new(5, 5, &mut pool);
320 align.render(area, &mut frame);
321
322 for y in 0..5 {
324 for x in 0..5u16 {
325 assert!(frame.buffer.get(x, y).unwrap().is_empty());
326 }
327 }
328 }
329
330 #[test]
331 fn zero_child_size_is_noop() {
332 let align = Align::new(Fill('X')).child_width(0).child_height(0);
333 let area = Rect::new(0, 0, 5, 5);
334 let mut pool = GraphemePool::new();
335 let mut frame = Frame::new(5, 5, &mut pool);
336 align.render(area, &mut frame);
337
338 for y in 0..5 {
339 for x in 0..5u16 {
340 assert!(frame.buffer.get(x, y).unwrap().is_empty());
341 }
342 }
343 }
344
345 #[test]
346 fn area_with_offset() {
347 let align = Align::new(Fill('X'))
348 .horizontal(Alignment::Center)
349 .child_width(2);
350 let area = Rect::new(10, 5, 6, 1);
351
352 let child = align.aligned_area(area);
353 assert_eq!(child.x, 12);
354 assert_eq!(child.y, 5);
355 assert_eq!(child.width, 2);
356 }
357
358 #[test]
359 fn aligned_area_right_bottom() {
360 let align = Align::new(Fill('X'))
361 .horizontal(Alignment::Right)
362 .vertical(VerticalAlignment::Bottom)
363 .child_width(2)
364 .child_height(1);
365 let area = Rect::new(0, 0, 10, 5);
366
367 let child = align.aligned_area(area);
368 assert_eq!(child.x, 8);
369 assert_eq!(child.y, 4);
370 assert_eq!(child.width, 2);
371 assert_eq!(child.height, 1);
372 }
373
374 #[test]
375 fn is_essential_delegates() {
376 struct Essential;
377 impl Widget for Essential {
378 fn render(&self, _: Rect, _: &mut Frame) {}
379 fn is_essential(&self) -> bool {
380 true
381 }
382 }
383
384 assert!(Align::new(Essential).is_essential());
385 assert!(!Align::new(Fill('X')).is_essential());
386 }
387}