1use crate::console::{Console, ConsoleOptions};
9use crate::measure::Measurement;
10use crate::protocol::Renderable;
11use crate::segment::Segment;
12use crate::style::Style;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum HorizontalAlign {
18 Left,
19 #[default]
20 Center,
21 Right,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum VerticalAlign {
28 Top,
29 Middle,
30 Bottom,
31}
32
33pub struct Align {
36 child: Box<dyn Renderable>,
37 align: HorizontalAlign,
38 style: Option<Style>,
39 vertical: Option<VerticalAlign>,
40 pad: bool,
41 width: Option<usize>,
42 height: Option<usize>,
43}
44
45impl Align {
46 pub fn new(child: Box<dyn Renderable>, align: HorizontalAlign) -> Self {
49 Align {
50 child,
51 align,
52 style: None,
53 vertical: None,
54 pad: true,
55 width: None,
56 height: None,
57 }
58 }
59
60 pub fn left(child: Box<dyn Renderable>) -> Self {
62 Align::new(child, HorizontalAlign::Left)
63 }
64
65 pub fn center(child: Box<dyn Renderable>) -> Self {
67 Align::new(child, HorizontalAlign::Center)
68 }
69
70 pub fn right(child: Box<dyn Renderable>) -> Self {
72 Align::new(child, HorizontalAlign::Right)
73 }
74
75 pub fn style(mut self, style: Style) -> Self {
78 self.style = Some(style);
79 self
80 }
81
82 pub fn vertical(mut self, vertical: VerticalAlign) -> Self {
85 self.vertical = Some(vertical);
86 self
87 }
88
89 pub fn pad(mut self, pad: bool) -> Self {
91 self.pad = pad;
92 self
93 }
94
95 pub fn width(mut self, width: usize) -> Self {
97 self.width = Some(width);
98 self
99 }
100
101 pub fn height(mut self, height: usize) -> Self {
103 self.height = Some(height);
104 self
105 }
106}
107
108impl Align {
109 pub fn render_child(
113 child: &dyn Renderable,
114 align: HorizontalAlign,
115 console: &Console,
116 options: &ConsoleOptions,
117 ) -> Vec<Segment> {
118 AlignSpec {
119 align,
120 style: None,
121 vertical: None,
122 pad: true,
123 width: None,
124 height: None,
125 }
126 .render(child, console, options)
127 }
128
129 fn spec(&self) -> AlignSpec {
130 AlignSpec {
131 align: self.align,
132 style: self.style.clone(),
133 vertical: self.vertical,
134 pad: self.pad,
135 width: self.width,
136 height: self.height,
137 }
138 }
139}
140
141struct AlignSpec {
143 align: HorizontalAlign,
144 style: Option<Style>,
145 vertical: Option<VerticalAlign>,
146 pad: bool,
147 width: Option<usize>,
148 height: Option<usize>,
149}
150
151impl AlignSpec {
152 fn render(
153 &self,
154 child: &dyn Renderable,
155 console: &Console,
156 options: &ConsoleOptions,
157 ) -> Vec<Segment> {
158 let measured = Measurement::get(console, options, child).maximum;
162 let block_width = match self.width {
163 Some(width) => measured.min(width),
164 None => measured,
165 };
166 let mut child_options = options.update_width(block_width.min(options.max_width));
167 child_options.height = None;
168 let lines = console.render_lines(child, &child_options, false);
169 let width = lines
170 .iter()
171 .map(|line| line.iter().map(Segment::cell_length).sum::<usize>())
172 .max()
173 .unwrap_or(0);
174 let height = lines.len();
175 let lines: Vec<Vec<Segment>> = lines
176 .iter()
177 .map(|line| Segment::adjust_line_length(line, width, None))
178 .collect();
179
180 let excess = options.max_width.saturating_sub(width);
181 let pad_style = Some(self.style.clone().unwrap_or_default());
182 let (left_pad, right_pad) = match self.align {
183 HorizontalAlign::Left => (0, if self.pad { excess } else { 0 }),
184 HorizontalAlign::Right => (excess, 0),
185 HorizontalAlign::Center => (excess / 2, if self.pad { excess - excess / 2 } else { 0 }),
186 };
187
188 let mut rows: Vec<Vec<Segment>> = Vec::with_capacity(lines.len());
189 for line in lines {
190 let mut row = Vec::new();
191 if left_pad > 0 {
192 row.push(Segment::new(" ".repeat(left_pad), pad_style.clone()));
193 }
194 row.extend(line);
195 if right_pad > 0 {
196 row.push(Segment::new(" ".repeat(right_pad), pad_style.clone()));
197 }
198 rows.push(row);
199 }
200
201 let blank = || {
203 if self.pad {
204 vec![Segment::new(
205 " ".repeat(self.width.unwrap_or(options.max_width)),
206 pad_style.clone(),
207 )]
208 } else {
209 Vec::new()
210 }
211 };
212 if let (Some(vertical), Some(total)) = (self.vertical, self.height.or(options.height)) {
213 let (top, bottom) = match vertical {
214 VerticalAlign::Top => (0, total.saturating_sub(height)),
215 VerticalAlign::Middle => {
216 let top = total.saturating_sub(height) / 2;
217 (top, total.saturating_sub(top + height))
218 }
219 VerticalAlign::Bottom => (total.saturating_sub(height), 0),
220 };
221 let mut shaped = Vec::with_capacity(top + rows.len() + bottom);
222 shaped.extend(std::iter::repeat_with(blank).take(top));
223 shaped.append(&mut rows);
224 shaped.extend(std::iter::repeat_with(blank).take(bottom));
225 rows = shaped;
226 }
227
228 let mut segments = Vec::new();
229 let last = rows.len().saturating_sub(1);
230 for (index, row) in rows.into_iter().enumerate() {
231 segments.extend(row);
232 if index != last {
233 segments.push(Segment::line());
234 }
235 }
236 match self.style.as_ref().filter(|style| !style.is_null()) {
237 Some(style) => Segment::apply_style(&segments, style),
238 None => segments,
239 }
240 }
241}
242
243impl Renderable for Align {
244 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
245 self.spec().render(self.child.as_ref(), console, options)
246 }
247
248 fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
250 Measurement::get(console, options, self.child.as_ref())
251 }
252
253 fn vertical(&self) -> Option<VerticalAlign> {
255 self.vertical
256 }
257}
258
259pub struct VerticalCenter {
262 child: Box<dyn Renderable>,
263 style: Option<Style>,
264}
265
266impl VerticalCenter {
267 pub fn new(child: Box<dyn Renderable>) -> Self {
268 VerticalCenter { child, style: None }
269 }
270
271 pub fn style(mut self, style: Style) -> Self {
273 self.style = Some(style);
274 self
275 }
276}
277
278impl Renderable for VerticalCenter {
279 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
280 let mut child_options = options.clone();
281 child_options.height = None;
282 let lines = console.render_lines(self.child.as_ref(), &child_options, false);
283 let width = lines
284 .iter()
285 .map(|line| line.iter().map(Segment::cell_length).sum::<usize>())
286 .max()
287 .unwrap_or(0);
288 let height = options.height.unwrap_or(options.size.height);
289 let top = height.saturating_sub(lines.len()) / 2;
290 let bottom = height.saturating_sub(top + lines.len());
291 let blank = || vec![Segment::new(" ".repeat(width), self.style.clone())];
292 let mut rows: Vec<Vec<Segment>> = Vec::new();
293 rows.extend(std::iter::repeat_with(blank).take(top));
294 rows.extend(lines);
295 rows.extend(std::iter::repeat_with(blank).take(bottom));
296 let mut segments = Vec::new();
297 let last = rows.len().saturating_sub(1);
298 for (index, row) in rows.into_iter().enumerate() {
299 segments.extend(row);
300 if index != last {
301 segments.push(Segment::line());
302 }
303 }
304 segments
305 }
306
307 fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
308 Measurement::get(console, options, self.child.as_ref())
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315 use crate::color::ColorSystem;
316 use crate::text::Text;
317
318 fn console(width: usize) -> Console {
319 Console::builder()
320 .force_terminal(true)
321 .color_system(Some(ColorSystem::Truecolor))
322 .width(width)
323 .build()
324 }
325
326 #[test]
327 fn center_pads_both_sides() {
328 let out = console(20).render_export(&Align::center(Box::new(Text::new("hi"))));
329 assert_eq!(out, " hi \n");
330 }
331
332 #[test]
333 fn right_pads_left() {
334 let out = console(20).render_export(&Align::right(Box::new(Text::new("hi"))));
335 assert_eq!(out, " hi\n");
336 }
337
338 #[test]
339 fn center_odd_remainder_floors_left() {
340 let out = console(21).render_export(&Align::center(Box::new(Text::new("hi"))));
341 assert_eq!(out, " hi \n");
342 }
343
344 #[test]
345 fn aligns_the_wrapped_block_not_each_line() {
346 let out = console(4).render_export(&Align::right(Box::new(Text::new("abcd ef"))));
349 assert_eq!(out, "abcd\nef \n");
350 let out = console(8).render_export(&Align::right(Box::new(Text::new("abc de"))));
351 assert_eq!(out, " abc de\n");
352 }
353}