| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
| Defined in `packages/flutter/lib/src/rendering/text_plugin.dart`, `TextPlugin` exposes a declarative `contentAccessIntent` property and five lifecycle hooks: | ||
|
|
||
| ```dart | ||
| enum ContentAccessIntent { highlight, scan } |
There was a problem hiding this comment.
this can use a better name.
e.g.,
or just a bool if there isn't going to be a third option
Sorry, something went wrong.
There was a problem hiding this comment.
also avoid ending the class with Intent it may confused with keyboard shortcut
Sorry, something went wrong.
There was a problem hiding this comment.
or just a bool if there isn't going to be a third option
drive-by:
Right now it reads like "highlight" is "read-write" and "scan" is "read-only" intent to the text. I'm not sure how that maps to you suggestion onScreen and includeOffScreen. I'd lean against using a bool and vs giving it a descriptive general purpose name (e.g. "highlight" doesn't feel general purpose enough if StockTicker changes the presentation).
Sorry, something went wrong.
There was a problem hiding this comment.
IIUC this is to workaround the issue where listview is lazyloaded, and it will only build and layout what is visible on the screen.
Everything offscreen won't be in the rendering tree (which means we don't know their geometry)
For ctrl+f, we want to be able to search for string even if they are off screen, and thus this enum is to tell the listview whether the plugin only care about what is visible on the screen (hightlight) or it cares about all the text even if they are off screen (scan)
The reason I suggest a boolean is that i can't think off other option the plugin may want, so a boolean like
bool includeOffScreen => true may be sufficient.
Sorry, something went wrong.
There was a problem hiding this comment.
Yeah, this is to cancel lazy loading. Initially I wanted to have a boolean like "cancelLazyLoading" but then I thought it shouldn't be up to plugin to decide if list view should cancel lazy loading. The plugin can only express their "intent".
But if "intent" already has other meanings. I think a boolean like "needOffScreenContent" also makes sense.
Sorry, something went wrong.
|
|
||
| ContentAccessIntent get contentAccessIntent => ContentAccessIntent.highlight; | ||
|
|
||
| void didAddText(TextDelegate delegate) {} |
There was a problem hiding this comment.
slightly confused at how should developer use these API, I think the text is not layout at the moment, which means most of its geometry API is not avaliable or can even crash
Sorry, something went wrong.
There was a problem hiding this comment.
should these callback only triggered after a layout is complete?
Sorry, something went wrong.
|
|
||
| 1. Before clearing `_matches`, it records `final SearchMatch? previousActive = activeMatch`. | ||
|
|
||
| 2. It sorts the mounted delegates in top-to-bottom document reading order via `final sortedDelegates = _delegates.toList()..sort()`, which delegates to `TextDelegate.compareTo` (lowest common ancestor pre-order traversal in the render tree). |
There was a problem hiding this comment.
if a single text change the entire text gets re-evaluated? this feels may be a performance problem if the document is huge.
Sorry, something went wrong.
|
|
||
| #### 3.6.2 Document-ordered match indexing and active match preservation | ||
|
|
||
| `SearchInPagePlugin` maintains the set of mounted text delegates in `final Set<TextDelegate> _delegates`. Whenever a delegate mounts (`didAddText`), changes text (`didUpdateText`), unmounts (`didRemoveText`), or the user updates `query` or `caseSensitive`, `_rebuildMatches()` runs: |
There was a problem hiding this comment.
didAddText is called before the layout, if the synchronous logic calls getBoxForSelections, this will force it to layout with old constraint.
Sorry, something went wrong.
|
|
||
| After indexing matches, `_updatePainters()` groups matches by `TextDelegate` and configures each delegate's painters: | ||
|
|
||
| 1. If a delegate has no matches, both `delegate.backgroundPainter` and `delegate.foregroundPainter` are set to `null`. |
There was a problem hiding this comment.
should backgroundPainter be a list if there are multiple plugin that want to inject the painters to the delegate?
Sorry, something went wrong.
|
|
||
| ## 6. Future directions and forward compatibility | ||
|
|
||
| Although the initial scope targets `RenderParagraph` and `RenderEditable`, we shaped `TextDelegate`, `TextPlugin`, and `TextPluginScope` so the architecture can expand to non-text widgets and native platform-view text fields without breaking existing plugins. |
There was a problem hiding this comment.
we should at least have an example code for non text widget
Sorry, something went wrong.
There was a problem hiding this comment.
e.g. custom widget that use textpainter directly
Sorry, something went wrong.
|
|
||
| To prevent this: | ||
|
|
||
| 1. `TextDelegate.hasLayout` exposes `_paragraph.hasSize && !_paragraph.debugNeedsLayout`. |
There was a problem hiding this comment.
If the delegate does have layout, it seems not useful to the caller. why trigger the callback at all?
Sorry, something went wrong.
|
|
||
| Inside `TextPainter` and `ui.Paragraph`, every `PlaceholderSpan` occupies 1 UTF-16 code unit represented by the Unicode Object Replacement Character `\uFFFC` (`0xFFFC`). | ||
|
|
||
| If `delegate.text` omitted placeholders (`includePlaceholders: false`), `' GOOG today'` would start at index `4` in `delegate.text`, but at offset `5` in `TextPainter`. Passing `TextSelection(baseOffset: 4, extentOffset: 8)` to `getBoxesForSelection` would highlight `'GOO'` instead of `'GOOG'`. Conversely, if `delegate.text` used `includeSemanticsLabels: true` (the default of `InlineSpan.toPlainText()`), a `TextSpan(text: 'GOOG', semanticsLabel: 'Alphabet Inc.')` would return `'Alphabet Inc.'` in `delegate.text`, desynchronizing string indices from `TextPainter` glyph offsets. |
There was a problem hiding this comment.
Do we have use case where we ever want to process semantics label in text plugin?
Sorry, something went wrong.
|
|
||
| `TextDelegate.text` calls `_paragraph.text.toPlainText(includeSemanticsLabels: false)`, which keeps `includePlaceholders: true` (emitting `\uFFFC` for each `PlaceholderSpan`) and sets `includeSemanticsLabels: false`. This preserves a 1-to-1 UTF-16 offset match between `delegate.text` indices and `TextSelection` offsets in `TextPainter`. | ||
|
|
||
| Because `\uFFFC` is not a word character (`\w`) or whitespace (`\s`), regex patterns like `\bGOOG\b` naturally treat adjacent `WidgetSpan`s as word boundaries without shifting character indices. Plugin authors can also pass `includePlaceholders: false` to `getBoxesForSelection` (or inspect `delegate.placeholderRanges`) so multi-word matches do not highlight across an inline widget. |
There was a problem hiding this comment.
It seems hard for plugin authors to handle this, should we provide opinionated API to them directly?
Sorry, something went wrong.
|
|
||
| ### 7.7 Duplicate plugin instances across nested scopes | ||
|
|
||
| A widget tree might mount the same `TextPlugin` instance twice, either in `TextPluginScope.multiple(plugins: [pluginA, pluginA])` or in both an outer `TextPluginScope(plugin: pluginA)` and an inner `TextPluginScope(plugin: pluginA)`. |
There was a problem hiding this comment.
I think in most other place like action shortcut, we deduplicate by type, so instead just take a list in inherited widget we do something like
<Type, Factory> map.
Sorry, something went wrong.
There was a problem hiding this comment.
Quick drive by review until I can take a deeper look:
The demo is awesome. Can you share the source code? I think the repo (https://github.com/hannah-hyj/flutter_text_plugin_demo) contains only the compiled assets.
One thing we should be wary of in this design is complexity that increases with the amount of text on the page. Flutter is currently already pretty bad at handling large amounts of text, but we should avoid making that worse if we can.
Sorry, something went wrong.
| Defined in `packages/flutter/lib/src/rendering/text_plugin.dart`, `TextPlugin` exposes a declarative `contentAccessIntent` property and five lifecycle hooks: | ||
|
|
||
| ```dart | ||
| enum ContentAccessIntent { highlight, scan } |
There was a problem hiding this comment.
Intent is also a widget (docs), so maybe consider using a different name to avoid confusion.
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Demo app: https://hannah-hyj.github.io/flutter_text_plugin_demo/
Prototype Branch: hannah-hyj/flutter@text-plugin-testing This branch includes code changes in framework and code for the demo app.
If you want to access demo app code , it's in https://github.com/hannah-hyj/flutter/tree/text-plugin-testing/examples/text_plugins
If you want to access framework code changes, it's in https://github.com/hannah-hyj/flutter/tree/text-plugin-testing/
issue: flutter/flutter#65504
#25 was closed in favor of this one
Summary
Proposes a composable text plugin architecture (TextPluginScope, TextPlugin, and TextDelegate) in package:flutter/rendering.dart and package:flutter/widgets.dart.
This enables cross-cutting text inspection, background/foreground decoration painting, pointer gesture handling, and scroll-to-range across standard Text, RichText, and EditableText (TextField) subtrees (RenderParagraph and RenderEditable) without requiring custom text widgets or TextSpan wrappers.
Key Use Cases
Pre-launch Checklist
If you need help, consider asking for advice on the #hackers channel on Discord.