Skip to main content

tui_math/
canvas_widget.rs

1//! Canvas-based math widget using Braille markers for sub-cell resolution
2//!
3//! Uses Braille characters for smooth lines (fraction bars, sqrt) while
4//! rendering text normally for better readability.
5
6use crate::{MathBox, MathRenderer};
7use ratatui::{
8    buffer::Buffer,
9    layout::Rect,
10    style::{Color, Style},
11    symbols::Marker,
12    widgets::{
13        canvas::{Canvas, Line},
14        Block, Widget,
15    },
16};
17
18/// A high-resolution math widget using Canvas with Braille markers for lines
19#[derive(Clone)]
20pub struct CanvasMathWidget<'a> {
21    latex: &'a str,
22    style: Style,
23    block: Option<Block<'a>>,
24    color: Color,
25}
26
27impl<'a> CanvasMathWidget<'a> {
28    /// Create a new CanvasMathWidget from a LaTeX expression
29    pub fn new(latex: &'a str) -> Self {
30        Self {
31            latex,
32            style: Style::default(),
33            block: None,
34            color: Color::White,
35        }
36    }
37
38    /// Set the style
39    pub fn style(mut self, style: Style) -> Self {
40        self.style = style;
41        self
42    }
43
44    /// Set the drawing color
45    pub fn color(mut self, color: Color) -> Self {
46        self.color = color;
47        self
48    }
49
50    /// Wrap in a block
51    pub fn block(mut self, block: Block<'a>) -> Self {
52        self.block = Some(block);
53        self
54    }
55}
56
57/// Line segment to draw with Braille
58struct BrailleLine {
59    x1: f64,
60    y1: f64,
61    x2: f64,
62    y2: f64,
63}
64
65/// Extract line segments and text positions from MathBox
66/// area_height is used to flip y coordinates for Canvas (which has y=0 at bottom)
67fn extract_elements(mbox: &MathBox, area_height: f64) -> (Vec<BrailleLine>, Vec<(usize, usize, char)>) {
68    let mut lines = Vec::new();
69    let mut text_chars = Vec::new();
70
71    let content = mbox.to_lines();
72
73    for (row, line) in content.iter().enumerate() {
74        for (col, ch) in line.chars().enumerate() {
75            // Convert screen row (0=top) to canvas y (0=bottom)
76            // For row r, we want the line in the middle of that cell
77            // Screen row r is at canvas y = area_height - r - 0.5 (middle of cell)
78            let canvas_y_mid = area_height - row as f64 - 0.5;
79            let _canvas_y_top = area_height - row as f64;
80            let _canvas_y_bot = area_height - row as f64 - 1.0;
81
82            match ch {
83                // Horizontal line for fractions - draw with Braille for smoothness
84                '─' => {
85                    let x1 = col as f64;
86                    let x2 = (col + 1) as f64;
87                    lines.push(BrailleLine { x1, y1: canvas_y_mid, x2, y2: canvas_y_mid });
88                }
89                // Keep box-drawing characters as text for better visual connection
90                // with adjacent symbols like √
91                '╱' | '╲' | '│' => {
92                    text_chars.push((col, row, ch));
93                }
94                // Everything else is text
95                ' ' => {} // skip spaces
96                _ => {
97                    text_chars.push((col, row, ch));
98                }
99            }
100        }
101    }
102
103    (lines, text_chars)
104}
105
106impl Widget for CanvasMathWidget<'_> {
107    fn render(self, area: Rect, buf: &mut Buffer) {
108        // First render to MathBox using existing renderer
109        let renderer = MathRenderer::new();
110        let mbox = match renderer.render_to_box(self.latex) {
111            Ok(b) => b,
112            Err(e) => {
113                buf.set_string(area.x, area.y, format!("Error: {}", e), self.style);
114                return;
115            }
116        };
117
118        // Calculate content area (accounting for block borders)
119        let content_area = if let Some(ref block) = self.block {
120            let inner = block.inner(area);
121            block.clone().render(area, buf);
122            inner
123        } else {
124            area
125        };
126
127        // Extract line segments and text
128        // Use MathBox height for coordinate mapping to ensure alignment
129        let mbox_height_f = mbox.height as f64;
130        let (braille_lines, text_chars) = extract_elements(&mbox, mbox_height_f);
131
132        // Render Canvas FIRST (so text can overlay it)
133        if !braille_lines.is_empty() {
134            let mbox_width = mbox.width as u16;
135            let mbox_height = mbox.height as u16;
136            let color = self.color;
137
138            // Create canvas area that matches MathBox size
139            let canvas_area = Rect::new(
140                content_area.x,
141                content_area.y,
142                mbox_width.min(content_area.width),
143                mbox_height.min(content_area.height),
144            );
145
146            let canvas_width = canvas_area.width as f64;
147            let canvas_height = canvas_area.height as f64;
148
149            let canvas = Canvas::default()
150                .marker(Marker::Braille)
151                .x_bounds([0.0, canvas_width])
152                .y_bounds([0.0, canvas_height])
153                .paint(move |ctx| {
154                    for line in &braille_lines {
155                        // Add 0.5 to x coordinates to align Braille dots with character cells
156                        ctx.draw(&Line {
157                            x1: line.x1 + 0.5,
158                            y1: line.y1,
159                            x2: line.x2 + 0.5,
160                            y2: line.y2,
161                            color,
162                        });
163                    }
164                });
165
166            canvas.render(canvas_area, buf);
167        }
168
169        // Render text characters AFTER canvas (so text overlays Braille)
170        for (col, row, ch) in &text_chars {
171            let x = content_area.x + *col as u16;
172            let y = content_area.y + *row as u16;
173            if x < content_area.right() && y < content_area.bottom() {
174                buf.set_string(x, y, ch.to_string(), self.style);
175            }
176        }
177    }
178}