Pārlūkot izejas kodu

光标能移到表达式最前面

点节点的处理原来是"无论点哪都落到它后面",点槽的空白处落到末尾 ——
于是**偏移 0(行首)那个位置根本够不着**,用户回不到表达式最前面。

改成按左右半边分:点左半边放到节点前,右半边放到节点后。
宽度用 Builder 拿 —— Builder 自身不是 RenderObject,findRenderObject()
会往下找到 GestureDetector 那个 box,尺寸正是节点的尺寸。

两个回归测试:点 x 左半边再打 2 应得 2x+1(不是 x2+1),右半边应得 x2+。
真机验证:x+1 把光标点到 x 前面后插入 2 得到 2|x+1。

Co-Authored-By: Claude Code <noreply@anthropic.com>
nonvioce 4 nedēļas atpakaļ
vecāks
revīzija
b28186c4e9
2 mainītis faili ar 64 papildinājumiem un 15 dzēšanām
  1. 29 15
      lib/expr_editor.dart
  2. 35 0
      test/widget_test.dart

+ 29 - 15
lib/expr_editor.dart

@@ -162,21 +162,35 @@ class _NodeView extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final scheme = Theme.of(context).colorScheme;
-    // 点中某个节点 → 光标落到它后面(符合"接着往下打"的直觉)
-    return GestureDetector(
-      onTap: () => onCursor(SlotPath(parent.steps, index + 1)),
-      behavior: HitTestBehavior.opaque,
-      child: switch (node) {
-        LitNode l => _literal(l, scheme),
-        FuncNode f => _function(context, f),
-        FracNode f => _fraction(context, f),
-        PowNode p => _power(context, p),
-        GroupNode g => _group(context, g),
-        PostfixNode p => _row([
-            _slot(context, p.operand, parent.push(index, 0)),
-            _text(p.op, size),
-          ]),
-      },
+    final body = switch (node) {
+      LitNode l => _literal(l, scheme),
+      FuncNode f => _function(context, f),
+      FracNode f => _fraction(context, f),
+      PowNode p => _power(context, p),
+      GroupNode g => _group(context, g),
+      PostfixNode p => _row([
+          _slot(context, p.operand, parent.push(index, 0)),
+          _text(p.op, size),
+        ]),
+    };
+
+    // 点节点的**左半边**把光标放到它前面,右半边放到它后面。
+    //
+    // 之前无论点哪都落到"后面",于是偏移 0(表达式最前面)那个位置永远够不着 ——
+    // 用户只能把光标停在第一个字符之后,回不到行首。
+    //
+    // 宽度用 Builder 拿:Builder 自身不是 RenderObject,findRenderObject()
+    // 会往下找到 GestureDetector 那个 box,尺寸正是节点的尺寸。
+    return Builder(
+      builder: (inner) => GestureDetector(
+        behavior: HitTestBehavior.opaque,
+        onTapDown: (d) {
+          final box = inner.findRenderObject() as RenderBox?;
+          final before = d.localPosition.dx < (box?.size.width ?? 0) / 2;
+          onCursor(SlotPath(parent.steps, before ? index : index + 1));
+        },
+        child: body,
+      ),
     );
   }
 

+ 35 - 0
test/widget_test.dart

@@ -6,6 +6,7 @@ import 'package:flutter_test/flutter_test.dart';
 
 import 'package:cal/calculator_page.dart';
 import 'package:cal/console_page.dart';
+import 'package:cal/expr_editor.dart';
 import 'package:cal/main.dart';
 import 'package:cal/tools_page.dart';
 
@@ -190,6 +191,40 @@ void main() {
       expect(find.byKey(const ValueKey('expr-caret')), findsOneWidget);
     });
 
+    testWidgets('点节点左半边能把光标放回最前面', (tester) async {
+      await pumpApp(tester);
+      await press(tester, 'x');
+      await press(tester, '+');
+      await press(tester, '1');
+      expect(find.text('x+1'), findsOneWidget);
+
+      // 点 x 的左半边 → 光标应当落到 x 前面
+      final xRect = tester.getRect(
+        find.descendant(of: find.byType(ExprEditor), matching: find.text('x')),
+      );
+      await tester.tapAt(Offset(xRect.left + 1, xRect.center.dy));
+      await tester.pumpAndSettle();
+
+      // 再打一个 2:插到最前面才是 2x+1;若光标落在 x 之后就成 x2+1
+      await press(tester, '2');
+      expect(find.text('2x+1'), findsOneWidget);
+    });
+
+    testWidgets('点节点右半边把光标放到它后面', (tester) async {
+      await pumpApp(tester);
+      await press(tester, 'x');
+      await press(tester, '+');
+
+      final xRect = tester.getRect(
+        find.descendant(of: find.byType(ExprEditor), matching: find.text('x')),
+      );
+      await tester.tapAt(Offset(xRect.right - 1, xRect.center.dy));
+      await tester.pumpAndSettle();
+
+      await press(tester, '2');
+      expect(find.text('x2+'), findsOneWidget);
+    });
+
     testWidgets('默认精确:求值原样送出去', (tester) async {
       await pumpApp(tester);
       await press(tester, 'sin');