1use crate::console::{Console, ConsoleOptions};
10use crate::measure::Measurement;
11use crate::protocol::Renderable;
12use crate::segment::Segment;
13use crate::style::Style;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum HorizontalAlign {
19 Left,
20 #[default]
21 Center,
22 Right,
23}
24
25pub struct Align {
27 child: Box<dyn Renderable>,
28 align: HorizontalAlign,
29}
30
31impl Align {
32 pub fn left(child: Box<dyn Renderable>) -> Self {
34 Align {
35 child,
36 align: HorizontalAlign::Left,
37 }
38 }
39
40 pub fn center(child: Box<dyn Renderable>) -> Self {
42 Align {
43 child,
44 align: HorizontalAlign::Center,
45 }
46 }
47
48 pub fn right(child: Box<dyn Renderable>) -> Self {
50 Align {
51 child,
52 align: HorizontalAlign::Right,
53 }
54 }
55}
56
57impl Renderable for Align {
58 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
59 let block_width = Measurement::get(console, options, self.child.as_ref()).maximum;
63 let mut child_options = options.update_width(block_width);
64 child_options.height = None;
65 let lines = console.render_lines(self.child.as_ref(), &child_options, false);
66 let width = lines
67 .iter()
68 .map(|line| line.iter().map(Segment::cell_length).sum::<usize>())
69 .max()
70 .unwrap_or(0);
71 let lines: Vec<Vec<Segment>> = lines
72 .iter()
73 .map(|line| Segment::adjust_line_length(line, width, None))
74 .collect();
75
76 let excess = options.max_width.saturating_sub(width);
77 let style = Some(Style::new());
78 let (left_pad, right_pad) = match self.align {
79 HorizontalAlign::Left => (0, excess),
80 HorizontalAlign::Right => (excess, 0),
81 HorizontalAlign::Center => (excess / 2, excess - excess / 2),
82 };
83
84 let mut rows: Vec<Vec<Segment>> = Vec::with_capacity(lines.len());
85 for line in lines {
86 let mut row = Vec::new();
87 if left_pad > 0 {
88 row.push(Segment::new(" ".repeat(left_pad), style.clone()));
89 }
90 row.extend(line);
91 if right_pad > 0 {
92 row.push(Segment::new(" ".repeat(right_pad), style.clone()));
93 }
94 rows.push(row);
95 }
96
97 let mut segments = Vec::new();
98 let last = rows.len().saturating_sub(1);
99 for (index, row) in rows.into_iter().enumerate() {
100 segments.extend(row);
101 if index != last {
102 segments.push(Segment::line());
103 }
104 }
105 segments
106 }
107
108 fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
110 Measurement::get(console, options, self.child.as_ref())
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::color::ColorSystem;
118 use crate::text::Text;
119
120 fn console(width: usize) -> Console {
121 Console::builder()
122 .force_terminal(true)
123 .color_system(Some(ColorSystem::Truecolor))
124 .width(width)
125 .build()
126 }
127
128 #[test]
129 fn center_pads_both_sides() {
130 let out = console(20).render_export(&Align::center(Box::new(Text::new("hi"))));
131 assert_eq!(out, " hi \n");
132 }
133
134 #[test]
135 fn right_pads_left() {
136 let out = console(20).render_export(&Align::right(Box::new(Text::new("hi"))));
137 assert_eq!(out, " hi\n");
138 }
139
140 #[test]
141 fn center_odd_remainder_floors_left() {
142 let out = console(21).render_export(&Align::center(Box::new(Text::new("hi"))));
143 assert_eq!(out, " hi \n");
144 }
145
146 #[test]
147 fn aligns_the_wrapped_block_not_each_line() {
148 let out = console(4).render_export(&Align::right(Box::new(Text::new("abcd ef"))));
151 assert_eq!(out, "abcd\nef \n");
152 let out = console(8).render_export(&Align::right(Box::new(Text::new("abc de"))));
153 assert_eq!(out, " abc de\n");
154 }
155}