Skip to main content

rich_rs/
align.rs

1//! Align: horizontal and vertical alignment wrapper for renderables.
2//!
3//! Align adds space around content to position it within a given width/height.
4//!
5//! # Example
6//!
7//! ```
8//! use rich_rs::{Align, Text};
9//!
10//! // Center-align text
11//! let text = Text::plain("Hello");
12//! let aligned = Align::center(Box::new(text));
13//!
14//! // Right-align with custom width
15//! let text = Text::plain("Right");
16//! let aligned = Align::right(Box::new(text)).with_width(40);
17//! ```
18
19use 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// ============================================================================
29// VerticalAlignMethod
30// ============================================================================
31
32/// Vertical alignment method.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum VerticalAlignMethod {
35    /// Align to the top (default).
36    #[default]
37    Top,
38    /// Align to the middle (vertically centered).
39    Middle,
40    /// Align to the bottom.
41    Bottom,
42}
43
44impl VerticalAlignMethod {
45    /// Parse a vertical alignment method from a string.
46    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
56// ============================================================================
57// Align
58// ============================================================================
59
60/// Align a renderable by adding spaces.
61///
62/// Align wraps a renderable and positions it within the available space
63/// by adding padding. Supports both horizontal (left, center, right) and
64/// vertical (top, middle, bottom) alignment.
65///
66/// # Example
67///
68/// ```
69/// use rich_rs::{Align, Text, Style};
70/// use rich_rs::align::VerticalAlignMethod;
71///
72/// // Simple center alignment
73/// let text = Text::plain("Centered");
74/// let aligned = Align::center(Box::new(text));
75///
76/// // Right-aligned with background style
77/// let text = Text::plain("Right");
78/// let aligned = Align::right(Box::new(text))
79///     .with_style(Style::new().with_bold(true));
80///
81/// // Full alignment with vertical centering
82/// let text = Text::plain("Middle");
83/// let aligned = Align::center(Box::new(text))
84///     .with_vertical(VerticalAlignMethod::Middle)
85///     .with_height(10);
86/// ```
87pub struct Align {
88    /// The wrapped renderable.
89    renderable: Box<dyn Renderable + Send + Sync>,
90    /// Horizontal alignment method.
91    align: AlignMethod,
92    /// Optional vertical alignment.
93    vertical: Option<VerticalAlignMethod>,
94    /// Style for padding spaces.
95    style: Style,
96    /// Whether to pad the right side (default: true).
97    pad: bool,
98    /// Optional fixed width constraint.
99    width: Option<usize>,
100    /// Optional fixed height constraint.
101    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    /// Create a new Align wrapper with the specified alignment.
119    ///
120    /// # Arguments
121    ///
122    /// * `renderable` - The content to align.
123    /// * `align` - Horizontal alignment method.
124    ///
125    /// # Example
126    ///
127    /// ```
128    /// use rich_rs::{Align, Text, AlignMethod};
129    ///
130    /// let text = Text::plain("Hello");
131    /// let aligned = Align::new(Box::new(text), AlignMethod::Center);
132    /// ```
133    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    /// Create a left-aligned wrapper.
146    ///
147    /// # Example
148    ///
149    /// ```
150    /// use rich_rs::{Align, Text};
151    ///
152    /// let text = Text::plain("Left-aligned");
153    /// let aligned = Align::left(Box::new(text));
154    /// ```
155    pub fn left(renderable: Box<dyn Renderable + Send + Sync>) -> Self {
156        Self::new(renderable, AlignMethod::Left)
157    }
158
159    /// Create a center-aligned wrapper.
160    ///
161    /// # Example
162    ///
163    /// ```
164    /// use rich_rs::{Align, Text};
165    ///
166    /// let text = Text::plain("Centered");
167    /// let aligned = Align::center(Box::new(text));
168    /// ```
169    pub fn center(renderable: Box<dyn Renderable + Send + Sync>) -> Self {
170        Self::new(renderable, AlignMethod::Center)
171    }
172
173    /// Create a right-aligned wrapper.
174    ///
175    /// # Example
176    ///
177    /// ```
178    /// use rich_rs::{Align, Text};
179    ///
180    /// let text = Text::plain("Right-aligned");
181    /// let aligned = Align::right(Box::new(text));
182    /// ```
183    pub fn right(renderable: Box<dyn Renderable + Send + Sync>) -> Self {
184        Self::new(renderable, AlignMethod::Right)
185    }
186
187    /// Set the style for padding spaces.
188    ///
189    /// The style is applied to the padding characters (spaces) used for alignment.
190    /// This is useful for setting a background color on the aligned area.
191    pub fn with_style(mut self, style: Style) -> Self {
192        self.style = style;
193        self
194    }
195
196    /// Set the vertical alignment.
197    ///
198    /// Vertical alignment requires a height to be set (either via `with_height()`
199    /// or from `ConsoleOptions::height`).
200    pub fn with_vertical(mut self, vertical: VerticalAlignMethod) -> Self {
201        self.vertical = Some(vertical);
202        self
203    }
204
205    /// Set whether to pad the right side.
206    ///
207    /// When `true` (default), padding is added to the right to fill the width.
208    /// When `false`, only left padding is added for center/right alignment.
209    pub fn with_pad(mut self, pad: bool) -> Self {
210        self.pad = pad;
211        self
212    }
213
214    /// Set a fixed width constraint.
215    ///
216    /// The content will be constrained to this width. If not set, uses
217    /// `ConsoleOptions::max_width`.
218    pub fn with_width(mut self, width: usize) -> Self {
219        self.width = Some(width);
220        self
221    }
222
223    /// Set a fixed height constraint.
224    ///
225    /// Required for vertical alignment. If not set but vertical alignment is
226    /// specified, falls back to `ConsoleOptions::height`.
227    pub fn with_height(mut self, height: usize) -> Self {
228        self.height = Some(height);
229        self
230    }
231
232    /// Get the horizontal alignment.
233    pub fn align(&self) -> AlignMethod {
234        self.align
235    }
236
237    /// Get the vertical alignment.
238    pub fn vertical(&self) -> Option<VerticalAlignMethod> {
239        self.vertical
240    }
241
242    /// Get the style.
243    pub fn style(&self) -> Style {
244        self.style
245    }
246
247    /// Get whether padding is enabled.
248    pub fn pad(&self) -> bool {
249        self.pad
250    }
251
252    /// Get the width constraint.
253    pub fn width(&self) -> Option<usize> {
254        self.width
255    }
256
257    /// Get the height constraint.
258    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        // Determine the available width
268        let available_width = self.width.unwrap_or(options.max_width);
269
270        // Measure the inner content to find its maximum width
271        let inner_measurement = self.renderable.measure(console, options);
272        let content_width = inner_measurement.maximum.min(available_width);
273
274        // Create render options for inner content
275        // Clear height constraint so inner content isn't constrained by Align's height
276        let mut render_options = options.update_width(content_width);
277        render_options.height = None;
278
279        // Render inner content to lines
280        let lines = console.render_lines(
281            self.renderable.as_ref(),
282            Some(&render_options),
283            None,  // Don't apply style to content
284            true,  // pad=true to normalize line widths
285            false, // new_lines=false (we add them ourselves)
286        );
287
288        // Get the shape of the rendered content
289        let (rendered_width, rendered_height) = Segment::get_shape(&lines);
290
291        // Normalize lines to have consistent width
292        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        // Generate horizontally aligned segments
298        let generate_segments = |result: &mut Segments| {
299            if excess_space == 0 {
300                // Exact fit - no alignment needed
301                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                        // Pad on the right
311                        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                        // Pad left and right
328                        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                        // Pad on the left
357                        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        // Handle vertical alignment
372        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                // Create blank line for vertical padding
377                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                // Content fills or exceeds available height
413                generate_segments(&mut result);
414            }
415        } else {
416            // No vertical alignment
417            generate_segments(&mut result);
418        }
419
420        // Apply style to all segments if set
421        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        // Align doesn't change the measurement of the inner content
430        self.renderable.measure(console, options)
431    }
432}
433
434// ============================================================================
435// Tests
436// ============================================================================
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441    use crate::cells::cell_len;
442    use crate::text::Text;
443
444    // ==================== VerticalAlignMethod tests ====================
445
446    #[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    // ==================== Align construction tests ====================
481
482    #[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    // ==================== Align render tests ====================
551
552    #[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        // "Hello" should be followed by padding (15 spaces) + newline
566        assert!(output.starts_with("Hello"));
567        assert!(output.ends_with('\n'));
568        // Total width should be 20 (content + padding)
569        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"); // 5 chars
576        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        // With width 15 and content 5, we have 10 excess
588        // Left padding: 5, right padding: 5
589        assert!(line.starts_with("     ")); // 5 spaces
590        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"); // 5 chars
597        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        // With width 20 and content 5, left padding should be 15
609        assert!(line.starts_with("               ")); // 15 spaces
610        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        // Center with no right padding: left padding only
629        // 20 - 5 = 15 excess, left = 7, no right padding
630        assert!(line.contains("Hello"));
631        // Without right padding, line should be shorter than max_width
632        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, // Larger than specified width
641            ..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        // Should use the specified width of 10, not max_width of 50
650        assert_eq!(cell_len(line), 10);
651    }
652
653    #[test]
654    fn test_align_render_exact_fit() {
655        let text = Text::plain("Hello"); // 5 chars
656        let align = Align::center(Box::new(text));
657        let console = Console::with_options(ConsoleOptions {
658            max_width: 5, // Exact fit
659            ..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        // No padding when content exactly fits
668        assert_eq!(line, "Hello");
669    }
670
671    // ==================== Vertical alignment tests ====================
672
673    #[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        // Should have 3 lines total: content at top, 2 blank lines below
691        assert_eq!(lines.len(), 3);
692        assert!(lines[0].contains("X")); // Content at top
693    }
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        // Should have 5 lines: 2 blank, content, 2 blank (or similar)
713        assert_eq!(lines.len(), 5);
714        // Middle line(s) should contain content
715        assert!(lines[2].contains("X")); // (5-1)/2 = 2
716    }
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        // Should have 3 lines: 2 blank lines above, content at bottom
736        assert_eq!(lines.len(), 3);
737        assert!(lines[2].contains("X")); // Content at bottom
738    }
739
740    // ==================== Measure tests ====================
741
742    #[test]
743    fn test_align_measure() {
744        let text = Text::plain("Hello World"); // min=5 (World), max=11
745        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        // Align passes through the inner measurement
751        assert_eq!(measurement.minimum, 5);
752        assert_eq!(measurement.maximum, 11);
753    }
754
755    // ==================== Send + Sync tests ====================
756
757    #[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    // ==================== Debug tests ====================
774
775    #[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    // ==================== CJK and Unicode tests ====================
788
789    #[test]
790    fn test_align_cjk_content() {
791        let text = Text::plain("你好"); // 4 cells
792        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!"); // 4 cells with emoji (2) + "Hi" (2) - actually "Hi!" is 3
810        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}