Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Flutter iOS Interactive Keyboard: Take Screenshot and Handle Pointer Movement #43972

Merged
merged 7 commits into from
Aug 4, 2023

Conversation

Matt2D
Copy link
Contributor

@Matt2D Matt2D commented Jul 24, 2023

This PR address the movement aspect of the flutter interactive keyboard. It handles pointer movement while a scroll view widget is visible, and the interactive behavior is chosen for keyboardDismissBehavior. This is a desired behavior of the keyboard that has not yet been implemented.
Design Document:
https://docs.google.com/document/d/1-T7_0mSkXzPaWxveeypIzzzAdyo-EEuP5V84161foL4/edit?pli=1

Issues Address:
flutter/flutter#57609

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 and the C++, Objective-C, Java style guides.
  • I listed at least one issue that this PR fixes in the description above.
  • I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test-exempt. See testing the engine for instructions on writing and running engine tests.
  • I updated/added relevant documentation (doc comments with ///).
  • I signed the CLA.
  • All existing and new tests are passing.

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

@Matt2D Matt2D requested review from hellohuanlin and cbracken July 24, 2023 21:10
@Matt2D Matt2D force-pushed the screenshot_pointer_move branch from 3091c86 to fb66944 Compare July 26, 2023 21:29
@Matt2D Matt2D changed the title Screenshot pointer move Flutter iOS Interactive Keyboard: Take Screenshot and Handle Pointer Movement Jul 26, 2023
@Matt2D Matt2D marked this pull request as ready for review July 26, 2023 21:44
Copy link
Contributor

@hellohuanlin hellohuanlin left a comment

Choose a reason for hiding this comment

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

Overall looks great. I will ask @cbracken to also take a look.

@@ -164,4 +164,9 @@ FLUTTER_DARWIN_EXPORT
- (instancetype)initWithOwner:(FlutterTextInputPlugin*)textInputPlugin NS_DESIGNATED_INITIALIZER;

@end

@interface UIView (FindFirstResponder)
- (id)firstResponder;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you rename to flt_firstResponder in case of naming conflict? (ObjC doesn't support namespace)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea! Went through and made the change.

@@ -45,6 +45,7 @@
static NSString* const kSetSelectionRectsMethod = @"Scribble.setSelectionRects";
static NSString* const kStartLiveTextInputMethod = @"TextInput.startLiveTextInput";
static NSString* const kUpdateConfigMethod = @"TextInput.updateConfig";
static NSString* const kOnPointerMoveMethod = @"TextInput.onPointerMove";
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit maybe kOnInteractiveKeyboardPointerMoveMethod = @"TextInput.onPointerMoveForInteractiveKeyboard", to be more specific.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just updated, I'll make sure to try and add Interactive Keyboard more in the future, definitely would help with clarity!

@@ -2272,11 +2289,57 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
} else if ([method isEqualToString:kUpdateConfigMethod]) {
[self updateConfig:args];
result(nil);
} else if ([method isEqualToString:kOnPointerMoveMethod]) {
double pointerY = [args[@"pointerY"] doubleValue];
Copy link
Contributor

Choose a reason for hiding this comment

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

use CGRect rather than double for UI stuff.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With regards to the pointerY this only contains a single double, so I am not sure that this would classify as UI.

Copy link
Contributor

Choose a reason for hiding this comment

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

sorry i meant CGFloat

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gotcha, just updated the code!

@hellohuanlin hellohuanlin requested a review from cyanglaz July 27, 2023 17:53
@Matt2D Matt2D force-pushed the screenshot_pointer_move branch 2 times, most recently from 5a5c269 to a41e685 Compare July 27, 2023 20:26
- (void)handlePointerMove:(double)pointerY {
double screenHeight = [UIScreen mainScreen].bounds.size.height;
double keyboardHeight = _keyboardRect.size.height;
// Determines if pointer goes overtop of the keyboard
Copy link
Contributor

Choose a reason for hiding this comment

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

nits:

Suggested change
// Determines if pointer goes overtop of the keyboard
// Determines if pointer goes overtop of the keyboard.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will do! Just slipped by me.

// Determines if pointer goes overtop of the keyboard
if (screenHeight - keyboardHeight <= pointerY) {
// If no screenshot has been taken
if (_keyboardViewContainer.subviews.count == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Using _keyboardViewContainer.subviews.count == 0 doesn't seems to be safe and future proof. For example, what if someone added a new subview for _keyboardViewContainer and didn't know this logic exist? Could you use a different indicator?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Definitely a concern, the same thing occurs in the tests so there definitely is a need to update this approach. I went through and included a UIView that represents the screenshot. Then we can see if it the superview matches instead.

Comment on lines 2312 to 2314
CGRect frameRect = _keyboardRect;
frameRect.origin.y = pointerY;
_keyboardViewContainer.frame = frameRect;
Copy link
Contributor

Choose a reason for hiding this comment

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

nits: could you refactor these lines into a method to match what you did in if
Maybe something like:
`[self setKeyboardContainerHeight:pointerY]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done! Always helpful to simplify code into sections to increase understandability.

@@ -60,6 +60,8 @@ @interface FlutterSecureTextInputView : FlutterTextInputView

@interface FlutterTextInputPlugin ()
@property(nonatomic, assign) FlutterTextInputView* activeView;
@property(nonatomic, assign) UIView* keyboardViewContainer;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
@property(nonatomic, assign) UIView* keyboardViewContainer;
@property(nonatomic, weak) UIView* keyboardViewContainer;

Copy link
Contributor

Choose a reason for hiding this comment

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

@cyanglaz i could be wrong, but iirc the standard practice is just assign when exposing the property in test header (similar to the activeView properly above). weak is confusing since it's actually a strong property.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh. I didn't realize it is only exposing the property. I'm not aware of the standard practice tho but since activeView also used assign, I will take your word for it :)

Copy link
Contributor

Choose a reason for hiding this comment

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

@cyanglaz and I just discussed offline - it should be readonly here, since your unit test does not need the setter.

@property(nonatomic, readonly) UIView* keyboardViewContainer;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gotcha! That works for me, just went through and made the changes.

@Matt2D Matt2D force-pushed the screenshot_pointer_move branch from a41e685 to 3e41f37 Compare July 27, 2023 21:48
@Matt2D Matt2D requested review from cyanglaz and hellohuanlin July 27, 2023 21:59
@Matt2D Matt2D force-pushed the screenshot_pointer_move branch from 3e41f37 to 314f3db Compare July 27, 2023 22:49
} else {
result(FlutterMethodNotImplemented);
}
}

- (void)handlePointerMove:(CGFloat)pointerY {
double screenHeight = [UIScreen mainScreen].bounds.size.height;
Copy link
Contributor

Choose a reason for hiding this comment

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

We are moving away from using [UIScreen mainScreen]. I don't think we should block this PR because of this, but we will need to follow up to clean up later.
FYI @mossmana

Copy link
Contributor

Choose a reason for hiding this comment

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

Is the pointerY based on the screen, or the flutter app?

Since it comes from the Framework, I assume it is based on the flutter app? So In scenarios (add to app, or app extension) when the flutter app does not fill the full screen, the calculation below that determines if the pointer goes overtop of the keyboard should be based on the flutter app's height instead of the screen height, correct?

Maybe instead of the screenHeight, we should use the height of the FlutterViewController's view?

Copy link
Contributor

Choose a reason for hiding this comment

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

We are moving away from using [UIScreen mainScreen]. I don't think we should block this PR because of this, but we will need to follow up to clean up later. FYI @mossmana

@Matt2D While usually I would agree with not blocking the PR, in this particular case, this is a new introduction of [UIScreen mainScreen]. I would prefer that we don't add new instances. You can refer to flutter/flutter#125496 for how we are handling the deprecation in other places.

Copy link
Contributor

Choose a reason for hiding this comment

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

@cyanglaz and I were discussing this the other day, and it's actually a bit tricky here - we would need to refactor this code to make it re-usable.

I think I can take this part over. Can make a PR today.

Copy link
Member

Choose a reason for hiding this comment

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

@cyanglaz in this case, since this is being used to determine the pointer position on the screen, this is relative to the screen rather than the flutter view.

Copy link
Contributor

Choose a reason for hiding this comment

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

FYI this is the PR promised: #44303

After that gets landed, you can do something like this here:

UIScreen *screen = _viewController.flutterScreenIfViewLoaded;
if (!screen) {
  return;
}
CGFloat screenHeight = screen.bounds.size.height;
// ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for the code snippet, I think the new api is much cleaner! Updated the code using the mentioned solution.

}

- (void)takeScreenshot {
UIView* keyboardSnap = [[UIScreen mainScreen] snapshotViewAfterScreenUpdates:YES];
Copy link
Contributor

Choose a reason for hiding this comment

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

ditto about [UIScreen mainScreen]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated with the fix provided by the new PR Huan did.

Comment on lines +2344 to +2373
[UIApplication.sharedApplication.delegate.window.rootViewController.view
addSubview:_keyboardViewContainer];
Copy link
Contributor

Choose a reason for hiding this comment

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

Should it be FlutterViewController instead?

In add to app scenario, the rootViewController might not be FlutterViewController, which might be covered by FlutterViewController. So the keyboard might be shown "behind" the FlutterViewController

Copy link
Member

Choose a reason for hiding this comment

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

Since this is positioning a keyboard relative to the full screen -- regardless of whether the FlutterView is full screen, or a subview covering only part of the screen in an add-to-app scenario -- I think we want the root view controller don't we?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah yeah that's true. We would need to make sure to position the keyboard screenshot on the very top. Set ZIndex to maxInt maybe?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Definitely worth adding!

// Determines if pointer goes overtop of the keyboard.
if (screenHeight - keyboardHeight <= pointerY) {
// If no screenshot has been taken.
if (_keyboardView.superview != nil) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you mean if (_keyboardView.superview == nil)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep you are right! Thanks for catching this.

Comment on lines +2324 to +2352
CGRect frameRect = _keyboardRect;
frameRect.origin.y = pointerY;
_keyboardViewContainer.frame = frameRect;
Copy link
Contributor

Choose a reason for hiding this comment

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

minor optional nits:

The logic of this method can also be used for the scenario where the pointer is not overtop the keyboard.
The frameRect's Y can be updated to be the max of pointerY and keyboard height.

  CGRect frameRect = _keyboardRect;
  if (screenHeight - keyboardHeight <= pointerY) {
      frameRect.origin.y = pointerY;
  }
  _keyboardViewContainer.frame = frameRect;

Then in the handlePointerMove, the logic can be simplified as

if (_keyboardView.superview != nil) {
   [self setKeyboardContainerHeight:pointerY];
} else {
   [self takeScreenshot];
   [self hideKeyboardWithoutAnimation];
}

Copy link
Contributor Author

@Matt2D Matt2D Aug 3, 2023

Choose a reason for hiding this comment

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

It turns out that for the change we still need to check the height for takeScreenshot since we don't want to take the screenshot too early:

- (void)handlePointerMove:(CGFloat)pointerY {
  //View must be loaded at this point.
  UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
  double screenHeight = screen.bounds.size.height;
  double keyboardHeight = _keyboardRect.size.height;

  if (_keyboardView.superview != nil) {
    // If screenshot is present.
    [self setKeyboardContainerHeight:pointerY];
  } else {
    // If no screenshot has been taken.
    if (screenHeight - keyboardHeight <= pointerY) {
      //If pointer is above keyboard.
      [self takeKeyboardScreenshotAndDisplay];
      [self hideKeyboardWithoutAnimation];
    }
  }
}

- (void)setKeyboardContainerHeight:(CGFloat)pointerY {
  //View must be loaded at this point.
  UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
  double screenHeight = screen.bounds.size.height;
  double keyboardHeight = _keyboardRect.size.height;

  CGRect frameRect = _keyboardRect;
  if (screenHeight - keyboardHeight <= pointerY) {
      frameRect.origin.y = pointerY;
  }
  _keyboardViewContainer.frame = frameRect;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah I missed that part. So the caller part can probably be

if (_keyboardView.superview != nil) {
   [self setKeyboardContainerHeight:pointerY];
} else if (screenHeight - keyboardHeight <= pointerY) {
   [self takeScreenshot];
   [self hideKeyboardWithoutAnimation];
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Discussed offline - the duplicate logic of screenHeight - keyboardHeight <= pointerY can get error prune.

Another idea is to pass in an additional BOOL to the helper function, but that also makes the code unnecessarily complicate.

I think it makes sense to keep it as it is.

@@ -164,4 +164,9 @@ FLUTTER_DARWIN_EXPORT
- (instancetype)initWithOwner:(FlutterTextInputPlugin*)textInputPlugin NS_DESIGNATED_INITIALIZER;

@end

@interface UIView (FindFirstResponder)
- (id)flt_firstResponder;
Copy link
Contributor

Choose a reason for hiding this comment

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

optional nits: use a readonly property instead.

Copy link
Member

Choose a reason for hiding this comment

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

Avoid underscore in obj-c identifiers. We're doing something similar in another outstanding PR on the macOS side and using flutter as the prefix here:
https://github.com/flutter/engine/pull/43101/files#diff-edbbe0be10d99338c2496cad85a27776c79ec8b260825ee4d309b1e4d194d5b2R10

So we'd end up with flutterFirstResponder here.

@cyanglaz @hellohuanlin preferences? We should probably be consistent across embedders.

Copy link
Contributor

Choose a reason for hiding this comment

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

flutterFirstResponder SGTM

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will do! Thank you for the suggestion

- (void)handlePointerMove:(CGFloat)pointerY {
double screenHeight = [UIScreen mainScreen].bounds.size.height;
double keyboardHeight = _keyboardRect.size.height;
// Determines if pointer goes overtop of the keyboard.
Copy link
Member

Choose a reason for hiding this comment

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

Maybe something like:

Suggested change
// Determines if pointer goes overtop of the keyboard.
// If the pointer is within the bounds of the keyboard.

Copy link
Member

Choose a reason for hiding this comment

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

(And maybe move the comment inside the conditional block)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, thank you!

[UIView setAnimationsEnabled:YES];
}

- (void)takeScreenshot {
Copy link
Member

Choose a reason for hiding this comment

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

Consider renaming to something more specific for readability -- e.g. take KeyboardSnapshot

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Definitely, I also added add to screen to be even better for readability.

result:^(id _Nullable result){
}];
XCTAssert(textInputPlugin.keyboardView.superview != nil);
CGFloat newHeight = 600;
Copy link
Member

@cbracken cbracken Aug 2, 2023

Choose a reason for hiding this comment

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

Consider just inlining this into the call below, or move this up above and use in both the event above and the check below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Definitely!

result:^(id _Nullable result){
}];
XCTAssert(textInputPlugin.keyboardView.superview != nil);
CGFloat newHeight = 600;
Copy link
Member

Choose a reason for hiding this comment

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

Similar comment here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

}
}

- (void)testInteractiveKeyboardScreenshotWillBeMovedUpAfterUserScroll {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe rather than just "up" something like "back to original position"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea! Done

Copy link
Member

@cbracken cbracken left a comment

Choose a reason for hiding this comment

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

Overall looks great - just a few comments aside from those left by others!

auto-submit bot pushed a commit that referenced this pull request Aug 3, 2023
…able (#44303)

The existing `screenIfViewLoaded` and `windowSceneIfLoaded` functions are private helpers of `FlutterViewController` class. This PR moves the logic to a category of the `UIViewController`, so it can be reused for any `UIViewController`s. 

*List which issues are fixed by this PR. You must list at least one issue.*

#43972 (comment)

*If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
@Matt2D Matt2D force-pushed the screenshot_pointer_move branch from 314f3db to 881e7be Compare August 3, 2023 21:23
@Matt2D Matt2D requested a review from cyanglaz August 3, 2023 21:30
@Matt2D Matt2D requested a review from cbracken August 3, 2023 21:30
Copy link
Contributor

@cyanglaz cyanglaz left a comment

Choose a reason for hiding this comment

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

LGTM

@hellohuanlin hellohuanlin added the autosubmit Merge PR when tree becomes green via auto submit App label Aug 3, 2023
@auto-submit
Copy link
Contributor

auto-submit bot commented Aug 3, 2023

auto label is removed for flutter/engine, pr: 43972, due to - The status or check suite Linux mac_unopt has failed. Please fix the issues identified (or deflake) before re-applying this label.

@auto-submit auto-submit bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Aug 3, 2023
@hellohuanlin hellohuanlin added the autosubmit Merge PR when tree becomes green via auto submit App label Aug 4, 2023
@auto-submit auto-submit bot merged commit 2ab72fa into flutter:main Aug 4, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Aug 4, 2023
auto-submit bot pushed a commit to flutter/flutter that referenced this pull request Aug 4, 2023
…131950)

flutter/engine@a7bea86...c254deb

2023-08-04 skia-flutter-autoroll@skia.org Roll Skia from 85938bb68e75 to 45c0a830d805 (3 revisions) (flutter/engine#44397)
2023-08-04 55360120+Matt2D@users.noreply.github.com Flutter iOS Interactive Keyboard: Take Screenshot and Handle Pointer Movement  (flutter/engine#43972)
2023-08-04 skia-flutter-autoroll@skia.org Roll Dart SDK from b3372378e487 to a0b59bac20fc (1 revision) (flutter/engine#44393)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-engine-flutter-autoroll
Please CC jacksongardner@google.com,rmistry@google.com,zra@google.com on the revert to ensure that a human
is aware of the problem.

To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
gaaclarke pushed a commit to gaaclarke/engine that referenced this pull request Aug 30, 2023
…able (flutter#44303)

The existing `screenIfViewLoaded` and `windowSceneIfLoaded` functions are private helpers of `FlutterViewController` class. This PR moves the logic to a category of the `UIViewController`, so it can be reused for any `UIViewController`s. 

*List which issues are fixed by this PR. You must list at least one issue.*

flutter#43972 (comment)

*If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
gaaclarke pushed a commit to gaaclarke/engine that referenced this pull request Aug 30, 2023
…Movement (flutter#43972)

This PR address the movement aspect of the flutter interactive keyboard. It handles pointer movement while a scroll view widget is visible, and the interactive behavior is chosen for keyboardDismissBehavior. This is a desired behavior of the keyboard that has not yet been implemented. 
Design Document:
https://docs.google.com/document/d/1-T7_0mSkXzPaWxveeypIzzzAdyo-EEuP5V84161foL4/edit?pli=1

Issues Address:
flutter/flutter#57609

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
autosubmit Merge PR when tree becomes green via auto submit App platform-ios
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants