1use std::io::Stdout;
20
21use crate::console::ConsoleOptions;
22use crate::measure::Measurement;
23use crate::rule::AlignMethod;
24use crate::segment::{Segment, Segments};
25use crate::style::Style;
26use crate::{Console, Renderable};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum VerticalAlignMethod {
35 #[default]
37 Top,
38 Middle,
40 Bottom,
42}
43
44impl VerticalAlignMethod {
45 pub fn parse(s: &str) -> Option<Self> {
47 match s.to_lowercase().as_str() {
48 "top" => Some(VerticalAlignMethod::Top),
49 "middle" => Some(VerticalAlignMethod::Middle),
50 "bottom" => Some(VerticalAlignMethod::Bottom),
51 _ => None,
52 }
53 }
54}
55
56pub struct Align {
88 renderable: Box<dyn Renderable + Send + Sync>,
90 align: AlignMethod,
92 vertical: Option<VerticalAlignMethod>,
94 style: Style,
96 pad: bool,
98 width: Option<usize>,
100 height: Option<usize>,
102}
103
104impl std::fmt::Debug for Align {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 f.debug_struct("Align")
107 .field("align", &self.align)
108 .field("vertical", &self.vertical)
109 .field("style", &self.style)
110 .field("pad", &self.pad)
111 .field("width", &self.width)
112 .field("height", &self.height)
113 .finish_non_exhaustive()
114 }
115}
116
117impl Align {
118 pub fn new(renderable: Box<dyn Renderable + Send + Sync>, align: AlignMethod) -> Self {
134 Align {
135 renderable,
136 align,
137 vertical: None,
138 style: Style::default(),
139 pad: true,
140 width: None,
141 height: None,
142 }
143 }
144
145 pub fn left(renderable: Box<dyn Renderable + Send + Sync>) -> Self {
156 Self::new(renderable, AlignMethod::Left)
157 }
158
159 pub fn center(renderable: Box<dyn Renderable + Send + Sync>) -> Self {
170 Self::new(renderable, AlignMethod::Center)
171 }
172
173 pub fn right(renderable: Box<dyn Renderable + Send + Sync>) -> Self {
184 Self::new(renderable, AlignMethod::Right)
185 }
186
187 pub fn with_style(mut self, style: Style) -> Self {
192 self.style = style;
193 self
194 }
195
196 pub fn with_vertical(mut self, vertical: VerticalAlignMethod) -> Self {
201 self.vertical = Some(vertical);
202 self
203 }
204
205 pub fn with_pad(mut self, pad: bool) -> Self {
210 self.pad = pad;
211 self
212 }
213
214 pub fn with_width(mut self, width: usize) -> Self {
219 self.width = Some(width);
220 self
221 }
222
223 pub fn with_height(mut self, height: usize) -> Self {
228 self.height = Some(height);
229 self
230 }
231
232 pub fn align(&self) -> AlignMethod {
234 self.align
235 }
236
237 pub fn vertical(&self) -> Option<VerticalAlignMethod> {
239 self.vertical
240 }
241
242 pub fn style(&self) -> Style {
244 self.style
245 }
246
247 pub fn pad(&self) -> bool {
249 self.pad
250 }
251
252 pub fn width(&self) -> Option<usize> {
254 self.width
255 }
256
257 pub fn height(&self) -> Option<usize> {
259 self.height
260 }
261}
262
263impl Renderable for Align {
264 fn render(&self, console: &Console<Stdout>, options: &ConsoleOptions) -> Segments {
265 let mut result = Segments::new();
266
267 let available_width = self.width.unwrap_or(options.max_width);
269
270 let inner_measurement = self.renderable.measure(console, options);
272 let content_width = inner_measurement.maximum.min(available_width);
273
274 let mut render_options = options.update_width(content_width);
277 render_options.height = None;
278
279 let lines = console.render_lines(
281 self.renderable.as_ref(),
282 Some(&render_options),
283 None, true, false, );
287
288 let (rendered_width, rendered_height) = Segment::get_shape(&lines);
290
291 let lines = Segment::set_shape(&lines, rendered_width, Some(rendered_height), None, false);
293
294 let new_line = Segment::line();
295 let excess_space = available_width.saturating_sub(rendered_width);
296
297 let generate_segments = |result: &mut Segments| {
299 if excess_space == 0 {
300 for line in &lines {
302 for segment in line {
303 result.push(segment.clone());
304 }
305 result.push(new_line.clone());
306 }
307 } else {
308 match self.align {
309 AlignMethod::Left => {
310 let pad_segment = if self.pad {
312 Some(Segment::styled(" ".repeat(excess_space), self.style))
313 } else {
314 None
315 };
316 for line in &lines {
317 for segment in line {
318 result.push(segment.clone());
319 }
320 if let Some(ref pad) = pad_segment {
321 result.push(pad.clone());
322 }
323 result.push(new_line.clone());
324 }
325 }
326 AlignMethod::Center => {
327 let left_padding = excess_space / 2;
329 let right_padding = excess_space - left_padding;
330
331 let left_segment = if left_padding > 0 {
332 Some(Segment::styled(" ".repeat(left_padding), self.style))
333 } else {
334 None
335 };
336 let right_segment = if self.pad && right_padding > 0 {
337 Some(Segment::styled(" ".repeat(right_padding), self.style))
338 } else {
339 None
340 };
341
342 for line in &lines {
343 if let Some(ref left) = left_segment {
344 result.push(left.clone());
345 }
346 for segment in line {
347 result.push(segment.clone());
348 }
349 if let Some(ref right) = right_segment {
350 result.push(right.clone());
351 }
352 result.push(new_line.clone());
353 }
354 }
355 AlignMethod::Right => {
356 let left_segment = Segment::styled(" ".repeat(excess_space), self.style);
358
359 for line in &lines {
360 result.push(left_segment.clone());
361 for segment in line {
362 result.push(segment.clone());
363 }
364 result.push(new_line.clone());
365 }
366 }
367 }
368 }
369 };
370
371 let vertical_height = self.height.or(options.height);
373
374 if let (Some(v_align), Some(v_height)) = (self.vertical, vertical_height) {
375 if v_height > rendered_height {
376 let blank_width = if self.pad { available_width } else { 0 };
378 let blank_line = if blank_width > 0 {
379 Segment::styled(format!("{}\n", " ".repeat(blank_width)), self.style)
380 } else {
381 Segment::new("\n")
382 };
383
384 let blank_lines = |result: &mut Segments, count: usize| {
385 for _ in 0..count {
386 result.push(blank_line.clone());
387 }
388 };
389
390 match v_align {
391 VerticalAlignMethod::Top => {
392 generate_segments(&mut result);
393 let bottom_space = v_height.saturating_sub(rendered_height);
394 blank_lines(&mut result, bottom_space);
395 }
396 VerticalAlignMethod::Middle => {
397 let top_space = (v_height.saturating_sub(rendered_height)) / 2;
398 let bottom_space = v_height
399 .saturating_sub(top_space)
400 .saturating_sub(rendered_height);
401 blank_lines(&mut result, top_space);
402 generate_segments(&mut result);
403 blank_lines(&mut result, bottom_space);
404 }
405 VerticalAlignMethod::Bottom => {
406 let top_space = v_height.saturating_sub(rendered_height);
407 blank_lines(&mut result, top_space);
408 generate_segments(&mut result);
409 }
410 }
411 } else {
412 generate_segments(&mut result);
414 }
415 } else {
416 generate_segments(&mut result);
418 }
419
420 if self.style != Style::default() {
422 Segment::apply_style_to_segments(result, Some(self.style), None)
423 } else {
424 result
425 }
426 }
427
428 fn measure(&self, console: &Console<Stdout>, options: &ConsoleOptions) -> Measurement {
429 self.renderable.measure(console, options)
431 }
432}
433
434#[cfg(test)]
439mod tests {
440 use super::*;
441 use crate::cells::cell_len;
442 use crate::text::Text;
443
444 #[test]
447 fn test_vertical_align_method_parse() {
448 assert_eq!(
449 VerticalAlignMethod::parse("top"),
450 Some(VerticalAlignMethod::Top)
451 );
452 assert_eq!(
453 VerticalAlignMethod::parse("TOP"),
454 Some(VerticalAlignMethod::Top)
455 );
456 assert_eq!(
457 VerticalAlignMethod::parse("middle"),
458 Some(VerticalAlignMethod::Middle)
459 );
460 assert_eq!(
461 VerticalAlignMethod::parse("MIDDLE"),
462 Some(VerticalAlignMethod::Middle)
463 );
464 assert_eq!(
465 VerticalAlignMethod::parse("bottom"),
466 Some(VerticalAlignMethod::Bottom)
467 );
468 assert_eq!(
469 VerticalAlignMethod::parse("BOTTOM"),
470 Some(VerticalAlignMethod::Bottom)
471 );
472 assert_eq!(VerticalAlignMethod::parse("invalid"), None);
473 }
474
475 #[test]
476 fn test_vertical_align_method_default() {
477 assert_eq!(VerticalAlignMethod::default(), VerticalAlignMethod::Top);
478 }
479
480 #[test]
483 fn test_align_new() {
484 let text = Text::plain("Hello");
485 let align = Align::new(Box::new(text), AlignMethod::Center);
486 assert_eq!(align.align(), AlignMethod::Center);
487 assert_eq!(align.vertical(), None);
488 assert!(align.pad());
489 assert_eq!(align.width(), None);
490 assert_eq!(align.height(), None);
491 }
492
493 #[test]
494 fn test_align_left() {
495 let text = Text::plain("Hello");
496 let align = Align::left(Box::new(text));
497 assert_eq!(align.align(), AlignMethod::Left);
498 }
499
500 #[test]
501 fn test_align_center() {
502 let text = Text::plain("Hello");
503 let align = Align::center(Box::new(text));
504 assert_eq!(align.align(), AlignMethod::Center);
505 }
506
507 #[test]
508 fn test_align_right() {
509 let text = Text::plain("Hello");
510 let align = Align::right(Box::new(text));
511 assert_eq!(align.align(), AlignMethod::Right);
512 }
513
514 #[test]
515 fn test_align_with_style() {
516 let text = Text::plain("Hello");
517 let style = Style::new().with_bold(true);
518 let align = Align::center(Box::new(text)).with_style(style);
519 assert_eq!(align.style().bold, Some(true));
520 }
521
522 #[test]
523 fn test_align_with_vertical() {
524 let text = Text::plain("Hello");
525 let align = Align::center(Box::new(text)).with_vertical(VerticalAlignMethod::Middle);
526 assert_eq!(align.vertical(), Some(VerticalAlignMethod::Middle));
527 }
528
529 #[test]
530 fn test_align_with_pad() {
531 let text = Text::plain("Hello");
532 let align = Align::center(Box::new(text)).with_pad(false);
533 assert!(!align.pad());
534 }
535
536 #[test]
537 fn test_align_with_width() {
538 let text = Text::plain("Hello");
539 let align = Align::center(Box::new(text)).with_width(40);
540 assert_eq!(align.width(), Some(40));
541 }
542
543 #[test]
544 fn test_align_with_height() {
545 let text = Text::plain("Hello");
546 let align = Align::center(Box::new(text)).with_height(10);
547 assert_eq!(align.height(), Some(10));
548 }
549
550 #[test]
553 fn test_align_render_left() {
554 let text = Text::plain("Hello");
555 let align = Align::left(Box::new(text));
556 let console = Console::with_options(ConsoleOptions {
557 max_width: 20,
558 ..Default::default()
559 });
560 let options = console.options().clone();
561
562 let segments = align.render(&console, &options);
563 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
564
565 assert!(output.starts_with("Hello"));
567 assert!(output.ends_with('\n'));
568 let line = output.lines().next().unwrap();
570 assert_eq!(cell_len(line), 20);
571 }
572
573 #[test]
574 fn test_align_render_center() {
575 let text = Text::plain("Hello"); let align = Align::center(Box::new(text));
577 let console = Console::with_options(ConsoleOptions {
578 max_width: 15,
579 ..Default::default()
580 });
581 let options = console.options().clone();
582
583 let segments = align.render(&console, &options);
584 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
585 let line = output.lines().next().unwrap();
586
587 assert!(line.starts_with(" ")); assert!(line.contains("Hello"));
591 assert_eq!(cell_len(line), 15);
592 }
593
594 #[test]
595 fn test_align_render_right() {
596 let text = Text::plain("Hello"); let align = Align::right(Box::new(text));
598 let console = Console::with_options(ConsoleOptions {
599 max_width: 20,
600 ..Default::default()
601 });
602 let options = console.options().clone();
603
604 let segments = align.render(&console, &options);
605 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
606 let line = output.lines().next().unwrap();
607
608 assert!(line.starts_with(" ")); assert!(line.ends_with("Hello"));
611 assert_eq!(cell_len(line), 20);
612 }
613
614 #[test]
615 fn test_align_render_no_pad() {
616 let text = Text::plain("Hello");
617 let align = Align::center(Box::new(text)).with_pad(false);
618 let console = Console::with_options(ConsoleOptions {
619 max_width: 20,
620 ..Default::default()
621 });
622 let options = console.options().clone();
623
624 let segments = align.render(&console, &options);
625 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
626 let line = output.lines().next().unwrap();
627
628 assert!(line.contains("Hello"));
631 assert!(cell_len(line) < 20);
633 }
634
635 #[test]
636 fn test_align_render_with_width() {
637 let text = Text::plain("Hello");
638 let align = Align::center(Box::new(text)).with_width(10);
639 let console = Console::with_options(ConsoleOptions {
640 max_width: 50, ..Default::default()
642 });
643 let options = console.options().clone();
644
645 let segments = align.render(&console, &options);
646 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
647 let line = output.lines().next().unwrap();
648
649 assert_eq!(cell_len(line), 10);
651 }
652
653 #[test]
654 fn test_align_render_exact_fit() {
655 let text = Text::plain("Hello"); let align = Align::center(Box::new(text));
657 let console = Console::with_options(ConsoleOptions {
658 max_width: 5, ..Default::default()
660 });
661 let options = console.options().clone();
662
663 let segments = align.render(&console, &options);
664 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
665 let line = output.lines().next().unwrap();
666
667 assert_eq!(line, "Hello");
669 }
670
671 #[test]
674 fn test_align_render_vertical_top() {
675 let text = Text::plain("X");
676 let align = Align::center(Box::new(text))
677 .with_vertical(VerticalAlignMethod::Top)
678 .with_height(3)
679 .with_width(5);
680 let console = Console::new();
681 let options = ConsoleOptions {
682 max_width: 5,
683 ..Default::default()
684 };
685
686 let segments = align.render(&console, &options);
687 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
688 let lines: Vec<&str> = output.lines().collect();
689
690 assert_eq!(lines.len(), 3);
692 assert!(lines[0].contains("X")); }
694
695 #[test]
696 fn test_align_render_vertical_middle() {
697 let text = Text::plain("X");
698 let align = Align::center(Box::new(text))
699 .with_vertical(VerticalAlignMethod::Middle)
700 .with_height(5)
701 .with_width(3);
702 let console = Console::new();
703 let options = ConsoleOptions {
704 max_width: 3,
705 ..Default::default()
706 };
707
708 let segments = align.render(&console, &options);
709 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
710 let lines: Vec<&str> = output.lines().collect();
711
712 assert_eq!(lines.len(), 5);
714 assert!(lines[2].contains("X")); }
717
718 #[test]
719 fn test_align_render_vertical_bottom() {
720 let text = Text::plain("X");
721 let align = Align::center(Box::new(text))
722 .with_vertical(VerticalAlignMethod::Bottom)
723 .with_height(3)
724 .with_width(5);
725 let console = Console::new();
726 let options = ConsoleOptions {
727 max_width: 5,
728 ..Default::default()
729 };
730
731 let segments = align.render(&console, &options);
732 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
733 let lines: Vec<&str> = output.lines().collect();
734
735 assert_eq!(lines.len(), 3);
737 assert!(lines[2].contains("X")); }
739
740 #[test]
743 fn test_align_measure() {
744 let text = Text::plain("Hello World"); let align = Align::center(Box::new(text));
746 let console = Console::new();
747 let options = ConsoleOptions::default();
748
749 let measurement = align.measure(&console, &options);
750 assert_eq!(measurement.minimum, 5);
752 assert_eq!(measurement.maximum, 11);
753 }
754
755 #[test]
758 fn test_align_is_send_sync() {
759 fn assert_send<T: Send>() {}
760 fn assert_sync<T: Sync>() {}
761 assert_send::<Align>();
762 assert_sync::<Align>();
763 }
764
765 #[test]
766 fn test_vertical_align_method_is_send_sync() {
767 fn assert_send<T: Send>() {}
768 fn assert_sync<T: Sync>() {}
769 assert_send::<VerticalAlignMethod>();
770 assert_sync::<VerticalAlignMethod>();
771 }
772
773 #[test]
776 fn test_align_debug() {
777 let text = Text::plain("Hello");
778 let align = Align::center(Box::new(text))
779 .with_vertical(VerticalAlignMethod::Middle)
780 .with_height(10);
781 let debug_str = format!("{:?}", align);
782 assert!(debug_str.contains("Align"));
783 assert!(debug_str.contains("Center"));
784 assert!(debug_str.contains("Middle"));
785 }
786
787 #[test]
790 fn test_align_cjk_content() {
791 let text = Text::plain("你好"); let align = Align::center(Box::new(text));
793 let console = Console::with_options(ConsoleOptions {
794 max_width: 10,
795 ..Default::default()
796 });
797 let options = console.options().clone();
798
799 let segments = align.render(&console, &options);
800 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
801 let line = output.lines().next().unwrap();
802
803 assert!(line.contains("你好"));
804 assert_eq!(cell_len(line), 10);
805 }
806
807 #[test]
808 fn test_align_emoji_content() {
809 let text = Text::plain("Hi!"); let align = Align::right(Box::new(text));
811 let console = Console::with_options(ConsoleOptions {
812 max_width: 10,
813 ..Default::default()
814 });
815 let options = console.options().clone();
816
817 let segments = align.render(&console, &options);
818 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
819 let line = output.lines().next().unwrap();
820
821 assert!(line.ends_with("Hi!"));
822 assert_eq!(cell_len(line), 10);
823 }
824}