Skip to content

Conversation

@LongCatIsLooong
Copy link
Contributor

@LongCatIsLooong LongCatIsLooong commented Dec 22, 2020

Description

This PR introduces 2 new fields forgroundPainter and painter to RenderEditable, and moves caret/text highlight painting logic into these painters, without introducing actual behavior changes (hopefully).
https://docs.google.com/document/d/10YiBZ_5oj6erdbgPSgpvEzNZUa7nEMYaa0X2Tw1GAWo/edit#

Tests

I added the following tests:

A few tests for custom painters.

Checklist

Before you create this PR, confirm that it meets all requirements listed below by checking the relevant checkboxes ([x]). This will ensure a smooth and quick review process.

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I signed the CLA.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I updated/added relevant documentation (doc comments with ///).
  • All existing and new tests are passing.
  • The analyzer (flutter analyze --flutter-repo) does not report any problems on my PR.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Did any tests fail when you ran them? Please read Handling breaking changes.

  • No, no existing tests failed, so this is not a breaking change.

@flutter-dashboard flutter-dashboard bot added f: cupertino flutter/packages/flutter/cupertino repository f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. c: contributor-productivity Team-specific productivity, code health, technical debt. labels Dec 22, 2020
@google-cla google-cla bot added the cla: yes label Dec 22, 2020
@@ -0,0 +1,12 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This doesn't seem to be referenced anywhere not sure if I should keep it (same for fullscreen_textfield_perf__e2e_summary.dart)

Copy link
Contributor

Choose a reason for hiding this comment

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

No opinion from me, I haven't done much with these devicelab perf tests before 🤷

_floatingCursorAddedMargin = value;
markNeedsPaint();
}
EdgeInsets floatingCursorAddedMargin;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is currently only used in setFloatingCursor which is triggered by platform channel messages so triggering a repaint does not seem necessary.

Copy link
Contributor

Choose a reason for hiding this comment

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

What if a user has implemented their own text field using RenderEditable and they directly set floatingCursorAddedMargin for some reason?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently the floating cursor implementation does not handle some edge cases properly, changing floatingCursorAddedMargin is one of them. The floating cursor doesn't reposition itself when the text input resizes or moves. The visual update only happens when the user moves the touch point (and we get a text channel message).

return;
_selection = value;
_selectionRects = null;
_selectionPainter.highlightedRange = value;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

technically this is not needed, since the selection painter will repaint with the RenderEditable.

canvas.drawRect(box.toRect().shift(effectiveOffset), _promptRectPaint);
_floatingCursorOn = state != FloatingCursorDragState.End;
_resetFloatingCursorAnimationValue = resetLerpValue;
if (_floatingCursorOn) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This part is different. The caret calculation was originally partially deferred to paint and is now performed in setFloatingCursor.

/// * [RenderEditable.setPainter], which takes a [RenderEditablePainter]
/// and sets it as the foreground painter of the [RenderEditable].
/// * [CustomPainter] a similar class which paints within a [RenderCustomPaint].
abstract class RenderEditablePainter extends ChangeNotifier {
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 painter doesn't have hitTest or semanticsBuilder because these currently don't seem to have a use case.

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 RenderEditable property was removed because you can use the same painter instance in different render objects simultaneously when the tree finalizes (for example in a Hero widget we'd keep 2 copies of the subtree so in case there's a painter in the subtree it will be attached to 2 different RenderEditables).


CaretChangedHandler caretPaintCallback;

bool showRegularCaret = false;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does not trigger a repaint because this is floating cursor stuff. Might need a bit of audit when we move the caret painting stuff to a different place.

bool showRegularCaret = false;

final Paint caretPaint = Paint();
late final Paint floatingCursorPaint = Paint();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Uses a different painter because of a gotcha in TestRecordingCanvas.

Copy link
Contributor

Choose a reason for hiding this comment

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

How is this late but it still has an initial value? I think I just have never encountered this NNBD situation.

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 this does lazy initialization. The instance variable won't be initialized until its first usage.

assert(renderEditable != null);
final TextSelection? selection = renderEditable.selection;

if (selection == null || !selection.isCollapsed /* || !selection.isValid*/)
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 thought we shouldn't paint the caret when selection is (-1, -1)?

Copy link
Contributor

Choose a reason for hiding this comment

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

I remember this came up awhile back in flutter/engine#19785, was there somewhere else that we were talking about this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not that I remember. Is the expected behavior of (-1, -1) documented anywhere? I couldn't seem to find anything about it.

The TextEditingValue constructor uses (-1, -1) as the default selection range for years. https://github.com/flutter/flutter/blame/d35a9db6fe439b1efaddffb19cea540b99917d16/packages/flutter/lib/src/services/text_input.dart#L82-L90

Copy link
Contributor

@justinmc justinmc Dec 29, 2020

Choose a reason for hiding this comment

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

No, I haven't seen any good docs about it. I think it makes sense to me uncomment that clause and make it not paint when the selection is (-1,-1). Is that not what was happening before this PR?

expect(lastCharEndpoint.length, 1);
// The last character is now on screen.
expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(786.73, epsilon: 0.01));
expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(786.73, epsilon: 2));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure why but I had to change the epsilon here so these tests pass.

Copy link
Contributor

Choose a reason for hiding this comment

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

Weird, nothing jumps out at me as a cause. It is a big epsilon, but if no golden tests are failing then maybe it's not a concern...

Copy link
Contributor Author

@LongCatIsLooong LongCatIsLooong Dec 28, 2020

Choose a reason for hiding this comment

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

I reverted the epsilon changes. Tests are passing now.

Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

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

LGTM 👍

I re-read your design doc and refreshed myself on the migration plan at the end. So it seems that the plan is to move the RenderEditablePainters into their own files and then pass them into RenderEditable via the backgroundPainter and foregroundPainter params. I'm on board with that, especially that it will simplify rendering/editable.dart, which is overly long and complicated right now.

Also, this doesn't seem to interfere with anything from my text editing refactor plan. If should actually make it easier to pare down RenderEditable even further.

'👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦'
'👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦'
'👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦'
'👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦'
Copy link
Contributor

Choose a reason for hiding this comment

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

Now we've got some extended grapheme clusters haha.


/// Computes the offset to apply to the given [sourceOffset] so it perfectly
/// snaps to physical pixels.
Offset _snapToPhysicalPixel(Offset sourceOffset) {
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 both of those, you're right 👍

|| oldDelegate.highlightColor != highlightColor
|| oldDelegate.highlightedRange != highlightedRange
|| oldDelegate.selectionHeightStyle != selectionHeightStyle
|| oldDelegate.selectionWidthStyle != selectionWidthStyle;
Copy link
Contributor

Choose a reason for hiding this comment

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

Ok that sounds good to me, it should always get repainted when needed.

@LongCatIsLooong LongCatIsLooong marked this pull request as ready for review January 6, 2021 00:18
Comment on lines +225 to +226
RenderEditablePainter? painter,
RenderEditablePainter? foregroundPainter,
Copy link
Member

Choose a reason for hiding this comment

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

When we do that, would we also remove/deprecate some of the existing arguments to RenderEditable?

/// The new [RenderEditablePainter] will replace the previously specified
/// foreground painter, and schedule a repaint if the new painter's
/// `shouldRepaint` method returns true.
void setForegroundPainter(RenderEditablePainter? newPainter) {
Copy link
Member

Choose a reason for hiding this comment

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

Why not a regular getter/setter?

/// The new [RenderEditablePainter] will replace the previously specified
/// painter, and schedule a repaint if the new painter's `shouldRepaint`
/// method returns true.
void setPainter(RenderEditablePainter? newPainter) {
Copy link
Member

Choose a reason for hiding this comment

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

same

/// to repaint without triggering a repaint on the entire [RenderEditable] stack
/// when only auxiliary content changes (e.g. a blinking cursor) are present. It
/// will be scheduled to repaint when:
/// * It's assigned to a new [RenderEditable] and the [shouldRepaint] method
Copy link
Member

Choose a reason for hiding this comment

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

nit: space before this line

Copy link
Contributor Author

Choose a reason for hiding this comment

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

reindented to 1 space and added an empty line between the bullet points(I think you meant the latter?)

/// painter's attributes change.
///
/// See also:
/// * [RenderEditable.setForegroundPainter], which takes a [RenderEditablePainter]
Copy link
Member

Choose a reason for hiding this comment

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

nit: space before this line

if (promptRectColor != null)
_promptRectPaint.color = promptRectColor;

_selectionPainter.highlightColor = selectionColor;
Copy link
Member

@goderbauer goderbauer Jan 12, 2021

Choose a reason for hiding this comment

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

For my understanding, the plan is to move all these painters out of this class? Or just some of them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ideally, all of them.

final _TextHighlightPainter _selectionPainter = _TextHighlightPainter();
final _TextHighlightPainter _autocorrectHighlightPainter = _TextHighlightPainter();

_CompositeRenderEditablePainter get _builtInForegounrdPainters => _cachedBuiltInForegroundPainters ??= _createBuiltInForegroundPainters();
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
_CompositeRenderEditablePainter get _builtInForegounrdPainters => _cachedBuiltInForegroundPainters ??= _createBuiltInForegroundPainters();
_CompositeRenderEditablePainter get _builtInForegroundPainters => _cachedBuiltInForegroundPainters ??= _createBuiltInForegroundPainters();

final _TextHighlightPainter _selectionPainter = _TextHighlightPainter();
final _TextHighlightPainter _autocorrectHighlightPainter = _TextHighlightPainter();

_CompositeRenderEditablePainter get _builtInForegounrdPainters => _cachedBuiltInForegroundPainters ??= _createBuiltInForegroundPainters();
Copy link
Member

Choose a reason for hiding this comment

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

Also, you should be able to just defines this as:

late final _CompositeRenderEditablePainter _builtInForegounrdPainters = _createBuiltInForegroundPainters();

With late it will only get instantiated on first access.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, never mind. I see below you need the option to clear this.

Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

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

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c: contributor-productivity Team-specific productivity, code health, technical debt. f: cupertino flutter/packages/flutter/cupertino repository f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants