1use std::fmt;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum AlignMethod {
14 Left,
15 Center,
16 Right,
17 Full, }
19
20impl AlignMethod {
21 pub fn align_text(&self, text: &str, width: usize) -> String {
23 let text_width = unicode_width::UnicodeWidthStr::width(text);
24 if text_width >= width {
25 return text.to_string();
26 }
27 let padding = width - text_width;
28 match self {
29 Self::Left => format!("{}{}", text, " ".repeat(padding)),
30 Self::Right => format!("{}{}", " ".repeat(padding), text),
31 Self::Center => {
32 let left = padding / 2;
33 let right = padding - left;
34 format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
35 }
36 Self::Full => {
37 let words: Vec<&str> = text.split_whitespace().collect();
39 if words.len() <= 1 {
40 return format!("{}{}", text, " ".repeat(padding));
41 }
42 let word_chars: usize = words.iter().map(|w| w.chars().count()).sum();
43 let total_gaps = width - word_chars;
44 let gap_count = words.len() - 1;
45 let gap_size = total_gaps / gap_count;
46 let extra = total_gaps % gap_count;
47 let mut out = String::new();
48 for (i, word) in words.iter().enumerate() {
49 out.push_str(word);
50 if i < gap_count {
51 let spaces = gap_size + if i < extra { 1 } else { 0 };
52 out.push_str(&" ".repeat(spaces));
53 }
54 }
55 out
56 }
57 }
58 }
59
60 pub fn from_str(s: &str) -> Self {
61 match s {
62 "left" | "default" => Self::Left,
63 "center" => Self::Center,
64 "right" => Self::Right,
65 "full" => Self::Full,
66 _ => Self::Left,
67 }
68 }
69}
70
71impl fmt::Display for AlignMethod {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 match self {
74 Self::Left => write!(f, "left"),
75 Self::Center => write!(f, "center"),
76 Self::Right => write!(f, "right"),
77 Self::Full => write!(f, "full"),
78 }
79 }
80}
81
82impl Default for AlignMethod {
83 fn default() -> Self {
84 Self::Left
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94pub enum VerticalAlignMethod {
95 Top,
96 Middle,
97 Bottom,
98}
99
100impl VerticalAlignMethod {
101 pub fn from_str(s: &str) -> Self {
102 match s {
103 "top" => Self::Top,
104 "middle" => Self::Middle,
105 "bottom" => Self::Bottom,
106 _ => Self::Top,
107 }
108 }
109}
110
111impl fmt::Display for VerticalAlignMethod {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 match self {
114 Self::Top => write!(f, "top"),
115 Self::Middle => write!(f, "middle"),
116 Self::Bottom => write!(f, "bottom"),
117 }
118 }
119}
120
121impl Default for VerticalAlignMethod {
122 fn default() -> Self {
123 Self::Top
124 }
125}
126
127use crate::console::{ConsoleOptions, RenderResult};
132use crate::segment::Segment;
133
134#[derive(Debug, Clone)]
136pub struct Align<T: crate::console::Renderable> {
137 pub renderable: T,
138 pub align: AlignMethod,
139 pub vertical: VerticalAlignMethod,
140 pub width: Option<usize>,
141 pub height: Option<usize>,
142}
143
144impl<T: crate::console::Renderable> Align<T> {
145 pub fn new(renderable: T) -> Self {
146 Self {
147 renderable,
148 align: AlignMethod::Left,
149 vertical: VerticalAlignMethod::Top,
150 width: None,
151 height: None,
152 }
153 }
154
155 pub fn align(mut self, align: AlignMethod) -> Self {
156 self.align = align;
157 self
158 }
159
160 pub fn vertical(mut self, vertical: VerticalAlignMethod) -> Self {
161 self.vertical = vertical;
162 self
163 }
164
165 pub fn center(renderable: T) -> Self {
166 Self::new(renderable).align(AlignMethod::Center)
167 }
168
169 pub fn middle(renderable: T) -> Self {
170 Self::new(renderable).vertical(VerticalAlignMethod::Middle)
171 }
172}
173
174impl<T: crate::console::Renderable + Clone> crate::console::Renderable for Align<T> {
175 fn render(&self, options: &ConsoleOptions) -> RenderResult {
176 let inner_result = self.renderable.render(options);
177 let width = self.width.unwrap_or(options.max_width);
178
179 let mut lines: Vec<Vec<Segment>> = Vec::new();
180
181 for line_segs in inner_result.lines {
182 let line_text: String = line_segs.iter().map(|s| s.text.as_str()).collect();
184 let line_width = unicode_width::UnicodeWidthStr::width(line_text.as_str());
185
186 if line_width >= width {
187 lines.push(line_segs);
188 } else {
189 let padding = width - line_width;
190 let (left_pad, _right_pad) = match self.align {
191 AlignMethod::Left => (0, padding),
192 AlignMethod::Right => (padding, 0),
193 AlignMethod::Center => (padding / 2, padding - padding / 2),
194 AlignMethod::Full => (0, padding),
195 };
196
197 let mut aligned = Vec::new();
198 if left_pad > 0 {
199 aligned.push(Segment::new(" ".repeat(left_pad)));
200 }
201 aligned.extend(line_segs);
202 if padding - left_pad > 0 {
203 aligned.push(Segment::new(" ".repeat(padding - left_pad)));
204 }
205 aligned.push(Segment::line());
206 lines.push(aligned);
207 }
208 }
209
210 if let Some(h) = self.height {
212 if lines.len() < h {
213 let empty_lines = h - lines.len();
214 match self.vertical {
215 VerticalAlignMethod::Bottom => {
216 let mut top: Vec<Vec<Segment>> = (0..empty_lines)
217 .map(|_| vec![Segment::new(" ".repeat(width)), Segment::line()])
218 .collect();
219 top.extend(lines);
220 lines = top;
221 }
222 VerticalAlignMethod::Middle => {
223 let top_h = empty_lines / 2;
224 let bottom_h = empty_lines - top_h;
225 let mut result: Vec<Vec<Segment>> = (0..top_h)
226 .map(|_| vec![Segment::new(" ".repeat(width)), Segment::line()])
227 .collect();
228 result.extend(lines);
229 result.extend((0..bottom_h).map(|_| {
230 vec![Segment::new(" ".repeat(width)), Segment::line()]
231 }));
232 lines = result;
233 }
234 VerticalAlignMethod::Top => {
235 lines.extend((0..empty_lines).map(|_| {
236 vec![Segment::new(" ".repeat(width)), Segment::line()]
237 }));
238 }
239 }
240 }
241 }
242
243 RenderResult { lines, items: Vec::new() }
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn test_align_center() {
253 let result = AlignMethod::Center.align_text("Hi", 10);
254 assert_eq!(result.len(), 10);
255 assert!(result.starts_with(" "));
256 }
257}