Skip to content

Windows trackpad pinch gesture has wrong focal point #136029

Open

Description

@FabrizioG202

Steps to reproduce

Create a gesture detector and add a callback on ScaleUpdate to inspect the focal point of the scale gesture and the number of pointer that it has.

 GestureDetector(
      onScaleUpdate: (d) {
        print("Focal Point: ${d.focalPoint} Scale: ${d.scale}, Pointer Count: ${d.pointerCount}");
      },
      child: Container(
        color: Colors.red,
        child: Center(),
      ),
   );

Then try to pinch and zoom first with a trackpad and then with a touch device.

Expected results

When Using The Touch device, the focalPoint stays correctly around the center of the gesture (give or take a take a few pixel due to human error) and the pointerCount is 2 as expected.

Actual results

When performing the same gesture on the trackpad (I am using the same device, a windows laptop with a touch screen), the focal point changes a lot (it appears to coincide with the location of one pointer) and the pointerCount is 1 despite using two fingers to perform the action.

Code sample

Code sample
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onScaleUpdate: (d) {
        print("Focal Point: ${d.focalPoint} Scale: ${d.scale}, Pointer Count: ${d.pointerCount}");
      },
      child: Container(
        color: Colors.red,
        child: const Center(),
      ),
    );
  }
}

Flutter Doctor output

Doctor output
[√] Flutter (Channel stable, 3.13.6, on Microsoft Windows [Version 10.0.22621.2283], locale en-US)
[√] Windows Version (Installed version of Windows is version 10 or higher)
[X] Android toolchain - develop for Android devices
    X cmdline-tools component is missing
      Run `path/to/sdkmanager --install "cmdline-tools;latest"`
      See https://developer.android.com/studio/command-line for more details.
[√] Chrome - develop for the web
[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.4.0)
[!] Android Studio (not installed)
[√] VS Code (version 1.83.0)
[√] Connected device (3 available)
[√] Network resources

! Doctor found issues in 2 categories.

Activity

  1. moffatman commented on Oct 5, 2023

    @moffatman
    Contributor

    Windows doesn't say how many pointers, I set it to 1. Maybe a mistake, it should be 2? Also the focal point is just passed on from Windows. Depending on your driver, it may not move at all. But I didn't hear of it moving strangely.

  2. FabrizioG202 commented on Oct 5, 2023

    @FabrizioG202
    Author

    The focal point on my machine appears to be one of the fingers, instead of somewhere in the middle of them. When I try the same gesture on other applications, like figma, the zoom seems to work correctly (as it does using touch in flutter). Is this normal behaviour?

    Basically, doing the same gesture (on the trackpad and touch screen) has two different effects due to the difference in focal point between the two.

  3. moffatman commented on Oct 5, 2023

    @moffatman
    Contributor

    I don't know what's normal here, in Windows, the driver has a lot to do with it. We don't receive the exact finger positions from Windows, it figures out the zoom and focal point for us. And to be honest, there are many drivers that do different things depending on the program, treating Google Chrome better for example. So if I can rephrase it, when you're zooming with trackpad in flutter, moving finger A will change both the scale and focalPoint, but moving finger B will just change the focal point? What's the device mdoel?

  4. FabrizioG202 commented on Oct 5, 2023

    @FabrizioG202
    Author

    I tested it with the following code:

    void main() => runApp(GestureTestApp());
    
    class GestureTestApp extends StatelessWidget {
      GestureTestApp({super.key});
    
      final ValueNotifier<Offset> focalPointDebugger = ValueNotifier<Offset>(Offset.zero);
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: GestureDetector(
            onScaleUpdate: (d) {
              focalPointDebugger.value = d.focalPoint;
            },
            child: Container(
              color: Colors.red,
              child: Stack(
                children: [
                  ValueListenableBuilder<Offset>(
                    valueListenable: focalPointDebugger,
                    builder: (context, value, child) {
                      return Positioned(
                        left: value.dx,
                        top: value.dy,
                        child: Container(
                          width: 20,
                          height: 20,
                          color: Colors.blue,
                        ),
                      );
                    },
                  ),
                ],
              ),
            ),
          ),
        );
      }
    }
    

    And I got this result:
    https://github.com/flutter/flutter/assets/54724353/b864c53c-588c-494a-9fcd-9ec921f9a428

    In the first part I am doing a pan-and-scale gesture using the touchscreen and it works as espected. While in the second part, the focalPoint moves on a straight line, which is unrelated to the position of the pointers or the movement.
    It does not coincide with the position of one finger, so I was wrong, but it seems to stay on the topLeft to bottomRight line.

    I am using a Microsoft Surface 7 pro.

  5. moffatman commented on Oct 5, 2023

    @moffatman
    Contributor

    OK, so this is a bug in my file here https://github.com/flutter/engine/blob/main/shell/platform/windows/direct_manipulation.cc within ConvertToGestureData function. transform[4] and transform[5] will change as you zoom in, even if focal point doesn't change, because they represent the top left of the viewport. Instead, the pan_x and pan_y should be calculated based on the center of viewport rectangle. I don't think it is a very big change, but I don't have any time or equipment to do this myself any time soon.

  6. changed the title [-]Scale Gesture is not recognized correctly using trackpad.[/-] [+]Windows trackpad pinch gesture has wrong focal point[/+] on Oct 5, 2023
  7. added
    in triagePresently being triaged by the triage team
    engineflutter/engine related. See also e: labels.
    platform-windowsBuilding on or for Windows specifically
    and removed
    in triagePresently being triaged by the triage team
    on Oct 6, 2023
  8. gspencergoog commented on Oct 12, 2023

    @gspencergoog
    Contributor
  9. 3 remaining items

  10. flutter-triage-bot commented on Jun 6, 2024

    @flutter-triage-bot

    The triaged-desktop label is irrelevant if there is no team-desktop label or fyi-desktop label.

  11. espresso3389 commented on May 14, 2025

    @espresso3389

    @Dusk-afk your link is dead now. Could you explain what did you do if possible?

  12. Dusk-afk commented on May 14, 2025

    @Dusk-afk

    @espresso3389 this is the commit I had linked earlier.

  13. JaffaKetchup commented on Aug 30, 2025

    @JaffaKetchup
    Contributor

    Hey @moffatman, apologies for the ping :D

    I'd love to investigate fixing this at the source in the code you mentioned, as it's a source of issue for flutter_map.

    From what I understand, ConvertToGestureData would need to accept another argument with the IDirectManipulationViewport* viewport, then calculate its center, potentially via GetViewportRect (as appears on these lines)?

    Could you expand on what you mean by...

    Instead, the pan_x and pan_y should be calculated based on the center of viewport rectangle.

    Is this just as simple as viewport.width / 2 & height / 2?

    I don't have any experience with working on the engine, or indeed with C/C++, but I do have a Windows laptop with trackpad demonstrating the issue, and would like to have a go. Any help would be hugely appriciated!

  14. moffatman commented on Aug 30, 2025

    @moffatman
    Contributor

    I think you might have to scale the rect as scale changes. But this is rather forgotten stuff now for me. Also could check the other values in the transform matrix. It might be useful.

  15. JaffaKetchup commented on Aug 31, 2025

    @JaffaKetchup
    Contributor

    Hey thanks @moffatman. Just trying to get my head around this now. Wouldn't it be necessary to know the point at which the gesture starts in screen space, not the viewport center? I'm really struggling to understand how to resolve the problem, but I understand what the problem is I think.

    The gesture starts at (0, 0) in gesture space. The viewport origin (0, 0) is at the top left. As the scale changes, the pan_x and pan_y are scaled based on that same scale, but to the wrong origin?

  16. moffatman commented on Aug 31, 2025

    @moffatman
    Contributor

    The windows direct manipulation thing does not really give you gesture values. You have to fake a viewport, see what it is after the gesture is applied, and reverse out what the gesture translations were.

  17. JaffaKetchup commented on Sep 1, 2025

    @JaffaKetchup
    Contributor

    I'm looking at Chromium's Direct Manipulation API code at https://source.chromium.org/chromium/chromium/src/+/main:content/browser/renderer_host/direct_manipulation_event_handler_win.cc.

    A comment in the code says:

    // DirectManipulation will send xy transform move to down-right which is noise
    // when pinch zoom. We should consider the gesture either Scroll or Pinch at
    // one sequence. But Pinch gesture may begin with some scroll transform since
    // DirectManipulation recognition maybe wrong at start if the user pinch with
    // slow motion. So we allow kScroll -> kPinch.

    This sounds like the problem we're experiencing. Does this suggest it's not possible to resolve this issue? It looks like Chromium also takes the route of only allowing one to occur in any one gesture:

    if (apply_scroll_begin) {
      helper_->event_target()->ApplyPanGestureScrollBegin(
          x_offset - last_x_offset_, y_offset - last_y_offset_);
    } else if (gesture_state_ == GestureState::kScroll) {
      helper_->event_target()->ApplyPanGestureScroll(x_offset - last_x_offset_,
                                                     y_offset - last_y_offset_);
    } else if (gesture_state_ == GestureState::kFling) {
      helper_->event_target()->ApplyPanGestureFling(x_offset - last_x_offset_,
                                                    y_offset - last_y_offset_);
    } else {
      helper_->event_target()->ApplyPinchZoomScale(scale / last_scale_);
    }
  18. moffatman commented on Sep 1, 2025

    @moffatman
    Contributor

    No it is possible to resolve it. we just need to calculate the center of the transformed rectangle as the focalPoint.

    They are talking about gesture disambiguation, which for flutter is done at the framework level, we need to keep all the raw data present in the engine.

  19. jacksonrl commented on Sep 28, 2025

    @jacksonrl

    Not a fix, but if you are using InteractiveViewer or similar you can just change it to use the initial focal point from onScaleStart on trackpads, which has an accurate focal point. This means you can't pan and scale with a trackpad at the same time but I don't think many apps use that feature anyway.

    Here's how

    note: This may also have to be special-cased to Windows trackpads specifically, not sure. It relies on the fact that pans are given scale = 1.0 exactly on Windows trackpads. You can get it to work with any pointerDevice but it would need more work.

    add these new members to _InteractiveViewerState:

      Offset _initialFocalPoint = Offset.zero;
      PointerDeviceKind? _currentGestureDeviceKind;
    

    add to onScaleStart:

        _currentGestureDeviceKind = details.kind;
    
        _initialFocalPoint = details.localFocalPoint;
    

    in onScaleUpdate, in case _GestureType.scale:

    final Offset zoomFocalPoint = isTrackpad
                ? _initialFocalPoint
                : details.localFocalPoint;
    
            final Offset focalPointSceneScaled = _transformer.toScene(
              zoomFocalPoint,
            );
            _transformer.value = _matrixTranslate(
              _transformer.value,
              focalPointSceneScaled - _referenceFocalPoint!,
            );
    
  20. JaffaKetchup commented on Dec 21, 2025

    @JaffaKetchup
    Contributor

    Unfortunately the time I had available to fix this was short and I didn't have enough experience to follow Callum's clues (thanks anyway :)). I'm open to share what I tried if anyone else wants to try to work on this, but it won't be helpful.

    Unfortunately any sort of workaround where the result is only being able to use pan or scale (not both) isn't the best for something like a map.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work lista: desktopRunning on desktopengineflutter/engine related. See also e: labels.platform-windowsBuilding on or for Windows specificallyteam-windowsOwned by the Windows platform teamtriaged-windowsTriaged by the Windows platform team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions