calculator_page.dart 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/services.dart';
  4. import 'package:flutter_math_fork/flutter_math.dart';
  5. import 'package:provider/provider.dart';
  6. import 'curve_plot.dart';
  7. import 'expr_editor.dart';
  8. import 'expr_node.dart';
  9. import 'expr_parse.dart';
  10. import 'symja.dart';
  11. import 'symja_functions.dart';
  12. /// 科学计算器(默认主界面)。
  13. ///
  14. /// 输入用**结构化节点树**(见 lib/expr_node.dart),不是字符串:
  15. /// 按键把节点插进当前光标槽,分数竖排、指数上标、多参数函数各有自己的槽。
  16. /// 纯文本输入留给控制台。
  17. class CalculatorPage extends StatefulWidget {
  18. const CalculatorPage({super.key});
  19. @override
  20. State<CalculatorPage> createState() => _CalculatorPageState();
  21. }
  22. class _CalculatorPageState extends State<CalculatorPage> {
  23. final _doc = ExprDoc();
  24. SymjaResult? _preview;
  25. SymjaResult? _committed;
  26. String? _error;
  27. Timer? _debounce;
  28. int _seq = 0;
  29. /// 用户点了"光标已经在的地方"—— 显示粘贴入口。
  30. bool _pastePrompt = false;
  31. @override
  32. void dispose() {
  33. _debounce?.cancel();
  34. super.dispose();
  35. }
  36. // ------------------------------------------------------------------ 求值
  37. /// 编辑器每次变化都走这里:清掉上次的提交态,防抖后重算预览。
  38. void _onDocChanged() {
  39. setState(() {
  40. _committed = null;
  41. _error = null;
  42. _pastePrompt = false; // 动过内容/光标就收起操作栏
  43. });
  44. _debounce?.cancel();
  45. _debounce = Timer(const Duration(milliseconds: 500), _runPreview);
  46. }
  47. String get _source => _doc.toSymja();
  48. void _snack(String msg) {
  49. if (!mounted) return;
  50. ScaffoldMessenger.of(context)
  51. ..hideCurrentSnackBar()
  52. ..showSnackBar(SnackBar(content: Text(msg)));
  53. }
  54. // ------------------------------------------------------------------ 复制 / 粘贴
  55. Future<void> _copy(String text, String what) async {
  56. await Clipboard.setData(ClipboardData(text: text));
  57. _snack('已复制$what:$text');
  58. }
  59. /// 从剪贴板读一段 Symja 表达式,解析成节点树后整体替换。
  60. ///
  61. /// 解析器只认一个明确的子集,**认不出来就报错**,不会猜着解析 ——
  62. /// 猜错了你会看到算错的结果,那比报错糟糕得多。
  63. Future<void> _paste() async {
  64. final data = await Clipboard.getData(Clipboard.kTextPlain);
  65. final text = (data?.text ?? '').trim();
  66. if (text.isEmpty) {
  67. _snack('剪贴板里没有文本');
  68. return;
  69. }
  70. try {
  71. final parsed = parseSymjaExpr(text);
  72. setState(() {
  73. _pastePrompt = false;
  74. if (_doc.selection != null) _doc.deleteSelection(); // 有选区就替换掉
  75. _doc.insertNodesAtCursor(parsed.children); // 否则插到光标处
  76. _preview = null;
  77. _committed = null;
  78. _error = null;
  79. });
  80. _onDocChanged();
  81. _snack('已粘贴到光标处:${_doc.toSymja()}');
  82. } on ExprParseException catch (e) {
  83. _snack('这段表达式没法转成按钮式输入 —— $e');
  84. }
  85. }
  86. // ------------------------------------------------------------------ 选区操作
  87. Future<void> _copySelection() async {
  88. final t = _doc.selectionText;
  89. if (t.isEmpty) return;
  90. await _copy(t, '选中内容');
  91. setState(() => _doc.clearSelection());
  92. }
  93. Future<void> _cutSelection() async {
  94. final t = _doc.selectionText;
  95. if (t.isEmpty) return;
  96. await Clipboard.setData(ClipboardData(text: t));
  97. setState(() {
  98. _doc.deleteSelection();
  99. _pastePrompt = false;
  100. });
  101. _onDocChanged();
  102. _snack('已剪切:$t');
  103. }
  104. void _deleteSelection() {
  105. setState(() {
  106. _doc.deleteSelection();
  107. _pastePrompt = false;
  108. });
  109. _onDocChanged();
  110. }
  111. void _dismissBar() {
  112. setState(() {
  113. _pastePrompt = false;
  114. _doc.clearSelection();
  115. });
  116. }
  117. /// 选中部分内容后自动出现的操作栏;或点了光标处时的粘贴入口。
  118. Widget? _contextBar() {
  119. final sel = _doc.selection;
  120. final hasSel = sel != null && !sel.isEmpty;
  121. if (!hasSel && !_pastePrompt) return null;
  122. final scheme = Theme.of(context).colorScheme;
  123. return Material(
  124. color: scheme.surfaceContainerHighest,
  125. child: Padding(
  126. padding: const EdgeInsets.only(left: 12, right: 4),
  127. child: Row(
  128. children: [
  129. Expanded(
  130. child: Text(
  131. hasSel ? '已选中 ${_doc.selectionText}' : '粘贴到光标处',
  132. maxLines: 1,
  133. overflow: TextOverflow.ellipsis,
  134. style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
  135. ),
  136. ),
  137. if (hasSel) ...[
  138. TextButton(onPressed: _copySelection, child: const Text('复制')),
  139. TextButton(onPressed: _cutSelection, child: const Text('剪切')),
  140. TextButton(onPressed: _deleteSelection, child: const Text('删除')),
  141. ] else
  142. TextButton(onPressed: _paste, child: const Text('粘贴')),
  143. IconButton(
  144. tooltip: '关闭',
  145. icon: const Icon(Icons.close, size: 18),
  146. onPressed: _dismissBar,
  147. ),
  148. ],
  149. ),
  150. ),
  151. );
  152. }
  153. Future<void> _menu(_Menu item) async {
  154. switch (item) {
  155. case _Menu.palette:
  156. await _showPalette();
  157. case _Menu.copyExpr:
  158. final s = _source;
  159. if (s.isEmpty || s == 'Null') {
  160. _snack('还没有表达式');
  161. } else {
  162. await _copy(s, '表达式');
  163. }
  164. case _Menu.copyResult:
  165. final r = _committed ?? _preview;
  166. if (r == null) {
  167. _snack('还没有结果');
  168. } else {
  169. await _copy(r.text, '结果');
  170. }
  171. case _Menu.paste:
  172. await _paste();
  173. case _Menu.clear:
  174. _doc.clear();
  175. _onDocChanged();
  176. }
  177. }
  178. Future<void> _runPreview() async {
  179. final expr = _source;
  180. if (expr.isEmpty || expr == 'Null') {
  181. if (mounted) setState(() => _preview = null);
  182. return;
  183. }
  184. final session = context.read<SymjaSession>();
  185. final id = ++_seq;
  186. final r = await session.preview(expr);
  187. if (!mounted || id != _seq) return; // 已有更新的预览在路上
  188. setState(() => _preview = r);
  189. }
  190. Future<void> _commit() async {
  191. final expr = _source;
  192. if (expr.isEmpty || expr == 'Null') return;
  193. _debounce?.cancel();
  194. final session = context.read<SymjaSession>();
  195. final id = ++_seq; // 让在途的预览作废
  196. try {
  197. final r = await session.evaluate(expr);
  198. if (!mounted || id != _seq) return;
  199. setState(() {
  200. _committed = r;
  201. _preview = null;
  202. _error = null;
  203. });
  204. } on SymjaException catch (e) {
  205. if (!mounted || id != _seq) return;
  206. setState(() {
  207. _error = '$e';
  208. _committed = null;
  209. _preview = null;
  210. });
  211. }
  212. await session.refreshVariables();
  213. }
  214. Future<void> _plotCurrent() async {
  215. final expr = _source;
  216. if (expr.isEmpty || expr == 'Null') return;
  217. final session = context.read<SymjaSession>();
  218. final messenger = ScaffoldMessenger.of(context);
  219. try {
  220. final s = await session.sample(expr, from: -6.3, to: 6.3, count: 240);
  221. if (!mounted) return;
  222. await showDialog<void>(
  223. context: context,
  224. builder: (_) => AlertDialog(
  225. title: Text('$expr (${s.ys.length} 点,${s.elapsedMs} ms)'),
  226. content: SizedBox(
  227. width: 360,
  228. height: 280,
  229. child: CurvePlot(series: [
  230. CurveSeries(
  231. label: expr,
  232. color: const Color(0xFF3F51B5),
  233. points: [
  234. for (var i = 0; i < s.ys.length; i++)
  235. Offset(s.xAt(i), s.ys[i]),
  236. ],
  237. ),
  238. ]),
  239. ),
  240. actions: [
  241. TextButton(
  242. onPressed: () => Navigator.pop(context),
  243. child: const Text('关闭'),
  244. ),
  245. ],
  246. ),
  247. );
  248. } on SymjaException catch (e) {
  249. messenger.showSnackBar(SnackBar(content: Text('$e')));
  250. }
  251. }
  252. // ------------------------------------------------------------------ 按键
  253. void _press(_Key k) {
  254. switch (k.act) {
  255. case _Act.char:
  256. if (k.mergeDigit) {
  257. // 数字键并入左边那个数字字面量,`42` 才是一个数而不是两个节点
  258. _doc.insertChar(k.payload);
  259. } else {
  260. _doc.insert(LitNode(k.payload));
  261. }
  262. case _Act.fn:
  263. _doc.makeFunction(k.payload);
  264. case _Act.fnPrefixed:
  265. // 像 Integrate 这种,第一个参数留空给用户填,其余预置好
  266. _doc.makeFunction(k.payload, prefill: k.prefill);
  267. case _Act.sqrt:
  268. _doc.makeFunction('Sqrt');
  269. case _Act.group:
  270. _doc.makeGroup();
  271. case _Act.power:
  272. _doc.makePower();
  273. case _Act.frac:
  274. _doc.makeFraction();
  275. case _Act.postfix:
  276. // 阶乘是后缀:把光标左边那个节点收进来当操作数
  277. final operand = _doc.takeLeft();
  278. _doc.insert(PostfixNode(
  279. operand: SeqNode(operand == null ? null : [operand]),
  280. ));
  281. case _Act.comma:
  282. _doc.nextArgument();
  283. case _Act.back:
  284. _doc.backspace();
  285. case _Act.clear:
  286. _doc.clear();
  287. case _Act.commit:
  288. _commit();
  289. return; // 提交不改文档,不用刷新
  290. }
  291. _onDocChanged();
  292. }
  293. Future<void> _showPalette() async {
  294. final picked = await showModalBottomSheet<({String value, bool isFn})>(
  295. context: context,
  296. isScrollControlled: true,
  297. showDragHandle: true,
  298. builder: (_) => const _FunctionPalette(),
  299. );
  300. if (picked == null) return;
  301. if (picked.isFn) {
  302. _doc.makeFunction(picked.value);
  303. } else {
  304. _doc.insert(LitNode(picked.value));
  305. }
  306. _onDocChanged();
  307. }
  308. // ------------------------------------------------------------------ UI
  309. @override
  310. Widget build(BuildContext context) {
  311. final session = context.watch<SymjaSession>();
  312. final bar = _contextBar();
  313. return Scaffold(
  314. appBar: AppBar(
  315. title: const Text('计算器'),
  316. actions: [
  317. // 精确 / 近似 切换。默认精确:`1/3` 就是 `1/3`,这是符号计算的意义所在。
  318. TextButton(
  319. onPressed: () {
  320. session.approximate = !session.approximate;
  321. _onDocChanged(); // 立刻按新模式重算预览
  322. },
  323. child: Text(
  324. session.approximate ? '≈ 近似' : '精确',
  325. style: TextStyle(
  326. fontFamily: 'monospace',
  327. fontSize: 13,
  328. color: Theme.of(context).colorScheme.primary,
  329. ),
  330. ),
  331. ),
  332. IconButton(
  333. tooltip: '画图(x ∈ [-6.3, 6.3])',
  334. icon: const Icon(Icons.show_chart),
  335. onPressed: _plotCurrent,
  336. ),
  337. PopupMenuButton<_Menu>(
  338. tooltip: '更多',
  339. onSelected: _menu,
  340. itemBuilder: (_) => const [
  341. PopupMenuItem(value: _Menu.palette, child: Text('函数面板')),
  342. PopupMenuItem(value: _Menu.copyExpr, child: Text('复制表达式')),
  343. PopupMenuItem(value: _Menu.copyResult, child: Text('复制结果')),
  344. PopupMenuItem(value: _Menu.paste, child: Text('从剪贴板粘贴')),
  345. PopupMenuItem(value: _Menu.clear, child: Text('清空')),
  346. ],
  347. ),
  348. ],
  349. ),
  350. body: SafeArea(
  351. child: Column(
  352. children: [
  353. _display(context),
  354. ?bar,
  355. const Divider(height: 1),
  356. Expanded(child: _keypad(context)),
  357. ],
  358. ),
  359. ),
  360. );
  361. }
  362. Widget _display(BuildContext context) {
  363. final scheme = Theme.of(context).colorScheme;
  364. final shown = _committed ?? _preview;
  365. return Container(
  366. width: double.infinity,
  367. constraints: const BoxConstraints(minHeight: 132),
  368. padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
  369. color: scheme.surfaceContainerLow,
  370. child: Column(
  371. crossAxisAlignment: CrossAxisAlignment.end,
  372. children: [
  373. ExprEditor(
  374. doc: _doc,
  375. onChanged: _onDocChanged,
  376. onCaretTapped: () => setState(() => _pastePrompt = true),
  377. ),
  378. const SizedBox(height: 6),
  379. // 序列化结果摆在明面上:用户能看到"到底会被求值成什么"
  380. Text(
  381. _source,
  382. textAlign: TextAlign.right,
  383. style: TextStyle(
  384. fontFamily: 'monospace',
  385. fontSize: 11,
  386. color: scheme.outline,
  387. ),
  388. ),
  389. const SizedBox(height: 6),
  390. SizedBox(
  391. height: 52,
  392. width: double.infinity,
  393. // 等比缩到放得下,**不横向滚动** ——
  394. // 滚动的话长结果只能看到尾巴,"显示不全"就是这么来的。
  395. child: FittedBox(
  396. fit: BoxFit.scaleDown,
  397. alignment: Alignment.centerRight,
  398. child: _resultView(context, shown),
  399. ),
  400. ),
  401. ],
  402. ),
  403. );
  404. }
  405. Widget _resultView(BuildContext context, SymjaResult? shown) {
  406. final scheme = Theme.of(context).colorScheme;
  407. if (_error != null) {
  408. return SelectableText(
  409. _error!,
  410. style: TextStyle(
  411. fontFamily: 'monospace',
  412. fontSize: 13,
  413. color: scheme.error,
  414. ),
  415. );
  416. }
  417. if (shown == null) return const SizedBox.shrink();
  418. final plain = SelectableText(
  419. shown.text,
  420. style: const TextStyle(fontFamily: 'monospace', fontSize: 22),
  421. );
  422. final tex = shown.tex;
  423. final body = tex == null
  424. ? plain
  425. : Math.tex(
  426. tex,
  427. mathStyle: MathStyle.display,
  428. textStyle: TextStyle(fontSize: 26, color: scheme.onSurface),
  429. onErrorFallback: (_) => plain,
  430. );
  431. return Row(
  432. mainAxisSize: MainAxisSize.min,
  433. children: [
  434. if (_committed != null) ...[
  435. Icon(Icons.check_circle, size: 16, color: scheme.primary),
  436. const SizedBox(width: 6),
  437. Text('${_committed!.elapsedMs} ms',
  438. style: TextStyle(fontSize: 11, color: scheme.outline)),
  439. const SizedBox(width: 10),
  440. ],
  441. body,
  442. ],
  443. );
  444. }
  445. Widget _keypad(BuildContext context) {
  446. return LayoutBuilder(
  447. builder: (context, box) {
  448. final rows = <List<_Key>>[
  449. [
  450. _Key('∫', _Act.fnPrefixed, payload: 'Integrate', prefill: ['', 'x']),
  451. _Key('Σ', _Act.fnPrefixed,
  452. payload: 'Sum', prefill: ['', '{n, 1, Infinity}']),
  453. _Key('d/dx', _Act.fnPrefixed, payload: 'D', prefill: ['', 'x']),
  454. _Key('lim', _Act.fnPrefixed, payload: 'Limit', prefill: ['', 'x->0']),
  455. _Key('solve', _Act.fnPrefixed, payload: 'Solve', prefill: ['', 'x']),
  456. ],
  457. [
  458. _Key('sin', _Act.fn, payload: 'Sin'),
  459. _Key('cos', _Act.fn, payload: 'Cos'),
  460. _Key('tan', _Act.fn, payload: 'Tan'),
  461. _Key('ln', _Act.fn, payload: 'Log'),
  462. _Key('√', _Act.sqrt),
  463. ],
  464. [
  465. _Key('7', _Act.char, payload: '7', mergeDigit: true),
  466. _Key('8', _Act.char, payload: '8', mergeDigit: true),
  467. _Key('9', _Act.char, payload: '9', mergeDigit: true),
  468. _Key('(', _Act.group),
  469. _Key('/', _Act.frac),
  470. ],
  471. [
  472. _Key('4', _Act.char, payload: '4', mergeDigit: true),
  473. _Key('5', _Act.char, payload: '5', mergeDigit: true),
  474. _Key('6', _Act.char, payload: '6', mergeDigit: true),
  475. _Key('×', _Act.char, payload: '*'),
  476. _Key('!', _Act.postfix),
  477. ],
  478. [
  479. _Key('1', _Act.char, payload: '1', mergeDigit: true),
  480. _Key('2', _Act.char, payload: '2', mergeDigit: true),
  481. _Key('3', _Act.char, payload: '3', mergeDigit: true),
  482. _Key('+', _Act.char, payload: '+'),
  483. _Key('−', _Act.char, payload: '-'),
  484. ],
  485. [
  486. _Key('0', _Act.char, payload: '0', mergeDigit: true),
  487. _Key('.', _Act.char, payload: '.', mergeDigit: true),
  488. _Key('^', _Act.power),
  489. _Key('x', _Act.char, payload: 'x'),
  490. _Key('n', _Act.char, payload: 'n'),
  491. ],
  492. [
  493. _Key('⌫', _Act.back, tint: true),
  494. _Key('CLR', _Act.clear, tint: true),
  495. _Key(',', _Act.comma),
  496. _Key('ANS', _Act.char, payload: '%', tint: true),
  497. _Key('=', _Act.commit, primary: true),
  498. ],
  499. ];
  500. // 7 行 + 6 个间隙,再扣掉 Padding 上下各 6 ——
  501. // box.maxHeight 是 padding **之前**的高度,漏掉会溢出。
  502. final h = (box.maxHeight - 12 - 6 * 6) / 7;
  503. return Padding(
  504. padding: const EdgeInsets.all(6),
  505. child: Column(
  506. children: [
  507. for (var r = 0; r < rows.length; r++) ...[
  508. if (r > 0) const SizedBox(height: 6),
  509. SizedBox(
  510. height: h,
  511. child: Row(
  512. children: [
  513. for (var c = 0; c < rows[r].length; c++) ...[
  514. if (c > 0) const SizedBox(width: 6),
  515. Expanded(child: _keyButton(rows[r][c])),
  516. ],
  517. ],
  518. ),
  519. ),
  520. ],
  521. ],
  522. ),
  523. );
  524. },
  525. );
  526. }
  527. Widget _keyButton(_Key k) {
  528. final scheme = Theme.of(context).colorScheme;
  529. final bg = k.primary
  530. ? scheme.primary
  531. : k.tint
  532. ? scheme.surfaceContainerHighest
  533. : scheme.surfaceContainerLow;
  534. final fg = k.primary ? scheme.onPrimary : scheme.onSurface;
  535. return Material(
  536. // 加 key 是为了让测试能精确定位按键 —— 编辑器里渲染出的字面量(比如 'x')
  537. // 和键盘上同名的按键都符合 find.text,会撞车。
  538. key: ValueKey('key-${k.label}'),
  539. color: bg,
  540. borderRadius: BorderRadius.circular(10),
  541. child: InkWell(
  542. borderRadius: BorderRadius.circular(10),
  543. onTap: () => _press(k),
  544. child: Center(
  545. child: Text(
  546. k.label,
  547. style: TextStyle(
  548. color: fg,
  549. fontSize: k.label.length > 2 ? 13 : 17,
  550. fontFamily: k.label.length > 2 ? null : 'monospace',
  551. ),
  552. ),
  553. ),
  554. ),
  555. );
  556. }
  557. }
  558. /// 计算器右上角「⋮」菜单的项。
  559. enum _Menu { palette, copyExpr, copyResult, paste, clear }
  560. enum _Act {
  561. char,
  562. fn,
  563. fnPrefixed,
  564. sqrt,
  565. group,
  566. power,
  567. frac,
  568. postfix,
  569. comma,
  570. back,
  571. clear,
  572. commit,
  573. }
  574. @immutable
  575. class _Key {
  576. final String label;
  577. final _Act act;
  578. final String payload;
  579. /// fnPrefixed 用:第一个参数留空给用户填,其余的预置。
  580. final List<String> prefill;
  581. /// 数字键:并入左边那个数字字面量,而不是各建一个节点。
  582. final bool mergeDigit;
  583. final bool tint;
  584. final bool primary;
  585. const _Key(
  586. this.label,
  587. this.act, {
  588. this.payload = '',
  589. this.prefill = const [],
  590. this.mergeDigit = false,
  591. this.tint = false,
  592. this.primary = false,
  593. });
  594. }
  595. /// 函数面板:检索 Symja 的两百多个函数名,选中后插入一个函数节点。
  596. class _FunctionPalette extends StatefulWidget {
  597. const _FunctionPalette();
  598. @override
  599. State<_FunctionPalette> createState() => _FunctionPaletteState();
  600. }
  601. class _FunctionPaletteState extends State<_FunctionPalette> {
  602. final _search = TextEditingController();
  603. String _q = '';
  604. @override
  605. void dispose() {
  606. _search.dispose();
  607. super.dispose();
  608. }
  609. @override
  610. Widget build(BuildContext context) {
  611. final q = _q.trim().toLowerCase();
  612. final fns = kSymjaFunctions
  613. .where((f) => q.isEmpty || f.toLowerCase().contains(q))
  614. .toList(growable: false);
  615. return DraggableScrollableSheet(
  616. expand: false,
  617. initialChildSize: 0.75,
  618. maxChildSize: 0.95,
  619. builder: (context, scroll) => Column(
  620. children: [
  621. Padding(
  622. padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
  623. child: TextField(
  624. controller: _search,
  625. autofocus: true,
  626. decoration: InputDecoration(
  627. isDense: true,
  628. hintText: '函数名(共 ${kSymjaFunctions.length} 个)',
  629. prefixIcon: const Icon(Icons.search),
  630. border: const OutlineInputBorder(),
  631. ),
  632. onChanged: (v) => setState(() => _q = v),
  633. ),
  634. ),
  635. // 常量做成可点的 —— 它们从键盘上挪走了,得有个地方能按。
  636. Padding(
  637. padding: const EdgeInsets.symmetric(horizontal: 16),
  638. child: Wrap(
  639. spacing: 8,
  640. runSpacing: 4,
  641. crossAxisAlignment: WrapCrossAlignment.center,
  642. children: [
  643. Text('常量', style: Theme.of(context).textTheme.bodySmall),
  644. for (final c in kSymjaConstants)
  645. ActionChip(
  646. label: Text(
  647. c,
  648. style: const TextStyle(
  649. fontFamily: 'monospace', fontSize: 12),
  650. ),
  651. visualDensity: VisualDensity.compact,
  652. materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
  653. onPressed: () =>
  654. Navigator.pop(context, (value: c, isFn: false)),
  655. ),
  656. ],
  657. ),
  658. ),
  659. const Divider(height: 16),
  660. Expanded(
  661. child: fns.isEmpty
  662. ? const Center(child: Text('没有匹配的函数'))
  663. : ListView.builder(
  664. controller: scroll,
  665. itemCount: fns.length,
  666. itemBuilder: (_, i) => ListTile(
  667. dense: true,
  668. title: Text(
  669. fns[i],
  670. style: const TextStyle(fontFamily: 'monospace'),
  671. ),
  672. onTap: () => Navigator.pop(
  673. context, (value: fns[i], isFn: true)),
  674. ),
  675. ),
  676. ),
  677. ],
  678. ),
  679. );
  680. }
  681. }