1use unicode_segmentation::UnicodeSegmentation;
4use unicode_width::UnicodeWidthStr;
5
6#[derive(Clone, Debug)]
10pub struct MathBox {
11 content: Vec<Vec<String>>,
12 pub width: usize,
13 pub height: usize,
14 pub baseline: usize,
16}
17
18impl MathBox {
19 pub fn from_text(text: &str) -> Self {
21 let graphemes: Vec<String> = text.graphemes(true).map(|g| g.to_string()).collect();
22 let width = text.width();
23
24 let mut cells = Vec::with_capacity(width);
26 for g in graphemes {
27 let g_width = g.width();
28 cells.push(g);
29 for _ in 1..g_width {
31 cells.push(String::new());
32 }
33 }
34 while cells.len() < width {
36 cells.push(" ".to_string());
37 }
38
39 Self {
40 content: vec![cells],
41 width,
42 height: 1,
43 baseline: 0,
44 }
45 }
46
47 pub fn empty(width: usize, height: usize, baseline: usize) -> Self {
49 Self {
50 content: vec![vec![" ".to_string(); width]; height],
51 width,
52 height,
53 baseline,
54 }
55 }
56
57 pub fn from_lines(lines: Vec<String>, baseline: usize) -> Self {
59 let height = lines.len();
60 let width = lines.iter().map(|l| l.width()).max().unwrap_or(0);
61 let mut content = vec![vec![" ".to_string(); width]; height];
62
63 for (y, line) in lines.iter().enumerate() {
64 let mut x = 0;
65 for g in line.graphemes(true) {
66 if x < width {
67 let g_width = g.width();
68 content[y][x] = g.to_string();
69 for i in 1..g_width {
71 if x + i < width {
72 content[y][x + i] = String::new();
73 }
74 }
75 x += g_width;
76 }
77 }
78 }
79
80 Self {
81 content,
82 width,
83 height,
84 baseline,
85 }
86 }
87
88 pub fn get(&self, x: usize, y: usize) -> char {
90 if y < self.height && x < self.width {
91 self.content[y][x].chars().next().unwrap_or(' ')
92 } else {
93 ' '
94 }
95 }
96
97 pub fn get_grapheme(&self, x: usize, y: usize) -> &str {
99 if y < self.height && x < self.width {
100 &self.content[y][x]
101 } else {
102 " "
103 }
104 }
105
106 pub fn set(&mut self, x: usize, y: usize, ch: char) {
108 if y < self.height && x < self.width {
109 self.content[y][x] = ch.to_string();
110 }
111 }
112
113 pub fn set_grapheme(&mut self, x: usize, y: usize, g: &str) {
115 if y < self.height && x < self.width {
116 self.content[y][x] = g.to_string();
117 }
118 }
119
120 pub fn blit(&mut self, other: &MathBox, x_offset: usize, y_offset: usize) {
122 for y in 0..other.height {
123 for x in 0..other.width {
124 let target_x = x_offset + x;
125 let target_y = y_offset + y;
126 if target_y < self.height && target_x < self.width {
127 let g = other.get_grapheme(x, y);
128 if !g.is_empty() && g != " " {
129 self.set_grapheme(target_x, target_y, g);
130 }
131 }
132 }
133 }
134 }
135
136 pub fn concat_horizontal(boxes: &[MathBox]) -> MathBox {
138 if boxes.is_empty() {
139 return MathBox::empty(0, 1, 0);
140 }
141
142 let max_ascent = boxes.iter().map(|b| b.baseline).max().unwrap_or(0);
144 let max_descent = boxes
145 .iter()
146 .map(|b| b.height.saturating_sub(b.baseline + 1))
147 .max()
148 .unwrap_or(0);
149
150 let total_width: usize = boxes.iter().map(|b| b.width).sum();
151 let total_height = max_ascent + 1 + max_descent;
152
153 let mut result = MathBox::empty(total_width, total_height, max_ascent);
154 let mut x_pos = 0;
155
156 for b in boxes {
157 let y_offset = max_ascent - b.baseline;
158 result.blit(b, x_pos, y_offset);
159 x_pos += b.width;
160 }
161
162 result
163 }
164
165 pub fn stack_vertical(boxes: &[MathBox]) -> MathBox {
167 if boxes.is_empty() {
168 return MathBox::empty(0, 1, 0);
169 }
170
171 let max_width = boxes.iter().map(|b| b.width).max().unwrap_or(0);
172 let total_height: usize = boxes.iter().map(|b| b.height).sum();
173
174 let mut result = MathBox::empty(max_width, total_height, 0);
175 let mut y_pos = 0;
176
177 for b in boxes {
178 let x_offset = (max_width - b.width) / 2;
179 result.blit(b, x_offset, y_pos);
180 y_pos += b.height;
181 }
182
183 result.baseline = total_height / 2;
185 result
186 }
187
188 pub fn fill_row(&mut self, y: usize, ch: char) {
190 if y < self.height {
191 for x in 0..self.width {
192 self.set(x, y, ch);
193 }
194 }
195 }
196
197 pub fn fill_col(&mut self, x: usize, ch: char) {
199 if x < self.width {
200 for y in 0..self.height {
201 self.set(x, y, ch);
202 }
203 }
204 }
205
206 pub fn to_string(&self) -> String {
208 self.content
209 .iter()
210 .map(|row| row.join("").trim_end().to_string())
211 .collect::<Vec<_>>()
212 .join("\n")
213 }
214
215 pub fn to_lines(&self) -> Vec<String> {
217 self.content.iter().map(|row| row.join("")).collect()
218 }
219}
220
221impl Default for MathBox {
222 fn default() -> Self {
223 Self::empty(0, 1, 0)
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_from_text() {
233 let mb = MathBox::from_text("abc");
234 assert_eq!(mb.width, 3);
235 assert_eq!(mb.height, 1);
236 assert_eq!(mb.get(0, 0), 'a');
237 assert_eq!(mb.get(2, 0), 'c');
238 }
239
240 #[test]
241 fn test_combining_chars() {
242 let mb = MathBox::from_text("T\u{0304}");
244 assert_eq!(mb.width, 1);
245 assert_eq!(mb.get_grapheme(0, 0), "T\u{0304}");
246 }
247
248 #[test]
249 fn test_concat_horizontal() {
250 let a = MathBox::from_text("x");
251 let b = MathBox::from_text("+");
252 let c = MathBox::from_text("y");
253 let result = MathBox::concat_horizontal(&[a, b, c]);
254 assert_eq!(result.to_string(), "x+y");
255 }
256}