Rust · 47009 bytes Raw Blame History
1 //! Math rendering with Cairo
2 //!
3 //! Draws mathematical notation with proper typesetting including
4 //! fraction bars, radical signs, integral symbols, etc.
5
6 use crate::layout::{LayoutBox, MathLayoutEngine};
7 use crate::mathbox::{LimitDirection, MathBox, Operator};
8 use cairo::Context;
9 use gartk_core::Color;
10 use std::cell::Cell;
11
12 /// Renderer for mathematical notation
13 pub struct MathRenderer<'a> {
14 ctx: &'a Context,
15 layout_engine: MathLayoutEngine,
16 /// Text/foreground color
17 pub fg_color: Color,
18 /// Slot background color
19 pub slot_bg_color: Color,
20 /// Slot border color when focused
21 pub slot_focus_color: Color,
22 /// Insertion cursor color
23 pub cursor_color: Color,
24 /// Whether the insertion cursor should be drawn
25 cursor_visible: Cell<bool>,
26 }
27
28 impl<'a> MathRenderer<'a> {
29 /// Create a new renderer with the given Cairo context
30 pub fn new(ctx: &'a Context, font_family: &str, font_size: f64) -> Self {
31 Self {
32 ctx,
33 layout_engine: MathLayoutEngine::new(font_family, font_size),
34 fg_color: Color::new(0.0, 0.0, 0.0, 1.0),
35 slot_bg_color: Color::new(0.9, 0.9, 0.95, 1.0),
36 slot_focus_color: Color::new(0.3, 0.5, 0.9, 1.0),
37 cursor_color: Color::new(0.12, 0.42, 1.0, 1.0),
38 cursor_visible: Cell::new(true),
39 }
40 }
41
42 /// Render a MathBox tree at the given position
43 /// (x, y) is the baseline position
44 pub fn render(&self, mathbox: &MathBox, x: f64, y: f64) {
45 self.render_at_depth(mathbox, x, y, 0, None, 0);
46 }
47
48 /// Render with cursor highlighting
49 pub fn render_with_cursor(
50 &self,
51 mathbox: &MathBox,
52 x: f64,
53 y: f64,
54 cursor_path: &[usize],
55 cursor_offset: usize,
56 cursor_visible: bool,
57 ) {
58 self.cursor_visible.set(cursor_visible);
59 self.render_at_depth(mathbox, x, y, 0, Some(cursor_path), cursor_offset);
60 self.cursor_visible.set(true);
61 }
62
63 /// Internal render with depth tracking
64 fn render_at_depth(
65 &self,
66 mathbox: &MathBox,
67 x: f64,
68 y: f64,
69 depth: u32,
70 cursor_path: Option<&[usize]>,
71 cursor_offset: usize,
72 ) {
73 let scale = self.scale_for_depth(depth);
74 let font_size = self.layout_engine.base_font_size * scale;
75
76 // Check if cursor is at this node
77 let is_cursor_here = cursor_path.map(|p| p.is_empty()).unwrap_or(false);
78 let is_active_container = cursor_path.map(|p| p.len() == 1).unwrap_or(false)
79 && Self::is_focusable_container(mathbox);
80
81 match mathbox {
82 MathBox::Number(s) => {
83 self.draw_text(s, x, y, font_size, false);
84 if is_cursor_here {
85 let width = self.text_advance(s, font_size, false);
86 self.draw_focus_frame(x, y, width, font_size, 1.4, 0.35);
87 }
88 if is_cursor_here && self.cursor_visible.get() {
89 let cursor_x =
90 x + self.text_advance_for_offset(s, font_size, false, cursor_offset);
91 self.draw_cursor(cursor_x, y, font_size);
92 }
93 }
94 MathBox::Symbol(s) => {
95 self.draw_text(s, x, y, font_size, true);
96 if is_cursor_here {
97 let width = self.text_advance(s, font_size, true);
98 self.draw_focus_frame(x, y, width, font_size, 1.4, 0.35);
99 }
100 if is_cursor_here && self.cursor_visible.get() {
101 let cursor_x =
102 x + self.text_advance_for_offset(s, font_size, true, cursor_offset);
103 self.draw_cursor(cursor_x, y, font_size);
104 }
105 }
106 MathBox::Operator(op) => {
107 self.draw_operator(*op, x, y, font_size);
108 }
109 MathBox::Slot => {
110 self.draw_slot(x, y, font_size, is_cursor_here);
111 if is_cursor_here && self.cursor_visible.get() {
112 let (slot_width, _, _) = Self::slot_geometry(font_size);
113 let cursor_x = x + slot_width * 0.15;
114 self.draw_cursor(cursor_x, y, font_size);
115 }
116 }
117 MathBox::Fraction { num, den } => {
118 self.render_fraction(num, den, x, y, depth, cursor_path, cursor_offset);
119 }
120 MathBox::Power { base, exp } => {
121 self.render_power(base, exp, x, y, depth, cursor_path, cursor_offset);
122 }
123 MathBox::Subscript { base, sub } => {
124 self.render_subscript(base, sub, x, y, depth, cursor_path, cursor_offset);
125 }
126 MathBox::Root { index, radicand } => {
127 self.render_root(
128 index.as_deref(),
129 radicand,
130 x,
131 y,
132 depth,
133 cursor_path,
134 cursor_offset,
135 );
136 }
137 MathBox::Func { name, args } => {
138 self.render_func(name, args, x, y, depth, cursor_path, cursor_offset);
139 }
140 MathBox::Abs(inner) => {
141 self.render_abs(inner, x, y, depth, cursor_path, cursor_offset);
142 }
143 MathBox::Parens(inner) => {
144 self.render_parens(inner, x, y, depth, cursor_path, cursor_offset);
145 }
146 MathBox::Integral {
147 lower,
148 upper,
149 body,
150 var,
151 } => {
152 self.render_integral(
153 lower.as_deref(),
154 upper.as_deref(),
155 body,
156 var,
157 x,
158 y,
159 depth,
160 cursor_path,
161 cursor_offset,
162 );
163 }
164 MathBox::Derivative { order, var, body } => {
165 self.render_derivative(*order, var, body, x, y, depth, cursor_path, cursor_offset);
166 }
167 MathBox::Limit {
168 var,
169 to,
170 direction,
171 body,
172 } => {
173 self.render_limit(
174 var,
175 to,
176 *direction,
177 body,
178 x,
179 y,
180 depth,
181 cursor_path,
182 cursor_offset,
183 );
184 }
185 MathBox::Sum {
186 var,
187 lower,
188 upper,
189 body,
190 } => {
191 self.render_bigop(
192 "∑",
193 var,
194 lower,
195 upper,
196 body,
197 x,
198 y,
199 depth,
200 cursor_path,
201 cursor_offset,
202 );
203 }
204 MathBox::Product {
205 var,
206 lower,
207 upper,
208 body,
209 } => {
210 self.render_bigop(
211 "∏",
212 var,
213 lower,
214 upper,
215 body,
216 x,
217 y,
218 depth,
219 cursor_path,
220 cursor_offset,
221 );
222 }
223 MathBox::Matrix { rows } => {
224 self.render_matrix(rows, x, y, depth, cursor_path, cursor_offset);
225 }
226 MathBox::Row(items) => {
227 self.render_row(items, x, y, depth, cursor_path, cursor_offset);
228 }
229 }
230
231 if is_active_container {
232 self.draw_container_focus_frame(mathbox, x, y, depth);
233 }
234 }
235
236 fn scale_for_depth(&self, depth: u32) -> f64 {
237 self.layout_engine.scale_for_depth(depth)
238 }
239
240 /// Draw plain text
241 fn draw_text(&self, text: &str, x: f64, y: f64, font_size: f64, italic: bool) {
242 self.ctx.save().unwrap();
243 self.set_color(&self.fg_color);
244
245 let slant = if italic {
246 cairo::FontSlant::Italic
247 } else {
248 cairo::FontSlant::Normal
249 };
250
251 self.ctx.select_font_face(
252 &self.layout_engine.font_family,
253 slant,
254 cairo::FontWeight::Normal,
255 );
256 self.ctx.set_font_size(font_size);
257 self.ctx.move_to(x, y);
258 self.ctx.show_text(text).unwrap();
259 self.ctx.restore().unwrap();
260 }
261
262 /// Measure text advance using the same style as `draw_text`
263 fn text_advance(&self, text: &str, font_size: f64, italic: bool) -> f64 {
264 self.ctx.save().unwrap();
265 let slant = if italic {
266 cairo::FontSlant::Italic
267 } else {
268 cairo::FontSlant::Normal
269 };
270 self.ctx.select_font_face(
271 &self.layout_engine.font_family,
272 slant,
273 cairo::FontWeight::Normal,
274 );
275 self.ctx.set_font_size(font_size);
276 let advance = self
277 .ctx
278 .text_extents(text)
279 .map(|e| e.x_advance())
280 .unwrap_or(0.0);
281 self.ctx.restore().unwrap();
282 advance
283 }
284
285 fn text_advance_for_offset(
286 &self,
287 text: &str,
288 font_size: f64,
289 italic: bool,
290 char_offset: usize,
291 ) -> f64 {
292 let prefix: String = text
293 .chars()
294 .take(char_offset.min(text.chars().count()))
295 .collect();
296 self.text_advance(&prefix, font_size, italic)
297 }
298
299 /// Draw an operator
300 fn draw_operator(&self, op: Operator, x: f64, y: f64, font_size: f64) {
301 let padding = font_size * 0.15;
302 self.draw_text(&op.as_char().to_string(), x + padding, y, font_size, false);
303 }
304
305 /// Draw an empty slot
306 fn draw_slot(&self, x: f64, y: f64, font_size: f64, focused: bool) {
307 let (width, height, ascent) = Self::slot_geometry(font_size);
308
309 self.ctx.save().unwrap();
310
311 // Background
312 self.set_color(&self.slot_bg_color);
313 self.ctx.rectangle(x, y - ascent, width, height);
314 self.ctx.fill().unwrap();
315
316 // Border
317 if focused {
318 self.set_color(&self.slot_focus_color);
319 self.ctx.set_line_width(2.0);
320 } else {
321 self.set_color(&self.fg_color);
322 self.ctx.set_line_width(0.5);
323 }
324 self.ctx.rectangle(x, y - ascent, width, height);
325 self.ctx.stroke().unwrap();
326
327 self.ctx.restore().unwrap();
328 }
329
330 /// Draw cursor
331 fn draw_cursor(&self, x: f64, y: f64, font_size: f64) {
332 self.ctx.save().unwrap();
333 self.set_color(&self.cursor_color);
334 self.ctx.set_line_width(1.0);
335
336 let (_, height, _) = Self::slot_geometry(font_size);
337 self.ctx.move_to(x, y - height * 0.6);
338 self.ctx.line_to(x, y + height * 0.4);
339 self.ctx.stroke().unwrap();
340
341 self.ctx.restore().unwrap();
342 }
343
344 /// Draw a focus frame around the active token
345 fn draw_focus_frame(
346 &self,
347 x: f64,
348 y: f64,
349 width: f64,
350 font_size: f64,
351 line_width: f64,
352 alpha: f64,
353 ) {
354 let ascent = font_size * 0.72;
355 let descent = font_size * 0.28;
356 let padding = (font_size * 0.12).max(1.2);
357 self.draw_focus_bounds(x, y, width, ascent, descent, padding, line_width, alpha);
358 }
359
360 /// Draw a focus frame around the active container (e.g. power/fraction)
361 fn draw_container_focus_frame(&self, mathbox: &MathBox, x: f64, y: f64, depth: u32) {
362 let layout = self
363 .layout_engine
364 .layout_with_depth(mathbox, self.ctx, depth);
365 let scale = self.scale_for_depth(depth);
366 let padding = (self.layout_engine.base_font_size * scale * 0.12).max(1.4);
367 self.draw_focus_bounds(
368 x,
369 y,
370 layout.width,
371 layout.ascent,
372 layout.descent,
373 padding,
374 1.5,
375 0.28,
376 );
377 }
378
379 fn draw_focus_bounds(
380 &self,
381 x: f64,
382 y: f64,
383 width: f64,
384 ascent: f64,
385 descent: f64,
386 padding: f64,
387 line_width: f64,
388 alpha: f64,
389 ) {
390 self.ctx.save().unwrap();
391 self.set_color(&Color::new(
392 self.slot_focus_color.r,
393 self.slot_focus_color.g,
394 self.slot_focus_color.b,
395 alpha,
396 ));
397 self.ctx.set_line_width(line_width);
398 self.ctx.rectangle(
399 x - padding,
400 y - ascent - padding,
401 width + padding * 2.0,
402 ascent + descent + padding * 2.0,
403 );
404 self.ctx.stroke().unwrap();
405 self.ctx.restore().unwrap();
406 }
407
408 fn is_focusable_container(mathbox: &MathBox) -> bool {
409 matches!(
410 mathbox,
411 MathBox::Fraction { .. }
412 | MathBox::Power { .. }
413 | MathBox::Subscript { .. }
414 | MathBox::Root { .. }
415 | MathBox::Func { .. }
416 | MathBox::Abs(_)
417 | MathBox::Parens(_)
418 | MathBox::Integral { .. }
419 | MathBox::Derivative { .. }
420 | MathBox::Limit { .. }
421 | MathBox::Sum { .. }
422 | MathBox::Product { .. }
423 | MathBox::Matrix { .. }
424 )
425 }
426
427 /// Render a fraction
428 fn render_fraction(
429 &self,
430 num: &MathBox,
431 den: &MathBox,
432 x: f64,
433 y: f64,
434 depth: u32,
435 cursor_path: Option<&[usize]>,
436 cursor_offset: usize,
437 ) {
438 let layout = self.layout_engine.layout(
439 &MathBox::Fraction {
440 num: Box::new(num.clone()),
441 den: Box::new(den.clone()),
442 },
443 self.ctx,
444 );
445
446 let scale = self.scale_for_depth(depth);
447 let bar_thickness = 1.0 * scale;
448 let gap = self.layout_engine.base_font_size * 0.1 * scale;
449
450 // Get child layouts for positioning
451 let num_layout = self.layout_engine.layout(num, self.ctx);
452 let den_layout = self.layout_engine.layout(den, self.ctx);
453
454 let num_x = x + (layout.width - num_layout.width) / 2.0;
455 let den_x = x + (layout.width - den_layout.width) / 2.0;
456 let num_y = y - (gap + bar_thickness / 2.0 + num_layout.descent);
457 let den_y = y + (gap + bar_thickness / 2.0 + den_layout.ascent);
458
459 // Draw fraction bar
460 self.ctx.save().unwrap();
461 self.set_color(&self.fg_color);
462 self.ctx.set_line_width(bar_thickness);
463 self.ctx.move_to(x, y);
464 self.ctx.line_to(x + layout.width, y);
465 self.ctx.stroke().unwrap();
466 self.ctx.restore().unwrap();
467
468 // Draw numerator and denominator
469 let num_cursor = cursor_path.and_then(|p| {
470 if !p.is_empty() && p[0] == 0 {
471 Some(&p[1..])
472 } else {
473 None
474 }
475 });
476 let den_cursor = cursor_path.and_then(|p| {
477 if !p.is_empty() && p[0] == 1 {
478 Some(&p[1..])
479 } else {
480 None
481 }
482 });
483
484 self.render_at_depth(num, num_x, num_y, depth + 1, num_cursor, cursor_offset);
485 self.render_at_depth(den, den_x, den_y, depth + 1, den_cursor, cursor_offset);
486 }
487
488 /// Render a power (superscript)
489 fn render_power(
490 &self,
491 base: &MathBox,
492 exp: &MathBox,
493 x: f64,
494 y: f64,
495 depth: u32,
496 cursor_path: Option<&[usize]>,
497 cursor_offset: usize,
498 ) {
499 let base_layout = self.layout_engine.layout_with_depth(base, self.ctx, depth);
500 let exp_layout = self
501 .layout_engine
502 .layout_with_depth(exp, self.ctx, depth + 1);
503 let scale = self.scale_for_depth(depth);
504 let font_size = self.layout_engine.base_font_size * scale;
505
506 let base_cursor = cursor_path.and_then(|p| {
507 if !p.is_empty() && p[0] == 0 {
508 Some(&p[1..])
509 } else {
510 None
511 }
512 });
513 let exp_cursor = cursor_path.and_then(|p| {
514 if !p.is_empty() && p[0] == 1 {
515 Some(&p[1..])
516 } else {
517 None
518 }
519 });
520
521 self.render_at_depth(base, x, y, depth, base_cursor, cursor_offset);
522
523 let exp_raise = base_layout.ascent * 0.58 + exp_layout.descent * 0.1;
524 let exp_kern = (font_size * 0.06).max(0.8);
525 self.render_at_depth(
526 exp,
527 x + base_layout.width + exp_kern,
528 y - exp_raise,
529 depth + 1,
530 exp_cursor,
531 cursor_offset,
532 );
533 }
534
535 /// Render a subscript
536 fn render_subscript(
537 &self,
538 base: &MathBox,
539 sub: &MathBox,
540 x: f64,
541 y: f64,
542 depth: u32,
543 cursor_path: Option<&[usize]>,
544 cursor_offset: usize,
545 ) {
546 let base_layout = self.layout_engine.layout_with_depth(base, self.ctx, depth);
547 let sub_layout = self
548 .layout_engine
549 .layout_with_depth(sub, self.ctx, depth + 1);
550 let scale = self.scale_for_depth(depth);
551 let font_size = self.layout_engine.base_font_size * scale;
552
553 let base_cursor = cursor_path.and_then(|p| {
554 if !p.is_empty() && p[0] == 0 {
555 Some(&p[1..])
556 } else {
557 None
558 }
559 });
560 let sub_cursor = cursor_path.and_then(|p| {
561 if !p.is_empty() && p[0] == 1 {
562 Some(&p[1..])
563 } else {
564 None
565 }
566 });
567
568 self.render_at_depth(base, x, y, depth, base_cursor, cursor_offset);
569
570 let sub_lower =
571 (base_layout.descent * 0.6 + sub_layout.ascent * 0.9).max(sub_layout.ascent * 0.75);
572 let sub_kern = (font_size * 0.05).max(0.6);
573 self.render_at_depth(
574 sub,
575 x + base_layout.width + sub_kern,
576 y + sub_lower,
577 depth + 1,
578 sub_cursor,
579 cursor_offset,
580 );
581 }
582
583 /// Render a root (square or nth)
584 fn render_root(
585 &self,
586 index: Option<&MathBox>,
587 radicand: &MathBox,
588 x: f64,
589 y: f64,
590 depth: u32,
591 cursor_path: Option<&[usize]>,
592 cursor_offset: usize,
593 ) {
594 let radicand_layout = self.layout_engine.layout(radicand, self.ctx);
595 let scale = self.scale_for_depth(depth);
596 let font_size = self.layout_engine.base_font_size * scale;
597
598 let radical_width = font_size * 0.6;
599 let bar_overhang = font_size * 0.1;
600 let gap = font_size * 0.1;
601
602 let _height = radicand_layout.height() + gap;
603 let mut radicand_x = x + radical_width;
604
605 // Draw index if present
606 if let Some(idx) = index {
607 let idx_layout = self.layout_engine.layout(idx, self.ctx);
608 let idx_cursor = cursor_path.and_then(|p| {
609 if !p.is_empty() && p[0] == 0 {
610 Some(&p[1..])
611 } else {
612 None
613 }
614 });
615 self.render_at_depth(
616 idx,
617 x,
618 y - radicand_layout.ascent * 0.5 - idx_layout.descent,
619 depth + 2,
620 idx_cursor,
621 cursor_offset,
622 );
623 radicand_x += idx_layout.width;
624 }
625
626 // Draw radical symbol
627 self.ctx.save().unwrap();
628 self.set_color(&self.fg_color);
629 self.ctx.set_line_width(1.0 * scale);
630
631 // Radical checkmark
632 let check_width = radical_width * 0.4;
633 self.ctx.move_to(radicand_x - radical_width, y);
634 self.ctx.line_to(
635 radicand_x - radical_width + check_width * 0.3,
636 y + radicand_layout.descent * 0.3,
637 );
638 self.ctx.line_to(
639 radicand_x - radical_width + check_width,
640 y + radicand_layout.descent,
641 );
642 self.ctx
643 .line_to(radicand_x, y - radicand_layout.ascent - gap);
644
645 // Overbar
646 self.ctx.line_to(
647 radicand_x + radicand_layout.width + bar_overhang,
648 y - radicand_layout.ascent - gap,
649 );
650 self.ctx.stroke().unwrap();
651 self.ctx.restore().unwrap();
652
653 // Draw radicand
654 let rad_cursor = cursor_path.and_then(|p| {
655 let idx = if index.is_some() { 1 } else { 0 };
656 if !p.is_empty() && p[0] == idx {
657 Some(&p[1..])
658 } else {
659 None
660 }
661 });
662 self.render_at_depth(radicand, radicand_x, y, depth, rad_cursor, cursor_offset);
663 }
664
665 /// Render a function call
666 fn render_func(
667 &self,
668 name: &str,
669 args: &[MathBox],
670 x: f64,
671 y: f64,
672 depth: u32,
673 cursor_path: Option<&[usize]>,
674 cursor_offset: usize,
675 ) {
676 if name == "factorial" && args.len() == 1 {
677 self.render_factorial(&args[0], x, y, depth, cursor_path, cursor_offset);
678 return;
679 }
680
681 let scale = self.scale_for_depth(depth);
682 let font_size = self.layout_engine.base_font_size * scale;
683 let paren_width = font_size * 0.3;
684
685 // Draw function name
686 self.draw_text(name, x, y, font_size, false);
687
688 self.ctx.set_font_size(font_size);
689 let name_extents = self.ctx.text_extents(name).unwrap();
690 let mut current_x = x + name_extents.x_advance();
691
692 // Draw opening paren
693 self.draw_text("(", current_x, y, font_size, false);
694 current_x += paren_width;
695
696 // Draw arguments
697 for (i, arg) in args.iter().enumerate() {
698 if i > 0 {
699 self.draw_text(",", current_x, y, font_size, false);
700 current_x += font_size * 0.3;
701 }
702
703 let arg_cursor = cursor_path.and_then(|p| {
704 if !p.is_empty() && p[0] == i {
705 Some(&p[1..])
706 } else {
707 None
708 }
709 });
710 self.render_at_depth(arg, current_x, y, depth, arg_cursor, cursor_offset);
711
712 let arg_layout = self.layout_engine.layout(arg, self.ctx);
713 current_x += arg_layout.width;
714 }
715
716 // Draw closing paren
717 self.draw_text(")", current_x, y, font_size, false);
718 }
719
720 fn render_factorial(
721 &self,
722 arg: &MathBox,
723 x: f64,
724 y: f64,
725 depth: u32,
726 cursor_path: Option<&[usize]>,
727 cursor_offset: usize,
728 ) {
729 let scale = self.scale_for_depth(depth);
730 let font_size = self.layout_engine.base_font_size * scale;
731 let gap = (font_size * 0.06).max(0.6);
732
733 let arg_cursor = cursor_path.and_then(|p| {
734 if !p.is_empty() && p[0] == 0 {
735 Some(&p[1..])
736 } else {
737 None
738 }
739 });
740 self.render_at_depth(arg, x, y, depth, arg_cursor, cursor_offset);
741
742 let arg_layout = self.layout_engine.layout_with_depth(arg, self.ctx, depth);
743 self.draw_text("!", x + arg_layout.width + gap, y, font_size, false);
744 }
745
746 /// Render absolute value
747 fn render_abs(
748 &self,
749 inner: &MathBox,
750 x: f64,
751 y: f64,
752 depth: u32,
753 cursor_path: Option<&[usize]>,
754 cursor_offset: usize,
755 ) {
756 let inner_layout = self.layout_engine.layout(inner, self.ctx);
757 let scale = self.scale_for_depth(depth);
758 let bar_width = self.layout_engine.base_font_size * 0.15 * scale;
759
760 let _height = inner_layout.height();
761
762 // Draw vertical bars
763 self.ctx.save().unwrap();
764 self.set_color(&self.fg_color);
765 self.ctx.set_line_width(1.5 * scale);
766
767 // Left bar
768 self.ctx
769 .move_to(x + bar_width / 2.0, y - inner_layout.ascent);
770 self.ctx
771 .line_to(x + bar_width / 2.0, y + inner_layout.descent);
772 self.ctx.stroke().unwrap();
773
774 // Right bar
775 let right_x = x + bar_width + inner_layout.width + bar_width / 2.0;
776 self.ctx.move_to(right_x, y - inner_layout.ascent);
777 self.ctx.line_to(right_x, y + inner_layout.descent);
778 self.ctx.stroke().unwrap();
779
780 self.ctx.restore().unwrap();
781
782 // Draw inner expression
783 let inner_cursor = cursor_path.and_then(|p| {
784 if !p.is_empty() && p[0] == 0 {
785 Some(&p[1..])
786 } else {
787 None
788 }
789 });
790 self.render_at_depth(inner, x + bar_width, y, depth, inner_cursor, cursor_offset);
791 }
792
793 /// Render parenthesized expression
794 fn render_parens(
795 &self,
796 inner: &MathBox,
797 x: f64,
798 y: f64,
799 depth: u32,
800 cursor_path: Option<&[usize]>,
801 cursor_offset: usize,
802 ) {
803 let inner_layout = self.layout_engine.layout(inner, self.ctx);
804 let scale = self.scale_for_depth(depth);
805 let font_size = self.layout_engine.base_font_size * scale;
806 let paren_width = font_size * 0.25;
807
808 // For now, draw text parentheses (could be replaced with curved paths)
809 self.draw_text("(", x, y, font_size * 1.2, false);
810 self.draw_text(
811 ")",
812 x + paren_width + inner_layout.width,
813 y,
814 font_size * 1.2,
815 false,
816 );
817
818 let inner_cursor = cursor_path.and_then(|p| {
819 if !p.is_empty() && p[0] == 0 {
820 Some(&p[1..])
821 } else {
822 None
823 }
824 });
825 self.render_at_depth(
826 inner,
827 x + paren_width,
828 y,
829 depth,
830 inner_cursor,
831 cursor_offset,
832 );
833 }
834
835 /// Render an integral
836 fn render_integral(
837 &self,
838 lower: Option<&MathBox>,
839 upper: Option<&MathBox>,
840 body: &MathBox,
841 var: &MathBox,
842 x: f64,
843 y: f64,
844 depth: u32,
845 cursor_path: Option<&[usize]>,
846 cursor_offset: usize,
847 ) {
848 let scale = self.scale_for_depth(depth);
849 let font_size = self.layout_engine.base_font_size * scale;
850 let body_layout = self.layout_engine.layout_with_depth(body, self.ctx, depth);
851
852 let symbol_size = body_layout.height().max(font_size * 1.8);
853 self.ctx.set_font_size(symbol_size);
854 let int_extents = self.ctx.text_extents("∫").unwrap();
855 let int_font_extents = self.ctx.font_extents().unwrap();
856 let symbol_width = int_extents.x_advance();
857 let symbol_ascent = int_font_extents.ascent();
858 let symbol_descent = int_font_extents.descent();
859
860 let lo_layout = lower.map(|lo| {
861 self.layout_engine
862 .layout_with_depth(lo, self.ctx, depth + 1)
863 });
864 let hi_layout = upper.map(|hi| {
865 self.layout_engine
866 .layout_with_depth(hi, self.ctx, depth + 1)
867 });
868
869 let bound_gap = font_size * 0.14;
870 let bounds_width = symbol_width
871 .max(lo_layout.as_ref().map(|l| l.width).unwrap_or(0.0))
872 .max(hi_layout.as_ref().map(|l| l.width).unwrap_or(0.0));
873 let body_gap = font_size * 0.22;
874 let dx_gap = font_size * 0.14;
875
876 // Draw integral symbol centered in bounds column
877 self.ctx.save().unwrap();
878 self.set_color(&self.fg_color);
879 self.ctx.set_font_size(symbol_size);
880 let symbol_x = x + (bounds_width - symbol_width) / 2.0;
881 let symbol_baseline = y + (symbol_ascent - symbol_descent) * 0.5;
882 self.ctx.move_to(symbol_x, symbol_baseline);
883 self.ctx.show_text("∫").unwrap();
884 self.ctx.restore().unwrap();
885
886 let mut child_idx = 0;
887
888 // Draw lower bound
889 if let (Some(lo), Some(lo_layout)) = (lower, lo_layout.as_ref()) {
890 let lo_cursor = cursor_path.and_then(|p| {
891 if !p.is_empty() && p[0] == child_idx {
892 Some(&p[1..])
893 } else {
894 None
895 }
896 });
897 let lo_x = x + (bounds_width - lo_layout.width) / 2.0;
898 let lo_baseline = y + symbol_descent + bound_gap + lo_layout.ascent;
899 self.render_at_depth(lo, lo_x, lo_baseline, depth + 1, lo_cursor, cursor_offset);
900 child_idx += 1;
901 }
902
903 // Draw upper bound
904 if let (Some(hi), Some(hi_layout)) = (upper, hi_layout.as_ref()) {
905 let hi_cursor = cursor_path.and_then(|p| {
906 if !p.is_empty() && p[0] == child_idx {
907 Some(&p[1..])
908 } else {
909 None
910 }
911 });
912 let hi_x = x + (bounds_width - hi_layout.width) / 2.0;
913 let hi_baseline = y - symbol_ascent - bound_gap - hi_layout.descent;
914 self.render_at_depth(hi, hi_x, hi_baseline, depth + 1, hi_cursor, cursor_offset);
915 child_idx += 1;
916 }
917
918 // Draw body
919 let body_x = x + bounds_width + body_gap;
920 let body_cursor = cursor_path.and_then(|p| {
921 if !p.is_empty() && p[0] == child_idx {
922 Some(&p[1..])
923 } else {
924 None
925 }
926 });
927 self.render_at_depth(body, body_x, y, depth, body_cursor, cursor_offset);
928 child_idx += 1;
929
930 // Draw "dx"
931 let d_width = self.text_advance("d", font_size, false);
932 let d_x = body_x + body_layout.width + dx_gap;
933 self.draw_text("d", d_x, y, font_size, false);
934
935 let var_cursor = cursor_path.and_then(|p| {
936 if !p.is_empty() && p[0] == child_idx {
937 Some(&p[1..])
938 } else {
939 None
940 }
941 });
942 self.render_at_depth(var, d_x + d_width, y, depth, var_cursor, cursor_offset);
943 }
944
945 /// Render a derivative
946 fn render_derivative(
947 &self,
948 order: u32,
949 var: &MathBox,
950 body: &MathBox,
951 x: f64,
952 y: f64,
953 depth: u32,
954 cursor_path: Option<&[usize]>,
955 cursor_offset: usize,
956 ) {
957 let scale = self.scale_for_depth(depth);
958 let font_size = self.layout_engine.base_font_size * scale;
959 let frac_font_size = font_size * 0.8;
960
961 // Build strings for numerator and denominator
962 let num_str = if order > 1 {
963 format!("d{}", superscript_digits(order))
964 } else {
965 "d".to_string()
966 };
967
968 let den_prefix = if order > 1 {
969 format!("d{}", superscript_digits(order))
970 } else {
971 "d".to_string()
972 };
973
974 // Measure text and guard spacing
975 let num_width = self.text_advance(&num_str, frac_font_size, false);
976 let den_prefix_width = self.text_advance(&den_prefix, frac_font_size, false);
977 self.ctx.set_font_size(frac_font_size);
978 let frac_font_extents = self.ctx.font_extents().unwrap();
979 let frac_ascent = frac_font_extents.ascent();
980 let frac_descent = frac_font_extents.descent();
981
982 let var_layout = self
983 .layout_engine
984 .layout_with_depth(var, self.ctx, depth + 1);
985 let den_sep = (frac_font_size * 0.08).max(0.6);
986 let denom_width = den_prefix_width + den_sep + var_layout.width;
987 let frac_width = num_width.max(denom_width) + font_size * 0.18;
988 let bar_gap = font_size * 0.14;
989 let body_gap = font_size * 0.3;
990
991 // Draw numerator
992 self.draw_text(
993 &num_str,
994 x + (frac_width - num_width) / 2.0,
995 y - bar_gap - frac_descent,
996 frac_font_size,
997 false,
998 );
999
1000 // Draw fraction bar
1001 self.ctx.save().unwrap();
1002 self.set_color(&self.fg_color);
1003 self.ctx.set_line_width(1.0 * scale);
1004 self.ctx.move_to(x, y);
1005 self.ctx.line_to(x + frac_width, y);
1006 self.ctx.stroke().unwrap();
1007 self.ctx.restore().unwrap();
1008
1009 // Draw denominator
1010 let den_x = x + (frac_width - denom_width) / 2.0;
1011 let den_baseline = y + bar_gap + frac_ascent.max(var_layout.ascent);
1012 self.draw_text(&den_prefix, den_x, den_baseline, frac_font_size, false);
1013
1014 let var_cursor = cursor_path.and_then(|p| {
1015 if !p.is_empty() && p[0] == 0 {
1016 Some(&p[1..])
1017 } else {
1018 None
1019 }
1020 });
1021 self.render_at_depth(
1022 var,
1023 den_x + den_prefix_width + den_sep,
1024 den_baseline,
1025 depth + 1,
1026 var_cursor,
1027 cursor_offset,
1028 );
1029
1030 // Draw body
1031 let body_cursor = cursor_path.and_then(|p| {
1032 if !p.is_empty() && p[0] == 1 {
1033 Some(&p[1..])
1034 } else {
1035 None
1036 }
1037 });
1038 self.render_at_depth(
1039 body,
1040 x + frac_width + body_gap,
1041 y,
1042 depth,
1043 body_cursor,
1044 cursor_offset,
1045 );
1046 }
1047
1048 /// Render a limit
1049 fn render_limit(
1050 &self,
1051 var: &MathBox,
1052 to: &MathBox,
1053 direction: Option<LimitDirection>,
1054 body: &MathBox,
1055 x: f64,
1056 y: f64,
1057 depth: u32,
1058 cursor_path: Option<&[usize]>,
1059 cursor_offset: usize,
1060 ) {
1061 let scale = self.scale_for_depth(depth);
1062 let font_size = self.layout_engine.base_font_size * scale;
1063 let sub_font_size = font_size * 0.7;
1064 let body_gap = font_size * 0.3;
1065
1066 let lim_width = self.text_advance("lim", font_size, false);
1067 let var_layout = self
1068 .layout_engine
1069 .layout_with_depth(var, self.ctx, depth + 1);
1070 let to_layout = self
1071 .layout_engine
1072 .layout_with_depth(to, self.ctx, depth + 1);
1073 let arrow_width = self.text_advance("→", sub_font_size, false);
1074 let dir_width = if direction.is_some() {
1075 self.text_advance("⁺", sub_font_size * 0.6, false)
1076 } else {
1077 0.0
1078 };
1079 let sub_sep = (sub_font_size * 0.08).max(0.5);
1080 let subscript_width =
1081 var_layout.width + sub_sep + arrow_width + sub_sep + to_layout.width + dir_width;
1082 let lim_col_width = lim_width.max(subscript_width);
1083 let lim_x = x + (lim_col_width - lim_width) / 2.0;
1084 let sub_start_x = x + (lim_col_width - subscript_width) / 2.0;
1085 let subscript_y = y + font_size * 0.2 + var_layout.ascent.max(to_layout.ascent);
1086
1087 // Draw "lim"
1088 self.draw_text("lim", lim_x, y, font_size, false);
1089
1090 let var_cursor = cursor_path.and_then(|p| {
1091 if !p.is_empty() && p[0] == 0 {
1092 Some(&p[1..])
1093 } else {
1094 None
1095 }
1096 });
1097 self.render_at_depth(
1098 var,
1099 sub_start_x,
1100 subscript_y,
1101 depth + 1,
1102 var_cursor,
1103 cursor_offset,
1104 );
1105
1106 let arrow_x = sub_start_x + var_layout.width + sub_sep;
1107 self.draw_text("→", arrow_x, subscript_y, sub_font_size, false);
1108
1109 let to_cursor = cursor_path.and_then(|p| {
1110 if !p.is_empty() && p[0] == 1 {
1111 Some(&p[1..])
1112 } else {
1113 None
1114 }
1115 });
1116 self.render_at_depth(
1117 to,
1118 arrow_x + arrow_width + sub_sep,
1119 subscript_y,
1120 depth + 1,
1121 to_cursor,
1122 cursor_offset,
1123 );
1124
1125 // Draw direction indicator if present
1126 if let Some(dir) = direction {
1127 let dir_str = match dir {
1128 LimitDirection::FromRight => "⁺",
1129 LimitDirection::FromLeft => "⁻",
1130 };
1131 self.draw_text(
1132 dir_str,
1133 arrow_x + arrow_width + sub_sep + to_layout.width,
1134 subscript_y - sub_font_size * 0.3,
1135 sub_font_size * 0.6,
1136 false,
1137 );
1138 }
1139
1140 // Draw body
1141 let body_x = x + lim_col_width + body_gap;
1142 let body_cursor = cursor_path.and_then(|p| {
1143 if !p.is_empty() && p[0] == 2 {
1144 Some(&p[1..])
1145 } else {
1146 None
1147 }
1148 });
1149 self.render_at_depth(body, body_x, y, depth, body_cursor, cursor_offset);
1150 }
1151
1152 /// Render a big operator (sum, product)
1153 fn render_bigop(
1154 &self,
1155 symbol: &str,
1156 var: &MathBox,
1157 lower: &MathBox,
1158 upper: &MathBox,
1159 body: &MathBox,
1160 x: f64,
1161 y: f64,
1162 depth: u32,
1163 cursor_path: Option<&[usize]>,
1164 cursor_offset: usize,
1165 ) {
1166 let scale = self.scale_for_depth(depth);
1167 let font_size = self.layout_engine.base_font_size * scale;
1168 let symbol_size = font_size * 1.5;
1169 let bound_scale = self.scale_for_depth(depth + 1);
1170 let bound_font_size = self.layout_engine.base_font_size * bound_scale;
1171
1172 let var_layout = self
1173 .layout_engine
1174 .layout_with_depth(var, self.ctx, depth + 1);
1175 let lower_layout = self
1176 .layout_engine
1177 .layout_with_depth(lower, self.ctx, depth + 1);
1178 let upper_layout = self
1179 .layout_engine
1180 .layout_with_depth(upper, self.ctx, depth + 1);
1181 let eq_width = self.text_advance("=", bound_font_size, false);
1182 let lower_sep = (bound_font_size * 0.08).max(0.6);
1183 let lower_block_width =
1184 var_layout.width + lower_sep + eq_width + lower_sep + lower_layout.width;
1185
1186 // Draw the big symbol centered in the operator column.
1187 self.ctx.save().unwrap();
1188 self.set_color(&self.fg_color);
1189 self.ctx.set_font_size(symbol_size);
1190 let symbol_extents = self.ctx.text_extents(symbol).unwrap();
1191 let symbol_width = symbol_extents.x_advance();
1192 let symbol_font_extents = self.ctx.font_extents().unwrap();
1193 let symbol_ascent = symbol_font_extents.ascent();
1194 let symbol_descent = symbol_font_extents.descent();
1195 let op_width = symbol_width.max(lower_block_width).max(upper_layout.width);
1196 let symbol_x = x + (op_width - symbol_width) / 2.0;
1197 let symbol_baseline = y + (symbol_ascent - symbol_descent) * 0.5;
1198 self.ctx.move_to(symbol_x, symbol_baseline);
1199 self.ctx.show_text(symbol).unwrap();
1200 self.ctx.restore().unwrap();
1201
1202 let bounds_gap = font_size * 0.16;
1203 let body_gap = font_size * 0.32;
1204 let symbol_top = symbol_baseline - symbol_ascent;
1205 let symbol_bottom = symbol_baseline + symbol_descent;
1206 let upper_baseline = symbol_top - bounds_gap - upper_layout.descent;
1207 let lower_baseline =
1208 symbol_bottom + bounds_gap + lower_layout.ascent.max(var_layout.ascent);
1209 let upper_x = x + (op_width - upper_layout.width) / 2.0;
1210 let lower_start_x = x + (op_width - lower_block_width) / 2.0;
1211
1212 // Draw upper bound
1213 let upper_cursor = cursor_path.and_then(|p| {
1214 if !p.is_empty() && p[0] == 2 {
1215 Some(&p[1..])
1216 } else {
1217 None
1218 }
1219 });
1220 self.render_at_depth(
1221 upper,
1222 upper_x,
1223 upper_baseline,
1224 depth + 1,
1225 upper_cursor,
1226 cursor_offset,
1227 );
1228
1229 // Draw lower bound as "var = lower"
1230 let var_cursor = cursor_path.and_then(|p| {
1231 if !p.is_empty() && p[0] == 0 {
1232 Some(&p[1..])
1233 } else {
1234 None
1235 }
1236 });
1237 self.render_at_depth(
1238 var,
1239 lower_start_x,
1240 lower_baseline,
1241 depth + 1,
1242 var_cursor,
1243 cursor_offset,
1244 );
1245
1246 let eq_x = lower_start_x + var_layout.width + lower_sep;
1247 self.draw_text("=", eq_x, lower_baseline, bound_font_size, false);
1248
1249 let lower_cursor = cursor_path.and_then(|p| {
1250 if !p.is_empty() && p[0] == 1 {
1251 Some(&p[1..])
1252 } else {
1253 None
1254 }
1255 });
1256 self.render_at_depth(
1257 lower,
1258 eq_x + eq_width + lower_sep,
1259 lower_baseline,
1260 depth + 1,
1261 lower_cursor,
1262 cursor_offset,
1263 );
1264
1265 // Draw body
1266 let body_x = x + op_width + body_gap;
1267 let body_cursor = cursor_path.and_then(|p| {
1268 if !p.is_empty() && p[0] == 3 {
1269 Some(&p[1..])
1270 } else {
1271 None
1272 }
1273 });
1274 self.render_at_depth(body, body_x, y, depth, body_cursor, cursor_offset);
1275 }
1276
1277 /// Render a matrix
1278 fn render_matrix(
1279 &self,
1280 rows: &[Vec<MathBox>],
1281 x: f64,
1282 y: f64,
1283 depth: u32,
1284 cursor_path: Option<&[usize]>,
1285 cursor_offset: usize,
1286 ) {
1287 if rows.is_empty() {
1288 return;
1289 }
1290
1291 let scale = self.scale_for_depth(depth);
1292 let font_size = self.layout_engine.base_font_size * scale;
1293 let cell_padding = font_size * 0.3;
1294 let bracket_width = font_size * 0.2;
1295
1296 let num_cols = rows.iter().map(|r| r.len()).max().unwrap_or(0);
1297 if num_cols == 0 {
1298 return;
1299 }
1300
1301 // Compute cell layouts
1302 let cell_layouts: Vec<Vec<LayoutBox>> = rows
1303 .iter()
1304 .map(|row| {
1305 row.iter()
1306 .map(|cell| self.layout_engine.layout(cell, self.ctx))
1307 .collect()
1308 })
1309 .collect();
1310
1311 // Find column widths and row heights
1312 let mut col_widths = vec![0.0f64; num_cols];
1313 let mut row_heights = vec![0.0f64; rows.len()];
1314
1315 for (r, row) in cell_layouts.iter().enumerate() {
1316 for (c, cell) in row.iter().enumerate() {
1317 col_widths[c] = col_widths[c].max(cell.width);
1318 row_heights[r] = row_heights[r].max(cell.height());
1319 }
1320 }
1321
1322 let total_width: f64 = col_widths.iter().sum::<f64>()
1323 + cell_padding * (num_cols as f64 - 1.0)
1324 + 2.0 * bracket_width;
1325 let total_height: f64 =
1326 row_heights.iter().sum::<f64>() + cell_padding * (rows.len() as f64 - 1.0);
1327
1328 // Draw brackets
1329 self.ctx.save().unwrap();
1330 self.set_color(&self.fg_color);
1331 self.ctx.set_line_width(1.5 * scale);
1332
1333 // Left bracket
1334 let bracket_gap = font_size * 0.15;
1335 self.ctx.move_to(x + bracket_gap, y - total_height / 2.0);
1336 self.ctx.line_to(x, y - total_height / 2.0);
1337 self.ctx.line_to(x, y + total_height / 2.0);
1338 self.ctx.line_to(x + bracket_gap, y + total_height / 2.0);
1339 self.ctx.stroke().unwrap();
1340
1341 // Right bracket
1342 let right_x = x + total_width - bracket_width;
1343 self.ctx
1344 .move_to(right_x - bracket_gap, y - total_height / 2.0);
1345 self.ctx.line_to(right_x, y - total_height / 2.0);
1346 self.ctx.line_to(right_x, y + total_height / 2.0);
1347 self.ctx
1348 .line_to(right_x - bracket_gap, y + total_height / 2.0);
1349 self.ctx.stroke().unwrap();
1350
1351 self.ctx.restore().unwrap();
1352
1353 // Draw cells
1354 let mut cell_y = y - total_height / 2.0;
1355 let mut cell_idx = 0;
1356
1357 for (r, row) in rows.iter().enumerate() {
1358 let row_h = row_heights[r];
1359 let mut cell_x = x + bracket_width;
1360
1361 for (c, cell) in row.iter().enumerate() {
1362 let col_w = col_widths[c];
1363 let layout = &cell_layouts[r][c];
1364
1365 // Center cell in its column
1366 let cx = cell_x + (col_w - layout.width) / 2.0;
1367 let cy = cell_y + row_h / 2.0;
1368
1369 let cell_cursor = cursor_path.and_then(|p| {
1370 if !p.is_empty() && p[0] == cell_idx {
1371 Some(&p[1..])
1372 } else {
1373 None
1374 }
1375 });
1376 self.render_at_depth(cell, cx, cy, depth, cell_cursor, cursor_offset);
1377
1378 cell_x += col_w + cell_padding;
1379 cell_idx += 1;
1380 }
1381
1382 cell_y += row_h + cell_padding;
1383 }
1384 }
1385
1386 /// Render a horizontal row
1387 fn render_row(
1388 &self,
1389 items: &[MathBox],
1390 x: f64,
1391 y: f64,
1392 depth: u32,
1393 cursor_path: Option<&[usize]>,
1394 cursor_offset: usize,
1395 ) {
1396 let mut current_x = x;
1397 let row_cursor_here = cursor_path.map(|p| p.is_empty()).unwrap_or(false);
1398 let row_cursor_idx = cursor_offset.min(items.len());
1399 let scale = self.scale_for_depth(depth);
1400 let font_size = self.layout_engine.base_font_size * scale;
1401 let mut pending_cursor_x = None;
1402
1403 for (i, item) in items.iter().enumerate() {
1404 if row_cursor_here && i == row_cursor_idx {
1405 pending_cursor_x = Some(current_x);
1406 }
1407
1408 let item_cursor = cursor_path.and_then(|p| {
1409 if !p.is_empty() && p[0] == i {
1410 Some(&p[1..])
1411 } else {
1412 None
1413 }
1414 });
1415 self.render_at_depth(item, current_x, y, depth, item_cursor, cursor_offset);
1416
1417 let layout = self.layout_engine.layout_with_depth(item, self.ctx, depth);
1418 current_x += layout.width;
1419 }
1420
1421 if row_cursor_here && self.cursor_visible.get() {
1422 let cursor_x = if row_cursor_idx == items.len() {
1423 current_x
1424 } else {
1425 pending_cursor_x.unwrap_or(current_x)
1426 };
1427 self.draw_cursor(cursor_x, y, font_size);
1428 }
1429 }
1430
1431 /// Set the current drawing color
1432 fn set_color(&self, color: &Color) {
1433 self.ctx.set_source_rgba(color.r, color.g, color.b, color.a);
1434 }
1435
1436 fn slot_geometry(font_size: f64) -> (f64, f64, f64) {
1437 let width = (font_size * 0.86).max(10.0);
1438 let height = (font_size * 0.9).max(11.0);
1439 let ascent = height * 0.62;
1440 (width, height, ascent)
1441 }
1442 }
1443
1444 /// Convert a number to superscript Unicode digits
1445 fn superscript_digits(n: u32) -> String {
1446 n.to_string()
1447 .chars()
1448 .map(|c| match c {
1449 '0' => '⁰',
1450 '1' => '¹',
1451 '2' => '²',
1452 '3' => '³',
1453 '4' => '⁴',
1454 '5' => '⁵',
1455 '6' => '⁶',
1456 '7' => '⁷',
1457 '8' => '⁸',
1458 '9' => '⁹',
1459 _ => c,
1460 })
1461 .collect()
1462 }
1463