-
Notifications
You must be signed in to change notification settings - Fork 6k
[ios][iOS 17] fix auto-correction highlight on top left corner #44604
Conversation
a248a39
to
76f3ab2
Compare
// The candidates view can't be shown if the framework has not sent the | ||
// first caret rect. | ||
if (CGRectEqualToRect(kInvalidFirstRect, _markedRect)) { | ||
return kInvalidFirstRect; | ||
return hostView ? [hostView convertRect:kInvalidFirstRect toView:self] : kInvalidFirstRect; |
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.
@LongCatIsLooong I'm a bit iffy on this one and could use another pair of eyes:
My understanding of your previous PR is that this should return rects in FlutterTextInputView's coord space - not only valid rects, but also this dummy invalid rect (because otherwise an invalid rect could very well become a valid rect with some transforms of FlutterTextInputView).
Let me know if you wanna discuss offline.
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.
Yeah I think kInvalidFirstRect
is probably specified in the window's coordinate system so it needs to be transformed. The input view used to be placed at the top left corner of the window iirc.
@@ -1426,44 +1427,53 @@ - (void)testUpdateFirstRectForRange { | |||
@(-6.0), @(3.0), @(9.0), @(1.0) | |||
]; | |||
|
|||
CGRect kInvalidFirstRectRelative = | |||
[textInputPlugin.viewController.view convertRect:kInvalidFirstRect toView:inputView]; |
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.
Wanna highlight the changes to this test case. Let me know if it makes sense @LongCatIsLooong
76f3ab2
to
48d36e8
Compare
I'm still unclear on why we can't make this work correctly. If the size is exactly correct, it's clearly based on rects that we are providing; why can't we ensure that we are providing correct location in addition to size so that the highlight actually works? |
@stuartmorgan Because the size is 1 character shorter, and there's no reliable API to tell whether the highlight is shown or not (If there were such API, we would be able to maybe manually highlight the last character). |
Leaving aside the issue of it missing the last character: do we know why the rect is in completely the wrong place, and can we fix that? Are we providing bogus offsets in our implementation of the query method? Then for the last-character issue, have we made a reduced test case and filled an issue with Apple? |
To clarify, our implementation of the query method ( The reason I chose to hide it in this PR is because the size doesn't match (missing 1 char). Other than that, we can easily position the highlight at the right place (pretty much the same logic as positioning it outside screen in this PR).
I have not filed a radar with Apple. It's a good idea. |
@stuartmorgan Actually, while I was writing this, it came to my mind that it might be better UX to show a slightly smaller highlight region, than not showing it at all. Let me try and see how it looks. |
If the view that we are telling UIKit the words are relative to is in the top left corner, and we're returning rects that don't have the correct offset for the word relative to the top left corner of the Flutter view, then we aren't returning the correct rects. Am I misunderstanding here? |
Oh the API should return the rects wrt the coordinate space of UITextInput itself, so I think we did it right.
I take back my word - Just tried it, looks pretty bad: Simulator.Screen.Recording.-.iPhone.14.Pro.Max.-.2023-08-11.at.09.54.50.mp4 |
I'm not following. You're saying that we are returning the actual offsets of the queried text, relative to the upper-left corner of the Flutter view (where the UITextInput is positioned), and yet UIKit is drawing them in a random location instead? |
We are returning the offset of queried text, relative to the upper-left corner of UITextInput, which happens to be positioned at the origin of FlutterView. UIKit is drawing the highlight relative to UITextInput correctly (not randomly). So in this PR, I moved this UITextInput outside screen. |
I do not understand how both of these things can possibly be true. If we were returning the actual offset of the words relative to the origin of the Flutter view (where the UITextInput is), and UIKit were drawing the rect using that same offset and origin, then the rectangle would be in the correct location. It's not, so either one of those things is not true, or something else is wrong that I still don't understand. |
Looking at the code here more closely, I see that the beginning of |
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'm interested in understanding more about why the highlight lags by 1 character if you know any more details @hellohuanlin. It would be great if we could fix that rather than hiding it off screen. If that's not possible then this approach looks good to me, though.
I wonder if some state is out of date (_markedRect
, text
?) at the time that firstRectsForRange is called.
I do not understand how both of these things can possibly be true.
My understanding is (before this PR) we returned the position of the text relative to the UITextInput correctly, but the UITextInput was just stuck naively at 0,0 rather than at the location of the TextField. We could easily put the UITextInput at the correct location of the TextField, but then the highlight would show up, and it looks wrong since it lags by 1 character. This PR moves UITextInput offscreen to avoid this.
So the real problem we have here seems to be the 1 character lag in the autocorrect highlight, if I'm understanding correctly.
@@ -34,6 +34,13 @@ | |||
// returns kInvalidFirstRect, iOS will not show the IME candidates view. | |||
const CGRect kInvalidFirstRect = {{-1, -1}, {9999, 9999}}; | |||
|
|||
// Position FlutterTextInputView outside of the screen (if scribble is disabled). |
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 scribble is enabled, do we still have the autocorrect highlight bug then?
(I posted my review without seeing @hellohuanlin's reply above) So firstRectsForRange is called one by one for each of the characters that should be highlighted, and we return the correct rects for all of those characters, but mysteriously the last character is not highlighted? |
Okay, so to make sure I'm understanding correctly: when you said:
what you actually meant is that we are returning the offset relative to the Flutter text input widget, not relative to the UITextInput? (That was my assumption based on the visual behavior, but I was confused by your assertions to the contrary.)
And you've validated that we are in fact returning the correctly sized and positioned rect for the final Also, is this specific to the very last character of the text field? I.e., if you type |
They look right to me. Note that
Yes, we return the correct rects (if UITextInput is overlapping the flutter rendered text, rather than at the origin of FlutterView). |
// top left corner of the screen (See: https://github.com/flutter/flutter/issues/131695) | ||
// and a bug where the native auto-correction suggestion menu displayed (See: | ||
// https://github.com/flutter/flutter/issues/130818). | ||
const CGRect kNonScribbleInputHiderOffScreenRect = {{0, -2000}, {0, 0}}; |
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 we need to keep the off-screen hack, please use a dynamic value that is definitively off-screen based on iterating all UIScreen
objects and uninioning their bounds.
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.
Updated to use the current UIScreen that it is on (I think current screen is good enough unless im misunderstanding)
@@ -34,6 +34,13 @@ | |||
// returns kInvalidFirstRect, iOS will not show the IME candidates view. | |||
const CGRect kInvalidFirstRect = {{-1, -1}, {9999, 9999}}; | |||
|
|||
// Position FlutterTextInputView outside of the screen (if scribble is disabled). |
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 effect does this have on IME display? Or do we do all of that via Flutter UI?
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.
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.
Ah I tested it again, and found iOS 16 and 17 have different behaviors for IME. Without the fix (main branch), iPhone shows IME on top (issue here). The current behavior is bad, but this PR will hide IME, which is even worse.
Even after putting the UIInputView to overlap with text doesn't 100% resolve the problem - the IME is still slightly off. I'd argue that it's somehow worse, because it covers the text:
Let me dig more into it, but I think we have to halt this workaround.
It sounds like what's happening could be, when the user types (btw are you using the software keyboard or the hardware keyboard?):
|
@stuartmorgan Well, I was actually thinking about In my mental model, Previously, we did not need this "invisible native text" to overlap with the Flutter drawing, so we put it anywhere (conveniently, top left corner). But later, when adding iPad scribble support, if scribble is enabled, we have to put this "invisible native text" to overlap with the Flutter drawing on screen. In either scribble or non-scribble case, the range query API returns the same offset, relative to
Yeah I have manually checked all rects. But let me try again (and post the result here in case I made terrible mistakes)
Let me try this too. |
Yes, which means the actual text only has one position on the screen: the position where Flutter draws it. I think it would help if any discussion of the position of text referred to the position of the actual text that has a position, not to the theoretical position of theoretical text that does not actually exist.
I understand that what we return needs to be the offset relative to the |
Yeah agree it was confusing. I missed a big "if" - it's correct if the UITextInput overlaps with the text drawn on screen (which is the iPad scribble case). Sorry I think i paid most attention on the |
48d36e8
to
894506e
Compare
894506e
to
a85b9bc
Compare
a85b9bc
to
3f16b72
Compare
Closing this PR in favor of #44779 |
Fix native auto-correction highlight region on top left corner
I did some github archaeology - some code changes in
firstRectForRange
are related to this PR (CC: @LongCatIsLooong). I have verified that it still works.Some investigation:
i
andw
are different, but this observation still holds. This is likely due to internal implementation of UIKit, and it's hard to guess why exactly Apple does this.The highlight region is drawn with respect to the coordinate of our FlutterTextInputView. However, it's not a subview, so likely a draw call on its CALayer.
As discussed in this PR, Apple does not provide any API to tell us when a word should be auto-corrected or not. This means under current API, it is unlikely possible to leverage the system highlight to replace the our custom highlight. We have to find other solutions. As a workaround, I put the FlutterTextInputView outside of the screen.
This happens on previous iOS as well, but just not as noticeable (much smaller, see screenshot below). So we should have this workaround for both iOS 16 and iOS 17.
List which issues are fixed by this PR. You must list at least one issue.
If you had to change anything in the flutter/tests repo, include a link to the migration guide as per the breaking change policy.
Pre-launch Checklist
///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.
Fixes flutter/flutter#131695
Fixes flutter/flutter#130818