Rust · 79171 bytes Raw Blame History
1 //! Keyboard input handling for math input
2 //!
3 //! Provides structured keyboard navigation through a MathBox tree
4 //! with support for template insertion via commands.
5
6 use crate::mathbox::{Cursor, MathBox, Operator};
7
8 /// Result of processing a key event
9 #[derive(Debug, Clone, PartialEq)]
10 pub enum InputResult {
11 /// Input was consumed, UI should redraw
12 Consumed,
13 /// Input was not handled
14 Ignored,
15 /// User pressed Enter to evaluate
16 Evaluate,
17 /// User requested to close/cancel
18 Cancel,
19 }
20
21 /// Math input state with cursor
22 pub struct MathInput {
23 /// Root of the expression tree
24 pub root: MathBox,
25 /// Current cursor position
26 pub cursor: Cursor,
27 /// Command mode buffer (active when typing \ commands)
28 pub command_buffer: Option<String>,
29 }
30
31 impl Default for MathInput {
32 fn default() -> Self {
33 Self::new()
34 }
35 }
36
37 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
64 /// Create a new empty input
65 pub fn new() -> Self {
66 let mut cursor = Cursor::new();
67 cursor.enter(0); // Start inside the first slot
68 Self {
69 root: MathBox::Row(vec![MathBox::Slot]),
70 cursor,
71 command_buffer: None,
72 }
73 }
74
75 /// Create from an existing MathBox
76 pub fn from_mathbox(mathbox: MathBox) -> Self {
77 Self {
78 root: mathbox,
79 cursor: Cursor::new(),
80 command_buffer: None,
81 }
82 }
83
84 /// Clear the input
85 pub fn clear(&mut self) {
86 self.root = MathBox::Row(vec![MathBox::Slot]);
87 let mut cursor = Cursor::new();
88 cursor.enter(0);
89 self.cursor = cursor;
90 self.command_buffer = None;
91 }
92
93 /// Handle a character input
94 pub fn handle_char(&mut self, ch: char) -> InputResult {
95 // Handle command mode
96 if let Some(ref mut buf) = self.command_buffer {
97 match ch {
98 ' ' | '\n' => {
99 // Execute command
100 let cmd = buf.clone();
101 self.command_buffer = None;
102 return self.execute_command(&cmd);
103 }
104 c if c.is_alphanumeric() => {
105 buf.push(c);
106 return InputResult::Consumed;
107 }
108 _ => {
109 // Cancel command mode
110 self.command_buffer = None;
111 return InputResult::Consumed;
112 }
113 }
114 }
115
116 // Normal input
117 match ch {
118 '\\' | ':' => {
119 // Enter command mode
120 self.command_buffer = Some(String::new());
121 InputResult::Consumed
122 }
123 '/' => {
124 self.maybe_promote_out_of_script();
125 // Insert fraction
126 self.insert_template(MathBox::fraction_template());
127 InputResult::Consumed
128 }
129 '^' => {
130 // Convert current element to power base
131 self.wrap_in_power();
132 InputResult::Consumed
133 }
134 '!' => {
135 // Apply factorial to current element
136 self.wrap_in_factorial();
137 InputResult::Consumed
138 }
139 '_' => {
140 // Convert current element to subscript base
141 self.wrap_in_subscript();
142 InputResult::Consumed
143 }
144 '(' => {
145 if !self.wrap_current_symbol_as_function_call() {
146 self.insert_at_cursor(MathBox::Parens(Box::new(MathBox::Slot)));
147 self.cursor.enter(0);
148 }
149 InputResult::Consumed
150 }
151 ')' => {
152 // Try to exit parens
153 self.try_exit_container();
154 InputResult::Consumed
155 }
156 '|' => {
157 self.insert_at_cursor(MathBox::Abs(Box::new(MathBox::Slot)));
158 self.cursor.enter(0);
159 InputResult::Consumed
160 }
161 '+' => {
162 self.maybe_promote_out_of_script();
163 self.insert_at_cursor(MathBox::Operator(Operator::Add));
164 InputResult::Consumed
165 }
166 '-' => {
167 self.maybe_promote_out_of_script();
168 self.insert_at_cursor(MathBox::Operator(Operator::Sub));
169 InputResult::Consumed
170 }
171 '*' => {
172 self.maybe_promote_out_of_script();
173 self.insert_at_cursor(MathBox::Operator(Operator::Mul));
174 InputResult::Consumed
175 }
176 '=' => {
177 self.maybe_promote_out_of_script();
178 self.insert_at_cursor(MathBox::Operator(Operator::Eq));
179 InputResult::Consumed
180 }
181 '<' => {
182 self.maybe_promote_out_of_script();
183 self.insert_at_cursor(MathBox::Operator(Operator::Lt));
184 InputResult::Consumed
185 }
186 '>' => {
187 self.maybe_promote_out_of_script();
188 self.insert_at_cursor(MathBox::Operator(Operator::Gt));
189 InputResult::Consumed
190 }
191 ',' => {
192 self.maybe_promote_out_of_script();
193 self.insert_at_cursor(MathBox::Operator(Operator::Comma));
194 InputResult::Consumed
195 }
196 c if c.is_ascii_digit() || c == '.' => {
197 self.append_to_number(c);
198 InputResult::Consumed
199 }
200 c if c.is_alphabetic() => {
201 self.append_to_symbol(c);
202 InputResult::Consumed
203 }
204 _ => InputResult::Ignored,
205 }
206 }
207
208 /// Handle a special key
209 pub fn handle_key(&mut self, key: SpecialKey) -> InputResult {
210 if self.command_buffer.is_some() {
211 match key {
212 SpecialKey::Enter => {
213 if let Some(cmd) = self.command_buffer.take() {
214 if cmd.is_empty() {
215 return InputResult::Consumed;
216 }
217 return self.execute_command(&cmd);
218 }
219 return InputResult::Ignored;
220 }
221 SpecialKey::Escape => {
222 self.command_buffer = None;
223 return InputResult::Consumed;
224 }
225 SpecialKey::Backspace => {
226 if let Some(ref mut buf) = self.command_buffer {
227 buf.pop();
228 if buf.is_empty() {
229 self.command_buffer = None;
230 }
231 }
232 return InputResult::Consumed;
233 }
234 _ => {
235 // For navigation/editing keys, leave command mode and continue handling the key.
236 self.command_buffer = None;
237 }
238 }
239 }
240
241 match key {
242 SpecialKey::Left => {
243 self.move_left();
244 InputResult::Consumed
245 }
246 SpecialKey::Right => {
247 self.move_right();
248 InputResult::Consumed
249 }
250 SpecialKey::Up => {
251 self.move_up();
252 InputResult::Consumed
253 }
254 SpecialKey::Down => {
255 self.move_down();
256 InputResult::Consumed
257 }
258 SpecialKey::Tab => {
259 self.move_to_next_slot();
260 InputResult::Consumed
261 }
262 SpecialKey::ShiftTab => {
263 self.move_to_prev_slot();
264 InputResult::Consumed
265 }
266 SpecialKey::Enter => InputResult::Evaluate,
267 SpecialKey::Escape => InputResult::Cancel,
268 SpecialKey::Backspace => {
269 self.delete_at_cursor();
270 InputResult::Consumed
271 }
272 SpecialKey::Delete => {
273 self.delete_forward();
274 InputResult::Consumed
275 }
276 SpecialKey::Home => {
277 self.move_to_start();
278 InputResult::Consumed
279 }
280 SpecialKey::End => {
281 self.move_to_end();
282 InputResult::Consumed
283 }
284 }
285 }
286
287 /// Execute a command (entered via \ prefix)
288 fn execute_command(&mut self, cmd: &str) -> InputResult {
289 let template = match cmd.to_lowercase().as_str() {
290 "sqrt" => Some(MathBox::sqrt_template()),
291 "nthroot" | "root" => Some(MathBox::nthroot_template()),
292 "frac" => Some(MathBox::fraction_template()),
293 "int" => Some(MathBox::integral_template()),
294 "dint" | "defint" => Some(MathBox::definite_integral_template()),
295 "ddx" | "diff" | "deriv" => Some(MathBox::derivative_template()),
296 "lim" | "limit" => Some(MathBox::limit_template()),
297 "sum" => Some(MathBox::sum_template()),
298 "prod" | "product" => Some(MathBox::product_template()),
299 "solve" => Some(MathBox::Func {
300 name: "solve".to_string(),
301 args: vec![MathBox::Slot, MathBox::Symbol("x".to_string())],
302 }),
303 "abs" => Some(MathBox::Abs(Box::new(MathBox::Slot))),
304 "pi" => Some(MathBox::Symbol("π".to_string())),
305 "theta" => Some(MathBox::Symbol("θ".to_string())),
306 "alpha" => Some(MathBox::Symbol("α".to_string())),
307 "beta" => Some(MathBox::Symbol("β".to_string())),
308 "gamma" => Some(MathBox::Symbol("γ".to_string())),
309 "delta" => Some(MathBox::Symbol("δ".to_string())),
310 "epsilon" => Some(MathBox::Symbol("ε".to_string())),
311 "lambda" => Some(MathBox::Symbol("λ".to_string())),
312 "mu" => Some(MathBox::Symbol("μ".to_string())),
313 "sigma" => Some(MathBox::Symbol("σ".to_string())),
314 "omega" => Some(MathBox::Symbol("ω".to_string())),
315 "inf" | "infinity" => Some(MathBox::Symbol("∞".to_string())),
316 "sin" | "cos" | "tan" | "ln" | "log" | "exp" => Some(MathBox::Func {
317 name: cmd.to_lowercase(),
318 args: vec![MathBox::Slot],
319 }),
320 "matrix" => Some(MathBox::matrix_template(2, 2)),
321 _ => None,
322 };
323
324 if let Some(t) = template {
325 self.insert_template(t);
326 InputResult::Consumed
327 } else {
328 InputResult::Ignored
329 }
330 }
331
332 /// Insert a template at cursor position
333 fn insert_template(&mut self, template: MathBox) {
334 // Replace current slot or insert at cursor
335 if let Some(current) = self.get_current_mut() {
336 if current.is_slot() {
337 *current = template;
338 // Move cursor into first child if it's a container
339 if 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 }
346 } else {
347 // Insert after current
348 if self.insert_after_current(template)
349 && self
350 .get_current()
351 .map(|c| c.child_count() > 0)
352 .unwrap_or(false)
353 {
354 self.focus_first_slot_in_current_subtree();
355 }
356 }
357 }
358 }
359
360 /// Wrap the current element in a power
361 fn wrap_in_power(&mut self) {
362 let current_path = self.cursor.path.clone();
363 let row_offset = self.cursor.offset;
364 let mut row_target = None;
365 {
366 if let Some(MathBox::Row(items)) =
367 Self::get_node_mut_at_path(&mut self.root, &current_path)
368 {
369 let idx = row_offset.min(items.len());
370 let prev_is_operator =
371 idx > 0 && matches!(items.get(idx - 1), Some(MathBox::Operator(_)));
372 if idx > 0 && !prev_is_operator {
373 let base = std::mem::replace(&mut items[idx - 1], MathBox::Slot);
374 items[idx - 1] = MathBox::Power {
375 base: Box::new(base),
376 exp: Box::new(MathBox::Slot),
377 };
378 row_target = Some(idx - 1);
379 } else {
380 items.insert(
381 idx,
382 MathBox::Power {
383 base: Box::new(MathBox::Slot),
384 exp: Box::new(MathBox::Slot),
385 },
386 );
387 row_target = Some(idx);
388 }
389 }
390 }
391 if let Some(item_idx) = row_target {
392 self.cursor.path = current_path;
393 self.cursor.enter(item_idx);
394 self.cursor.enter(1);
395 return;
396 }
397
398 if let Some(current) = self.get_current_mut() {
399 if !current.is_slot() {
400 let base = std::mem::replace(current, MathBox::Slot);
401 *current = MathBox::Power {
402 base: Box::new(base),
403 exp: Box::new(MathBox::Slot),
404 };
405 // Move to exponent slot
406 self.cursor.enter(1);
407 } else {
408 // Insert power with slot base
409 *current = MathBox::Power {
410 base: Box::new(MathBox::Slot),
411 exp: Box::new(MathBox::Slot),
412 };
413 self.cursor.enter(1);
414 }
415 }
416 }
417
418 /// Wrap the current element in a subscript
419 fn wrap_in_subscript(&mut self) {
420 let current_path = self.cursor.path.clone();
421 let row_offset = self.cursor.offset;
422 let mut row_target = None;
423 {
424 if let Some(MathBox::Row(items)) =
425 Self::get_node_mut_at_path(&mut self.root, &current_path)
426 {
427 let idx = row_offset.min(items.len());
428 let prev_is_operator =
429 idx > 0 && matches!(items.get(idx - 1), Some(MathBox::Operator(_)));
430 if idx > 0 && !prev_is_operator {
431 let base = std::mem::replace(&mut items[idx - 1], MathBox::Slot);
432 items[idx - 1] = MathBox::Subscript {
433 base: Box::new(base),
434 sub: Box::new(MathBox::Slot),
435 };
436 row_target = Some(idx - 1);
437 } else {
438 items.insert(
439 idx,
440 MathBox::Subscript {
441 base: Box::new(MathBox::Slot),
442 sub: Box::new(MathBox::Slot),
443 },
444 );
445 row_target = Some(idx);
446 }
447 }
448 }
449 if let Some(item_idx) = row_target {
450 self.cursor.path = current_path;
451 self.cursor.enter(item_idx);
452 self.cursor.enter(1);
453 return;
454 }
455
456 if let Some(current) = self.get_current_mut() {
457 if !current.is_slot() {
458 let base = std::mem::replace(current, MathBox::Slot);
459 *current = MathBox::Subscript {
460 base: Box::new(base),
461 sub: Box::new(MathBox::Slot),
462 };
463 self.cursor.enter(1);
464 } else {
465 *current = MathBox::Subscript {
466 base: Box::new(MathBox::Slot),
467 sub: Box::new(MathBox::Slot),
468 };
469 self.cursor.enter(1);
470 }
471 }
472 }
473
474 /// Wrap the current element in factorial
475 fn wrap_in_factorial(&mut self) {
476 let current_path = self.cursor.path.clone();
477 let row_offset = self.cursor.offset;
478 enum RowFactorialTarget {
479 Wrapped(usize),
480 Inserted(usize),
481 }
482 let mut row_target = None;
483 {
484 if let Some(MathBox::Row(items)) =
485 Self::get_node_mut_at_path(&mut self.root, &current_path)
486 {
487 let idx = row_offset.min(items.len());
488 let prev_is_operator =
489 idx > 0 && matches!(items.get(idx - 1), Some(MathBox::Operator(_)));
490 if idx > 0 && !prev_is_operator {
491 let arg = std::mem::replace(&mut items[idx - 1], MathBox::Slot);
492 items[idx - 1] = MathBox::Func {
493 name: "factorial".to_string(),
494 args: vec![arg],
495 };
496 row_target = Some(RowFactorialTarget::Wrapped(idx - 1));
497 } else {
498 items.insert(
499 idx,
500 MathBox::Func {
501 name: "factorial".to_string(),
502 args: vec![MathBox::Slot],
503 },
504 );
505 row_target = Some(RowFactorialTarget::Inserted(idx));
506 }
507 }
508 }
509 if let Some(target) = row_target {
510 self.cursor.path = current_path;
511 match target {
512 RowFactorialTarget::Wrapped(item_idx) => {
513 self.cursor.enter(item_idx);
514 self.cursor.offset = 0;
515 }
516 RowFactorialTarget::Inserted(item_idx) => {
517 self.cursor.enter(item_idx);
518 self.cursor.enter(0);
519 }
520 }
521 return;
522 }
523
524 if let Some(current) = self.get_current_mut() {
525 if !current.is_slot() {
526 let arg = std::mem::replace(current, MathBox::Slot);
527 *current = MathBox::Func {
528 name: "factorial".to_string(),
529 args: vec![arg],
530 };
531 self.cursor.offset = 0;
532 } else {
533 *current = MathBox::Func {
534 name: "factorial".to_string(),
535 args: vec![MathBox::Slot],
536 };
537 self.cursor.enter(0);
538 }
539 }
540 }
541
542 /// Append a digit to the current number
543 fn append_to_number(&mut self, ch: char) {
544 let offset = self.cursor.offset;
545 let mut new_offset = None;
546 if let Some(current) = self.get_current_mut() {
547 match current {
548 MathBox::Number(s) => {
549 let insert_at = Self::byte_index_at_char(s, offset);
550 s.insert(insert_at, ch);
551 new_offset = Some(offset + 1);
552 }
553 MathBox::Slot => {
554 *current = MathBox::Number(ch.to_string());
555 new_offset = Some(1);
556 }
557 _ => {
558 if self.insert_after_current(MathBox::Number(ch.to_string())) {
559 new_offset = Some(1);
560 }
561 }
562 }
563 }
564 if let Some(new_offset) = new_offset {
565 self.cursor.offset = new_offset;
566 }
567 }
568
569 /// Append a character to the current symbol
570 fn append_to_symbol(&mut self, ch: char) {
571 let offset = self.cursor.offset;
572 let mut new_offset = None;
573 if let Some(current) = self.get_current_mut() {
574 match current {
575 MathBox::Symbol(s) => {
576 let insert_at = Self::byte_index_at_char(s, offset);
577 s.insert(insert_at, ch);
578 new_offset = Some(offset + 1);
579 }
580 MathBox::Slot => {
581 *current = MathBox::Symbol(ch.to_string());
582 new_offset = Some(1);
583 }
584 _ => {
585 if self.insert_after_current(MathBox::Symbol(ch.to_string())) {
586 new_offset = Some(1);
587 }
588 }
589 }
590 }
591 if let Some(new_offset) = new_offset {
592 self.cursor.offset = new_offset;
593 }
594 }
595
596 /// Insert an element at the cursor position
597 fn insert_at_cursor(&mut self, element: MathBox) {
598 if let Some(current) = self.get_current_mut() {
599 if current.is_slot() {
600 *current = element;
601 } else {
602 self.insert_after_current(element);
603 }
604 }
605 }
606
607 /// Insert an element after the current one (in a Row)
608 fn insert_after_current(&mut self, element: MathBox) -> bool {
609 let current_path = self.cursor.path.clone();
610
611 // Case 1: cursor is on a Row node; insert at the row offset.
612 if let Some(MathBox::Row(items)) = Self::get_node_mut_at_path(&mut self.root, &current_path)
613 {
614 let insert_idx = self.cursor.offset.min(items.len());
615 items.insert(insert_idx, element);
616 self.cursor.path = current_path;
617 self.cursor.enter(insert_idx);
618 return true;
619 }
620
621 // Case 2: parent is a Row; insert as next sibling.
622 if let Some((&idx, parent_path)) = current_path.split_last() {
623 if let Some(MathBox::Row(items)) =
624 Self::get_node_mut_at_path(&mut self.root, parent_path)
625 {
626 let insert_idx = (idx + 1).min(items.len());
627 items.insert(insert_idx, element);
628 self.cursor.path = parent_path.to_vec();
629 self.cursor.enter(insert_idx);
630 return true;
631 }
632 }
633
634 // Case 3: no row context; wrap current node into a Row and append.
635 if let Some(current) = Self::get_node_mut_at_path(&mut self.root, &current_path) {
636 let old = std::mem::replace(current, MathBox::Slot);
637 *current = MathBox::Row(vec![old, element]);
638 self.cursor.path = current_path;
639 self.cursor.enter(1);
640 return true;
641 }
642
643 false
644 }
645
646 fn wrap_current_symbol_as_function_call(&mut self) -> bool {
647 let path = self.cursor.path.clone();
648 let symbol_name = match Self::get_node_at_path(&self.root, &path) {
649 Some(MathBox::Symbol(name)) => name.clone(),
650 _ => return false,
651 };
652
653 let normalized = symbol_name.to_ascii_lowercase();
654 if !Self::is_known_function_name(&normalized) {
655 return false;
656 }
657
658 if let Some(node) = Self::get_node_mut_at_path(&mut self.root, &path) {
659 *node = MathBox::Func {
660 name: normalized,
661 args: vec![MathBox::Slot],
662 };
663 self.cursor.enter(0);
664 self.cursor.offset = 0;
665 true
666 } else {
667 false
668 }
669 }
670
671 fn is_known_function_name(name: &str) -> bool {
672 matches!(
673 name,
674 "sin"
675 | "cos"
676 | "tan"
677 | "cot"
678 | "sec"
679 | "csc"
680 | "asin"
681 | "acos"
682 | "atan"
683 | "sinh"
684 | "cosh"
685 | "tanh"
686 | "asinh"
687 | "acosh"
688 | "atanh"
689 | "ln"
690 | "log"
691 | "log10"
692 | "log2"
693 | "exp"
694 | "sqrt"
695 | "cbrt"
696 | "abs"
697 | "floor"
698 | "ceil"
699 | "round"
700 | "trunc"
701 | "sign"
702 | "gamma"
703 | "factorial"
704 | "diff"
705 | "derivative"
706 | "integrate"
707 | "integral"
708 | "limit"
709 | "lim"
710 | "solve"
711 | "sum"
712 | "product"
713 | "prod"
714 | "simplify"
715 | "expand"
716 | "factor"
717 | "substitute"
718 | "subs"
719 | "min"
720 | "max"
721 | "gcd"
722 | "lcm"
723 | "det"
724 | "determinant"
725 | "inv"
726 | "inverse"
727 | "transpose"
728 | "trace"
729 | "matmul"
730 | "identity"
731 )
732 }
733
734 /// Try to exit current container (parens, etc.)
735 fn try_exit_container(&mut self) {
736 if !self.cursor.is_at_root() {
737 self.cursor.exit();
738 }
739 }
740
741 fn cursor_is_at_end_of_current(&self) -> bool {
742 match self.get_current() {
743 Some(MathBox::Number(s)) | Some(MathBox::Symbol(s)) => {
744 self.cursor.offset >= Self::char_count(s)
745 }
746 Some(MathBox::Row(items)) => self.cursor.offset >= items.len(),
747 Some(_) => true,
748 None => false,
749 }
750 }
751
752 /// Promote cursor out of exponent/subscript when typing operators at script end.
753 fn maybe_promote_out_of_script(&mut self) -> bool {
754 if !self.cursor_is_at_end_of_current() {
755 return false;
756 }
757
758 let Some((&child_idx, parent_path)) = self.cursor.path.split_last() else {
759 return false;
760 };
761 if child_idx != 1 {
762 return false;
763 }
764
765 let is_script = matches!(
766 Self::get_node_at_path(&self.root, parent_path),
767 Some(MathBox::Power { .. }) | Some(MathBox::Subscript { .. })
768 );
769 if !is_script {
770 return false;
771 }
772
773 self.cursor.path = parent_path.to_vec();
774 self.cursor.offset = 0;
775 true
776 }
777
778 /// Delete at cursor
779 fn delete_at_cursor(&mut self) {
780 let path = self.cursor.path.clone();
781 let offset = self.cursor.offset;
782
783 match self.get_current() {
784 Some(MathBox::Number(_)) => {
785 if offset > 0 {
786 let mut became_slot = false;
787 let mut removed = false;
788 if let Some(MathBox::Number(s)) =
789 Self::get_node_mut_at_path(&mut self.root, &path)
790 {
791 removed = Self::remove_char_at(s, offset - 1);
792 if s.is_empty() {
793 became_slot = true;
794 }
795 }
796 if became_slot {
797 if let Some(current) = Self::get_node_mut_at_path(&mut self.root, &path) {
798 *current = MathBox::Slot;
799 }
800 self.cursor.offset = 0;
801 } else if removed {
802 self.cursor.offset = offset - 1;
803 }
804 } else {
805 self.delete_previous_from_path(&path);
806 }
807 }
808 Some(MathBox::Symbol(_)) => {
809 if offset > 0 {
810 let mut became_slot = false;
811 let mut removed = false;
812 if let Some(MathBox::Symbol(s)) =
813 Self::get_node_mut_at_path(&mut self.root, &path)
814 {
815 removed = Self::remove_char_at(s, offset - 1);
816 if s.is_empty() {
817 became_slot = true;
818 }
819 }
820 if became_slot {
821 if let Some(current) = Self::get_node_mut_at_path(&mut self.root, &path) {
822 *current = MathBox::Slot;
823 }
824 self.cursor.offset = 0;
825 } else if removed {
826 self.cursor.offset = offset - 1;
827 }
828 } else {
829 self.delete_previous_from_path(&path);
830 }
831 }
832 Some(MathBox::Slot) => {
833 if !self.delete_current_slot_in_row(&path) {
834 self.delete_previous_from_path(&path);
835 }
836 }
837 Some(_) => {
838 if !path.is_empty() {
839 self.replace_current_with_slot(&path);
840 }
841 }
842 None => {}
843 }
844 }
845
846 /// Delete forward
847 fn delete_forward(&mut self) {
848 let path = self.cursor.path.clone();
849 let offset = self.cursor.offset;
850
851 match self.get_current() {
852 Some(MathBox::Number(s)) => {
853 let len = Self::char_count(s);
854 if offset < len {
855 let mut became_slot = false;
856 if let Some(MathBox::Number(cur)) =
857 Self::get_node_mut_at_path(&mut self.root, &path)
858 {
859 Self::remove_char_at(cur, offset);
860 if cur.is_empty() {
861 became_slot = true;
862 }
863 }
864 if became_slot {
865 if let Some(current) = Self::get_node_mut_at_path(&mut self.root, &path) {
866 *current = MathBox::Slot;
867 }
868 self.cursor.offset = 0;
869 }
870 } else {
871 self.delete_next_from_path(&path);
872 }
873 }
874 Some(MathBox::Symbol(s)) => {
875 let len = Self::char_count(s);
876 if offset < len {
877 let mut became_slot = false;
878 if let Some(MathBox::Symbol(cur)) =
879 Self::get_node_mut_at_path(&mut self.root, &path)
880 {
881 Self::remove_char_at(cur, offset);
882 if cur.is_empty() {
883 became_slot = true;
884 }
885 }
886 if became_slot {
887 if let Some(current) = Self::get_node_mut_at_path(&mut self.root, &path) {
888 *current = MathBox::Slot;
889 }
890 self.cursor.offset = 0;
891 }
892 } else {
893 self.delete_next_from_path(&path);
894 }
895 }
896 Some(MathBox::Slot) => {
897 self.delete_next_from_path(&path);
898 }
899 Some(_) => {
900 if !path.is_empty() {
901 self.replace_current_with_slot(&path);
902 }
903 }
904 None => {}
905 }
906 }
907
908 /// Move cursor left
909 fn move_left(&mut self) {
910 let row_prev_idx = match self.get_current() {
911 Some(MathBox::Row(items)) if self.cursor.offset > 0 => {
912 Some(self.cursor.offset.min(items.len()) - 1)
913 }
914 _ => None,
915 };
916 if let Some(prev_idx) = row_prev_idx {
917 self.cursor.enter(prev_idx);
918 self.move_to_end_of_current();
919 return;
920 }
921
922 if let Some(current) = self.get_current() {
923 if matches!(current, MathBox::Number(_) | MathBox::Symbol(_)) && self.cursor.offset > 0
924 {
925 self.cursor.offset -= 1;
926 return;
927 }
928 }
929
930 let mut path = self.cursor.path.clone();
931 while let Some(idx) = path.pop() {
932 if idx > 0 {
933 path.push(idx - 1);
934 self.cursor.path = path;
935 self.cursor.offset = 0;
936 self.move_to_end_of_current();
937 return;
938 }
939 }
940 }
941
942 /// Move cursor right
943 fn move_right(&mut self) {
944 let row_next_idx = match self.get_current() {
945 Some(MathBox::Row(items)) if self.cursor.offset < items.len() => {
946 Some(self.cursor.offset)
947 }
948 _ => None,
949 };
950 if let Some(next_idx) = row_next_idx {
951 self.cursor.enter(next_idx);
952 return;
953 }
954
955 if let Some(current) = self.get_current() {
956 match current {
957 MathBox::Number(s) | MathBox::Symbol(s)
958 if self.cursor.offset < Self::char_count(s) =>
959 {
960 self.cursor.offset += 1;
961 return;
962 }
963 // Row uses cursor.offset as an insertion index, so at row boundaries
964 // we should climb out/advance rather than re-enter row child 0.
965 MathBox::Row(_) => {}
966 _ if current.child_count() > 0 => {
967 self.cursor.enter(0);
968 return;
969 }
970 _ => {}
971 }
972 }
973
974 let original_path = self.cursor.path.clone();
975 let mut path = original_path.clone();
976 while let Some(idx) = path.pop() {
977 if let Some(parent) = Self::get_node_at_path(&self.root, &path) {
978 if idx + 1 < parent.child_count() {
979 path.push(idx + 1);
980 self.cursor.path = path;
981 self.cursor.offset = 0;
982 return;
983 }
984 }
985 }
986
987 if let Some((row_path, insert_offset)) = self.row_insertion_after_path(&original_path) {
988 self.cursor.path = row_path;
989 self.cursor.offset = insert_offset;
990 }
991 }
992
993 /// Move cursor up (for fractions, powers)
994 fn move_up(&mut self) {
995 // Check if parent is a fraction and we're in denominator
996 if self.cursor.path.len() >= 1 {
997 let last_idx = self.cursor.path[self.cursor.path.len() - 1];
998 self.cursor.exit();
999 if let Some(parent) = self.get_current() {
1000 match parent {
1001 MathBox::Fraction { .. } if last_idx == 1 => {
1002 // Move from denominator to numerator
1003 self.cursor.enter(0);
1004 }
1005 MathBox::Power { .. } if last_idx == 0 => {
1006 // Move from base to exponent
1007 self.cursor.enter(1);
1008 }
1009 _ => {
1010 // Restore position
1011 self.cursor.enter(last_idx);
1012 }
1013 }
1014 }
1015 }
1016 }
1017
1018 /// Move cursor down (for fractions)
1019 fn move_down(&mut self) {
1020 if self.cursor.path.len() >= 1 {
1021 let last_idx = self.cursor.path[self.cursor.path.len() - 1];
1022 self.cursor.exit();
1023 if let Some(parent) = self.get_current() {
1024 match parent {
1025 MathBox::Fraction { .. } if last_idx == 0 => {
1026 // Move from numerator to denominator
1027 self.cursor.enter(1);
1028 }
1029 MathBox::Power { .. } if last_idx == 1 => {
1030 // Move from exponent to base
1031 self.cursor.enter(0);
1032 }
1033 _ => {
1034 self.cursor.enter(last_idx);
1035 }
1036 }
1037 }
1038 }
1039 }
1040
1041 /// Move to next slot (Tab)
1042 fn move_to_next_slot(&mut self) {
1043 let slots = self.collect_slot_paths();
1044 if slots.is_empty() {
1045 return;
1046 }
1047
1048 let current_path = self.cursor.path.clone();
1049 if let Some(idx) = slots.iter().position(|p| p == &current_path) {
1050 self.cursor.path = slots[(idx + 1) % slots.len()].clone();
1051 self.cursor.offset = 0;
1052 return;
1053 }
1054
1055 let ordered_paths = self.collect_node_paths();
1056 let target =
1057 if let Some(current_idx) = ordered_paths.iter().position(|p| p == &current_path) {
1058 slots
1059 .iter()
1060 .find(|slot| {
1061 ordered_paths
1062 .iter()
1063 .position(|p| p == *slot)
1064 .is_some_and(|slot_idx| slot_idx > current_idx)
1065 })
1066 .cloned()
1067 .unwrap_or_else(|| slots[0].clone())
1068 } else {
1069 slots[0].clone()
1070 };
1071
1072 self.cursor.path = target;
1073 self.cursor.offset = 0;
1074 }
1075
1076 /// Move to previous slot (Shift+Tab)
1077 fn move_to_prev_slot(&mut self) {
1078 let slots = self.collect_slot_paths();
1079 if slots.is_empty() {
1080 return;
1081 }
1082
1083 let current_path = self.cursor.path.clone();
1084 if let Some(idx) = slots.iter().position(|p| p == &current_path) {
1085 self.cursor.path = if idx == 0 {
1086 slots[slots.len() - 1].clone()
1087 } else {
1088 slots[idx - 1].clone()
1089 };
1090 self.cursor.offset = 0;
1091 return;
1092 }
1093
1094 let ordered_paths = self.collect_node_paths();
1095 let target =
1096 if let Some(current_idx) = ordered_paths.iter().position(|p| p == &current_path) {
1097 slots
1098 .iter()
1099 .rev()
1100 .find(|slot| {
1101 ordered_paths
1102 .iter()
1103 .position(|p| p == *slot)
1104 .is_some_and(|slot_idx| slot_idx < current_idx)
1105 })
1106 .cloned()
1107 .unwrap_or_else(|| slots[slots.len() - 1].clone())
1108 } else {
1109 slots[slots.len() - 1].clone()
1110 };
1111
1112 self.cursor.path = target;
1113 self.cursor.offset = 0;
1114 }
1115
1116 /// Move cursor to start
1117 fn move_to_start(&mut self) {
1118 self.cursor = Cursor::new();
1119 }
1120
1121 /// Move cursor to end
1122 fn move_to_end(&mut self) {
1123 self.cursor = Cursor::new();
1124 self.move_to_end_of_current();
1125 }
1126
1127 /// Move to end of current element
1128 fn move_to_end_of_current(&mut self) {
1129 if let Some(current) = self.get_current() {
1130 match current {
1131 MathBox::Number(s) | MathBox::Symbol(s) => {
1132 self.cursor.offset = Self::char_count(s);
1133 }
1134 _ if current.child_count() > 0 => {
1135 self.cursor.enter(current.child_count() - 1);
1136 self.move_to_end_of_current();
1137 }
1138 _ => {}
1139 }
1140 }
1141 }
1142
1143 /// Get the current element at cursor
1144 fn get_current(&self) -> Option<&MathBox> {
1145 Self::get_node_at_path(&self.root, &self.cursor.path)
1146 }
1147
1148 /// Get mutable reference to current element
1149 fn get_current_mut(&mut self) -> Option<&mut MathBox> {
1150 Self::get_node_mut_at_path(&mut self.root, &self.cursor.path)
1151 }
1152
1153 /// Get the root MathBox
1154 pub fn mathbox(&self) -> &MathBox {
1155 &self.root
1156 }
1157
1158 /// Get the cursor path for rendering
1159 pub fn cursor_path(&self) -> &[usize] {
1160 &self.cursor.path
1161 }
1162
1163 /// Get character offset within the currently focused token
1164 pub fn cursor_offset(&self) -> usize {
1165 self.cursor.offset
1166 }
1167
1168 fn get_node_at_path<'a>(root: &'a MathBox, path: &[usize]) -> Option<&'a MathBox> {
1169 let mut current = root;
1170 for &idx in path {
1171 current = current.child(idx)?;
1172 }
1173 Some(current)
1174 }
1175
1176 fn get_node_mut_at_path<'a>(root: &'a mut MathBox, path: &[usize]) -> Option<&'a mut MathBox> {
1177 let mut current = root;
1178 for &idx in path {
1179 current = current.child_mut(idx)?;
1180 }
1181 Some(current)
1182 }
1183
1184 fn collect_slot_paths(&self) -> Vec<Vec<usize>> {
1185 fn walk(node: &MathBox, path: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
1186 if matches!(node, MathBox::Slot) {
1187 out.push(path.clone());
1188 return;
1189 }
1190 for i in MathInput::ordered_child_indices(node) {
1191 if let Some(child) = node.child(i) {
1192 path.push(i);
1193 walk(child, path, out);
1194 path.pop();
1195 }
1196 }
1197 }
1198
1199 let mut out = Vec::new();
1200 let mut path = Vec::new();
1201 walk(&self.root, &mut path, &mut out);
1202 out
1203 }
1204
1205 fn collect_node_paths(&self) -> Vec<Vec<usize>> {
1206 fn walk(node: &MathBox, path: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
1207 out.push(path.clone());
1208 for i in MathInput::ordered_child_indices(node) {
1209 if let Some(child) = node.child(i) {
1210 path.push(i);
1211 walk(child, path, out);
1212 path.pop();
1213 }
1214 }
1215 }
1216
1217 let mut out = Vec::new();
1218 let mut path = Vec::new();
1219 walk(&self.root, &mut path, &mut out);
1220 out
1221 }
1222
1223 fn ordered_child_indices(node: &MathBox) -> Vec<usize> {
1224 match node {
1225 MathBox::Power { .. } => vec![0, 1],
1226 MathBox::Derivative { .. } => vec![0, 1],
1227 MathBox::Sum { .. } | MathBox::Product { .. } => vec![1, 2, 3, 0],
1228 MathBox::Integral { lower, upper, .. } => {
1229 let has_lower = lower.is_some();
1230 let has_upper = upper.is_some();
1231 let mut indices = Vec::new();
1232 if has_upper {
1233 indices.push(if has_lower { 1 } else { 0 });
1234 }
1235 if has_lower {
1236 indices.push(0);
1237 }
1238 let body_idx = usize::from(has_lower) + usize::from(has_upper);
1239 indices.push(body_idx);
1240 indices.push(body_idx + 1); // variable slot/symbol
1241 indices
1242 }
1243 _ => (0..node.child_count()).collect(),
1244 }
1245 }
1246
1247 fn focus_first_slot_in_current_subtree(&mut self) {
1248 let base = self.cursor.path.clone();
1249 let slots = self.collect_slot_paths();
1250 if let Some(path) = slots
1251 .into_iter()
1252 .find(|p| p.len() > base.len() && p.starts_with(&base))
1253 {
1254 self.cursor.path = path;
1255 self.cursor.offset = 0;
1256 }
1257 }
1258
1259 fn previous_sibling_path(&self, path: &[usize]) -> Option<Vec<usize>> {
1260 let mut cursor = path.to_vec();
1261 while let Some(idx) = cursor.pop() {
1262 if idx > 0 {
1263 cursor.push(idx - 1);
1264 return Some(cursor);
1265 }
1266 }
1267 None
1268 }
1269
1270 fn next_sibling_path(&self, path: &[usize]) -> Option<Vec<usize>> {
1271 let mut cursor = path.to_vec();
1272 while let Some(idx) = cursor.pop() {
1273 if let Some(parent) = Self::get_node_at_path(&self.root, &cursor) {
1274 if idx + 1 < parent.child_count() {
1275 cursor.push(idx + 1);
1276 return Some(cursor);
1277 }
1278 }
1279 }
1280 None
1281 }
1282
1283 fn row_insertion_after_path(&self, path: &[usize]) -> Option<(Vec<usize>, usize)> {
1284 let mut cursor = path.to_vec();
1285 while let Some(idx) = cursor.pop() {
1286 if let Some(MathBox::Row(items)) = Self::get_node_at_path(&self.root, &cursor) {
1287 return Some((cursor.clone(), (idx + 1).min(items.len())));
1288 }
1289 }
1290 None
1291 }
1292
1293 fn replace_current_with_slot(&mut self, path: &[usize]) {
1294 if let Some(current) = Self::get_node_mut_at_path(&mut self.root, path) {
1295 *current = MathBox::Slot;
1296 self.cursor.path = path.to_vec();
1297 self.cursor.offset = 0;
1298 }
1299 }
1300
1301 fn is_effectively_empty(node: &MathBox) -> bool {
1302 match node {
1303 MathBox::Slot => true,
1304 MathBox::Number(s) | MathBox::Symbol(s) => s.is_empty(),
1305 MathBox::Operator(_) => false,
1306 MathBox::Fraction { num, den } => {
1307 Self::is_effectively_empty(num) && Self::is_effectively_empty(den)
1308 }
1309 MathBox::Power { base, exp } => {
1310 Self::is_effectively_empty(base) && Self::is_effectively_empty(exp)
1311 }
1312 MathBox::Subscript { base, sub } => {
1313 Self::is_effectively_empty(base) && Self::is_effectively_empty(sub)
1314 }
1315 MathBox::Root { index, radicand } => {
1316 let index_empty = index
1317 .as_deref()
1318 .map(Self::is_effectively_empty)
1319 .unwrap_or(true);
1320 index_empty && Self::is_effectively_empty(radicand)
1321 }
1322 MathBox::Func { args, .. } => args.iter().all(Self::is_effectively_empty),
1323 MathBox::Abs(inner) | MathBox::Parens(inner) => Self::is_effectively_empty(inner),
1324 MathBox::Integral {
1325 lower,
1326 upper,
1327 body,
1328 var,
1329 } => {
1330 let lower_empty = lower
1331 .as_deref()
1332 .map(Self::is_effectively_empty)
1333 .unwrap_or(true);
1334 let upper_empty = upper
1335 .as_deref()
1336 .map(Self::is_effectively_empty)
1337 .unwrap_or(true);
1338 lower_empty
1339 && upper_empty
1340 && Self::is_effectively_empty(body)
1341 && Self::is_effectively_empty(var)
1342 }
1343 MathBox::Derivative { var, body, .. } => {
1344 Self::is_effectively_empty(var) && Self::is_effectively_empty(body)
1345 }
1346 MathBox::Limit { var, to, body, .. } => {
1347 Self::is_effectively_empty(var)
1348 && Self::is_effectively_empty(to)
1349 && Self::is_effectively_empty(body)
1350 }
1351 MathBox::Sum {
1352 var,
1353 lower,
1354 upper,
1355 body,
1356 }
1357 | MathBox::Product {
1358 var,
1359 lower,
1360 upper,
1361 body,
1362 } => {
1363 Self::is_effectively_empty(var)
1364 && Self::is_effectively_empty(lower)
1365 && Self::is_effectively_empty(upper)
1366 && Self::is_effectively_empty(body)
1367 }
1368 MathBox::Matrix { rows } => rows.iter().flatten().all(Self::is_effectively_empty),
1369 MathBox::Row(items) => items.iter().all(Self::is_effectively_empty),
1370 }
1371 }
1372
1373 fn collapse_parent_if_empty(&mut self, current_path: &[usize]) -> bool {
1374 let Some((_, parent_path)) = current_path.split_last() else {
1375 return false;
1376 };
1377 if parent_path.is_empty() {
1378 return false;
1379 }
1380
1381 let should_collapse = Self::get_node_at_path(&self.root, parent_path)
1382 .map(Self::is_effectively_empty)
1383 .unwrap_or(false);
1384 if should_collapse {
1385 self.replace_current_with_slot(parent_path);
1386 return true;
1387 }
1388 false
1389 }
1390
1391 fn remove_sibling_from_parent_row(
1392 &mut self,
1393 parent_path: &[usize],
1394 remove_idx: usize,
1395 cursor_idx: usize,
1396 ) -> bool {
1397 if let Some(MathBox::Row(items)) = Self::get_node_mut_at_path(&mut self.root, parent_path) {
1398 if remove_idx >= items.len() {
1399 return false;
1400 }
1401
1402 items.remove(remove_idx);
1403 if items.is_empty() {
1404 items.push(MathBox::Slot);
1405 }
1406
1407 let new_idx = cursor_idx.min(items.len().saturating_sub(1));
1408 self.cursor.path = parent_path.to_vec();
1409 self.cursor.enter(new_idx);
1410 self.cursor.offset = 0;
1411 return true;
1412 }
1413 false
1414 }
1415
1416 fn delete_current_slot_in_row(&mut self, path: &[usize]) -> bool {
1417 let Some((&idx, parent_path)) = path.split_last() else {
1418 return false;
1419 };
1420 self.remove_sibling_from_parent_row(parent_path, idx, idx.saturating_sub(1))
1421 }
1422
1423 fn delete_previous_from_path(&mut self, current_path: &[usize]) {
1424 if let Some(prev_path) = self.previous_sibling_path(current_path) {
1425 if let (Some((&cur_idx, cur_parent)), Some((&prev_idx, prev_parent))) =
1426 (current_path.split_last(), prev_path.split_last())
1427 {
1428 if cur_parent == prev_parent
1429 && prev_idx + 1 == cur_idx
1430 && self.remove_sibling_from_parent_row(
1431 cur_parent,
1432 prev_idx,
1433 cur_idx.saturating_sub(1),
1434 )
1435 {
1436 return;
1437 }
1438 }
1439
1440 if let Some(node) = Self::get_node_mut_at_path(&mut self.root, &prev_path) {
1441 match node {
1442 MathBox::Number(s) if !s.is_empty() => {
1443 let last = Self::char_count(s).saturating_sub(1);
1444 Self::remove_char_at(s, last);
1445 if s.is_empty() {
1446 *node = MathBox::Slot;
1447 self.cursor.offset = 0;
1448 } else {
1449 self.cursor.offset = Self::char_count(s);
1450 }
1451 self.cursor.path = prev_path;
1452 }
1453 MathBox::Symbol(s) if !s.is_empty() => {
1454 let last = Self::char_count(s).saturating_sub(1);
1455 Self::remove_char_at(s, last);
1456 if s.is_empty() {
1457 *node = MathBox::Slot;
1458 self.cursor.offset = 0;
1459 } else {
1460 self.cursor.offset = Self::char_count(s);
1461 }
1462 self.cursor.path = prev_path;
1463 }
1464 _ => {
1465 *node = MathBox::Slot;
1466 self.cursor.path = prev_path;
1467 self.cursor.offset = 0;
1468 }
1469 }
1470 }
1471 return;
1472 }
1473
1474 let _ = self.collapse_parent_if_empty(current_path);
1475 }
1476
1477 fn delete_next_from_path(&mut self, current_path: &[usize]) {
1478 if let Some(next_path) = self.next_sibling_path(current_path) {
1479 if let (Some((&cur_idx, cur_parent)), Some((&next_idx, next_parent))) =
1480 (current_path.split_last(), next_path.split_last())
1481 {
1482 if cur_parent == next_parent
1483 && cur_idx + 1 == next_idx
1484 && self.remove_sibling_from_parent_row(cur_parent, next_idx, cur_idx)
1485 {
1486 return;
1487 }
1488 }
1489
1490 if let Some(node) = Self::get_node_mut_at_path(&mut self.root, &next_path) {
1491 match node {
1492 MathBox::Number(s) if !s.is_empty() => {
1493 Self::remove_char_at(s, 0);
1494 if s.is_empty() {
1495 *node = MathBox::Slot;
1496 }
1497 }
1498 MathBox::Symbol(s) if !s.is_empty() => {
1499 Self::remove_char_at(s, 0);
1500 if s.is_empty() {
1501 *node = MathBox::Slot;
1502 }
1503 }
1504 _ => {
1505 *node = MathBox::Slot;
1506 }
1507 }
1508 }
1509 return;
1510 }
1511
1512 let _ = self.collapse_parent_if_empty(current_path);
1513 }
1514 }
1515
1516 /// Special keys that can be handled
1517 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1518 pub enum SpecialKey {
1519 Left,
1520 Right,
1521 Up,
1522 Down,
1523 Tab,
1524 ShiftTab,
1525 Enter,
1526 Escape,
1527 Backspace,
1528 Delete,
1529 Home,
1530 End,
1531 }
1532
1533 #[cfg(test)]
1534 mod tests {
1535 use super::*;
1536 use crate::convert::to_expr;
1537
1538 fn run_command(input: &mut MathInput, cmd: &str) {
1539 input.handle_char('\\');
1540 for ch in cmd.chars() {
1541 input.handle_char(ch);
1542 }
1543 input.handle_char(' ');
1544 }
1545
1546 #[test]
1547 fn test_new_input() {
1548 let input = MathInput::new();
1549 assert!(matches!(input.root, MathBox::Row(_)));
1550 }
1551
1552 #[test]
1553 fn test_number_input() {
1554 let mut input = MathInput::new();
1555 input.handle_char('1');
1556 input.handle_char('2');
1557 input.handle_char('3');
1558
1559 // Should have "123" as number
1560 if let MathBox::Row(items) = &input.root {
1561 if let MathBox::Number(s) = &items[0] {
1562 assert_eq!(s, "123");
1563 } else {
1564 panic!("Expected Number");
1565 }
1566 }
1567 }
1568
1569 #[test]
1570 fn test_fraction_input() {
1571 let mut input = MathInput::new();
1572 input.handle_char('/');
1573
1574 // Should have fraction template
1575 if let MathBox::Row(items) = &input.root {
1576 assert!(matches!(items[0], MathBox::Fraction { .. }));
1577 }
1578 }
1579
1580 #[test]
1581 fn test_power_input() {
1582 let mut input = MathInput::new();
1583 input.handle_char('2');
1584 input.handle_char('^');
1585 input.handle_char('2');
1586
1587 if let MathBox::Row(items) = &input.root {
1588 if let MathBox::Power { base, exp } = &items[0] {
1589 assert!(matches!(base.as_ref(), MathBox::Number(s) if s == "2"));
1590 assert!(matches!(exp.as_ref(), MathBox::Number(s) if s == "2"));
1591 } else {
1592 panic!("Expected Power");
1593 }
1594 }
1595 }
1596
1597 #[test]
1598 fn test_operator_after_exponent_promotes_outside_power() {
1599 let mut input = MathInput::new();
1600 input.handle_char('x');
1601 input.handle_char('^');
1602 input.handle_char('2');
1603 input.handle_char('+');
1604 input.handle_char('3');
1605
1606 if let MathBox::Row(items) = &input.root {
1607 assert_eq!(items.len(), 3);
1608 if let MathBox::Power { base, exp } = &items[0] {
1609 assert!(matches!(base.as_ref(), MathBox::Symbol(s) if s == "x"));
1610 assert!(matches!(exp.as_ref(), MathBox::Number(s) if s == "2"));
1611 } else {
1612 panic!("Expected Power");
1613 }
1614 assert!(matches!(&items[1], MathBox::Operator(Operator::Add)));
1615 assert!(matches!(&items[2], MathBox::Number(s) if s == "3"));
1616 } else {
1617 panic!("Expected Row");
1618 }
1619 }
1620
1621 #[test]
1622 fn test_right_from_exponent_end_moves_to_row_insertion_point() {
1623 let mut input = MathInput::new();
1624 input.handle_char('x');
1625 input.handle_char('^');
1626 input.handle_char('2');
1627
1628 input.handle_key(SpecialKey::Right);
1629 assert_eq!(input.cursor_path(), &[]);
1630 assert_eq!(input.cursor_offset(), 1);
1631
1632 input.handle_char('+');
1633 input.handle_char('3');
1634
1635 if let MathBox::Row(items) = &input.root {
1636 assert_eq!(items.len(), 3);
1637 assert!(matches!(&items[1], MathBox::Operator(Operator::Add)));
1638 assert!(matches!(&items[2], MathBox::Number(s) if s == "3"));
1639 } else {
1640 panic!("Expected Row");
1641 }
1642 }
1643
1644 #[test]
1645 fn test_right_from_parenthesized_exponent_can_exit_to_outer_row() {
1646 let mut input = MathInput::new();
1647 input.handle_char('2');
1648 input.handle_char('^');
1649 input.handle_char('(');
1650 input.handle_char('x');
1651 input.handle_char('+');
1652 input.handle_char('3');
1653
1654 // Right #1: from number end to inner-row insertion at end.
1655 input.handle_key(SpecialKey::Right);
1656 assert_eq!(input.cursor_path(), &[0, 1, 0]);
1657 assert_eq!(input.cursor_offset(), 3);
1658
1659 // Right #2: climb out of exponent context to outer row insertion.
1660 input.handle_key(SpecialKey::Right);
1661 assert_eq!(input.cursor_path(), &[]);
1662 assert_eq!(input.cursor_offset(), 1);
1663
1664 input.handle_char('+');
1665 input.handle_char('4');
1666
1667 if let MathBox::Row(items) = &input.root {
1668 assert_eq!(items.len(), 3);
1669 assert!(matches!(&items[1], MathBox::Operator(Operator::Add)));
1670 assert!(matches!(&items[2], MathBox::Number(s) if s == "4"));
1671 } else {
1672 panic!("Expected Row");
1673 }
1674 }
1675
1676 #[test]
1677 fn test_subscript_after_exponent_at_row_insertion_wraps_power() {
1678 let mut input = MathInput::new();
1679 input.handle_char('2');
1680 input.handle_char('^');
1681 input.handle_char('3');
1682
1683 input.handle_key(SpecialKey::Right);
1684 assert_eq!(input.cursor_path(), &[]);
1685 assert_eq!(input.cursor_offset(), 1);
1686
1687 input.handle_char('_');
1688 input.handle_char('4');
1689
1690 if let MathBox::Row(items) = &input.root {
1691 assert_eq!(items.len(), 1);
1692 if let MathBox::Subscript { base, sub } = &items[0] {
1693 if let MathBox::Power {
1694 base: power_base,
1695 exp,
1696 } = base.as_ref()
1697 {
1698 assert!(matches!(power_base.as_ref(), MathBox::Number(s) if s == "2"));
1699 assert!(matches!(exp.as_ref(), MathBox::Number(s) if s == "3"));
1700 } else {
1701 panic!("Expected Power base for subscript");
1702 }
1703 assert!(matches!(sub.as_ref(), MathBox::Number(s) if s == "4"));
1704 } else {
1705 panic!("Expected Subscript");
1706 }
1707 } else {
1708 panic!("Expected Row");
1709 }
1710 }
1711
1712 #[test]
1713 fn test_operator_after_subscript_promotes_outside_subscript() {
1714 let mut input = MathInput::new();
1715 input.handle_char('x');
1716 input.handle_char('_');
1717 input.handle_char('1');
1718 input.handle_char('+');
1719 input.handle_char('2');
1720
1721 if let MathBox::Row(items) = &input.root {
1722 assert_eq!(items.len(), 3);
1723 if let MathBox::Subscript { base, sub } = &items[0] {
1724 assert!(matches!(base.as_ref(), MathBox::Symbol(s) if s == "x"));
1725 assert!(matches!(sub.as_ref(), MathBox::Number(s) if s == "1"));
1726 } else {
1727 panic!("Expected Subscript");
1728 }
1729 assert!(matches!(&items[1], MathBox::Operator(Operator::Add)));
1730 assert!(matches!(&items[2], MathBox::Number(s) if s == "2"));
1731 } else {
1732 panic!("Expected Row");
1733 }
1734 }
1735
1736 #[test]
1737 fn test_factorial_input_wraps_current() {
1738 let mut input = MathInput::new();
1739 input.handle_char('5');
1740 input.handle_char('!');
1741
1742 if let MathBox::Row(items) = &input.root {
1743 if let MathBox::Func { name, args } = &items[0] {
1744 assert_eq!(name, "factorial");
1745 assert_eq!(args.len(), 1);
1746 assert!(matches!(&args[0], MathBox::Number(s) if s == "5"));
1747 } else {
1748 panic!("Expected factorial function");
1749 }
1750 } else {
1751 panic!("Expected Row");
1752 }
1753 }
1754
1755 #[test]
1756 fn test_factorial_input_on_empty_slot_inserts_template() {
1757 let mut input = MathInput::new();
1758 input.handle_char('!');
1759
1760 if let MathBox::Row(items) = &input.root {
1761 if let MathBox::Func { name, args } = &items[0] {
1762 assert_eq!(name, "factorial");
1763 assert_eq!(args.len(), 1);
1764 assert!(matches!(&args[0], MathBox::Slot));
1765 } else {
1766 panic!("Expected factorial function");
1767 }
1768 } else {
1769 panic!("Expected Row");
1770 }
1771 assert_eq!(input.cursor_path(), &[0, 0]);
1772 }
1773
1774 #[test]
1775 fn test_insert_in_middle_of_number() {
1776 let mut input = MathInput::new();
1777 input.handle_char('1');
1778 input.handle_char('2');
1779 input.handle_char('3');
1780
1781 input.handle_key(SpecialKey::Left);
1782 input.handle_key(SpecialKey::Left);
1783 input.handle_char('4');
1784
1785 if let MathBox::Row(items) = &input.root {
1786 assert!(matches!(&items[0], MathBox::Number(s) if s == "1423"));
1787 }
1788 assert_eq!(input.cursor_offset(), 2);
1789 }
1790
1791 #[test]
1792 fn test_backspace_uses_cursor_offset() {
1793 let mut input = MathInput::new();
1794 input.handle_char('1');
1795 input.handle_char('2');
1796 input.handle_char('3');
1797
1798 input.handle_key(SpecialKey::Left);
1799 input.handle_key(SpecialKey::Left);
1800 input.handle_key(SpecialKey::Backspace);
1801
1802 if let MathBox::Row(items) = &input.root {
1803 assert!(matches!(&items[0], MathBox::Number(s) if s == "23"));
1804 }
1805 assert_eq!(input.cursor_offset(), 0);
1806 }
1807
1808 #[test]
1809 fn test_delete_uses_cursor_offset() {
1810 let mut input = MathInput::new();
1811 input.handle_char('1');
1812 input.handle_char('2');
1813 input.handle_char('3');
1814
1815 input.handle_key(SpecialKey::Left);
1816 input.handle_key(SpecialKey::Left);
1817 input.handle_key(SpecialKey::Delete);
1818
1819 if let MathBox::Row(items) = &input.root {
1820 assert!(matches!(&items[0], MathBox::Number(s) if s == "13"));
1821 }
1822 assert_eq!(input.cursor_offset(), 1);
1823 }
1824
1825 #[test]
1826 fn test_row_insertion_after_number() {
1827 let mut input = MathInput::new();
1828 input.handle_char('2');
1829 input.handle_char('+');
1830 input.handle_char('3');
1831
1832 if let MathBox::Row(items) = &input.root {
1833 assert_eq!(items.len(), 3);
1834 assert!(matches!(&items[0], MathBox::Number(s) if s == "2"));
1835 assert!(matches!(&items[1], MathBox::Operator(Operator::Add)));
1836 assert!(matches!(&items[2], MathBox::Number(s) if s == "3"));
1837 } else {
1838 panic!("Expected Row");
1839 }
1840 }
1841
1842 #[test]
1843 fn test_command_mode() {
1844 let mut input = MathInput::new();
1845 input.handle_char('\\');
1846 assert!(input.command_buffer.is_some());
1847
1848 input.handle_char('s');
1849 input.handle_char('q');
1850 input.handle_char('r');
1851 input.handle_char('t');
1852 input.handle_char(' '); // Execute command
1853
1854 // Should have sqrt template
1855 if let MathBox::Row(items) = &input.root {
1856 assert!(matches!(items[0], MathBox::Root { index: None, .. }));
1857 }
1858 }
1859
1860 #[test]
1861 fn test_command_mode_colon_alias() {
1862 let mut input = MathInput::new();
1863 input.handle_char(':');
1864 assert!(input.command_buffer.is_some());
1865
1866 input.handle_char('s');
1867 input.handle_char('u');
1868 input.handle_char('m');
1869 input.handle_char(' ');
1870
1871 if let MathBox::Row(items) = &input.root {
1872 assert!(matches!(items[0], MathBox::Sum { .. }));
1873 }
1874 }
1875
1876 #[test]
1877 fn test_command_mode_executes_on_enter() {
1878 let mut input = MathInput::new();
1879 input.handle_char(':');
1880 input.handle_char('f');
1881 input.handle_char('r');
1882 input.handle_char('a');
1883 input.handle_char('c');
1884
1885 let result = input.handle_key(SpecialKey::Enter);
1886 assert!(matches!(result, InputResult::Consumed));
1887 assert!(input.command_buffer.is_none());
1888
1889 if let MathBox::Row(items) = &input.root {
1890 assert!(matches!(items[0], MathBox::Fraction { .. }));
1891 } else {
1892 panic!("Expected Row");
1893 }
1894 }
1895
1896 #[test]
1897 fn test_sum_command_template() {
1898 let mut input = MathInput::new();
1899 run_command(&mut input, "sum");
1900
1901 if let MathBox::Row(items) = &input.root {
1902 if let MathBox::Sum {
1903 var,
1904 lower,
1905 upper,
1906 body,
1907 } = &items[0]
1908 {
1909 assert!(matches!(var.as_ref(), MathBox::Symbol(s) if s == "i"));
1910 assert!(matches!(lower.as_ref(), MathBox::Slot));
1911 assert!(matches!(upper.as_ref(), MathBox::Slot));
1912 assert!(matches!(body.as_ref(), MathBox::Slot));
1913 } else {
1914 panic!("Expected Sum template");
1915 }
1916 }
1917 }
1918
1919 #[test]
1920 fn test_diff_command_template() {
1921 let mut input = MathInput::new();
1922 run_command(&mut input, "diff");
1923
1924 if let MathBox::Row(items) = &input.root {
1925 if let MathBox::Derivative { order, var, body } = &items[0] {
1926 assert_eq!(*order, 1);
1927 assert!(matches!(var.as_ref(), MathBox::Slot));
1928 assert!(matches!(body.as_ref(), MathBox::Slot));
1929 } else {
1930 panic!("Expected Derivative template");
1931 }
1932 }
1933 }
1934
1935 #[test]
1936 fn test_lim_command_template() {
1937 let mut input = MathInput::new();
1938 run_command(&mut input, "lim");
1939
1940 if let MathBox::Row(items) = &input.root {
1941 if let MathBox::Limit {
1942 var,
1943 to,
1944 direction,
1945 body,
1946 } = &items[0]
1947 {
1948 assert!(matches!(var.as_ref(), MathBox::Symbol(s) if s == "x"));
1949 assert!(matches!(to.as_ref(), MathBox::Slot));
1950 assert!(direction.is_none());
1951 assert!(matches!(body.as_ref(), MathBox::Slot));
1952 } else {
1953 panic!("Expected Limit template");
1954 }
1955 }
1956 }
1957
1958 #[test]
1959 fn test_dint_command_template() {
1960 let mut input = MathInput::new();
1961 run_command(&mut input, "dint");
1962
1963 if let MathBox::Row(items) = &input.root {
1964 if let MathBox::Integral {
1965 lower,
1966 upper,
1967 body,
1968 var,
1969 } = &items[0]
1970 {
1971 assert!(matches!(lower.as_deref(), Some(MathBox::Slot)));
1972 assert!(matches!(upper.as_deref(), Some(MathBox::Slot)));
1973 assert!(matches!(body.as_ref(), MathBox::Slot));
1974 assert!(matches!(var.as_ref(), MathBox::Symbol(s) if s == "x"));
1975 } else {
1976 panic!("Expected definite Integral template");
1977 }
1978 }
1979 }
1980
1981 #[test]
1982 fn test_solve_command_template() {
1983 let mut input = MathInput::new();
1984 run_command(&mut input, "solve");
1985
1986 assert_eq!(input.cursor_path(), &[0, 0]);
1987 if let MathBox::Row(items) = &input.root {
1988 if let MathBox::Func { name, args } = &items[0] {
1989 assert_eq!(name, "solve");
1990 assert_eq!(args.len(), 2);
1991 assert!(matches!(&args[0], MathBox::Slot));
1992 assert!(matches!(&args[1], MathBox::Symbol(s) if s == "x"));
1993 } else {
1994 panic!("Expected solve function template");
1995 }
1996 } else {
1997 panic!("Expected Row");
1998 }
1999 }
2000
2001 #[test]
2002 fn test_tab_cycles_fraction_slots() {
2003 let mut input = MathInput::new();
2004 run_command(&mut input, "frac");
2005
2006 assert_eq!(input.cursor_path(), &[0, 0]);
2007
2008 input.handle_key(SpecialKey::Tab);
2009 assert_eq!(input.cursor_path(), &[0, 1]);
2010
2011 input.handle_key(SpecialKey::Tab);
2012 assert_eq!(input.cursor_path(), &[0, 0]);
2013
2014 input.handle_key(SpecialKey::ShiftTab);
2015 assert_eq!(input.cursor_path(), &[0, 1]);
2016 }
2017
2018 #[test]
2019 fn test_tab_skips_non_slot_children_in_sum() {
2020 let mut input = MathInput::new();
2021 run_command(&mut input, "sum");
2022
2023 // Starts at lower bound slot by policy (non-slot var is skipped).
2024 assert_eq!(input.cursor_path(), &[0, 1]);
2025
2026 input.handle_key(SpecialKey::Tab);
2027 assert_eq!(input.cursor_path(), &[0, 2]);
2028
2029 input.handle_key(SpecialKey::Tab);
2030 assert_eq!(input.cursor_path(), &[0, 3]);
2031
2032 input.handle_key(SpecialKey::Tab);
2033 assert_eq!(input.cursor_path(), &[0, 1]);
2034 }
2035
2036 #[test]
2037 fn test_nested_tab_order_with_power_in_sum_body() {
2038 let mut input = MathInput::new();
2039 run_command(&mut input, "sum");
2040
2041 input.handle_key(SpecialKey::Tab);
2042 input.handle_key(SpecialKey::Tab);
2043 assert_eq!(input.cursor_path(), &[0, 3]);
2044
2045 input.handle_char('x');
2046 input.handle_char('^');
2047 assert_eq!(input.cursor_path(), &[0, 3, 1]);
2048
2049 // From nested exponent slot, Tab should cycle to first slot in the expression.
2050 input.handle_key(SpecialKey::Tab);
2051 assert_eq!(input.cursor_path(), &[0, 1]);
2052
2053 // Shift+Tab should return to the nested exponent slot.
2054 input.handle_key(SpecialKey::ShiftTab);
2055 assert_eq!(input.cursor_path(), &[0, 3, 1]);
2056 }
2057
2058 #[test]
2059 fn test_dint_focus_and_tab_order_upper_lower_body() {
2060 let mut input = MathInput::new();
2061 run_command(&mut input, "dint");
2062
2063 // Policy: upper -> lower -> body -> var (var may be non-slot default).
2064 assert_eq!(input.cursor_path(), &[0, 1]);
2065
2066 input.handle_key(SpecialKey::Tab);
2067 assert_eq!(input.cursor_path(), &[0, 0]);
2068
2069 input.handle_key(SpecialKey::Tab);
2070 assert_eq!(input.cursor_path(), &[0, 2]);
2071
2072 input.handle_key(SpecialKey::Tab);
2073 assert_eq!(input.cursor_path(), &[0, 1]);
2074 }
2075
2076 #[test]
2077 fn test_dint_sequence_builds_expected_power_integrand() {
2078 let mut input = MathInput::new();
2079 run_command(&mut input, "dint");
2080
2081 // Upper bound slot first
2082 input.handle_char('1');
2083 input.handle_key(SpecialKey::Tab);
2084 // Lower bound slot
2085 input.handle_char('0');
2086 input.handle_key(SpecialKey::Tab);
2087 // Body slot: x^(2+x)
2088 input.handle_char('x');
2089 input.handle_char('^');
2090 input.handle_char('(');
2091 input.handle_char('2');
2092 input.handle_char('+');
2093 input.handle_char('x');
2094 input.handle_char(')');
2095
2096 let expr = to_expr(input.mathbox()).expect("structured input should convert");
2097 let rendered = expr.to_string();
2098 assert!(rendered.contains("integrate("), "rendered: {rendered}");
2099 assert!(rendered.contains("x^(2+x)"), "rendered: {rendered}");
2100 assert!(rendered.contains(", x, 0, 1)"), "rendered: {rendered}");
2101 }
2102
2103 #[test]
2104 fn test_integral_body_typed_function_parses_as_function_call() {
2105 let mut input = MathInput::new();
2106 run_command(&mut input, "int");
2107
2108 input.handle_char('s');
2109 input.handle_char('i');
2110 input.handle_char('n');
2111 input.handle_char('(');
2112 input.handle_char('x');
2113 input.handle_char(')');
2114
2115 let expr = to_expr(input.mathbox()).expect("structured input should convert");
2116 assert_eq!(expr.to_string(), "integrate(sin(x), x)");
2117 }
2118
2119 #[test]
2120 fn test_diff_focus_and_tab_order_var_body() {
2121 let mut input = MathInput::new();
2122 run_command(&mut input, "diff");
2123
2124 assert_eq!(input.cursor_path(), &[0, 0]);
2125 input.handle_key(SpecialKey::Tab);
2126 assert_eq!(input.cursor_path(), &[0, 1]);
2127 input.handle_key(SpecialKey::Tab);
2128 assert_eq!(input.cursor_path(), &[0, 0]);
2129 }
2130
2131 #[test]
2132 fn test_close_paren_exits_container() {
2133 let mut input = MathInput::new();
2134 input.handle_char('(');
2135 assert_eq!(input.cursor_path(), &[0, 0]);
2136
2137 input.handle_char(')');
2138 assert_eq!(input.cursor_path(), &[0]);
2139 }
2140
2141 #[test]
2142 fn test_open_paren_after_function_name_wraps_as_func() {
2143 let mut input = MathInput::new();
2144 input.handle_char('s');
2145 input.handle_char('i');
2146 input.handle_char('n');
2147 input.handle_char('(');
2148
2149 assert_eq!(input.cursor_path(), &[0, 0]);
2150 if let MathBox::Row(items) = &input.root {
2151 if let MathBox::Func { name, args } = &items[0] {
2152 assert_eq!(name, "sin");
2153 assert_eq!(args.len(), 1);
2154 assert!(matches!(&args[0], MathBox::Slot));
2155 } else {
2156 panic!("Expected function call");
2157 }
2158 } else {
2159 panic!("Expected row root");
2160 }
2161 }
2162
2163 #[test]
2164 fn test_open_paren_after_non_function_symbol_keeps_parens() {
2165 let mut input = MathInput::new();
2166 input.handle_char('f');
2167 input.handle_char('(');
2168
2169 assert_eq!(input.cursor_path(), &[1, 0]);
2170 if let MathBox::Row(items) = &input.root {
2171 assert!(matches!(&items[0], MathBox::Symbol(s) if s == "f"));
2172 assert!(matches!(&items[1], MathBox::Parens(_)));
2173 } else {
2174 panic!("Expected row root");
2175 }
2176 }
2177
2178 #[test]
2179 fn test_open_paren_after_mixed_case_function_normalizes_name() {
2180 let mut input = MathInput::new();
2181 input.handle_char('S');
2182 input.handle_char('i');
2183 input.handle_char('N');
2184 input.handle_char('(');
2185
2186 if let MathBox::Row(items) = &input.root {
2187 assert!(matches!(&items[0], MathBox::Func { name, .. } if name == "sin"));
2188 } else {
2189 panic!("Expected row root");
2190 }
2191 }
2192
2193 #[test]
2194 fn test_backspace_at_exponent_start_deletes_base() {
2195 let mut input = MathInput::new();
2196 input.handle_char('2');
2197 input.handle_char('^');
2198 input.handle_char('3');
2199
2200 input.handle_key(SpecialKey::Left);
2201 assert_eq!(input.cursor_path(), &[0, 1]);
2202 assert_eq!(input.cursor_offset(), 0);
2203
2204 input.handle_key(SpecialKey::Backspace);
2205 assert_eq!(input.cursor_path(), &[0, 0]);
2206 assert_eq!(input.cursor_offset(), 0);
2207
2208 if let MathBox::Row(items) = &input.root {
2209 if let MathBox::Power { base, exp } = &items[0] {
2210 assert!(matches!(base.as_ref(), MathBox::Slot));
2211 assert!(matches!(exp.as_ref(), MathBox::Number(s) if s == "3"));
2212 } else {
2213 panic!("Expected Power");
2214 }
2215 }
2216 }
2217
2218 #[test]
2219 fn test_delete_at_base_end_deletes_exponent_prefix() {
2220 let mut input = MathInput::new();
2221 input.handle_char('2');
2222 input.handle_char('^');
2223 input.handle_char('3');
2224 input.handle_char('4');
2225
2226 input.handle_key(SpecialKey::Down);
2227 assert_eq!(input.cursor_path(), &[0, 0]);
2228 input.handle_key(SpecialKey::Right);
2229 assert_eq!(input.cursor_offset(), 1);
2230
2231 input.handle_key(SpecialKey::Delete);
2232 assert_eq!(input.cursor_path(), &[0, 0]);
2233 assert_eq!(input.cursor_offset(), 1);
2234
2235 if let MathBox::Row(items) = &input.root {
2236 if let MathBox::Power { exp, .. } = &items[0] {
2237 assert!(matches!(exp.as_ref(), MathBox::Number(s) if s == "4"));
2238 } else {
2239 panic!("Expected Power");
2240 }
2241 }
2242 }
2243
2244 #[test]
2245 fn test_backspace_from_denominator_slot_targets_numerator() {
2246 let mut input = MathInput::new();
2247 run_command(&mut input, "frac");
2248 input.handle_char('1');
2249
2250 input.handle_key(SpecialKey::Tab);
2251 assert_eq!(input.cursor_path(), &[0, 1]);
2252 assert!(matches!(input.get_current(), Some(MathBox::Slot)));
2253
2254 input.handle_key(SpecialKey::Backspace);
2255 assert_eq!(input.cursor_path(), &[0, 0]);
2256
2257 if let MathBox::Row(items) = &input.root {
2258 if let MathBox::Fraction { num, den } = &items[0] {
2259 assert!(matches!(num.as_ref(), MathBox::Slot));
2260 assert!(matches!(den.as_ref(), MathBox::Slot));
2261 } else {
2262 panic!("Expected Fraction");
2263 }
2264 }
2265 }
2266
2267 #[test]
2268 fn test_delete_from_numerator_slot_targets_denominator() {
2269 let mut input = MathInput::new();
2270 run_command(&mut input, "frac");
2271
2272 input.handle_key(SpecialKey::Tab);
2273 input.handle_char('5');
2274 input.handle_key(SpecialKey::ShiftTab);
2275 assert_eq!(input.cursor_path(), &[0, 0]);
2276
2277 input.handle_key(SpecialKey::Delete);
2278 assert_eq!(input.cursor_path(), &[0, 0]);
2279
2280 if let MathBox::Row(items) = &input.root {
2281 if let MathBox::Fraction { num, den } = &items[0] {
2282 assert!(matches!(num.as_ref(), MathBox::Slot));
2283 assert!(matches!(den.as_ref(), MathBox::Slot));
2284 } else {
2285 panic!("Expected Fraction");
2286 }
2287 }
2288 }
2289
2290 #[test]
2291 fn test_backspace_on_container_replaces_with_slot() {
2292 let mut input = MathInput::new();
2293 input.handle_char('(');
2294 input.handle_char('x');
2295 input.handle_char(')');
2296 assert_eq!(input.cursor_path(), &[0]);
2297
2298 input.handle_key(SpecialKey::Backspace);
2299 assert_eq!(input.cursor_path(), &[0]);
2300
2301 if let MathBox::Row(items) = &input.root {
2302 assert!(matches!(&items[0], MathBox::Slot));
2303 } else {
2304 panic!("Expected Row");
2305 }
2306 }
2307
2308 #[test]
2309 fn test_backspace_clears_empty_sum_template() {
2310 let mut input = MathInput::new();
2311 run_command(&mut input, "sum");
2312
2313 assert_eq!(input.cursor_path(), &[0, 1]);
2314 input.handle_key(SpecialKey::Backspace);
2315 assert_eq!(input.cursor_path(), &[0, 0]);
2316
2317 input.handle_key(SpecialKey::Backspace);
2318 assert_eq!(input.cursor_path(), &[0]);
2319
2320 if let MathBox::Row(items) = &input.root {
2321 assert!(matches!(&items[0], MathBox::Slot));
2322 } else {
2323 panic!("Expected Row");
2324 }
2325 }
2326
2327 #[test]
2328 fn test_backspace_on_row_slot_removes_current_slot() {
2329 let mut input = MathInput::new();
2330 input.handle_char('2');
2331 input.handle_char('+');
2332 input.handle_char('3');
2333
2334 input.handle_key(SpecialKey::Backspace);
2335 input.handle_key(SpecialKey::Backspace);
2336
2337 assert_eq!(input.cursor_path(), &[1]);
2338 if let MathBox::Row(items) = &input.root {
2339 assert_eq!(items.len(), 2);
2340 assert!(matches!(&items[0], MathBox::Number(s) if s == "2"));
2341 assert!(matches!(&items[1], MathBox::Operator(Operator::Add)));
2342 } else {
2343 panic!("Expected Row");
2344 }
2345 }
2346 }
2347