1use crate::console::{Console, ConsoleOptions};
9use crate::measure::Measurement;
10use crate::protocol::Renderable;
11use crate::segment::Segment;
12use crate::style::StyleType;
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<StyleType>,
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: impl Into<StyleType>) -> Self {
79 self.style = Some(style.into());
80 self
81 }
82
83 pub fn vertical(mut self, vertical: VerticalAlign) -> Self {
86 self.vertical = Some(vertical);
87 self
88 }
89
90 pub fn pad(mut self, pad: bool) -> Self {
92 self.pad = pad;
93 self
94 }
95
96 pub fn width(mut self, width: usize) -> Self {
98 self.width = Some(width);
99 self
100 }
101
102 pub fn height(mut self, height: usize) -> Self {
104 self.height = Some(height);
105 self
106 }
107}
108
109impl Align {
110 pub fn render_child(
114 child: &dyn Renderable,
115 align: HorizontalAlign,
116 console: &Console,
117 options: &ConsoleOptions,
118 ) -> Vec<Segment> {
119 AlignSpec {
120 align,
121 style: None,
122 vertical: None,
123 pad: true,
124 width: None,
125 height: None,
126 }
127 .render(child, console, options)
128 }
129
130 fn spec(&self) -> AlignSpec {
131 AlignSpec {
132 align: self.align,
133 style: self.style.clone(),
134 vertical: self.vertical,
135 pad: self.pad,
136 width: self.width,
137 height: self.height,
138 }
139 }
140}
141
142struct AlignSpec {
144 align: HorizontalAlign,
145 style: Option<StyleType>,
146 vertical: Option<VerticalAlign>,
147 pad: bool,
148 width: Option<usize>,
149 height: Option<usize>,
150}
151
152impl AlignSpec {
153 fn render(
154 &self,
155 child: &dyn Renderable,
156 console: &Console,
157 options: &ConsoleOptions,
158 ) -> Vec<Segment> {
159 let measured = Measurement::get(console, options, child).maximum;
163 let block_width = match self.width {
164 Some(width) => measured.min(width),
165 None => measured,
166 };
167 let mut child_options = options.update_width(block_width.min(options.max_width));
168 child_options.height = None;
169 let lines = console.render_lines(child, &child_options, false);
170 let width = lines
171 .iter()
172 .map(|line| line.iter().map(Segment::cell_length).sum::<usize>())
173 .max()
174 .unwrap_or(0);
175 let height = lines.len();
176 let lines: Vec<Vec<Segment>> = lines
177 .iter()
178 .map(|line| Segment::adjust_line_length(line, width, None))
179 .collect();
180
181 let excess = options.max_width.saturating_sub(width);
182 let style = self
184 .style
185 .as_ref()
186 .map(|style| console.get_style(style).unwrap_or_default());
187 let pad_style = Some(style.clone().unwrap_or_default());
188 let (left_pad, right_pad) = match self.align {
189 HorizontalAlign::Left => (0, if self.pad { excess } else { 0 }),
190 HorizontalAlign::Right => (excess, 0),
191 HorizontalAlign::Center => (excess / 2, if self.pad { excess - excess / 2 } else { 0 }),
192 };
193
194 let mut rows: Vec<Vec<Segment>> = Vec::with_capacity(lines.len());
195 for line in lines {
196 let mut row = Vec::new();
197 if left_pad > 0 {
198 row.push(Segment::new(" ".repeat(left_pad), pad_style.clone()));
199 }
200 row.extend(line);
201 if right_pad > 0 {
202 row.push(Segment::new(" ".repeat(right_pad), pad_style.clone()));
203 }
204 rows.push(row);
205 }
206
207 let blank = || {
209 if self.pad {
210 vec![Segment::new(
212 " ".repeat(
213 self.width
214 .filter(|&width| width > 0)
215 .unwrap_or(options.max_width),
216 ),
217 pad_style.clone(),
218 )]
219 } else {
220 Vec::new()
221 }
222 };
223 let total = self.height.filter(|&height| height > 0).or(options.height);
225 if let (Some(vertical), Some(total)) = (self.vertical, total) {
226 let (top, bottom) = match vertical {
227 VerticalAlign::Top => (0, total.saturating_sub(height)),
228 VerticalAlign::Middle => {
229 let top = total.saturating_sub(height) / 2;
230 (top, total.saturating_sub(top + height))
231 }
232 VerticalAlign::Bottom => (total.saturating_sub(height), 0),
233 };
234 let mut shaped = Vec::with_capacity(top + rows.len() + bottom);
235 shaped.extend(std::iter::repeat_with(blank).take(top));
236 shaped.append(&mut rows);
237 shaped.extend(std::iter::repeat_with(blank).take(bottom));
238 rows = shaped;
239 }
240
241 let mut segments = Vec::new();
242 let last = rows.len().saturating_sub(1);
243 for (index, row) in rows.into_iter().enumerate() {
244 segments.extend(row);
245 if index != last {
246 segments.push(Segment::line());
247 }
248 }
249 match style.as_ref().filter(|style| !style.is_null()) {
250 Some(style) => Segment::apply_style(&segments, style),
251 None => segments,
252 }
253 }
254}
255
256impl Renderable for Align {
257 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
258 self.spec().render(self.child.as_ref(), console, options)
259 }
260
261 fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
263 Measurement::get(console, options, self.child.as_ref())
264 }
265
266 fn vertical(&self) -> Option<VerticalAlign> {
268 self.vertical
269 }
270}
271
272pub struct VerticalCenter {
275 child: Box<dyn Renderable>,
276 style: Option<StyleType>,
277}
278
279impl VerticalCenter {
280 pub fn new(child: Box<dyn Renderable>) -> Self {
281 VerticalCenter { child, style: None }
282 }
283
284 pub fn style(mut self, style: impl Into<StyleType>) -> Self {
287 self.style = Some(style.into());
288 self
289 }
290}
291
292impl Renderable for VerticalCenter {
293 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
294 let mut child_options = options.clone();
295 child_options.height = None;
296 let lines = console.render_lines(self.child.as_ref(), &child_options, false);
297 let width = lines
298 .iter()
299 .map(|line| line.iter().map(Segment::cell_length).sum::<usize>())
300 .max()
301 .unwrap_or(0);
302 let height = options
304 .height
305 .filter(|&height| height > 0)
306 .unwrap_or(options.size.height);
307 let top = height.saturating_sub(lines.len()) / 2;
308 let bottom = height.saturating_sub(top + lines.len());
309 let style = self
310 .style
311 .as_ref()
312 .map(|style| console.get_style(style).unwrap_or_default());
313 let blank = || vec![Segment::new(" ".repeat(width), style.clone())];
314 let mut rows: Vec<Vec<Segment>> = Vec::new();
315 rows.extend(std::iter::repeat_with(blank).take(top));
316 rows.extend(lines);
317 rows.extend(std::iter::repeat_with(blank).take(bottom));
318 let mut segments = Vec::new();
319 let last = rows.len().saturating_sub(1);
320 for (index, row) in rows.into_iter().enumerate() {
321 segments.extend(row);
322 if index != last {
323 segments.push(Segment::line());
324 }
325 }
326 segments
327 }
328
329 fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
330 Measurement::get(console, options, self.child.as_ref())
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use crate::color::ColorSystem;
338 use crate::text::Text;
339
340 fn console(width: usize) -> Console {
341 Console::builder()
342 .force_terminal(true)
343 .color_system(Some(ColorSystem::Truecolor))
344 .width(width)
345 .build()
346 }
347
348 #[test]
349 fn center_pads_both_sides() {
350 let out = console(20).render_export(&Align::center(Box::new(Text::new("hi"))));
351 assert_eq!(out, " hi \n");
352 }
353
354 #[test]
355 fn right_pads_left() {
356 let out = console(20).render_export(&Align::right(Box::new(Text::new("hi"))));
357 assert_eq!(out, " hi\n");
358 }
359
360 #[test]
361 fn center_odd_remainder_floors_left() {
362 let out = console(21).render_export(&Align::center(Box::new(Text::new("hi"))));
363 assert_eq!(out, " hi \n");
364 }
365
366 #[test]
367 fn aligns_the_wrapped_block_not_each_line() {
368 let out = console(4).render_export(&Align::right(Box::new(Text::new("abcd ef"))));
371 assert_eq!(out, "abcd\nef \n");
372 let out = console(8).render_export(&Align::right(Box::new(Text::new("abc de"))));
373 assert_eq!(out, " abc de\n");
374 }
375}