Rust · 17556 bytes Raw Blame History
1 //! Structured math input model
2 //!
3 //! MathBox represents mathematical expressions as a tree structure
4 //! with navigable slots for user input.
5
6 use serde::{Deserialize, Serialize};
7
8 /// Structured math input with navigation slots
9 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10 pub enum MathBox {
11 /// Numeric literal (digits and decimal point)
12 Number(String),
13
14 /// Variable or constant symbol
15 Symbol(String),
16
17 /// Operator symbol (+, -, ×, ÷, =)
18 Operator(Operator),
19
20 /// Fraction with numerator and denominator
21 Fraction {
22 num: Box<MathBox>,
23 den: Box<MathBox>,
24 },
25
26 /// Power/exponent
27 Power {
28 base: Box<MathBox>,
29 exp: Box<MathBox>,
30 },
31
32 /// Subscript (for indexed variables like x_1)
33 Subscript {
34 base: Box<MathBox>,
35 sub: Box<MathBox>,
36 },
37
38 /// Square root or nth root
39 Root {
40 /// None for square root, Some for nth root
41 index: Option<Box<MathBox>>,
42 radicand: Box<MathBox>,
43 },
44
45 /// Function call with arguments
46 Func { name: String, args: Vec<MathBox> },
47
48 /// Absolute value
49 Abs(Box<MathBox>),
50
51 /// Parenthesized expression
52 Parens(Box<MathBox>),
53
54 /// Integral
55 Integral {
56 /// Lower bound (for definite integral)
57 lower: Option<Box<MathBox>>,
58 /// Upper bound (for definite integral)
59 upper: Option<Box<MathBox>>,
60 /// The integrand
61 body: Box<MathBox>,
62 /// The variable of integration
63 var: Box<MathBox>,
64 },
65
66 /// Derivative
67 Derivative {
68 /// Order of derivative (1 = first, 2 = second, etc.)
69 order: u32,
70 /// Variable to differentiate with respect to
71 var: Box<MathBox>,
72 /// Expression to differentiate
73 body: Box<MathBox>,
74 },
75
76 /// Limit
77 Limit {
78 /// Variable approaching
79 var: Box<MathBox>,
80 /// Value being approached
81 to: Box<MathBox>,
82 /// Direction (optional: +, -, or none for two-sided)
83 direction: Option<LimitDirection>,
84 /// Expression to take limit of
85 body: Box<MathBox>,
86 },
87
88 /// Summation
89 Sum {
90 /// Index variable
91 var: Box<MathBox>,
92 /// Lower bound (e.g., i=0)
93 lower: Box<MathBox>,
94 /// Upper bound (e.g., n)
95 upper: Box<MathBox>,
96 /// Expression to sum
97 body: Box<MathBox>,
98 },
99
100 /// Product (capital Pi)
101 Product {
102 /// Index variable
103 var: Box<MathBox>,
104 /// Lower bound
105 lower: Box<MathBox>,
106 /// Upper bound
107 upper: Box<MathBox>,
108 /// Expression to multiply
109 body: Box<MathBox>,
110 },
111
112 /// Matrix with rows of cells
113 Matrix { rows: Vec<Vec<MathBox>> },
114
115 /// Horizontal sequence of elements (e.g., 2 + 3 × x)
116 Row(Vec<MathBox>),
117
118 /// Empty slot waiting for input (cursor can enter)
119 Slot,
120 }
121
122 /// Mathematical operators
123 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124 pub enum Operator {
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, // ,
136 }
137
138 impl Operator {
139 /// Get the display character for this operator
140 pub fn as_char(&self) -> char {
141 match self {
142 Operator::Add => '+',
143 Operator::Sub => '−',
144 Operator::Mul => '×',
145 Operator::Div => '÷',
146 Operator::Eq => '=',
147 Operator::Lt => '<',
148 Operator::Gt => '>',
149 Operator::Le => '≤',
150 Operator::Ge => '≥',
151 Operator::Ne => '≠',
152 Operator::Comma => ',',
153 }
154 }
155 }
156
157 /// Direction for one-sided limits
158 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
159 pub enum LimitDirection {
160 FromLeft, // x → a⁻
161 FromRight, // x → a⁺
162 }
163
164 /// Cursor position in the MathBox tree
165 #[derive(Debug, Clone, PartialEq, Eq, Default)]
166 pub struct Cursor {
167 /// Path through the tree (indices at each level)
168 pub path: Vec<usize>,
169 /// Position within current element (for Number, Symbol, Row)
170 pub offset: usize,
171 }
172
173 impl Cursor {
174 /// Create a new cursor at the root
175 pub fn new() -> Self {
176 Self::default()
177 }
178
179 /// Get the depth of the cursor in the tree
180 pub fn depth(&self) -> usize {
181 self.path.len()
182 }
183
184 /// Check if cursor is at the root level
185 pub fn is_at_root(&self) -> bool {
186 self.path.is_empty()
187 }
188
189 /// Move cursor into a child slot
190 pub fn enter(&mut self, index: usize) {
191 self.path.push(index);
192 self.offset = 0;
193 }
194
195 /// Move cursor out of current slot to parent
196 pub fn exit(&mut self) -> Option<usize> {
197 self.offset = 0;
198 self.path.pop()
199 }
200 }
201
202 impl MathBox {
203 /// Create an empty slot
204 pub fn slot() -> Self {
205 MathBox::Slot
206 }
207
208 /// Create a fraction template with empty slots
209 pub fn fraction_template() -> Self {
210 MathBox::Fraction {
211 num: Box::new(MathBox::Slot),
212 den: Box::new(MathBox::Slot),
213 }
214 }
215
216 /// Create a power template with the given base
217 pub fn power_template(base: MathBox) -> Self {
218 MathBox::Power {
219 base: Box::new(base),
220 exp: Box::new(MathBox::Slot),
221 }
222 }
223
224 /// Create a square root template
225 pub fn sqrt_template() -> Self {
226 MathBox::Root {
227 index: None,
228 radicand: Box::new(MathBox::Slot),
229 }
230 }
231
232 /// Create an nth root template
233 pub fn nthroot_template() -> Self {
234 MathBox::Root {
235 index: Some(Box::new(MathBox::Slot)),
236 radicand: Box::new(MathBox::Slot),
237 }
238 }
239
240 /// Create an indefinite integral template
241 pub fn integral_template() -> Self {
242 MathBox::Integral {
243 lower: None,
244 upper: None,
245 body: Box::new(MathBox::Slot),
246 var: Box::new(MathBox::Symbol("x".to_string())),
247 }
248 }
249
250 /// Create a definite integral template
251 pub fn definite_integral_template() -> Self {
252 MathBox::Integral {
253 lower: Some(Box::new(MathBox::Slot)),
254 upper: Some(Box::new(MathBox::Slot)),
255 body: Box::new(MathBox::Slot),
256 var: Box::new(MathBox::Symbol("x".to_string())),
257 }
258 }
259
260 /// Create a derivative template
261 pub fn derivative_template() -> Self {
262 MathBox::Derivative {
263 order: 1,
264 var: Box::new(MathBox::Slot),
265 body: Box::new(MathBox::Slot),
266 }
267 }
268
269 /// Create a limit template
270 pub fn limit_template() -> Self {
271 MathBox::Limit {
272 var: Box::new(MathBox::Symbol("x".to_string())),
273 to: Box::new(MathBox::Slot),
274 direction: None,
275 body: Box::new(MathBox::Slot),
276 }
277 }
278
279 /// Create a summation template
280 pub fn sum_template() -> Self {
281 MathBox::Sum {
282 var: Box::new(MathBox::Symbol("i".to_string())),
283 lower: Box::new(MathBox::Slot),
284 upper: Box::new(MathBox::Slot),
285 body: Box::new(MathBox::Slot),
286 }
287 }
288
289 /// Create a product template
290 pub fn product_template() -> Self {
291 MathBox::Product {
292 var: Box::new(MathBox::Symbol("i".to_string())),
293 lower: Box::new(MathBox::Slot),
294 upper: Box::new(MathBox::Slot),
295 body: Box::new(MathBox::Slot),
296 }
297 }
298
299 /// Create a matrix template with given dimensions
300 pub fn matrix_template(rows: usize, cols: usize) -> Self {
301 MathBox::Matrix {
302 rows: (0..rows)
303 .map(|_| (0..cols).map(|_| MathBox::Slot).collect())
304 .collect(),
305 }
306 }
307
308 /// Check if this is an empty slot
309 pub fn is_slot(&self) -> bool {
310 matches!(self, MathBox::Slot)
311 }
312
313 /// Check if this element is empty (slot or empty row/number)
314 pub fn is_empty(&self) -> bool {
315 match self {
316 MathBox::Slot => true,
317 MathBox::Number(s) | MathBox::Symbol(s) => s.is_empty(),
318 MathBox::Row(items) => items.is_empty(),
319 _ => false,
320 }
321 }
322
323 /// Get the number of child slots this element has
324 pub fn child_count(&self) -> usize {
325 match self {
326 MathBox::Number(_) | MathBox::Symbol(_) | MathBox::Operator(_) | MathBox::Slot => 0,
327 MathBox::Fraction { .. } | MathBox::Power { .. } | MathBox::Subscript { .. } => 2,
328 MathBox::Root { index: Some(_), .. } => 2,
329 MathBox::Root { index: None, .. } => 1,
330 MathBox::Func { args, .. } => args.len(),
331 MathBox::Abs(_) | MathBox::Parens(_) => 1,
332 MathBox::Integral { lower, upper, .. } => {
333 2 + if lower.is_some() { 1 } else { 0 } + if upper.is_some() { 1 } else { 0 }
334 }
335 MathBox::Derivative { .. } => 2,
336 MathBox::Limit { .. } => 3,
337 MathBox::Sum { .. } | MathBox::Product { .. } => 4,
338 MathBox::Matrix { rows } => rows.iter().map(|r| r.len()).sum(),
339 MathBox::Row(items) => items.len(),
340 }
341 }
342
343 /// Get a mutable reference to a child by index
344 pub fn child_mut(&mut self, index: usize) -> Option<&mut MathBox> {
345 match self {
346 MathBox::Fraction { num, den } => match index {
347 0 => Some(num),
348 1 => Some(den),
349 _ => None,
350 },
351 MathBox::Power { base, exp } => match index {
352 0 => Some(base),
353 1 => Some(exp),
354 _ => None,
355 },
356 MathBox::Subscript { base, sub } => match index {
357 0 => Some(base),
358 1 => Some(sub),
359 _ => None,
360 },
361 MathBox::Root {
362 index: idx,
363 radicand,
364 } => {
365 if let Some(i) = idx {
366 match index {
367 0 => Some(i),
368 1 => Some(radicand),
369 _ => None,
370 }
371 } else {
372 match index {
373 0 => Some(radicand),
374 _ => None,
375 }
376 }
377 }
378 MathBox::Func { args, .. } => args.get_mut(index),
379 MathBox::Abs(inner) | MathBox::Parens(inner) => match index {
380 0 => Some(inner),
381 _ => None,
382 },
383 MathBox::Integral {
384 lower,
385 upper,
386 body,
387 var,
388 } => {
389 let mut i = 0;
390 if let Some(l) = lower {
391 if index == i {
392 return Some(l);
393 }
394 i += 1;
395 }
396 if let Some(u) = upper {
397 if index == i {
398 return Some(u);
399 }
400 i += 1;
401 }
402 if index == i {
403 return Some(body);
404 }
405 if index == i + 1 {
406 return Some(var);
407 }
408 None
409 }
410 MathBox::Derivative { var, body, .. } => match index {
411 0 => Some(var),
412 1 => Some(body),
413 _ => None,
414 },
415 MathBox::Limit { var, to, body, .. } => match index {
416 0 => Some(var),
417 1 => Some(to),
418 2 => Some(body),
419 _ => None,
420 },
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 {
433 0 => Some(var),
434 1 => Some(lower),
435 2 => Some(upper),
436 3 => Some(body),
437 _ => None,
438 },
439 MathBox::Matrix { rows } => {
440 let mut idx = 0;
441 for row in rows.iter_mut() {
442 for cell in row.iter_mut() {
443 if idx == index {
444 return Some(cell);
445 }
446 idx += 1;
447 }
448 }
449 None
450 }
451 MathBox::Row(items) => items.get_mut(index),
452 _ => None,
453 }
454 }
455
456 /// Get an immutable reference to a child by index
457 pub fn child(&self, index: usize) -> Option<&MathBox> {
458 match self {
459 MathBox::Fraction { num, den } => match index {
460 0 => Some(num),
461 1 => Some(den),
462 _ => None,
463 },
464 MathBox::Power { base, exp } => match index {
465 0 => Some(base),
466 1 => Some(exp),
467 _ => None,
468 },
469 MathBox::Subscript { base, sub } => match index {
470 0 => Some(base),
471 1 => Some(sub),
472 _ => None,
473 },
474 MathBox::Root {
475 index: idx,
476 radicand,
477 } => {
478 if let Some(i) = idx {
479 match index {
480 0 => Some(i),
481 1 => Some(radicand),
482 _ => None,
483 }
484 } else {
485 match index {
486 0 => Some(radicand),
487 _ => None,
488 }
489 }
490 }
491 MathBox::Func { args, .. } => args.get(index),
492 MathBox::Abs(inner) | MathBox::Parens(inner) => match index {
493 0 => Some(inner),
494 _ => None,
495 },
496 MathBox::Integral {
497 lower,
498 upper,
499 body,
500 var,
501 } => {
502 let mut i = 0;
503 if let Some(l) = lower {
504 if index == i {
505 return Some(l);
506 }
507 i += 1;
508 }
509 if let Some(u) = upper {
510 if index == i {
511 return Some(u);
512 }
513 i += 1;
514 }
515 if index == i {
516 return Some(body);
517 }
518 if index == i + 1 {
519 return Some(var);
520 }
521 None
522 }
523 MathBox::Derivative { var, body, .. } => match index {
524 0 => Some(var),
525 1 => Some(body),
526 _ => None,
527 },
528 MathBox::Limit { var, to, body, .. } => match index {
529 0 => Some(var),
530 1 => Some(to),
531 2 => Some(body),
532 _ => None,
533 },
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 {
546 0 => Some(var),
547 1 => Some(lower),
548 2 => Some(upper),
549 3 => Some(body),
550 _ => None,
551 },
552 MathBox::Matrix { rows } => {
553 let mut idx = 0;
554 for row in rows.iter() {
555 for cell in row.iter() {
556 if idx == index {
557 return Some(cell);
558 }
559 idx += 1;
560 }
561 }
562 None
563 }
564 MathBox::Row(items) => items.get(index),
565 _ => None,
566 }
567 }
568 }
569
570 #[cfg(test)]
571 mod tests {
572 use super::*;
573
574 #[test]
575 fn test_fraction_template() {
576 let frac = MathBox::fraction_template();
577 if let MathBox::Fraction { num, den } = frac {
578 assert!(num.is_slot());
579 assert!(den.is_slot());
580 } else {
581 panic!("Expected Fraction");
582 }
583 }
584
585 #[test]
586 fn test_cursor_navigation() {
587 let mut cursor = Cursor::new();
588 assert!(cursor.is_at_root());
589 assert_eq!(cursor.depth(), 0);
590
591 cursor.enter(0);
592 assert!(!cursor.is_at_root());
593 assert_eq!(cursor.depth(), 1);
594
595 cursor.enter(1);
596 assert_eq!(cursor.depth(), 2);
597
598 assert_eq!(cursor.exit(), Some(1));
599 assert_eq!(cursor.depth(), 1);
600 }
601
602 #[test]
603 fn test_child_access() {
604 let mut frac = MathBox::Fraction {
605 num: Box::new(MathBox::Number("1".to_string())),
606 den: Box::new(MathBox::Number("2".to_string())),
607 };
608
609 assert_eq!(frac.child_count(), 2);
610
611 if let Some(MathBox::Number(s)) = frac.child(0) {
612 assert_eq!(s, "1");
613 }
614
615 if let Some(num) = frac.child_mut(0) {
616 *num = MathBox::Number("42".to_string());
617 }
618
619 if let Some(MathBox::Number(s)) = frac.child(0) {
620 assert_eq!(s, "42");
621 }
622 }
623 }
624