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

✨ Introduces PositionedGestureDetails #160714

Open
wants to merge 22 commits into
base: master
Choose a base branch
from

Conversation

AlexV525
Copy link
Member

@AlexV525 AlexV525 commented Dec 21, 2024

Resolves #101530

PositionedGestureDetails is a base class that implements positions for pointer events.

Pre-launch Checklist

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

@github-actions github-actions bot added framework flutter/packages/flutter repository. See also f: labels. f: scrolling Viewports, list views, slivers, etc. f: gestures flutter/packages/flutter/gestures repository. labels Dec 21, 2024
@AlexV525 AlexV525 force-pushed the feat/gesture-details-with-positions branch from a13e863 to efb7cae Compare December 23, 2024 03:11
@AlexV525
Copy link
Member Author

The request was shaped and we can discuss the idea before it gets completely ready. This is similar to the previous request #101665

cc @goderbauer @justinmc

@nate-thegrate nate-thegrate self-requested a review December 23, 2024 20:27
@nate-thegrate
Copy link
Contributor

@AlexV525 thanks for contributing! Go ahead and hit the "Ready for review" button when it's ready for review :)

@AlexV525
Copy link
Member Author

AlexV525 commented Dec 28, 2024

@AlexV525 thanks for contributing! Go ahead and hit the "Ready for review" button when it's ready for review :)

Previously @goderbauer had an opposite opinion in adding this abstraction #101665 (comment). I'd like to hear from others before we go further and add tests for it.

EDIT: Tests were added.

@AlexV525 AlexV525 marked this pull request as ready for review December 30, 2024 02:20
Copy link
Contributor

@nate-thegrate nate-thegrate left a comment

Choose a reason for hiding this comment

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

This looks fantastic! I just have a few bits of feedback, mostly relating to Diagnosticable stuff.

I'm a huge fan of this API, for reasons described in the linked issue.

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Offset>('globalPosition', globalPosition));
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
properties.add(DiagnosticsProperty<Offset>('globalPosition', globalPosition));
properties.add(DiagnosticsProperty<Offset>('globalPosition', globalPosition, defaultValue: Offset.zero));

Copy link
Member Author

@AlexV525 AlexV525 Jan 9, 2025

Choose a reason for hiding this comment

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

I think we don't need the defaultValue. The field will be ignored when the default value is applied in the reading which might cause extra drama. What do you think?

For now, DragDownDetails().toString() prints: DragDownDetails#63b8c(localPosition: Offset(0.0, 0.0)) which is a bit abnormal.

@AlexV525 AlexV525 requested a review from nate-thegrate January 7, 2025 14:19
Copy link
Contributor

@nate-thegrate nate-thegrate left a comment

Choose a reason for hiding this comment

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

Thanks for the updates!

I found a few more properties to change; after which this PR will be ready to merge IMO.

@AlexV525
Copy link
Member Author

AlexV525 commented Jan 8, 2025

I found a few more properties to change

I used to think they were not my updates, I just realized I changed them 🤣

Copy link
Contributor

@nate-thegrate nate-thegrate left a comment

Choose a reason for hiding this comment

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

Flutter_LGTM

Thanks for the contribution!

@AlexV525 AlexV525 added the autosubmit Merge PR when tree becomes green via auto submit App label Jan 8, 2025
@auto-submit auto-submit bot added this pull request to the merge queue Jan 8, 2025
@matanlurey
Copy link
Contributor

Given this comment:

Previously @goderbauer had an opposite opinion in adding this abstraction #101665 (comment). I'd like to hear from others before we go further and add tests for it.

I'd like to ask you wait for @goderbauer to review the PR before landing it. Thank you.

@@ -0,0 +1,47 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
Copy link
Contributor

Choose a reason for hiding this comment

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

Where is the design document motivating this new API?

Copy link
Contributor

Choose a reason for hiding this comment

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

Based on a Discord conversation, the ideal "design doc" for this change might just be a 1-pager that we discuss on the GitHub issue rather than in a Google Doc.

Copy link
Contributor

@nate-thegrate nate-thegrate left a comment

Choose a reason for hiding this comment

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

This morning, I was going to leave a comment along the lines of

I think it'd be nice to get some more perspectives on this PR before we land the change, but I also know that other reviewers already have a lot on their plate.

Maybe it'd be ideal to wait a week or so before merging, so that others have a chance to give their thoughts here.

but decided against it after seeing it had been added to the merge queue.


To me, this seems to be a simple change with clear benefits, but I can also see why we'd want to discuss further 👍

@@ -0,0 +1,47 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
Copy link
Contributor

Choose a reason for hiding this comment

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

Based on a Discord conversation, the ideal "design doc" for this change might just be a 1-pager that we discuss on the GitHub issue rather than in a Google Doc.

Comment on lines 10 to 11
/// Details that contain positions at which the single pointer interacts
/// with the screen.
Copy link
Member

Choose a reason for hiding this comment

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

This doc isn't very informative as it basically just blows the name of the class up into a sentence, see also https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md#avoid-useless-documentation.

See also https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md#writing-prompts-for-good-documentation for some prompts on writing better documentation.

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated.

abstract class GestureDetailsWithPositions with Diagnosticable {
/// Creates details with positions.
const GestureDetailsWithPositions({this.globalPosition = Offset.zero, Offset? localPosition})
: localPosition = localPosition ?? globalPosition;
Copy link
Member

Choose a reason for hiding this comment

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

So, localPosition defaulting to globalPosition was done for backwards-compatibility when we introduced localPosition. This is not a great choice for new GestureDetails and we shouldn't elevate this to be the default.

Copy link
Member Author

Choose a reason for hiding this comment

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

No new gesture details were added in this PR IMO, only some information was extracted as an abstraction.

Copy link
Member

Choose a reason for hiding this comment

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

Sure, but encoding this in a base class implicitly says all future gesture detail subclasses should follow this model.

Copy link
Member Author

Choose a reason for hiding this comment

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

There are still some gestures that do not provide position info, they have another mechanism (focal points) which I was planning to abstract in the future. So maybe we will have multiple gesture details base classes like GestureDetailsWithPositions, GestureDetailsWithPoints, etc.

Copy link
Contributor

Choose a reason for hiding this comment

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

I noticed that a lot of the gesture callback typedefs use details as the parameter name; I'm wondering if it'd make sense for the superclass name to also end with "details" for consistency.

As far as fallback values for compatibility, maybe there could be multiple constructors:

abstract class PositionedGestureDetails with Diagnosticable {
  const PositionedGestureDetails({
    required this.globalPosition,
    required this.localPosition,
  });

  const PositionedGestureDetails.withDefaults({
    this.globalPosition = Offset.zero,
    Offset? localPosition,
  }) : localPosition = localPosition ?? globalPosition;

  // ...
}

Copy link
Member Author

@AlexV525 AlexV525 Jan 13, 2025

Choose a reason for hiding this comment

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

I'm not a native English speaker so I asked the agent for the comparison and it seems the PositionedGestureDetails is preferred, so I guess we go for it and see if we may have more feedback.

I've also updated the base class always to require both arguments.

Copy link
Contributor

Choose a reason for hiding this comment

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

I believe as of now, localPosition is still set as an optional parameter.

abstract class PositionedGestureDetails with Diagnosticable {
  const PositionedGestureDetails({required this.globalPosition, Offset? localPosition})
    : localPosition = localPosition ?? globalPosition;

  // ...
}

If we tweaked the base class as described above, it'd be possible to require both parameters in the unnamed constructor without breaking changes.

class DragDownDetails extends PositionedGestureDetails {
  /// Super-parameters are required unless `super.withDefaults` is specified.
  const DragDownDetails({super.globalPosition, super.localPosition}) : super.withDefaults();
}

Copy link
Member Author

Choose a reason for hiding this comment

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

If we tweaked the base class as described above, it'd be possible to require both parameters in the unnamed constructor without breaking changes.

I believe we can write this:

class DragDownDetails extends PositionedGestureDetails {
  /// Creates details for a [GestureDragDownCallback].
  const DragDownDetails({super.globalPosition = Offset.zero, Offset? localPosition})
    : super(localPosition: localPosition ?? globalPosition);
}

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 true! If we don't mind a bit of extra boilerplate, a second constructor isn't necessary :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated. ☕

Comment on lines 18 to 20
/// * For *start details, interact means contacting the screen.
/// * For *update details, interact means moving on the screen.
/// * For *end details, interact means lifted from the screen.
Copy link
Member

Choose a reason for hiding this comment

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

This is hard to comprehend. I had to do a double take to understand what star-start details are.

Copy link
Member

Choose a reason for hiding this comment

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

Also, its odd that the base class is describing semantics differences of subclasses. Might be a sign that this is not a great abstraction, see https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md#refactor-the-code-when-the-documentation-would-be-incomprehensible

Copy link
Member Author

Choose a reason for hiding this comment

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

Also, its odd that the base class is describing semantics differences of subclasses.

It depends on how much info we want from this abstraction. All subclasses were used to have the same comment: "The global position at which the pointer interacts with the screen.", so we may revert to this version instead?

Comment on lines 32 to 34
/// * For *start details, interact means contacting the screen.
/// * For *update details, interact means moving on the screen.
/// * For *end details, interact means lifted from the screen.
Copy link
Member

Choose a reason for hiding this comment

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

Same

final Offset localPosition;

@override
String toString() => '${objectRuntimeType(this, 'DragDownDetails')}($globalPosition)';
Copy link
Member

@goderbauer goderbauer Jan 9, 2025

Choose a reason for hiding this comment

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

What does DragDownDetails.toString() give you now with this removed?

Copy link
Member Author

Choose a reason for hiding this comment

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

It was taken by the diagnostic builder, so the result of
DragDownDetails(globalPosition: Offset(1.0, 1.0), localPosition: Offset(2.0, 2.0)).toString()
will be
DragDownDetails#63b8c(globalPosition: Offset(1.0, 1.0), localPosition: Offset(2.0, 2.0)).

@goderbauer
Copy link
Member

For my own understanding: The motivation for this is so that instead of ...

GestureDetector(
  onPanStart: (PanStartDetails d) => foo(d.globalPosition),
  onPanUpdate: (PanUpdateDetails d) => foo(d.globalPosition),
)

void foo(Offset position) {
 ...
}

... i can write ...

GestureDetector(
  onPanStart: foo,
  onPanUpdate: foo,
)

void foo(GestureDetailsWithPosition position) {
 ...
}

Is this the motivation behind introducing this abstraction?

@AlexV525
Copy link
Member Author

AlexV525 commented Jan 9, 2025

Is this the motivation behind introducing this abstraction?

As for me, yes.

GestureDetector(
 onPanStart: (PanStartDetails d) => foo(d.globalPosition),
 onPanUpdate: (PanUpdateDetails d) => foo(d.globalPosition),
)

The above would be a workaround when someone is trying to write this (as @nate-thegrate mentioned in #101530 (comment)) :

/// Used for both [onPanStart] and [onPanUpdate].
void touchRecognition(dynamic details) {
  final Offset offset = details.localPosition;
  // ...
}

@nate-thegrate
Copy link
Contributor

@goderbauer after having a chance to read through the feedback you offered here, it's clear that removing this PR from the merge queue was the right call. I wish I had thought this over a bit more before sending out the approval two days ago.

It looks like we mostly share the same understanding for the motivation behind this abstraction. I like to follow the avoid abbreviations guideline in my own projects; going from (PanStartDetails details) => foo(details.globalPosition) to foo would be really nice.

It's also easy to imagine other examples beyond my specific use case that become much more complex without the common interface:

void handleGesture(GestureDetailsWithPosition details) {
  // some logic

  if (details is PanStartDetails) {
    // handle pan start
  }

  // some more logic
}

extension on GestureDetailsWithPosition {
  Offset get shiftAmount => globalPosition - localPosition;
}

I feel strongly that providing a common interface for similar APIs can be beneficial in many cases, including this one. (Though to be fair, I personally feel that this common interface wouldn't be quite as valuable as some sort of "disposable" API that could unify ChangeNotifier and AnimationController).

It might also be worth mentioning that if the impact of class inheritance on compile size influences decision-making in the Dart SDK, perhaps it should affect the decision here too.

I don't think closing this PR would be the end of the world, but I do feel that it's a nice improvement.

@AlexV525 AlexV525 changed the title ✨ Introduces GestureDetailsWithPositions ✨ Introduces PositionedGestureDetails Jan 13, 2025
@AlexV525
Copy link
Member Author

AlexV525 commented Jan 13, 2025

To summarize the above changes:

  1. GestureDetailsWithPositions was renamed to PositionedGestureDetails.
  2. globalPosition and localPosition are now both required parameters when constructing* the base class.
  3. The description of the class has also been enriched
  4. The requirement of arguments has also been reset to the correct state same as previous.

/// void handleGesture(PositionedGestureDetails details) {
/// final Offset globalPosition = details.globalPosition;
///
/// if (details is TapDownDetails) {
Copy link
Member

Choose a reason for hiding this comment

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

This is exactly the code we don't want people to write, see also https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md#summary:

Avoid is [...].

This is part of my reservations against this pattern. IMO, the better way to handle this is to factor out the common business logic in a method and define separate handler methods that then call the common code where needed.

Copy link
Member Author

@AlexV525 AlexV525 Jan 17, 2025

Choose a reason for hiding this comment

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

This is exactly the code we don't want people to write, see also https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md#summary:

Avoid is [...].

Well, this is the style guide for the Flutter repo, but the example is for all users regardless Flutter repo themselves. Also, the style guide does not clarify why the pattern should be avoided. Unfortunately, our repo also contains is in multiple places, those are also reasonably written.

This is part of my reservations against this pattern. IMO, the better way to handle this is to factor out the common business logic in a method and define separate handler methods that then call the common code where needed.

My understanding is people write code as they wish, the quality could be defeated by unsound declarations.

If I'm writing a gesture helper package to handle these gestures, I would definitely go for some solid type definition such as:

Offset handleYourPositionedGestures(PositionedGestureDetails details) {
  final Offset transformedBySomeMathematics = calculate(details.globalPosition, details.localPosition);
  return transformedBySomeMathematics;
}

Rather than:

Offset handleYourPositionedGestures(Offset globalPosition, Offset localPosition) {
  final Offset transformedBySomeMathematics = calculate(globalPosition, localPosition);
  return transformedBySomeMathematics;
}

The latter lost consistency and users could pass whatever positions even when the parameter suggested specified, while the previous one declared that only positions in a single gesture could be handled.

Of course, there would a multiple use cases that argue how a common business logic should be written, but providing a good common interface would also be helpful in some way IMO (as @nate-thegrate also said in #160714 (comment)).

P.S. The example here could be updated to a proper one to avoid is, totally agree. Maybe the one I just wrote stands.

Copy link
Member Author

Choose a reason for hiding this comment

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

The example has been updated.

@nate-thegrate nate-thegrate mentioned this pull request Jan 28, 2025
@AlexV525 AlexV525 requested a review from goderbauer January 29, 2025 09:49
@johnmccutchan johnmccutchan removed their request for review January 30, 2025 05:18
@AlexV525 AlexV525 dismissed johnmccutchan’s stale review January 31, 2025 00:54

The reviewer has dismissed review from the request.

@AlexV525
Copy link
Member Author

Friendly ping @goderbauer

@yakagami
Copy link
Contributor

Since I started working with gestures again, I came back to this issue. looking at it again, are we sure this is a good abstraction? To me, it seems to be rather leaky, as it does not support all gesture types (scale gesture) and within the gesture types that it does support, it does not encode all or even most possible fields. For example DragUpdateDetails is missing sourceTimeStamp, delta and primaryDelta and DragEndDetails is missing velocity and primaryVelocity. I think an abstraction that only covers a fraction of what it is abstracting (even if it is a large fraction) is often worse than no abstraction.

To use the above example:

void handleGesture(GestureDetailsWithPosition details) {
  // some logic

  if (details is PanStartDetails) {
    // handle pan start
  }

  // some more logic
}

What would I do if I had to start supporting scale gestures here? we can't actually abstract handling gestures in general with this class because it doesn't support all gestures.

If the goal is type safety, how about creating a type for globalPosition and localPosition specifically? The cleanest way to do this would be a breaking change like this:

class ScreenPosition {

  final Offset localPosition;
  final Offset globalPosition;

  ScreenPosition({required this.localPosition, required this.globalPosition});

}

The gestures details would then all have a ScreenPosition:

DragUpdateDetails({
    required ScreenPosition screenPosition,
    this.sourceTimeStamp,
    this.delta = Offset.zero,
    this.primaryDelta,
  })

Then you could pass this into your functions like so:

```dart
Offset handleYourPositionedGestures(ScreenPosition details) {
  final Offset transformedBySomeMathematics = calculate(details.globalPosition, details.localPosition);
  return transformedBySomeMathematics;
}

That seems to be the correct way to do this, although I personally don't see the benefits. It seems to me like a classic example of "is a" vs. "has a." Gesture details have global and local positions, but they are not themselves special cases of those positions.

@AlexV525
Copy link
Member Author

@yakagami Welcome back!

My idea to support more gestures is to create more pieces of abstractions as I wrote here #160714 (comment). The new PositionedGestureDetails is meant to only include positioning details.

I don't think the "handleGesture" example is a good one, so I brought another in #160714 (comment). Also, here is how I wrote to extract positions from common gesture details in the real world, and IMO it's inefficient to write so.

And I also think that gestures are in a relatively fixed group given to how humans interact with screens, so the framework won't always introduce new gestures.

A breaking change to provide ScreenPosition doesn't seem to resolve "support all gesture types" and "encode possible fields", back to the origin, our goal was only to provide a common link with related gestures.

@justinmc justinmc requested review from dkwingsmt and removed request for goderbauer March 18, 2025 22:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
f: gestures flutter/packages/flutter/gestures repository. f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Generalize gesture details that concerns cartesian positioning in a common interface
6 participants