superconsole/components/
alignment.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under both the MIT license found in the
5 * LICENSE-MIT file in the root directory of this source tree and the Apache
6 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
7 * of this source tree.
8 */
9
10//! Oftentimes it is useful to align text inside the bounding box.
11//! This component offers a composiion based approach to do so.
12//! One may align text left, center, or right, and top, middle, or bottom.
13//! Additionally, horizontally left aligned text may be optionally justified.
14
15use crate::components::Blank;
16use crate::Component;
17use crate::Dimensions;
18use crate::DrawMode;
19use crate::Lines;
20
21/// Select the alignment of the vertical content
22#[derive(Debug, Eq, PartialEq, Clone, Copy)]
23pub enum VerticalAlignmentKind {
24    /// Content appears at the top.
25    Top,
26    /// Content appears approximately equidistant between top and bottom
27    Center,
28    /// Content appears at the bottom.
29    Bottom,
30}
31
32/// Select the alignment of the horizontal content
33#[derive(Debug, Eq, PartialEq, Clone, Copy)]
34pub enum HorizontalAlignmentKind {
35    /// Content appears at the left.
36    /// The argument determines whether the text is justified.
37    Left(bool),
38    /// Content appears in the middle.
39    Center,
40    /// Content appears to the right.
41    Right,
42}
43
44/// The [`Aligned`](Aligned) [`Component`](Component) can be used to specify in which part of the view the content should live.
45/// The [`HorizontalAlignmentKind`](HorizontalAlignmentKind) enum specifies the location relative to the x-axis.
46/// The [`VerticalAlignmentKind`](VerticalAlignmentKind) enum specified the location relative to the y-axis.
47#[derive(Debug)]
48pub struct Aligned<C: Component = Box<dyn Component>> {
49    pub child: C,
50    pub horizontal: HorizontalAlignmentKind,
51    pub vertical: VerticalAlignmentKind,
52}
53
54impl<C: Component> Aligned<C> {
55    /// Creates a new `Alignment` component with the given alignments.
56    pub fn new(
57        child: C,
58        horizontal: HorizontalAlignmentKind,
59        vertical: VerticalAlignmentKind,
60    ) -> Self {
61        Self {
62            child,
63            horizontal,
64            vertical,
65        }
66    }
67}
68
69impl Default for Aligned {
70    fn default() -> Self {
71        Self {
72            child: Box::new(Blank),
73            horizontal: HorizontalAlignmentKind::Left(false),
74            vertical: VerticalAlignmentKind::Top,
75        }
76    }
77}
78
79impl<C: Component> Component for Aligned<C> {
80    fn draw_unchecked(&self, dimensions: Dimensions, mode: DrawMode) -> anyhow::Result<Lines> {
81        let Dimensions { width, height } = dimensions;
82        let mut output = self.child.draw(dimensions, mode)?;
83
84        let number_of_lines = output.len();
85        let padding_needed = height.saturating_sub(number_of_lines);
86        match self.vertical {
87            VerticalAlignmentKind::Top => {}
88            VerticalAlignmentKind::Center => {
89                let top_pad = padding_needed / 2;
90                output.pad_lines_top(top_pad);
91                output.pad_lines_bottom(padding_needed - top_pad);
92            }
93            VerticalAlignmentKind::Bottom => {
94                output.pad_lines_top(padding_needed);
95            }
96        }
97
98        match self.horizontal {
99            HorizontalAlignmentKind::Left(justified) => {
100                if justified {
101                    output.justify();
102                }
103            }
104            HorizontalAlignmentKind::Center => {
105                for line in output.iter_mut() {
106                    let output_len = line.len();
107                    let padding_needed = width.saturating_sub(output_len);
108                    let left_pad = padding_needed / 2;
109                    line.pad_left(left_pad);
110                    // handles any rounding issues
111                    line.pad_right(padding_needed - left_pad);
112                }
113            }
114            HorizontalAlignmentKind::Right => {
115                for line in output.iter_mut() {
116                    line.pad_left(width.saturating_sub(line.len()));
117                }
118            }
119        }
120        Ok(output)
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use derive_more::AsRef;
127
128    use crate::components::alignment::HorizontalAlignmentKind;
129    use crate::components::alignment::VerticalAlignmentKind;
130    use crate::components::echo::Echo;
131    use crate::components::Aligned;
132    use crate::components::DrawMode;
133    use crate::Component;
134    use crate::Dimensions;
135    use crate::Line;
136    use crate::Lines;
137
138    #[derive(AsRef, Debug)]
139    struct Msg(Lines);
140
141    #[test]
142    fn test_align_left_unjustified() {
143        let original = Lines(vec![
144            vec!["hello world"].try_into().unwrap(),
145            vec!["pretty normal test"].try_into().unwrap(),
146        ]);
147        let component = Aligned::new(
148            Echo(original.clone()),
149            HorizontalAlignmentKind::Left(false),
150            VerticalAlignmentKind::Top,
151        );
152        let dimensions = Dimensions::new(20, 20);
153        let actual = component.draw(dimensions, DrawMode::Normal).unwrap();
154
155        assert_eq!(actual, original);
156    }
157
158    #[test]
159    fn test_align_left_justified() {
160        let original = Lines(vec![
161            vec!["hello world"].try_into().unwrap(),        // 11 chars
162            vec!["pretty normal test"].try_into().unwrap(), // 18 chars
163            vec!["short"].try_into().unwrap(),              // 5 chars
164        ]);
165        let component = Aligned::new(
166            Echo(original),
167            HorizontalAlignmentKind::Left(true),
168            VerticalAlignmentKind::Top,
169        );
170        let dimensions = Dimensions::new(20, 20);
171        let actual = component.draw(dimensions, DrawMode::Normal).unwrap();
172        let expected = Lines(vec![
173            vec!["hello world", &" ".repeat(18 - 11)]
174                .try_into()
175                .unwrap(),
176            vec!["pretty normal test"].try_into().unwrap(),
177            vec!["short", &" ".repeat(18 - 5)].try_into().unwrap(),
178        ]);
179
180        assert_eq!(actual, expected,);
181    }
182
183    #[test]
184    fn test_align_col_center() {
185        let original = Lines(vec![
186            vec!["hello world"].try_into().unwrap(),          // 11 chars
187            vec!["pretty normal testss"].try_into().unwrap(), // 20 chars
188            vec!["shorts"].try_into().unwrap(),               // 6 chars
189        ]);
190        let component = Aligned::new(
191            Echo(original),
192            HorizontalAlignmentKind::Center,
193            VerticalAlignmentKind::Top,
194        );
195        let dimensions = Dimensions::new(20, 20);
196        let actual = component.draw(dimensions, DrawMode::Normal).unwrap();
197        let expected = Lines(vec![
198            vec![" ".repeat(4).as_ref(), "hello world", &" ".repeat(5)]
199                .try_into()
200                .unwrap(),
201            vec!["pretty normal testss"].try_into().unwrap(),
202            vec![" ".repeat(7).as_ref(), "shorts", &" ".repeat(7)]
203                .try_into()
204                .unwrap(),
205        ]);
206
207        assert_eq!(actual, expected);
208    }
209
210    #[test]
211    fn test_align_right() {
212        let original = Lines(vec![
213            vec!["hello world"].try_into().unwrap(),           // 11 chars
214            vec!["pretty normal testsss"].try_into().unwrap(), // 21 chars
215            vec!["shorts"].try_into().unwrap(),                // 6 chars
216        ]);
217        let component = Aligned::new(
218            Echo(original),
219            HorizontalAlignmentKind::Right,
220            VerticalAlignmentKind::Top,
221        );
222        let dimensions = Dimensions::new(20, 20);
223        let actual = component.draw(dimensions, DrawMode::Normal).unwrap();
224        let expected = Lines(vec![
225            vec![" ".repeat(9).as_ref(), "hello world"]
226                .try_into()
227                .unwrap(),
228            vec!["pretty normal testss"].try_into().unwrap(),
229            vec![" ".repeat(14).as_ref(), "shorts"].try_into().unwrap(),
230        ]);
231
232        assert_eq!(actual, expected);
233    }
234
235    #[test]
236    fn test_align_top() {
237        let original = Lines(vec![
238            vec!["hello world"].try_into().unwrap(),           // 11 chars
239            vec!["pretty normal testsss"].try_into().unwrap(), // 21 chars
240            vec!["shorts"].try_into().unwrap(),                // 6 chars
241        ]);
242        let component = Aligned::new(
243            Echo(original),
244            HorizontalAlignmentKind::Left(false),
245            VerticalAlignmentKind::Top,
246        );
247        let dimensions = Dimensions::new(20, 20);
248        let actual = component.draw(dimensions, DrawMode::Normal).unwrap();
249        let expected = Lines(vec![
250            vec!["hello world"].try_into().unwrap(),
251            vec!["pretty normal testss"].try_into().unwrap(),
252            vec!["shorts"].try_into().unwrap(),
253        ]);
254
255        assert_eq!(actual, expected);
256    }
257
258    #[test]
259    fn test_align_row_center() {
260        let original = Lines(vec![
261            vec!["hello world"].try_into().unwrap(),           // 11 chars
262            vec!["pretty normal testsss"].try_into().unwrap(), // 21 chars
263            vec!["shorts"].try_into().unwrap(),                // 6 chars
264        ]);
265        let component = Aligned::new(
266            Echo(original),
267            HorizontalAlignmentKind::Left(false),
268            VerticalAlignmentKind::Center,
269        );
270        let dimensions = Dimensions::new(20, 10);
271        let actual = component.draw(dimensions, DrawMode::Normal).unwrap();
272        let expected = Lines(vec![
273            Line::default(),
274            Line::default(),
275            Line::default(),
276            vec!["hello world"].try_into().unwrap(),
277            vec!["pretty normal testss"].try_into().unwrap(),
278            vec!["shorts"].try_into().unwrap(),
279            Line::default(),
280            Line::default(),
281            Line::default(),
282            Line::default(),
283        ]);
284
285        assert_eq!(actual, expected);
286    }
287
288    #[test]
289    fn test_align_bottom() {
290        let original = Lines(vec![
291            vec!["hello world"].try_into().unwrap(),           // 11 chars
292            vec!["pretty normal testsss"].try_into().unwrap(), // 21 chars
293            vec!["shorts"].try_into().unwrap(),                // 6 chars
294        ]);
295        let component = Aligned::new(
296            Echo(original),
297            HorizontalAlignmentKind::Left(false),
298            VerticalAlignmentKind::Bottom,
299        );
300        let dimensions = Dimensions::new(20, 10);
301        let actual = component.draw(dimensions, DrawMode::Normal).unwrap();
302        let expected = Lines(vec![
303            Line::default(),
304            Line::default(),
305            Line::default(),
306            Line::default(),
307            Line::default(),
308            Line::default(),
309            Line::default(),
310            vec!["hello world"].try_into().unwrap(),
311            vec!["pretty normal testss"].try_into().unwrap(),
312            vec!["shorts"].try_into().unwrap(),
313        ]);
314
315        assert_eq!(actual, expected);
316    }
317}