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