Skip to main content

tui_math/
widget.rs

1//! Ratatui widget for rendering math expressions
2
3use crate::{MathRenderer, RenderError};
4use ratatui::{
5    buffer::Buffer,
6    layout::Rect,
7    style::{Color, Style},
8    text::{Line, Span},
9    widgets::{Block, Paragraph, Widget, Wrap},
10};
11
12/// A ratatui widget for rendering LaTeX math expressions
13#[derive(Clone)]
14pub struct MathWidget<'a> {
15    latex: &'a str,
16    style: Style,
17    block: Option<Block<'a>>,
18    use_unicode_scripts: bool,
19    wrap: bool,
20}
21
22impl<'a> MathWidget<'a> {
23    /// Create a new MathWidget from a LaTeX expression
24    pub fn new(latex: &'a str) -> Self {
25        Self {
26            latex,
27            style: Style::default(),
28            block: None,
29            use_unicode_scripts: true,
30            wrap: false,
31        }
32    }
33
34    /// Set the style for the rendered math
35    pub fn style(mut self, style: Style) -> Self {
36        self.style = style;
37        self
38    }
39
40    /// Set the foreground color
41    pub fn fg(mut self, color: Color) -> Self {
42        self.style = self.style.fg(color);
43        self
44    }
45
46    /// Set the background color
47    pub fn bg(mut self, color: Color) -> Self {
48        self.style = self.style.bg(color);
49        self
50    }
51
52    /// Wrap the widget in a block
53    pub fn block(mut self, block: Block<'a>) -> Self {
54        self.block = Some(block);
55        self
56    }
57
58    /// Enable or disable Unicode superscript/subscript characters
59    pub fn use_unicode_scripts(mut self, use_unicode: bool) -> Self {
60        self.use_unicode_scripts = use_unicode;
61        self
62    }
63
64    /// Enable or disable text wrapping
65    pub fn wrap(mut self, wrap: bool) -> Self {
66        self.wrap = wrap;
67        self
68    }
69
70    /// Render the LaTeX to a string (useful for debugging)
71    pub fn render_to_string(&self) -> Result<String, RenderError> {
72        let renderer = MathRenderer::new().use_unicode_scripts(self.use_unicode_scripts);
73        renderer.render_latex(self.latex)
74    }
75}
76
77impl Widget for MathWidget<'_> {
78    fn render(self, area: Rect, buf: &mut Buffer) {
79        let renderer = MathRenderer::new().use_unicode_scripts(self.use_unicode_scripts);
80
81        let rendered = match renderer.render_latex(self.latex) {
82            Ok(s) => s,
83            Err(e) => format!("Error: {}", e),
84        };
85
86        let lines: Vec<Line> = rendered
87            .lines()
88            .map(|line| Line::from(Span::styled(line.to_string(), self.style)))
89            .collect();
90
91        let mut paragraph = Paragraph::new(lines);
92
93        if let Some(block) = self.block {
94            paragraph = paragraph.block(block);
95        }
96
97        if self.wrap {
98            paragraph = paragraph.wrap(Wrap { trim: false });
99        }
100
101        paragraph.render(area, buf);
102    }
103}
104
105/// A stateful version of MathWidget that caches the rendered output
106pub struct MathWidgetState {
107    rendered: Option<String>,
108    error: Option<String>,
109}
110
111impl MathWidgetState {
112    pub fn new() -> Self {
113        Self {
114            rendered: None,
115            error: None,
116        }
117    }
118
119    /// Pre-render the math expression (call this when latex changes)
120    pub fn update(&mut self, latex: &str, use_unicode_scripts: bool) {
121        let renderer = MathRenderer::new().use_unicode_scripts(use_unicode_scripts);
122        match renderer.render_latex(latex) {
123            Ok(s) => {
124                self.rendered = Some(s);
125                self.error = None;
126            }
127            Err(e) => {
128                self.rendered = None;
129                self.error = Some(e.to_string());
130            }
131        }
132    }
133
134    /// Get the rendered string
135    pub fn rendered(&self) -> Option<&str> {
136        self.rendered.as_deref()
137    }
138
139    /// Get the error if any
140    pub fn error(&self) -> Option<&str> {
141        self.error.as_deref()
142    }
143}
144
145impl Default for MathWidgetState {
146    fn default() -> Self {
147        Self::new()
148    }
149}
150
151/// Stateful math widget that uses cached rendering
152pub struct StatefulMathWidget<'a> {
153    style: Style,
154    block: Option<Block<'a>>,
155    wrap: bool,
156}
157
158impl<'a> StatefulMathWidget<'a> {
159    pub fn new() -> Self {
160        Self {
161            style: Style::default(),
162            block: None,
163            wrap: false,
164        }
165    }
166
167    pub fn style(mut self, style: Style) -> Self {
168        self.style = style;
169        self
170    }
171
172    pub fn block(mut self, block: Block<'a>) -> Self {
173        self.block = Some(block);
174        self
175    }
176
177    pub fn wrap(mut self, wrap: bool) -> Self {
178        self.wrap = wrap;
179        self
180    }
181
182    pub fn render(self, area: Rect, buf: &mut Buffer, state: &MathWidgetState) {
183        let text = state
184            .rendered
185            .as_deref()
186            .or(state.error.as_deref())
187            .unwrap_or("");
188
189        let lines: Vec<Line> = text
190            .lines()
191            .map(|line| Line::from(Span::styled(line.to_string(), self.style)))
192            .collect();
193
194        let mut paragraph = Paragraph::new(lines);
195
196        if let Some(block) = self.block {
197            paragraph = paragraph.block(block);
198        }
199
200        if self.wrap {
201            paragraph = paragraph.wrap(Wrap { trim: false });
202        }
203
204        paragraph.render(area, buf);
205    }
206}
207
208impl Default for StatefulMathWidget<'_> {
209    fn default() -> Self {
210        Self::new()
211    }
212}