|
|
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|
|
import 'package:flutter_math_fork/flutter_math.dart';
|
|
|
import 'package:provider/provider.dart';
|
|
|
|
|
|
+import 'curve_plot.dart';
|
|
|
import 'symja.dart';
|
|
|
import 'symja_functions.dart';
|
|
|
|
|
|
@@ -133,14 +134,56 @@ class _CalculatorPageState extends State<CalculatorPage> {
|
|
|
await session.refreshVariables();
|
|
|
}
|
|
|
|
|
|
+ /// 把当前表达式在 x ∈ [-6.3, 6.3] 上采样并画出来。
|
|
|
+ Future<void> _plotCurrent() async {
|
|
|
+ final expr = _ctrl.text.trim();
|
|
|
+ if (expr.isEmpty) return;
|
|
|
+ final session = context.read<SymjaSession>();
|
|
|
+ final messenger = ScaffoldMessenger.of(context);
|
|
|
+ try {
|
|
|
+ final s = await session.sample(expr, from: -6.3, to: 6.3, count: 240);
|
|
|
+ if (!mounted) return;
|
|
|
+ await showDialog<void>(
|
|
|
+ context: context,
|
|
|
+ builder: (_) => AlertDialog(
|
|
|
+ title: Text('$expr (${s.ys.length} 点,${s.elapsedMs} ms)'),
|
|
|
+ content: SizedBox(
|
|
|
+ width: 360,
|
|
|
+ height: 280,
|
|
|
+ child: CurvePlot(series: [
|
|
|
+ CurveSeries(
|
|
|
+ label: expr,
|
|
|
+ color: const Color(0xFF3F51B5),
|
|
|
+ points: [
|
|
|
+ for (var i = 0; i < s.ys.length; i++)
|
|
|
+ Offset(s.xAt(i), s.ys[i]),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ]),
|
|
|
+ ),
|
|
|
+ actions: [
|
|
|
+ TextButton(
|
|
|
+ onPressed: () => Navigator.pop(context),
|
|
|
+ child: const Text('关闭'),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ } on SymjaException catch (e) {
|
|
|
+ messenger.showSnackBar(SnackBar(content: Text('$e')));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
Future<void> _showPalette() async {
|
|
|
- final picked = await showModalBottomSheet<String>(
|
|
|
+ // 面板返回「要插入什么」+「光标从末尾回退几位」:
|
|
|
+ // 函数是 `Sin[]` 退 1(光标在括号里),常量是 `Pi` 退 0。
|
|
|
+ final picked = await showModalBottomSheet<({String text, int back})>(
|
|
|
context: context,
|
|
|
isScrollControlled: true,
|
|
|
showDragHandle: true,
|
|
|
builder: (_) => const _FunctionPalette(),
|
|
|
);
|
|
|
- if (picked != null) _insert('$picked[]', back: 1);
|
|
|
+ if (picked != null) _insert(picked.text, back: picked.back);
|
|
|
}
|
|
|
|
|
|
// ------------------------------------------------------------------ UI
|
|
|
@@ -151,6 +194,11 @@ class _CalculatorPageState extends State<CalculatorPage> {
|
|
|
appBar: AppBar(
|
|
|
title: const Text('计算器'),
|
|
|
actions: [
|
|
|
+ IconButton(
|
|
|
+ tooltip: '画图(x ∈ [-6.3, 6.3])',
|
|
|
+ icon: const Icon(Icons.show_chart),
|
|
|
+ onPressed: _plotCurrent,
|
|
|
+ ),
|
|
|
IconButton(
|
|
|
tooltip: '函数面板',
|
|
|
icon: const Icon(Icons.functions),
|
|
|
@@ -310,14 +358,16 @@ class _CalculatorPageState extends State<CalculatorPage> {
|
|
|
_Key('0', insert: '0'),
|
|
|
_Key('.', insert: '.'),
|
|
|
_Key('^', insert: '^'),
|
|
|
- _Key('ANS', insert: '%', tint: true),
|
|
|
- _Key(',', insert: ', '),
|
|
|
+ // 变量键:没有它们就没法只用按钮写出 Integrate[x^2, x]。
|
|
|
+ // π / e 挪进了函数面板(那里的常量是可以点的)。
|
|
|
+ _Key('x', insert: 'x'),
|
|
|
+ _Key('n', insert: 'n'),
|
|
|
],
|
|
|
[
|
|
|
_Key('⌫', action: _KeyAction.backspace, tint: true),
|
|
|
_Key('CLR', action: _KeyAction.clear, tint: true),
|
|
|
- _Key('π', insert: 'Pi'),
|
|
|
- _Key('e', insert: 'E'),
|
|
|
+ _Key(',', insert: ', '),
|
|
|
+ _Key('ANS', insert: '%', tint: true),
|
|
|
_Key('=', action: _KeyAction.commit, primary: true),
|
|
|
],
|
|
|
];
|
|
|
@@ -467,14 +517,29 @@ class _FunctionPaletteState extends State<_FunctionPalette> {
|
|
|
onChanged: (v) => setState(() => _q = v),
|
|
|
),
|
|
|
),
|
|
|
+ // 常量做成可点的 —— 它们从键盘上挪走了,得有个地方能按。
|
|
|
Padding(
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
|
- child: Align(
|
|
|
- alignment: Alignment.centerLeft,
|
|
|
- child: Text(
|
|
|
- '常量:${kSymjaConstants.join(' ')}',
|
|
|
- style: Theme.of(context).textTheme.bodySmall,
|
|
|
- ),
|
|
|
+ child: Wrap(
|
|
|
+ spacing: 8,
|
|
|
+ runSpacing: 4,
|
|
|
+ crossAxisAlignment: WrapCrossAlignment.center,
|
|
|
+ children: [
|
|
|
+ Text('常量',
|
|
|
+ style: Theme.of(context).textTheme.bodySmall),
|
|
|
+ for (final c in kSymjaConstants)
|
|
|
+ ActionChip(
|
|
|
+ label: Text(
|
|
|
+ c,
|
|
|
+ style: const TextStyle(
|
|
|
+ fontFamily: 'monospace', fontSize: 12),
|
|
|
+ ),
|
|
|
+ visualDensity: VisualDensity.compact,
|
|
|
+ materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
|
+ onPressed: () =>
|
|
|
+ Navigator.pop(context, (text: c, back: 0)),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
),
|
|
|
),
|
|
|
const Divider(height: 16),
|
|
|
@@ -490,7 +555,8 @@ class _FunctionPaletteState extends State<_FunctionPalette> {
|
|
|
fns[i],
|
|
|
style: const TextStyle(fontFamily: 'monospace'),
|
|
|
),
|
|
|
- onTap: () => Navigator.pop(context, fns[i]),
|
|
|
+ onTap: () => Navigator.pop(
|
|
|
+ context, (text: '${fns[i]}[]', back: 1)),
|
|
|
),
|
|
|
),
|
|
|
),
|