Skip to main content

tui_math/
mathbox.rs

1//! MathBox - A 2D character grid for math rendering
2
3/// Represents a box of characters for rendering math expressions.
4/// Uses a 2D grid with baseline tracking for proper vertical alignment.
5#[derive(Clone, Debug)]
6pub struct MathBox {
7    content: Vec<Vec<char>>,
8    pub width: usize,
9    pub height: usize,
10    /// The baseline row (0-indexed from top)
11    pub baseline: usize,
12}
13
14impl MathBox {
15    /// Create a MathBox from a single-line string
16    pub fn from_text(text: &str) -> Self {
17        let chars: Vec<char> = text.chars().collect();
18        let width = chars.len();
19        Self {
20            content: vec![chars],
21            width,
22            height: 1,
23            baseline: 0,
24        }
25    }
26
27    /// Create an empty MathBox with specified dimensions
28    pub fn empty(width: usize, height: usize, baseline: usize) -> Self {
29        Self {
30            content: vec![vec![' '; width]; height],
31            width,
32            height,
33            baseline,
34        }
35    }
36
37    /// Create a MathBox from multiple lines
38    pub fn from_lines(lines: Vec<String>, baseline: usize) -> Self {
39        let height = lines.len();
40        let width = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0);
41        let mut content = vec![vec![' '; width]; height];
42
43        for (y, line) in lines.iter().enumerate() {
44            for (x, ch) in line.chars().enumerate() {
45                if x < width {
46                    content[y][x] = ch;
47                }
48            }
49        }
50
51        Self {
52            content,
53            width,
54            height,
55            baseline,
56        }
57    }
58
59    /// Get character at position (returns space if out of bounds)
60    pub fn get(&self, x: usize, y: usize) -> char {
61        if y < self.height && x < self.width {
62            self.content[y][x]
63        } else {
64            ' '
65        }
66    }
67
68    /// Set character at position
69    pub fn set(&mut self, x: usize, y: usize, ch: char) {
70        if y < self.height && x < self.width {
71            self.content[y][x] = ch;
72        }
73    }
74
75    /// Copy another MathBox into this one at the specified offset
76    pub fn blit(&mut self, other: &MathBox, x_offset: usize, y_offset: usize) {
77        for y in 0..other.height {
78            for x in 0..other.width {
79                let target_x = x_offset + x;
80                let target_y = y_offset + y;
81                if target_y < self.height && target_x < self.width {
82                    let ch = other.get(x, y);
83                    if ch != ' ' {
84                        self.set(target_x, target_y, ch);
85                    }
86                }
87            }
88        }
89    }
90
91    /// Concatenate horizontally, aligning by baseline
92    pub fn concat_horizontal(boxes: &[MathBox]) -> MathBox {
93        if boxes.is_empty() {
94            return MathBox::empty(0, 1, 0);
95        }
96
97        // Find max ascent (baseline) and max descent (height - baseline - 1)
98        let max_ascent = boxes.iter().map(|b| b.baseline).max().unwrap_or(0);
99        let max_descent = boxes
100            .iter()
101            .map(|b| b.height.saturating_sub(b.baseline + 1))
102            .max()
103            .unwrap_or(0);
104
105        let total_width: usize = boxes.iter().map(|b| b.width).sum();
106        let total_height = max_ascent + 1 + max_descent;
107
108        let mut result = MathBox::empty(total_width, total_height, max_ascent);
109        let mut x_pos = 0;
110
111        for b in boxes {
112            let y_offset = max_ascent - b.baseline;
113            result.blit(b, x_pos, y_offset);
114            x_pos += b.width;
115        }
116
117        result
118    }
119
120    /// Stack vertically, centered horizontally
121    pub fn stack_vertical(boxes: &[MathBox]) -> MathBox {
122        if boxes.is_empty() {
123            return MathBox::empty(0, 1, 0);
124        }
125
126        let max_width = boxes.iter().map(|b| b.width).max().unwrap_or(0);
127        let total_height: usize = boxes.iter().map(|b| b.height).sum();
128
129        let mut result = MathBox::empty(max_width, total_height, 0);
130        let mut y_pos = 0;
131
132        for b in boxes {
133            let x_offset = (max_width - b.width) / 2;
134            result.blit(b, x_offset, y_pos);
135            y_pos += b.height;
136        }
137
138        // Baseline at middle
139        result.baseline = total_height / 2;
140        result
141    }
142
143    /// Fill a row with a character
144    pub fn fill_row(&mut self, y: usize, ch: char) {
145        if y < self.height {
146            for x in 0..self.width {
147                self.set(x, y, ch);
148            }
149        }
150    }
151
152    /// Fill a column with a character
153    pub fn fill_col(&mut self, x: usize, ch: char) {
154        if x < self.width {
155            for y in 0..self.height {
156                self.set(x, y, ch);
157            }
158        }
159    }
160
161    /// Convert to string representation
162    pub fn to_string(&self) -> String {
163        self.content
164            .iter()
165            .map(|row| row.iter().collect::<String>().trim_end().to_string())
166            .collect::<Vec<_>>()
167            .join("\n")
168    }
169
170    /// Get lines as vector of strings
171    pub fn to_lines(&self) -> Vec<String> {
172        self.content
173            .iter()
174            .map(|row| row.iter().collect::<String>())
175            .collect()
176    }
177}
178
179impl Default for MathBox {
180    fn default() -> Self {
181        Self::empty(0, 1, 0)
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_from_text() {
191        let mb = MathBox::from_text("abc");
192        assert_eq!(mb.width, 3);
193        assert_eq!(mb.height, 1);
194        assert_eq!(mb.get(0, 0), 'a');
195        assert_eq!(mb.get(2, 0), 'c');
196    }
197
198    #[test]
199    fn test_concat_horizontal() {
200        let a = MathBox::from_text("x");
201        let b = MathBox::from_text("+");
202        let c = MathBox::from_text("y");
203        let result = MathBox::concat_horizontal(&[a, b, c]);
204        assert_eq!(result.to_string(), "x+y");
205    }
206}