-
Notifications
You must be signed in to change notification settings - Fork 28.5k
Add InlineSpan.updateAttributes
#154017
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Add InlineSpan.updateAttributes
#154017
Conversation
/// `newAttributes` to the given [TextRange] within this span. | ||
/// | ||
/// The `offset` parameter is used by the implementation to track the offsets | ||
/// from the start of the root span to sub-spans , it should be set to null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: strange spacing around the ,
expect(const Right<int, bool>(true).toString(), 'Right<int, bool>(true)'); | ||
}); | ||
|
||
test('Equality', () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about testing Right
? And maybe include some cases that are not equal?
/// | ||
/// The `offset` parameter is used by the implementation to track the offsets | ||
/// from the start of the root span to sub-spans , it should be set to null | ||
/// (the default) if you are not implementing an `InlineSpan` subclass. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What should I do if I am implementing an InlineSpan subclass? Is there somewhere a reader could learn more about this parameter? This doc is very ominous about it...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can hide the parameter from the userfacing API by introducing another method that just calls this one with offset
set to Accumulator()
, and that's what other methods do, I guess I'll follow the convention.
@@ -364,6 +379,15 @@ abstract class InlineSpan extends DiagnosticableTree { | |||
@protected | |||
int? codeUnitAtVisitor(int index, Accumulator offset); | |||
|
|||
/// Creates a new [InlineSpan] from this [InlineSpan] by applying | |||
/// `newAttributes` to the given [TextRange] within this span. | |||
/// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Document: How do I create an InlineSpan that removes an attribute?
|
||
/// An attribute which overwrites an [InlineSpan]'s [TextStyle.foreground] and | ||
/// [TextStyle.color] if set to non-null. | ||
final Either<ui.Color, ui.Paint>? foreground; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code to call this is kinda odd:
InlineSpanAttributes(
foreground: Left(Colors.green),
);
As a reader of that, it is not clear what the "Left" means here. Why do we need to invent something new here instead of using the same pattern that e.g. TextStyle is using for [TextStyle.foreground] and [TextStyle.color]?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also applies to the other Either
properties below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right
is also kinda awkwardly overloaded. Here, it means an alternative representation (Paint instead of Color). For recognizer
it means remove the property.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The goal is to make it statically impossible to set both color and foreground. I can either typedef Foreground = Left
or use an assert (if the InlineSpanAttributes construction happens in a const context then I think the assert will also be evaluated at compile time, so I guess that's static-ish?), WDYT?
/// from the start of the root span to sub-spans , it should be set to null | ||
/// (the default) if you are not implementing an `InlineSpan` subclass. | ||
@useResult | ||
InlineSpan updateAttributes(covariant InlineSpanAttributes newAttributes, TextRange textRange, { Accumulator? offset }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To me, "update" indicates an in-place mutation, not that a new instance is returned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the best I could come up with unfortunately. Any suggestions?
On the other hand I think TextStyle.merge
or TextStyle.apply
indicate mutation too (without context). The InlineSpan class has to be immutable since equality is important for the class, so I guess in that context the caller should assume all InlineSpan operations are non-mutating.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DBC: To my eye "apply" can equally well describe something that mutates, or something that returns its result instead. After all it's a generic term used for function calls, and functions can be either mutating or non-mutating.
Similarly I read "merge" as describing how the result relates to the inputs, but leaving open whether something gets mutated into the result state or instead everything's immutable and the result gets returned.
But "update" on the other hand specifically sounds to me like it's talking about changing something, mutating it.
So I'd read "applyAttributes" or "mergeAttributes" as leaving unstated whether the span gets mutated, which is OK because either the method's doc or a good understanding of InlineSpan in general resolves the uncertainty; but "updateAttributes" as misleading because it seems to specifically say something that isn't actually what's intended.
? List<TextSpan>.filled(1, child1) | ||
: (List<TextSpan>.filled(2, child1)..[1] = child2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: indentation (increase by 1 space)
addTearDown(recognizer.dispose); | ||
final int length = simpleSpan.toPlainText().length; | ||
final TextSpan withRecognizer = simpleSpan.updateAttributes( | ||
InlineSpanAttributes(recognizer: Left(recognizer)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As stated elsewhere, the Left
in there feels out of place. When reading this code, it is unclear what this means and if you click through on Left
it gives you a very generic description about Left that isn't helpful. In fact, I don't even know what docs to point people to if they want to understand what this Left instantiation here does.
final TextSpan partiallyDeleted = withRecognizer.updateAttributes( | ||
const InlineSpanAttributes(recognizer: InlineSpanAttributes.remove), | ||
const TextRange(start: 5, end: 6), | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The remove API is kinda awkward. Could we split up updateAttributes into addAttribute and removeAttribute to make this more straight-forward?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The downside of that is we'll need two separate methods and two separate attribute sets (one for update and one for removal) for basically the same thing on InlineSpan
. Maybe introducing sentinel values for removal is the way?
Could you add a little more context (e.g. to the PR description or via a linked issue) what you envision this new |
4033d59
to
31f07cb
Compare
Removed the |
31f07cb
to
bde30bc
Compare
@@ -50,7 +50,7 @@ export 'src/painting/image_decoder.dart'; | |||
export 'src/painting/image_provider.dart'; | |||
export 'src/painting/image_resolution.dart'; | |||
export 'src/painting/image_stream.dart'; | |||
export 'src/painting/inline_span.dart'; | |||
export 'src/painting/inline_span.dart' hide RemoveInlineSpanAttribute; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We usually export everything, https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md#structure
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would still prefer hiding the type here since its only inhabitant is a sentinel value that only supports the identical
operation.
/// | ||
/// All attributes are nullable, and default to `null`. Attributes set to `null` | ||
/// in an [InlineSpanAttributes] will not be updated. | ||
/// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably document how attributes can be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am still not sure I understand the philosophy behind why some attributes are removable and others are not. You can create a fresh TextStyle without setting a font family (which I think in practice means it's gonna be inferred from e.g. a sounding DefaultTextStyle (although I am not sure right now at what level that happens)). Why would you never want to remove the fontFamily from an existing TextStyle using this API?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If that situation is uncommon (you probably have a better feeling than I for that) we should still document what's possible and what isn't.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove the fontFamily
Ah that sounds like a legit use case. There's no way to use the "default" font family unless it's set to null.
it's gonna be inferred from e.g. a sounding DefaultTextStyle
It happens in the Text
widget I think. But yeah to support removing fontSize
(such that the text uses the ambient DefaultTextStyle fontSize) we would need a different sentinel value.
&& lineThrough == null | ||
&& decorationColor == null | ||
&& decorationStyle == null | ||
&& decorationThickness == null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: Could InlineSpanAttributes in its constructor just take a TextStyle instead of all its individual properties?
/// the span of this [InlineSpan]. The implementation is responsible for | ||
/// advancing the offset by this [InlineSpan]'s length in UTF16 code units, | ||
/// such that the `offset` points to the end (enclusive) of this [InlineSpan] | ||
/// when this method returns. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe leave a note that you'd typically override this method, but call updateAttributes?
InlineSpan updateAttributesAtOffset(covariant InlineSpanAttributes newAttributes, TextRange textRange, Accumulator offset); | ||
|
||
/// Creates a new [InlineSpan] from this [InlineSpan] by applying | ||
/// `newAttributes` to the given [TextRange] within this span. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, maybe leave a comment that subclasses should override updateAttributesAtOffset instead of this one?
/// | ||
/// See also: | ||
/// | ||
/// * [InlineSpan.updateAttributesAtOffset], which takes an [InlineSpanAttributes], and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here and elsewhere in the docs: Should the docs link to updateAttributes
instead?
/// replaces existing recognizers (if any) within the given range with the | ||
/// specified recognizer. | ||
/// | ||
/// When this value is set to [remove], the [InlineSpan.updateAttributesAtOffset] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering: Could we use null to indicate "remove" by using remove
(under another name) as the default value for this property? That default value could maybe even have a private type so we don't need to export RemoveInlineSpanAttribute.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the sentinel values aren't exposed, whether an attribute should be updated would become a compile-time decision?
E.g.:
final Color? maybeColor = ...;
if (maybeColor == null) {
updateAttributes(.... other attributes);
} else {
updateAttributes(.... other attributes, color: maybeColor);
}
It will quickly become unscaleable configuration matrix I think, not great for APIs that want to wrap this API.
/// [TextSpan.onEnter] can not be set from a non-null value to null using this | ||
/// attribute. Considering setting [TextSpan.onEnter] to a function that does | ||
/// nothing if you want to unset the callback from a [TextSpan]. | ||
final PointerEnterEventListener? onEnter; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That default value could also work for these, reducing the number of remove options from 3 (unremovable, removable with remove
, removable by setting to a no-op) to 2 (unremovable, use null to indicate remove).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the updating function mostly looks good to me
} | ||
} | ||
|
||
final class _PoorMansBottomType with DiagnosticableTreeMixin implements GestureRecognizer { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This really needs a better name....
/// [InlineSpan.updateAttributes] does not mutate the target | ||
/// [InlineSpan]. Rather, when [recognizer] is set to [remove], that method | ||
/// returns a new [InlineSpan] with [recognizer] set to null. | ||
static const RemoveInlineSpanAttribute remove = _PoorMansBottomType._(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can avoid this by doing something like this
InlineSpanAttributes(
GestureRecognizer? this.recognizer = _somePrivateDefaultValue
)
if (recognizer == _somePrivateDefaultValue) // no change to recognizer
else // replace whatever value that passed in
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and should update the doc says passing in null to recognizer will remove recognizer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks like it is also used in text_span.dart, but I think it is still better to ask people to use remove
property to clean up the field.
Also have we consider merging these span libraries into one file? it feels unecessary to separate them and expose a bunch of internal method...
&& lineThrough == null | ||
&& decorationColor == null | ||
&& decorationStyle == null | ||
&& decorationThickness == null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
Still evaluating some alternatives as I would like to expose more lower level text layout apis, and at the same time some hacks in the PR made the API less appealing (e.g., the removal problem). |
(Triage) Confirmed with @LongCatIsLooong this is still on their radar |
Hello from triage again! Is this still something you'd like to work on? |
Use cases in the framework:
flutter/packages/flutter/lib/src/widgets/spell_check.dart
Lines 312 to 414 in ad268e2
TextEditingController.buildTextSpan
returns aTextSpan
, it's difficult to update the returned span to add new styles without aupdate
method.Use cases outside the framework
InlineSpan
with new styles, especially when you only have theInlineSpan
representation.Pre-launch Checklist
///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.