gardesk/garcalc / 206b11b

Browse files

Expand MathBox input templates and pretty rendering

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
206b11b5ef604d9492bb4ea6cca9d4beb010cd0d
Parents
ca03027
Tree
579012e

6 changed files

StatusFile+-
M garcalc-math/src/convert.rs 312 100
M garcalc-math/src/input.rs 1194 120
M garcalc-math/src/layout.rs 348 142
M garcalc-math/src/lib.rs 6 6
M garcalc-math/src/mathbox.rs 87 40
M garcalc-math/src/render.rs 717 169
garcalc-math/src/convert.rsmodified
@@ -4,8 +4,8 @@
44
 //! - MathBox -> Expr for evaluation
55
 //! - Expr -> MathBox for result display
66
 
7
-use crate::mathbox::{MathBox, Operator, LimitDirection as MathLimitDirection};
8
-use garcalc_cas::expr::{Expr, Rational, Symbol, LimitDirection, Sign};
7
+use crate::mathbox::{LimitDirection as MathLimitDirection, MathBox, Operator};
8
+use garcalc_cas::expr::{Expr, LimitDirection, Rational, Sign, Symbol};
99
 use thiserror::Error;
1010
 
1111
 /// Errors that can occur during conversion
@@ -46,7 +46,9 @@ pub fn to_expr(mathbox: &MathBox) -> Result<Expr, ConvertError> {
4646
 
4747
         MathBox::Subscript { base, sub } => {
4848
             // Subscripted variables become a combined symbol
49
-            if let (MathBox::Symbol(b), MathBox::Number(s) | MathBox::Symbol(s)) = (base.as_ref(), sub.as_ref()) {
49
+            if let (MathBox::Symbol(b), MathBox::Number(s) | MathBox::Symbol(s)) =
50
+                (base.as_ref(), sub.as_ref())
51
+            {
5052
                 Ok(Expr::Symbol(Symbol::new(format!("{}_{}", b, s))))
5153
             } else {
5254
                 let base_expr = to_expr(base)?;
@@ -81,7 +83,12 @@ pub fn to_expr(mathbox: &MathBox) -> Result<Expr, ConvertError> {
8183
 
8284
         MathBox::Parens(inner) => to_expr(inner),
8385
 
84
-        MathBox::Integral { lower, upper, body, var } => {
86
+        MathBox::Integral {
87
+            lower,
88
+            upper,
89
+            body,
90
+            var,
91
+        } => {
8592
             let body_expr = to_expr(body)?;
8693
             let var_sym = Symbol::new(extract_symbol_str(var)?);
8794
 
@@ -112,7 +119,12 @@ pub fn to_expr(mathbox: &MathBox) -> Result<Expr, ConvertError> {
112119
             })
113120
         }
114121
 
115
-        MathBox::Limit { var, to, direction, body } => {
122
+        MathBox::Limit {
123
+            var,
124
+            to,
125
+            direction,
126
+            body,
127
+        } => {
116128
             let body_expr = to_expr(body)?;
117129
             let var_sym = Symbol::new(extract_symbol_str(var)?);
118130
             let point_expr = to_expr(to)?;
@@ -130,7 +142,12 @@ pub fn to_expr(mathbox: &MathBox) -> Result<Expr, ConvertError> {
130142
             })
131143
         }
132144
 
133
-        MathBox::Sum { var, lower, upper, body } => {
145
+        MathBox::Sum {
146
+            var,
147
+            lower,
148
+            upper,
149
+            body,
150
+        } => {
134151
             let var_sym = Symbol::new(extract_symbol_str(var)?);
135152
             let lower_expr = to_expr(lower)?;
136153
             let upper_expr = to_expr(upper)?;
@@ -144,7 +161,12 @@ pub fn to_expr(mathbox: &MathBox) -> Result<Expr, ConvertError> {
144161
             })
145162
         }
146163
 
147
-        MathBox::Product { var, lower, upper, body } => {
164
+        MathBox::Product {
165
+            var,
166
+            lower,
167
+            upper,
168
+            body,
169
+        } => {
148170
             let var_sym = Symbol::new(extract_symbol_str(var)?);
149171
             let lower_expr = to_expr(lower)?;
150172
             let upper_expr = to_expr(upper)?;
@@ -249,22 +271,35 @@ pub fn from_expr(expr: &Expr) -> MathBox {
249271
 
250272
         Expr::Mul(factors) => {
251273
             // Check for division pattern (x * y^-1)
252
-            let (numerator, denominator): (Vec<_>, Vec<_>) = factors.iter().partition(|f| !is_reciprocal(f));
274
+            let (numerator, denominator): (Vec<_>, Vec<_>) =
275
+                factors.iter().partition(|f| !is_reciprocal(f));
276
+
277
+            let mut numerator_factors: Vec<Expr> = numerator.into_iter().cloned().collect();
278
+            numerator_factors.retain(|f| !is_multiplicative_identity(f));
253279
 
254280
             if !denominator.is_empty() {
255
-                let num_box = if numerator.is_empty() {
281
+                let den_factors: Vec<Expr> = denominator
282
+                    .iter()
283
+                    .map(|f| extract_base_of_reciprocal(f))
284
+                    .filter(|f| !is_multiplicative_identity(f))
285
+                    .collect();
286
+
287
+                let num_box = if numerator_factors.is_empty() {
256288
                     MathBox::Number("1".to_string())
257
-                } else if numerator.len() == 1 {
258
-                    from_expr(numerator[0])
289
+                } else if numerator_factors.len() == 1 {
290
+                    render_mul_factor(&numerator_factors[0])
259291
                 } else {
260
-                    from_expr(&Expr::Mul(numerator.into_iter().cloned().collect()))
292
+                    from_mul_factors(&numerator_factors)
261293
                 };
262294
 
263
-                let den_factors: Vec<Expr> = denominator.iter().map(|f| extract_base_of_reciprocal(f)).collect();
295
+                if den_factors.is_empty() {
296
+                    return num_box;
297
+                }
298
+
264299
                 let den_box = if den_factors.len() == 1 {
265
-                    from_expr(&den_factors[0])
300
+                    render_mul_factor(&den_factors[0])
266301
                 } else {
267
-                    from_expr(&Expr::Mul(den_factors))
302
+                    from_mul_factors(&den_factors)
268303
                 };
269304
 
270305
                 return MathBox::Fraction {
@@ -274,17 +309,12 @@ pub fn from_expr(expr: &Expr) -> MathBox {
274309
             }
275310
 
276311
             // Normal multiplication
277
-            let mut items = Vec::new();
278
-            for (i, factor) in factors.iter().enumerate() {
279
-                if i > 0 {
280
-                    items.push(MathBox::Operator(Operator::Mul));
281
-                }
282
-                items.push(from_expr(factor));
283
-            }
284
-            if items.len() == 1 {
285
-                items.pop().unwrap()
312
+            if numerator_factors.is_empty() {
313
+                MathBox::Number("1".to_string())
314
+            } else if numerator_factors.len() == 1 {
315
+                render_mul_factor(&numerator_factors[0])
286316
             } else {
287
-                MathBox::Row(items)
317
+                from_mul_factors(&numerator_factors)
288318
             }
289319
         }
290320
 
@@ -310,14 +340,21 @@ pub fn from_expr(expr: &Expr) -> MathBox {
310340
             }
311341
         }
312342
 
313
-        Expr::Neg(inner) => {
314
-            MathBox::Row(vec![
315
-                MathBox::Operator(Operator::Sub),
316
-                from_expr(inner),
317
-            ])
318
-        }
343
+        Expr::Neg(inner) => MathBox::Row(vec![MathBox::Operator(Operator::Sub), from_expr(inner)]),
319344
 
320345
         Expr::Func(name, args) => {
346
+            if name == "factorial" && args.len() == 1 {
347
+                let arg_expr = &args[0];
348
+                let arg_box = from_expr(arg_expr);
349
+                let formatted_arg = if factorial_arg_needs_parens(arg_expr) {
350
+                    MathBox::Parens(Box::new(arg_box))
351
+                } else {
352
+                    arg_box
353
+                };
354
+
355
+                return MathBox::Row(vec![formatted_arg, MathBox::Symbol("!".to_string())]);
356
+            }
357
+
321358
             let arg_boxes: Vec<MathBox> = args.iter().map(from_expr).collect();
322359
 
323360
             // Special functions get special rendering
@@ -332,52 +369,62 @@ pub fn from_expr(expr: &Expr) -> MathBox {
332369
             }
333370
         }
334371
 
335
-        Expr::Derivative { expr, var, order } => {
336
-            MathBox::Derivative {
337
-                order: *order,
338
-                var: Box::new(MathBox::Symbol(var.0.clone())),
339
-                body: Box::new(from_expr(expr)),
340
-            }
341
-        }
342
-
343
-        Expr::Integral { expr, var, lower, upper } => {
344
-            MathBox::Integral {
345
-                lower: lower.as_ref().map(|lo| Box::new(from_expr(lo))),
346
-                upper: upper.as_ref().map(|hi| Box::new(from_expr(hi))),
347
-                body: Box::new(from_expr(expr)),
348
-                var: Box::new(MathBox::Symbol(var.0.clone())),
349
-            }
350
-        }
351
-
352
-        Expr::Limit { expr, var, point, direction } => {
353
-            MathBox::Limit {
354
-                var: Box::new(MathBox::Symbol(var.0.clone())),
355
-                to: Box::new(from_expr(point)),
356
-                direction: direction.map(|d| match d {
357
-                    LimitDirection::Left => MathLimitDirection::FromLeft,
358
-                    LimitDirection::Right => MathLimitDirection::FromRight,
359
-                }),
360
-                body: Box::new(from_expr(expr)),
361
-            }
362
-        }
363
-
364
-        Expr::Sum { expr, var, lower, upper } => {
365
-            MathBox::Sum {
366
-                var: Box::new(MathBox::Symbol(var.0.clone())),
367
-                lower: Box::new(from_expr(lower)),
368
-                upper: Box::new(from_expr(upper)),
369
-                body: Box::new(from_expr(expr)),
370
-            }
371
-        }
372
-
373
-        Expr::Product { expr, var, lower, upper } => {
374
-            MathBox::Product {
375
-                var: Box::new(MathBox::Symbol(var.0.clone())),
376
-                lower: Box::new(from_expr(lower)),
377
-                upper: Box::new(from_expr(upper)),
378
-                body: Box::new(from_expr(expr)),
379
-            }
380
-        }
372
+        Expr::Derivative { expr, var, order } => MathBox::Derivative {
373
+            order: *order,
374
+            var: Box::new(MathBox::Symbol(var.0.clone())),
375
+            body: Box::new(from_expr(expr)),
376
+        },
377
+
378
+        Expr::Integral {
379
+            expr,
380
+            var,
381
+            lower,
382
+            upper,
383
+        } => MathBox::Integral {
384
+            lower: lower.as_ref().map(|lo| Box::new(from_expr(lo))),
385
+            upper: upper.as_ref().map(|hi| Box::new(from_expr(hi))),
386
+            body: Box::new(from_expr(expr)),
387
+            var: Box::new(MathBox::Symbol(var.0.clone())),
388
+        },
389
+
390
+        Expr::Limit {
391
+            expr,
392
+            var,
393
+            point,
394
+            direction,
395
+        } => MathBox::Limit {
396
+            var: Box::new(MathBox::Symbol(var.0.clone())),
397
+            to: Box::new(from_expr(point)),
398
+            direction: direction.map(|d| match d {
399
+                LimitDirection::Left => MathLimitDirection::FromLeft,
400
+                LimitDirection::Right => MathLimitDirection::FromRight,
401
+            }),
402
+            body: Box::new(from_expr(expr)),
403
+        },
404
+
405
+        Expr::Sum {
406
+            expr,
407
+            var,
408
+            lower,
409
+            upper,
410
+        } => MathBox::Sum {
411
+            var: Box::new(MathBox::Symbol(var.0.clone())),
412
+            lower: Box::new(from_expr(lower)),
413
+            upper: Box::new(from_expr(upper)),
414
+            body: Box::new(from_expr(expr)),
415
+        },
416
+
417
+        Expr::Product {
418
+            expr,
419
+            var,
420
+            lower,
421
+            upper,
422
+        } => MathBox::Product {
423
+            var: Box::new(MathBox::Symbol(var.0.clone())),
424
+            lower: Box::new(from_expr(lower)),
425
+            upper: Box::new(from_expr(upper)),
426
+            body: Box::new(from_expr(expr)),
427
+        },
381428
 
382429
         Expr::Matrix(rows) => {
383430
             let box_rows: Vec<Vec<MathBox>> = rows
@@ -387,29 +434,27 @@ pub fn from_expr(expr: &Expr) -> MathBox {
387434
             MathBox::Matrix { rows: box_rows }
388435
         }
389436
 
390
-        Expr::Equation(lhs, rhs) => {
391
-            MathBox::Row(vec![
392
-                from_expr(lhs),
393
-                MathBox::Operator(Operator::Eq),
394
-                from_expr(rhs),
395
-            ])
396
-        }
437
+        Expr::Equation(lhs, rhs) => MathBox::Row(vec![
438
+            from_expr(lhs),
439
+            MathBox::Operator(Operator::Eq),
440
+            from_expr(rhs),
441
+        ]),
397442
 
398443
         Expr::Undefined => MathBox::Symbol("undefined".to_string()),
399
-        Expr::Infinity(sign) => {
400
-            match sign {
401
-                Sign::Positive => MathBox::Symbol("∞".to_string()),
402
-                Sign::Negative => MathBox::Row(vec![
403
-                    MathBox::Operator(Operator::Sub),
404
-                    MathBox::Symbol("∞".to_string()),
405
-                ]),
406
-            }
407
-        }
444
+        Expr::Infinity(sign) => match sign {
445
+            Sign::Positive => MathBox::Symbol("∞".to_string()),
446
+            Sign::Negative => MathBox::Row(vec![
447
+                MathBox::Operator(Operator::Sub),
448
+                MathBox::Symbol("∞".to_string()),
449
+            ]),
450
+        },
408451
 
409452
         Expr::Vector(elems) => {
410453
             // Display vector as a row matrix
411454
             let box_row: Vec<MathBox> = elems.iter().map(from_expr).collect();
412
-            MathBox::Matrix { rows: vec![box_row] }
455
+            MathBox::Matrix {
456
+                rows: vec![box_row],
457
+            }
413458
         }
414459
 
415460
         Expr::Inequality { lhs, op, rhs } => {
@@ -421,11 +466,7 @@ pub fn from_expr(expr: &Expr) -> MathBox {
421466
                 InequalityOp::Ge => MathBox::Operator(Operator::Ge),
422467
                 InequalityOp::Ne => MathBox::Operator(Operator::Ne),
423468
             };
424
-            MathBox::Row(vec![
425
-                from_expr(lhs),
426
-                op_box,
427
-                from_expr(rhs),
428
-            ])
469
+            MathBox::Row(vec![from_expr(lhs), op_box, from_expr(rhs)])
429470
         }
430471
     }
431472
 }
@@ -470,6 +511,14 @@ fn convert_row(items: &[MathBox]) -> Result<Expr, ConvertError> {
470511
             MathBox::Operator(op) => {
471512
                 operators.push(*op);
472513
             }
514
+            MathBox::Symbol(s) if s == "!" => {
515
+                let Some(last) = operands.pop() else {
516
+                    return Err(ConvertError::MissingField(
517
+                        "factorial operand before '!'".to_string(),
518
+                    ));
519
+                };
520
+                operands.push(Expr::Func("factorial".to_string(), vec![last]));
521
+            }
473522
             other => {
474523
                 operands.push(to_expr(other)?);
475524
             }
@@ -507,7 +556,10 @@ fn format_float(f: f64) -> String {
507556
     if f == f.trunc() && f.abs() < 1e15 {
508557
         format!("{:.0}", f)
509558
     } else {
510
-        format!("{:.10}", f).trim_end_matches('0').trim_end_matches('.').to_string()
559
+        format!("{:.10}", f)
560
+            .trim_end_matches('0')
561
+            .trim_end_matches('.')
562
+            .to_string()
511563
     }
512564
 }
513565
 
@@ -542,6 +594,53 @@ fn extract_base_of_reciprocal(expr: &Expr) -> Expr {
542594
     }
543595
 }
544596
 
597
+fn is_multiplicative_identity(expr: &Expr) -> bool {
598
+    match expr {
599
+        Expr::Integer(1) => true,
600
+        Expr::Rational(r) if r.den != 0 && r.num == r.den => true,
601
+        Expr::Float(f) if *f == 1.0 => true,
602
+        _ => false,
603
+    }
604
+}
605
+
606
+fn mul_factor_needs_parens(expr: &Expr) -> bool {
607
+    matches!(
608
+        expr,
609
+        Expr::Add(_) | Expr::Equation(_, _) | Expr::Inequality { .. }
610
+    )
611
+}
612
+
613
+fn render_mul_factor(expr: &Expr) -> MathBox {
614
+    let rendered = from_expr(expr);
615
+    if mul_factor_needs_parens(expr) {
616
+        MathBox::Parens(Box::new(rendered))
617
+    } else {
618
+        rendered
619
+    }
620
+}
621
+
622
+fn from_mul_factors(factors: &[Expr]) -> MathBox {
623
+    let mut items = Vec::new();
624
+    for (i, factor) in factors.iter().enumerate() {
625
+        if i > 0 {
626
+            items.push(MathBox::Operator(Operator::Mul));
627
+        }
628
+        items.push(render_mul_factor(factor));
629
+    }
630
+    if items.len() == 1 {
631
+        items.pop().unwrap()
632
+    } else {
633
+        MathBox::Row(items)
634
+    }
635
+}
636
+
637
+fn factorial_arg_needs_parens(arg: &Expr) -> bool {
638
+    !matches!(
639
+        arg,
640
+        Expr::Integer(_) | Expr::Rational(_) | Expr::Float(_) | Expr::Symbol(_) | Expr::Func(_, _)
641
+    )
642
+}
643
+
545644
 #[cfg(test)]
546645
 mod tests {
547646
     use super::*;
@@ -575,4 +674,117 @@ mod tests {
575674
 
576675
         assert!(matches!(mb, MathBox::Fraction { .. }));
577676
     }
677
+
678
+    #[test]
679
+    fn test_factorial_renders_as_postfix_bang() {
680
+        let expr = Expr::Func(
681
+            "factorial".to_string(),
682
+            vec![Expr::Symbol(Symbol::new("n"))],
683
+        );
684
+        let mb = from_expr(&expr);
685
+
686
+        if let MathBox::Row(items) = mb {
687
+            assert_eq!(items.len(), 2);
688
+            assert!(matches!(&items[0], MathBox::Symbol(s) if s == "n"));
689
+            assert!(matches!(&items[1], MathBox::Symbol(s) if s == "!"));
690
+        } else {
691
+            panic!("expected row for factorial rendering");
692
+        }
693
+    }
694
+
695
+    #[test]
696
+    fn test_factorial_wraps_complex_arg_in_parens() {
697
+        let expr = Expr::Func(
698
+            "factorial".to_string(),
699
+            vec![Expr::Add(vec![
700
+                Expr::Symbol(Symbol::new("n")),
701
+                Expr::Integer(1),
702
+            ])],
703
+        );
704
+        let mb = from_expr(&expr);
705
+
706
+        if let MathBox::Row(items) = mb {
707
+            assert!(matches!(&items[0], MathBox::Parens(_)));
708
+            assert!(matches!(&items[1], MathBox::Symbol(s) if s == "!"));
709
+        } else {
710
+            panic!("expected row for factorial rendering");
711
+        }
712
+    }
713
+
714
+    #[test]
715
+    fn test_row_postfix_factorial_converts_to_expr() {
716
+        let mb = MathBox::Row(vec![
717
+            MathBox::Symbol("n".to_string()),
718
+            MathBox::Symbol("!".to_string()),
719
+        ]);
720
+
721
+        let expr = to_expr(&mb).unwrap();
722
+        assert_eq!(
723
+            expr,
724
+            Expr::Func(
725
+                "factorial".to_string(),
726
+                vec![Expr::Symbol(Symbol::new("n"))]
727
+            )
728
+        );
729
+    }
730
+
731
+    #[test]
732
+    fn test_from_expr_fraction_drops_unity_factor_in_numerator() {
733
+        let expr = Expr::Mul(vec![
734
+            Expr::Integer(1),
735
+            Expr::Symbol(Symbol::new("n")),
736
+            Expr::Pow(Box::new(Expr::Integer(2)), Box::new(Expr::Integer(-1))),
737
+        ]);
738
+
739
+        let mb = from_expr(&expr);
740
+        if let MathBox::Fraction { num, den } = mb {
741
+            assert!(!matches!(num.as_ref(), MathBox::Number(s) if s == "1"));
742
+            assert!(matches!(den.as_ref(), MathBox::Number(s) if s == "2"));
743
+        } else {
744
+            panic!("expected fraction");
745
+        }
746
+    }
747
+
748
+    #[test]
749
+    fn test_from_expr_mul_wraps_additive_factor_with_parens() {
750
+        let expr = Expr::Mul(vec![
751
+            Expr::Symbol(Symbol::new("n")),
752
+            Expr::Add(vec![Expr::Integer(1), Expr::Symbol(Symbol::new("n"))]),
753
+        ]);
754
+
755
+        let mb = from_expr(&expr);
756
+        if let MathBox::Row(items) = mb {
757
+            assert_eq!(items.len(), 3);
758
+            assert!(matches!(&items[0], MathBox::Symbol(s) if s == "n"));
759
+            assert!(matches!(&items[1], MathBox::Operator(Operator::Mul)));
760
+            assert!(matches!(&items[2], MathBox::Parens(_)));
761
+        } else {
762
+            panic!("expected multiplication row");
763
+        }
764
+    }
765
+
766
+    #[test]
767
+    fn test_from_expr_fraction_numerator_hides_one_and_wraps_additive_factor() {
768
+        let expr = Expr::Mul(vec![
769
+            Expr::Integer(1),
770
+            Expr::Symbol(Symbol::new("n")),
771
+            Expr::Add(vec![Expr::Integer(1), Expr::Symbol(Symbol::new("n"))]),
772
+            Expr::Pow(Box::new(Expr::Integer(2)), Box::new(Expr::Integer(-1))),
773
+        ]);
774
+
775
+        let mb = from_expr(&expr);
776
+        if let MathBox::Fraction { num, den } = mb {
777
+            assert!(matches!(den.as_ref(), MathBox::Number(s) if s == "2"));
778
+            if let MathBox::Row(items) = num.as_ref() {
779
+                assert_eq!(items.len(), 3);
780
+                assert!(matches!(&items[0], MathBox::Symbol(s) if s == "n"));
781
+                assert!(matches!(&items[1], MathBox::Operator(Operator::Mul)));
782
+                assert!(matches!(&items[2], MathBox::Parens(_)));
783
+            } else {
784
+                panic!("expected row in fraction numerator");
785
+            }
786
+        } else {
787
+            panic!("expected fraction");
788
+        }
789
+    }
578790
 }
garcalc-math/src/input.rsmodified
1494 lines changed — click to load
@@ -3,7 +3,7 @@
33
 //! Provides structured keyboard navigation through a MathBox tree
44
 //! with support for template insertion via commands.
55
 
6
-use crate::mathbox::{MathBox, Cursor, Operator};
6
+use crate::mathbox::{Cursor, MathBox, Operator};
77
 
88
 /// Result of processing a key event
99
 #[derive(Debug, Clone, PartialEq)]
@@ -35,6 +35,32 @@ impl Default for MathInput {
3535
 }
3636
 
3737
 impl MathInput {
38
+    fn char_count(s: &str) -> usize {
39
+        s.chars().count()
40
+    }
41
+
42
+    fn byte_index_at_char(s: &str, char_idx: usize) -> usize {
43
+        let target = char_idx.min(Self::char_count(s));
44
+        if target == 0 {
45
+            return 0;
46
+        }
47
+        s.char_indices()
48
+            .nth(target)
49
+            .map(|(idx, _)| idx)
50
+            .unwrap_or_else(|| s.len())
51
+    }
52
+
53
+    fn remove_char_at(s: &mut String, char_idx: usize) -> bool {
54
+        let len = Self::char_count(s);
55
+        if char_idx >= len {
56
+            return false;
57
+        }
58
+        let start = Self::byte_index_at_char(s, char_idx);
59
+        let end = Self::byte_index_at_char(s, char_idx + 1);
60
+        s.drain(start..end);
61
+        true
62
+    }
63
+
3864
     /// Create a new empty input
3965
     pub fn new() -> Self {
4066
         let mut cursor = Cursor::new();
@@ -89,7 +115,7 @@ impl MathInput {
89115
 
90116
         // Normal input
91117
         match ch {
92
-            '\\' => {
118
+            '\\' | ':' => {
93119
                 // Enter command mode
94120
                 self.command_buffer = Some(String::new());
95121
                 InputResult::Consumed
@@ -104,6 +130,11 @@ impl MathInput {
104130
                 self.wrap_in_power();
105131
                 InputResult::Consumed
106132
             }
133
+            '!' => {
134
+                // Apply factorial to current element
135
+                self.wrap_in_factorial();
136
+                InputResult::Consumed
137
+            }
107138
             '_' => {
108139
                 // Convert current element to subscript base
109140
                 self.wrap_in_subscript();
@@ -166,9 +197,35 @@ impl MathInput {
166197
 
167198
     /// Handle a special key
168199
     pub fn handle_key(&mut self, key: SpecialKey) -> InputResult {
169
-        // Cancel command mode on special keys
170
-        if self.command_buffer.is_some() && !matches!(key, SpecialKey::Backspace) {
171
-            self.command_buffer = None;
200
+        if self.command_buffer.is_some() {
201
+            match key {
202
+                SpecialKey::Enter => {
203
+                    if let Some(cmd) = self.command_buffer.take() {
204
+                        if cmd.is_empty() {
205
+                            return InputResult::Consumed;
206
+                        }
207
+                        return self.execute_command(&cmd);
208
+                    }
209
+                    return InputResult::Ignored;
210
+                }
211
+                SpecialKey::Escape => {
212
+                    self.command_buffer = None;
213
+                    return InputResult::Consumed;
214
+                }
215
+                SpecialKey::Backspace => {
216
+                    if let Some(ref mut buf) = self.command_buffer {
217
+                        buf.pop();
218
+                        if buf.is_empty() {
219
+                            self.command_buffer = None;
220
+                        }
221
+                    }
222
+                    return InputResult::Consumed;
223
+                }
224
+                _ => {
225
+                    // For navigation/editing keys, leave command mode and continue handling the key.
226
+                    self.command_buffer = None;
227
+                }
228
+            }
172229
         }
173230
 
174231
         match key {
@@ -197,25 +254,10 @@ impl MathInput {
197254
                 InputResult::Consumed
198255
             }
199256
             SpecialKey::Enter => InputResult::Evaluate,
200
-            SpecialKey::Escape => {
201
-                if self.command_buffer.is_some() {
202
-                    self.command_buffer = None;
203
-                    InputResult::Consumed
204
-                } else {
205
-                    InputResult::Cancel
206
-                }
207
-            }
257
+            SpecialKey::Escape => InputResult::Cancel,
208258
             SpecialKey::Backspace => {
209
-                if let Some(ref mut buf) = self.command_buffer {
210
-                    buf.pop();
211
-                    if buf.is_empty() {
212
-                        self.command_buffer = None;
213
-                    }
214
-                    InputResult::Consumed
215
-                } else {
216
-                    self.delete_at_cursor();
217
-                    InputResult::Consumed
218
-                }
259
+                self.delete_at_cursor();
260
+                InputResult::Consumed
219261
             }
220262
             SpecialKey::Delete => {
221263
                 self.delete_forward();
@@ -244,6 +286,10 @@ impl MathInput {
244286
             "lim" | "limit" => Some(MathBox::limit_template()),
245287
             "sum" => Some(MathBox::sum_template()),
246288
             "prod" | "product" => Some(MathBox::product_template()),
289
+            "solve" => Some(MathBox::Func {
290
+                name: "solve".to_string(),
291
+                args: vec![MathBox::Slot, MathBox::Symbol("x".to_string())],
292
+            }),
247293
             "abs" => Some(MathBox::Abs(Box::new(MathBox::Slot))),
248294
             "pi" => Some(MathBox::Symbol("π".to_string())),
249295
             "theta" => Some(MathBox::Symbol("θ".to_string())),
@@ -257,12 +303,10 @@ impl MathInput {
257303
             "sigma" => Some(MathBox::Symbol("σ".to_string())),
258304
             "omega" => Some(MathBox::Symbol("ω".to_string())),
259305
             "inf" | "infinity" => Some(MathBox::Symbol("∞".to_string())),
260
-            "sin" | "cos" | "tan" | "ln" | "log" | "exp" => {
261
-                Some(MathBox::Func {
262
-                    name: cmd.to_lowercase(),
263
-                    args: vec![MathBox::Slot],
264
-                })
265
-            }
306
+            "sin" | "cos" | "tan" | "ln" | "log" | "exp" => Some(MathBox::Func {
307
+                name: cmd.to_lowercase(),
308
+                args: vec![MathBox::Slot],
309
+            }),
266310
             "matrix" => Some(MathBox::matrix_template(2, 2)),
267311
             _ => None,
268312
         };
@@ -282,12 +326,23 @@ impl MathInput {
282326
             if current.is_slot() {
283327
                 *current = template;
284328
                 // Move cursor into first child if it's a container
285
-                if self.get_current().map(|c| c.child_count() > 0).unwrap_or(false) {
286
-                    self.cursor.enter(0);
329
+                if self
330
+                    .get_current()
331
+                    .map(|c| c.child_count() > 0)
332
+                    .unwrap_or(false)
333
+                {
334
+                    self.focus_first_slot_in_current_subtree();
287335
                 }
288336
             } else {
289337
                 // Insert after current
290
-                self.insert_after_current(template);
338
+                if self.insert_after_current(template)
339
+                    && self
340
+                        .get_current()
341
+                        .map(|c| c.child_count() > 0)
342
+                        .unwrap_or(false)
343
+                {
344
+                    self.focus_first_slot_in_current_subtree();
345
+                }
291346
             }
292347
         }
293348
     }
@@ -334,39 +389,78 @@ impl MathInput {
334389
         }
335390
     }
336391
 
392
+    /// Wrap the current element in factorial
393
+    fn wrap_in_factorial(&mut self) {
394
+        if let Some(current) = self.get_current_mut() {
395
+            if !current.is_slot() {
396
+                let arg = std::mem::replace(current, MathBox::Slot);
397
+                *current = MathBox::Func {
398
+                    name: "factorial".to_string(),
399
+                    args: vec![arg],
400
+                };
401
+                self.cursor.offset = 0;
402
+            } else {
403
+                *current = MathBox::Func {
404
+                    name: "factorial".to_string(),
405
+                    args: vec![MathBox::Slot],
406
+                };
407
+                self.cursor.enter(0);
408
+            }
409
+        }
410
+    }
411
+
337412
     /// Append a digit to the current number
338413
     fn append_to_number(&mut self, ch: char) {
414
+        let offset = self.cursor.offset;
415
+        let mut new_offset = None;
339416
         if let Some(current) = self.get_current_mut() {
340417
             match current {
341418
                 MathBox::Number(s) => {
342
-                    s.push(ch);
419
+                    let insert_at = Self::byte_index_at_char(s, offset);
420
+                    s.insert(insert_at, ch);
421
+                    new_offset = Some(offset + 1);
343422
                 }
344423
                 MathBox::Slot => {
345424
                     *current = MathBox::Number(ch.to_string());
425
+                    new_offset = Some(1);
346426
                 }
347427
                 _ => {
348
-                    // Insert after current
349
-                    self.insert_after_current(MathBox::Number(ch.to_string()));
428
+                    if self.insert_after_current(MathBox::Number(ch.to_string())) {
429
+                        new_offset = Some(1);
430
+                    }
350431
                 }
351432
             }
352433
         }
434
+        if let Some(new_offset) = new_offset {
435
+            self.cursor.offset = new_offset;
436
+        }
353437
     }
354438
 
355439
     /// Append a character to the current symbol
356440
     fn append_to_symbol(&mut self, ch: char) {
441
+        let offset = self.cursor.offset;
442
+        let mut new_offset = None;
357443
         if let Some(current) = self.get_current_mut() {
358444
             match current {
359445
                 MathBox::Symbol(s) => {
360
-                    s.push(ch);
446
+                    let insert_at = Self::byte_index_at_char(s, offset);
447
+                    s.insert(insert_at, ch);
448
+                    new_offset = Some(offset + 1);
361449
                 }
362450
                 MathBox::Slot => {
363451
                     *current = MathBox::Symbol(ch.to_string());
452
+                    new_offset = Some(1);
364453
                 }
365454
                 _ => {
366
-                    self.insert_after_current(MathBox::Symbol(ch.to_string()));
455
+                    if self.insert_after_current(MathBox::Symbol(ch.to_string())) {
456
+                        new_offset = Some(1);
457
+                    }
367458
                 }
368459
             }
369460
         }
461
+        if let Some(new_offset) = new_offset {
462
+            self.cursor.offset = new_offset;
463
+        }
370464
     }
371465
 
372466
     /// Insert an element at the cursor position
@@ -381,16 +475,42 @@ impl MathInput {
381475
     }
382476
 
383477
     /// Insert an element after the current one (in a Row)
384
-    fn insert_after_current(&mut self, element: MathBox) {
385
-        // This is complex - need to handle row insertion
386
-        // For now, simplified implementation
387
-        if let Some(current) = self.get_current_mut() {
388
-            if matches!(current, MathBox::Row(_)) {
389
-                if let MathBox::Row(items) = current {
390
-                    items.push(element);
391
-                }
478
+    fn insert_after_current(&mut self, element: MathBox) -> bool {
479
+        let current_path = self.cursor.path.clone();
480
+
481
+        // Case 1: cursor is on a Row node; insert at the row offset.
482
+        if let Some(MathBox::Row(items)) = Self::get_node_mut_at_path(&mut self.root, &current_path)
483
+        {
484
+            let insert_idx = self.cursor.offset.min(items.len());
485
+            items.insert(insert_idx, element);
486
+            self.cursor.path = current_path;
487
+            self.cursor.enter(insert_idx);
488
+            return true;
489
+        }
490
+
491
+        // Case 2: parent is a Row; insert as next sibling.
492
+        if let Some((&idx, parent_path)) = current_path.split_last() {
493
+            if let Some(MathBox::Row(items)) =
494
+                Self::get_node_mut_at_path(&mut self.root, parent_path)
495
+            {
496
+                let insert_idx = (idx + 1).min(items.len());
497
+                items.insert(insert_idx, element);
498
+                self.cursor.path = parent_path.to_vec();
499
+                self.cursor.enter(insert_idx);
500
+                return true;
392501
             }
393502
         }
503
+
504
+        // Case 3: no row context; wrap current node into a Row and append.
505
+        if let Some(current) = Self::get_node_mut_at_path(&mut self.root, &current_path) {
506
+            let old = std::mem::replace(current, MathBox::Slot);
507
+            *current = MathBox::Row(vec![old, element]);
508
+            self.cursor.path = current_path;
509
+            self.cursor.enter(1);
510
+            return true;
511
+        }
512
+
513
+        false
394514
     }
395515
 
396516
     /// Try to exit current container (parens, etc.)
@@ -402,48 +522,152 @@ impl MathInput {
402522
 
403523
     /// Delete at cursor
404524
     fn delete_at_cursor(&mut self) {
405
-        if let Some(current) = self.get_current_mut() {
406
-            match current {
407
-                MathBox::Number(s) if !s.is_empty() => {
408
-                    s.pop();
409
-                    if s.is_empty() {
410
-                        *current = MathBox::Slot;
525
+        let path = self.cursor.path.clone();
526
+        let offset = self.cursor.offset;
527
+
528
+        match self.get_current() {
529
+            Some(MathBox::Number(_)) => {
530
+                if offset > 0 {
531
+                    let mut became_slot = false;
532
+                    let mut removed = false;
533
+                    if let Some(MathBox::Number(s)) =
534
+                        Self::get_node_mut_at_path(&mut self.root, &path)
535
+                    {
536
+                        removed = Self::remove_char_at(s, offset - 1);
537
+                        if s.is_empty() {
538
+                            became_slot = true;
539
+                        }
411540
                     }
412
-                }
413
-                MathBox::Symbol(s) if !s.is_empty() => {
414
-                    s.pop();
415
-                    if s.is_empty() {
416
-                        *current = MathBox::Slot;
541
+                    if became_slot {
542
+                        if let Some(current) = Self::get_node_mut_at_path(&mut self.root, &path) {
543
+                            *current = MathBox::Slot;
544
+                        }
545
+                        self.cursor.offset = 0;
546
+                    } else if removed {
547
+                        self.cursor.offset = offset - 1;
417548
                     }
549
+                } else {
550
+                    self.delete_previous_from_path(&path);
418551
                 }
419
-                _ => {
420
-                    // Replace with slot or exit
421
-                    if !self.cursor.is_at_root() {
422
-                        self.cursor.exit();
552
+            }
553
+            Some(MathBox::Symbol(_)) => {
554
+                if offset > 0 {
555
+                    let mut became_slot = false;
556
+                    let mut removed = false;
557
+                    if let Some(MathBox::Symbol(s)) =
558
+                        Self::get_node_mut_at_path(&mut self.root, &path)
559
+                    {
560
+                        removed = Self::remove_char_at(s, offset - 1);
561
+                        if s.is_empty() {
562
+                            became_slot = true;
563
+                        }
564
+                    }
565
+                    if became_slot {
566
+                        if let Some(current) = Self::get_node_mut_at_path(&mut self.root, &path) {
567
+                            *current = MathBox::Slot;
568
+                        }
569
+                        self.cursor.offset = 0;
570
+                    } else if removed {
571
+                        self.cursor.offset = offset - 1;
423572
                     }
573
+                } else {
574
+                    self.delete_previous_from_path(&path);
424575
                 }
425576
             }
577
+            Some(MathBox::Slot) => {
578
+                if !self.delete_current_slot_in_row(&path) {
579
+                    self.delete_previous_from_path(&path);
580
+                }
581
+            }
582
+            Some(_) => {
583
+                if !path.is_empty() {
584
+                    self.replace_current_with_slot(&path);
585
+                }
586
+            }
587
+            None => {}
426588
         }
427589
     }
428590
 
429591
     /// Delete forward
430592
     fn delete_forward(&mut self) {
431
-        // For now, same as backspace
432
-        self.delete_at_cursor();
593
+        let path = self.cursor.path.clone();
594
+        let offset = self.cursor.offset;
595
+
596
+        match self.get_current() {
597
+            Some(MathBox::Number(s)) => {
598
+                let len = Self::char_count(s);
599
+                if offset < len {
600
+                    let mut became_slot = false;
601
+                    if let Some(MathBox::Number(cur)) =
602
+                        Self::get_node_mut_at_path(&mut self.root, &path)
603
+                    {
604
+                        Self::remove_char_at(cur, offset);
605
+                        if cur.is_empty() {
606
+                            became_slot = true;
607
+                        }
608
+                    }
609
+                    if became_slot {
610
+                        if let Some(current) = Self::get_node_mut_at_path(&mut self.root, &path) {
611
+                            *current = MathBox::Slot;
612
+                        }
613
+                        self.cursor.offset = 0;
614
+                    }
615
+                } else {
616
+                    self.delete_next_from_path(&path);
617
+                }
618
+            }
619
+            Some(MathBox::Symbol(s)) => {
620
+                let len = Self::char_count(s);
621
+                if offset < len {
622
+                    let mut became_slot = false;
623
+                    if let Some(MathBox::Symbol(cur)) =
624
+                        Self::get_node_mut_at_path(&mut self.root, &path)
625
+                    {
626
+                        Self::remove_char_at(cur, offset);
627
+                        if cur.is_empty() {
628
+                            became_slot = true;
629
+                        }
630
+                    }
631
+                    if became_slot {
632
+                        if let Some(current) = Self::get_node_mut_at_path(&mut self.root, &path) {
633
+                            *current = MathBox::Slot;
634
+                        }
635
+                        self.cursor.offset = 0;
636
+                    }
637
+                } else {
638
+                    self.delete_next_from_path(&path);
639
+                }
640
+            }
641
+            Some(MathBox::Slot) => {
642
+                self.delete_next_from_path(&path);
643
+            }
644
+            Some(_) => {
645
+                if !path.is_empty() {
646
+                    self.replace_current_with_slot(&path);
647
+                }
648
+            }
649
+            None => {}
650
+        }
433651
     }
434652
 
435653
     /// Move cursor left
436654
     fn move_left(&mut self) {
437
-        if self.cursor.offset > 0 {
438
-            self.cursor.offset -= 1;
439
-        } else if !self.cursor.is_at_root() {
440
-            // Exit current and try to move to previous sibling
441
-            if let Some(idx) = self.cursor.exit() {
442
-                if idx > 0 {
443
-                    self.cursor.enter(idx - 1);
444
-                    // Move to end of new element
445
-                    self.move_to_end_of_current();
446
-                }
655
+        if let Some(current) = self.get_current() {
656
+            if matches!(current, MathBox::Number(_) | MathBox::Symbol(_)) && self.cursor.offset > 0
657
+            {
658
+                self.cursor.offset -= 1;
659
+                return;
660
+            }
661
+        }
662
+
663
+        let mut path = self.cursor.path.clone();
664
+        while let Some(idx) = path.pop() {
665
+            if idx > 0 {
666
+                path.push(idx - 1);
667
+                self.cursor.path = path;
668
+                self.cursor.offset = 0;
669
+                self.move_to_end_of_current();
670
+                return;
447671
             }
448672
         }
449673
     }
@@ -452,23 +676,28 @@ impl MathInput {
452676
     fn move_right(&mut self) {
453677
         if let Some(current) = self.get_current() {
454678
             match current {
455
-                MathBox::Number(s) | MathBox::Symbol(s) if self.cursor.offset < s.len() => {
679
+                MathBox::Number(s) | MathBox::Symbol(s)
680
+                    if self.cursor.offset < Self::char_count(s) =>
681
+                {
456682
                     self.cursor.offset += 1;
683
+                    return;
457684
                 }
458
-                _ => {
459
-                    // Try to enter first child or move to next sibling
460
-                    if current.child_count() > 0 && self.cursor.offset == 0 {
461
-                        self.cursor.enter(0);
462
-                    } else if !self.cursor.is_at_root() {
463
-                        if let Some(idx) = self.cursor.exit() {
464
-                            let parent = self.get_current();
465
-                            if let Some(p) = parent {
466
-                                if idx + 1 < p.child_count() {
467
-                                    self.cursor.enter(idx + 1);
468
-                                }
469
-                            }
470
-                        }
471
-                    }
685
+                _ if current.child_count() > 0 => {
686
+                    self.cursor.enter(0);
687
+                    return;
688
+                }
689
+                _ => {}
690
+            }
691
+        }
692
+
693
+        let mut path = self.cursor.path.clone();
694
+        while let Some(idx) = path.pop() {
695
+            if let Some(parent) = Self::get_node_at_path(&self.root, &path) {
696
+                if idx + 1 < parent.child_count() {
697
+                    path.push(idx + 1);
698
+                    self.cursor.path = path;
699
+                    self.cursor.offset = 0;
700
+                    return;
472701
                 }
473702
             }
474703
         }
@@ -524,35 +753,41 @@ impl MathInput {
524753
 
525754
     /// Move to next slot (Tab)
526755
     fn move_to_next_slot(&mut self) {
527
-        // Simple: try next sibling, or exit and try next
528
-        if let Some(current) = self.get_current() {
529
-            if current.child_count() > 0 {
530
-                self.cursor.enter(0);
531
-                return;
532
-            }
756
+        let slots = self.collect_slot_paths();
757
+        if slots.is_empty() {
758
+            return;
533759
         }
534760
 
535
-        if !self.cursor.is_at_root() {
536
-            if let Some(idx) = self.cursor.exit() {
537
-                if let Some(parent) = self.get_current() {
538
-                    if idx + 1 < parent.child_count() {
539
-                        self.cursor.enter(idx + 1);
540
-                    }
541
-                }
542
-            }
543
-        }
761
+        let current_path = self.cursor.path.clone();
762
+        let next_idx = if let Some(idx) = slots.iter().position(|p| p == &current_path) {
763
+            (idx + 1) % slots.len()
764
+        } else {
765
+            slots.iter().position(|p| p > &current_path).unwrap_or(0)
766
+        };
767
+
768
+        self.cursor.path = slots[next_idx].clone();
769
+        self.cursor.offset = 0;
544770
     }
545771
 
546772
     /// Move to previous slot (Shift+Tab)
547773
     fn move_to_prev_slot(&mut self) {
548
-        if !self.cursor.is_at_root() {
549
-            if let Some(idx) = self.cursor.exit() {
550
-                if idx > 0 {
551
-                    self.cursor.enter(idx - 1);
552
-                    self.move_to_end_of_current();
553
-                }
554
-            }
774
+        let slots = self.collect_slot_paths();
775
+        if slots.is_empty() {
776
+            return;
555777
         }
778
+
779
+        let current_path = self.cursor.path.clone();
780
+        let prev_idx = if let Some(idx) = slots.iter().position(|p| p == &current_path) {
781
+            if idx == 0 { slots.len() - 1 } else { idx - 1 }
782
+        } else {
783
+            match slots.iter().rposition(|p| p < &current_path) {
784
+                Some(idx) => idx,
785
+                None => slots.len() - 1,
786
+            }
787
+        };
788
+
789
+        self.cursor.path = slots[prev_idx].clone();
790
+        self.cursor.offset = 0;
556791
     }
557792
 
558793
     /// Move cursor to start
@@ -571,7 +806,7 @@ impl MathInput {
571806
         if let Some(current) = self.get_current() {
572807
             match current {
573808
                 MathBox::Number(s) | MathBox::Symbol(s) => {
574
-                    self.cursor.offset = s.len();
809
+                    self.cursor.offset = Self::char_count(s);
575810
                 }
576811
                 _ if current.child_count() > 0 => {
577812
                     self.cursor.enter(current.child_count() - 1);
@@ -584,20 +819,12 @@ impl MathInput {
584819
 
585820
     /// Get the current element at cursor
586821
     fn get_current(&self) -> Option<&MathBox> {
587
-        let mut current = &self.root;
588
-        for &idx in &self.cursor.path {
589
-            current = current.child(idx)?;
590
-        }
591
-        Some(current)
822
+        Self::get_node_at_path(&self.root, &self.cursor.path)
592823
     }
593824
 
594825
     /// Get mutable reference to current element
595826
     fn get_current_mut(&mut self) -> Option<&mut MathBox> {
596
-        let mut current = &mut self.root;
597
-        for &idx in &self.cursor.path {
598
-            current = current.child_mut(idx)?;
599
-        }
600
-        Some(current)
827
+        Self::get_node_mut_at_path(&mut self.root, &self.cursor.path)
601828
     }
602829
 
603830
     /// Get the root MathBox
@@ -609,6 +836,330 @@ impl MathInput {
609836
     pub fn cursor_path(&self) -> &[usize] {
610837
         &self.cursor.path
611838
     }
839
+
840
+    /// Get character offset within the currently focused token
841
+    pub fn cursor_offset(&self) -> usize {
842
+        self.cursor.offset
843
+    }
844
+
845
+    fn get_node_at_path<'a>(root: &'a MathBox, path: &[usize]) -> Option<&'a MathBox> {
846
+        let mut current = root;
847
+        for &idx in path {
848
+            current = current.child(idx)?;
849
+        }
850
+        Some(current)
851
+    }
852
+
853
+    fn get_node_mut_at_path<'a>(root: &'a mut MathBox, path: &[usize]) -> Option<&'a mut MathBox> {
854
+        let mut current = root;
855
+        for &idx in path {
856
+            current = current.child_mut(idx)?;
857
+        }
858
+        Some(current)
859
+    }
860
+
861
+    fn collect_slot_paths(&self) -> Vec<Vec<usize>> {
862
+        fn walk(node: &MathBox, path: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
863
+            if matches!(node, MathBox::Slot) {
864
+                out.push(path.clone());
865
+                return;
866
+            }
867
+            for i in MathInput::ordered_child_indices(node) {
868
+                if let Some(child) = node.child(i) {
869
+                    path.push(i);
870
+                    walk(child, path, out);
871
+                    path.pop();
872
+                }
873
+            }
874
+        }
875
+
876
+        let mut out = Vec::new();
877
+        let mut path = Vec::new();
878
+        walk(&self.root, &mut path, &mut out);
879
+        out
880
+    }
881
+
882
+    fn ordered_child_indices(node: &MathBox) -> Vec<usize> {
883
+        match node {
884
+            MathBox::Power { .. } => vec![0, 1],
885
+            MathBox::Derivative { .. } => vec![0, 1],
886
+            MathBox::Sum { .. } | MathBox::Product { .. } => vec![1, 2, 3, 0],
887
+            MathBox::Integral { lower, upper, .. } => {
888
+                let has_lower = lower.is_some();
889
+                let has_upper = upper.is_some();
890
+                let mut indices = Vec::new();
891
+                if has_upper {
892
+                    indices.push(if has_lower { 1 } else { 0 });
893
+                }
894
+                if has_lower {
895
+                    indices.push(0);
896
+                }
897
+                let body_idx = usize::from(has_lower) + usize::from(has_upper);
898
+                indices.push(body_idx);
899
+                indices.push(body_idx + 1); // variable slot/symbol
900
+                indices
901
+            }
902
+            _ => (0..node.child_count()).collect(),
903
+        }
904
+    }
905
+
906
+    fn focus_first_slot_in_current_subtree(&mut self) {
907
+        let base = self.cursor.path.clone();
908
+        let slots = self.collect_slot_paths();
909
+        if let Some(path) = slots
910
+            .into_iter()
911
+            .find(|p| p.len() > base.len() && p.starts_with(&base))
912
+        {
913
+            self.cursor.path = path;
914
+            self.cursor.offset = 0;
915
+        }
916
+    }
917
+
918
+    fn previous_sibling_path(&self, path: &[usize]) -> Option<Vec<usize>> {
919
+        let mut cursor = path.to_vec();
920
+        while let Some(idx) = cursor.pop() {
921
+            if idx > 0 {
922
+                cursor.push(idx - 1);
923
+                return Some(cursor);
924
+            }
925
+        }
926
+        None
927
+    }
928
+
929
+    fn next_sibling_path(&self, path: &[usize]) -> Option<Vec<usize>> {
930
+        let mut cursor = path.to_vec();
931
+        while let Some(idx) = cursor.pop() {
932
+            if let Some(parent) = Self::get_node_at_path(&self.root, &cursor) {
933
+                if idx + 1 < parent.child_count() {
934
+                    cursor.push(idx + 1);
935
+                    return Some(cursor);
936
+                }
937
+            }
938
+        }
939
+        None
940
+    }
941
+
942
+    fn replace_current_with_slot(&mut self, path: &[usize]) {
943
+        if let Some(current) = Self::get_node_mut_at_path(&mut self.root, path) {
944
+            *current = MathBox::Slot;
945
+            self.cursor.path = path.to_vec();
946
+            self.cursor.offset = 0;
947
+        }
948
+    }
949
+
950
+    fn is_effectively_empty(node: &MathBox) -> bool {
951
+        match node {
952
+            MathBox::Slot => true,
953
+            MathBox::Number(s) | MathBox::Symbol(s) => s.is_empty(),
954
+            MathBox::Operator(_) => false,
955
+            MathBox::Fraction { num, den } => {
956
+                Self::is_effectively_empty(num) && Self::is_effectively_empty(den)
957
+            }
958
+            MathBox::Power { base, exp } => {
959
+                Self::is_effectively_empty(base) && Self::is_effectively_empty(exp)
960
+            }
961
+            MathBox::Subscript { base, sub } => {
962
+                Self::is_effectively_empty(base) && Self::is_effectively_empty(sub)
963
+            }
964
+            MathBox::Root { index, radicand } => {
965
+                let index_empty = index
966
+                    .as_deref()
967
+                    .map(Self::is_effectively_empty)
968
+                    .unwrap_or(true);
969
+                index_empty && Self::is_effectively_empty(radicand)
970
+            }
971
+            MathBox::Func { args, .. } => args.iter().all(Self::is_effectively_empty),
972
+            MathBox::Abs(inner) | MathBox::Parens(inner) => Self::is_effectively_empty(inner),
973
+            MathBox::Integral {
974
+                lower,
975
+                upper,
976
+                body,
977
+                var,
978
+            } => {
979
+                let lower_empty = lower
980
+                    .as_deref()
981
+                    .map(Self::is_effectively_empty)
982
+                    .unwrap_or(true);
983
+                let upper_empty = upper
984
+                    .as_deref()
985
+                    .map(Self::is_effectively_empty)
986
+                    .unwrap_or(true);
987
+                lower_empty
988
+                    && upper_empty
989
+                    && Self::is_effectively_empty(body)
990
+                    && Self::is_effectively_empty(var)
991
+            }
992
+            MathBox::Derivative { var, body, .. } => {
993
+                Self::is_effectively_empty(var) && Self::is_effectively_empty(body)
994
+            }
995
+            MathBox::Limit { var, to, body, .. } => {
996
+                Self::is_effectively_empty(var)
997
+                    && Self::is_effectively_empty(to)
998
+                    && Self::is_effectively_empty(body)
999
+            }
1000
+            MathBox::Sum {
1001
+                var,
1002
+                lower,
1003
+                upper,
1004
+                body,
1005
+            }
1006
+            | MathBox::Product {
1007
+                var,
1008
+                lower,
1009
+                upper,
1010
+                body,
1011
+            } => {
1012
+                Self::is_effectively_empty(var)
1013
+                    && Self::is_effectively_empty(lower)
1014
+                    && Self::is_effectively_empty(upper)
1015
+                    && Self::is_effectively_empty(body)
1016
+            }
1017
+            MathBox::Matrix { rows } => rows.iter().flatten().all(Self::is_effectively_empty),
1018
+            MathBox::Row(items) => items.iter().all(Self::is_effectively_empty),
1019
+        }
1020
+    }
1021
+
1022
+    fn collapse_parent_if_empty(&mut self, current_path: &[usize]) -> bool {
1023
+        let Some((_, parent_path)) = current_path.split_last() else {
1024
+            return false;
1025
+        };
1026
+        if parent_path.is_empty() {
1027
+            return false;
1028
+        }
1029
+
1030
+        let should_collapse = Self::get_node_at_path(&self.root, parent_path)
1031
+            .map(Self::is_effectively_empty)
1032
+            .unwrap_or(false);
1033
+        if should_collapse {
1034
+            self.replace_current_with_slot(parent_path);
1035
+            return true;
1036
+        }
1037
+        false
1038
+    }
1039
+
1040
+    fn remove_sibling_from_parent_row(
1041
+        &mut self,
1042
+        parent_path: &[usize],
1043
+        remove_idx: usize,
1044
+        cursor_idx: usize,
1045
+    ) -> bool {
1046
+        if let Some(MathBox::Row(items)) = Self::get_node_mut_at_path(&mut self.root, parent_path) {
1047
+            if remove_idx >= items.len() {
1048
+                return false;
1049
+            }
1050
+
1051
+            items.remove(remove_idx);
1052
+            if items.is_empty() {
1053
+                items.push(MathBox::Slot);
1054
+            }
1055
+
1056
+            let new_idx = cursor_idx.min(items.len().saturating_sub(1));
1057
+            self.cursor.path = parent_path.to_vec();
1058
+            self.cursor.enter(new_idx);
1059
+            self.cursor.offset = 0;
1060
+            return true;
1061
+        }
1062
+        false
1063
+    }
1064
+
1065
+    fn delete_current_slot_in_row(&mut self, path: &[usize]) -> bool {
1066
+        let Some((&idx, parent_path)) = path.split_last() else {
1067
+            return false;
1068
+        };
1069
+        self.remove_sibling_from_parent_row(parent_path, idx, idx.saturating_sub(1))
1070
+    }
1071
+
1072
+    fn delete_previous_from_path(&mut self, current_path: &[usize]) {
1073
+        if let Some(prev_path) = self.previous_sibling_path(current_path) {
1074
+            if let (Some((&cur_idx, cur_parent)), Some((&prev_idx, prev_parent))) =
1075
+                (current_path.split_last(), prev_path.split_last())
1076
+            {
1077
+                if cur_parent == prev_parent
1078
+                    && prev_idx + 1 == cur_idx
1079
+                    && self.remove_sibling_from_parent_row(
1080
+                        cur_parent,
1081
+                        prev_idx,
1082
+                        cur_idx.saturating_sub(1),
1083
+                    )
1084
+                {
1085
+                    return;
1086
+                }
1087
+            }
1088
+
1089
+            if let Some(node) = Self::get_node_mut_at_path(&mut self.root, &prev_path) {
1090
+                match node {
1091
+                    MathBox::Number(s) if !s.is_empty() => {
1092
+                        let last = Self::char_count(s).saturating_sub(1);
1093
+                        Self::remove_char_at(s, last);
1094
+                        if s.is_empty() {
1095
+                            *node = MathBox::Slot;
1096
+                            self.cursor.offset = 0;
1097
+                        } else {
1098
+                            self.cursor.offset = Self::char_count(s);
1099
+                        }
1100
+                        self.cursor.path = prev_path;
1101
+                    }
1102
+                    MathBox::Symbol(s) if !s.is_empty() => {
1103
+                        let last = Self::char_count(s).saturating_sub(1);
1104
+                        Self::remove_char_at(s, last);
1105
+                        if s.is_empty() {
1106
+                            *node = MathBox::Slot;
1107
+                            self.cursor.offset = 0;
1108
+                        } else {
1109
+                            self.cursor.offset = Self::char_count(s);
1110
+                        }
1111
+                        self.cursor.path = prev_path;
1112
+                    }
1113
+                    _ => {
1114
+                        *node = MathBox::Slot;
1115
+                        self.cursor.path = prev_path;
1116
+                        self.cursor.offset = 0;
1117
+                    }
1118
+                }
1119
+            }
1120
+            return;
1121
+        }
1122
+
1123
+        let _ = self.collapse_parent_if_empty(current_path);
1124
+    }
1125
+
1126
+    fn delete_next_from_path(&mut self, current_path: &[usize]) {
1127
+        if let Some(next_path) = self.next_sibling_path(current_path) {
1128
+            if let (Some((&cur_idx, cur_parent)), Some((&next_idx, next_parent))) =
1129
+                (current_path.split_last(), next_path.split_last())
1130
+            {
1131
+                if cur_parent == next_parent
1132
+                    && cur_idx + 1 == next_idx
1133
+                    && self.remove_sibling_from_parent_row(cur_parent, next_idx, cur_idx)
1134
+                {
1135
+                    return;
1136
+                }
1137
+            }
1138
+
1139
+            if let Some(node) = Self::get_node_mut_at_path(&mut self.root, &next_path) {
1140
+                match node {
1141
+                    MathBox::Number(s) if !s.is_empty() => {
1142
+                        Self::remove_char_at(s, 0);
1143
+                        if s.is_empty() {
1144
+                            *node = MathBox::Slot;
1145
+                        }
1146
+                    }
1147
+                    MathBox::Symbol(s) if !s.is_empty() => {
1148
+                        Self::remove_char_at(s, 0);
1149
+                        if s.is_empty() {
1150
+                            *node = MathBox::Slot;
1151
+                        }
1152
+                    }
1153
+                    _ => {
1154
+                        *node = MathBox::Slot;
1155
+                    }
1156
+                }
1157
+            }
1158
+            return;
1159
+        }
1160
+
1161
+        let _ = self.collapse_parent_if_empty(current_path);
1162
+    }
6121163
 }
6131164
 
6141165
 /// Special keys that can be handled
@@ -632,6 +1183,14 @@ pub enum SpecialKey {
6321183
 mod tests {
6331184
     use super::*;
6341185
 
1186
+    fn run_command(input: &mut MathInput, cmd: &str) {
1187
+        input.handle_char('\\');
1188
+        for ch in cmd.chars() {
1189
+            input.handle_char(ch);
1190
+        }
1191
+        input.handle_char(' ');
1192
+    }
1193
+
6351194
     #[test]
6361195
     fn test_new_input() {
6371196
         let input = MathInput::new();
@@ -666,6 +1225,129 @@ mod tests {
6661225
         }
6671226
     }
6681227
 
1228
+    #[test]
1229
+    fn test_power_input() {
1230
+        let mut input = MathInput::new();
1231
+        input.handle_char('2');
1232
+        input.handle_char('^');
1233
+        input.handle_char('2');
1234
+
1235
+        if let MathBox::Row(items) = &input.root {
1236
+            if let MathBox::Power { base, exp } = &items[0] {
1237
+                assert!(matches!(base.as_ref(), MathBox::Number(s) if s == "2"));
1238
+                assert!(matches!(exp.as_ref(), MathBox::Number(s) if s == "2"));
1239
+            } else {
1240
+                panic!("Expected Power");
1241
+            }
1242
+        }
1243
+    }
1244
+
1245
+    #[test]
1246
+    fn test_factorial_input_wraps_current() {
1247
+        let mut input = MathInput::new();
1248
+        input.handle_char('5');
1249
+        input.handle_char('!');
1250
+
1251
+        if let MathBox::Row(items) = &input.root {
1252
+            if let MathBox::Func { name, args } = &items[0] {
1253
+                assert_eq!(name, "factorial");
1254
+                assert_eq!(args.len(), 1);
1255
+                assert!(matches!(&args[0], MathBox::Number(s) if s == "5"));
1256
+            } else {
1257
+                panic!("Expected factorial function");
1258
+            }
1259
+        } else {
1260
+            panic!("Expected Row");
1261
+        }
1262
+    }
1263
+
1264
+    #[test]
1265
+    fn test_factorial_input_on_empty_slot_inserts_template() {
1266
+        let mut input = MathInput::new();
1267
+        input.handle_char('!');
1268
+
1269
+        if let MathBox::Row(items) = &input.root {
1270
+            if let MathBox::Func { name, args } = &items[0] {
1271
+                assert_eq!(name, "factorial");
1272
+                assert_eq!(args.len(), 1);
1273
+                assert!(matches!(&args[0], MathBox::Slot));
1274
+            } else {
1275
+                panic!("Expected factorial function");
1276
+            }
1277
+        } else {
1278
+            panic!("Expected Row");
1279
+        }
1280
+        assert_eq!(input.cursor_path(), &[0, 0]);
1281
+    }
1282
+
1283
+    #[test]
1284
+    fn test_insert_in_middle_of_number() {
1285
+        let mut input = MathInput::new();
1286
+        input.handle_char('1');
1287
+        input.handle_char('2');
1288
+        input.handle_char('3');
1289
+
1290
+        input.handle_key(SpecialKey::Left);
1291
+        input.handle_key(SpecialKey::Left);
1292
+        input.handle_char('4');
1293
+
1294
+        if let MathBox::Row(items) = &input.root {
1295
+            assert!(matches!(&items[0], MathBox::Number(s) if s == "1423"));
1296
+        }
1297
+        assert_eq!(input.cursor_offset(), 2);
1298
+    }
1299
+
1300
+    #[test]
1301
+    fn test_backspace_uses_cursor_offset() {
1302
+        let mut input = MathInput::new();
1303
+        input.handle_char('1');
1304
+        input.handle_char('2');
1305
+        input.handle_char('3');
1306
+
1307
+        input.handle_key(SpecialKey::Left);
1308
+        input.handle_key(SpecialKey::Left);
1309
+        input.handle_key(SpecialKey::Backspace);
1310
+
1311
+        if let MathBox::Row(items) = &input.root {
1312
+            assert!(matches!(&items[0], MathBox::Number(s) if s == "23"));
1313
+        }
1314
+        assert_eq!(input.cursor_offset(), 0);
1315
+    }
1316
+
1317
+    #[test]
1318
+    fn test_delete_uses_cursor_offset() {
1319
+        let mut input = MathInput::new();
1320
+        input.handle_char('1');
1321
+        input.handle_char('2');
1322
+        input.handle_char('3');
1323
+
1324
+        input.handle_key(SpecialKey::Left);
1325
+        input.handle_key(SpecialKey::Left);
1326
+        input.handle_key(SpecialKey::Delete);
1327
+
1328
+        if let MathBox::Row(items) = &input.root {
1329
+            assert!(matches!(&items[0], MathBox::Number(s) if s == "13"));
1330
+        }
1331
+        assert_eq!(input.cursor_offset(), 1);
1332
+    }
1333
+
1334
+    #[test]
1335
+    fn test_row_insertion_after_number() {
1336
+        let mut input = MathInput::new();
1337
+        input.handle_char('2');
1338
+        input.handle_char('+');
1339
+        input.handle_char('3');
1340
+
1341
+        if let MathBox::Row(items) = &input.root {
1342
+            assert_eq!(items.len(), 3);
1343
+            assert!(matches!(&items[0], MathBox::Number(s) if s == "2"));
1344
+            assert!(matches!(&items[1], MathBox::Operator(Operator::Add)));
1345
+            assert!(matches!(&items[2], MathBox::Number(s) if s == "3"));
1346
+        } else {
1347
+            panic!("Expected Row");
1348
+        }
1349
+    }
1350
+
6691351
     #[test]
6701352
     fn test_command_mode() {
6711353
         let mut input = MathInput::new();
@@ -683,4 +1365,396 @@ mod tests {
6831365
             assert!(matches!(items[0], MathBox::Root { index: None, .. }));
6841366
         }
6851367
     }
1368
+
1369
+    #[test]
1370
+    fn test_command_mode_colon_alias() {
1371
+        let mut input = MathInput::new();
1372
+        input.handle_char(':');
1373
+        assert!(input.command_buffer.is_some());
1374
+
1375
+        input.handle_char('s');
1376
+        input.handle_char('u');
1377
+        input.handle_char('m');
1378
+        input.handle_char(' ');
1379
+
1380
+        if let MathBox::Row(items) = &input.root {
1381
+            assert!(matches!(items[0], MathBox::Sum { .. }));
1382
+        }
1383
+    }
1384
+
1385
+    #[test]
1386
+    fn test_command_mode_executes_on_enter() {
1387
+        let mut input = MathInput::new();
1388
+        input.handle_char(':');
1389
+        input.handle_char('f');
1390
+        input.handle_char('r');
1391
+        input.handle_char('a');
1392
+        input.handle_char('c');
1393
+
1394
+        let result = input.handle_key(SpecialKey::Enter);
1395
+        assert!(matches!(result, InputResult::Consumed));
1396
+        assert!(input.command_buffer.is_none());
1397
+
1398
+        if let MathBox::Row(items) = &input.root {
1399
+            assert!(matches!(items[0], MathBox::Fraction { .. }));
1400
+        } else {
1401
+            panic!("Expected Row");
1402
+        }
1403
+    }
1404
+
1405
+    #[test]
1406
+    fn test_sum_command_template() {
1407
+        let mut input = MathInput::new();
1408
+        run_command(&mut input, "sum");
1409
+
1410
+        if let MathBox::Row(items) = &input.root {
1411
+            if let MathBox::Sum {
1412
+                var,
1413
+                lower,
1414
+                upper,
1415
+                body,
1416
+            } = &items[0]
1417
+            {
1418
+                assert!(matches!(var.as_ref(), MathBox::Symbol(s) if s == "i"));
1419
+                assert!(matches!(lower.as_ref(), MathBox::Slot));
1420
+                assert!(matches!(upper.as_ref(), MathBox::Slot));
1421
+                assert!(matches!(body.as_ref(), MathBox::Slot));
1422
+            } else {
1423
+                panic!("Expected Sum template");
1424
+            }
1425
+        }
1426
+    }
1427
+
1428
+    #[test]
1429
+    fn test_diff_command_template() {
1430
+        let mut input = MathInput::new();
1431
+        run_command(&mut input, "diff");
1432
+
1433
+        if let MathBox::Row(items) = &input.root {
1434
+            if let MathBox::Derivative { order, var, body } = &items[0] {
1435
+                assert_eq!(*order, 1);
1436
+                assert!(matches!(var.as_ref(), MathBox::Slot));
1437
+                assert!(matches!(body.as_ref(), MathBox::Slot));
1438
+            } else {
1439
+                panic!("Expected Derivative template");
1440
+            }
1441
+        }
1442
+    }
1443
+
1444
+    #[test]
1445
+    fn test_lim_command_template() {
1446
+        let mut input = MathInput::new();
1447
+        run_command(&mut input, "lim");
1448
+
1449
+        if let MathBox::Row(items) = &input.root {
1450
+            if let MathBox::Limit {
1451
+                var,
1452
+                to,
1453
+                direction,
1454
+                body,
1455
+            } = &items[0]
1456
+            {
1457
+                assert!(matches!(var.as_ref(), MathBox::Symbol(s) if s == "x"));
1458
+                assert!(matches!(to.as_ref(), MathBox::Slot));
1459
+                assert!(direction.is_none());
1460
+                assert!(matches!(body.as_ref(), MathBox::Slot));
1461
+            } else {
1462
+                panic!("Expected Limit template");
1463
+            }
1464
+        }
1465
+    }
1466
+
1467
+    #[test]
1468
+    fn test_dint_command_template() {
1469
+        let mut input = MathInput::new();
1470
+        run_command(&mut input, "dint");
1471
+
1472
+        if let MathBox::Row(items) = &input.root {
1473
+            if let MathBox::Integral {
1474
+                lower,
1475
+                upper,
1476
+                body,
1477
+                var,
1478
+            } = &items[0]
1479
+            {
1480
+                assert!(matches!(lower.as_deref(), Some(MathBox::Slot)));
1481
+                assert!(matches!(upper.as_deref(), Some(MathBox::Slot)));
1482
+                assert!(matches!(body.as_ref(), MathBox::Slot));
1483
+                assert!(matches!(var.as_ref(), MathBox::Symbol(s) if s == "x"));
1484
+            } else {
1485
+                panic!("Expected definite Integral template");
1486
+            }
1487
+        }
1488
+    }
1489
+
1490
+    #[test]
1491
+    fn test_solve_command_template() {
1492
+        let mut input = MathInput::new();
1493
+        run_command(&mut input, "solve");
1494
+
1495
+        assert_eq!(input.cursor_path(), &[0, 0]);
1496
+        if let MathBox::Row(items) = &input.root {
1497
+            if let MathBox::Func { name, args } = &items[0] {
1498
+                assert_eq!(name, "solve");
1499
+                assert_eq!(args.len(), 2);
1500
+                assert!(matches!(&args[0], MathBox::Slot));
1501
+                assert!(matches!(&args[1], MathBox::Symbol(s) if s == "x"));
1502
+            } else {
1503
+                panic!("Expected solve function template");
1504
+            }
1505
+        } else {
1506
+            panic!("Expected Row");
1507
+        }
1508
+    }
1509
+
1510
+    #[test]
1511
+    fn test_tab_cycles_fraction_slots() {
1512
+        let mut input = MathInput::new();
1513
+        run_command(&mut input, "frac");
1514
+
1515
+        assert_eq!(input.cursor_path(), &[0, 0]);
1516
+
1517
+        input.handle_key(SpecialKey::Tab);
1518
+        assert_eq!(input.cursor_path(), &[0, 1]);
1519
+
1520
+        input.handle_key(SpecialKey::Tab);
1521
+        assert_eq!(input.cursor_path(), &[0, 0]);
1522
+
1523
+        input.handle_key(SpecialKey::ShiftTab);
1524
+        assert_eq!(input.cursor_path(), &[0, 1]);
1525
+    }
1526
+
1527
+    #[test]
1528
+    fn test_tab_skips_non_slot_children_in_sum() {
1529
+        let mut input = MathInput::new();
1530
+        run_command(&mut input, "sum");
1531
+
1532
+        // Starts at lower bound slot by policy (non-slot var is skipped).
1533
+        assert_eq!(input.cursor_path(), &[0, 1]);
1534
+
1535
+        input.handle_key(SpecialKey::Tab);
1536
+        assert_eq!(input.cursor_path(), &[0, 2]);
1537
+
1538
+        input.handle_key(SpecialKey::Tab);
1539
+        assert_eq!(input.cursor_path(), &[0, 3]);
1540
+
1541
+        input.handle_key(SpecialKey::Tab);
1542
+        assert_eq!(input.cursor_path(), &[0, 1]);
1543
+    }
1544
+
1545
+    #[test]
1546
+    fn test_nested_tab_order_with_power_in_sum_body() {
1547
+        let mut input = MathInput::new();
1548
+        run_command(&mut input, "sum");
1549
+
1550
+        input.handle_key(SpecialKey::Tab);
1551
+        input.handle_key(SpecialKey::Tab);
1552
+        assert_eq!(input.cursor_path(), &[0, 3]);
1553
+
1554
+        input.handle_char('x');
1555
+        input.handle_char('^');
1556
+        assert_eq!(input.cursor_path(), &[0, 3, 1]);
1557
+
1558
+        // From nested exponent slot, Tab should cycle to first slot in the expression.
1559
+        input.handle_key(SpecialKey::Tab);
1560
+        assert_eq!(input.cursor_path(), &[0, 1]);
1561
+
1562
+        // Shift+Tab should return to the nested exponent slot.
1563
+        input.handle_key(SpecialKey::ShiftTab);
1564
+        assert_eq!(input.cursor_path(), &[0, 3, 1]);
1565
+    }
1566
+
1567
+    #[test]
1568
+    fn test_dint_focus_and_tab_order_upper_lower_body() {
1569
+        let mut input = MathInput::new();
1570
+        run_command(&mut input, "dint");
1571
+
1572
+        // Policy: upper -> lower -> body -> var (var may be non-slot default).
1573
+        assert_eq!(input.cursor_path(), &[0, 1]);
1574
+
1575
+        input.handle_key(SpecialKey::Tab);
1576
+        assert_eq!(input.cursor_path(), &[0, 0]);
1577
+
1578
+        input.handle_key(SpecialKey::Tab);
1579
+        assert_eq!(input.cursor_path(), &[0, 2]);
1580
+
1581
+        input.handle_key(SpecialKey::Tab);
1582
+        assert_eq!(input.cursor_path(), &[0, 1]);
1583
+    }
1584
+
1585
+    #[test]
1586
+    fn test_diff_focus_and_tab_order_var_body() {
1587
+        let mut input = MathInput::new();
1588
+        run_command(&mut input, "diff");
1589
+
1590
+        assert_eq!(input.cursor_path(), &[0, 0]);
1591
+        input.handle_key(SpecialKey::Tab);
1592
+        assert_eq!(input.cursor_path(), &[0, 1]);
1593
+        input.handle_key(SpecialKey::Tab);
1594
+        assert_eq!(input.cursor_path(), &[0, 0]);
1595
+    }
1596
+
1597
+    #[test]
1598
+    fn test_close_paren_exits_container() {
1599
+        let mut input = MathInput::new();
1600
+        input.handle_char('(');
1601
+        assert_eq!(input.cursor_path(), &[0, 0]);
1602
+
1603
+        input.handle_char(')');
1604
+        assert_eq!(input.cursor_path(), &[0]);
1605
+    }
1606
+
1607
+    #[test]
1608
+    fn test_backspace_at_exponent_start_deletes_base() {
1609
+        let mut input = MathInput::new();
1610
+        input.handle_char('2');
1611
+        input.handle_char('^');
1612
+        input.handle_char('3');
1613
+
1614
+        input.handle_key(SpecialKey::Left);
1615
+        assert_eq!(input.cursor_path(), &[0, 1]);
1616
+        assert_eq!(input.cursor_offset(), 0);
1617
+
1618
+        input.handle_key(SpecialKey::Backspace);
1619
+        assert_eq!(input.cursor_path(), &[0, 0]);
1620
+        assert_eq!(input.cursor_offset(), 0);
1621
+
1622
+        if let MathBox::Row(items) = &input.root {
1623
+            if let MathBox::Power { base, exp } = &items[0] {
1624
+                assert!(matches!(base.as_ref(), MathBox::Slot));
1625
+                assert!(matches!(exp.as_ref(), MathBox::Number(s) if s == "3"));
1626
+            } else {
1627
+                panic!("Expected Power");
1628
+            }
1629
+        }
1630
+    }
1631
+
1632
+    #[test]
1633
+    fn test_delete_at_base_end_deletes_exponent_prefix() {
1634
+        let mut input = MathInput::new();
1635
+        input.handle_char('2');
1636
+        input.handle_char('^');
1637
+        input.handle_char('3');
1638
+        input.handle_char('4');
1639
+
1640
+        input.handle_key(SpecialKey::Down);
1641
+        assert_eq!(input.cursor_path(), &[0, 0]);
1642
+        input.handle_key(SpecialKey::Right);
1643
+        assert_eq!(input.cursor_offset(), 1);
1644
+
1645
+        input.handle_key(SpecialKey::Delete);
1646
+        assert_eq!(input.cursor_path(), &[0, 0]);
1647
+        assert_eq!(input.cursor_offset(), 1);
1648
+
1649
+        if let MathBox::Row(items) = &input.root {
1650
+            if let MathBox::Power { exp, .. } = &items[0] {
1651
+                assert!(matches!(exp.as_ref(), MathBox::Number(s) if s == "4"));
1652
+            } else {
1653
+                panic!("Expected Power");
1654
+            }
1655
+        }
1656
+    }
1657
+
1658
+    #[test]
1659
+    fn test_backspace_from_denominator_slot_targets_numerator() {
1660
+        let mut input = MathInput::new();
1661
+        run_command(&mut input, "frac");
1662
+        input.handle_char('1');
1663
+
1664
+        input.handle_key(SpecialKey::Tab);
1665
+        assert_eq!(input.cursor_path(), &[0, 1]);
1666
+        assert!(matches!(input.get_current(), Some(MathBox::Slot)));
1667
+
1668
+        input.handle_key(SpecialKey::Backspace);
1669
+        assert_eq!(input.cursor_path(), &[0, 0]);
1670
+
1671
+        if let MathBox::Row(items) = &input.root {
1672
+            if let MathBox::Fraction { num, den } = &items[0] {
1673
+                assert!(matches!(num.as_ref(), MathBox::Slot));
1674
+                assert!(matches!(den.as_ref(), MathBox::Slot));
1675
+            } else {
1676
+                panic!("Expected Fraction");
1677
+            }
1678
+        }
1679
+    }
1680
+
1681
+    #[test]
1682
+    fn test_delete_from_numerator_slot_targets_denominator() {
1683
+        let mut input = MathInput::new();
1684
+        run_command(&mut input, "frac");
1685
+
1686
+        input.handle_key(SpecialKey::Tab);
1687
+        input.handle_char('5');
1688
+        input.handle_key(SpecialKey::ShiftTab);
1689
+        assert_eq!(input.cursor_path(), &[0, 0]);
1690
+
1691
+        input.handle_key(SpecialKey::Delete);
1692
+        assert_eq!(input.cursor_path(), &[0, 0]);
1693
+
1694
+        if let MathBox::Row(items) = &input.root {
1695
+            if let MathBox::Fraction { num, den } = &items[0] {
1696
+                assert!(matches!(num.as_ref(), MathBox::Slot));
1697
+                assert!(matches!(den.as_ref(), MathBox::Slot));
1698
+            } else {
1699
+                panic!("Expected Fraction");
1700
+            }
1701
+        }
1702
+    }
1703
+
1704
+    #[test]
1705
+    fn test_backspace_on_container_replaces_with_slot() {
1706
+        let mut input = MathInput::new();
1707
+        input.handle_char('(');
1708
+        input.handle_char('x');
1709
+        input.handle_char(')');
1710
+        assert_eq!(input.cursor_path(), &[0]);
1711
+
1712
+        input.handle_key(SpecialKey::Backspace);
1713
+        assert_eq!(input.cursor_path(), &[0]);
1714
+
1715
+        if let MathBox::Row(items) = &input.root {
1716
+            assert!(matches!(&items[0], MathBox::Slot));
1717
+        } else {
1718
+            panic!("Expected Row");
1719
+        }
1720
+    }
1721
+
1722
+    #[test]
1723
+    fn test_backspace_clears_empty_sum_template() {
1724
+        let mut input = MathInput::new();
1725
+        run_command(&mut input, "sum");
1726
+
1727
+        assert_eq!(input.cursor_path(), &[0, 1]);
1728
+        input.handle_key(SpecialKey::Backspace);
1729
+        assert_eq!(input.cursor_path(), &[0, 0]);
1730
+
1731
+        input.handle_key(SpecialKey::Backspace);
1732
+        assert_eq!(input.cursor_path(), &[0]);
1733
+
1734
+        if let MathBox::Row(items) = &input.root {
1735
+            assert!(matches!(&items[0], MathBox::Slot));
1736
+        } else {
1737
+            panic!("Expected Row");
1738
+        }
1739
+    }
1740
+
1741
+    #[test]
1742
+    fn test_backspace_on_row_slot_removes_current_slot() {
1743
+        let mut input = MathInput::new();
1744
+        input.handle_char('2');
1745
+        input.handle_char('+');
1746
+        input.handle_char('3');
1747
+
1748
+        input.handle_key(SpecialKey::Backspace);
1749
+        input.handle_key(SpecialKey::Backspace);
1750
+
1751
+        assert_eq!(input.cursor_path(), &[1]);
1752
+        if let MathBox::Row(items) = &input.root {
1753
+            assert_eq!(items.len(), 2);
1754
+            assert!(matches!(&items[0], MathBox::Number(s) if s == "2"));
1755
+            assert!(matches!(&items[1], MathBox::Operator(Operator::Add)));
1756
+        } else {
1757
+            panic!("Expected Row");
1758
+        }
1759
+    }
6861760
 }
garcalc-math/src/layout.rsmodified
@@ -3,7 +3,7 @@
33
 //! Computes bounding boxes and positions for mathematical typesetting.
44
 //! Uses baseline-aligned layout with proper ascent/descent metrics.
55
 
6
-use crate::mathbox::{MathBox, Operator, LimitDirection};
6
+use crate::mathbox::{LimitDirection, MathBox, Operator};
77
 use cairo::Context;
88
 
99
 /// Layout metrics for a rendered element
@@ -62,6 +62,11 @@ impl MathLayoutEngine {
6262
         self.layout_at_depth(mathbox, ctx, 0)
6363
     }
6464
 
65
+    /// Compute layout from an explicit starting depth
66
+    pub fn layout_with_depth(&self, mathbox: &MathBox, ctx: &Context, depth: u32) -> LayoutBox {
67
+        self.layout_at_depth(mathbox, ctx, depth)
68
+    }
69
+
6570
     /// Compute layout at a specific nesting depth
6671
     fn layout_at_depth(&self, mathbox: &MathBox, ctx: &Context, depth: u32) -> LayoutBox {
6772
         let scale = self.scale_for_depth(depth);
@@ -72,45 +77,42 @@ impl MathLayoutEngine {
7277
             MathBox::Symbol(s) => self.layout_symbol(s, ctx, font_size),
7378
             MathBox::Operator(op) => self.layout_operator(*op, ctx, font_size),
7479
             MathBox::Slot => self.layout_slot(ctx, font_size),
75
-            MathBox::Fraction { num, den } => {
76
-                self.layout_fraction(num, den, ctx, depth)
77
-            }
78
-            MathBox::Power { base, exp } => {
79
-                self.layout_power(base, exp, ctx, depth)
80
-            }
81
-            MathBox::Subscript { base, sub } => {
82
-                self.layout_subscript(base, sub, ctx, depth)
83
-            }
80
+            MathBox::Fraction { num, den } => self.layout_fraction(num, den, ctx, depth),
81
+            MathBox::Power { base, exp } => self.layout_power(base, exp, ctx, depth),
82
+            MathBox::Subscript { base, sub } => self.layout_subscript(base, sub, ctx, depth),
8483
             MathBox::Root { index, radicand } => {
8584
                 self.layout_root(index.as_deref(), radicand, ctx, depth)
8685
             }
87
-            MathBox::Func { name, args } => {
88
-                self.layout_func(name, args, ctx, depth)
89
-            }
86
+            MathBox::Func { name, args } => self.layout_func(name, args, ctx, depth),
9087
             MathBox::Abs(inner) => self.layout_abs(inner, ctx, depth),
9188
             MathBox::Parens(inner) => self.layout_parens(inner, ctx, depth),
92
-            MathBox::Integral { lower, upper, body, var } => {
93
-                self.layout_integral(
94
-                    lower.as_deref(),
95
-                    upper.as_deref(),
96
-                    body,
97
-                    var,
98
-                    ctx,
99
-                    depth,
100
-                )
101
-            }
89
+            MathBox::Integral {
90
+                lower,
91
+                upper,
92
+                body,
93
+                var,
94
+            } => self.layout_integral(lower.as_deref(), upper.as_deref(), body, var, ctx, depth),
10295
             MathBox::Derivative { order, var, body } => {
10396
                 self.layout_derivative(*order, var, body, ctx, depth)
10497
             }
105
-            MathBox::Limit { var, to, direction, body } => {
106
-                self.layout_limit(var, to, *direction, body, ctx, depth)
107
-            }
108
-            MathBox::Sum { var, lower, upper, body } => {
109
-                self.layout_bigop("∑", var, lower, upper, body, ctx, depth)
110
-            }
111
-            MathBox::Product { var, lower, upper, body } => {
112
-                self.layout_bigop("∏", var, lower, upper, body, ctx, depth)
113
-            }
98
+            MathBox::Limit {
99
+                var,
100
+                to,
101
+                direction,
102
+                body,
103
+            } => self.layout_limit(var, to, *direction, body, ctx, depth),
104
+            MathBox::Sum {
105
+                var,
106
+                lower,
107
+                upper,
108
+                body,
109
+            } => self.layout_bigop("∑", var, lower, upper, body, ctx, depth),
110
+            MathBox::Product {
111
+                var,
112
+                lower,
113
+                upper,
114
+                body,
115
+            } => self.layout_bigop("∏", var, lower, upper, body, ctx, depth),
114116
             MathBox::Matrix { rows } => self.layout_matrix(rows, ctx, depth),
115117
             MathBox::Row(items) => self.layout_row(items, ctx, depth),
116118
         }
@@ -141,9 +143,17 @@ impl MathLayoutEngine {
141143
 
142144
     /// Layout a symbol (may use italic)
143145
     fn layout_symbol(&self, symbol: &str, ctx: &Context, font_size: f64) -> LayoutBox {
144
-        ctx.select_font_face(&self.font_family, cairo::FontSlant::Italic, cairo::FontWeight::Normal);
146
+        ctx.select_font_face(
147
+            &self.font_family,
148
+            cairo::FontSlant::Italic,
149
+            cairo::FontWeight::Normal,
150
+        );
145151
         let layout = self.layout_text(symbol, ctx, font_size);
146
-        ctx.select_font_face(&self.font_family, cairo::FontSlant::Normal, cairo::FontWeight::Normal);
152
+        ctx.select_font_face(
153
+            &self.font_family,
154
+            cairo::FontSlant::Normal,
155
+            cairo::FontWeight::Normal,
156
+        );
147157
         layout
148158
     }
149159
 
@@ -158,13 +168,14 @@ impl MathLayoutEngine {
158168
 
159169
     /// Layout an empty slot (placeholder box)
160170
     fn layout_slot(&self, _ctx: &Context, font_size: f64) -> LayoutBox {
161
-        let slot_width = font_size * 0.8;
162
-        let slot_height = font_size * 0.8;
171
+        // Keep slots readable at nested depths by enforcing a minimum visual size.
172
+        let slot_width = (font_size * 0.86).max(10.0);
173
+        let slot_height = (font_size * 0.9).max(11.0);
163174
 
164175
         LayoutBox {
165176
             width: slot_width,
166
-            ascent: slot_height * 0.6,
167
-            descent: slot_height * 0.4,
177
+            ascent: slot_height * 0.62,
178
+            descent: slot_height * 0.38,
168179
             children: vec![],
169180
         }
170181
     }
@@ -196,34 +207,28 @@ impl MathLayoutEngine {
196207
             width,
197208
             ascent: gap + bar_thickness / 2.0 + num_layout.height(),
198209
             descent: gap + bar_thickness / 2.0 + den_layout.height(),
199
-            children: vec![
200
-                (num_x, num_y, num_layout),
201
-                (den_x, den_y, den_layout),
202
-            ],
210
+            children: vec![(num_x, num_y, num_layout), (den_x, den_y, den_layout)],
203211
         }
204212
     }
205213
 
206214
     /// Layout a power (superscript)
207
-    fn layout_power(
208
-        &self,
209
-        base: &MathBox,
210
-        exp: &MathBox,
211
-        ctx: &Context,
212
-        depth: u32,
213
-    ) -> LayoutBox {
215
+    fn layout_power(&self, base: &MathBox, exp: &MathBox, ctx: &Context, depth: u32) -> LayoutBox {
214216
         let base_layout = self.layout_at_depth(base, ctx, depth);
215217
         let exp_layout = self.layout_at_depth(exp, ctx, depth + 1);
218
+        let scale = self.scale_for_depth(depth);
219
+        let font_size = self.base_font_size * scale;
216220
 
217
-        // Exponent is raised above the baseline
218
-        let exp_raise = base_layout.ascent * 0.5;
221
+        // Exponent is raised above the baseline and slightly kerned to the right.
222
+        let exp_raise = base_layout.ascent * 0.58 + exp_layout.descent * 0.1;
223
+        let exp_kern = (font_size * 0.06).max(0.8);
219224
 
220225
         LayoutBox {
221
-            width: base_layout.width + exp_layout.width,
222
-            ascent: (base_layout.ascent).max(exp_raise + exp_layout.height()),
226
+            width: base_layout.width + exp_kern + exp_layout.width,
227
+            ascent: base_layout.ascent.max(exp_raise + exp_layout.ascent),
223228
             descent: base_layout.descent,
224229
             children: vec![
225230
                 (0.0, 0.0, base_layout.clone()),
226
-                (base_layout.width, -exp_raise, exp_layout),
231
+                (base_layout.width + exp_kern, -exp_raise, exp_layout),
227232
             ],
228233
         }
229234
     }
@@ -238,17 +243,23 @@ impl MathLayoutEngine {
238243
     ) -> LayoutBox {
239244
         let base_layout = self.layout_at_depth(base, ctx, depth);
240245
         let sub_layout = self.layout_at_depth(sub, ctx, depth + 1);
246
+        let scale = self.scale_for_depth(depth);
247
+        let font_size = self.base_font_size * scale;
241248
 
242
-        // Subscript is lowered below the baseline
243
-        let sub_lower = base_layout.descent + sub_layout.ascent * 0.3;
249
+        // Subscript is lowered with a small right kern to avoid touching the base.
250
+        let sub_lower =
251
+            (base_layout.descent * 0.6 + sub_layout.ascent * 0.9).max(sub_layout.ascent * 0.75);
252
+        let sub_kern = (font_size * 0.05).max(0.6);
244253
 
245254
         LayoutBox {
246
-            width: base_layout.width + sub_layout.width,
247
-            ascent: base_layout.ascent,
248
-            descent: (base_layout.descent).max(sub_lower + sub_layout.height()),
255
+            width: base_layout.width + sub_kern + sub_layout.width,
256
+            ascent: base_layout
257
+                .ascent
258
+                .max((sub_layout.ascent - sub_lower).max(0.0)),
259
+            descent: base_layout.descent.max(sub_lower + sub_layout.descent),
249260
             children: vec![
250261
                 (0.0, 0.0, base_layout.clone()),
251
-                (base_layout.width, sub_lower, sub_layout),
262
+                (base_layout.width + sub_kern, sub_lower, sub_layout),
252263
             ],
253264
         }
254265
     }
@@ -299,13 +310,11 @@ impl MathLayoutEngine {
299310
     }
300311
 
301312
     /// Layout a function call
302
-    fn layout_func(
303
-        &self,
304
-        name: &str,
305
-        args: &[MathBox],
306
-        ctx: &Context,
307
-        depth: u32,
308
-    ) -> LayoutBox {
313
+    fn layout_func(&self, name: &str, args: &[MathBox], ctx: &Context, depth: u32) -> LayoutBox {
314
+        if name == "factorial" && args.len() == 1 {
315
+            return self.layout_factorial(&args[0], ctx, depth);
316
+        }
317
+
309318
         let scale = self.scale_for_depth(depth);
310319
         let font_size = self.base_font_size * scale;
311320
 
@@ -346,6 +355,23 @@ impl MathLayoutEngine {
346355
         }
347356
     }
348357
 
358
+    fn layout_factorial(&self, arg: &MathBox, ctx: &Context, depth: u32) -> LayoutBox {
359
+        let scale = self.scale_for_depth(depth);
360
+        let font_size = self.base_font_size * scale;
361
+        let gap = (font_size * 0.06).max(0.6);
362
+
363
+        let arg_layout = self.layout_at_depth(arg, ctx, depth);
364
+        let arg_width = arg_layout.width;
365
+        let bang_layout = self.layout_text("!", ctx, font_size);
366
+
367
+        LayoutBox {
368
+            width: arg_width + gap + bang_layout.width,
369
+            ascent: arg_layout.ascent.max(bang_layout.ascent),
370
+            descent: arg_layout.descent.max(bang_layout.descent),
371
+            children: vec![(0.0, 0.0, arg_layout), (arg_width + gap, 0.0, bang_layout)],
372
+        }
373
+    }
374
+
349375
     /// Layout absolute value
350376
     fn layout_abs(&self, inner: &MathBox, ctx: &Context, depth: u32) -> LayoutBox {
351377
         let inner_layout = self.layout_at_depth(inner, ctx, depth);
@@ -389,48 +415,45 @@ impl MathLayoutEngine {
389415
 
390416
         let body_layout = self.layout_at_depth(body, ctx, depth);
391417
         let var_layout = self.layout_at_depth(var, ctx, depth);
418
+        let d_layout = self.layout_text("d", ctx, font_size);
392419
 
393
-        // Integral symbol sizing
394
-        let int_height = (body_layout.height()).max(font_size * 1.5);
395
-        let int_width = font_size * 0.5;
420
+        // Integral symbol sizing and metrics
421
+        let symbol_size = body_layout.height().max(font_size * 1.8);
422
+        ctx.set_font_size(symbol_size);
423
+        let int_extents = ctx.text_extents("∫").unwrap();
424
+        let int_font_extents = ctx.font_extents().unwrap();
425
+        let symbol_width = int_extents.x_advance();
426
+        let symbol_ascent = int_font_extents.ascent();
427
+        let symbol_descent = int_font_extents.descent();
428
+
429
+        let lo_layout = lower.map(|lo| self.layout_at_depth(lo, ctx, depth + 1));
430
+        let hi_layout = upper.map(|hi| self.layout_at_depth(hi, ctx, depth + 1));
431
+
432
+        let bound_gap = font_size * 0.14;
433
+        let bounds_width = symbol_width
434
+            .max(lo_layout.as_ref().map(|l| l.width).unwrap_or(0.0))
435
+            .max(hi_layout.as_ref().map(|l| l.width).unwrap_or(0.0));
436
+        let body_gap = font_size * 0.22;
437
+        let dx_gap = font_size * 0.14;
396438
 
397
-        let mut width = int_width;
398
-        let mut ascent = int_height / 2.0;
399
-        let mut descent = int_height / 2.0;
400439
         let mut children = vec![];
401
-
402
-        // Lower bound
403
-        if let Some(lo) = lower {
404
-            let lo_layout = self.layout_at_depth(lo, ctx, depth + 1);
405
-            children.push((0.0, int_height / 2.0 + lo_layout.ascent, lo_layout.clone()));
406
-            descent = descent.max(int_height / 2.0 + lo_layout.height());
407
-            width = width.max(lo_layout.width);
408
-        }
409
-
410
-        // Upper bound
411
-        if let Some(hi) = upper {
412
-            let hi_layout = self.layout_at_depth(hi, ctx, depth + 1);
413
-            children.push((0.0, -(int_height / 2.0 + hi_layout.descent), hi_layout.clone()));
414
-            ascent = ascent.max(int_height / 2.0 + hi_layout.height());
415
-            width = width.max(hi_layout.width);
416
-        }
417
-
418
-        // Body
419
-        let body_x = width + font_size * 0.2;
440
+        let body_x = bounds_width + body_gap;
420441
         children.push((body_x, 0.0, body_layout.clone()));
421
-        width = body_x + body_layout.width;
422442
 
423
-        // "dx" part
424
-        let d_layout = self.layout_text("d", ctx, font_size);
425
-        children.push((width + font_size * 0.1, 0.0, d_layout.clone()));
426
-        width += font_size * 0.1 + d_layout.width;
427
-        children.push((width, 0.0, var_layout.clone()));
428
-        width += var_layout.width;
443
+        let d_x = body_x + body_layout.width + dx_gap;
444
+        children.push((d_x, 0.0, d_layout.clone()));
445
+        let var_x = d_x + d_layout.width;
446
+        children.push((var_x, 0.0, var_layout.clone()));
447
+
448
+        let ascent =
449
+            symbol_ascent + bound_gap + hi_layout.as_ref().map(|l| l.height()).unwrap_or(0.0);
450
+        let descent =
451
+            symbol_descent + bound_gap + lo_layout.as_ref().map(|l| l.height()).unwrap_or(0.0);
429452
 
430453
         LayoutBox {
431
-            width,
432
-            ascent,
433
-            descent,
454
+            width: var_x + var_layout.width,
455
+            ascent: ascent.max(body_layout.ascent),
456
+            descent: descent.max(body_layout.descent),
434457
             children,
435458
         }
436459
     }
@@ -446,6 +469,7 @@ impl MathLayoutEngine {
446469
     ) -> LayoutBox {
447470
         let scale = self.scale_for_depth(depth);
448471
         let font_size = self.base_font_size * scale;
472
+        let frac_font_size = font_size * 0.8;
449473
 
450474
         let var_layout = self.layout_at_depth(var, ctx, depth + 1);
451475
         let body_layout = self.layout_at_depth(body, ctx, depth);
@@ -463,22 +487,20 @@ impl MathLayoutEngine {
463487
             "d".to_string()
464488
         };
465489
 
466
-        let d_layout = self.layout_text(&d_str, ctx, font_size);
467
-        let dx_layout = self.layout_text(&dx_str, ctx, font_size);
468
-
469
-        let frac_width = d_layout.width.max(dx_layout.width + var_layout.width) + font_size * 0.2;
470
-        let bar_gap = font_size * 0.1;
471
-
472
-        let width = frac_width + font_size * 0.2 + body_layout.width;
473
-        let frac_height = d_layout.height() + dx_layout.height() + var_layout.height() + bar_gap * 2.0;
490
+        let d_layout = self.layout_text(&d_str, ctx, frac_font_size);
491
+        let dx_layout = self.layout_text(&dx_str, ctx, frac_font_size);
492
+        let den_sep = (frac_font_size * 0.08).max(0.6);
493
+        let denom_width = dx_layout.width + den_sep + var_layout.width;
494
+        let denom_height = dx_layout.height().max(var_layout.height());
495
+        let frac_width = d_layout.width.max(denom_width) + font_size * 0.18;
496
+        let bar_gap = font_size * 0.14;
497
+        let body_gap = font_size * 0.3;
474498
 
475499
         LayoutBox {
476
-            width,
477
-            ascent: frac_height / 2.0 + bar_gap,
478
-            descent: frac_height / 2.0 + bar_gap,
479
-            children: vec![
480
-                (frac_width + font_size * 0.2, 0.0, body_layout),
481
-            ],
500
+            width: frac_width + body_gap + body_layout.width,
501
+            ascent: (bar_gap + d_layout.height()).max(body_layout.ascent),
502
+            descent: (bar_gap + denom_height).max(body_layout.descent),
503
+            children: vec![(frac_width + body_gap, 0.0, body_layout)],
482504
         }
483505
     }
484506
 
@@ -501,33 +523,43 @@ impl MathLayoutEngine {
501523
 
502524
         // "lim" text
503525
         let lim_layout = self.layout_text("lim", ctx, font_size);
526
+        let lim_width = lim_layout.width;
504527
 
505528
         // Build subscript: "x→a" or "x→a⁺" or "x→a⁻"
506
-        let arrow_layout = self.layout_text("→", ctx, font_size * 0.7);
529
+        let sub_font_size = font_size * 0.7;
530
+        let arrow_layout = self.layout_text("→", ctx, sub_font_size);
507531
         let dir_str = match direction {
508532
             Some(LimitDirection::FromRight) => "⁺",
509533
             Some(LimitDirection::FromLeft) => "⁻",
510534
             None => "",
511535
         };
512536
         let dir_layout = if !dir_str.is_empty() {
513
-            Some(self.layout_text(dir_str, ctx, font_size * 0.5))
537
+            Some(self.layout_text(dir_str, ctx, sub_font_size * 0.6))
514538
         } else {
515539
             None
516540
         };
517541
 
518
-        let subscript_width = var_layout.width + arrow_layout.width + to_layout.width
542
+        let sub_sep = (sub_font_size * 0.08).max(0.5);
543
+        let subscript_width = var_layout.width
544
+            + sub_sep
545
+            + arrow_layout.width
546
+            + sub_sep
547
+            + to_layout.width
519548
             + dir_layout.as_ref().map(|l| l.width).unwrap_or(0.0);
520
-        let subscript_height = var_layout.height().max(arrow_layout.height()).max(to_layout.height());
549
+        let subscript_height = var_layout
550
+            .height()
551
+            .max(arrow_layout.height())
552
+            .max(to_layout.height());
521553
 
522
-        let lim_width = lim_layout.width.max(subscript_width);
554
+        let lim_col_width = lim_width.max(subscript_width);
555
+        let body_gap = font_size * 0.3;
556
+        let subscript_drop = font_size * 0.2 + subscript_height;
523557
 
524558
         LayoutBox {
525
-            width: lim_width + font_size * 0.3 + body_layout.width,
559
+            width: lim_col_width + body_gap + body_layout.width,
526560
             ascent: lim_layout.ascent.max(body_layout.ascent),
527
-            descent: (lim_layout.descent + subscript_height + font_size * 0.1).max(body_layout.descent),
528
-            children: vec![
529
-                (lim_width + font_size * 0.3, 0.0, body_layout),
530
-            ],
561
+            descent: subscript_drop.max(body_layout.descent),
562
+            children: vec![(lim_col_width + body_gap, 0.0, body_layout)],
531563
         }
532564
     }
533565
 
@@ -535,7 +567,7 @@ impl MathLayoutEngine {
535567
     fn layout_bigop(
536568
         &self,
537569
         symbol: &str,
538
-        _var: &MathBox,
570
+        var: &MathBox,
539571
         lower: &MathBox,
540572
         upper: &MathBox,
541573
         body: &MathBox,
@@ -546,6 +578,7 @@ impl MathLayoutEngine {
546578
         let font_size = self.base_font_size * scale;
547579
 
548580
         let body_layout = self.layout_at_depth(body, ctx, depth);
581
+        let var_layout = self.layout_at_depth(var, ctx, depth + 1);
549582
         let lower_layout = self.layout_at_depth(lower, ctx, depth + 1);
550583
         let upper_layout = self.layout_at_depth(upper, ctx, depth + 1);
551584
 
@@ -554,21 +587,34 @@ impl MathLayoutEngine {
554587
         ctx.set_font_size(symbol_size);
555588
         let symbol_extents = ctx.text_extents(symbol).unwrap();
556589
         let symbol_width = symbol_extents.x_advance();
557
-        let symbol_height = symbol_size;
558
-
559
-        let op_width = symbol_width.max(lower_layout.width).max(upper_layout.width);
560
-        let gap = font_size * 0.1;
561
-
562
-        let ascent = symbol_height / 2.0 + gap + upper_layout.height();
563
-        let descent = symbol_height / 2.0 + gap + lower_layout.height();
590
+        let symbol_font_extents = ctx.font_extents().unwrap();
591
+        let symbol_ascent = symbol_font_extents.ascent();
592
+        let symbol_descent = symbol_font_extents.descent();
593
+
594
+        let bound_font_size = self.base_font_size * self.scale_for_depth(depth + 1);
595
+        let eq_layout = self.layout_text("=", ctx, bound_font_size);
596
+        let lower_sep = (bound_font_size * 0.08).max(0.6);
597
+        let lower_block_width =
598
+            var_layout.width + lower_sep + eq_layout.width + lower_sep + lower_layout.width;
599
+
600
+        let op_width = symbol_width.max(lower_block_width).max(upper_layout.width);
601
+        let bounds_gap = font_size * 0.16;
602
+        let body_gap = font_size * 0.32;
603
+        // Rendering centers big-op symbols around the expression baseline.
604
+        // Reserve half of total glyph height above and below for consistent placement.
605
+        let symbol_half_height = (symbol_ascent + symbol_descent) * 0.5;
606
+
607
+        let ascent =
608
+            (symbol_half_height + bounds_gap + upper_layout.height()).max(body_layout.ascent);
609
+        let descent =
610
+            (symbol_half_height + bounds_gap + lower_layout.height().max(var_layout.height()))
611
+                .max(body_layout.descent);
564612
 
565613
         LayoutBox {
566
-            width: op_width + font_size * 0.3 + body_layout.width,
567
-            ascent: ascent.max(body_layout.ascent),
568
-            descent: descent.max(body_layout.descent),
569
-            children: vec![
570
-                (op_width + font_size * 0.3, 0.0, body_layout),
571
-            ],
614
+            width: op_width + body_gap + body_layout.width,
615
+            ascent,
616
+            descent,
617
+            children: vec![(op_width + body_gap, 0.0, body_layout)],
572618
         }
573619
     }
574620
 
@@ -613,8 +659,8 @@ impl MathLayoutEngine {
613659
         let total_width: f64 = col_widths.iter().sum::<f64>()
614660
             + cell_padding * (num_cols as f64 - 1.0)
615661
             + 2.0 * bracket_width;
616
-        let total_height: f64 = row_heights.iter().sum::<f64>()
617
-            + cell_padding * (rows.len() as f64 - 1.0);
662
+        let total_height: f64 =
663
+            row_heights.iter().sum::<f64>() + cell_padding * (rows.len() as f64 - 1.0);
618664
 
619665
         // Position cells
620666
         let mut children = vec![];
@@ -694,6 +740,12 @@ fn superscript_digits(n: u32) -> String {
694740
 #[cfg(test)]
695741
 mod tests {
696742
     use super::*;
743
+    use cairo::{Context, Format, ImageSurface};
744
+
745
+    fn test_context() -> Context {
746
+        let surface = ImageSurface::create(Format::ARgb32, 256, 256).unwrap();
747
+        Context::new(&surface).unwrap()
748
+    }
697749
 
698750
     #[test]
699751
     fn test_scale_for_depth() {
@@ -709,4 +761,158 @@ mod tests {
709761
         assert_eq!(superscript_digits(2), "²");
710762
         assert_eq!(superscript_digits(123), "¹²³");
711763
     }
764
+
765
+    #[test]
766
+    fn test_slot_min_size_at_nested_depth() {
767
+        let engine = MathLayoutEngine::default();
768
+        let ctx = test_context();
769
+        let slot = engine.layout_with_depth(&MathBox::Slot, &ctx, 2);
770
+        assert!(slot.width >= 10.0);
771
+        assert!(slot.height() >= 11.0);
772
+    }
773
+
774
+    #[test]
775
+    fn test_power_layout_raises_exponent() {
776
+        let engine = MathLayoutEngine::default();
777
+        let ctx = test_context();
778
+        let power = MathBox::Power {
779
+            base: Box::new(MathBox::Number("2".to_string())),
780
+            exp: Box::new(MathBox::Slot),
781
+        };
782
+        let layout = engine.layout(&power, &ctx);
783
+        let base_layout = engine.layout(&MathBox::Number("2".to_string()), &ctx);
784
+
785
+        assert!(layout.width > base_layout.width);
786
+        assert!(layout.ascent > base_layout.ascent);
787
+        assert_eq!(layout.children.len(), 2);
788
+        assert!(layout.children[1].1 < 0.0);
789
+    }
790
+
791
+    #[test]
792
+    fn test_subscript_layout_lowers_and_extends_descent() {
793
+        let engine = MathLayoutEngine::default();
794
+        let ctx = test_context();
795
+        let sub = MathBox::Subscript {
796
+            base: Box::new(MathBox::Number("x".to_string())),
797
+            sub: Box::new(MathBox::Slot),
798
+        };
799
+        let layout = engine.layout(&sub, &ctx);
800
+        let base_layout = engine.layout(&MathBox::Number("x".to_string()), &ctx);
801
+
802
+        assert!(layout.descent > base_layout.descent);
803
+        assert_eq!(layout.children.len(), 2);
804
+        assert!(layout.children[1].1 > 0.0);
805
+    }
806
+
807
+    #[test]
808
+    fn test_bigop_layout_reserves_space_for_var_equals_lower() {
809
+        let engine = MathLayoutEngine::default();
810
+        let ctx = test_context();
811
+
812
+        let short = MathBox::Sum {
813
+            var: Box::new(MathBox::Symbol("i".to_string())),
814
+            lower: Box::new(MathBox::Number("1".to_string())),
815
+            upper: Box::new(MathBox::Number("5".to_string())),
816
+            body: Box::new(MathBox::Slot),
817
+        };
818
+        let wide = MathBox::Sum {
819
+            var: Box::new(MathBox::Symbol("index".to_string())),
820
+            lower: Box::new(MathBox::Number("123456".to_string())),
821
+            upper: Box::new(MathBox::Number("5".to_string())),
822
+            body: Box::new(MathBox::Slot),
823
+        };
824
+
825
+        let short_layout = engine.layout(&short, &ctx);
826
+        let wide_layout = engine.layout(&wide, &ctx);
827
+        assert!(wide_layout.width > short_layout.width);
828
+    }
829
+
830
+    #[test]
831
+    fn test_bigop_layout_expands_ascent_descent_for_bounds() {
832
+        let engine = MathLayoutEngine::default();
833
+        let ctx = test_context();
834
+
835
+        let sum = MathBox::Sum {
836
+            var: Box::new(MathBox::Symbol("i".to_string())),
837
+            lower: Box::new(MathBox::Slot),
838
+            upper: Box::new(MathBox::Slot),
839
+            body: Box::new(MathBox::Number("x".to_string())),
840
+        };
841
+        let body_layout = engine.layout(&MathBox::Number("x".to_string()), &ctx);
842
+        let sum_layout = engine.layout(&sum, &ctx);
843
+
844
+        assert!(sum_layout.ascent > body_layout.ascent);
845
+        assert!(sum_layout.descent > body_layout.descent);
846
+    }
847
+
848
+    #[test]
849
+    fn test_derivative_layout_widens_for_long_variable() {
850
+        let engine = MathLayoutEngine::default();
851
+        let ctx = test_context();
852
+
853
+        let short = MathBox::Derivative {
854
+            order: 1,
855
+            var: Box::new(MathBox::Symbol("x".to_string())),
856
+            body: Box::new(MathBox::Slot),
857
+        };
858
+        let long = MathBox::Derivative {
859
+            order: 1,
860
+            var: Box::new(MathBox::Symbol("variable".to_string())),
861
+            body: Box::new(MathBox::Slot),
862
+        };
863
+
864
+        let short_layout = engine.layout(&short, &ctx);
865
+        let long_layout = engine.layout(&long, &ctx);
866
+        assert!(long_layout.width > short_layout.width);
867
+    }
868
+
869
+    #[test]
870
+    fn test_integral_layout_bounds_expand_vertical_space() {
871
+        let engine = MathLayoutEngine::default();
872
+        let ctx = test_context();
873
+
874
+        let plain = MathBox::Integral {
875
+            lower: None,
876
+            upper: None,
877
+            body: Box::new(MathBox::Slot),
878
+            var: Box::new(MathBox::Symbol("x".to_string())),
879
+        };
880
+        let bounded = MathBox::Integral {
881
+            lower: Some(Box::new(MathBox::Slot)),
882
+            upper: Some(Box::new(MathBox::Slot)),
883
+            body: Box::new(MathBox::Slot),
884
+            var: Box::new(MathBox::Symbol("x".to_string())),
885
+        };
886
+
887
+        let plain_layout = engine.layout(&plain, &ctx);
888
+        let bounded_layout = engine.layout(&bounded, &ctx);
889
+        assert!(bounded_layout.ascent > plain_layout.ascent);
890
+        assert!(bounded_layout.descent > plain_layout.descent);
891
+    }
892
+
893
+    #[test]
894
+    fn test_limit_layout_widens_for_wide_subscript_and_extends_descent() {
895
+        let engine = MathLayoutEngine::default();
896
+        let ctx = test_context();
897
+
898
+        let narrow = MathBox::Limit {
899
+            var: Box::new(MathBox::Symbol("x".to_string())),
900
+            to: Box::new(MathBox::Number("1".to_string())),
901
+            direction: None,
902
+            body: Box::new(MathBox::Slot),
903
+        };
904
+        let wide = MathBox::Limit {
905
+            var: Box::new(MathBox::Symbol("veryLongVariable".to_string())),
906
+            to: Box::new(MathBox::Number("123456".to_string())),
907
+            direction: Some(LimitDirection::FromRight),
908
+            body: Box::new(MathBox::Slot),
909
+        };
910
+
911
+        let narrow_layout = engine.layout(&narrow, &ctx);
912
+        let wide_layout = engine.layout(&wide, &ctx);
913
+        let body_layout = engine.layout(&MathBox::Slot, &ctx);
914
+
915
+        assert!(wide_layout.width > narrow_layout.width);
916
+        assert!(wide_layout.descent > body_layout.descent);
917
+    }
712918
 }
garcalc-math/src/lib.rsmodified
@@ -6,14 +6,14 @@
66
 //! - Keyboard navigation through expression tree
77
 //! - Bidirectional conversion with CAS Expr type
88
 
9
-pub mod mathbox;
9
+pub mod convert;
10
+pub mod input;
1011
 pub mod layout;
12
+pub mod mathbox;
1113
 pub mod render;
12
-pub mod input;
13
-pub mod convert;
1414
 
15
-pub use mathbox::{MathBox, Cursor};
15
+pub use convert::{ConvertError, from_expr, to_expr};
16
+pub use input::{InputResult, MathInput};
1617
 pub use layout::{LayoutBox, MathLayoutEngine};
18
+pub use mathbox::{Cursor, MathBox};
1719
 pub use render::MathRenderer;
18
-pub use input::{MathInput, InputResult};
19
-pub use convert::{to_expr, from_expr, ConvertError};
garcalc-math/src/mathbox.rsmodified
@@ -43,10 +43,7 @@ pub enum MathBox {
4343
     },
4444
 
4545
     /// Function call with arguments
46
-    Func {
47
-        name: String,
48
-        args: Vec<MathBox>,
49
-    },
46
+    Func { name: String, args: Vec<MathBox> },
5047
 
5148
     /// Absolute value
5249
     Abs(Box<MathBox>),
@@ -113,9 +110,7 @@ pub enum MathBox {
113110
     },
114111
 
115112
     /// Matrix with rows of cells
116
-    Matrix {
117
-        rows: Vec<Vec<MathBox>>,
118
-    },
113
+    Matrix { rows: Vec<Vec<MathBox>> },
119114
 
120115
     /// Horizontal sequence of elements (e.g., 2 + 3 × x)
121116
     Row(Vec<MathBox>),
@@ -127,17 +122,17 @@ pub enum MathBox {
127122
 /// Mathematical operators
128123
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
129124
 pub enum Operator {
130
-    Add,      // +
131
-    Sub,      // -
132
-    Mul,      // × or *
133
-    Div,      // ÷ (for inline division, not fraction)
134
-    Eq,       // =
135
-    Lt,       // <
136
-    Gt,       // >
137
-    Le,       // ≤
138
-    Ge,       // ≥
139
-    Ne,       // ≠
140
-    Comma,    // ,
125
+    Add,   // +
126
+    Sub,   // -
127
+    Mul,   // × or *
128
+    Div,   // ÷ (for inline division, not fraction)
129
+    Eq,    // =
130
+    Lt,    // <
131
+    Gt,    // >
132
+    Le,    // ≤
133
+    Ge,    // ≥
134
+    Ne,    // ≠
135
+    Comma, // ,
141136
 }
142137
 
143138
 impl Operator {
@@ -363,7 +358,10 @@ impl MathBox {
363358
                 1 => Some(sub),
364359
                 _ => None,
365360
             },
366
-            MathBox::Root { index: idx, radicand } => {
361
+            MathBox::Root {
362
+                index: idx,
363
+                radicand,
364
+            } => {
367365
                 if let Some(i) = idx {
368366
                     match index {
369367
                         0 => Some(i),
@@ -376,26 +374,39 @@ impl MathBox {
376374
                         _ => None,
377375
                     }
378376
                 }
379
-            },
377
+            }
380378
             MathBox::Func { args, .. } => args.get_mut(index),
381379
             MathBox::Abs(inner) | MathBox::Parens(inner) => match index {
382380
                 0 => Some(inner),
383381
                 _ => None,
384382
             },
385
-            MathBox::Integral { lower, upper, body, var } => {
383
+            MathBox::Integral {
384
+                lower,
385
+                upper,
386
+                body,
387
+                var,
388
+            } => {
386389
                 let mut i = 0;
387390
                 if let Some(l) = lower {
388
-                    if index == i { return Some(l); }
391
+                    if index == i {
392
+                        return Some(l);
393
+                    }
389394
                     i += 1;
390395
                 }
391396
                 if let Some(u) = upper {
392
-                    if index == i { return Some(u); }
397
+                    if index == i {
398
+                        return Some(u);
399
+                    }
393400
                     i += 1;
394401
                 }
395
-                if index == i { return Some(body); }
396
-                if index == i + 1 { return Some(var); }
402
+                if index == i {
403
+                    return Some(body);
404
+                }
405
+                if index == i + 1 {
406
+                    return Some(var);
407
+                }
397408
                 None
398
-            },
409
+            }
399410
             MathBox::Derivative { var, body, .. } => match index {
400411
                 0 => Some(var),
401412
                 1 => Some(body),
@@ -407,8 +418,18 @@ impl MathBox {
407418
                 2 => Some(body),
408419
                 _ => None,
409420
             },
410
-            MathBox::Sum { var, lower, upper, body }
411
-            | MathBox::Product { var, lower, upper, body } => match index {
421
+            MathBox::Sum {
422
+                var,
423
+                lower,
424
+                upper,
425
+                body,
426
+            }
427
+            | MathBox::Product {
428
+                var,
429
+                lower,
430
+                upper,
431
+                body,
432
+            } => match index {
412433
                 0 => Some(var),
413434
                 1 => Some(lower),
414435
                 2 => Some(upper),
@@ -426,7 +447,7 @@ impl MathBox {
426447
                     }
427448
                 }
428449
                 None
429
-            },
450
+            }
430451
             MathBox::Row(items) => items.get_mut(index),
431452
             _ => None,
432453
         }
@@ -450,7 +471,10 @@ impl MathBox {
450471
                 1 => Some(sub),
451472
                 _ => None,
452473
             },
453
-            MathBox::Root { index: idx, radicand } => {
474
+            MathBox::Root {
475
+                index: idx,
476
+                radicand,
477
+            } => {
454478
                 if let Some(i) = idx {
455479
                     match index {
456480
                         0 => Some(i),
@@ -463,26 +487,39 @@ impl MathBox {
463487
                         _ => None,
464488
                     }
465489
                 }
466
-            },
490
+            }
467491
             MathBox::Func { args, .. } => args.get(index),
468492
             MathBox::Abs(inner) | MathBox::Parens(inner) => match index {
469493
                 0 => Some(inner),
470494
                 _ => None,
471495
             },
472
-            MathBox::Integral { lower, upper, body, var } => {
496
+            MathBox::Integral {
497
+                lower,
498
+                upper,
499
+                body,
500
+                var,
501
+            } => {
473502
                 let mut i = 0;
474503
                 if let Some(l) = lower {
475
-                    if index == i { return Some(l); }
504
+                    if index == i {
505
+                        return Some(l);
506
+                    }
476507
                     i += 1;
477508
                 }
478509
                 if let Some(u) = upper {
479
-                    if index == i { return Some(u); }
510
+                    if index == i {
511
+                        return Some(u);
512
+                    }
480513
                     i += 1;
481514
                 }
482
-                if index == i { return Some(body); }
483
-                if index == i + 1 { return Some(var); }
515
+                if index == i {
516
+                    return Some(body);
517
+                }
518
+                if index == i + 1 {
519
+                    return Some(var);
520
+                }
484521
                 None
485
-            },
522
+            }
486523
             MathBox::Derivative { var, body, .. } => match index {
487524
                 0 => Some(var),
488525
                 1 => Some(body),
@@ -494,8 +531,18 @@ impl MathBox {
494531
                 2 => Some(body),
495532
                 _ => None,
496533
             },
497
-            MathBox::Sum { var, lower, upper, body }
498
-            | MathBox::Product { var, lower, upper, body } => match index {
534
+            MathBox::Sum {
535
+                var,
536
+                lower,
537
+                upper,
538
+                body,
539
+            }
540
+            | MathBox::Product {
541
+                var,
542
+                lower,
543
+                upper,
544
+                body,
545
+            } => match index {
499546
                 0 => Some(var),
500547
                 1 => Some(lower),
501548
                 2 => Some(upper),
@@ -513,7 +560,7 @@ impl MathBox {
513560
                     }
514561
                 }
515562
                 None
516
-            },
563
+            }
517564
             MathBox::Row(items) => items.get(index),
518565
             _ => None,
519566
         }
garcalc-math/src/render.rsmodified
1305 lines changed — click to load
@@ -4,9 +4,10 @@
44
 //! fraction bars, radical signs, integral symbols, etc.
55
 
66
 use crate::layout::{LayoutBox, MathLayoutEngine};
7
-use crate::mathbox::{MathBox, Operator, LimitDirection};
7
+use crate::mathbox::{LimitDirection, MathBox, Operator};
88
 use cairo::Context;
99
 use gartk_core::Color;
10
+use std::cell::Cell;
1011
 
1112
 /// Renderer for mathematical notation
1213
 pub struct MathRenderer<'a> {
@@ -18,6 +19,8 @@ pub struct MathRenderer<'a> {
1819
     pub slot_bg_color: Color,
1920
     /// Slot border color when focused
2021
     pub slot_focus_color: Color,
22
+    /// Whether the insertion cursor should be drawn
23
+    cursor_visible: Cell<bool>,
2124
 }
2225
 
2326
 impl<'a> MathRenderer<'a> {
@@ -29,18 +32,29 @@ impl<'a> MathRenderer<'a> {
2932
             fg_color: Color::new(0.0, 0.0, 0.0, 1.0),
3033
             slot_bg_color: Color::new(0.9, 0.9, 0.95, 1.0),
3134
             slot_focus_color: Color::new(0.3, 0.5, 0.9, 1.0),
35
+            cursor_visible: Cell::new(true),
3236
         }
3337
     }
3438
 
3539
     /// Render a MathBox tree at the given position
3640
     /// (x, y) is the baseline position
3741
     pub fn render(&self, mathbox: &MathBox, x: f64, y: f64) {
38
-        self.render_at_depth(mathbox, x, y, 0, None);
42
+        self.render_at_depth(mathbox, x, y, 0, None, 0);
3943
     }
4044
 
4145
     /// Render with cursor highlighting
42
-    pub fn render_with_cursor(&self, mathbox: &MathBox, x: f64, y: f64, cursor_path: &[usize]) {
43
-        self.render_at_depth(mathbox, x, y, 0, Some(cursor_path));
46
+    pub fn render_with_cursor(
47
+        &self,
48
+        mathbox: &MathBox,
49
+        x: f64,
50
+        y: f64,
51
+        cursor_path: &[usize],
52
+        cursor_offset: usize,
53
+        cursor_visible: bool,
54
+    ) {
55
+        self.cursor_visible.set(cursor_visible);
56
+        self.render_at_depth(mathbox, x, y, 0, Some(cursor_path), cursor_offset);
57
+        self.cursor_visible.set(true);
4458
     }
4559
 
4660
     /// Internal render with depth tracking
@@ -51,24 +65,39 @@ impl<'a> MathRenderer<'a> {
5165
         y: f64,
5266
         depth: u32,
5367
         cursor_path: Option<&[usize]>,
68
+        cursor_offset: usize,
5469
     ) {
5570
         let scale = self.scale_for_depth(depth);
5671
         let font_size = self.layout_engine.base_font_size * scale;
5772
 
5873
         // Check if cursor is at this node
5974
         let is_cursor_here = cursor_path.map(|p| p.is_empty()).unwrap_or(false);
75
+        let is_active_container = cursor_path.map(|p| p.len() == 1).unwrap_or(false)
76
+            && Self::is_focusable_container(mathbox);
6077
 
6178
         match mathbox {
6279
             MathBox::Number(s) => {
6380
                 self.draw_text(s, x, y, font_size, false);
6481
                 if is_cursor_here {
65
-                    self.draw_cursor(x, y, font_size);
82
+                    let width = self.text_advance(s, font_size, false);
83
+                    self.draw_focus_frame(x, y, width, font_size, 1.4, 0.35);
84
+                }
85
+                if is_cursor_here && self.cursor_visible.get() {
86
+                    let cursor_x =
87
+                        x + self.text_advance_for_offset(s, font_size, false, cursor_offset);
88
+                    self.draw_cursor(cursor_x, y, font_size);
6689
                 }
6790
             }
6891
             MathBox::Symbol(s) => {
6992
                 self.draw_text(s, x, y, font_size, true);
7093
                 if is_cursor_here {
71
-                    self.draw_cursor(x, y, font_size);
94
+                    let width = self.text_advance(s, font_size, true);
95
+                    self.draw_focus_frame(x, y, width, font_size, 1.4, 0.35);
96
+                }
97
+                if is_cursor_here && self.cursor_visible.get() {
98
+                    let cursor_x =
99
+                        x + self.text_advance_for_offset(s, font_size, true, cursor_offset);
100
+                    self.draw_cursor(cursor_x, y, font_size);
72101
                 }
73102
             }
74103
             MathBox::Operator(op) => {
@@ -78,27 +107,40 @@ impl<'a> MathRenderer<'a> {
78107
                 self.draw_slot(x, y, font_size, is_cursor_here);
79108
             }
80109
             MathBox::Fraction { num, den } => {
81
-                self.render_fraction(num, den, x, y, depth, cursor_path);
110
+                self.render_fraction(num, den, x, y, depth, cursor_path, cursor_offset);
82111
             }
83112
             MathBox::Power { base, exp } => {
84
-                self.render_power(base, exp, x, y, depth, cursor_path);
113
+                self.render_power(base, exp, x, y, depth, cursor_path, cursor_offset);
85114
             }
86115
             MathBox::Subscript { base, sub } => {
87
-                self.render_subscript(base, sub, x, y, depth, cursor_path);
116
+                self.render_subscript(base, sub, x, y, depth, cursor_path, cursor_offset);
88117
             }
89118
             MathBox::Root { index, radicand } => {
90
-                self.render_root(index.as_deref(), radicand, x, y, depth, cursor_path);
119
+                self.render_root(
120
+                    index.as_deref(),
121
+                    radicand,
122
+                    x,
123
+                    y,
124
+                    depth,
125
+                    cursor_path,
126
+                    cursor_offset,
127
+                );
91128
             }
92129
             MathBox::Func { name, args } => {
93
-                self.render_func(name, args, x, y, depth, cursor_path);
130
+                self.render_func(name, args, x, y, depth, cursor_path, cursor_offset);
94131
             }
95132
             MathBox::Abs(inner) => {
96
-                self.render_abs(inner, x, y, depth, cursor_path);
133
+                self.render_abs(inner, x, y, depth, cursor_path, cursor_offset);
97134
             }
98135
             MathBox::Parens(inner) => {
99
-                self.render_parens(inner, x, y, depth, cursor_path);
136
+                self.render_parens(inner, x, y, depth, cursor_path, cursor_offset);
100137
             }
101
-            MathBox::Integral { lower, upper, body, var } => {
138
+            MathBox::Integral {
139
+                lower,
140
+                upper,
141
+                body,
142
+                var,
143
+            } => {
102144
                 self.render_integral(
103145
                     lower.as_deref(),
104146
                     upper.as_deref(),
@@ -108,27 +150,79 @@ impl<'a> MathRenderer<'a> {
108150
                     y,
109151
                     depth,
110152
                     cursor_path,
153
+                    cursor_offset,
111154
                 );
112155
             }
113156
             MathBox::Derivative { order, var, body } => {
114
-                self.render_derivative(*order, var, body, x, y, depth, cursor_path);
157
+                self.render_derivative(*order, var, body, x, y, depth, cursor_path, cursor_offset);
115158
             }
116
-            MathBox::Limit { var, to, direction, body } => {
117
-                self.render_limit(var, to, *direction, body, x, y, depth, cursor_path);
159
+            MathBox::Limit {
160
+                var,
161
+                to,
162
+                direction,
163
+                body,
164
+            } => {
165
+                self.render_limit(
166
+                    var,
167
+                    to,
168
+                    *direction,
169
+                    body,
170
+                    x,
171
+                    y,
172
+                    depth,
173
+                    cursor_path,
174
+                    cursor_offset,
175
+                );
118176
             }
119
-            MathBox::Sum { var, lower, upper, body } => {
120
-                self.render_bigop("∑", var, lower, upper, body, x, y, depth, cursor_path);
177
+            MathBox::Sum {
178
+                var,
179
+                lower,
180
+                upper,
181
+                body,
182
+            } => {
183
+                self.render_bigop(
184
+                    "∑",
185
+                    var,
186
+                    lower,
187
+                    upper,
188
+                    body,
189
+                    x,
190
+                    y,
191
+                    depth,
192
+                    cursor_path,
193
+                    cursor_offset,
194
+                );
121195
             }
122
-            MathBox::Product { var, lower, upper, body } => {
123
-                self.render_bigop("∏", var, lower, upper, body, x, y, depth, cursor_path);
196
+            MathBox::Product {
197
+                var,
198
+                lower,
199
+                upper,
200
+                body,
201
+            } => {
202
+                self.render_bigop(
203
+                    "∏",
204
+                    var,
205
+                    lower,
206
+                    upper,
207
+                    body,
208
+                    x,
209
+                    y,
210
+                    depth,
211
+                    cursor_path,
212
+                    cursor_offset,
213
+                );
124214
             }
125215
             MathBox::Matrix { rows } => {
126
-                self.render_matrix(rows, x, y, depth, cursor_path);
216
+                self.render_matrix(rows, x, y, depth, cursor_path, cursor_offset);
127217
             }
128218
             MathBox::Row(items) => {
129
-                self.render_row(items, x, y, depth, cursor_path);
219
+                self.render_row(items, x, y, depth, cursor_path, cursor_offset);
130220
             }
131221
         }
222
+
223
+        if is_active_container {
224
+            self.draw_container_focus_frame(mathbox, x, y, depth);
225
+        }
132226
     }
133227
 
134228
     fn scale_for_depth(&self, depth: u32) -> f64 {
@@ -157,6 +251,43 @@ impl<'a> MathRenderer<'a> {
157251
         self.ctx.restore().unwrap();
158252
     }
159253
 
254
+    /// Measure text advance using the same style as `draw_text`
255
+    fn text_advance(&self, text: &str, font_size: f64, italic: bool) -> f64 {
256
+        self.ctx.save().unwrap();
257
+        let slant = if italic {
258
+            cairo::FontSlant::Italic
259
+        } else {
260
+            cairo::FontSlant::Normal
261
+        };
262
+        self.ctx.select_font_face(
263
+            &self.layout_engine.font_family,
264
+            slant,
265
+            cairo::FontWeight::Normal,
266
+        );
267
+        self.ctx.set_font_size(font_size);
268
+        let advance = self
269
+            .ctx
270
+            .text_extents(text)
271
+            .map(|e| e.x_advance())
272
+            .unwrap_or(0.0);
273
+        self.ctx.restore().unwrap();
274
+        advance
275
+    }
276
+
277
+    fn text_advance_for_offset(
278
+        &self,
279
+        text: &str,
280
+        font_size: f64,
281
+        italic: bool,
282
+        char_offset: usize,
283
+    ) -> f64 {
284
+        let prefix: String = text
285
+            .chars()
286
+            .take(char_offset.min(text.chars().count()))
287
+            .collect();
288
+        self.text_advance(&prefix, font_size, italic)
289
+    }
290
+
160291
     /// Draw an operator
161292
     fn draw_operator(&self, op: Operator, x: f64, y: f64, font_size: f64) {
162293
         let padding = font_size * 0.15;
@@ -165,9 +296,7 @@ impl<'a> MathRenderer<'a> {
165296
 
166297
     /// Draw an empty slot
167298
     fn draw_slot(&self, x: f64, y: f64, font_size: f64, focused: bool) {
168
-        let width = font_size * 0.8;
169
-        let height = font_size * 0.8;
170
-        let ascent = height * 0.6;
299
+        let (width, height, ascent) = Self::slot_geometry(font_size);
171300
 
172301
         self.ctx.save().unwrap();
173302
 
@@ -196,7 +325,7 @@ impl<'a> MathRenderer<'a> {
196325
         self.set_color(&self.slot_focus_color);
197326
         self.ctx.set_line_width(2.0);
198327
 
199
-        let height = font_size * 0.8;
328
+        let (_, height, _) = Self::slot_geometry(font_size);
200329
         self.ctx.move_to(x, y - height * 0.6);
201330
         self.ctx.line_to(x, y + height * 0.4);
202331
         self.ctx.stroke().unwrap();
@@ -204,6 +333,89 @@ impl<'a> MathRenderer<'a> {
204333
         self.ctx.restore().unwrap();
205334
     }
206335
 
336
+    /// Draw a focus frame around the active token
337
+    fn draw_focus_frame(
338
+        &self,
339
+        x: f64,
340
+        y: f64,
341
+        width: f64,
342
+        font_size: f64,
343
+        line_width: f64,
344
+        alpha: f64,
345
+    ) {
346
+        let ascent = font_size * 0.72;
347
+        let descent = font_size * 0.28;
348
+        let padding = (font_size * 0.12).max(1.2);
349
+        self.draw_focus_bounds(x, y, width, ascent, descent, padding, line_width, alpha);
350
+    }
351
+
352
+    /// Draw a focus frame around the active container (e.g. power/fraction)
353
+    fn draw_container_focus_frame(&self, mathbox: &MathBox, x: f64, y: f64, depth: u32) {
354
+        let layout = self
355
+            .layout_engine
356
+            .layout_with_depth(mathbox, self.ctx, depth);
357
+        let scale = self.scale_for_depth(depth);
358
+        let padding = (self.layout_engine.base_font_size * scale * 0.12).max(1.4);
359
+        self.draw_focus_bounds(
360
+            x,
361
+            y,
362
+            layout.width,
363
+            layout.ascent,
364
+            layout.descent,
365
+            padding,
366
+            1.5,
367
+            0.28,
368
+        );
369
+    }
370
+
371
+    fn draw_focus_bounds(
372
+        &self,
373
+        x: f64,
374
+        y: f64,
375
+        width: f64,
376
+        ascent: f64,
377
+        descent: f64,
378
+        padding: f64,
379
+        line_width: f64,
380
+        alpha: f64,
381
+    ) {
382
+        self.ctx.save().unwrap();
383
+        self.set_color(&Color::new(
384
+            self.slot_focus_color.r,
385
+            self.slot_focus_color.g,
386
+            self.slot_focus_color.b,
387
+            alpha,
388
+        ));
389
+        self.ctx.set_line_width(line_width);
390
+        self.ctx.rectangle(
391
+            x - padding,
392
+            y - ascent - padding,
393
+            width + padding * 2.0,
394
+            ascent + descent + padding * 2.0,
395
+        );
396
+        self.ctx.stroke().unwrap();
397
+        self.ctx.restore().unwrap();
398
+    }
399
+
400
+    fn is_focusable_container(mathbox: &MathBox) -> bool {
401
+        matches!(
402
+            mathbox,
403
+            MathBox::Fraction { .. }
404
+                | MathBox::Power { .. }
405
+                | MathBox::Subscript { .. }
406
+                | MathBox::Root { .. }
407
+                | MathBox::Func { .. }
408
+                | MathBox::Abs(_)
409
+                | MathBox::Parens(_)
410
+                | MathBox::Integral { .. }
411
+                | MathBox::Derivative { .. }
412
+                | MathBox::Limit { .. }
413
+                | MathBox::Sum { .. }
414
+                | MathBox::Product { .. }
415
+                | MathBox::Matrix { .. }
416
+        )
417
+    }
418
+
207419
     /// Render a fraction
208420
     fn render_fraction(
209421
         &self,
@@ -213,11 +425,15 @@ impl<'a> MathRenderer<'a> {
213425
         y: f64,
214426
         depth: u32,
215427
         cursor_path: Option<&[usize]>,
428
+        cursor_offset: usize,
216429
     ) {
217
-        let layout = self.layout_engine.layout(&MathBox::Fraction {
218
-            num: Box::new(num.clone()),
219
-            den: Box::new(den.clone()),
220
-        }, self.ctx);
430
+        let layout = self.layout_engine.layout(
431
+            &MathBox::Fraction {
432
+                num: Box::new(num.clone()),
433
+                den: Box::new(den.clone()),
434
+            },
435
+            self.ctx,
436
+        );
221437
 
222438
         let scale = self.scale_for_depth(depth);
223439
         let bar_thickness = 1.0 * scale;
@@ -243,14 +459,22 @@ impl<'a> MathRenderer<'a> {
243459
 
244460
         // Draw numerator and denominator
245461
         let num_cursor = cursor_path.and_then(|p| {
246
-            if !p.is_empty() && p[0] == 0 { Some(&p[1..]) } else { None }
462
+            if !p.is_empty() && p[0] == 0 {
463
+                Some(&p[1..])
464
+            } else {
465
+                None
466
+            }
247467
         });
248468
         let den_cursor = cursor_path.and_then(|p| {
249
-            if !p.is_empty() && p[0] == 1 { Some(&p[1..]) } else { None }
469
+            if !p.is_empty() && p[0] == 1 {
470
+                Some(&p[1..])
471
+            } else {
472
+                None
473
+            }
250474
         });
251475
 
252
-        self.render_at_depth(num, num_x, num_y, depth + 1, num_cursor);
253
-        self.render_at_depth(den, den_x, den_y, depth + 1, den_cursor);
476
+        self.render_at_depth(num, num_x, num_y, depth + 1, num_cursor, cursor_offset);
477
+        self.render_at_depth(den, den_x, den_y, depth + 1, den_cursor, cursor_offset);
254478
     }
255479
 
256480
     /// Render a power (superscript)
@@ -262,20 +486,42 @@ impl<'a> MathRenderer<'a> {
262486
         y: f64,
263487
         depth: u32,
264488
         cursor_path: Option<&[usize]>,
489
+        cursor_offset: usize,
265490
     ) {
266
-        let base_layout = self.layout_engine.layout(base, self.ctx);
491
+        let base_layout = self.layout_engine.layout_with_depth(base, self.ctx, depth);
492
+        let exp_layout = self
493
+            .layout_engine
494
+            .layout_with_depth(exp, self.ctx, depth + 1);
495
+        let scale = self.scale_for_depth(depth);
496
+        let font_size = self.layout_engine.base_font_size * scale;
267497
 
268498
         let base_cursor = cursor_path.and_then(|p| {
269
-            if !p.is_empty() && p[0] == 0 { Some(&p[1..]) } else { None }
499
+            if !p.is_empty() && p[0] == 0 {
500
+                Some(&p[1..])
501
+            } else {
502
+                None
503
+            }
270504
         });
271505
         let exp_cursor = cursor_path.and_then(|p| {
272
-            if !p.is_empty() && p[0] == 1 { Some(&p[1..]) } else { None }
506
+            if !p.is_empty() && p[0] == 1 {
507
+                Some(&p[1..])
508
+            } else {
509
+                None
510
+            }
273511
         });
274512
 
275
-        self.render_at_depth(base, x, y, depth, base_cursor);
276
-
277
-        let exp_raise = base_layout.ascent * 0.5;
278
-        self.render_at_depth(exp, x + base_layout.width, y - exp_raise, depth + 1, exp_cursor);
513
+        self.render_at_depth(base, x, y, depth, base_cursor, cursor_offset);
514
+
515
+        let exp_raise = base_layout.ascent * 0.58 + exp_layout.descent * 0.1;
516
+        let exp_kern = (font_size * 0.06).max(0.8);
517
+        self.render_at_depth(
518
+            exp,
519
+            x + base_layout.width + exp_kern,
520
+            y - exp_raise,
521
+            depth + 1,
522
+            exp_cursor,
523
+            cursor_offset,
524
+        );
279525
     }
280526
 
281527
     /// Render a subscript
@@ -287,21 +533,43 @@ impl<'a> MathRenderer<'a> {
287533
         y: f64,
288534
         depth: u32,
289535
         cursor_path: Option<&[usize]>,
536
+        cursor_offset: usize,
290537
     ) {
291
-        let base_layout = self.layout_engine.layout(base, self.ctx);
292
-        let sub_layout = self.layout_engine.layout(sub, self.ctx);
538
+        let base_layout = self.layout_engine.layout_with_depth(base, self.ctx, depth);
539
+        let sub_layout = self
540
+            .layout_engine
541
+            .layout_with_depth(sub, self.ctx, depth + 1);
542
+        let scale = self.scale_for_depth(depth);
543
+        let font_size = self.layout_engine.base_font_size * scale;
293544
 
294545
         let base_cursor = cursor_path.and_then(|p| {
295
-            if !p.is_empty() && p[0] == 0 { Some(&p[1..]) } else { None }
546
+            if !p.is_empty() && p[0] == 0 {
547
+                Some(&p[1..])
548
+            } else {
549
+                None
550
+            }
296551
         });
297552
         let sub_cursor = cursor_path.and_then(|p| {
298
-            if !p.is_empty() && p[0] == 1 { Some(&p[1..]) } else { None }
553
+            if !p.is_empty() && p[0] == 1 {
554
+                Some(&p[1..])
555
+            } else {
556
+                None
557
+            }
299558
         });
300559
 
301
-        self.render_at_depth(base, x, y, depth, base_cursor);
302
-
303
-        let sub_lower = base_layout.descent + sub_layout.ascent * 0.3;
304
-        self.render_at_depth(sub, x + base_layout.width, y + sub_lower, depth + 1, sub_cursor);
560
+        self.render_at_depth(base, x, y, depth, base_cursor, cursor_offset);
561
+
562
+        let sub_lower =
563
+            (base_layout.descent * 0.6 + sub_layout.ascent * 0.9).max(sub_layout.ascent * 0.75);
564
+        let sub_kern = (font_size * 0.05).max(0.6);
565
+        self.render_at_depth(
566
+            sub,
567
+            x + base_layout.width + sub_kern,
568
+            y + sub_lower,
569
+            depth + 1,
570
+            sub_cursor,
571
+            cursor_offset,
572
+        );
305573
     }
306574
 
307575
     /// Render a root (square or nth)
@@ -313,6 +581,7 @@ impl<'a> MathRenderer<'a> {
313581
         y: f64,
314582
         depth: u32,
315583
         cursor_path: Option<&[usize]>,
584
+        cursor_offset: usize,
316585
     ) {
317586
         let radicand_layout = self.layout_engine.layout(radicand, self.ctx);
318587
         let scale = self.scale_for_depth(depth);
@@ -329,7 +598,11 @@ impl<'a> MathRenderer<'a> {
329598
         if let Some(idx) = index {
330599
             let idx_layout = self.layout_engine.layout(idx, self.ctx);
331600
             let idx_cursor = cursor_path.and_then(|p| {
332
-                if !p.is_empty() && p[0] == 0 { Some(&p[1..]) } else { None }
601
+                if !p.is_empty() && p[0] == 0 {
602
+                    Some(&p[1..])
603
+                } else {
604
+                    None
605
+                }
333606
             });
334607
             self.render_at_depth(
335608
                 idx,
@@ -337,6 +610,7 @@ impl<'a> MathRenderer<'a> {
337610
                 y - radicand_layout.ascent * 0.5 - idx_layout.descent,
338611
                 depth + 2,
339612
                 idx_cursor,
613
+                cursor_offset,
340614
             );
341615
             radicand_x += idx_layout.width;
342616
         }
@@ -349,21 +623,35 @@ impl<'a> MathRenderer<'a> {
349623
         // Radical checkmark
350624
         let check_width = radical_width * 0.4;
351625
         self.ctx.move_to(radicand_x - radical_width, y);
352
-        self.ctx.line_to(radicand_x - radical_width + check_width * 0.3, y + radicand_layout.descent * 0.3);
353
-        self.ctx.line_to(radicand_x - radical_width + check_width, y + radicand_layout.descent);
354
-        self.ctx.line_to(radicand_x, y - radicand_layout.ascent - gap);
626
+        self.ctx.line_to(
627
+            radicand_x - radical_width + check_width * 0.3,
628
+            y + radicand_layout.descent * 0.3,
629
+        );
630
+        self.ctx.line_to(
631
+            radicand_x - radical_width + check_width,
632
+            y + radicand_layout.descent,
633
+        );
634
+        self.ctx
635
+            .line_to(radicand_x, y - radicand_layout.ascent - gap);
355636
 
356637
         // Overbar
357
-        self.ctx.line_to(radicand_x + radicand_layout.width + bar_overhang, y - radicand_layout.ascent - gap);
638
+        self.ctx.line_to(
639
+            radicand_x + radicand_layout.width + bar_overhang,
640
+            y - radicand_layout.ascent - gap,
641
+        );
358642
         self.ctx.stroke().unwrap();
359643
         self.ctx.restore().unwrap();
360644
 
361645
         // Draw radicand
362646
         let rad_cursor = cursor_path.and_then(|p| {
363647
             let idx = if index.is_some() { 1 } else { 0 };
364
-            if !p.is_empty() && p[0] == idx { Some(&p[1..]) } else { None }
648
+            if !p.is_empty() && p[0] == idx {
649
+                Some(&p[1..])
650
+            } else {
651
+                None
652
+            }
365653
         });
366
-        self.render_at_depth(radicand, radicand_x, y, depth, rad_cursor);
654
+        self.render_at_depth(radicand, radicand_x, y, depth, rad_cursor, cursor_offset);
367655
     }
368656
 
369657
     /// Render a function call
@@ -375,7 +663,13 @@ impl<'a> MathRenderer<'a> {
375663
         y: f64,
376664
         depth: u32,
377665
         cursor_path: Option<&[usize]>,
666
+        cursor_offset: usize,
378667
     ) {
668
+        if name == "factorial" && args.len() == 1 {
669
+            self.render_factorial(&args[0], x, y, depth, cursor_path, cursor_offset);
670
+            return;
671
+        }
672
+
379673
         let scale = self.scale_for_depth(depth);
380674
         let font_size = self.layout_engine.base_font_size * scale;
381675
         let paren_width = font_size * 0.3;
@@ -399,9 +693,13 @@ impl<'a> MathRenderer<'a> {
399693
             }
400694
 
401695
             let arg_cursor = cursor_path.and_then(|p| {
402
-                if !p.is_empty() && p[0] == i { Some(&p[1..]) } else { None }
696
+                if !p.is_empty() && p[0] == i {
697
+                    Some(&p[1..])
698
+                } else {
699
+                    None
700
+                }
403701
             });
404
-            self.render_at_depth(arg, current_x, y, depth, arg_cursor);
702
+            self.render_at_depth(arg, current_x, y, depth, arg_cursor, cursor_offset);
405703
 
406704
             let arg_layout = self.layout_engine.layout(arg, self.ctx);
407705
             current_x += arg_layout.width;
@@ -411,6 +709,32 @@ impl<'a> MathRenderer<'a> {
411709
         self.draw_text(")", current_x, y, font_size, false);
412710
     }
413711
 
712
+    fn render_factorial(
713
+        &self,
714
+        arg: &MathBox,
715
+        x: f64,
716
+        y: f64,
717
+        depth: u32,
718
+        cursor_path: Option<&[usize]>,
719
+        cursor_offset: usize,
720
+    ) {
721
+        let scale = self.scale_for_depth(depth);
722
+        let font_size = self.layout_engine.base_font_size * scale;
723
+        let gap = (font_size * 0.06).max(0.6);
724
+
725
+        let arg_cursor = cursor_path.and_then(|p| {
726
+            if !p.is_empty() && p[0] == 0 {
727
+                Some(&p[1..])
728
+            } else {
729
+                None
730
+            }
731
+        });
732
+        self.render_at_depth(arg, x, y, depth, arg_cursor, cursor_offset);
733
+
734
+        let arg_layout = self.layout_engine.layout_with_depth(arg, self.ctx, depth);
735
+        self.draw_text("!", x + arg_layout.width + gap, y, font_size, false);
736
+    }
737
+
414738
     /// Render absolute value
415739
     fn render_abs(
416740
         &self,
@@ -419,6 +743,7 @@ impl<'a> MathRenderer<'a> {
419743
         y: f64,
420744
         depth: u32,
421745
         cursor_path: Option<&[usize]>,
746
+        cursor_offset: usize,
422747
     ) {
423748
         let inner_layout = self.layout_engine.layout(inner, self.ctx);
424749
         let scale = self.scale_for_depth(depth);
@@ -432,8 +757,10 @@ impl<'a> MathRenderer<'a> {
432757
         self.ctx.set_line_width(1.5 * scale);
433758
 
434759
         // Left bar
435
-        self.ctx.move_to(x + bar_width / 2.0, y - inner_layout.ascent);
436
-        self.ctx.line_to(x + bar_width / 2.0, y + inner_layout.descent);
760
+        self.ctx
761
+            .move_to(x + bar_width / 2.0, y - inner_layout.ascent);
762
+        self.ctx
763
+            .line_to(x + bar_width / 2.0, y + inner_layout.descent);
437764
         self.ctx.stroke().unwrap();
438765
 
439766
         // Right bar
@@ -446,9 +773,13 @@ impl<'a> MathRenderer<'a> {
446773
 
447774
         // Draw inner expression
448775
         let inner_cursor = cursor_path.and_then(|p| {
449
-            if !p.is_empty() && p[0] == 0 { Some(&p[1..]) } else { None }
776
+            if !p.is_empty() && p[0] == 0 {
777
+                Some(&p[1..])
778
+            } else {
779
+                None
780
+            }
450781
         });
451
-        self.render_at_depth(inner, x + bar_width, y, depth, inner_cursor);
782
+        self.render_at_depth(inner, x + bar_width, y, depth, inner_cursor, cursor_offset);
452783
     }
453784
 
454785
     /// Render parenthesized expression
@@ -459,6 +790,7 @@ impl<'a> MathRenderer<'a> {
459790
         y: f64,
460791
         depth: u32,
461792
         cursor_path: Option<&[usize]>,
793
+        cursor_offset: usize,
462794
     ) {
463795
         let inner_layout = self.layout_engine.layout(inner, self.ctx);
464796
         let scale = self.scale_for_depth(depth);
@@ -467,12 +799,29 @@ impl<'a> MathRenderer<'a> {
467799
 
468800
         // For now, draw text parentheses (could be replaced with curved paths)
469801
         self.draw_text("(", x, y, font_size * 1.2, false);
470
-        self.draw_text(")", x + paren_width + inner_layout.width, y, font_size * 1.2, false);
802
+        self.draw_text(
803
+            ")",
804
+            x + paren_width + inner_layout.width,
805
+            y,
806
+            font_size * 1.2,
807
+            false,
808
+        );
471809
 
472810
         let inner_cursor = cursor_path.and_then(|p| {
473
-            if !p.is_empty() && p[0] == 0 { Some(&p[1..]) } else { None }
811
+            if !p.is_empty() && p[0] == 0 {
812
+                Some(&p[1..])
813
+            } else {
814
+                None
815
+            }
474816
         });
475
-        self.render_at_depth(inner, x + paren_width, y, depth, inner_cursor);
817
+        self.render_at_depth(
818
+            inner,
819
+            x + paren_width,
820
+            y,
821
+            depth,
822
+            inner_cursor,
823
+            cursor_offset,
824
+        );
476825
     }
477826
 
478827
     /// Render an integral
@@ -486,76 +835,103 @@ impl<'a> MathRenderer<'a> {
486835
         y: f64,
487836
         depth: u32,
488837
         cursor_path: Option<&[usize]>,
838
+        cursor_offset: usize,
489839
     ) {
490
-        let body_layout = self.layout_engine.layout(body, self.ctx);
491840
         let scale = self.scale_for_depth(depth);
492841
         let font_size = self.layout_engine.base_font_size * scale;
842
+        let body_layout = self.layout_engine.layout_with_depth(body, self.ctx, depth);
843
+
844
+        let symbol_size = body_layout.height().max(font_size * 1.8);
845
+        self.ctx.set_font_size(symbol_size);
846
+        let int_extents = self.ctx.text_extents("∫").unwrap();
847
+        let int_font_extents = self.ctx.font_extents().unwrap();
848
+        let symbol_width = int_extents.x_advance();
849
+        let symbol_ascent = int_font_extents.ascent();
850
+        let symbol_descent = int_font_extents.descent();
851
+
852
+        let lo_layout = lower.map(|lo| {
853
+            self.layout_engine
854
+                .layout_with_depth(lo, self.ctx, depth + 1)
855
+        });
856
+        let hi_layout = upper.map(|hi| {
857
+            self.layout_engine
858
+                .layout_with_depth(hi, self.ctx, depth + 1)
859
+        });
493860
 
494
-        let int_height = body_layout.height().max(font_size * 1.5);
495
-        let int_width = font_size * 0.5;
861
+        let bound_gap = font_size * 0.14;
862
+        let bounds_width = symbol_width
863
+            .max(lo_layout.as_ref().map(|l| l.width).unwrap_or(0.0))
864
+            .max(hi_layout.as_ref().map(|l| l.width).unwrap_or(0.0));
865
+        let body_gap = font_size * 0.22;
866
+        let dx_gap = font_size * 0.14;
496867
 
497
-        // Draw integral symbol using a large font size
868
+        // Draw integral symbol centered in bounds column
498869
         self.ctx.save().unwrap();
499870
         self.set_color(&self.fg_color);
500
-        self.ctx.set_font_size(int_height);
501
-        self.ctx.move_to(x, y + int_height * 0.3);
871
+        self.ctx.set_font_size(symbol_size);
872
+        let symbol_x = x + (bounds_width - symbol_width) / 2.0;
873
+        let symbol_baseline = y + (symbol_ascent - symbol_descent) * 0.5;
874
+        self.ctx.move_to(symbol_x, symbol_baseline);
502875
         self.ctx.show_text("∫").unwrap();
503876
         self.ctx.restore().unwrap();
504877
 
505
-        let mut bounds_width = int_width;
506878
         let mut child_idx = 0;
507879
 
508880
         // Draw lower bound
509
-        if let Some(lo) = lower {
510
-            let lo_layout = self.layout_engine.layout(lo, self.ctx);
881
+        if let (Some(lo), Some(lo_layout)) = (lower, lo_layout.as_ref()) {
511882
             let lo_cursor = cursor_path.and_then(|p| {
512
-                if !p.is_empty() && p[0] == child_idx { Some(&p[1..]) } else { None }
883
+                if !p.is_empty() && p[0] == child_idx {
884
+                    Some(&p[1..])
885
+                } else {
886
+                    None
887
+                }
513888
             });
514
-            self.render_at_depth(
515
-                lo,
516
-                x,
517
-                y + int_height / 2.0 + lo_layout.ascent,
518
-                depth + 1,
519
-                lo_cursor,
520
-            );
521
-            bounds_width = bounds_width.max(lo_layout.width);
889
+            let lo_x = x + (bounds_width - lo_layout.width) / 2.0;
890
+            let lo_baseline = y + symbol_descent + bound_gap + lo_layout.ascent;
891
+            self.render_at_depth(lo, lo_x, lo_baseline, depth + 1, lo_cursor, cursor_offset);
522892
             child_idx += 1;
523893
         }
524894
 
525895
         // Draw upper bound
526
-        if let Some(hi) = upper {
527
-            let hi_layout = self.layout_engine.layout(hi, self.ctx);
896
+        if let (Some(hi), Some(hi_layout)) = (upper, hi_layout.as_ref()) {
528897
             let hi_cursor = cursor_path.and_then(|p| {
529
-                if !p.is_empty() && p[0] == child_idx { Some(&p[1..]) } else { None }
898
+                if !p.is_empty() && p[0] == child_idx {
899
+                    Some(&p[1..])
900
+                } else {
901
+                    None
902
+                }
530903
             });
531
-            self.render_at_depth(
532
-                hi,
533
-                x,
534
-                y - int_height / 2.0 - hi_layout.descent,
535
-                depth + 1,
536
-                hi_cursor,
537
-            );
538
-            bounds_width = bounds_width.max(hi_layout.width);
904
+            let hi_x = x + (bounds_width - hi_layout.width) / 2.0;
905
+            let hi_baseline = y - symbol_ascent - bound_gap - hi_layout.descent;
906
+            self.render_at_depth(hi, hi_x, hi_baseline, depth + 1, hi_cursor, cursor_offset);
539907
             child_idx += 1;
540908
         }
541909
 
542910
         // Draw body
543
-        let body_x = x + bounds_width + font_size * 0.2;
911
+        let body_x = x + bounds_width + body_gap;
544912
         let body_cursor = cursor_path.and_then(|p| {
545
-            if !p.is_empty() && p[0] == child_idx { Some(&p[1..]) } else { None }
913
+            if !p.is_empty() && p[0] == child_idx {
914
+                Some(&p[1..])
915
+            } else {
916
+                None
917
+            }
546918
         });
547
-        self.render_at_depth(body, body_x, y, depth, body_cursor);
919
+        self.render_at_depth(body, body_x, y, depth, body_cursor, cursor_offset);
548920
         child_idx += 1;
549921
 
550922
         // Draw "dx"
551
-        let var_x = body_x + body_layout.width + font_size * 0.1;
552
-        self.draw_text("d", var_x, y, font_size, false);
923
+        let d_width = self.text_advance("d", font_size, false);
924
+        let d_x = body_x + body_layout.width + dx_gap;
925
+        self.draw_text("d", d_x, y, font_size, false);
553926
 
554
-        let d_extents = self.ctx.text_extents("d").unwrap();
555927
         let var_cursor = cursor_path.and_then(|p| {
556
-            if !p.is_empty() && p[0] == child_idx { Some(&p[1..]) } else { None }
928
+            if !p.is_empty() && p[0] == child_idx {
929
+                Some(&p[1..])
930
+            } else {
931
+                None
932
+            }
557933
         });
558
-        self.render_at_depth(var, var_x + d_extents.x_advance(), y, depth, var_cursor);
934
+        self.render_at_depth(var, d_x + d_width, y, depth, var_cursor, cursor_offset);
559935
     }
560936
 
561937
     /// Render a derivative
@@ -568,9 +944,11 @@ impl<'a> MathRenderer<'a> {
568944
         y: f64,
569945
         depth: u32,
570946
         cursor_path: Option<&[usize]>,
947
+        cursor_offset: usize,
571948
     ) {
572949
         let scale = self.scale_for_depth(depth);
573950
         let font_size = self.layout_engine.base_font_size * scale;
951
+        let frac_font_size = font_size * 0.8;
574952
 
575953
         // Build strings for numerator and denominator
576954
         let num_str = if order > 1 {
@@ -585,19 +963,31 @@ impl<'a> MathRenderer<'a> {
585963
             "d".to_string()
586964
         };
587965
 
588
-        // Measure text
589
-        self.ctx.set_font_size(font_size * 0.8);
590
-        let num_extents = self.ctx.text_extents(&num_str).unwrap();
591
-        let den_prefix_extents = self.ctx.text_extents(&den_prefix).unwrap();
592
-
593
-        let var_layout = self.layout_engine.layout(var, self.ctx);
594
-        let _body_layout = self.layout_engine.layout(body, self.ctx);
595
-
596
-        let frac_width = num_extents.x_advance().max(den_prefix_extents.x_advance() + var_layout.width);
597
-        let bar_gap = font_size * 0.15;
966
+        // Measure text and guard spacing
967
+        let num_width = self.text_advance(&num_str, frac_font_size, false);
968
+        let den_prefix_width = self.text_advance(&den_prefix, frac_font_size, false);
969
+        self.ctx.set_font_size(frac_font_size);
970
+        let frac_font_extents = self.ctx.font_extents().unwrap();
971
+        let frac_ascent = frac_font_extents.ascent();
972
+        let frac_descent = frac_font_extents.descent();
973
+
974
+        let var_layout = self
975
+            .layout_engine
976
+            .layout_with_depth(var, self.ctx, depth + 1);
977
+        let den_sep = (frac_font_size * 0.08).max(0.6);
978
+        let denom_width = den_prefix_width + den_sep + var_layout.width;
979
+        let frac_width = num_width.max(denom_width) + font_size * 0.18;
980
+        let bar_gap = font_size * 0.14;
981
+        let body_gap = font_size * 0.3;
598982
 
599983
         // Draw numerator
600
-        self.draw_text(&num_str, x + (frac_width - num_extents.x_advance()) / 2.0, y - bar_gap - font_size * 0.3, font_size * 0.8, false);
984
+        self.draw_text(
985
+            &num_str,
986
+            x + (frac_width - num_width) / 2.0,
987
+            y - bar_gap - frac_descent,
988
+            frac_font_size,
989
+            false,
990
+        );
601991
 
602992
         // Draw fraction bar
603993
         self.ctx.save().unwrap();
@@ -609,19 +999,42 @@ impl<'a> MathRenderer<'a> {
609999
         self.ctx.restore().unwrap();
6101000
 
6111001
         // Draw denominator
612
-        let den_x = x + (frac_width - den_prefix_extents.x_advance() - var_layout.width) / 2.0;
613
-        self.draw_text(&den_prefix, den_x, y + bar_gap + font_size * 0.6, font_size * 0.8, false);
1002
+        let den_x = x + (frac_width - denom_width) / 2.0;
1003
+        let den_baseline = y + bar_gap + frac_ascent.max(var_layout.ascent);
1004
+        self.draw_text(&den_prefix, den_x, den_baseline, frac_font_size, false);
6141005
 
6151006
         let var_cursor = cursor_path.and_then(|p| {
616
-            if !p.is_empty() && p[0] == 0 { Some(&p[1..]) } else { None }
1007
+            if !p.is_empty() && p[0] == 0 {
1008
+                Some(&p[1..])
1009
+            } else {
1010
+                None
1011
+            }
6171012
         });
618
-        self.render_at_depth(var, den_x + den_prefix_extents.x_advance(), y + bar_gap + font_size * 0.6, depth + 1, var_cursor);
1013
+        self.render_at_depth(
1014
+            var,
1015
+            den_x + den_prefix_width + den_sep,
1016
+            den_baseline,
1017
+            depth + 1,
1018
+            var_cursor,
1019
+            cursor_offset,
1020
+        );
6191021
 
6201022
         // Draw body
6211023
         let body_cursor = cursor_path.and_then(|p| {
622
-            if !p.is_empty() && p[0] == 1 { Some(&p[1..]) } else { None }
1024
+            if !p.is_empty() && p[0] == 1 {
1025
+                Some(&p[1..])
1026
+            } else {
1027
+                None
1028
+            }
6231029
         });
624
-        self.render_at_depth(body, x + frac_width + font_size * 0.3, y, depth, body_cursor);
1030
+        self.render_at_depth(
1031
+            body,
1032
+            x + frac_width + body_gap,
1033
+            y,
1034
+            depth,
1035
+            body_cursor,
1036
+            cursor_offset,
1037
+        );
6251038
     }
6261039
 
6271040
     /// Render a limit
@@ -635,37 +1048,71 @@ impl<'a> MathRenderer<'a> {
6351048
         y: f64,
6361049
         depth: u32,
6371050
         cursor_path: Option<&[usize]>,
1051
+        cursor_offset: usize,
6381052
     ) {
6391053
         let scale = self.scale_for_depth(depth);
6401054
         let font_size = self.layout_engine.base_font_size * scale;
1055
+        let sub_font_size = font_size * 0.7;
1056
+        let body_gap = font_size * 0.3;
1057
+
1058
+        let lim_width = self.text_advance("lim", font_size, false);
1059
+        let var_layout = self
1060
+            .layout_engine
1061
+            .layout_with_depth(var, self.ctx, depth + 1);
1062
+        let to_layout = self
1063
+            .layout_engine
1064
+            .layout_with_depth(to, self.ctx, depth + 1);
1065
+        let arrow_width = self.text_advance("→", sub_font_size, false);
1066
+        let dir_width = if direction.is_some() {
1067
+            self.text_advance("⁺", sub_font_size * 0.6, false)
1068
+        } else {
1069
+            0.0
1070
+        };
1071
+        let sub_sep = (sub_font_size * 0.08).max(0.5);
1072
+        let subscript_width =
1073
+            var_layout.width + sub_sep + arrow_width + sub_sep + to_layout.width + dir_width;
1074
+        let lim_col_width = lim_width.max(subscript_width);
1075
+        let lim_x = x + (lim_col_width - lim_width) / 2.0;
1076
+        let sub_start_x = x + (lim_col_width - subscript_width) / 2.0;
1077
+        let subscript_y = y + font_size * 0.2 + var_layout.ascent.max(to_layout.ascent);
6411078
 
6421079
         // Draw "lim"
643
-        self.draw_text("lim", x, y, font_size, false);
644
-        self.ctx.set_font_size(font_size);
645
-        let lim_extents = self.ctx.text_extents("lim").unwrap();
646
-
647
-        // Draw subscript: var → to
648
-        let subscript_y = y + font_size * 0.5;
649
-        let sub_font_size = font_size * 0.7;
1080
+        self.draw_text("lim", lim_x, y, font_size, false);
6501081
 
6511082
         let var_cursor = cursor_path.and_then(|p| {
652
-            if !p.is_empty() && p[0] == 0 { Some(&p[1..]) } else { None }
1083
+            if !p.is_empty() && p[0] == 0 {
1084
+                Some(&p[1..])
1085
+            } else {
1086
+                None
1087
+            }
6531088
         });
654
-        self.render_at_depth(var, x, subscript_y, depth + 1, var_cursor);
1089
+        self.render_at_depth(
1090
+            var,
1091
+            sub_start_x,
1092
+            subscript_y,
1093
+            depth + 1,
1094
+            var_cursor,
1095
+            cursor_offset,
1096
+        );
6551097
 
656
-        let var_layout = self.layout_engine.layout(var, self.ctx);
657
-        let arrow_x = x + var_layout.width;
1098
+        let arrow_x = sub_start_x + var_layout.width + sub_sep;
6581099
         self.draw_text("→", arrow_x, subscript_y, sub_font_size, false);
6591100
 
660
-        self.ctx.set_font_size(sub_font_size);
661
-        let arrow_extents = self.ctx.text_extents("→").unwrap();
662
-
6631101
         let to_cursor = cursor_path.and_then(|p| {
664
-            if !p.is_empty() && p[0] == 1 { Some(&p[1..]) } else { None }
1102
+            if !p.is_empty() && p[0] == 1 {
1103
+                Some(&p[1..])
1104
+            } else {
1105
+                None
1106
+            }
6651107
         });
666
-        self.render_at_depth(to, arrow_x + arrow_extents.x_advance(), subscript_y, depth + 1, to_cursor);
667
-
668
-        let to_layout = self.layout_engine.layout(to, self.ctx);
1108
+        self.render_at_depth(
1109
+            to,
1110
+            arrow_x + arrow_width + sub_sep,
1111
+            subscript_y,
1112
+            depth + 1,
1113
+            to_cursor,
1114
+            cursor_offset,
1115
+        );
6691116
 
6701117
         // Draw direction indicator if present
6711118
         if let Some(dir) = direction {
@@ -673,22 +1120,32 @@ impl<'a> MathRenderer<'a> {
6731120
                 LimitDirection::FromRight => "⁺",
6741121
                 LimitDirection::FromLeft => "⁻",
6751122
             };
676
-            self.draw_text(dir_str, arrow_x + arrow_extents.x_advance() + to_layout.width, subscript_y - sub_font_size * 0.3, sub_font_size * 0.6, false);
1123
+            self.draw_text(
1124
+                dir_str,
1125
+                arrow_x + arrow_width + sub_sep + to_layout.width,
1126
+                subscript_y - sub_font_size * 0.3,
1127
+                sub_font_size * 0.6,
1128
+                false,
1129
+            );
6771130
         }
6781131
 
6791132
         // Draw body
680
-        let body_x = x + lim_extents.x_advance() + font_size * 0.3;
1133
+        let body_x = x + lim_col_width + body_gap;
6811134
         let body_cursor = cursor_path.and_then(|p| {
682
-            if !p.is_empty() && p[0] == 2 { Some(&p[1..]) } else { None }
1135
+            if !p.is_empty() && p[0] == 2 {
1136
+                Some(&p[1..])
1137
+            } else {
1138
+                None
1139
+            }
6831140
         });
684
-        self.render_at_depth(body, body_x, y, depth, body_cursor);
1141
+        self.render_at_depth(body, body_x, y, depth, body_cursor, cursor_offset);
6851142
     }
6861143
 
6871144
     /// Render a big operator (sum, product)
6881145
     fn render_bigop(
6891146
         &self,
6901147
         symbol: &str,
691
-        _var: &MathBox,
1148
+        var: &MathBox,
6921149
         lower: &MathBox,
6931150
         upper: &MathBox,
6941151
         body: &MathBox,
@@ -696,45 +1153,117 @@ impl<'a> MathRenderer<'a> {
6961153
         y: f64,
6971154
         depth: u32,
6981155
         cursor_path: Option<&[usize]>,
1156
+        cursor_offset: usize,
6991157
     ) {
7001158
         let scale = self.scale_for_depth(depth);
7011159
         let font_size = self.layout_engine.base_font_size * scale;
7021160
         let symbol_size = font_size * 1.5;
703
-
704
-        // Draw the big symbol
1161
+        let bound_scale = self.scale_for_depth(depth + 1);
1162
+        let bound_font_size = self.layout_engine.base_font_size * bound_scale;
1163
+
1164
+        let var_layout = self
1165
+            .layout_engine
1166
+            .layout_with_depth(var, self.ctx, depth + 1);
1167
+        let lower_layout = self
1168
+            .layout_engine
1169
+            .layout_with_depth(lower, self.ctx, depth + 1);
1170
+        let upper_layout = self
1171
+            .layout_engine
1172
+            .layout_with_depth(upper, self.ctx, depth + 1);
1173
+        let eq_width = self.text_advance("=", bound_font_size, false);
1174
+        let lower_sep = (bound_font_size * 0.08).max(0.6);
1175
+        let lower_block_width =
1176
+            var_layout.width + lower_sep + eq_width + lower_sep + lower_layout.width;
1177
+
1178
+        // Draw the big symbol centered in the operator column.
7051179
         self.ctx.save().unwrap();
7061180
         self.set_color(&self.fg_color);
7071181
         self.ctx.set_font_size(symbol_size);
708
-        self.ctx.move_to(x, y + symbol_size * 0.3);
709
-        self.ctx.show_text(symbol).unwrap();
7101182
         let symbol_extents = self.ctx.text_extents(symbol).unwrap();
1183
+        let symbol_width = symbol_extents.x_advance();
1184
+        let symbol_font_extents = self.ctx.font_extents().unwrap();
1185
+        let symbol_ascent = symbol_font_extents.ascent();
1186
+        let symbol_descent = symbol_font_extents.descent();
1187
+        let op_width = symbol_width.max(lower_block_width).max(upper_layout.width);
1188
+        let symbol_x = x + (op_width - symbol_width) / 2.0;
1189
+        let symbol_baseline = y + (symbol_ascent - symbol_descent) * 0.5;
1190
+        self.ctx.move_to(symbol_x, symbol_baseline);
1191
+        self.ctx.show_text(symbol).unwrap();
7111192
         self.ctx.restore().unwrap();
7121193
 
713
-        let op_width = symbol_extents.x_advance();
714
-        let gap = font_size * 0.15;
1194
+        let bounds_gap = font_size * 0.16;
1195
+        let body_gap = font_size * 0.32;
1196
+        let symbol_top = symbol_baseline - symbol_ascent;
1197
+        let symbol_bottom = symbol_baseline + symbol_descent;
1198
+        let upper_baseline = symbol_top - bounds_gap - upper_layout.descent;
1199
+        let lower_baseline =
1200
+            symbol_bottom + bounds_gap + lower_layout.ascent.max(var_layout.ascent);
1201
+        let upper_x = x + (op_width - upper_layout.width) / 2.0;
1202
+        let lower_start_x = x + (op_width - lower_block_width) / 2.0;
7151203
 
7161204
         // Draw upper bound
717
-        let upper_layout = self.layout_engine.layout(upper, self.ctx);
718
-        let upper_x = x + (op_width - upper_layout.width) / 2.0;
7191205
         let upper_cursor = cursor_path.and_then(|p| {
720
-            if !p.is_empty() && p[0] == 2 { Some(&p[1..]) } else { None }
1206
+            if !p.is_empty() && p[0] == 2 {
1207
+                Some(&p[1..])
1208
+            } else {
1209
+                None
1210
+            }
7211211
         });
722
-        self.render_at_depth(upper, upper_x, y - symbol_size / 2.0 - gap - upper_layout.descent, depth + 1, upper_cursor);
1212
+        self.render_at_depth(
1213
+            upper,
1214
+            upper_x,
1215
+            upper_baseline,
1216
+            depth + 1,
1217
+            upper_cursor,
1218
+            cursor_offset,
1219
+        );
1220
+
1221
+        // Draw lower bound as "var = lower"
1222
+        let var_cursor = cursor_path.and_then(|p| {
1223
+            if !p.is_empty() && p[0] == 0 {
1224
+                Some(&p[1..])
1225
+            } else {
1226
+                None
1227
+            }
1228
+        });
1229
+        self.render_at_depth(
1230
+            var,
1231
+            lower_start_x,
1232
+            lower_baseline,
1233
+            depth + 1,
1234
+            var_cursor,
1235
+            cursor_offset,
1236
+        );
1237
+
1238
+        let eq_x = lower_start_x + var_layout.width + lower_sep;
1239
+        self.draw_text("=", eq_x, lower_baseline, bound_font_size, false);
7231240
 
724
-        // Draw lower bound (includes "var=")
725
-        let lower_layout = self.layout_engine.layout(lower, self.ctx);
726
-        let lower_x = x + (op_width - lower_layout.width) / 2.0;
7271241
         let lower_cursor = cursor_path.and_then(|p| {
728
-            if !p.is_empty() && p[0] == 1 { Some(&p[1..]) } else { None }
1242
+            if !p.is_empty() && p[0] == 1 {
1243
+                Some(&p[1..])
1244
+            } else {
1245
+                None
1246
+            }
7291247
         });
730
-        self.render_at_depth(lower, lower_x, y + symbol_size / 2.0 + gap + lower_layout.ascent, depth + 1, lower_cursor);
1248
+        self.render_at_depth(
1249
+            lower,
1250
+            eq_x + eq_width + lower_sep,
1251
+            lower_baseline,
1252
+            depth + 1,
1253
+            lower_cursor,
1254
+            cursor_offset,
1255
+        );
7311256
 
7321257
         // Draw body
733
-        let body_x = x + op_width + font_size * 0.3;
1258
+        let body_x = x + op_width + body_gap;
7341259
         let body_cursor = cursor_path.and_then(|p| {
735
-            if !p.is_empty() && p[0] == 3 { Some(&p[1..]) } else { None }
1260
+            if !p.is_empty() && p[0] == 3 {
1261
+                Some(&p[1..])
1262
+            } else {
1263
+                None
1264
+            }
7361265
         });
737
-        self.render_at_depth(body, body_x, y, depth, body_cursor);
1266
+        self.render_at_depth(body, body_x, y, depth, body_cursor, cursor_offset);
7381267
     }
7391268
 
7401269
     /// Render a matrix
@@ -745,6 +1274,7 @@ impl<'a> MathRenderer<'a> {
7451274
         y: f64,
7461275
         depth: u32,
7471276
         cursor_path: Option<&[usize]>,
1277
+        cursor_offset: usize,
7481278
     ) {
7491279
         if rows.is_empty() {
7501280
             return;
@@ -784,8 +1314,8 @@ impl<'a> MathRenderer<'a> {
7841314
         let total_width: f64 = col_widths.iter().sum::<f64>()
7851315
             + cell_padding * (num_cols as f64 - 1.0)
7861316
             + 2.0 * bracket_width;
787
-        let total_height: f64 = row_heights.iter().sum::<f64>()
788
-            + cell_padding * (rows.len() as f64 - 1.0);
1317
+        let total_height: f64 =
1318
+            row_heights.iter().sum::<f64>() + cell_padding * (rows.len() as f64 - 1.0);
7891319
 
7901320
         // Draw brackets
7911321
         self.ctx.save().unwrap();
@@ -802,10 +1332,12 @@ impl<'a> MathRenderer<'a> {
8021332
 
8031333
         // Right bracket
8041334
         let right_x = x + total_width - bracket_width;
805
-        self.ctx.move_to(right_x - bracket_gap, y - total_height / 2.0);
1335
+        self.ctx
1336
+            .move_to(right_x - bracket_gap, y - total_height / 2.0);
8061337
         self.ctx.line_to(right_x, y - total_height / 2.0);
8071338
         self.ctx.line_to(right_x, y + total_height / 2.0);
808
-        self.ctx.line_to(right_x - bracket_gap, y + total_height / 2.0);
1339
+        self.ctx
1340
+            .line_to(right_x - bracket_gap, y + total_height / 2.0);
8091341
         self.ctx.stroke().unwrap();
8101342
 
8111343
         self.ctx.restore().unwrap();
@@ -827,9 +1359,13 @@ impl<'a> MathRenderer<'a> {
8271359
                 let cy = cell_y + row_h / 2.0;
8281360
 
8291361
                 let cell_cursor = cursor_path.and_then(|p| {
830
-                    if !p.is_empty() && p[0] == cell_idx { Some(&p[1..]) } else { None }
1362
+                    if !p.is_empty() && p[0] == cell_idx {
1363
+                        Some(&p[1..])
1364
+                    } else {
1365
+                        None
1366
+                    }
8311367
                 });
832
-                self.render_at_depth(cell, cx, cy, depth, cell_cursor);
1368
+                self.render_at_depth(cell, cx, cy, depth, cell_cursor, cursor_offset);
8331369
 
8341370
                 cell_x += col_w + cell_padding;
8351371
                 cell_idx += 1;
@@ -847,14 +1383,19 @@ impl<'a> MathRenderer<'a> {
8471383
         y: f64,
8481384
         depth: u32,
8491385
         cursor_path: Option<&[usize]>,
1386
+        cursor_offset: usize,
8501387
     ) {
8511388
         let mut current_x = x;
8521389
 
8531390
         for (i, item) in items.iter().enumerate() {
8541391
             let item_cursor = cursor_path.and_then(|p| {
855
-                if !p.is_empty() && p[0] == i { Some(&p[1..]) } else { None }
1392
+                if !p.is_empty() && p[0] == i {
1393
+                    Some(&p[1..])
1394
+                } else {
1395
+                    None
1396
+                }
8561397
             });
857
-            self.render_at_depth(item, current_x, y, depth, item_cursor);
1398
+            self.render_at_depth(item, current_x, y, depth, item_cursor, cursor_offset);
8581399
 
8591400
             let layout = self.layout_engine.layout(item, self.ctx);
8601401
             current_x += layout.width;
@@ -865,6 +1406,13 @@ impl<'a> MathRenderer<'a> {
8651406
     fn set_color(&self, color: &Color) {
8661407
         self.ctx.set_source_rgba(color.r, color.g, color.b, color.a);
8671408
     }
1409
+
1410
+    fn slot_geometry(font_size: f64) -> (f64, f64, f64) {
1411
+        let width = (font_size * 0.86).max(10.0);
1412
+        let height = (font_size * 0.9).max(11.0);
1413
+        let ascent = height * 0.62;
1414
+        (width, height, ascent)
1415
+    }
8681416
 }
8691417
 
8701418
 /// Convert a number to superscript Unicode digits