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