Skip to content
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

Expose text boundary combiner class #112085

Merged
merged 9 commits into from
Sep 26, 2022

Conversation

chunhtai
Copy link
Contributor

@chunhtai chunhtai commented Sep 21, 2022

More public classes

fixes #111751

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@flutter-dashboard flutter-dashboard bot added a: text input Entering text in a text field or keyboard related problems framework flutter/packages/flutter repository. See also f: labels. labels Sep 21, 2022
@chunhtai chunhtai changed the title Text boundary part ii! Expose text boundary combiner class Sep 21, 2022
@chunhtai chunhtai marked this pull request as ready for review September 21, 2022 16:50
packages/flutter/lib/src/services/text_boundary.dart Outdated Show resolved Hide resolved
packages/flutter/lib/src/services/text_boundary.dart Outdated Show resolved Hide resolved
packages/flutter/lib/src/services/text_boundary.dart Outdated Show resolved Hide resolved
return position;
}

for (index -= 1; index >= 0; index -= 1) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the first term index -= 1 isn't that common in for loops. Consider using something like

while ((index -= 1) >= 0)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be easier to read if there's an _effectiveTextOffset method that turns caret locations to int offsets?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while loop makes sense, I am not sure how _effectiveTextOffset should be used to implement this for loop. Can you elaborate?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like (I thought you added a similar function in the previous PR?):
int _toTextOffset(TextPosition position) => max(0, min(upstream ? position.offset - 1 : position.offset, textLength - 1));
Then:

int offset = _toTextOffset(position);
while (offset >= 0 && !TextLayoutMetrics.isWhitespace(_text.codeUnitAt(offset)) { 
  offset -= 1;
}
return Offset(offset: offset + 1, affinity: offset < 0 ? upstream : downstream); // Is the affinity correct?

Copy link
Contributor Author

@chunhtai chunhtai Sep 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I don't want to change the the position to text offset before the first

    if (position.affinity == TextAffinity.downstream && !TextLayoutMetrics.isWhitespace(_text.codeUnitAt(index))) {
      return position;
    }

is that I need to preserve the affinity of the input if the input already point to a non whitespace character.

An example lets say you have string without any white space 'abcd'.

The expected behavior is the following:

getLeadingTextBoundaryAt(TextPosition(3, upstream)) = TextPosition(3, upstream)
getLeadingTextBoundaryAt(TextPosition(2, downstream)) = TextPosition(2, downstream)

however your code will produce

getLeadingTextBoundaryAt(TextPosition(3, upstream)) = TextPosition(3, upstream)
getLeadingTextBoundaryAt(TextPosition(2, downstream)) = TextPosition(3, upstream)

The problem is you can't tell between TextPosition(3, upstream) and TextPosition(2, downstream) because they are the same in text offset. If you convert it first before the return, you can't differentiate them.

I could do the conversion after the first return, but at that point, I don't care about affinity anymore because I only need to find first non whitespace character and do (index+1 upstream).


@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
// Handles outside of right bound.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: left -> leading/start, right -> trailing/end. The TextBoundary class does not know the directionality of the text.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I don't remember why we need to clamp here, doesn't seem to be the case for other text boundary classes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did this for character and this because these require manually calculation on the string, the others call into textlayoutmetrics, which handle the boundary check. we could do this for all text boundaries regardless though, WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't most text boundaries assert the incoming position is within the document range?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PushTextPosition may potentially push out side of the bound, unless I add a API to query text length in TextBoundary, I can't do perform clamping there. I think it is not worth it though.

packages/flutter/lib/src/services/text_boundary.dart Outdated Show resolved Hide resolved
packages/flutter/lib/src/services/text_boundary.dart Outdated Show resolved Hide resolved
packages/flutter/lib/src/services/text_boundary.dart Outdated Show resolved Hide resolved
packages/flutter/lib/src/services/text_boundary.dart Outdated Show resolved Hide resolved
return inner.getTrailingTextBoundaryAt(
outer.getTrailingTextBoundaryAt(position),
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider overridding

TextRange getTextBoundaryAt(TextPosition position)

too.

This method exists because it's sometimes slightly cheaper to query both the leading + trailing than to query them separately. (for example, the low-level line boundary call on ui.Paragraph actually gives you both offsets so there's no need to call that twice to get both the leading boundary and trailing boundary). That probably should have been documented...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah LineBreak didn't override that. Could you add that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added line break and wordboundary override, but I can't find a meaningful implementation of ExpandedTextBoundary.getTextBoundaryAt since the return value doesnot contain affinity. I can't use the return value of the outer textboundary to call inner textboundary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's sad. Can we assume the TextRange's start position is always downstream and end position upstream?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well if the TextRange is collapsed we'll lose information. Maybe we should make TextPosition a subclass of TextRange?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's sad. Can we assume the TextRange's start position is always downstream and end position upstream?

That is not the case for whitespace text boundary.

abc def the position at the whitespace in the middle will return
leading = TextPosition(3, upstream)
trailing = TextPosition(4, downstream)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think TextRange.start and TextRange.end should be a TextPosition instead of int

packages/flutter/lib/src/services/text_boundary.dart Outdated Show resolved Hide resolved
/// by one with its affinity sets to upstream. For example,
/// `TextPosition(1, upstream)` becomes `TextPosition(1, downstream)`,
/// `TextPosition(4, downstream)` becomes `TextPosition(5, upstream)`.
class PushTextPosition extends TextBoundary {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to use TextBoundary composition instead of taking a TextBoundary explicitly? With the former say we create 2 constants PreviousCaretLocation and NextCaretLocation,

So today's

_PushTextPosition(mixedBoundary, intent.forward);

will be equivalent to

(intent.forward ? TextBoundary.NextCaretLocation : TextBoundary.PreviousCaretLocation) + mixedBoundary;

or

_PushTextPosition(info.forward) + mixedBoundary;

The order of operations will be clearer (from left to right, with _PushTextPosition it's not immediately clear if mixedBoundary will be applied first or push will be applied first).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it hard to understand than the current code

PushTextPosition( WhiteSpaceBoundary() + WordBoundary(), true)
will become
(WordBoundary()+ WordBoundary).nextCaretLocation where the nextCaretLocation is from right to left.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not suggesting making that a getter. Your example will be equivalent to NextCaretLocation + WhiteSpaceBoundary + WordBoundary

return inner.getTrailingTextBoundaryAt(
outer.getTrailingTextBoundaryAt(position),
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's sad. Can we assume the TextRange's start position is always downstream and end position upstream?

return inner.getTrailingTextBoundaryAt(
outer.getTrailingTextBoundaryAt(position),
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well if the TextRange is collapsed we'll lose information. Maybe we should make TextPosition a subclass of TextRange?

Copy link
Contributor

@LongCatIsLooong LongCatIsLooong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. IMO it's better to make _PushTextPosition a regular TextBoundary so you could do _PushTextPosition + Whitespace + WordBoundary, instead of letting it handle composition itself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a: text input Entering text in a text field or keyboard related problems autosubmit Merge PR when tree becomes green via auto submit App framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ui.Paragraph getWordBoundary should treat input position as caret position.
2 participants