gardesk/garcalc / 3cd3071

Browse files

add calculator button grid with extended mode toggle

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
3cd307143666a2777067eb3b88776cac8f25335c
Parents
4afec87
Tree
10e20dd

6 changed files

StatusFile+-
M garcalc-cas/src/eval.rs 2 10
M garcalc-cas/src/expr.rs 1 5
M garcalc-math/src/input.rs 347 1
M garcalc-math/src/render.rs 29 3
M garcalc/src/app.rs 118 3
M garcalc/src/ui.rs 602 3
garcalc-cas/src/eval.rsmodified
@@ -1495,11 +1495,7 @@ impl Evaluator {
14951495
                         _ => return None,
14961496
                     }
14971497
                 }
1498
-                if saw_var {
1499
-                    Some(shift)
1500
-                } else {
1501
-                    None
1502
-                }
1498
+                if saw_var { Some(shift) } else { None }
15031499
             }
15041500
             _ => None,
15051501
         }
@@ -1638,11 +1634,7 @@ fn factorial(n: u64) -> u64 {
16381634
 
16391635
 fn gcd(a: i64, b: i64) -> i64 {
16401636
     let (a, b) = (a.abs(), b.abs());
1641
-    if b == 0 {
1642
-        a
1643
-    } else {
1644
-        gcd(b, a % b)
1645
-    }
1637
+    if b == 0 { a } else { gcd(b, a % b) }
16461638
 }
16471639
 
16481640
 fn lcm(a: i64, b: i64) -> i64 {
garcalc-cas/src/expr.rsmodified
@@ -75,11 +75,7 @@ impl fmt::Display for Rational {
7575
 }
7676
 
7777
 fn gcd(a: i64, b: i64) -> i64 {
78
-    if b == 0 {
79
-        a
80
-    } else {
81
-        gcd(b, a % b)
82
-    }
78
+    if b == 0 { a } else { gcd(b, a % b) }
8379
 }
8480
 
8581
 /// Mathematical expression AST
garcalc-math/src/input.rsmodified
@@ -121,6 +121,7 @@ impl MathInput {
121121
                 InputResult::Consumed
122122
             }
123123
             '/' => {
124
+                self.maybe_promote_out_of_script();
124125
                 // Insert fraction
125126
                 self.insert_template(MathBox::fraction_template());
126127
                 InputResult::Consumed
@@ -158,30 +159,37 @@ impl MathInput {
158159
                 InputResult::Consumed
159160
             }
160161
             '+' => {
162
+                self.maybe_promote_out_of_script();
161163
                 self.insert_at_cursor(MathBox::Operator(Operator::Add));
162164
                 InputResult::Consumed
163165
             }
164166
             '-' => {
167
+                self.maybe_promote_out_of_script();
165168
                 self.insert_at_cursor(MathBox::Operator(Operator::Sub));
166169
                 InputResult::Consumed
167170
             }
168171
             '*' => {
172
+                self.maybe_promote_out_of_script();
169173
                 self.insert_at_cursor(MathBox::Operator(Operator::Mul));
170174
                 InputResult::Consumed
171175
             }
172176
             '=' => {
177
+                self.maybe_promote_out_of_script();
173178
                 self.insert_at_cursor(MathBox::Operator(Operator::Eq));
174179
                 InputResult::Consumed
175180
             }
176181
             '<' => {
182
+                self.maybe_promote_out_of_script();
177183
                 self.insert_at_cursor(MathBox::Operator(Operator::Lt));
178184
                 InputResult::Consumed
179185
             }
180186
             '>' => {
187
+                self.maybe_promote_out_of_script();
181188
                 self.insert_at_cursor(MathBox::Operator(Operator::Gt));
182189
                 InputResult::Consumed
183190
             }
184191
             ',' => {
192
+                self.maybe_promote_out_of_script();
185193
                 self.insert_at_cursor(MathBox::Operator(Operator::Comma));
186194
                 InputResult::Consumed
187195
             }
@@ -351,6 +359,42 @@ impl MathInput {
351359
 
352360
     /// Wrap the current element in a power
353361
     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
+
354398
         if let Some(current) = self.get_current_mut() {
355399
             if !current.is_slot() {
356400
                 let base = std::mem::replace(current, MathBox::Slot);
@@ -373,6 +417,42 @@ impl MathInput {
373417
 
374418
     /// Wrap the current element in a subscript
375419
     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
+
376456
         if let Some(current) = self.get_current_mut() {
377457
             if !current.is_slot() {
378458
                 let base = std::mem::replace(current, MathBox::Slot);
@@ -393,6 +473,54 @@ impl MathInput {
393473
 
394474
     /// Wrap the current element in factorial
395475
     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
+
396524
         if let Some(current) = self.get_current_mut() {
397525
             if !current.is_slot() {
398526
                 let arg = std::mem::replace(current, MathBox::Slot);
@@ -610,6 +738,43 @@ impl MathInput {
610738
         }
611739
     }
612740
 
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
+
613778
     /// Delete at cursor
614779
     fn delete_at_cursor(&mut self) {
615780
         let path = self.cursor.path.clone();
@@ -742,6 +907,18 @@ impl MathInput {
742907
 
743908
     /// Move cursor left
744909
     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
+
745922
         if let Some(current) = self.get_current() {
746923
             if matches!(current, MathBox::Number(_) | MathBox::Symbol(_)) && self.cursor.offset > 0
747924
             {
@@ -764,6 +941,17 @@ impl MathInput {
764941
 
765942
     /// Move cursor right
766943
     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
+
767955
         if let Some(current) = self.get_current() {
768956
             match current {
769957
                 MathBox::Number(s) | MathBox::Symbol(s)
@@ -772,6 +960,9 @@ impl MathInput {
772960
                     self.cursor.offset += 1;
773961
                     return;
774962
                 }
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(_) => {}
775966
                 _ if current.child_count() > 0 => {
776967
                     self.cursor.enter(0);
777968
                     return;
@@ -780,7 +971,8 @@ impl MathInput {
780971
             }
781972
         }
782973
 
783
-        let mut path = self.cursor.path.clone();
974
+        let original_path = self.cursor.path.clone();
975
+        let mut path = original_path.clone();
784976
         while let Some(idx) = path.pop() {
785977
             if let Some(parent) = Self::get_node_at_path(&self.root, &path) {
786978
                 if idx + 1 < parent.child_count() {
@@ -791,6 +983,11 @@ impl MathInput {
791983
                 }
792984
             }
793985
         }
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
+        }
794991
     }
795992
 
796993
     /// Move cursor up (for fractions, powers)
@@ -1083,6 +1280,16 @@ impl MathInput {
10831280
         None
10841281
     }
10851282
 
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
+
10861293
     fn replace_current_with_slot(&mut self, path: &[usize]) {
10871294
         if let Some(current) = Self::get_node_mut_at_path(&mut self.root, path) {
10881295
             *current = MathBox::Slot;
@@ -1387,6 +1594,145 @@ mod tests {
13871594
         }
13881595
     }
13891596
 
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
+
13901736
     #[test]
13911737
     fn test_factorial_input_wraps_current() {
13921738
         let mut input = MathInput::new();
garcalc-math/src/render.rsmodified
@@ -19,6 +19,8 @@ pub struct MathRenderer<'a> {
1919
     pub slot_bg_color: Color,
2020
     /// Slot border color when focused
2121
     pub slot_focus_color: Color,
22
+    /// Insertion cursor color
23
+    pub cursor_color: Color,
2224
     /// Whether the insertion cursor should be drawn
2325
     cursor_visible: Cell<bool>,
2426
 }
@@ -32,6 +34,7 @@ impl<'a> MathRenderer<'a> {
3234
             fg_color: Color::new(0.0, 0.0, 0.0, 1.0),
3335
             slot_bg_color: Color::new(0.9, 0.9, 0.95, 1.0),
3436
             slot_focus_color: Color::new(0.3, 0.5, 0.9, 1.0),
37
+            cursor_color: Color::new(0.12, 0.42, 1.0, 1.0),
3538
             cursor_visible: Cell::new(true),
3639
         }
3740
     }
@@ -105,6 +108,11 @@ impl<'a> MathRenderer<'a> {
105108
             }
106109
             MathBox::Slot => {
107110
                 self.draw_slot(x, y, font_size, is_cursor_here);
111
+                if is_cursor_here && self.cursor_visible.get() {
112
+                    let (slot_width, _, _) = Self::slot_geometry(font_size);
113
+                    let cursor_x = x + slot_width * 0.15;
114
+                    self.draw_cursor(cursor_x, y, font_size);
115
+                }
108116
             }
109117
             MathBox::Fraction { num, den } => {
110118
                 self.render_fraction(num, den, x, y, depth, cursor_path, cursor_offset);
@@ -322,8 +330,8 @@ impl<'a> MathRenderer<'a> {
322330
     /// Draw cursor
323331
     fn draw_cursor(&self, x: f64, y: f64, font_size: f64) {
324332
         self.ctx.save().unwrap();
325
-        self.set_color(&self.slot_focus_color);
326
-        self.ctx.set_line_width(2.0);
333
+        self.set_color(&self.cursor_color);
334
+        self.ctx.set_line_width(1.0);
327335
 
328336
         let (_, height, _) = Self::slot_geometry(font_size);
329337
         self.ctx.move_to(x, y - height * 0.6);
@@ -1386,8 +1394,17 @@ impl<'a> MathRenderer<'a> {
13861394
         cursor_offset: usize,
13871395
     ) {
13881396
         let mut current_x = x;
1397
+        let row_cursor_here = cursor_path.map(|p| p.is_empty()).unwrap_or(false);
1398
+        let row_cursor_idx = cursor_offset.min(items.len());
1399
+        let scale = self.scale_for_depth(depth);
1400
+        let font_size = self.layout_engine.base_font_size * scale;
1401
+        let mut pending_cursor_x = None;
13891402
 
13901403
         for (i, item) in items.iter().enumerate() {
1404
+            if row_cursor_here && i == row_cursor_idx {
1405
+                pending_cursor_x = Some(current_x);
1406
+            }
1407
+
13911408
             let item_cursor = cursor_path.and_then(|p| {
13921409
                 if !p.is_empty() && p[0] == i {
13931410
                     Some(&p[1..])
@@ -1397,9 +1414,18 @@ impl<'a> MathRenderer<'a> {
13971414
             });
13981415
             self.render_at_depth(item, current_x, y, depth, item_cursor, cursor_offset);
13991416
 
1400
-            let layout = self.layout_engine.layout(item, self.ctx);
1417
+            let layout = self.layout_engine.layout_with_depth(item, self.ctx, depth);
14011418
             current_x += layout.width;
14021419
         }
1420
+
1421
+        if row_cursor_here && self.cursor_visible.get() {
1422
+            let cursor_x = if row_cursor_idx == items.len() {
1423
+                current_x
1424
+            } else {
1425
+                pending_cursor_x.unwrap_or(current_x)
1426
+            };
1427
+            self.draw_cursor(cursor_x, y, font_size);
1428
+        }
14031429
     }
14041430
 
14051431
     /// Set the current drawing color
garcalc/src/app.rsmodified
@@ -1,19 +1,19 @@
11
 //! Application state and event loop
22
 
33
 use anyhow::Result;
4
-use garcalc_cas::{parser, Evaluator};
4
+use garcalc_cas::{Evaluator, parser};
55
 use garcalc_graph::{Graph2D, Graph3D};
66
 use garcalc_ipc::Mode;
77
 use garcalc_math::input::SpecialKey;
88
 use garcalc_math::{
9
-    from_expr, to_expr, ConvertError, InputResult as MathInputResult, MathBox, MathInput,
9
+    ConvertError, InputResult as MathInputResult, MathBox, MathInput, from_expr, to_expr,
1010
 };
1111
 use gartk_core::{InputEvent, Key, Modifiers, MouseButton};
1212
 use gartk_x11::{Connection, EventLoop, EventLoopConfig, Window, WindowConfig};
1313
 use std::time::{Duration, Instant};
1414
 
1515
 use crate::config::Config;
16
-use crate::ui::CalculatorUI;
16
+use crate::ui::{CalcButtonAction, CalculatorUI};
1717
 
1818
 /// Calculator entry (input + result)
1919
 #[derive(Debug, Clone)]
@@ -55,6 +55,8 @@ pub struct App {
5555
     should_quit: bool,
5656
     /// Whether the help modal overlay is open
5757
     help_modal_open: bool,
58
+    /// Whether calculator buttons are in extended mode
59
+    calc_buttons_extended: bool,
5860
     /// Whether the cursor is currently visible (blink state)
5961
     cursor_visible: bool,
6062
     /// Last time the cursor blink state toggled
@@ -129,6 +131,7 @@ impl App {
129131
             popup_mode: popup,
130132
             should_quit: false,
131133
             help_modal_open: false,
134
+            calc_buttons_extended: false,
132135
             cursor_visible: true,
133136
             last_cursor_blink: Instant::now(),
134137
             has_focus: false,
@@ -168,6 +171,18 @@ impl App {
168171
                             self.help_modal_open = false;
169172
                             ev.request_redraw();
170173
                         }
174
+                    } else if self.mode == Mode::Calculator {
175
+                        if mouse_ev.button == Some(MouseButton::Left) {
176
+                            if let Some(action) = self.ui.calculator_button_action_at(
177
+                                x,
178
+                                y,
179
+                                self.calc_buttons_extended,
180
+                            ) {
181
+                                self.handle_calculator_button_action(action);
182
+                                self.reset_cursor_blink();
183
+                                ev.request_redraw();
184
+                            }
185
+                        }
171186
                     } else if self.mode == Mode::Graph {
172187
                         if mouse_ev.button == Some(MouseButton::Left) {
173188
                             // Left click - start drag for pan
@@ -525,6 +540,105 @@ impl App {
525540
         }
526541
     }
527542
 
543
+    fn run_math_command(&mut self, cmd: &str) -> bool {
544
+        let mut changed = false;
545
+        if !matches!(self.math_input.handle_char('\\'), MathInputResult::Ignored) {
546
+            changed = true;
547
+        }
548
+        for ch in cmd.chars() {
549
+            if !matches!(self.math_input.handle_char(ch), MathInputResult::Ignored) {
550
+                changed = true;
551
+            }
552
+        }
553
+        if !matches!(self.math_input.handle_char(' '), MathInputResult::Ignored) {
554
+            changed = true;
555
+        }
556
+        changed
557
+    }
558
+
559
+    fn insert_math_text(&mut self, text: &str) -> bool {
560
+        let mut changed = false;
561
+        for ch in text.chars() {
562
+            if !matches!(self.math_input.handle_char(ch), MathInputResult::Ignored) {
563
+                changed = true;
564
+            }
565
+        }
566
+        changed
567
+    }
568
+
569
+    fn handle_calculator_button_action(&mut self, action: CalcButtonAction) {
570
+        match action {
571
+            CalcButtonAction::ToggleExtended => {
572
+                self.calc_buttons_extended = !self.calc_buttons_extended;
573
+            }
574
+            CalcButtonAction::Evaluate => {
575
+                self.evaluate();
576
+            }
577
+            CalcButtonAction::Clear => {
578
+                self.math_input.clear();
579
+                self.history_index = None;
580
+                self.calc_history_index = None;
581
+            }
582
+            CalcButtonAction::Backspace => {
583
+                if !matches!(
584
+                    self.math_input.handle_key(SpecialKey::Backspace),
585
+                    MathInputResult::Ignored
586
+                ) {
587
+                    self.history_index = None;
588
+                    self.calc_history_index = None;
589
+                }
590
+            }
591
+            CalcButtonAction::Delete => {
592
+                if !matches!(
593
+                    self.math_input.handle_key(SpecialKey::Delete),
594
+                    MathInputResult::Ignored
595
+                ) {
596
+                    self.history_index = None;
597
+                    self.calc_history_index = None;
598
+                }
599
+            }
600
+            CalcButtonAction::MoveLeft => {
601
+                if !matches!(
602
+                    self.math_input.handle_key(SpecialKey::Left),
603
+                    MathInputResult::Ignored
604
+                ) {
605
+                    self.history_index = None;
606
+                    self.calc_history_index = None;
607
+                }
608
+            }
609
+            CalcButtonAction::MoveRight => {
610
+                if !matches!(
611
+                    self.math_input.handle_key(SpecialKey::Right),
612
+                    MathInputResult::Ignored
613
+                ) {
614
+                    self.history_index = None;
615
+                    self.calc_history_index = None;
616
+                }
617
+            }
618
+            CalcButtonAction::Tab => {
619
+                if !matches!(
620
+                    self.math_input.handle_key(SpecialKey::Tab),
621
+                    MathInputResult::Ignored
622
+                ) {
623
+                    self.history_index = None;
624
+                    self.calc_history_index = None;
625
+                }
626
+            }
627
+            CalcButtonAction::InsertText(text) => {
628
+                if self.insert_math_text(text) {
629
+                    self.history_index = None;
630
+                    self.calc_history_index = None;
631
+                }
632
+            }
633
+            CalcButtonAction::Command(cmd) => {
634
+                if self.run_math_command(cmd) {
635
+                    self.history_index = None;
636
+                    self.calc_history_index = None;
637
+                }
638
+            }
639
+        }
640
+    }
641
+
528642
     fn is_blank_math_input(&self) -> bool {
529643
         match self.math_input.mathbox() {
530644
             MathBox::Row(items) => {
@@ -937,6 +1051,7 @@ impl App {
9371051
             &self.graph,
9381052
             &self.graph3d,
9391053
             self.help_modal_open,
1054
+            self.calc_buttons_extended,
9401055
         )?;
9411056
         Ok(())
9421057
     }
garcalc/src/ui.rsmodified
@@ -21,6 +21,53 @@ pub struct CalculatorUI {
2121
     gc: u32,
2222
 }
2323
 
24
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25
+pub enum CalcButtonAction {
26
+    InsertText(&'static str),
27
+    Command(&'static str),
28
+    Backspace,
29
+    Delete,
30
+    MoveLeft,
31
+    MoveRight,
32
+    Tab,
33
+    Clear,
34
+    Evaluate,
35
+    ToggleExtended,
36
+}
37
+
38
+#[derive(Debug, Clone, Copy)]
39
+enum CalcButtonRole {
40
+    Numeric,
41
+    Operator,
42
+    Function,
43
+    Command,
44
+    Control,
45
+    Evaluate,
46
+}
47
+
48
+#[derive(Debug, Clone, Copy)]
49
+struct CalcButtonSpec {
50
+    label: &'static str,
51
+    action: CalcButtonAction,
52
+    role: CalcButtonRole,
53
+}
54
+
55
+#[derive(Debug, Clone, Copy)]
56
+struct CalcButtonRender {
57
+    rect: Rect,
58
+    label: &'static str,
59
+    action: CalcButtonAction,
60
+    role: CalcButtonRole,
61
+}
62
+
63
+#[derive(Debug, Clone)]
64
+struct CalcButtonLayout {
65
+    panel_rect: Rect,
66
+    toggle_rect: Option<Rect>,
67
+    buttons: Vec<CalcButtonRender>,
68
+    hidden_optional_rows: usize,
69
+}
70
+
2471
 impl CalculatorUI {
2572
     const HELP_BUTTON_SIZE: u32 = 28;
2673
 
@@ -71,6 +118,7 @@ impl CalculatorUI {
71118
         graph: &Graph2D,
72119
         graph3d: &Graph3D,
73120
         show_help_modal: bool,
121
+        calc_buttons_extended: bool,
74122
     ) -> Result<()> {
75123
         let size = self.renderer.size();
76124
 
@@ -89,14 +137,27 @@ impl CalculatorUI {
89137
             // Mode indicator
90138
             self.draw_mode_indicator(mode)?;
91139
 
140
+            let input_height: i32 = if math_input.is_some() { 88 } else { 50 };
141
+            let input_y = size.height as i32 - input_height - 10;
142
+            let button_layout = if math_input.is_some() {
143
+                self.calculator_button_layout(input_y, calc_buttons_extended)
144
+            } else {
145
+                None
146
+            };
147
+
92148
             // History area
93149
             let history_start_y = 40;
94
-            let input_height: i32 = if math_input.is_some() { 88 } else { 50 };
95
-            let history_end_y = size.height as i32 - input_height - 20;
150
+            let history_end_y = button_layout
151
+                .as_ref()
152
+                .map(|layout| layout.panel_rect.y - 10)
153
+                .unwrap_or(size.height as i32 - input_height - 20);
96154
             self.draw_history(history, history_start_y, history_end_y)?;
97155
 
156
+            if let Some(layout) = button_layout.as_ref() {
157
+                self.draw_calculator_buttons(layout, calc_buttons_extended)?;
158
+            }
159
+
98160
             // Input area
99
-            let input_y = size.height as i32 - input_height - 10;
100161
             if let Some(math_input) = math_input {
101162
                 self.draw_math_input(math_input, cursor_visible, input_y, input_height as u32)?;
102163
             } else {
@@ -130,6 +191,30 @@ impl CalculatorUI {
130191
             .contains_point(Point::new(x as i32, y as i32))
131192
     }
132193
 
194
+    pub fn calculator_button_action_at(
195
+        &self,
196
+        x: f64,
197
+        y: f64,
198
+        calc_buttons_extended: bool,
199
+    ) -> Option<CalcButtonAction> {
200
+        let size = self.renderer.size();
201
+        let input_y = size.height as i32 - 88 - 10;
202
+        let layout = self.calculator_button_layout(input_y, calc_buttons_extended)?;
203
+        let point = Point::new(x as i32, y as i32);
204
+
205
+        if let Some(toggle) = layout.toggle_rect {
206
+            if toggle.contains_point(point) {
207
+                return Some(CalcButtonAction::ToggleExtended);
208
+            }
209
+        }
210
+
211
+        layout
212
+            .buttons
213
+            .iter()
214
+            .find(|button| button.rect.contains_point(point))
215
+            .map(|button| button.action)
216
+    }
217
+
133218
     fn render_graph_mode(
134219
         &mut self,
135220
         input: &str,
@@ -335,6 +420,517 @@ impl CalculatorUI {
335420
         Ok(())
336421
     }
337422
 
423
+    fn calc_core_rows() -> Vec<Vec<CalcButtonSpec>> {
424
+        vec![
425
+            vec![
426
+                CalcButtonSpec {
427
+                    label: "7",
428
+                    action: CalcButtonAction::InsertText("7"),
429
+                    role: CalcButtonRole::Numeric,
430
+                },
431
+                CalcButtonSpec {
432
+                    label: "8",
433
+                    action: CalcButtonAction::InsertText("8"),
434
+                    role: CalcButtonRole::Numeric,
435
+                },
436
+                CalcButtonSpec {
437
+                    label: "9",
438
+                    action: CalcButtonAction::InsertText("9"),
439
+                    role: CalcButtonRole::Numeric,
440
+                },
441
+                CalcButtonSpec {
442
+                    label: "/",
443
+                    action: CalcButtonAction::InsertText("/"),
444
+                    role: CalcButtonRole::Operator,
445
+                },
446
+                CalcButtonSpec {
447
+                    label: "(",
448
+                    action: CalcButtonAction::InsertText("("),
449
+                    role: CalcButtonRole::Operator,
450
+                },
451
+                CalcButtonSpec {
452
+                    label: ")",
453
+                    action: CalcButtonAction::InsertText(")"),
454
+                    role: CalcButtonRole::Operator,
455
+                },
456
+            ],
457
+            vec![
458
+                CalcButtonSpec {
459
+                    label: "4",
460
+                    action: CalcButtonAction::InsertText("4"),
461
+                    role: CalcButtonRole::Numeric,
462
+                },
463
+                CalcButtonSpec {
464
+                    label: "5",
465
+                    action: CalcButtonAction::InsertText("5"),
466
+                    role: CalcButtonRole::Numeric,
467
+                },
468
+                CalcButtonSpec {
469
+                    label: "6",
470
+                    action: CalcButtonAction::InsertText("6"),
471
+                    role: CalcButtonRole::Numeric,
472
+                },
473
+                CalcButtonSpec {
474
+                    label: "*",
475
+                    action: CalcButtonAction::InsertText("*"),
476
+                    role: CalcButtonRole::Operator,
477
+                },
478
+                CalcButtonSpec {
479
+                    label: "x",
480
+                    action: CalcButtonAction::InsertText("x"),
481
+                    role: CalcButtonRole::Function,
482
+                },
483
+                CalcButtonSpec {
484
+                    label: "y",
485
+                    action: CalcButtonAction::InsertText("y"),
486
+                    role: CalcButtonRole::Function,
487
+                },
488
+            ],
489
+            vec![
490
+                CalcButtonSpec {
491
+                    label: "1",
492
+                    action: CalcButtonAction::InsertText("1"),
493
+                    role: CalcButtonRole::Numeric,
494
+                },
495
+                CalcButtonSpec {
496
+                    label: "2",
497
+                    action: CalcButtonAction::InsertText("2"),
498
+                    role: CalcButtonRole::Numeric,
499
+                },
500
+                CalcButtonSpec {
501
+                    label: "3",
502
+                    action: CalcButtonAction::InsertText("3"),
503
+                    role: CalcButtonRole::Numeric,
504
+                },
505
+                CalcButtonSpec {
506
+                    label: "-",
507
+                    action: CalcButtonAction::InsertText("-"),
508
+                    role: CalcButtonRole::Operator,
509
+                },
510
+                CalcButtonSpec {
511
+                    label: "^",
512
+                    action: CalcButtonAction::InsertText("^"),
513
+                    role: CalcButtonRole::Operator,
514
+                },
515
+                CalcButtonSpec {
516
+                    label: "!",
517
+                    action: CalcButtonAction::InsertText("!"),
518
+                    role: CalcButtonRole::Operator,
519
+                },
520
+            ],
521
+            vec![
522
+                CalcButtonSpec {
523
+                    label: "0",
524
+                    action: CalcButtonAction::InsertText("0"),
525
+                    role: CalcButtonRole::Numeric,
526
+                },
527
+                CalcButtonSpec {
528
+                    label: ".",
529
+                    action: CalcButtonAction::InsertText("."),
530
+                    role: CalcButtonRole::Numeric,
531
+                },
532
+                CalcButtonSpec {
533
+                    label: ",",
534
+                    action: CalcButtonAction::InsertText(","),
535
+                    role: CalcButtonRole::Operator,
536
+                },
537
+                CalcButtonSpec {
538
+                    label: "+",
539
+                    action: CalcButtonAction::InsertText("+"),
540
+                    role: CalcButtonRole::Operator,
541
+                },
542
+                CalcButtonSpec {
543
+                    label: "π",
544
+                    action: CalcButtonAction::InsertText("pi"),
545
+                    role: CalcButtonRole::Function,
546
+                },
547
+                CalcButtonSpec {
548
+                    label: "=",
549
+                    action: CalcButtonAction::Evaluate,
550
+                    role: CalcButtonRole::Evaluate,
551
+                },
552
+            ],
553
+        ]
554
+    }
555
+
556
+    fn calc_scientific_rows() -> Vec<Vec<CalcButtonSpec>> {
557
+        vec![
558
+            vec![
559
+                CalcButtonSpec {
560
+                    label: "sin(",
561
+                    action: CalcButtonAction::InsertText("sin("),
562
+                    role: CalcButtonRole::Function,
563
+                },
564
+                CalcButtonSpec {
565
+                    label: "cos(",
566
+                    action: CalcButtonAction::InsertText("cos("),
567
+                    role: CalcButtonRole::Function,
568
+                },
569
+                CalcButtonSpec {
570
+                    label: "tan(",
571
+                    action: CalcButtonAction::InsertText("tan("),
572
+                    role: CalcButtonRole::Function,
573
+                },
574
+                CalcButtonSpec {
575
+                    label: "ln(",
576
+                    action: CalcButtonAction::InsertText("ln("),
577
+                    role: CalcButtonRole::Function,
578
+                },
579
+                CalcButtonSpec {
580
+                    label: "log(",
581
+                    action: CalcButtonAction::InsertText("log("),
582
+                    role: CalcButtonRole::Function,
583
+                },
584
+                CalcButtonSpec {
585
+                    label: "√",
586
+                    action: CalcButtonAction::InsertText("sqrt("),
587
+                    role: CalcButtonRole::Function,
588
+                },
589
+            ],
590
+            vec![
591
+                CalcButtonSpec {
592
+                    label: "sin⁻¹",
593
+                    action: CalcButtonAction::InsertText("asin("),
594
+                    role: CalcButtonRole::Function,
595
+                },
596
+                CalcButtonSpec {
597
+                    label: "cos⁻¹",
598
+                    action: CalcButtonAction::InsertText("acos("),
599
+                    role: CalcButtonRole::Function,
600
+                },
601
+                CalcButtonSpec {
602
+                    label: "tan⁻¹",
603
+                    action: CalcButtonAction::InsertText("atan("),
604
+                    role: CalcButtonRole::Function,
605
+                },
606
+                CalcButtonSpec {
607
+                    label: "eˣ",
608
+                    action: CalcButtonAction::InsertText("exp("),
609
+                    role: CalcButtonRole::Function,
610
+                },
611
+                CalcButtonSpec {
612
+                    label: "abs(",
613
+                    action: CalcButtonAction::InsertText("abs("),
614
+                    role: CalcButtonRole::Function,
615
+                },
616
+                CalcButtonSpec {
617
+                    label: "gamma(",
618
+                    action: CalcButtonAction::InsertText("gamma("),
619
+                    role: CalcButtonRole::Function,
620
+                },
621
+            ],
622
+        ]
623
+    }
624
+
625
+    fn calc_extended_rows() -> Vec<Vec<CalcButtonSpec>> {
626
+        vec![
627
+            vec![
628
+                CalcButtonSpec {
629
+                    label: "a⁄b",
630
+                    action: CalcButtonAction::Command("frac"),
631
+                    role: CalcButtonRole::Command,
632
+                },
633
+                CalcButtonSpec {
634
+                    label: "∑",
635
+                    action: CalcButtonAction::Command("sum"),
636
+                    role: CalcButtonRole::Command,
637
+                },
638
+                CalcButtonSpec {
639
+                    label: "∏",
640
+                    action: CalcButtonAction::Command("prod"),
641
+                    role: CalcButtonRole::Command,
642
+                },
643
+                CalcButtonSpec {
644
+                    label: "∫",
645
+                    action: CalcButtonAction::Command("int"),
646
+                    role: CalcButtonRole::Command,
647
+                },
648
+                CalcButtonSpec {
649
+                    label: "∫ᵇₐ",
650
+                    action: CalcButtonAction::Command("dint"),
651
+                    role: CalcButtonRole::Command,
652
+                },
653
+                CalcButtonSpec {
654
+                    label: "d/dx",
655
+                    action: CalcButtonAction::Command("diff"),
656
+                    role: CalcButtonRole::Command,
657
+                },
658
+            ],
659
+            vec![
660
+                CalcButtonSpec {
661
+                    label: "limₓ→a",
662
+                    action: CalcButtonAction::Command("lim"),
663
+                    role: CalcButtonRole::Command,
664
+                },
665
+                CalcButtonSpec {
666
+                    label: "x=?",
667
+                    action: CalcButtonAction::Command("solve"),
668
+                    role: CalcButtonRole::Command,
669
+                },
670
+                CalcButtonSpec {
671
+                    label: "ⁿ√",
672
+                    action: CalcButtonAction::Command("nthroot"),
673
+                    role: CalcButtonRole::Command,
674
+                },
675
+                CalcButtonSpec {
676
+                    label: "▦",
677
+                    action: CalcButtonAction::Command("matrix"),
678
+                    role: CalcButtonRole::Command,
679
+                },
680
+                CalcButtonSpec {
681
+                    label: "↓min",
682
+                    action: CalcButtonAction::InsertText("min("),
683
+                    role: CalcButtonRole::Function,
684
+                },
685
+                CalcButtonSpec {
686
+                    label: "↑max",
687
+                    action: CalcButtonAction::InsertText("max("),
688
+                    role: CalcButtonRole::Function,
689
+                },
690
+            ],
691
+        ]
692
+    }
693
+
694
+    fn calc_control_row() -> Vec<CalcButtonSpec> {
695
+        vec![
696
+            CalcButtonSpec {
697
+                label: "Bksp",
698
+                action: CalcButtonAction::Backspace,
699
+                role: CalcButtonRole::Control,
700
+            },
701
+            CalcButtonSpec {
702
+                label: "Del",
703
+                action: CalcButtonAction::Delete,
704
+                role: CalcButtonRole::Control,
705
+            },
706
+            CalcButtonSpec {
707
+                label: "Clr",
708
+                action: CalcButtonAction::Clear,
709
+                role: CalcButtonRole::Control,
710
+            },
711
+            CalcButtonSpec {
712
+                label: "<",
713
+                action: CalcButtonAction::MoveLeft,
714
+                role: CalcButtonRole::Control,
715
+            },
716
+            CalcButtonSpec {
717
+                label: ">",
718
+                action: CalcButtonAction::MoveRight,
719
+                role: CalcButtonRole::Control,
720
+            },
721
+            CalcButtonSpec {
722
+                label: "Tab",
723
+                action: CalcButtonAction::Tab,
724
+                role: CalcButtonRole::Control,
725
+            },
726
+        ]
727
+    }
728
+
729
+    fn calculator_button_layout(&self, input_y: i32, extended: bool) -> Option<CalcButtonLayout> {
730
+        let size = self.renderer.size();
731
+        let min_history_height = 90i32;
732
+        let top_reserved = 46i32;
733
+        let max_panel_height = input_y - top_reserved - min_history_height - 8;
734
+        if max_panel_height < 132 {
735
+            return None;
736
+        }
737
+
738
+        let cols = 6i32;
739
+        let gap = 8i32;
740
+        let panel_side_padding = 12i32;
741
+        let panel_margin = 12i32;
742
+        let panel_max_width = size.width as i32 - panel_margin * 2;
743
+        if panel_max_width < 320 {
744
+            return None;
745
+        }
746
+
747
+        let mut button_w = (panel_max_width - panel_side_padding * 2 - (cols - 1) * gap) / cols;
748
+        button_w = button_w.clamp(44, 92);
749
+        let button_h = ((button_w as f64) * 0.58).round() as i32;
750
+        let button_h = button_h.clamp(30, 42);
751
+        let inner_width = cols * button_w + (cols - 1) * gap;
752
+        let panel_width = inner_width + panel_side_padding * 2;
753
+        let panel_x = (size.width as i32 - panel_width) / 2;
754
+
755
+        let mut rows = Vec::new();
756
+        rows.push(Self::calc_control_row());
757
+        rows.extend(Self::calc_core_rows());
758
+
759
+        let mut optional_rows = Self::calc_scientific_rows();
760
+        if extended {
761
+            optional_rows.extend(Self::calc_extended_rows());
762
+        }
763
+
764
+        let toolbar_h = 34i32;
765
+        let row_gap = gap;
766
+        let panel_vertical_padding = 12i32;
767
+        let max_rows_fit = ((max_panel_height - toolbar_h - panel_vertical_padding * 2 + row_gap)
768
+            / (button_h + row_gap))
769
+            .max(0) as usize;
770
+        let required_rows = rows.len();
771
+        if max_rows_fit < required_rows {
772
+            return None;
773
+        }
774
+
775
+        let extra_fit = max_rows_fit - required_rows;
776
+        let optional_visible = optional_rows.len().min(extra_fit);
777
+        let hidden_optional_rows = optional_rows.len().saturating_sub(optional_visible);
778
+        rows.extend(optional_rows.into_iter().take(optional_visible));
779
+
780
+        let row_count = rows.len() as i32;
781
+        let panel_height = panel_vertical_padding * 2
782
+            + toolbar_h
783
+            + row_count * button_h
784
+            + (row_count - 1).max(0) * row_gap;
785
+        let panel_bottom = input_y - 10;
786
+        let panel_y = panel_bottom - panel_height;
787
+
788
+        if panel_y < top_reserved + min_history_height {
789
+            return None;
790
+        }
791
+
792
+        let panel_rect = Rect::new(panel_x, panel_y, panel_width as u32, panel_height as u32);
793
+        let toggle_rect = if panel_width >= 340 {
794
+            Some(Rect::new(
795
+                panel_x + (panel_width - 190) / 2,
796
+                panel_y + 7,
797
+                190,
798
+                22,
799
+            ))
800
+        } else {
801
+            None
802
+        };
803
+
804
+        let mut buttons = Vec::new();
805
+        let grid_top = panel_y + panel_vertical_padding + toolbar_h;
806
+        for (row_idx, row) in rows.iter().enumerate() {
807
+            let cols_this_row = row.len() as i32;
808
+            if cols_this_row == 0 {
809
+                continue;
810
+            }
811
+            let row_width = cols_this_row * button_w + (cols_this_row - 1) * gap;
812
+            let row_x = panel_x + (panel_width - row_width) / 2;
813
+            let y = grid_top + row_idx as i32 * (button_h + row_gap);
814
+
815
+            for (col_idx, spec) in row.iter().enumerate() {
816
+                let x = row_x + col_idx as i32 * (button_w + gap);
817
+                buttons.push(CalcButtonRender {
818
+                    rect: Rect::new(x, y, button_w as u32, button_h as u32),
819
+                    label: spec.label,
820
+                    action: spec.action,
821
+                    role: spec.role,
822
+                });
823
+            }
824
+        }
825
+
826
+        Some(CalcButtonLayout {
827
+            panel_rect,
828
+            toggle_rect,
829
+            buttons,
830
+            hidden_optional_rows,
831
+        })
832
+    }
833
+
834
+    fn draw_calculator_buttons(
835
+        &mut self,
836
+        layout: &CalcButtonLayout,
837
+        calc_buttons_extended: bool,
838
+    ) -> Result<()> {
839
+        self.renderer.fill_rounded_rect(
840
+            layout.panel_rect,
841
+            10.0,
842
+            self.theme.background.lighten(0.03).with_alpha(0.96),
843
+        )?;
844
+        self.renderer.stroke_rounded_rect(
845
+            layout.panel_rect,
846
+            10.0,
847
+            self.theme.border.with_alpha(0.75),
848
+            1.0,
849
+        )?;
850
+
851
+        if let Some(toggle_rect) = layout.toggle_rect {
852
+            let toggle_bg = if calc_buttons_extended {
853
+                self.theme.selection_background.with_alpha(0.95)
854
+            } else {
855
+                self.theme.item_hover_background.with_alpha(0.95)
856
+            };
857
+            self.renderer
858
+                .fill_rounded_rect(toggle_rect, 7.0, toggle_bg)?;
859
+            self.renderer.stroke_rounded_rect(
860
+                toggle_rect,
861
+                7.0,
862
+                self.theme.border.with_alpha(0.85),
863
+                1.0,
864
+            )?;
865
+
866
+            let toggle_label = if calc_buttons_extended {
867
+                "Mode: Extended"
868
+            } else {
869
+                "Mode: Scientific"
870
+            };
871
+            let toggle_style = TextStyle::new()
872
+                .font_family(&self.theme.font_family)
873
+                .font_size(11.0)
874
+                .color(self.theme.selection_foreground);
875
+            let text_size = self.renderer.measure_text(toggle_label, &toggle_style)?;
876
+            self.renderer.text(
877
+                toggle_label,
878
+                toggle_rect.x as f64 + (toggle_rect.width as f64 - text_size.width as f64) * 0.5,
879
+                toggle_rect.y as f64 + (toggle_rect.height as f64 - text_size.height as f64) * 0.5,
880
+                &toggle_style,
881
+            )?;
882
+        }
883
+
884
+        if layout.hidden_optional_rows > 0 {
885
+            let hint = format!(
886
+                "{} function row(s) hidden by size",
887
+                layout.hidden_optional_rows
888
+            );
889
+            let hint_style = TextStyle::new()
890
+                .font_family(&self.theme.font_family)
891
+                .font_size(9.5)
892
+                .color(self.theme.foreground.with_alpha(0.62));
893
+            self.renderer.text(
894
+                &hint,
895
+                (layout.panel_rect.x + 10) as f64,
896
+                (layout.panel_rect.y + 10) as f64,
897
+                &hint_style,
898
+            )?;
899
+        }
900
+
901
+        for button in &layout.buttons {
902
+            let bg = match button.role {
903
+                CalcButtonRole::Numeric => self.theme.input_background.lighten(0.08),
904
+                CalcButtonRole::Operator => self.theme.selection_background.with_alpha(0.45),
905
+                CalcButtonRole::Function => self.theme.item_hover_background.with_alpha(0.88),
906
+                CalcButtonRole::Command => Color::rgb(0.22, 0.34, 0.52).with_alpha(0.92),
907
+                CalcButtonRole::Control => self.theme.background.lighten(0.09),
908
+                CalcButtonRole::Evaluate => self.theme.selection_background.with_alpha(0.92),
909
+            };
910
+            self.renderer.fill_rounded_rect(button.rect, 7.0, bg)?;
911
+            self.renderer.stroke_rounded_rect(
912
+                button.rect,
913
+                7.0,
914
+                self.theme.border.with_alpha(0.7),
915
+                1.0,
916
+            )?;
917
+
918
+            let label_style = TextStyle::new()
919
+                .font_family(&self.theme.font_family)
920
+                .font_size(11.0)
921
+                .color(self.theme.foreground.with_alpha(0.96));
922
+            let text_size = self.renderer.measure_text(button.label, &label_style)?;
923
+            self.renderer.text(
924
+                button.label,
925
+                button.rect.x as f64 + (button.rect.width as f64 - text_size.width as f64) * 0.5,
926
+                button.rect.y as f64 + (button.rect.height as f64 - text_size.height as f64) * 0.5,
927
+                &label_style,
928
+            )?;
929
+        }
930
+
931
+        Ok(())
932
+    }
933
+
338934
     fn help_button_rect(&self) -> Rect {
339935
         let size = self.renderer.size();
340936
         let margin = 10i32;
@@ -670,6 +1266,7 @@ impl CalculatorUI {
6701266
                     math_renderer.fg_color = self.theme.foreground.with_alpha(0.85);
6711267
                     math_renderer.slot_bg_color = self.theme.input_background.lighten(0.15);
6721268
                     math_renderer.slot_focus_color = self.theme.selection_background;
1269
+                    math_renderer.cursor_color = self.theme.input_cursor;
6731270
                     math_renderer.render(&prompt, padding as f64, baseline);
6741271
                     math_renderer.render(
6751272
                         mathbox,
@@ -714,6 +1311,7 @@ impl CalculatorUI {
7141311
                     math_renderer.fg_color = self.theme.selection_foreground;
7151312
                     math_renderer.slot_bg_color = self.theme.input_background.lighten(0.15);
7161313
                     math_renderer.slot_focus_color = self.theme.selection_background;
1314
+                    math_renderer.cursor_color = self.theme.input_cursor;
7171315
                     math_renderer.render(&equals, equals_x, baseline);
7181316
                     math_renderer.render(
7191317
                         mathbox,
@@ -865,6 +1463,7 @@ impl CalculatorUI {
8651463
             math_renderer.fg_color = self.theme.foreground;
8661464
             math_renderer.slot_bg_color = self.theme.input_background.lighten(0.15);
8671465
             math_renderer.slot_focus_color = self.theme.selection_background;
1466
+            math_renderer.cursor_color = self.theme.input_cursor;
8681467
             math_renderer.render_with_cursor(
8691468
                 math_input.mathbox(),
8701469
                 (padding + 10) as f64,