fast_rich/
align.rs

1//! Alignment wrapper for renderables.
2
3use crate::console::RenderContext;
4use crate::renderable::{BoxedRenderable, Renderable, Segment};
5use crate::text::{Alignment, Span};
6
7/// Vertical alignment options.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum VerticalAlignment {
10    /// Top-aligned (default)
11    #[default]
12    Top,
13    /// Middle-aligned
14    Middle,
15    /// Bottom-aligned
16    Bottom,
17}
18
19/// A renderable that aligns its child within the available space.
20pub struct Align {
21    child: BoxedRenderable,
22    align: Alignment,
23    vertical: VerticalAlignment,
24    height: Option<usize>,
25    pad: bool,
26}
27
28impl Align {
29    /// Create a new left-aligned wrapper.
30    pub fn left(child: impl Renderable + Send + Sync + 'static) -> Self {
31        Align {
32            child: Box::new(child),
33            align: Alignment::Left,
34            vertical: VerticalAlignment::Top,
35            height: None,
36            pad: true,
37        }
38    }
39
40    /// Create a new center-aligned wrapper.
41    pub fn center(child: impl Renderable + Send + Sync + 'static) -> Self {
42        Align {
43            child: Box::new(child),
44            align: Alignment::Center,
45            vertical: VerticalAlignment::Top,
46            height: None,
47            pad: true,
48        }
49    }
50
51    /// Create a new right-aligned wrapper.
52    pub fn right(child: impl Renderable + Send + Sync + 'static) -> Self {
53        Align {
54            child: Box::new(child),
55            align: Alignment::Right,
56            vertical: VerticalAlignment::Top,
57            height: None,
58            pad: true,
59        }
60    }
61
62    /// Set vertical alignment.
63    pub fn vertical(mut self, vertical: VerticalAlignment) -> Self {
64        self.vertical = vertical;
65        self
66    }
67
68    /// Set height constraint.
69    pub fn height(mut self, height: usize) -> Self {
70        self.height = Some(height);
71        self
72    }
73}
74
75impl Renderable for Align {
76    fn render(&self, context: &RenderContext) -> Vec<Segment> {
77        let segments = self.child.render(context);
78        let width = context.width;
79        let mut aligned_segments = Vec::with_capacity(segments.len());
80
81        for segment in segments {
82            let mut line = segment.spans.clone();
83            let line_width: usize = line.iter().map(|s| s.width()).sum();
84
85            if line_width < width {
86                let padding = width - line_width;
87                match self.align {
88                    Alignment::Left => {
89                        if self.pad {
90                            line.push(Span::raw(" ".repeat(padding)));
91                        }
92                    }
93                    Alignment::Right => {
94                        let mut new_line = vec![Span::raw(" ".repeat(padding))];
95                        new_line.extend(line);
96                        line = new_line;
97                    }
98                    Alignment::Center => {
99                        let left_pad = padding / 2;
100                        let right_pad = padding - left_pad;
101                        let mut new_line = vec![Span::raw(" ".repeat(left_pad))];
102                        new_line.extend(line);
103                        if self.pad {
104                            new_line.push(Span::raw(" ".repeat(right_pad)));
105                        }
106                        line = new_line;
107                    }
108                }
109            }
110
111            // Reconstruct segment
112            if segment.newline {
113                aligned_segments.push(Segment::line(line));
114            } else {
115                aligned_segments.push(Segment::new(line));
116            }
117        }
118
119        // Handle vertical alignment if height is set (TODO: Context height?)
120        // For now only if explicit height is set.
121        if let Some(target_height) = self.height {
122            let current_height = aligned_segments.len();
123            if current_height < target_height {
124                let diff = target_height - current_height;
125                match self.vertical {
126                    VerticalAlignment::Top => {
127                        // Add empty lines at bottom
128                        for _ in 0..diff {
129                            aligned_segments
130                                .push(Segment::line(vec![Span::raw(" ".repeat(width))]));
131                        }
132                    }
133                    VerticalAlignment::Bottom => {
134                        let mut new_segments = Vec::with_capacity(target_height);
135                        for _ in 0..diff {
136                            new_segments.push(Segment::line(vec![Span::raw(" ".repeat(width))]));
137                        }
138                        new_segments.extend(aligned_segments);
139                        aligned_segments = new_segments;
140                    }
141                    VerticalAlignment::Middle => {
142                        let top_pad = diff / 2;
143                        let bottom_pad = diff - top_pad;
144                        let mut new_segments = Vec::with_capacity(target_height);
145                        for _ in 0..top_pad {
146                            new_segments.push(Segment::line(vec![Span::raw(" ".repeat(width))]));
147                        }
148                        new_segments.extend(aligned_segments);
149                        for _ in 0..bottom_pad {
150                            new_segments.push(Segment::line(vec![Span::raw(" ".repeat(width))]));
151                        }
152                        aligned_segments = new_segments;
153                    }
154                }
155            }
156        }
157
158        aligned_segments
159    }
160}