diff --git a/.gitignore b/.gitignore index e38ed2a3..adce420a 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,9 @@ coverage .pub/ build/ pubspec.lock +!example/pubspec.lock +/test/failures/ +/test/golden/temp/ # Android related **/android/**/gradle-wrapper.jar @@ -72,6 +75,7 @@ pubspec.lock **/ios/Flutter/app.zip **/ios/Flutter/flutter_assets/ **/ios/Flutter/flutter_export_environment.sh +**/ios/Flutter/ephemeral/ **/ios/ServiceDefinitions.json **/ios/Runner/GeneratedPluginRegistrant.* diff --git a/.pubignore b/.pubignore new file mode 100644 index 00000000..9075a29e --- /dev/null +++ b/.pubignore @@ -0,0 +1,8 @@ +# Generated local build and test artifacts. +/build/ +/test/failures/ +/test/golden/temp/ + +# Local example artifacts that should not ship with the package. +/example/pubspec.lock +/example/ios/Flutter/ephemeral/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 916cefb0..0a320bfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## 0.8.0 + +* Add compatibility updates for Flutter `3.38.x` and Dart `3.10.x`. +* Raise the minimum Flutter version to `3.38.0`. +* Update package constraints for `flutter_svg`, `provider`, `collection`, and `tuple`. +* Remove the direct `meta` dependency because the current Flutter SDK pins `meta` through `flutter_test`. +* Clean up deprecated Flutter API usage across rendering, selection, web, and tests. +* **Breaking:** replace direct `ToolbarOptions` usage with `SelectableMathToolbarOptions` in `SelectableMath`. +* Fix selection toolbar behavior and focus handling on current Flutter releases. +* Fix matrix child updates, font metric fallback lookup, and other null-safety/code-structure issues. +* Update the example app SDK constraints and package resolution for the current Flutter toolchain. +* Refresh golden files and documentation for the current renderer output. +* Add shaped `\text{...}` rendering for multilingual inline text, including Bangla, Arabic, Hindi, Japanese and more mixed with math on the same line. +* Add RTL-aware text-run handling for Arabic inside `\text{...}`. +* Document current Unicode/UTF-8 support behavior and add widget coverage for multilingual text rendering in both `Math.tex` and `SelectableMath.tex`. + ## 0.7.4 * Add support for flutter 3.32.0 diff --git a/README.md b/README.md index 4422cfb3..475d6097 100644 --- a/README.md +++ b/README.md @@ -1,121 +1,222 @@ -# Flutter Math +# Flutter Math Fork -[![Build Status](https://travis-ci.com/znjameswu/flutter_math.svg?branch=master)](https://travis-ci.com/znjameswu/flutter_math) [![codecov](https://codecov.io/gh/znjameswu/flutter_math/branch/master/graph/badge.svg)](https://codecov.io/gh/znjameswu/flutter_math) [![Pub Version](https://img.shields.io/pub/v/flutter_math_fork)](https://pub.dev/packages/flutter_math_fork) +[![Pub Version](https://img.shields.io/pub/v/flutter_math_fork)](https://pub.dev/packages/flutter_math_fork) +Math equation rendering in pure Dart and Flutter, with a parser derived from +[KaTeX](https://github.com/KaTeX/KaTeX). -## ⚠ fork +## Fork status -This is a fork of [flutter_math](https://github.com/znjameswu/flutter_math) addressing compatibility -problems while `flutter_math` is not being maintained. +`flutter_math_fork` is a maintained fork of +[flutter_math](https://github.com/znjameswu/flutter_math), with active updates +in [simpleclub/flutter_math](https://github.com/simpleclub/flutter_math) to keep +the package working on current Flutter stable releases. ---- +This release is tested with Flutter `3.38.9` on the stable channel. +## Features +* TeX math parsing and rendering in pure Flutter. +* Selectable math with copy and select-all support. +* Shaped multilingual text inside `\text{...}`, including mixed inline math. +* Manual parser and AST APIs for advanced integrations. +* TeX-style line breaking support. -Math equation rendering in pure Dart & Flutter. +Unsupported or partially supported KaTeX features are documented in +[doc/unsupported.md](doc/unsupported.md). +## Unicode and UTF-8 support -This project aims to achieve maximum compatibility and fidelity with regard to the [KaTeX](https://github.com/KaTeX/KaTeX) project, while maintaining the performance advantage of Dart and Flutter. A further [UnicodeMath](https://www.unicode.org/notes/tn28/UTN28-PlainTextMath-v3.1.pdf)-style equation editing support will be experimented in the future. +Unicode input is supported, but not as unrestricted plain-text input in every +parser mode. For multilingual text mixed with math, the recommended path is +`\text{...}`. +Short version: Unicode/UTF-8 works, but not 100% in every parser mode, +script, or font setup. -The TeX parser is a Dart port of the KaTeX parser. There are only a few unsupported features and parsing differences compared to the original KaTeX parser. List of some unsupported features can be found [here](doc/unsupported.md). +What works: -## [Online Demo](https://znjameswu.github.io/flutter_math_demo/) +* Unicode math symbols supported by the parser, such as Greek letters, + arrows, operators, and many mathematical Unicode code points. +* Unicode text inside `\text{...}`. +* Complex-script shaping inside `\text{...}` for scripts such as Bangla, + Arabic, Hindi, Japanese, and more, mixed with math on the same line. +* The same `\text{...}` shaping path in both `Math.tex` and + `SelectableMath.tex`. +* Top-level Unicode text with the default parser settings or with + `TexParserSettings(strict: Strict.ignore)`. -## Rendering Samples +Important limits: -`x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}` +* If you use `TexParserSettings(strict: Strict.error)`, top-level Unicode text + in math mode is rejected by design. +* Raw Unicode typed directly in math mode is still treated as math content, + not as normal paragraph text. +* Correct shaping still depends on the app or device having a font that + supports the script. This package can shape the text run, but it does not + bundle every script font. +* Font metrics and spacing are primarily tuned for math. Arbitrary Unicode, + especially emoji and unsupported scripts, may render with fallback fonts but + should not be considered fully TeX-equivalent. -![Example1](https://raw.githubusercontent.com/znjameswu/flutter_math/master/doc/img/delta.png) +Examples: -`i\hbar\frac{\partial}{\partial t}\Psi(\vec x,t) = -\frac{\hbar}{2m}\nabla^2\Psi(\vec x,t)+ V(\vec x)\Psi(\vec x,t)` +```dart +Math.tex(r'\text{বাংলা } + x^2 = 25'); -![Example2](https://raw.githubusercontent.com/znjameswu/flutter_math/master/doc/img/schrodinger.png) +Math.tex( + r'\text{العربية } + x^2 = 25', +); -`\hat f(\xi) = \int_{-\infty}^\infty f(x)e^{- 2\pi i \xi x}\mathrm{d}x` +SelectableMath.tex( + r'\text{हिन्दी } + \frac{a}{b}', +); -![Example3](https://raw.githubusercontent.com/znjameswu/flutter_math/master/doc/img/fourier.png) +Math.tex( + 'বাংলা 試 é', + settings: const TexParserSettings(strict: Strict.ignore), +) +``` +Rendered sample: -## How to use +`x = \frac{-b+\sqrt{b^2-4ac}}{2a}\quad \text{বাংলা}, \text{العربية}, \text{हिन्दी}, \text{日本語}, \text{中国人} + x^2 = 25` -Add `flutter_math` to your `pubspec.yaml` dependencies +![Multilingual inline sample](doc/img/unicode-inline.png) -### Mobile -Currently only Android platform has been tested. If you encounter any issues with iOS, please file them. +If you need arbitrary multilingual paragraph text, use Flutter's `Text` or +`RichText` for the prose and use `flutter_math_fork` for the math parts or for +explicit text runs inside `\text{...}`. -### Web -Web support is added in v0.1.6. It is tested for DomCanvas backend. In general it should behave largely the same with mobile. It is expected to break with CanvasKit backend. Check out the [Online Demo](https://znjameswu.github.io/flutter_math_demo/) +## Installation -## API usage (v0.2.0) -The usage is straightforward. Just `Math.tex(r'\frac a b')`. There is also optional arguments of `TexParserSettings settings`, which corresponds to Settings in KaTeX and support a subset of its features. +Add the package to your `pubspec.yaml`: -Display-style equations: -```dart -Math.tex(r'\frac a b', mathStyle: MathStyle.display) // Default +```yaml +dependencies: + flutter_math_fork: ^0.8.0 ``` -In-line equations +The current release targets Flutter `3.38.0` or newer. + +## Quick start + +Render a display equation: + ```dart -Math.tex(r'\frac a b', mathStyle: MathStyle.text) +import 'package:flutter/material.dart'; +import 'package:flutter_math_fork/flutter_math.dart'; + +Math.tex( + r'x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}', + mathStyle: MathStyle.display, + textStyle: const TextStyle(fontSize: 24), +) ``` -The default size of the equation is obtained from the build context. If you wish to specify the size, you can use `textStyle`. Note: this parameter will also change how big 1cm/1pt/1inch is rendered on the screen. If you wish to specify the size of those absolute units, use `logicalPpi` +Render inline math: ```dart Math.tex( r'\frac a b', - textStyle: TextStyle(fontSize: 42), - // logicalPpi: MathOptions.defaultLogicalPpiFor(42), + mathStyle: MathStyle.text, +) +``` + +Control sizing explicitly with `MathOptions`: + +```dart +Math.tex( + r'\int_0^\infty e^{-x^2}\,\mathrm{d}x', + options: MathOptions( + style: MathStyle.display, + fontSize: 22, + ), ) ``` -There is also a selectable variant `SelectableMath` that creates selectable and copy-able equations on both mobile and web. (EXPERIMENTAL) Users can select part of the equation and obtain the encoded TeX strings. The usage is similar to Flutter's `SelectableText`. +## Selectable math + +Use `SelectableMath` when the user should be able to select or copy the TeX: ```dart -SelectableMath.tex(r'\frac a b', textStyle: TextStyle(fontSize: 42)) +SelectableMath.tex( + r'\frac a b', + textStyle: const TextStyle(fontSize: 24), + toolbarOptions: const SelectableMathToolbarOptions( + copy: true, + selectAll: true, + ), +) ``` -If you would like to display custom styled error message, you should use `onErrorFallback` parameter. You can also process the errors in this function. But beware this function is called in build function. +Starting with `0.8.0`, `SelectableMath.toolbarOptions` uses the package-owned +`SelectableMathToolbarOptions` type instead of Flutter's deprecated +`ToolbarOptions`. + +## Error handling + +Provide `onErrorFallback` to render your own widget when parsing or building +fails: + ```dart Math.tex( - r'\garbled $tring', - textStyle: TextStyle(color: Colors.green), + r'\garbled $tring', onErrorFallback: (err) => Container( color: Colors.red, - child: Text(err.messageWithType, style: TextStyle(color: Colors.yellow)), + padding: const EdgeInsets.all(8), + child: Text( + err.messageWithType, + style: const TextStyle(color: Colors.white), + ), ), ) ``` -If you wish to have more granularity dealing with equations, you can manually invoke the parser and supply AST into the widget. +## Advanced usage + +For manual parsing and AST handling: + ```dart -SyntaxTree ast; -try { - ast = SyntaxTree(greenRoot: TexParser(r'\frac a b', TexParserSettings()).parse()); -} on ParseException catch (e) { - // Handle my error here -} - -SelectableMath( +import 'package:flutter_math_fork/flutter_math.dart'; +import 'package:flutter_math_fork/tex.dart'; + +final ast = SyntaxTree( + greenRoot: TexParser( + r'\frac a b', + const TexParserSettings(), + ).parse(), +); + +final widget = SelectableMath( ast: ast, mathStyle: MathStyle.text, - textStyle: TextStyle(fontSize: 42), -) + textStyle: const TextStyle(fontSize: 24), +); ``` -## [Line Breaking](doc/line_breaking.md) +See also: + +* [doc/line_breaking.md](doc/line_breaking.md) +* [doc/design.md](doc/design.md) + +## Rendering samples + +`x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}` + +![Quadratic equation](doc/img/delta.png) + +`i\hbar\frac{\partial}{\partial t}\Psi(\vec x,t) = -\frac{\hbar}{2m}\nabla^2\Psi(\vec x,t)+ V(\vec x)\Psi(\vec x,t)` + +![Schrodinger equation](doc/img/schrodinger.png) + +`\hat f(\xi) = \int_{-\infty}^\infty f(x)e^{- 2\pi i \xi x}\mathrm{d}x` + +![Fourier transform](doc/img/fourier.png) ## Credits -This project is possible thanks to the inspirations and resources from [the KaTeX Project](https://katex.org/), [MathJax](www.mathjax.org), [Zefyr](https://github.com/memspace/zefyr), and [CaTeX](https://github.com/simpleclub/CaTeX). - -## Goals -- [x] : TeX math parsing (See [design doc](doc/design.md)) -- [x] : AST rendering in flutter -- [x] : Selectable widget -- [x] : TeX output (WIP) -- [ ] : UnicodeMath parsing and encoding -- [ ] : [UnicodeMath](https://www.unicode.org/notes/tn28/UTN28-PlainTextMath-v3.1.pdf)-style editing -- [ ] : Breakable equations -- [ ] : MathML parsing and encoding +This project draws heavily from the work of +[KaTeX](https://katex.org/), [MathJax](https://www.mathjax.org/), +[Zefyr](https://github.com/memspace/zefyr), and +[CaTeX](https://github.com/simpleclub/CaTeX). diff --git a/doc/img/delta.png b/doc/img/delta.png index 9ea880f7..8d93671b 100644 Binary files a/doc/img/delta.png and b/doc/img/delta.png differ diff --git a/doc/img/fourier.png b/doc/img/fourier.png index 68bf3512..2615c42f 100644 Binary files a/doc/img/fourier.png and b/doc/img/fourier.png differ diff --git a/doc/img/leftright.png b/doc/img/leftright.png index 4b82a6dc..05981901 100644 Binary files a/doc/img/leftright.png and b/doc/img/leftright.png differ diff --git a/doc/img/matrix.png b/doc/img/matrix.png index f5a20a3f..33fcf756 100644 Binary files a/doc/img/matrix.png and b/doc/img/matrix.png differ diff --git a/doc/img/nary.png b/doc/img/nary.png index a10b9eb6..ac1495f9 100644 Binary files a/doc/img/nary.png and b/doc/img/nary.png differ diff --git a/doc/img/schrodinger.png b/doc/img/schrodinger.png index ddc7773a..fa72be65 100644 Binary files a/doc/img/schrodinger.png and b/doc/img/schrodinger.png differ diff --git a/doc/img/stretchyop.png b/doc/img/stretchyop.png index 1113a2c5..613a9cc3 100644 Binary files a/doc/img/stretchyop.png and b/doc/img/stretchyop.png differ diff --git a/doc/img/underover.png b/doc/img/underover.png index a385d642..d21a00ff 100644 Binary files a/doc/img/underover.png and b/doc/img/underover.png differ diff --git a/doc/img/unicode-inline.png b/doc/img/unicode-inline.png new file mode 100644 index 00000000..4d7d9074 Binary files /dev/null and b/doc/img/unicode-inline.png differ diff --git a/example/README.md b/example/README.md index a1356260..ba3d02f0 100644 --- a/example/README.md +++ b/example/README.md @@ -1,16 +1,19 @@ -# example +# flutter_math_fork example -A new Flutter project. +This directory contains the example application for `flutter_math_fork`. -## Getting Started +## Run the example -This project is a starting point for a Flutter application. +From the repository root: -A few resources to get you started if this is your first Flutter project: +```bash +cd example +flutter pub get +flutter run +``` -- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) +## Notes -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +* The example depends on the local package via `path: ../`. +* The example targets the same current Flutter baseline as the package. +* It is not intended to be published separately. diff --git a/example/pubspec.lock b/example/pubspec.lock index 5fe02485..87ac282d 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,16 +5,16 @@ packages: dependency: transitive description: name: args - sha256: c372bb384f273f0c2a8aaaa226dad84dc27c8519a691b888725dec59518ad53a - url: "https://pub.dev" + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.1" + version: "2.7.0" async: dependency: transitive description: name: async sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.13.0" boolean_selector: @@ -22,7 +22,7 @@ packages: description: name: boolean_selector sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" characters: @@ -30,65 +30,65 @@ packages: description: name: characters sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" - charcode: - dependency: transitive - description: - name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 - url: "https://pub.dev" - source: hosted - version: "1.3.1" clock: dependency: transitive description: name: clock sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" collection: dependency: transitive description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.19.1" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab - url: "https://pub.dev" + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.3" + version: "3.0.7" fake_async: dependency: transitive description: name: fake_async sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.3" ffi: dependency: transitive description: name: ffi - sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 - url: "https://pub.dev" + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.2" + version: "2.2.0" file: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" - url: "https://pub.dev" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.flutter-io.cn" source: hosted - version: "6.1.4" + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -100,15 +100,15 @@ packages: path: ".." relative: true source: path - version: "0.7.3" + version: "0.8.0" flutter_svg: dependency: transitive description: name: flutter_svg - sha256: f991fdb1533c3caeee0cdc14b04f50f0c3916f0dbcbc05237ccbe4e3c6b93f3f - url: "https://pub.dev" + sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.5" + version: "2.2.4" flutter_test: dependency: "direct dev" description: flutter @@ -118,72 +118,101 @@ packages: dependency: "direct main" description: name: flutter_tex - sha256: "8fa2079e8b338da05ceada989826bcf69ab3a9c767d710c21362dad16eca4166" - url: "https://pub.dev" + sha256: "05df1f862832a3785640ff0377863b26bde85a2fac8edad532a179226e5888c5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.3" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.flutter-io.cn" source: hosted - version: "4.0.3+4" + version: "2.1.3" google_fonts: dependency: "direct main" description: name: google_fonts - sha256: "6b6f10f0ce3c42f6552d1c70d2c28d764cf22bb487f50f66cca31dcd5194f4d6" - url: "https://pub.dev" + sha256: "2776c66b3e97c6cdd58d1bd3281548b074b64f1fd5c8f82391f7456e38849567" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.5" + hooks: + dependency: transitive + description: + name: hooks + sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + url: "https://pub.flutter-io.cn" source: hosted - version: "4.0.4" + version: "1.0.2" http: dependency: transitive description: name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" - url: "https://pub.dev" + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.13.6" + version: "1.6.0" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.dev" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.flutter-io.cn" source: hosted - version: "4.0.2" + version: "4.1.2" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" - url: "https://pub.dev" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.flutter-io.cn" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 - url: "https://pub.dev" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.dev" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.1" + version: "3.0.2" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" markdown: dependency: transitive description: name: markdown - sha256: "01512006c8429f604eb10f9848717baeaedf99e991d14a50d540d9beff08e5c6" - url: "https://pub.dev" + sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" + url: "https://pub.flutter-io.cn" source: hosted - version: "4.0.1" + version: "7.3.0" matcher: dependency: transitive description: name: matcher sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.17" material_color_utilities: @@ -191,137 +220,153 @@ packages: description: name: material_color_utilities sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c - url: "https://pub.dev" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e - url: "https://pub.dev" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.4" + version: "2.0.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "92b2ca62c8bd2b8d2f267cdfccf9bfbdb7322f778f8f91b3ce5b5cda23a3899f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.17.5" nested: dependency: transitive description: name: nested sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.flutter-io.cn" + source: hosted + version: "9.3.0" path: dependency: transitive description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.9.1" path_parsing: dependency: transitive description: name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf - url: "https://pub.dev" + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.1" + version: "1.1.0" path_provider: dependency: transitive description: name: path_provider - sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" - url: "https://pub.dev" + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.15" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" - url: "https://pub.dev" + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.27" + version: "2.2.22" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3" - url: "https://pub.dev" + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.2.3" + version: "2.6.0" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" - url: "https://pub.dev" + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.10" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" - url: "https://pub.dev" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.6" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6 - url: "https://pub.dev" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.6" + version: "2.3.0" petitparser: dependency: transitive description: name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 - url: "https://pub.dev" + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.flutter-io.cn" source: hosted - version: "5.4.0" + version: "7.0.2" platform: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" - url: "https://pub.dev" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.1.0" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - process: - dependency: transitive - description: - name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" - url: "https://pub.dev" + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.flutter-io.cn" source: hosted - version: "4.2.4" + version: "2.1.8" provider: dependency: "direct main" description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f - url: "https://pub.dev" + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.flutter-io.cn" source: hosted - version: "6.0.5" + version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" sky_engine: dependency: transitive description: flutter @@ -331,16 +376,16 @@ packages: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.10.1" + version: "1.10.2" stack_trace: dependency: transitive description: name: stack_trace sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.12.1" stream_channel: @@ -348,7 +393,7 @@ packages: description: name: stream_channel sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.4" string_scanner: @@ -356,7 +401,7 @@ packages: description: name: string_scanner sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" term_glyph: @@ -364,137 +409,209 @@ packages: description: name: term_glyph sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd - url: "https://pub.dev" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.flutter-io.cn" source: hosted - version: "0.7.4" + version: "0.7.7" tuple: dependency: transitive description: name: tuple - sha256: "0ea99cd2f9352b2586583ab2ce6489d1f95a5f6de6fb9492faaf97ae2060f0aa" - url: "https://pub.dev" + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.1" + version: "2.0.2" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c - url: "https://pub.dev" + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.2" + version: "1.4.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.3.28" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.5" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "59a230f8bf37dd8b077335d1d64d895bccef0fb14f50730e3d79e8990bf3ed2b" - url: "https://pub.dev" + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.5+1" + version: "1.1.19" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "40781fe91c6d10a617c0289f7ec16cdb2d85a7f3654af2778c6d0adbf3bf45a3" - url: "https://pub.dev" + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.5+1" + version: "1.1.13" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "6ca1298b70edcc3486fdb14032f1a186a593f1b5f6b5e82fb10febddcb1c61bb" - url: "https://pub.dev" + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.5+1" + version: "1.2.0" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 - url: "https://pub.dev" + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.flutter-io.cn" + source: hosted + version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.flutter-io.cn" source: hosted - version: "15.0.0" + version: "1.1.1" webview_flutter: dependency: transitive description: name: webview_flutter - sha256: "392c1d83b70fe2495de3ea2c84531268d5b8de2de3f01086a53334d8b6030a88" - url: "https://pub.dev" + sha256: a3da219916aba44947d3a5478b1927876a09781174b5a2b67fa5be0555154bf9 + url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.4" + version: "4.13.1" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - sha256: "8b3b2450e98876c70bfcead876d9390573b34b9418c19e28168b74f6cb252dbd" - url: "https://pub.dev" + sha256: "2a03df01df2fd30b075d1e7f24c28aee593f2e5d5ac4c3c4283c5eda63717b24" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.10.4" + version: "4.10.13" webview_flutter_platform_interface: dependency: transitive description: name: webview_flutter_platform_interface - sha256: "812165e4e34ca677bdfbfa58c01e33b27fd03ab5fa75b70832d4b7d4ca1fa8cf" - url: "https://pub.dev" + sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.9.5" + version: "2.14.0" webview_flutter_plus: dependency: transitive description: name: webview_flutter_plus - sha256: bea8756ae096529254725def7c4a633851a785c7d49206e0817125ab02b14307 - url: "https://pub.dev" + sha256: "43628503cb687c3e02b34cf4c21c67a347b2e27563e006e0789676c8ed8ea65b" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.3.0+2" + version: "0.4.20" webview_flutter_wkwebview: dependency: transitive description: name: webview_flutter_wkwebview - sha256: a5364369c758892aa487cbf59ea41d9edd10f9d9baf06a94e80f1bd1b4c7bbc0 - url: "https://pub.dev" - source: hosted - version: "2.9.5" - win32: - dependency: transitive - description: - name: win32 - sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" - url: "https://pub.dev" + sha256: fc0af89d403e1c053f03d023d97550412fa79f35332e2939514c82e6fe633198 + url: "https://pub.flutter-io.cn" source: hosted - version: "4.1.4" + version: "3.23.8" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 - url: "https://pub.dev" + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.0" + version: "1.1.0" xml: dependency: transitive description: name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" - url: "https://pub.dev" + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.flutter-io.cn" source: hosted - version: "6.3.0" + version: "3.1.3" sdks: - dart: ">=3.7.0-0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.4" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 62ad6730..a128a620 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -7,7 +7,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.38.0" dependencies: flutter: @@ -16,7 +17,7 @@ dependencies: flutter_math_fork: path: ../ - provider: any + provider: ^6.1.5+1 flutter_tex: ^4.0.3+4 google_fonts: ^4.0.4 diff --git a/lib/flutter_math.dart b/lib/flutter_math.dart index cc757a85..ccb11c57 100644 --- a/lib/flutter_math.dart +++ b/lib/flutter_math.dart @@ -11,4 +11,5 @@ export 'src/parser/tex/parse_error.dart'; export 'src/parser/tex/settings.dart'; export 'src/widgets/exception.dart'; export 'src/widgets/math.dart'; -export 'src/widgets/selectable.dart' show SelectableMath; +export 'src/widgets/selectable.dart' + show SelectableMath, SelectableMathToolbarOptions; diff --git a/lib/src/ast/nodes/accent.dart b/lib/src/ast/nodes/accent.dart index 1f4cfcda..278cd7a3 100644 --- a/lib/src/ast/nodes/accent.dart +++ b/lib/src/ast/nodes/accent.dart @@ -105,7 +105,7 @@ class AccentNode extends SlotableNode { padding: EdgeInsets.only(bottom: 3 * defaultRuleThickness), child: Container( width: constraints.minWidth, - height: defaultRuleThickness, // TODO minRuleThickness + height: defaultRuleThickness, // minRuleThickness color: options.color, ), ); diff --git a/lib/src/ast/nodes/accent_under.dart b/lib/src/ast/nodes/accent_under.dart index 9f287760..5745305e 100644 --- a/lib/src/ast/nodes/accent_under.dart +++ b/lib/src/ast/nodes/accent_under.dart @@ -52,7 +52,7 @@ class AccentUnderNode extends SlotableNode { padding: EdgeInsets.only(top: 3 * defaultRuleThickness), child: Container( width: constraints.minWidth, - height: defaultRuleThickness, // TODO minRuleThickness + height: defaultRuleThickness, // minRuleThickness color: options.color, ), ); diff --git a/lib/src/ast/nodes/enclosure.dart b/lib/src/ast/nodes/enclosure.dart index 51bdb8a9..c0c1fae5 100644 --- a/lib/src/ast/nodes/enclosure.dart +++ b/lib/src/ast/nodes/enclosure.dart @@ -60,7 +60,7 @@ class EnclosureNode extends SlotableNode { ? BoxDecoration( color: backgroundcolor, border: Border.all( - // TODO minRuleThickness + // minRuleThickness width: options.fontMetrics.fboxrule.cssEm.toLpUnder(options), color: bordercolor ?? options.color, diff --git a/lib/src/ast/nodes/frac.dart b/lib/src/ast/nodes/frac.dart index 324d2849..15973783 100644 --- a/lib/src/ast/nodes/frac.dart +++ b/lib/src/ast/nodes/frac.dart @@ -23,7 +23,7 @@ class FracNode extends SlotableNode { final Measurement? barSize; /// Whether it is a continued frac `\cfrac`. - final bool continued; // TODO continued + final bool continued; // continued FracNode({ // this.options, diff --git a/lib/src/ast/nodes/matrix.dart b/lib/src/ast/nodes/matrix.dart index b545e570..8006e27b 100644 --- a/lib/src/ast/nodes/matrix.dart +++ b/lib/src/ast/nodes/matrix.dart @@ -178,7 +178,7 @@ class MatrixNode extends SlotableNode { .mapIndexed((index, result) => result == null ? null : CustomLayoutId(id: index, child: result.widget)) - .whereNotNull() + .nonNulls .toList(growable: false), ), ), @@ -206,12 +206,12 @@ class MatrixNode extends SlotableNode { @override MatrixNode updateChildren(List newChildren) { assert(newChildren.length >= rows * cols); - var body = List>.generate( + final updatedBody = List>.generate( rows, - (i) => newChildren.sublist(i * cols + (i + 1) * cols), + (i) => newChildren.sublist(i * cols, (i + 1) * cols), growable: false, ); - return copyWith(body: body); + return copyWith(body: updatedBody); } MatrixNode copyWith({ @@ -340,15 +340,13 @@ class MatrixLayoutDelegate extends IntrinsicLayoutDelegate { // Determine position of children final childPos = List.generate(rows * cols, (index) { final col = index % cols; - switch (columnAligns[col]) { - case MatrixColumnAlign.left: - return colPos[col]; - case MatrixColumnAlign.right: - return colPos[col] + colWidths[col] - childWidths[index]; - case MatrixColumnAlign.center: - default: - return colPos[col] + (colWidths[col] - childWidths[index]) / 2; - } + return switch (columnAligns[col]) { + MatrixColumnAlign.left => colPos[col], + MatrixColumnAlign.right => + colPos[col] + colWidths[col] - childWidths[index], + MatrixColumnAlign.center => + colPos[col] + (colWidths[col] - childWidths[index]) / 2, + }; }, growable: false); if (!isComputingIntrinsics) { @@ -461,7 +459,8 @@ class MatrixLayoutDelegate extends IntrinsicLayoutDelegate { paint); } break; - default: + case MatrixSeparatorStyle.none: + break; } } @@ -493,7 +492,8 @@ class MatrixLayoutDelegate extends IntrinsicLayoutDelegate { paint); } break; - default: + case MatrixSeparatorStyle.none: + break; } } } diff --git a/lib/src/ast/nodes/over.dart b/lib/src/ast/nodes/over.dart index 76ec74cb..1f6ea7af 100644 --- a/lib/src/ast/nodes/over.dart +++ b/lib/src/ast/nodes/over.dart @@ -65,12 +65,12 @@ class OverNode extends SlotableNode { @override AtomType get leftType => stackRel ? AtomType.rel - : AtomType.ord; // TODO: they should align with binrelclass with base + : AtomType.ord; // they should align with binrelclass with base @override AtomType get rightType => stackRel ? AtomType.rel - : AtomType.ord; // TODO: they should align with binrelclass with base + : AtomType.ord; // they should align with binrelclass with base @override bool shouldRebuildWidget(MathOptions oldOptions, MathOptions newOptions) => diff --git a/lib/src/ast/nodes/phantom.dart b/lib/src/ast/nodes/phantom.dart index f9d0555b..624f785c 100644 --- a/lib/src/ast/nodes/phantom.dart +++ b/lib/src/ast/nodes/phantom.dart @@ -12,7 +12,7 @@ class PhantomNode extends LeafNode { Mode get mode => Mode.math; /// The phantomed child. - // TODO: suppress editbox in edit mode + // suppress editbox in edit mode // If we use arbitrary GreenNode here, then we will face the danger of // transparent node final EquationRowNode phantomChild; diff --git a/lib/src/ast/nodes/sqrt.dart b/lib/src/ast/nodes/sqrt.dart index f0b6d941..f5de8ac9 100644 --- a/lib/src/ast/nodes/sqrt.dart +++ b/lib/src/ast/nodes/sqrt.dart @@ -316,8 +316,8 @@ Widget sqrtSvg({ final extraViniculum = 0.0; //math.max(0.0, options) // final ruleWidth = - // options.fontMetrics.sqrtRuleThickness.cssEm.toLpUnder(options); - // TODO: support Settings.minRuleThickness. + // options.fontMetrics.sqrtRuleThickness.cssEm.toLpUnder(options); + // support Settings.minRuleThickness. // These are the known height + depth for \u221A if (delimConf != null) { diff --git a/lib/src/ast/nodes/symbol.dart b/lib/src/ast/nodes/symbol.dart index 3ec674b9..cbbb8280 100644 --- a/lib/src/ast/nodes/symbol.dart +++ b/lib/src/ast/nodes/symbol.dart @@ -86,7 +86,7 @@ class SymbolNode extends LeafNode { } return SyntaxNode(parent: null, value: res, pos: 0).buildWidget(options); } else { - // TODO: log a warning here. + //log a warning here. return BuildResult( widget: Container( height: 0, diff --git a/lib/src/ast/nodes/text_run.dart b/lib/src/ast/nodes/text_run.dart new file mode 100644 index 00000000..fb0a8a22 --- /dev/null +++ b/lib/src/ast/nodes/text_run.dart @@ -0,0 +1,118 @@ +import 'package:flutter/widgets.dart'; + +import '../../utils/unicode_literal.dart'; +import '../options.dart'; +import '../syntax_tree.dart'; +import '../types.dart'; + +const _defaultTextFont = FontOptions(); +const _katexTextFontFamilies = { + 'AMS', + 'Caligraphic', + 'Fraktur', + 'Main', + 'Math', + 'SansSerif', + 'Script', + 'Size1', + 'Size2', + 'Size3', + 'Size4', + 'Typewriter', +}; + +/// A shaped text run rendered as a single Flutter text paragraph. +class TextRunNode extends LeafNode { + final String text; + final FontOptions? overrideFont; + final AtomType _leftType; + final AtomType _rightType; + + TextRunNode({ + required this.text, + FontOptions? overrideFont, + AtomType leftType = AtomType.ord, + AtomType rightType = AtomType.ord, + }) : assert(text.isNotEmpty), + overrideFont = overrideFont, + _leftType = leftType, + _rightType = rightType; + + @override + Mode get mode => Mode.text; + + @override + BuildResult buildWidget( + MathOptions options, + List childBuildResults, + ) { + final baseStyle = (options.textModeTextStyle ?? const TextStyle()).copyWith( + color: options.color, + fontSize: options.fontSize, + ); + final explicitFont = overrideFont ?? options.textFontOptions; + final effectiveFont = explicitFont ?? _defaultTextFont; + + return BuildResult( + options: options, + widget: RichText( + textDirection: _resolveTextDirection(text), + text: TextSpan( + text: text, + locale: options.textLocale, + style: baseStyle.copyWith( + fontFamily: baseStyle.fontFamily ?? + _resolveFontFamily(effectiveFont.fontFamily), + fontWeight: explicitFont?.fontWeight ?? baseStyle.fontWeight, + fontStyle: explicitFont?.fontShape ?? baseStyle.fontStyle, + ), + ), + overflow: TextOverflow.visible, + softWrap: false, + ), + ); + } + + @override + bool shouldRebuildWidget(MathOptions oldOptions, MathOptions newOptions) => + oldOptions.color != newOptions.color || + oldOptions.sizeMultiplier != newOptions.sizeMultiplier || + oldOptions.textFontOptions != newOptions.textFontOptions || + oldOptions.textModeTextStyle != newOptions.textModeTextStyle || + oldOptions.textLocale != newOptions.textLocale; + + @override + AtomType get leftType => _leftType; + + @override + AtomType get rightType => _rightType; + + @override + Map toJson() => super.toJson() + ..addAll({ + 'mode': mode.toString(), + 'text': unicodeLiteral(text), + if (overrideFont != null) 'overrideFont': overrideFont.toString(), + if (_leftType != AtomType.ord) 'leftType': _leftType.toString(), + if (_rightType != AtomType.ord) 'rightType': _rightType.toString(), + }); + + String _resolveFontFamily(String fontFamily) { + if (_katexTextFontFamilies.contains(fontFamily)) { + return 'packages/flutter_math_fork/KaTeX_$fontFamily'; + } + return fontFamily; + } + + TextDirection? _resolveTextDirection(String text) { + if (text.runes.any(_isRtlCodepoint)) { + return TextDirection.rtl; + } + return null; + } + + bool _isRtlCodepoint(int codepoint) => + (codepoint >= 0x0590 && codepoint <= 0x08FF) || + (codepoint >= 0xFB1D && codepoint <= 0xFDFF) || + (codepoint >= 0xFE70 && codepoint <= 0xFEFF); +} diff --git a/lib/src/ast/options.dart b/lib/src/ast/options.dart index 98f19b82..ac08fa00 100644 --- a/lib/src/ast/options.dart +++ b/lib/src/ast/options.dart @@ -45,6 +45,12 @@ class MathOptions { /// Math-mode font options will override each other. final FontOptions? mathFontOptions; + /// Flutter text style used for shaped text-mode runs such as `\text{...}`. + final TextStyle? textModeTextStyle; + + /// Locale forwarded to shaped text-mode runs. + final Locale? textLocale; + /// Size multiplier applied to equation elements. late final double sizeMultiplier = this.size.sizeMultiplier; @@ -78,6 +84,8 @@ class MathOptions { this.sizeUnderTextStyle = MathSize.normalsize, this.textFontOptions, this.mathFontOptions, + this.textModeTextStyle, + this.textLocale, // required this.maxSize, // required this.minRuleThickness, }); @@ -95,6 +103,8 @@ class MathOptions { MathSize sizeUnderTextStyle = MathSize.normalsize, FontOptions? textFontOptions, FontOptions? mathFontOptions, + TextStyle? textModeTextStyle, + Locale? textLocale, double? fontSize, double? logicalPpi, // required this.maxSize, @@ -114,6 +124,8 @@ class MathOptions { sizeUnderTextStyle: sizeUnderTextStyle, mathFontOptions: mathFontOptions, textFontOptions: textFontOptions, + textModeTextStyle: textModeTextStyle, + textLocale: textLocale, ); } @@ -233,6 +245,8 @@ class MathOptions { MathSize? sizeUnderTextStyle, FontOptions? textFontOptions, FontOptions? mathFontOptions, + TextStyle? textModeTextStyle, + Locale? textLocale, // double maxSize, // num minRuleThickness, }) => @@ -244,6 +258,8 @@ class MathOptions { sizeUnderTextStyle: sizeUnderTextStyle ?? this.sizeUnderTextStyle, textFontOptions: textFontOptions ?? this.textFontOptions, mathFontOptions: mathFontOptions ?? this.mathFontOptions, + textModeTextStyle: textModeTextStyle ?? this.textModeTextStyle, + textLocale: textLocale ?? this.textLocale, // maxSize: maxSize ?? this.maxSize, // minRuleThickness: minRuleThickness ?? this.minRuleThickness, ); diff --git a/lib/src/ast/style.dart b/lib/src/ast/style.dart index f954e470..d049fcc2 100644 --- a/lib/src/ast/style.dart +++ b/lib/src/ast/style.dart @@ -1,16 +1,34 @@ import 'size.dart'; -/// Math styles for equation elements. +/// Controls how TeX layout is built for an expression. /// -/// \displaystyle \textstyle etc. +/// Use [display] for standalone equations and [text] for inline math inside a +/// sentence. The cramped and script variants are TeX layout styles that are +/// mostly used internally for fractions, subscripts, superscripts, and nested +/// expressions. enum MathStyle { + /// Display-style math for standalone equations. display, + + /// Display-style math with cramped vertical spacing. displayCramped, + + /// Inline math used inside surrounding text. text, + + /// Inline math with cramped vertical spacing. textCramped, + + /// Smaller style used in superscripts and similar nested layouts. script, + + /// Script style with cramped vertical spacing. scriptCramped, + + /// The smallest style used in deeply nested superscripts and subscripts. scriptscript, + + /// Scriptscript style with cramped vertical spacing. scriptscriptCramped, } diff --git a/lib/src/ast/syntax_tree.dart b/lib/src/ast/syntax_tree.dart index 3491b637..8992e357 100644 --- a/lib/src/ast/syntax_tree.dart +++ b/lib/src/ast/syntax_tree.dart @@ -16,7 +16,9 @@ import '../widgets/controller.dart'; import '../widgets/mode.dart'; import '../widgets/selectable.dart'; import 'nodes/space.dart'; +import 'nodes/symbol.dart'; import 'nodes/sqrt.dart'; +import 'nodes/text_run.dart'; import 'options.dart'; import 'spacing.dart'; import 'types.dart'; @@ -218,9 +220,9 @@ abstract class GreenNode { /// [children] stores structural information of the Red-Green Tree. /// Used for green tree updates. The order of children should strictly /// adheres to the cursor-visiting order in editing mode, in order to get a - /// correct cursor range in the editing mode. E.g., for [SqrtNode], when - /// moving cursor from left to right, the cursor first enters index, then - /// base, so it should return [index, base]. + /// correct cursor range in the editing mode. For example, for [SqrtNode], + /// when moving cursor from left to right, the cursor first enters the index, + /// then the base, so it should return `index` before `base`. /// /// Please ensure [children] works in the same order as [updateChildren], /// [computeChildOptions], and [buildWidget]. @@ -487,8 +489,16 @@ class EquationRowNode extends ParentableNode final flattenedBuildResults = childBuildResults .expand((result) => result!.results ?? [result]) .toList(growable: false); - final flattenedChildOptions = - flattenedBuildResults.map((e) => e.options).toList(growable: false); + final renderedChildren = _collapseTextRunsForRendering( + flattenedChildList, + flattenedBuildResults, + ); + final renderedNodes = + renderedChildren.map((entry) => entry.node).toList(growable: false); + final renderedBuildResults = + renderedChildren.map((entry) => entry.result).toList(growable: false); + final renderedChildOptions = + renderedBuildResults.map((e) => e.options).toList(growable: false); // assert(flattenedChildList.length == actualChildWidgets.length); // We need to calculate spacings between nodes @@ -498,11 +508,11 @@ class EquationRowNode extends ParentableNode // - There could aligners and spacers. We need to calculate the spacing // after filtering them out, hence the [traverseNonSpaceNodes] final childSpacingConfs = List.generate( - flattenedChildList.length, + renderedNodes.length, (index) { - final e = flattenedChildList[index]; + final e = renderedNodes[index]; return _NodeSpacingConf( - e.leftType, e.rightType, flattenedChildOptions[index], 0.0); + e.leftType, e.rightType, renderedChildOptions[index], 0.0); }, growable: false, ); @@ -547,12 +557,12 @@ class EquationRowNode extends ParentableNode _key = GlobalKey(); final lineChildren = List.generate( - flattenedBuildResults.length, + renderedBuildResults.length, (index) => LineElement( - child: flattenedBuildResults[index].widget, - canBreakBefore: false, // TODO - alignerOrSpacer: flattenedChildList[index] is SpaceNode && - (flattenedChildList[index] as SpaceNode).alignerOrSpacer, + child: renderedBuildResults[index].widget, + canBreakBefore: false, + alignerOrSpacer: renderedNodes[index] is SpaceNode && + (renderedNodes[index] as SpaceNode).alignerOrSpacer, trailingMargin: childSpacingConfs[index].spacingAfter, ), growable: false, @@ -813,7 +823,6 @@ enum AtomType { inner, spacing, // symbols - } /// Only for improvisional use during parsing. Do not use. @@ -858,6 +867,118 @@ class BuildResult { }); } +List<_RenderedNodeResult> _collapseTextRunsForRendering( + List nodes, + List buildResults, +) { + assert(nodes.length == buildResults.length); + + final collapsed = <_RenderedNodeResult>[]; + final currentRun = <_RenderedNodeResult>[]; + var currentRunHasComplexShaping = false; + + void flushCurrentRun() { + if (currentRun.isEmpty) { + return; + } + + if (currentRun.length > 1 && currentRunHasComplexShaping) { + final firstNode = currentRun.first.node as SymbolNode; + final lastNode = currentRun.last.node; + final text = StringBuffer(); + for (final entry in currentRun) { + text.write((entry.node as SymbolNode).symbol); + } + final textRunNode = TextRunNode( + text: text.toString(), + overrideFont: firstNode.overrideFont, + leftType: firstNode.leftType, + rightType: lastNode.rightType, + ); + collapsed.add( + _RenderedNodeResult( + textRunNode, + textRunNode.buildWidget(currentRun.first.result.options, const []), + ), + ); + } else { + collapsed.addAll(currentRun); + } + currentRun.clear(); + currentRunHasComplexShaping = false; + } + + for (var index = 0; index < nodes.length; index++) { + final entry = _RenderedNodeResult(nodes[index], buildResults[index]); + if (_canCollapseIntoTextRun(entry, currentRun)) { + currentRun.add(entry); + currentRunHasComplexShaping = currentRunHasComplexShaping || + _symbolUsesComplexShaping((entry.node as SymbolNode).symbol); + continue; + } + + flushCurrentRun(); + if (_isTextRunCandidate(entry)) { + currentRun.add(entry); + currentRunHasComplexShaping = + _symbolUsesComplexShaping((entry.node as SymbolNode).symbol); + } else { + collapsed.add(entry); + } + } + + flushCurrentRun(); + return collapsed; +} + +bool _canCollapseIntoTextRun( + _RenderedNodeResult entry, + List<_RenderedNodeResult> currentRun, +) { + if (!_isTextRunCandidate(entry)) { + return false; + } + final node = entry.node as SymbolNode; + + if (currentRun.isEmpty) { + return true; + } + + final firstNode = currentRun.first.node as SymbolNode; + return firstNode.overrideFont == node.overrideFont && + firstNode.overrideAtomType == node.overrideAtomType && + _sameTextRunStyle(currentRun.first.result.options, entry.result.options); +} + +bool _sameTextRunStyle(MathOptions left, MathOptions right) => + left.color == right.color && + left.sizeMultiplier == right.sizeMultiplier && + left.textFontOptions == right.textFontOptions && + left.textModeTextStyle == right.textModeTextStyle && + left.textLocale == right.textLocale; + +bool _isTextRunCandidate(_RenderedNodeResult entry) { + final node = entry.node; + return node is SymbolNode && node.mode == Mode.text && !node.variantForm; +} + +bool _symbolUsesComplexShaping(String text) => text.runes.any( + _isComplexShapingCodepoint, + ); + +bool _isComplexShapingCodepoint(int codepoint) => + _isBrahmicCodepoint(codepoint) || _isArabicCodepoint(codepoint); + +bool _isBrahmicCodepoint(int codepoint) => + codepoint >= 0x0900 && codepoint <= 0x109F; + +bool _isArabicCodepoint(int codepoint) => + (codepoint >= 0x0600 && codepoint <= 0x06FF) || + (codepoint >= 0x0750 && codepoint <= 0x077F) || + (codepoint >= 0x08A0 && codepoint <= 0x08FF) || + (codepoint >= 0xFB50 && codepoint <= 0xFDFF) || + (codepoint >= 0xFE70 && codepoint <= 0xFEFF); + void _traverseNonSpaceNodes( List<_NodeSpacingConf> childTypeList, void Function(_NodeSpacingConf? prev, _NodeSpacingConf? curr) callback, @@ -889,3 +1010,10 @@ class _NodeSpacingConf { this.spacingAfter, ); } + +class _RenderedNodeResult { + final GreenNode node; + final BuildResult result; + + const _RenderedNodeResult(this.node, this.result); +} diff --git a/lib/src/encoder/tex/encoder.dart b/lib/src/encoder/tex/encoder.dart index 219f2d63..0d424403 100644 --- a/lib/src/encoder/tex/encoder.dart +++ b/lib/src/encoder/tex/encoder.dart @@ -194,7 +194,7 @@ class TexCommandEncodeResult extends EncodeResult { if (index < numOptionalArgs) { return string.isEmpty ? '' : '[$string]'; } else { - return '{$string}'; // TODO optimize + return '{$string}'; // optimize } }, ).join(); diff --git a/lib/src/encoder/tex/functions/frac.dart b/lib/src/encoder/tex/functions/frac.dart index a68a2b63..b5425ff2 100644 --- a/lib/src/encoder/tex/functions/frac.dart +++ b/lib/src/encoder/tex/functions/frac.dart @@ -98,7 +98,6 @@ final _fracOptimizationEntries = [ final res = TexCommandEncodeResult( command: '\\genfrac', args: [ - // TODO leftRight.leftDelim == null ? null : SymbolNode(symbol: leftRight.leftDelim!), diff --git a/lib/src/encoder/tex/functions/style.dart b/lib/src/encoder/tex/functions/style.dart index 18738a21..86f77559 100644 --- a/lib/src/encoder/tex/functions/style.dart +++ b/lib/src/encoder/tex/functions/style.dart @@ -78,10 +78,12 @@ EncodeResult _optionsDiffEncode(OptionsDiff diff, List children) { } } if (diff.color != null) { + final rgbHex = + (diff.color!.toARGB32() & 0x00ffffff).toRadixString(16).padLeft(6, '0'); res = TexCommandEncodeResult( command: '\\textcolor', args: [ - '#${diff.color!.value.toRadixString(16).padLeft(6, '0')}', + '#$rgbHex', res, ], ); diff --git a/lib/src/font/metrics/font_metrics.dart b/lib/src/font/metrics/font_metrics.dart index 79ca00d3..53865a0d 100644 --- a/lib/src/font/metrics/font_metrics.dart +++ b/lib/src/font/metrics/font_metrics.dart @@ -209,7 +209,7 @@ CharacterMetrics? getCharacterMetrics( final extraCh = extraCharacterMap[character[0]]?.codeUnitAt(0); if (extraCh != null) { - return metricsMapFont[ch]; + return metricsMapFont[extraCh]; } if (mode == Mode.text && supportedCodepoint(ch)) { // We don't typically have font metrics for Asian scripts. @@ -241,7 +241,5 @@ FontMetrics getGlobalMetrics(MathSize size) { case MathSize.huge: case MathSize.HUGE: return textFontMetrics; - default: - throw ArgumentError(size); } } diff --git a/lib/src/font/metrics/unicode_scripts.dart b/lib/src/font/metrics/unicode_scripts.dart index b186551d..423dc4be 100644 --- a/lib/src/font/metrics/unicode_scripts.dart +++ b/lib/src/font/metrics/unicode_scripts.dart @@ -22,6 +22,15 @@ const Map>> scriptData = { [0x0400, 0x04ff] ], + // Arabic and related blocks used for Arabic-script languages. + 'arabic': [ + [0x0600, 0x06FF], // Arabic + [0x0750, 0x077F], // Arabic Supplement + [0x08A0, 0x08FF], // Arabic Extended-A + [0xFB50, 0xFDFF], // Arabic Presentation Forms-A + [0xFE70, 0xFEFF], // Arabic Presentation Forms-B + ], + // The Brahmic scripts of South and Southeast Asia // Devanagari (0900–097F) // Bengali (0980–09FF) @@ -51,7 +60,7 @@ const Map>> scriptData = { [0x3000, 0x30FF], // CJK symbols and punctuation, Hiragana, Katakana [0x4E00, 0x9FAF], // CJK ideograms [0xFF00, 0xFF60], // Fullwidth punctuation - // TODO: add halfwidth Katakana and Romanji glyphs + // add halfwidth Katakana and Romanji glyphs ], // Korean diff --git a/lib/src/parser/tex/functions/katex_base/delimsizing.dart b/lib/src/parser/tex/functions/katex_base/delimsizing.dart index f603cd10..1b087e4f 100644 --- a/lib/src/parser/tex/functions/katex_base/delimsizing.dart +++ b/lib/src/parser/tex/functions/katex_base/delimsizing.dart @@ -168,7 +168,7 @@ String? _checkDelimiter(GreenNode delim, FunctionContext context) { return delim.symbol; } } else { - // TODO: this throw omitted the token location + //this throw omitted the token location throw ParseException( "Invalid delimiter '${delim.symbol}' after '${context.funcName}'"); } diff --git a/lib/src/parser/tex/functions/katex_base/font.dart b/lib/src/parser/tex/functions/katex_base/font.dart index cf02758a..b138108d 100644 --- a/lib/src/parser/tex/functions/katex_base/font.dart +++ b/lib/src/parser/tex/functions/katex_base/font.dart @@ -62,7 +62,6 @@ GreenNode _fontHandler(TexParser parser, FunctionContext context) { GreenNode _boldSymbolHandler(TexParser parser, FunctionContext context) { final body = parser.parseArgNode(mode: null, optional: false)!; - // TODO // amsbsy.sty's \boldsymbol uses \binrel spacing to inherit the // argument's bin|rel|ord status return StyleNode( diff --git a/lib/src/parser/tex/macros.dart b/lib/src/parser/tex/macros.dart index 67a70a1f..55eeb9ca 100644 --- a/lib/src/parser/tex/macros.dart +++ b/lib/src/parser/tex/macros.dart @@ -134,7 +134,7 @@ String newcommand(MacroContext context, bool existsOK, bool nonexistsOK) { var argText = ''; var token = context.expandNextToken(); while (token.text != "]" && token.text != "EOF") { - // TODO: Should properly expand arg, e.g., ignore {}s + // Should properly expand arg, e.g., ignore {}s argText += token.text; token = context.expandNextToken(); } @@ -352,7 +352,7 @@ final Map builtinMacros = { // \newcommand{\macro}[args]{definition} // \renewcommand{\macro}[args]{definition} -// TODO: Optional arguments: \newcommand{\macro}[args][default]{definition} +// Optional arguments: \newcommand{\macro}[args][default]{definition} '\\newcommand': MacroDefinition.fromCtxString( (context) => newcommand(context, false, true)), @@ -396,7 +396,7 @@ final Map builtinMacros = { // '\\aa': MacroDefinition.fromString("\\r a"), // '\\AA': MacroDefinition.fromString("\\r A"), -// TODO these should be migrated into renderconfigs +// these should be migrated into renderconfigs // Characters omitted from Unicode range 1D400–1D7FF '\u212C': MacroDefinition.fromString("\\mathscr{B}"), // script '\u2130': MacroDefinition.fromString("\\mathscr{E}"), @@ -429,7 +429,7 @@ final Map builtinMacros = { // It's thus treated like a \mathrel, but defined by a symbol that has zero // width but extends to the right. We use \rlap to get that spacing. // For MathML we write U+0338 here. buildMathML.js will then do the overlay. -// TODO fold 'not' with applicable operators +// fold 'not' with applicable operators // defineMacro( // "\\not", // MacroDefinition.fromString( @@ -460,7 +460,7 @@ final Map builtinMacros = { // The KaTeX fonts have corners at codepoints that don't match Unicode. // For MathML purposes, use the Unicode code point. -// TODO strip useless @ +// strip useless @ '\\ulcorner': MacroDefinition.fromString("\\@ulcorner"), '\\urcorner': MacroDefinition.fromString("\\@urcorner"), '\\llcorner': MacroDefinition.fromString("\\@llcorner"), @@ -473,7 +473,7 @@ final Map builtinMacros = { // \kern6\p@\hbox{.}\hbox{.}\hbox{.}}} // We'll call \varvdots, which gets a glyph from symbols.js. // The zero-width rule gets us an equivalent to the vertical 6pt kern. -// TODO should we accept \vdots's kern ? +//should we accept \vdots's kern ? '\\vdots': MacroDefinition.fromString("\\mathord{\\varvdots\\rule{0pt}{15pt}}"), '\u22ee': MacroDefinition.fromString("\\vdots"), @@ -484,7 +484,7 @@ final Map builtinMacros = { // Italic Greek capital letters. AMS defines these with \DeclareMathSymbol, // but they are equivalent to \mathit{\Letter}. -// TODO make them as overrided fonts +// make them as overrided fonts '\\varGamma': MacroDefinition.fromString("\\mathit{\\Gamma}"), '\\varDelta': MacroDefinition.fromString("\\mathit{\\Delta}"), '\\varTheta': MacroDefinition.fromString("\\mathit{\\Theta}"), @@ -505,7 +505,7 @@ final Map builtinMacros = { // \mkern-\thinmuskip{:}\mskip6muplus1mu\relax} // \newcommand{\boxed}[1]{\fbox{\m@th$\displaystyle#1$}} -// TODO fbox +//fbox '\\boxed': MacroDefinition.fromString("\\fbox{\$\\displaystyle{#1}\$}"), // \def\iff{\DOTSB\;\Longleftrightarrow\;} @@ -518,7 +518,7 @@ final Map builtinMacros = { // AMSMath's automatic \dots, based on \mdots@@ macro. '\\dots': MacroDefinition.fromCtxString((context) { - // TODO: If used in text mode, should expand to \textellipsis. + // If used in text mode, should expand to \textellipsis. // However, in KaTeX, \textellipsis and \ldots behave the same // (in text mode), and it's unlikely we'd see any of the math commands // that affect the behavior of \dots when in text mode. So fine for now @@ -567,32 +567,32 @@ final Map builtinMacros = { '\\tmspace': MacroDefinition.fromString( "\\TextOrMath{\\kern#1#3}{\\mskip#1#2}\\relax"), // \renewcommand{\,}{\tmspace+\thinmuskip{.1667em}} -// TODO: math mode should use \thinmuskip +// math mode should use \thinmuskip '\\,': MacroDefinition.fromString("\\tmspace+{3mu}{.1667em}"), // \let\thinspace\, '\\thinspace': MacroDefinition.fromString("\\,"), // \def\>{\mskip\medmuskip} // \renewcommand{\:}{\tmspace+\medmuskip{.2222em}} -// TODO: \> and math mode of \: should use \medmuskip = 4mu plus 2mu minus 4mu +// \> and math mode of \: should use \medmuskip = 4mu plus 2mu minus 4mu '\\>': MacroDefinition.fromString("\\mskip{4mu}"), '\\:': MacroDefinition.fromString("\\tmspace+{4mu}{.2222em}"), // \let\medspace\: '\\medspace': MacroDefinition.fromString("\\:"), // \renewcommand{\;}{\tmspace+\thickmuskip{.2777em}} -// TODO: math mode should use \thickmuskip = 5mu plus 5mu +//math mode should use \thickmuskip = 5mu plus 5mu '\\;': MacroDefinition.fromString("\\tmspace+{5mu}{.2777em}"), // \let\thickspace\; '\\thickspace': MacroDefinition.fromString("\\;"), // \renewcommand{\!}{\tmspace-\thinmuskip{.1667em}} -// TODO: math mode should use \thinmuskip +// math mode should use \thinmuskip '\\!': MacroDefinition.fromString("\\tmspace-{3mu}{.1667em}"), // \let\negthinspace\! '\\negthinspace': MacroDefinition.fromString("\\!"), // \newcommand{\negmedspace}{\tmspace-\medmuskip{.2222em}} -// TODO: math mode should use \medmuskip +// math mode should use \medmuskip '\\negmedspace': MacroDefinition.fromString("\\tmspace-{4mu}{.2222em}"), // \newcommand{\negthickspace}{\tmspace-\thickmuskip{.2777em}} -// TODO: math mode should use \thickmuskip +//math mode should use \thickmuskip '\\negthickspace': MacroDefinition.fromString("\\tmspace-{5mu}{.277em}"), // \def\enspace{\kern.5em } '\\enspace': MacroDefinition.fromString("\\kern.5em "), @@ -604,7 +604,7 @@ final Map builtinMacros = { '\\qquad': MacroDefinition.fromString("\\hskip2em\\relax"), // \tag@in@display form of \tag -// TODO tag +// tag '\\tag': MacroDefinition.fromString("\\@ifstar\\tag@literal\\tag@paren"), '\\tag@paren': MacroDefinition.fromString("\\tag@literal{({#1})}"), '\\tag@literal': MacroDefinition.fromCtxString((context) { @@ -622,11 +622,11 @@ final Map builtinMacros = { // \renewcommand{\pmod}[1]{\pod{{\operator@font mod}\mkern6mu#1}} // \newcommand{\mod}[1]{\allowbreak\if@display\mkern18mu // \else\mkern12mu\fi{\operator@font mod}\,\,#1} -// TODO: math mode should use \medmuskip = 4mu plus 2mu minus 4mu +//math mode should use \medmuskip = 4mu plus 2mu minus 4mu '\\bmod': MacroDefinition.fromString("\\mskip5mu" "\\mathbin{\\rm mod}" "\\mskip5mu"), -// TODO what should we do about \pod ? +// what should we do about \pod ? '\\pod': MacroDefinition.fromString("\\allowbreak" "\\mkern8mu(#1)"), '\\pmod': MacroDefinition.fromString("\\pod{{\\rm mod}\\mkern6mu#1}"), @@ -640,7 +640,7 @@ final Map builtinMacros = { '\\\\': MacroDefinition.fromString("\\newline"), // \def\TeX{T\kern-.1667em\lower.5ex\hbox{E}\kern-.125emX\@} -// TODO: Doesn't normally work in math mode because \@ fails. KaTeX doesn't +// Doesn't normally work in math mode because \@ fails. KaTeX doesn't // support \@ yet, so that's omitted, and we add \text so that the result // doesn't look funny in math mode. @@ -680,12 +680,12 @@ final Map builtinMacros = { ////////////////////////////////////////////////////////////////////// // mathtools.sty migrated to extra_symbols -// TODO: make as overrided type & font +// make as overrided type & font //\providecommand\ordinarycolon{:} '\\ordinarycolon': MacroDefinition.fromString(":"), //\def\vcentcolon{\mathrel{\mathop\ordinarycolon}} -//TODO(edemaine): Not yet centered. Fix via \raisebox or #726 +//(edemaine): Not yet centered. Fix via \raisebox or #726 '\\vcentcolon': MacroDefinition.fromString("\\mathrel{\\mathop\\ordinarycolon}"), @@ -727,7 +727,7 @@ final Map builtinMacros = { // https://en.wikipedia.org/wiki/Help:Displaying_a_formula#Deprecated_syntax // We also omit texvc's \O, which conflicts with \text{\O} -// TODO: make as override font +//make as override font '\\darr': MacroDefinition.fromString("\\downarrow"), '\\dArr': MacroDefinition.fromString("\\Downarrow"), '\\Darr': MacroDefinition.fromString("\\Downarrow"), @@ -789,7 +789,7 @@ final Map builtinMacros = { '\\supe': MacroDefinition.fromString("\\supseteq"), '\\Tau': MacroDefinition.fromString("\\mathrm{T}"), '\\thetasym': MacroDefinition.fromString("\\vartheta"), -// TODO: '\\varcoppa': MacroDefinition.fromString("\\\mbox{\\coppa}"), +//'\\varcoppa': MacroDefinition.fromString("\\\mbox{\\coppa}"), '\\weierp': MacroDefinition.fromString("\\wp"), '\\Zeta': MacroDefinition.fromString("\\mathrm{Z}"), diff --git a/lib/src/parser/tex/parser.dart b/lib/src/parser/tex/parser.dart index d196127d..28284d35 100644 --- a/lib/src/parser/tex/parser.dart +++ b/lib/src/parser/tex/parser.dart @@ -24,8 +24,6 @@ import 'dart:collection'; import 'dart:ui'; -import 'package:collection/collection.dart'; - import '../../ast/nodes/multiscripts.dart'; import '../../ast/nodes/over.dart'; import '../../ast/nodes/style.dart'; @@ -335,11 +333,11 @@ class TexParser { /// /// If `optional` is false or absent, this parses an ordinary group, /// which is either a single nucleus (like "x") or an expression - /// in braces (like "{x+y}") or an implicit group, a group that starts + /// in braces (like `"{x+y}"`) or an implicit group, a group that starts /// at the current position, and ends right before a higher explicit /// group ends, or at EOF. /// If `optional` is true, it parses either a bracket-delimited expression - /// (like "[x+y]") or returns null to indicate the absence of a + /// (like `"[x+y]"`) or returns null to indicate the absence of a /// bracket-enclosed group. /// If `mode` is present, switches to that mode while parsing the group, /// and switches back after. @@ -616,7 +614,8 @@ class TexParser { } else { return StyleNode( optionsDiff: OptionsDiff(style: MathStyle.text), - children: res?.children.whereNotNull().toList(growable: false) ?? [], + children: res?.children.nonNulls.toList(growable: false) ?? + const [], ); } } @@ -812,7 +811,6 @@ class TexParser { } GreenNode _formatUnsuppotedCmd(String text) { - //TODO throw UnimplementedError(); } } diff --git a/lib/src/parser/tex/settings.dart b/lib/src/parser/tex/settings.dart index 075f036a..6016ecf3 100644 --- a/lib/src/parser/tex/settings.dart +++ b/lib/src/parser/tex/settings.dart @@ -45,8 +45,8 @@ enum Strict { /// Settings for [TexParser] class TexParserSettings { - final bool displayMode; // TODO - final bool throwOnError; // TODO + final bool displayMode; + final bool throwOnError; /// Extra macros final Map macros; @@ -61,7 +61,7 @@ class TexParserSettings { /// [TexParserSettings.strict] to [Strict.function] final Strict Function(String, String, Token?)? strictFun; - final bool globalGroup; // TODO + final bool globalGroup; /// Behavior of `\color` command /// diff --git a/lib/src/render/layout/eqn_array.dart b/lib/src/render/layout/eqn_array.dart index d987d14f..e9244865 100644 --- a/lib/src/render/layout/eqn_array.dart +++ b/lib/src/render/layout/eqn_array.dart @@ -238,7 +238,7 @@ class RenderEqnArray extends RenderBox Paint()..strokeWidth = ruleThickness, ); } - // TODO dashed line + // dashed line } } } diff --git a/lib/src/render/layout/line_editable.dart b/lib/src/render/layout/line_editable.dart index c9b1a3f4..d1316c74 100644 --- a/lib/src/render/layout/line_editable.dart +++ b/lib/src/render/layout/line_editable.dart @@ -297,6 +297,8 @@ class RenderEditableLine extends RenderLine { EquationRowNode node; /// {@template flutter.rendering.editable.paintCursorOnTop} + /// Whether to paint the cursor above the text. + /// {@endtemplate} bool get paintCursorAboveText => _paintCursorAboveText; bool _paintCursorAboveText; set paintCursorAboveText(bool value) { @@ -461,8 +463,9 @@ class RenderEditableLine extends RenderLine { void _paintCaret(Canvas canvas, Offset baselineOffset) { final paint = Paint() - ..color = - _cursorColor.withOpacity(_cursorBlinkOpacityController?.value ?? 0); + ..color = _cursorColor.withValues( + alpha: _cursorBlinkOpacityController?.value ?? 0, + ); Rect _caretPrototype; diff --git a/lib/src/render/layout/reset_dimension.dart b/lib/src/render/layout/reset_dimension.dart index 59988233..eb745c78 100644 --- a/lib/src/render/layout/reset_dimension.dart +++ b/lib/src/render/layout/reset_dimension.dart @@ -156,7 +156,6 @@ class RenderResetDimension extends RenderShiftedBox { dx = width - childWidth; break; case CrossAxisAlignment.center: - default: dx = (width - childWidth) / 2; break; } diff --git a/lib/src/render/layout/vlist.dart b/lib/src/render/layout/vlist.dart index 65c09dcc..22ad0dd6 100644 --- a/lib/src/render/layout/vlist.dart +++ b/lib/src/render/layout/vlist.dart @@ -334,8 +334,7 @@ class RenderRelativeWidthColumn extends RenderBox return 0; case CrossAxisAlignment.start: case CrossAxisAlignment.baseline: - case CrossAxisAlignment.stretch: // TODO - default: + case CrossAxisAlignment.stretch: return width; } } diff --git a/lib/src/render/svg/svg_string.dart b/lib/src/render/svg/svg_string.dart index 3a3b32fe..5c868938 100644 --- a/lib/src/render/svg/svg_string.dart +++ b/lib/src/render/svg/svg_string.dart @@ -1,6 +1,9 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_svg/flutter_svg.dart'; +int _colorChannelToInt(double value) => + (value * 255.0).round().clamp(0, 255).toInt(); + String svgStringFromPath( String path, Size viewPort, @@ -14,7 +17,7 @@ String svgStringFromPath( 'viewBox=' '"${viewBox.left} ${viewBox.top} ${viewBox.width} ${viewBox.height}" ' '>' - '' + '' ''; final _alignmentToString = { diff --git a/lib/src/utils/canvas_kit/canvas_kit_web.dart b/lib/src/utils/canvas_kit/canvas_kit_web.dart index 1f87e6a3..335a57ea 100644 --- a/lib/src/utils/canvas_kit/canvas_kit_web.dart +++ b/lib/src/utils/canvas_kit/canvas_kit_web.dart @@ -1,9 +1,11 @@ -// ignore: avoid_web_libraries_in_flutter -import 'dart:js'; +import 'dart:js_interop'; /// Whether the CanvasKit renderer is being used on web. /// /// Always returns `false` on non-web. /// /// See https://stackoverflow.com/a/66777112/6509751 for reference. -bool get isCanvasKit => context['flutterCanvasKit'] != null; +@JS('window.flutterCanvasKit') +external JSAny? get _windowFlutterCanvasKit; + +bool get isCanvasKit => _windowFlutterCanvasKit != null; diff --git a/lib/src/widgets/math.dart b/lib/src/widgets/math.dart index e9684f8f..85d0d6c7 100644 --- a/lib/src/widgets/math.dart +++ b/lib/src/widgets/math.dart @@ -12,29 +12,31 @@ import 'exception.dart'; import 'mode.dart'; import 'selectable.dart'; +/// Signature for rendering an alternative widget when parsing or building fails. typedef OnErrorFallback = Widget Function(FlutterMathException errmsg); /// Static, non-selectable widget for equations. /// -/// Sample usage: +/// Use [Math] when you only need rendering and do not need selection or copy +/// support. Compared to [SelectableMath], it has less overhead and is usually +/// the better default for read-only equations. +/// +/// Example: /// /// ```dart /// Math.tex( /// r'\frac a b\sqrt[3]{n}', /// mathStyle: MathStyle.display, -/// textStyle: TextStyle(fontSize: 42), +/// textStyle: const TextStyle(fontSize: 42), /// ) /// ``` -/// -/// Compared to [SelectableMath], [Math] will offer a significant performance -/// advantage. So if no selection capability is needed or the equation counts -/// on the same screen is huge, it's preferable to use [Math]. class Math extends StatelessWidget { - /// Math widget default constructor + /// Creates a math widget from an already parsed [SyntaxTree]. /// - /// Requires either a parsed [ast] or a [parseError]. + /// Provide either a built [ast] or a [parseError]. /// - /// See [Math] for its member documentation + /// Most applications should prefer [Math.tex], which parses a TeX string and + /// returns a ready-to-use widget. const Math({ Key? key, this.ast, @@ -53,13 +55,13 @@ class Math extends StatelessWidget { /// It can be null only when [parseError] is not null. final SyntaxTree? ast; - /// {@template flutter_math_fork.widgets.math.options} - /// Equation style. + /// {@template flutter_math_fork.widgets.math.mathStyle} + /// Layout style for the rendered equation. /// /// Choose [MathStyle.display] for displayed equations and [MathStyle.text] - /// for in-line equations. + /// for inline equations. /// - /// Will be overruled if [options] is present. + /// Ignored when [options] is provided. /// {@endtemplate} final MathStyle mathStyle; @@ -70,64 +72,78 @@ class Math extends StatelessWidget { /// [TextStyle.fontSize]. You can obtain the default scaled value by /// [MathOptions.defaultLogicalPpiFor]. /// - /// Will be overruled if [options] is present. + /// Ignored when [options] is provided. /// /// {@endtemplate} final double? logicalPpi; /// {@template flutter_math_fork.widgets.math.onErrorFallback} - /// Fallback widget when there are uncaught errors during parsing or building. + /// Fallback widget used when parsing or building fails. /// - /// Will be invoked when: + /// Called when: /// - /// * [parseError] is not null. - /// * [SyntaxTree.buildWidget] throw an error. + /// * a stored parse exception is already present. + /// * [SyntaxTree.buildWidget] throws an error. /// - /// Either case, this fallback function is invoked in build functions. So use - /// with care. + /// This callback runs during build, so it should stay cheap and avoid side + /// effects. /// {@endtemplate} final OnErrorFallback onErrorFallback; /// {@template flutter_math_fork.widgets.math.options} - /// Overriding [MathOptions] to build the AST. + /// Complete rendering options for the equation. /// - /// Will overrule [mathStyle] and [textStyle] if not null. + /// When provided, these options take precedence over [mathStyle], + /// [textStyle], and [logicalPpi]. /// {@endtemplate} final MathOptions? options; /// {@template flutter_math_fork.widgets.math.parseError} /// Errors generated during parsing. /// - /// If not null, the [onErrorFallback] widget will be presented. + /// If non-null, [onErrorFallback] is shown instead of rendering math. /// {@endtemplate} final ParseException? parseError; - /// {@macro flutter.widgets.editableText.textScaleFactor} + /// Multiplier applied to the effective text size before rendering. + /// + /// When null, the equation follows the ambient [MediaQuery] text scaling. final double? textScaleFactor; - /// {@template fluttermath.widgets.math.textStyle} - /// The style for rendered math analogous to [Text.style]. + /// {@template flutter_math_fork.widgets.math.textStyle} + /// Base text style used to size and color the rendered equation. /// - /// Can controll the size of the equation via [TextStyle.fontSize]. It can - /// also affect the font weight and font shape of the equation. + /// [TextStyle.fontSize] controls the overall equation size. Text weight, + /// shape, color, and inherited font settings also affect rendering. Text + /// inside `\text{...}` uses the same effective style and locale-aware text + /// shaping. /// - /// If set to null, `DefaultTextStyle` from the context will be used. + /// If null, [DefaultTextStyle] from the current context is used. /// - /// Will be overruled if [options] is present. + /// Ignored when [options] is provided. /// {@endtemplate} final TextStyle? textStyle; - /// Math builder using a TeX string + /// Creates a math widget from a TeX [expression]. /// /// {@template flutter_math_fork.widgets.math.tex_builder} - /// [expression] will first be parsed under [settings]. Then the acquired - /// [SyntaxTree] will be built under a specific options. If [ParseException] - /// is thrown or a build error occurs, [onErrorFallback] will be displayed. + /// The expression is parsed with [settings] and then rendered using either + /// [options] or the simpler [mathStyle] and [textStyle] inputs. + /// + /// If parsing fails or a render-time build error occurs, + /// [onErrorFallback] is displayed. + /// + /// Example: /// - /// You can control the options via [mathStyle] and [textStyle]. + /// ```dart + /// Math.tex( + /// r'\text{বাংলা } + x^2 = 25', + /// mathStyle: MathStyle.text, + /// ) + /// ``` /// {@endtemplate} /// - /// See alse: + /// See also: /// /// * [Math.mathStyle] /// * [Math.textStyle] @@ -149,7 +165,7 @@ class Math extends StatelessWidget { parseError = e; } on Object catch (e) { parseError = ParseException('Unsanitized parse exception detected: $e.' - 'Please report this error with correponding input.'); + 'Please report this error with corresponding input.'); } return Math( key: key, @@ -181,17 +197,26 @@ class Math extends StatelessWidget { .merge(const TextStyle(fontWeight: FontWeight.bold)); } - final textScaleFactor = - this.textScaleFactor ?? MediaQuery.textScaleFactorOf(context); + final baseFontSize = + effectiveTextStyle.fontSize ?? MathOptions.defaultFontSize; + final scaledFontSize = this.textScaleFactor != null + ? baseFontSize * this.textScaleFactor! + : MediaQuery.textScalerOf(context).scale(baseFontSize); + final effectiveColor = effectiveTextStyle.color ?? + DefaultTextStyle.of(context).style.color ?? + Colors.black; options = MathOptions( style: mathStyle, - fontSize: effectiveTextStyle.fontSize! * textScaleFactor, - mathFontOptions: effectiveTextStyle.fontWeight != FontWeight.normal && effectiveTextStyle.fontWeight != null + fontSize: scaledFontSize, + mathFontOptions: effectiveTextStyle.fontWeight != FontWeight.normal && + effectiveTextStyle.fontWeight != null ? FontOptions(fontWeight: effectiveTextStyle.fontWeight!) : null, + textModeTextStyle: effectiveTextStyle, + textLocale: Localizations.maybeLocaleOf(context), logicalPpi: logicalPpi, - color: effectiveTextStyle.color!, + color: effectiveColor, ); } @@ -204,7 +229,7 @@ class Math extends StatelessWidget { } on Object catch (e) { return onErrorFallback( BuildException('Unsanitized build exception detected: $e.' - 'Please report this error with correponding input.')); + 'Please report this error with corresponding input.')); } return Provider.value( @@ -213,7 +238,7 @@ class Math extends StatelessWidget { ); } - /// Default fallback function for [Math], [SelectableMath] + /// Default fallback used by [Math] and [SelectableMath]. static Widget defaultOnErrorFallback(FlutterMathException error) => SelectableText(error.messageWithType); diff --git a/lib/src/widgets/selectable.dart b/lib/src/widgets/selectable.dart index a23f5e94..02ce16d9 100644 --- a/lib/src/widgets/selectable.dart +++ b/lib/src/widgets/selectable.dart @@ -2,6 +2,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; @@ -17,13 +18,68 @@ import 'exception.dart'; import 'math.dart'; import 'mode.dart'; import 'selection/cursor_timer_manager.dart'; -import 'selection/focus_manager.dart'; import 'selection/overlay_manager.dart'; import 'selection/selection_manager.dart'; import 'selection/web_selection_manager.dart'; const defaultSelection = TextSelection.collapsed(offset: -1); +/// Configures which context-menu actions are enabled for [SelectableMath]. +@immutable +class SelectableMathToolbarOptions { + /// Creates toolbar options for [SelectableMath]. + const SelectableMathToolbarOptions({ + this.copy = true, + this.cut = false, + this.paste = false, + this.selectAll = true, + }); + + /// Whether the copy action should be available. + final bool copy; + + /// Whether the cut action should be available. + /// + /// This is currently ignored because [SelectableMath] is read-only. + final bool cut; + + /// Whether the paste action should be available. + /// + /// This is currently ignored because [SelectableMath] is read-only. + final bool paste; + + /// Whether the select-all action should be available. + final bool selectAll; + + /// Returns a copy of this configuration with the provided fields replaced. + SelectableMathToolbarOptions copyWith({ + bool? copy, + bool? cut, + bool? paste, + bool? selectAll, + }) { + return SelectableMathToolbarOptions( + copy: copy ?? this.copy, + cut: cut ?? this.cut, + paste: paste ?? this.paste, + selectAll: selectAll ?? this.selectAll, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is SelectableMathToolbarOptions && + other.copy == copy && + other.cut == cut && + other.paste == paste && + other.selectAll == selectAll; + } + + @override + int get hashCode => Object.hash(copy, cut, paste, selectAll); +} + /// Selectable math widget. /// /// On top of non-selectable [Math], it adds selection functionality. Users can @@ -31,13 +87,17 @@ const defaultSelection = TextSelection.collapsed(offset: -1); /// pointer selection. The selected region can be encoded into TeX and copied /// to clipboard. /// -/// See [SelectableText] as this widget aims to fully imitate its behavior. +/// Use [SelectableMath] when users need to copy or inspect the TeX selection. +/// If you only need display, prefer [Math] for lower overhead. +/// +/// See [SelectableText] as this widget aims to imitate its selection behavior. class SelectableMath extends StatelessWidget { - /// SelectableMath default constructor. + /// Creates selectable math from an already parsed [SyntaxTree]. /// - /// Requires either a parsed [ast] or a [parseException]. + /// Provide either a built [ast] or a [parseException]. /// - /// See [SelectableMath] for its member documentation. + /// Most applications should prefer [SelectableMath.tex], which parses a TeX + /// string and returns a ready-to-use widget. const SelectableMath({ Key? key, this.ast, @@ -58,13 +118,9 @@ class SelectableMath extends StatelessWidget { this.textScaleFactor, this.textSelectionControls, this.textStyle, - ToolbarOptions? toolbarOptions, + SelectableMathToolbarOptions? toolbarOptions, }) : assert(ast != null || parseException != null), - toolbarOptions = toolbarOptions ?? - const ToolbarOptions( - selectAll: true, - copy: true, - ), + toolbarOptions = toolbarOptions ?? const SelectableMathToolbarOptions(), super(key: key); /// The equation to display. @@ -139,29 +195,31 @@ class SelectableMath extends StatelessWidget { /// {@macro flutter.widgets.editableText.showCursor} final bool showCursor; - /// {@macro flutter.widgets.editableText.textScaleFactor} + /// Multiplier applied to the effective text size before rendering. + /// + /// When null, the equation follows the ambient [MediaQuery] text scaling. final double? textScaleFactor; /// Optional delegate for building the text selection handles and toolbar. /// - /// Just works like [EditableText.selectionControls] + /// Works like [EditableText.selectionControls]. final TextSelectionControls? textSelectionControls; - /// {@macro fluttermath.widgets.math.textStyle} + /// {@macro flutter_math_fork.widgets.math.textStyle} final TextStyle? textStyle; - /// Configuration of toolbar options. + /// Configuration of context menu options. /// - /// Paste and cut will be disabled regardless. + /// Paste and cut are disabled regardless because this widget is read-only. /// - /// If not set, select all and copy will be enabled by default. - final ToolbarOptions toolbarOptions; + /// If not set, copy and select-all are enabled by default. + final SelectableMathToolbarOptions toolbarOptions; - /// SelectableMath builder using a TeX string + /// Creates selectable math from a TeX [expression]. /// /// {@macro flutter_math_fork.widgets.math.tex_builder} /// - /// See alse: + /// See also: /// /// * [SelectableMath.mathStyle] /// * [SelectableMath.textStyle] @@ -185,7 +243,7 @@ class SelectableMath extends StatelessWidget { double? textScaleFactor, TextSelectionControls? textSelectionControls, TextStyle? textStyle, - ToolbarOptions? toolbarOptions, + SelectableMathToolbarOptions? toolbarOptions, }) { SyntaxTree? ast; ParseException? parseError; @@ -195,7 +253,7 @@ class SelectableMath extends StatelessWidget { parseError = e; } on Object catch (e) { parseError = ParseException('Unsanitized parse exception detected: $e.' - 'Please report this error with correponding input.'); + 'Please report this error with corresponding input.'); } return SelectableMath( key: key, @@ -235,18 +293,27 @@ class SelectableMath extends StatelessWidget { .merge(const TextStyle(fontWeight: FontWeight.bold)); } - final textScaleFactor = - this.textScaleFactor ?? MediaQuery.textScaleFactorOf(context); + final baseFontSize = + effectiveTextStyle.fontSize ?? MathOptions.defaultFontSize; + final scaledFontSize = this.textScaleFactor != null + ? baseFontSize * this.textScaleFactor! + : MediaQuery.textScalerOf(context).scale(baseFontSize); + final effectiveColor = effectiveTextStyle.color ?? + DefaultTextStyle.of(context).style.color ?? + Colors.black; final options = this.options ?? MathOptions( style: mathStyle, - fontSize: effectiveTextStyle.fontSize! * textScaleFactor, - mathFontOptions: effectiveTextStyle.fontWeight != FontWeight.normal && effectiveTextStyle.fontWeight != null + fontSize: scaledFontSize, + mathFontOptions: effectiveTextStyle.fontWeight != FontWeight.normal && + effectiveTextStyle.fontWeight != null ? FontOptions(fontWeight: effectiveTextStyle.fontWeight!) : null, + textModeTextStyle: effectiveTextStyle, + textLocale: Localizations.maybeLocaleOf(context), logicalPpi: logicalPpi, - color: effectiveTextStyle.color!, + color: effectiveColor, ); // A trial build to catch any potential build errors @@ -257,7 +324,7 @@ class SelectableMath extends StatelessWidget { } on Object catch (e) { return onErrorFallback( BuildException('Unsanitized build exception detected: $e.' - 'Please report this error with correponding input.')); + 'Please report this error with corresponding input.')); } final theme = Theme.of(context); @@ -395,7 +462,7 @@ class InternalSelectableMath extends StatefulWidget { final TextSelectionControls textSelectionControls; - final ToolbarOptions toolbarOptions; + final SelectableMathToolbarOptions toolbarOptions; @override InternalSelectableMathState createState() => InternalSelectableMathState(); @@ -404,12 +471,14 @@ class InternalSelectableMath extends StatefulWidget { class InternalSelectableMathState extends State with AutomaticKeepAliveClientMixin, - FocusManagerMixin, + TextSelectionDelegate, SelectionManagerMixin, SelectionOverlayManagerMixin, WebSelectionControlsManagerMixin, SingleTickerProviderStateMixin, CursorTimerManagerMixin { + static InternalSelectableMathState? _activeSelectableMath; + TextSelectionControls get textSelectionControls => widget.textSelectionControls; @@ -455,7 +524,7 @@ class InternalSelectableMathState extends State _didAutoFocus = true; SchedulerBinding.instance.addPostFrameCallback((_) { if (mounted) { - FocusScope.of(context).autofocus(widget.focusNode!); + FocusScope.of(context).autofocus(focusNode); } }); } @@ -464,8 +533,54 @@ class InternalSelectableMathState extends State @override void dispose() { _oldFocusNode.removeListener(updateKeepAlive); - super.dispose(); + if (_activeSelectableMath == this) { + _activeSelectableMath = null; + } controller.dispose(); + super.dispose(); + } + + @override + void requestFocusForInteraction() { + final activeSelectableMath = _activeSelectableMath; + if (activeSelectableMath != null && + activeSelectableMath != this && + activeSelectableMath.mounted) { + activeSelectableMath.handleSelectionChanged( + defaultSelection, + null, + ExtraSelectionChangedCause.unfocus, + ); + } + _activeSelectableMath = this; + focusNode.requestFocus(); + } + + @override + void handleSelectionChanged( + TextSelection selection, + SelectionChangedCause? cause, [ + ExtraSelectionChangedCause? extraCause, + ]) { + if (extraCause == ExtraSelectionChangedCause.unfocus) { + if (_activeSelectableMath == this) { + _activeSelectableMath = null; + } + } else if (extraCause != ExtraSelectionChangedCause.exterior) { + final activeSelectableMath = _activeSelectableMath; + if (activeSelectableMath != null && + activeSelectableMath != this && + activeSelectableMath.mounted) { + activeSelectableMath.handleSelectionChanged( + defaultSelection, + null, + ExtraSelectionChangedCause.unfocus, + ); + } + _activeSelectableMath = this; + } + + super.handleSelectionChanged(selection, cause, extraCause); } void onSelectionChanged( @@ -490,38 +605,57 @@ class InternalSelectableMathState extends State final child = controller.ast.buildWidget(widget.options); - return selectionGestureDetectorBuilder.buildGestureDetector( - child: MouseRegion( - cursor: SystemMouseCursors.text, - child: CompositedTransformTarget( - link: toolbarLayerLink, - child: MultiProvider( - providers: [ - Provider.value(value: FlutterMathMode.select), - ChangeNotifierProvider.value(value: controller), - ProxyProvider( - create: (context) => const TextSelection.collapsed(offset: -1), - update: (context, value, previous) => value.selection, - ), - Provider.value( - value: SelectionStyle( - cursorColor: widget.cursorColor, - cursorOffset: widget.cursorOffset, - cursorRadius: widget.cursorRadius, - cursorWidth: widget.cursorWidth, - cursorHeight: widget.cursorHeight, - selectionColor: widget.selectionColor, - paintCursorAboveText: widget.paintCursorAboveText, - ), - ), - Provider.value( - value: Tuple2(startHandleLayerLink, endHandleLayerLink), + return Focus.withExternalFocusNode( + focusNode: focusNode, + includeSemantics: false, + child: TapRegion( + groupId: toolbarLayerLink, + onTapOutside: (_) { + if (controller.selection == defaultSelection) { + return; + } + handleSelectionChanged( + defaultSelection, + null, + ExtraSelectionChangedCause.unfocus, + ); + }, + child: selectionGestureDetectorBuilder.buildGestureDetector( + behavior: HitTestBehavior.translucent, + child: MouseRegion( + cursor: SystemMouseCursors.text, + child: CompositedTransformTarget( + link: toolbarLayerLink, + child: MultiProvider( + providers: [ + Provider.value(value: FlutterMathMode.select), + ChangeNotifierProvider.value(value: controller), + ProxyProvider( + create: (context) => + const TextSelection.collapsed(offset: -1), + update: (context, value, previous) => value.selection, + ), + Provider.value( + value: SelectionStyle( + cursorColor: widget.cursorColor, + cursorOffset: widget.cursorOffset, + cursorRadius: widget.cursorRadius, + cursorWidth: widget.cursorWidth, + cursorHeight: widget.cursorHeight, + selectionColor: widget.selectionColor, + paintCursorAboveText: widget.paintCursorAboveText, + ), + ), + Provider.value( + value: Tuple2(startHandleLayerLink, endHandleLayerLink), + ), + // We can't just provide an AnimationController, otherwise + // Provider will throw + Provider.value(value: Wrapper(cursorBlinkOpacityController)), + ], + child: child, ), - // We can't just provide an AnimationController, otherwise - // Provider will throw - Provider.value(value: Wrapper(cursorBlinkOpacityController)), - ], - child: child, + ), ), ), ), @@ -553,10 +687,103 @@ class InternalSelectableMathState extends State double get preferredLineHeight => widget.options.fontSize; @override - dynamic noSuchMethod(Invocation invocation) { - // We override noSuchMethod since we do not have concrete implementations - // for all methods of the selection manager mixins. - throw NoSuchMethodError.withInvocation(this, invocation); + List get contextMenuButtonItems { + return [ + if (copyEnabled) + ContextMenuButtonItem( + type: ContextMenuButtonType.copy, + onPressed: () => copySelection(SelectionChangedCause.toolbar), + ), + if (selectAllEnabled && !_selectionCoversAllContent) + ContextMenuButtonItem( + type: ContextMenuButtonType.selectAll, + onPressed: () => selectAll(SelectionChangedCause.toolbar), + ), + ]; + } + + bool get _selectionCoversAllContent => + controller.selection.start == 0 && + controller.selection.end == controller.ast.greenRoot.capturedCursor - 1; + + @override + void bringIntoView(TextPosition position) { + final targetContext = controller.ast.greenRoot.key?.currentContext; + if (targetContext == null) { + return; + } + Scrollable.ensureVisible( + targetContext, + duration: Duration.zero, + alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, + ); + } + + @override + void copySelection(SelectionChangedCause cause) { + if (controller.selection.isCollapsed) { + return; + } + + final selectedText = textEditingValue.selection.textInside( + textEditingValue.text, + ); + if (selectedText.isEmpty) { + return; + } + + Clipboard.setData(ClipboardData(text: selectedText)); + + if (cause != SelectionChangedCause.toolbar) { + return; + } + + switch (Theme.of(context).platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + controller.selection = defaultSelection; + break; + case TargetPlatform.iOS: + hideToolbar(false); + break; + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.windows: + hideToolbar(); + break; + } + } + + @override + void cutSelection(SelectionChangedCause cause) {} + + @override + Future pasteText(SelectionChangedCause cause) async {} + + @override + void selectAll(SelectionChangedCause cause) { + final fullSelection = TextSelection( + baseOffset: 0, + extentOffset: controller.ast.greenRoot.capturedCursor - 1, + ); + handleSelectionChanged( + fullSelection, + cause, + cause == SelectionChangedCause.toolbar + ? ExtraSelectionChangedCause.handle + : null, + ); + if (cause == SelectionChangedCause.toolbar) { + bringIntoView(fullSelection.extent); + } + } + + @override + void userUpdateTextEditingValue( + TextEditingValue value, + SelectionChangedCause cause, + ) { + textEditingValue = value; } } diff --git a/lib/src/widgets/selection/gesture_detector_builder.dart b/lib/src/widgets/selection/gesture_detector_builder.dart index f44d1c3a..a22daea8 100644 --- a/lib/src/widgets/selection/gesture_detector_builder.dart +++ b/lib/src/widgets/selection/gesture_detector_builder.dart @@ -7,6 +7,8 @@ abstract class MathSelectionGestureDetectorBuilderDelegate { bool get forcePressEnabled; bool get selectionEnabled; + + void requestFocusForInteraction(); } class MathSelectionGestureDetectorBuilder { @@ -30,6 +32,9 @@ class MathSelectionGestureDetectorBuilder { @protected void onTapDown(TapDragDownDetails details) { lastTapDownPosition = details.globalPosition; + if (delegate.selectionEnabled) { + delegate.requestFocusForInteraction(); + } // The selection overlay should only be shown when the user is interacting // through a touch screen (via either a finger or a stylus). A mouse // shouldn't trigger the selection overlay. diff --git a/lib/src/widgets/selection/overlay.dart b/lib/src/widgets/selection/overlay.dart index 12be5a42..cf013ef4 100644 --- a/lib/src/widgets/selection/overlay.dart +++ b/lib/src/widgets/selection/overlay.dart @@ -1,4 +1,5 @@ import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart' show AdaptiveTextSelectionToolbar; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -157,7 +158,9 @@ class MathSelectionOverlay { void hideHandles() { if (_handles != null) { _handles![0].remove(); + _handles![0].dispose(); _handles![1].remove(); + _handles![1].dispose(); _handles = null; } } @@ -207,7 +210,9 @@ class MathSelectionOverlay { void hide() { if (_handles != null) { _handles![0].remove(); + _handles![0].dispose(); _handles![1].remove(); + _handles![1].dispose(); _handles = null; } if (_toolbar != null) { @@ -219,9 +224,12 @@ class MathSelectionOverlay { /// /// To hide the whole overlay, see [hide]. void hideToolbar() { - assert(_toolbar != null); + if (_toolbar == null) { + return; + } _toolbarController.stop(); _toolbar!.remove(); + _toolbar!.dispose(); _toolbar = null; } @@ -236,71 +244,51 @@ class MathSelectionOverlay { if ((_selection.isCollapsed && position == MathSelectionHandlePosition.end) || selectionControls == null) { - return Container(); + return const SizedBox.shrink(); } // hide the second handle when collapsed - return Visibility( - visible: handlesVisible, - child: MathSelectionHandleOverlay( - manager: manager, - onSelectionHandleChanged: (TextSelection newSelection) { - _handleSelectionHandleChanged(newSelection, position); - }, - onSelectionHandleTapped: onSelectionHandleTapped, - startHandleLayerLink: startHandleLayerLink, - endHandleLayerLink: endHandleLayerLink, - selection: _selection, - selectionControls: selectionControls!, - position: position, - dragStartBehavior: dragStartBehavior, + return TapRegion( + groupId: toolbarLayerLink, + child: Visibility( + visible: handlesVisible, + child: MathSelectionHandleOverlay( + manager: manager, + onSelectionHandleChanged: (TextSelection newSelection) { + _handleSelectionHandleChanged(newSelection, position); + }, + onSelectionHandleTapped: onSelectionHandleTapped, + startHandleLayerLink: startHandleLayerLink, + endHandleLayerLink: endHandleLayerLink, + selection: _selection, + selectionControls: selectionControls!, + position: position, + dragStartBehavior: dragStartBehavior, + ), ), ); } Widget _buildToolbar(BuildContext context) { - if (selectionControls == null) return Container(); + if (manager.contextMenuButtonItems.isEmpty) return const SizedBox.shrink(); - // Find the horizontal midpoint, just above the selected text. final endpoint1 = manager.getLocalEndpointForPosition(_selection.start); - final endpoint2 = manager.getLocalEndpointForPosition(_selection.end); - - final editingRegion = manager.getLocalEditingRegion(); - - final isMultiline = false; // TODO - // endpoints.last.point.dy - endpoints.first.point.dy > - // manager.preferredLineHeight / 2; - - // If the selected text spans more than 1 line, horizontally center the - // toolbar. - // Derived from both iOS and Android. - final midX = isMultiline - ? editingRegion.width / 2 - : (endpoint1.dx + endpoint2.dx) / 2; - - final midpoint = Offset( - midX, - // The y-coordinate won't be made use of most likely. - endpoint1.dy - manager.preferredLineHeight, + final anchors = TextSelectionToolbarAnchors.fromSelection( + renderBox: manager.rootRenderBox, + startGlyphHeight: manager.preferredLineHeight, + endGlyphHeight: manager.preferredLineHeight, + selectionEndpoints: [ + TextSelectionPoint(endpoint1, TextDirection.ltr), + TextSelectionPoint(endpoint2, TextDirection.ltr), + ], ); return FadeTransition( opacity: _toolbarOpacity, - child: CompositedTransformFollower( - link: toolbarLayerLink, - showWhenUnlinked: false, - offset: -editingRegion.topLeft, - child: selectionControls!.buildToolbar( - context, - editingRegion, - manager.preferredLineHeight, - midpoint, - [ - TextSelectionPoint(endpoint1, TextDirection.ltr), - TextSelectionPoint(endpoint2, TextDirection.ltr), - ], - manager, - clipboardStatus!, - null, + child: TapRegion( + groupId: toolbarLayerLink, + child: AdaptiveTextSelectionToolbar.buttonItems( + anchors: anchors, + buttonItems: manager.contextMenuButtonItems, ), ), ); diff --git a/lib/src/widgets/selection/overlay_manager.dart b/lib/src/widgets/selection/overlay_manager.dart index e64083ee..3420afa9 100644 --- a/lib/src/widgets/selection/overlay_manager.dart +++ b/lib/src/widgets/selection/overlay_manager.dart @@ -16,6 +16,8 @@ mixin SelectionOverlayManagerMixin double get preferredLineHeight; + List get contextMenuButtonItems; + TextSelectionControls get textSelectionControls; DragStartBehavior get dragStartBehavior; @@ -75,6 +77,9 @@ mixin SelectionOverlayManagerMixin if (controller.selection.isCollapsed) { return false; } + if (contextMenuButtonItems.isEmpty) { + return false; + } _selectionOverlay!.showToolbar(); toolbarVisible = true; return true; diff --git a/lib/src/widgets/selection/web_selection_manager.dart b/lib/src/widgets/selection/web_selection_manager.dart index ecd5a8f2..6d09c3bf 100644 --- a/lib/src/widgets/selection/web_selection_manager.dart +++ b/lib/src/widgets/selection/web_selection_manager.dart @@ -127,6 +127,14 @@ mixin WebSelectionControlsManagerMixin _textInputConnection = null; } + @override + void didChangeInputControl( + TextInputControl? oldControl, + TextInputControl? newControl, + ) { + // no-op + } + @override AutofillScope? get currentAutofillScope => null; @@ -143,6 +151,26 @@ mixin WebSelectionControlsManagerMixin // no-op } + @override + void insertContent(KeyboardInsertedContent content) { + // no-op + } + + @override + void insertTextPlaceholder(Size size) { + // no-op + } + + @override + void removeTextPlaceholder() { + // no-op + } + + @override + void performSelector(String selectorName) { + // no-op + } + @override void showAutocorrectionPromptRect(int start, int end) { // no-op diff --git a/pubspec.yaml b/pubspec.yaml index 78ad10d3..a441bd77 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,21 +1,22 @@ name: flutter_math_fork -description: Fast and high-quality TeX math equation rendering with pure Dart & Flutter. -version: 0.7.4 +description: Fast TeX math equation rendering for Flutter, with selectable math support and maintained compatibility for current Flutter releases. +version: 0.8.0 homepage: https://github.com/simpleclub/flutter_math +repository: https://github.com/simpleclub/flutter_math +issue_tracker: https://github.com/simpleclub/flutter_math/issues environment: sdk: '>=3.0.0 <4.0.0' - flutter: '>=3.0.0' + flutter: '>=3.38.0' dependencies: flutter: sdk: flutter - flutter_svg: ">=2.0.0+1 <3.0.0" - provider: ^6.0.5 - meta: ^1.8.0 - collection: ^1.17.0 - tuple: ^2.0.1 + flutter_svg: ^2.2.2 + provider: ^6.1.5+1 + collection: ^1.19.1 + tuple: ^2.0.2 dev_dependencies: flutter_test: diff --git a/test/golden/29221604.png b/test/golden/29221604.png index 8559b8b9..0b80fe8f 100644 Binary files a/test/golden/29221604.png and b/test/golden/29221604.png differ diff --git a/test/golden/355369240.png b/test/golden/355369240.png index c8639ea4..4b9bd7a5 100644 Binary files a/test/golden/355369240.png and b/test/golden/355369240.png differ diff --git a/test/golden/750111939.png b/test/golden/750111939.png index da56237a..73ded241 100644 Binary files a/test/golden/750111939.png and b/test/golden/750111939.png differ diff --git a/test/golden/860616833.png b/test/golden/860616833.png index f9effb1b..1b5e991b 100644 Binary files a/test/golden/860616833.png and b/test/golden/860616833.png differ diff --git a/test/golden/928722428.png b/test/golden/928722428.png index aad6ebc7..4e5bd6c1 100644 Binary files a/test/golden/928722428.png and b/test/golden/928722428.png differ diff --git a/test/golden/Overrightarrow76505663.png b/test/golden/Overrightarrow76505663.png index 87acaea9..c1136986 100644 Binary files a/test/golden/Overrightarrow76505663.png and b/test/golden/Overrightarrow76505663.png differ diff --git a/test/golden/acute267057593.png b/test/golden/acute267057593.png index 883d97b5..8f4f163e 100644 Binary files a/test/golden/acute267057593.png and b/test/golden/acute267057593.png differ diff --git a/test/golden/bar574947087.png b/test/golden/bar574947087.png index 89efab7c..a6157977 100644 Binary files a/test/golden/bar574947087.png and b/test/golden/bar574947087.png differ diff --git a/test/golden/breve107975770.png b/test/golden/breve107975770.png index 6a6ba823..e37db3b2 100644 Binary files a/test/golden/breve107975770.png and b/test/golden/breve107975770.png differ diff --git a/test/golden/check842862033.png b/test/golden/check842862033.png index a87543d5..3a68b8d6 100644 Binary files a/test/golden/check842862033.png and b/test/golden/check842862033.png differ diff --git a/test/golden/ddot349178810.png b/test/golden/ddot349178810.png index c8600dd3..eb92e391 100644 Binary files a/test/golden/ddot349178810.png and b/test/golden/ddot349178810.png differ diff --git a/test/golden/dot270815021.png b/test/golden/dot270815021.png index fe81bfeb..329ae22a 100644 Binary files a/test/golden/dot270815021.png and b/test/golden/dot270815021.png differ diff --git a/test/golden/enclosure.png b/test/golden/enclosure.png index bdb915c8..c0c84dca 100644 Binary files a/test/golden/enclosure.png and b/test/golden/enclosure.png differ diff --git a/test/golden/eqnarray.png b/test/golden/eqnarray.png index 44ab3725..d7e4fa11 100644 Binary files a/test/golden/eqnarray.png and b/test/golden/eqnarray.png differ diff --git a/test/golden/grave165690505.png b/test/golden/grave165690505.png index d1baf77b..19cbedb2 100644 Binary files a/test/golden/grave165690505.png and b/test/golden/grave165690505.png differ diff --git a/test/golden/hat823583055.png b/test/golden/hat823583055.png index 412ed017..bbb66c5c 100644 Binary files a/test/golden/hat823583055.png and b/test/golden/hat823583055.png differ diff --git a/test/golden/mathring740120027.png b/test/golden/mathring740120027.png index 6435ccd3..275b8f5b 100644 Binary files a/test/golden/mathring740120027.png and b/test/golden/mathring740120027.png differ diff --git a/test/golden/overleftarrow567687850.png b/test/golden/overleftarrow567687850.png index bb17c14e..e3ad825f 100644 Binary files a/test/golden/overleftarrow567687850.png and b/test/golden/overleftarrow567687850.png differ diff --git a/test/golden/overleftharpoon580184352.png b/test/golden/overleftharpoon580184352.png index 4bfab583..ae53f995 100644 Binary files a/test/golden/overleftharpoon580184352.png and b/test/golden/overleftharpoon580184352.png differ diff --git a/test/golden/overleftrightarrow424866188.png b/test/golden/overleftrightarrow424866188.png index 3fbf58c1..35c5a080 100644 Binary files a/test/golden/overleftrightarrow424866188.png and b/test/golden/overleftrightarrow424866188.png differ diff --git a/test/golden/overline53006047.png b/test/golden/overline53006047.png index 42ec5079..7bb3d070 100644 Binary files a/test/golden/overline53006047.png and b/test/golden/overline53006047.png differ diff --git a/test/golden/overrightarrow138596512.png b/test/golden/overrightarrow138596512.png index 656ce85f..bda01c0c 100644 Binary files a/test/golden/overrightarrow138596512.png and b/test/golden/overrightarrow138596512.png differ diff --git a/test/golden/overrightharpoon301951104.png b/test/golden/overrightharpoon301951104.png index 0dad5003..55754fe7 100644 Binary files a/test/golden/overrightharpoon301951104.png and b/test/golden/overrightharpoon301951104.png differ diff --git a/test/golden/temp/1038570769.png b/test/golden/temp/1038570769.png deleted file mode 100644 index f1f50a40..00000000 Binary files a/test/golden/temp/1038570769.png and /dev/null differ diff --git a/test/golden/temp/1060711029.png b/test/golden/temp/1060711029.png deleted file mode 100644 index 2f6f4eed..00000000 Binary files a/test/golden/temp/1060711029.png and /dev/null differ diff --git a/test/golden/temp/108564718.png b/test/golden/temp/108564718.png deleted file mode 100644 index ce2d758e..00000000 Binary files a/test/golden/temp/108564718.png and /dev/null differ diff --git a/test/golden/temp/132707078.png b/test/golden/temp/132707078.png deleted file mode 100644 index 49952345..00000000 Binary files a/test/golden/temp/132707078.png and /dev/null differ diff --git a/test/golden/temp/138701843.png b/test/golden/temp/138701843.png deleted file mode 100644 index 701525e8..00000000 Binary files a/test/golden/temp/138701843.png and /dev/null differ diff --git a/test/golden/temp/149472409.png b/test/golden/temp/149472409.png deleted file mode 100644 index 0b343a7f..00000000 Binary files a/test/golden/temp/149472409.png and /dev/null differ diff --git a/test/golden/temp/163922097.png b/test/golden/temp/163922097.png deleted file mode 100644 index c6be1373..00000000 Binary files a/test/golden/temp/163922097.png and /dev/null differ diff --git a/test/golden/temp/167173928.png b/test/golden/temp/167173928.png deleted file mode 100644 index 22b0bb3c..00000000 Binary files a/test/golden/temp/167173928.png and /dev/null differ diff --git a/test/golden/temp/177228769.png b/test/golden/temp/177228769.png deleted file mode 100644 index a1ed495d..00000000 Binary files a/test/golden/temp/177228769.png and /dev/null differ diff --git a/test/golden/temp/181148755.png b/test/golden/temp/181148755.png deleted file mode 100644 index 80614bb9..00000000 Binary files a/test/golden/temp/181148755.png and /dev/null differ diff --git a/test/golden/temp/18365951.png b/test/golden/temp/18365951.png deleted file mode 100644 index 7b985bc8..00000000 Binary files a/test/golden/temp/18365951.png and /dev/null differ diff --git a/test/golden/temp/18700801.png b/test/golden/temp/18700801.png deleted file mode 100644 index 5f2c48f8..00000000 Binary files a/test/golden/temp/18700801.png and /dev/null differ diff --git a/test/golden/temp/192078322.png b/test/golden/temp/192078322.png deleted file mode 100644 index 77e62b96..00000000 Binary files a/test/golden/temp/192078322.png and /dev/null differ diff --git a/test/golden/temp/229523423.png b/test/golden/temp/229523423.png deleted file mode 100644 index a5cf73c4..00000000 Binary files a/test/golden/temp/229523423.png and /dev/null differ diff --git a/test/golden/temp/244578803.png b/test/golden/temp/244578803.png deleted file mode 100644 index c7fd7218..00000000 Binary files a/test/golden/temp/244578803.png and /dev/null differ diff --git a/test/golden/temp/311172474.png b/test/golden/temp/311172474.png deleted file mode 100644 index c7fd7218..00000000 Binary files a/test/golden/temp/311172474.png and /dev/null differ diff --git a/test/golden/temp/315666507.png b/test/golden/temp/315666507.png deleted file mode 100644 index 75f9f2c5..00000000 Binary files a/test/golden/temp/315666507.png and /dev/null differ diff --git a/test/golden/temp/334789020.png b/test/golden/temp/334789020.png deleted file mode 100644 index 03601ad1..00000000 Binary files a/test/golden/temp/334789020.png and /dev/null differ diff --git a/test/golden/temp/364385848.png b/test/golden/temp/364385848.png deleted file mode 100644 index c491187e..00000000 Binary files a/test/golden/temp/364385848.png and /dev/null differ diff --git a/test/golden/temp/374852144.png b/test/golden/temp/374852144.png deleted file mode 100644 index f0373c06..00000000 Binary files a/test/golden/temp/374852144.png and /dev/null differ diff --git a/test/golden/temp/385527251.png b/test/golden/temp/385527251.png deleted file mode 100644 index c7ce7718..00000000 Binary files a/test/golden/temp/385527251.png and /dev/null differ diff --git a/test/golden/temp/387350573.png b/test/golden/temp/387350573.png deleted file mode 100644 index c7fd7218..00000000 Binary files a/test/golden/temp/387350573.png and /dev/null differ diff --git a/test/golden/temp/394965460.png b/test/golden/temp/394965460.png deleted file mode 100644 index a24800e7..00000000 Binary files a/test/golden/temp/394965460.png and /dev/null differ diff --git a/test/golden/temp/398283101.png b/test/golden/temp/398283101.png deleted file mode 100644 index c7ce7718..00000000 Binary files a/test/golden/temp/398283101.png and /dev/null differ diff --git a/test/golden/temp/410885435.png b/test/golden/temp/410885435.png deleted file mode 100644 index a24800e7..00000000 Binary files a/test/golden/temp/410885435.png and /dev/null differ diff --git a/test/golden/temp/460938518.png b/test/golden/temp/460938518.png deleted file mode 100644 index 4b85f84e..00000000 Binary files a/test/golden/temp/460938518.png and /dev/null differ diff --git a/test/golden/temp/476918051.png b/test/golden/temp/476918051.png deleted file mode 100644 index c7fd7218..00000000 Binary files a/test/golden/temp/476918051.png and /dev/null differ diff --git a/test/golden/temp/485097675.png b/test/golden/temp/485097675.png deleted file mode 100644 index bf948f0f..00000000 Binary files a/test/golden/temp/485097675.png and /dev/null differ diff --git a/test/golden/temp/492747826.png b/test/golden/temp/492747826.png deleted file mode 100644 index a24800e7..00000000 Binary files a/test/golden/temp/492747826.png and /dev/null differ diff --git a/test/golden/temp/510403892.png b/test/golden/temp/510403892.png deleted file mode 100644 index a189a003..00000000 Binary files a/test/golden/temp/510403892.png and /dev/null differ diff --git a/test/golden/temp/510517529.png b/test/golden/temp/510517529.png deleted file mode 100644 index e5e2152b..00000000 Binary files a/test/golden/temp/510517529.png and /dev/null differ diff --git a/test/golden/temp/550865336.png b/test/golden/temp/550865336.png deleted file mode 100644 index f18c5cfc..00000000 Binary files a/test/golden/temp/550865336.png and /dev/null differ diff --git a/test/golden/temp/575741909.png b/test/golden/temp/575741909.png deleted file mode 100644 index 1372ef6e..00000000 Binary files a/test/golden/temp/575741909.png and /dev/null differ diff --git a/test/golden/temp/57682459.png b/test/golden/temp/57682459.png deleted file mode 100644 index a24800e7..00000000 Binary files a/test/golden/temp/57682459.png and /dev/null differ diff --git a/test/golden/temp/586114.png b/test/golden/temp/586114.png deleted file mode 100644 index d8b10ea1..00000000 Binary files a/test/golden/temp/586114.png and /dev/null differ diff --git a/test/golden/temp/627646929.png b/test/golden/temp/627646929.png deleted file mode 100644 index a6268ed9..00000000 Binary files a/test/golden/temp/627646929.png and /dev/null differ diff --git a/test/golden/temp/62807860.png b/test/golden/temp/62807860.png deleted file mode 100644 index e8644994..00000000 Binary files a/test/golden/temp/62807860.png and /dev/null differ diff --git a/test/golden/temp/632023797.png b/test/golden/temp/632023797.png deleted file mode 100644 index a24800e7..00000000 Binary files a/test/golden/temp/632023797.png and /dev/null differ diff --git a/test/golden/temp/644274436.png b/test/golden/temp/644274436.png deleted file mode 100644 index ce2d758e..00000000 Binary files a/test/golden/temp/644274436.png and /dev/null differ diff --git a/test/golden/temp/646192306.png b/test/golden/temp/646192306.png deleted file mode 100644 index 43b4928b..00000000 Binary files a/test/golden/temp/646192306.png and /dev/null differ diff --git a/test/golden/temp/677215405.png b/test/golden/temp/677215405.png deleted file mode 100644 index f18c5cfc..00000000 Binary files a/test/golden/temp/677215405.png and /dev/null differ diff --git a/test/golden/temp/73362715.png b/test/golden/temp/73362715.png deleted file mode 100644 index c6be1373..00000000 Binary files a/test/golden/temp/73362715.png and /dev/null differ diff --git a/test/golden/temp/73599379.png b/test/golden/temp/73599379.png deleted file mode 100644 index a24800e7..00000000 Binary files a/test/golden/temp/73599379.png and /dev/null differ diff --git a/test/golden/temp/742393617.png b/test/golden/temp/742393617.png deleted file mode 100644 index f5c76214..00000000 Binary files a/test/golden/temp/742393617.png and /dev/null differ diff --git a/test/golden/temp/748278783.png b/test/golden/temp/748278783.png deleted file mode 100644 index 5a4b2789..00000000 Binary files a/test/golden/temp/748278783.png and /dev/null differ diff --git a/test/golden/temp/756284122.png b/test/golden/temp/756284122.png deleted file mode 100644 index a24800e7..00000000 Binary files a/test/golden/temp/756284122.png and /dev/null differ diff --git a/test/golden/temp/760483081.png b/test/golden/temp/760483081.png deleted file mode 100644 index f1a67682..00000000 Binary files a/test/golden/temp/760483081.png and /dev/null differ diff --git a/test/golden/temp/760931274.png b/test/golden/temp/760931274.png deleted file mode 100644 index 5749b736..00000000 Binary files a/test/golden/temp/760931274.png and /dev/null differ diff --git a/test/golden/temp/775255816.png b/test/golden/temp/775255816.png deleted file mode 100644 index fe07fa00..00000000 Binary files a/test/golden/temp/775255816.png and /dev/null differ diff --git a/test/golden/temp/776562482.png b/test/golden/temp/776562482.png deleted file mode 100644 index 9113c848..00000000 Binary files a/test/golden/temp/776562482.png and /dev/null differ diff --git a/test/golden/temp/780748998.png b/test/golden/temp/780748998.png deleted file mode 100644 index 53055206..00000000 Binary files a/test/golden/temp/780748998.png and /dev/null differ diff --git a/test/golden/temp/788702447.png b/test/golden/temp/788702447.png deleted file mode 100644 index 49952345..00000000 Binary files a/test/golden/temp/788702447.png and /dev/null differ diff --git a/test/golden/temp/803180120.png b/test/golden/temp/803180120.png deleted file mode 100644 index 879c2884..00000000 Binary files a/test/golden/temp/803180120.png and /dev/null differ diff --git a/test/golden/temp/810379325.png b/test/golden/temp/810379325.png deleted file mode 100644 index 13aae40d..00000000 Binary files a/test/golden/temp/810379325.png and /dev/null differ diff --git a/test/golden/temp/826621488.png b/test/golden/temp/826621488.png deleted file mode 100644 index 28458ed9..00000000 Binary files a/test/golden/temp/826621488.png and /dev/null differ diff --git a/test/golden/temp/899333225.png b/test/golden/temp/899333225.png deleted file mode 100644 index 0b343a7f..00000000 Binary files a/test/golden/temp/899333225.png and /dev/null differ diff --git a/test/golden/temp/905381913.png b/test/golden/temp/905381913.png deleted file mode 100644 index 80614bb9..00000000 Binary files a/test/golden/temp/905381913.png and /dev/null differ diff --git a/test/golden/temp/925644139.png b/test/golden/temp/925644139.png deleted file mode 100644 index a24800e7..00000000 Binary files a/test/golden/temp/925644139.png and /dev/null differ diff --git a/test/golden/temp/983122783.png b/test/golden/temp/983122783.png deleted file mode 100644 index a5a756b3..00000000 Binary files a/test/golden/temp/983122783.png and /dev/null differ diff --git a/test/golden/text-accents-1.png b/test/golden/text-accents-1.png index 0802229a..2f202973 100644 Binary files a/test/golden/text-accents-1.png and b/test/golden/text-accents-1.png differ diff --git a/test/golden/text-accents-2.png b/test/golden/text-accents-2.png index b27a95b3..2bdd389d 100644 Binary files a/test/golden/text-accents-2.png and b/test/golden/text-accents-2.png differ diff --git a/test/golden/tilde150386117.png b/test/golden/tilde150386117.png index acf9684a..a372afab 100644 Binary files a/test/golden/tilde150386117.png and b/test/golden/tilde150386117.png differ diff --git a/test/golden/undergroup274196240.png b/test/golden/undergroup274196240.png index 2c133981..2183bfe4 100644 Binary files a/test/golden/undergroup274196240.png and b/test/golden/undergroup274196240.png differ diff --git a/test/golden/underleftarrow1031964323.png b/test/golden/underleftarrow1031964323.png index f5fe248c..6867a215 100644 Binary files a/test/golden/underleftarrow1031964323.png and b/test/golden/underleftarrow1031964323.png differ diff --git a/test/golden/underleftrightarrow128845106.png b/test/golden/underleftrightarrow128845106.png index 9a0cf69a..d33656c9 100644 Binary files a/test/golden/underleftrightarrow128845106.png and b/test/golden/underleftrightarrow128845106.png differ diff --git a/test/golden/underline160699299.png b/test/golden/underline160699299.png index ad1119a7..88b2821c 100644 Binary files a/test/golden/underline160699299.png and b/test/golden/underline160699299.png differ diff --git a/test/golden/underrightarrow963759956.png b/test/golden/underrightarrow963759956.png index 018d239e..83201533 100644 Binary files a/test/golden/underrightarrow963759956.png and b/test/golden/underrightarrow963759956.png differ diff --git a/test/golden/unicode-accents.png b/test/golden/unicode-accents.png index bb24655f..d84bf16d 100644 Binary files a/test/golden/unicode-accents.png and b/test/golden/unicode-accents.png differ diff --git a/test/golden/utilde829741846.png b/test/golden/utilde829741846.png index 9e99670d..9a4d222e 100644 Binary files a/test/golden/utilde829741846.png and b/test/golden/utilde829741846.png differ diff --git a/test/golden/vec928772845.png b/test/golden/vec928772845.png index 1ee84972..6f1f842d 100644 Binary files a/test/golden/vec928772845.png and b/test/golden/vec928772845.png differ diff --git a/test/golden/widecheck478155530.png b/test/golden/widecheck478155530.png index 54163b1a..7eada429 100644 Binary files a/test/golden/widecheck478155530.png and b/test/golden/widecheck478155530.png differ diff --git a/test/golden/widehat812256357.png b/test/golden/widehat812256357.png index 8c5a8e57..6b260d0e 100644 Binary files a/test/golden/widehat812256357.png and b/test/golden/widehat812256357.png differ diff --git a/test/golden/widetilde326421305.png b/test/golden/widetilde326421305.png index 4daadd81..6b39d400 100644 Binary files a/test/golden/widetilde326421305.png and b/test/golden/widetilde326421305.png differ diff --git a/test/helper.dart b/test/helper.dart index 9728a75a..4b061052 100644 --- a/test/helper.dart +++ b/test/helper.dart @@ -1,7 +1,10 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:ui' as ui; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_math_fork/ast.dart'; import 'package:flutter_math_fork/flutter_math.dart'; import 'package:flutter_math_fork/src/parser/tex/parser.dart'; @@ -14,20 +17,21 @@ void testTexToMatchGoldenFile( double scale = 1, }) { testWidgets(description, (WidgetTester tester) async { - tester.binding.window.physicalSizeTestValue = - Size(500 * scale, 300 * scale); - tester.binding.window.devicePixelRatioTestValue = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + tester.view.physicalSize = Size(500 * scale, 300 * scale); + tester.view.devicePixelRatio = 1.0; final key = GlobalKey(); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: RepaintBoundary( + key: key, child: Padding( padding: const EdgeInsets.all(8.0), child: Math.tex( expression, - key: key, options: MathOptions( style: MathStyle.display, fontSize: scale * MathOptions.defaultFontSize, @@ -93,11 +97,11 @@ void testTexToRenderLike( home: Scaffold( body: Center( child: RepaintBoundary( + key: key, child: Padding( padding: const EdgeInsets.all(8.0), child: Math.tex( expression1, - key: key, options: MathOptions( fontSize: MathOptions.defaultFontSize, style: MathStyle.display, @@ -110,13 +114,7 @@ void testTexToRenderLike( ), ); await tester.pumpAndSettle(); - if (Platform.isWindows) { - // Android-specific code - await expectLater( - find.byKey(key), - matchesGoldenFile( - 'golden/temp/${(description + expression1 + expression2).hashCode}.png')); - } + final firstImage = await _captureWidgetImage(tester, key); final key2 = GlobalKey(); await tester.pumpWidget( @@ -124,11 +122,11 @@ void testTexToRenderLike( home: Scaffold( body: Center( child: RepaintBoundary( + key: key2, child: Padding( padding: const EdgeInsets.all(8.0), child: Math.tex( expression2, - key: key2, options: MathOptions( fontSize: MathOptions.defaultFontSize, style: MathStyle.display, @@ -141,14 +139,61 @@ void testTexToRenderLike( ), ); await tester.pumpAndSettle(); - if (Platform.isWindows) { - // Android-specific code - await expectLater( - find.byKey(key2), - matchesGoldenFile( - 'golden/temp/${(description + expression1 + expression2).hashCode}.png')); + final secondImage = await _captureWidgetImage(tester, key2); + + expect( + secondImage.width, + firstImage.width, + reason: 'Rendered width mismatch for "$description"', + ); + expect( + secondImage.height, + firstImage.height, + reason: 'Rendered height mismatch for "$description"', + ); + expect( + listEquals(secondImage.bytes, firstImage.bytes), + isTrue, + reason: 'Rendered pixels mismatch for "$description"', + ); + }); +} + +Future<_CapturedImage> _captureWidgetImage(WidgetTester tester, Key key) async { + final boundary = tester.renderObject(find.byKey(key)); + final capturedImage = await tester.runAsync(() async { + final image = await boundary.toImage(pixelRatio: 1.0); + try { + final byteData = + await image.toByteData(format: ui.ImageByteFormat.rawRgba); + if (byteData == null) { + throw StateError('Failed to capture image bytes for widget.'); + } + return _CapturedImage( + width: image.width, + height: image.height, + bytes: byteData.buffer.asUint8List(), + ); + } finally { + image.dispose(); } }); + if (capturedImage == null) { + throw StateError('Failed to capture image for widget.'); + } + return capturedImage; +} + +class _CapturedImage { + const _CapturedImage({ + required this.width, + required this.height, + required this.bytes, + }); + + final int width; + final int height; + final Uint8List bytes; } const strictSettings = TexParserSettings(strict: Strict.error); diff --git a/test/katex_spec_test.dart b/test/katex_spec_test.dart index f35ac6fa..381f3ade 100644 --- a/test/katex_spec_test.dart +++ b/test/katex_spec_test.dart @@ -371,7 +371,6 @@ void main() { expect(r'\begingroup xy }', toNotParse()); }); - //TODO // test("should produce a semi-simple group", () { // final parse = getParsed(r'\begingroup xy \endgroup'); @@ -384,7 +383,6 @@ void main() { // // expect(ord.semisimple).toBeTruthy(); // }); - //TODO // test("should not affect spacing in math mode", () { // expect(r'\begingroup x+ \endgroup y'.toBuildLike(r'x+y'); // }); @@ -1377,7 +1375,6 @@ void main() { expect(m3.body.length, 2); }); - // TODO // test("should grab \\arraystretch", () { // final parse = getParsed(r'\def\arraystretch{1.5}\begin{matrix}a&b\\c&d\end{matrix}'); // expect(parse).toMatchSnapshot(); @@ -1560,7 +1557,7 @@ void main() { expect("\\iint\nolimits_i^n", toBuild); expect("\\iiint\nolimits_i^n", toBuild); expect("\\oint_i^n", toBuild); - // expect("\\oiint_i^n",toBuild); // TODO + // expect("\\oiint_i^n",toBuild); // expect("\\oiiint_i^n",toBuild); expect("\\oint\nolimits_i^n", toBuild); // expect("\\oiint\nolimits_i^n",toBuild); @@ -1606,11 +1603,11 @@ void main() { expect(r'\mathrm x', toParse()); expect(r'\mathbb x', toParse()); expect(r'\mathit x', toParse()); - // expect(r'\mathnormal x', toParse()); // TODO + // expect(r'\mathnormal x', toParse()); expect(r'\mathrm {x + 1}', toParse()); expect(r'\mathbb {x + 1}', toParse()); expect(r'\mathit {x + 1}', toParse()); - // expect(r'\mathnormal {x + 1}', toParse()); // TODO + // expect(r'\mathnormal {x + 1}', toParse()); }); test("should parse \\mathcal and \\mathfrak", () { @@ -1645,7 +1642,6 @@ void main() { texMathFontOptions["\\mathfrak"]); }); - // TODO // test("should parse nested font commands", () { // final nestedParse = getParsed(r'\mathbb{R \neq \mathrm{R}}').children[0]; // expect(nestedParse.font, "mathbb"); @@ -1759,7 +1755,6 @@ void main() { toParse()); }); - //TODO // test("should parse comments in the macro definition", () { // expect("\\def\\foo{1 %}\n2}\n\\foo").toParseLike(r'12'); // }); @@ -1776,15 +1771,12 @@ void main() { testTexToRenderLike("should not produce or consume space", "\\text{hello% comment 1\nworld}", r'\text{helloworld}'); - // TODO // testTexToRenderLike("should not produce or consume space", // "\\text{hello% comment\n\nworld}", r'\text{hello world}'); testTexToRenderLike( "should not include comments in the output", "5 % comment\n", r'5'); }); - -// TODO // group("A bin builder", () { // test("should create mbins normally", () { // final built = getParsed(r'x + y'); @@ -1815,7 +1807,6 @@ void main() { // }); // }); -// TODO // group("A \\phantom builder and \\smash builder", () { // test("should both build a mord", () { // expect(getBuilt(r'\hphantom{a}').children[0].classes).toContain("mord"); @@ -1889,7 +1880,6 @@ void main() { expect(r'\vec{x}_2', toBuild); expect(r'\vec{x}_2^2', toBuild); }); - // TODO // test("should produce mords", () { // expect(getBuilt(r'\vec x').children[0].classes).toContain("mord"); // expect(getBuilt(r'\vec +').children[0].classes).toContain("mord"); @@ -2293,7 +2283,6 @@ void main() { // }); // }); -// TODO // group("A document fragment", () { // test("should have paddings applied inside an extensible arrow", () { // final markup = katex.renderToString("\\tiny\\xrightarrow\\textcolor{red}{x}"); @@ -2311,7 +2300,6 @@ void main() { // }); // }); -// TODO // group("A parser error", () { // test("should report the position of an error", () { // try { @@ -2474,7 +2462,6 @@ void main() { // }); }); -// TODO // group("A parser that does not throw on unsupported commands", () { // // The parser breaks on unsupported commands unless it is explicitly // // told not to @@ -2546,7 +2533,6 @@ void main() { }); group("A macro expander", () { - // TODO // test("should produce individual tokens", () { // expect(r'e^\foo'.toParseLike("e^1 23", // new Settings({macros: {"\\foo": "123"}})); @@ -2658,7 +2644,7 @@ void main() { // }})); // }); - // TODO: The following is not currently possible to get working, given that + // The following is not currently possible to get working, given that // functions and macros are dealt with separately. /* test("should allow for space function arguments", () { @@ -2768,7 +2754,7 @@ void main() { expect(r'\varsubsetneqq\varsupsetneq\varsupsetneqq', toBuild); }); - // TODO(edemaine): This doesn't work yet. Parses like `\text text`, + //(edemaine): This doesn't work yet. Parses like `\text text`, // which doesn't treat all four letters as an argument. //test("\\TextOrMath should work in a macro passed to \\text", () { // expect(r'\text\mode'.toParseLike(r`([r'\text{text}', new Settings({macros: @@ -2790,7 +2776,7 @@ void main() { // expect(r'\gdef\foo\bar', toParse()); // expect(r'\gdef{\foo\bar}{}'.not, toParse()); // expect(r'\gdef{}{}'.not, toParse()); - // // TODO: These shouldn't work, but `1` and `{1}` are currently treated + // // These shouldn't work, but `1` and `{1}` are currently treated // // the same, as are `\foo(r' and '){\foo}`. // //expect(r'\gdef\foo1'.not, toParse()); // //expect(r'\gdef{\foo}{}'.not, toParse()); @@ -2814,7 +2800,7 @@ void main() { // test("\\global needs to followed by \\def", () => { // expect(r'\global\def\foo{}\foo'.toParseLike(r''); - // // TODO: This doesn't work yet; \global needs to expand argument. + // // This doesn't work yet; \global needs to expand argument. // //expect(r'\def\DEF{\def}\global\DEF\foo{}\foo'.toParseLike(r''); // expect(r'\global\foo'.not, toParse()); // expect(r'\global\bar x'.not, toParse()); @@ -2953,7 +2939,7 @@ void main() { r'\operatorname*{arg\,max}'); }); -// TODO +// // group("\\tag support", () { // final displayMode = new Settings({displayMode: true}); @@ -3018,10 +3004,10 @@ void main() { // }); // }); -// TODO +// // group("Unicode accents", () { // test("should parse Latin-1 letters in math mode", () { -// // TODO(edemaine): Unsupported Latin-1 letters in math: ÇÐÞçðþ +// // (edemaine): Unsupported Latin-1 letters in math: ÇÐÞçðþ // expect(`ÀÁÂÃÄÅÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝàáâãäåèéêëìíîïñòóôõöùúûüýÿ` // .toParseLike( // r(r'\grave A\acute A\hat A\tilde A\ddot A\mathring A') + @@ -3041,7 +3027,7 @@ void main() { // }); // test("should parse Latin-1 letters in text mode", () { -// // TODO(edemaine): Unsupported Latin-1 letters in text: ÇÐÞçðþ +// //(edemaine): Unsupported Latin-1 letters in text: ÇÐÞçðþ // expect(`\text{ÀÁÂÃÄÅÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝàáâãäåèéêëìíîïñòóôõöùúûüýÿ}` // .toParseLike( // r(r'\text{\')A\'A\^A\~A\"A\r A` + @@ -3084,7 +3070,7 @@ void main() { // }); group("Unicode", () { - // TODO + // // test("should parse negated relations", () { // expect(r'∉∤∦≁≆≠≨≩≮≯≰≱⊀⊁⊈⊉⊊⊋⊬⊭⊮⊯⋠⋡⋦⋧⋨⋩⋬⋭⪇⪈⪉⪊⪵⪶⪹⪺⫋⫌', toParse()); // }); @@ -3097,7 +3083,7 @@ void main() { expect(r'∏∐∑∫∬∭∮⋀⋁⋂⋃⨀⨁⨂⨄⨆', toBuildStrict); }); - // TODO + // // test("should build more relations", () { // expect( // r'⊂⊃⊆⊇⊏⊐⊑⊒⊢⊣⊩⊪⊸⋈⋍⋐⋑⋔⋛⋞⋟⌢⌣⩾⪆⪌⪕⪖⪯⪰⪷⪸⫅⫆≘≙≚≛≝≞≟≲⩽⪅≶⋚⪋', diff --git a/test/widgets/selectable_test.dart b/test/widgets/selectable_test.dart index 9fd70b15..28d90a9d 100644 --- a/test/widgets/selectable_test.dart +++ b/test/widgets/selectable_test.dart @@ -96,7 +96,7 @@ void main() { final selectableMath = tester.state( find.byType(InternalSelectableMath)); // selectable text cannot open keyboard. - // TODO + // // await tester.showKeyboard(find.byType(InternalSelectableMath)); // expect(tester.testTextInput.hasAnyClients, false); // await skipPastScrollingAnimation(tester); diff --git a/test/widgets/unicode_text_test.dart b/test/widgets/unicode_text_test.dart new file mode 100644 index 00000000..799e0f6f --- /dev/null +++ b/test/widgets/unicode_text_test.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_math_fork/flutter_math.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../load_fonts.dart'; + +void main() { + setUpAll(loadKaTeXFonts); + + Finder richTextWithPlainText(String text) => find.byWidgetPredicate( + (widget) => widget is RichText && widget.text.toPlainText() == text, + ); + + testWidgets('renders unicode text inside text mode', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Math.tex( + r'\text{試 বাংলা é}', + textStyle: const TextStyle(fontSize: 24), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + expect(richTextWithPlainText('বাংলা'), findsOneWidget); + expect(richTextWithPlainText('ব'), findsNothing); + }); + + testWidgets('renders arabic text inside text mode', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Math.tex( + r'\text{العربية } + x^2 = 25', + textStyle: const TextStyle(fontSize: 24), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + expect(richTextWithPlainText('العربية'), findsOneWidget); + expect(richTextWithPlainText('ا'), findsNothing); + }); + + testWidgets('renders selectable arabic text inside text mode', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SelectableMath.tex( + r'\text{العربية } + x^2 = 25', + textStyle: const TextStyle(fontSize: 24), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + expect(richTextWithPlainText('العربية'), findsOneWidget); + expect(richTextWithPlainText('ا'), findsNothing); + }); + + testWidgets('renders top-level unicode with non-strict settings', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Math.tex( + '試 বাংলা é', + settings: const TexParserSettings(strict: Strict.ignore), + textStyle: const TextStyle(fontSize: 24), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + }); + + testWidgets('renders top-level unicode with default settings', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Math.tex( + '試 বাংলা é', + textStyle: const TextStyle(fontSize: 24), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + }); + + testWidgets('renders math and unicode text on the same line', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Math.tex( + r'x^2 + \text{試 বাংলা é} + \alpha', + textStyle: const TextStyle(fontSize: 24), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + expect(richTextWithPlainText('বাংলা'), findsOneWidget); + expect(richTextWithPlainText('ব'), findsNothing); + }); + + testWidgets('renders math and raw unicode on the same line', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Math.tex( + '試 + x^2 + বাংলা', + settings: const TexParserSettings(strict: Strict.ignore), + textStyle: const TextStyle(fontSize: 24), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + }); +}