Kaynağa Gözat

键盘扩到 8 行:加矩阵键和 a/b/t

  {   造一对花括号,光标停在里面
  },{ 矩阵换行
  a b t  三个常用变量(原来只有 x 和 n,参数方程、系数这些天天要用 t/a/b)

**换行键是结构操作,不是插两个字符**:`{{1,2},{3,4}}` 的结构是"外层花括号
里放若干行花括号、行之间用逗号",所以换行 = 在外层那个槽里追加「逗号 +
一个新的空花括号」,光标进新行。这样 `{`、`}` 永远成对,不会插出对不上的
半个括号。光标不在花括号里时按它无反应。

顺带把变量行单独排出来(x a b t n 一行),CAS 键、函数键、数字区各归各位。

新布局:
  ∫ Σ d/dx lim solve
  sin cos tan ln √
  x a b t n
  7 8 9 ( /
  4 5 6 × !
  1 2 3 + −
  0 . ^ { },{
  ⌫ CLR , ANS =

87 个测试通过(新增 5 个矩阵 + 1 个解析往返)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
nonvioce 4 hafta önce
ebeveyn
işleme
ecc6acaf69

+ 21 - 5
lib/calculator_page.dart

@@ -278,6 +278,10 @@ class _CalculatorPageState extends State<CalculatorPage> {
         _doc.makeGroup();
       case _Act.power:
         _doc.makePower();
+      case _Act.brace:
+        _doc.makeBrace();
+      case _Act.newRow:
+        _doc.addMatrixRow();
       case _Act.frac:
         _doc.makeFraction();
       case _Act.postfix:
@@ -477,8 +481,8 @@ class _CalculatorPageState extends State<CalculatorPage> {
     return LayoutBuilder(
       builder: (context, box) {
         final rows = <List<_Key>>[
+          // 1 大运算符
           [
-            // 不定积分:只给变量,不给上下限
             _Key('∫', _Act.bigOp, payload: 'Integrate', prefill: ['x']),
             _Key('Σ', _Act.bigOp,
                 payload: 'Sum', prefill: ['n', '1', 'Infinity']),
@@ -486,6 +490,7 @@ class _CalculatorPageState extends State<CalculatorPage> {
             _Key('lim', _Act.bigOp, payload: 'Limit', prefill: ['x', '0']),
             _Key('solve', _Act.fnPrefixed, payload: 'Solve', prefill: ['', 'x']),
           ],
+          // 2 常用函数
           [
             _Key('sin', _Act.fn, payload: 'Sin'),
             _Key('cos', _Act.fn, payload: 'Cos'),
@@ -493,6 +498,15 @@ class _CalculatorPageState extends State<CalculatorPage> {
             _Key('ln', _Act.fn, payload: 'Log'),
             _Key('√', _Act.sqrt),
           ],
+          // 3 变量
+          [
+            _Key('x', _Act.char, payload: 'x'),
+            _Key('a', _Act.char, payload: 'a'),
+            _Key('b', _Act.char, payload: 'b'),
+            _Key('t', _Act.char, payload: 't'),
+            _Key('n', _Act.char, payload: 'n'),
+          ],
+          // 4 数字与括号
           [
             _Key('7', _Act.char, payload: '7', mergeDigit: true),
             _Key('8', _Act.char, payload: '8', mergeDigit: true),
@@ -514,18 +528,18 @@ class _CalculatorPageState extends State<CalculatorPage> {
             _Key('+', _Act.char, payload: '+'),
             _Key('−', _Act.char, payload: '-'),
           ],
+          // 7 花括号:矩阵用
           [
             _Key('0', _Act.char, payload: '0', mergeDigit: true),
             _Key('.', _Act.char, payload: '.', mergeDigit: true),
             _Key('^', _Act.power),
-            _Key('x', _Act.char, payload: 'x'),
-            _Key('n', _Act.char, payload: 'n'),
+            _Key('{', _Act.brace),
+            _Key('},{', _Act.newRow, tint: true),
           ],
           [
             _Key('⌫', _Act.back, tint: true),
             _Key('CLR', _Act.clear, tint: true),
             _Key(',', _Act.comma),
-            // 编辑框里显示 ANS,送进引擎的仍是 %
             _Key('ANS', _Act.char, payload: '%', tint: true, display: 'ANS'),
             _Key('=', _Act.commit, primary: true),
           ],
@@ -533,7 +547,7 @@ class _CalculatorPageState extends State<CalculatorPage> {
 
         // 7 行 + 6 个间隙,再扣掉 Padding 上下各 6 ——
         // box.maxHeight 是 padding **之前**的高度,漏掉会溢出。
-        final h = (box.maxHeight - 12 - 6 * 6) / 7;
+        final h = (box.maxHeight - 12 - 7 * 6) / 8;
         return Padding(
           padding: const EdgeInsets.all(6),
           child: Column(
@@ -597,6 +611,8 @@ enum _Act {
   fn,
   fnPrefixed,
   bigOp,
+  brace,
+  newRow,
   sqrt,
   group,
   power,

+ 36 - 0
lib/expr_node.dart

@@ -353,6 +353,42 @@ class ExprDoc {
     );
   }
 
+  /// 插入一对花括号(光标停在里面)。矩阵的每一行、整个矩阵本身都是花括号。
+  void makeBrace() => insert(BraceNode(SeqNode()), enterSlot: 0);
+
+  /// 矩阵换行。
+  ///
+  /// `{{1,2},{3,4}}` 的结构是「**外层**花括号里放若干**行**花括号,行之间用逗号」,
+  /// 所以换行不是"插两个字符",而是在外层那个槽里追加「逗号 + 一个新的空花括号」,
+  /// 光标进新行。这样结构始终成立,不会出现 `{`、`}` 各插一个对不上的情况。
+  ///
+  /// 光标不在花括号里时什么都不做。
+  void addMatrixRow() {
+    // 沿光标路径往回找**最外层**的那个花括号
+    SlotPath? braceParent;
+    int braceIndex = -1;
+    BraceNode? brace;
+    for (var k = cursor.steps.length - 1; k >= 0; k--) {
+      final step = cursor.steps[k];
+      final path = SlotPath(cursor.steps.sublist(0, k), step.child);
+      final siblings = slotAt(path);
+      if (step.child >= siblings.children.length) continue;
+      final node = siblings.children[step.child];
+      if (node is BraceNode) {
+        braceParent = path;
+        braceIndex = step.child;
+        brace = node;
+      }
+    }
+    if (braceParent == null || brace == null) return;
+
+    brace.inner.children.add(LitNode(', '));
+    brace.inner.children.add(BraceNode(SeqNode()));
+    cursor = braceParent
+        .push(braceIndex, 0) // 进外层花括号的内部槽
+        .push(brace.inner.children.length - 1, 0); // 进新那一行的内部槽
+  }
+
   /// 插入一对括号(光标停在里面)。
   void makeGroup() => insert(GroupNode(SeqNode()), enterSlot: 0);
 

+ 53 - 0
test/expr_node_test.dart

@@ -333,6 +333,59 @@ void main() {
     });
   });
 
+  group('矩阵', () {
+    test('花括号键造一对括号,光标在里面', () {
+      final d = ExprDoc();
+      d.makeBrace();
+      expect(d.toSymja(), '{Null}');
+      d.insertChar('1');
+      expect(d.toSymja(), '{1}');
+    });
+
+    test('拼出一个 2x2 矩阵', () {
+      final d = ExprDoc();
+      d.makeBrace(); // 外层
+      d.makeBrace(); // 第一行
+      d.insertChar('1');
+      d.insert(LitNode(','));
+      d.insertChar('2');
+      d.addMatrixRow(); // 换行
+      d.insertChar('3');
+      d.insert(LitNode(','));
+      d.insertChar('4');
+      expect(d.toSymja(), '{{1,2}, {3,4}}');
+    });
+
+    test('换行是结构操作:新行是花括号,不是两个字符', () {
+      final d = ExprDoc();
+      d.makeBrace();
+      d.makeBrace();
+      d.addMatrixRow();
+      final outer = d.root.children.single as BraceNode;
+      // 外层里应当是「行花括号、逗号、新行花括号」
+      expect(outer.inner.children.whereType<BraceNode>().length, 2);
+      expect(outer.inner.children.whereType<LitNode>().length, 1);
+    });
+
+    test('不在花括号里按换行键什么都不做', () {
+      final d = ExprDoc();
+      d.insertChar('1');
+      d.addMatrixRow();
+      expect(d.toSymja(), '1');
+    });
+
+    test('三层嵌套时换行加在**最外层**', () {
+      final d = ExprDoc();
+      d.makeBrace(); // 外层
+      d.makeBrace(); // 行
+      d.makeBrace(); // 行里再嵌一层
+      d.addMatrixRow();
+      final outer = d.root.children.single as BraceNode;
+      expect(outer.inner.children.whereType<BraceNode>().length, 2,
+          reason: '新行应该加到最外层,不是嵌套的那一层');
+    });
+  });
+
   group('光标槽判定', () {
     test('槽内位置不同,仍算同一个槽(渲染光标靠这个)', () {
       const a = SlotPath(<SlotStep>[], 0);

+ 9 - 0
test/expr_parse_test.dart

@@ -27,6 +27,15 @@ void main() {
       expect(rt('Sin[]'), 'Sin[]');
     });
 
+    test('矩阵能解析成嵌套花括号并往返', () {
+      final t = parseSymjaExpr('{{1,2},{3,4}}');
+      final outer = t.children.single as BraceNode;
+      expect(outer.inner.children.whereType<BraceNode>().length, 2);
+      // 解析时逗号统一成 ", "(语义不变,Symja 也不在乎空格)
+      expect(symjaOf(t), '{{1, 2}, {3, 4}}');
+      expect(symjaOf(parseSymjaExpr('{{1, 2}, {3, 4}}')), '{{1, 2}, {3, 4}}');
+    });
+
     test('幂与分式做成结构(要竖排/上标显示)', () {
       final doc = parseSymjaExpr('x^2');
       expect(doc.children.single, isA<PowNode>());