Skip to content

Fix Tooltip child state loss when cursor enters/leaves window#1303

Merged
bdlukaa merged 2 commits intomasterfrom
copilot/fix-tooltip-widget-replace-issue
Mar 6, 2026
Merged

Fix Tooltip child state loss when cursor enters/leaves window#1303
bdlukaa merged 2 commits intomasterfrom
copilot/fix-tooltip-widget-replace-issue

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 3, 2026

Tooltip rebuilds its child with a new State whenever the mouse cursor enters or leaves the window. Root cause: _handleMouseTrackerChange calls setState on _mouseIsConnected, which conditionally adds/removes a MouseRegion wrapper in build(), changing the widget tree depth and destroying child state.

Same class of bug fixed in Flutter material: flutter/flutter#119199

Changes

  • Remove _handleMouseTrackerChange listener and its registration/teardown — eliminates the setState that triggered tree restructuring
  • Replace _mouseIsConnected state field with a getter reading directly from RendererBinding.instance.mouseTracker.mouseIsConnected
  • Always include MouseRegion in the widget tree — tree structure stays stable regardless of mouse connection state, preserving child state
  • Add regression test verifying child initState is called exactly once across mouse pointer add/remove cycles
// Before: conditional wrapping changes tree depth → child state lost
if (_mouseIsConnected) {
  result = MouseRegion(..., child: result);
}

// After: always present → stable tree → child state preserved
result = MouseRegion(..., child: GestureDetector(..., child: result));

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://storage.googleapis.com/
    • Triggering command: /usr/bin/curl curl -v REDACTED bash _eng�� b-cache/_temp/dirPIYQRU/bin/shell_completion_generator.dart (http block)
  • https://storage.googleapis.com/flutter_infra_release/flutter/109150893958777c8f2215f6cfd3e89e984e8dea/dart-sdk-linux-x64.zip
    • Triggering command: /usr/bin/curl curl --retry 3 --continue-at - --location --output /tmp/flutter/bin/cache/dart-sdk-linux-x64.zip REDACTED (http block)
    • Triggering command: /usr/bin/curl curl -sI REDACTED (http block)
  • https://storage.googleapis.com/flutter_infra_release/flutter/109150893958777c8f2215f6cfd3e89e984e8dea/linux-x64/font-subset.zip
    • Triggering command: /tmp/flutter/bin/cache/dart-sdk/bin/dart /tmp/flutter/bin/cache/dart-sdk/bin/dart --packages=/tmp/flutter/packages/flutter_tools/.dart_tool/package_config.json /tmp/flutter/bin/cache/flutter_tools.snapshot test test/tooltip_test.dart --no-pub --local /home/REDACTED/.local/bin/git user.name (http block)
  • https://storage.googleapis.com/flutter_infra_release/flutter/109150893958777c8f2215f6cfd3e89e984e8dea/sky_engine.zip
    • Triggering command: /tmp/flutter/bin/cache/dart-sdk/bin/dart /tmp/flutter/bin/cache/dart-sdk/bin/dart --packages=/tmp/flutter/packages/flutter_tools/.dart_tool/package_config.json /tmp/flutter/bin/cache/flutter_tools.snapshot test test/tooltip_test.dart --no-pub --global or_parser credential.usernbash (http block)
  • https://storage.googleapis.com/flutter_infra_release/flutter/18818009497c581ede5d8a3b8b833b81d00cebb7/dart-sdk-linux-x64.zip
    • Triggering command: /usr/bin/curl curl --retry 3 --continue-at - --location --output /tmp/flutter/bin/cache/dart-sdk-linux-x64.zip REDACTED (http block)
  • https://storage.googleapis.com/flutter_infra_release/flutter/327ed814500baca0480a02f8555aa84a9afe32bf/dart-sdk-linux-x64.zip
    • Triggering command: /usr/bin/curl curl --retry 3 --continue-at - --location --output /tmp/flutter/bin/cache/dart-sdk-linux-x64.zip REDACTED (http block)
  • https://storage.googleapis.com/flutter_infra_release/flutter/fonts/3012db47f3130e62f7cc0beabff968a33cbec8d8/fonts.zip
    • Triggering command: /tmp/flutter/bin/cache/dart-sdk/bin/dart /tmp/flutter/bin/cache/dart-sdk/bin/dart --packages=/tmp/flutter/packages/flutter_tools/.dart_tool/package_config.json /tmp/flutter/bin/cache/flutter_tools.snapshot pub get (http block)
    • Triggering command: /tmp/flutter/bin/cache/dart-sdk/bin/dart /tmp/flutter/bin/cache/dart-sdk/bin/dart --packages=/tmp/flutter/packages/flutter_tools/.dart_tool/package_config.json /tmp/flutter/bin/cache/flutter_tools.snapshot test test/tooltip_test.dart (http block)
    • Triggering command: /tmp/flutter/bin/cache/dart-sdk/bin/dart /tmp/flutter/bin/cache/dart-sdk/bin/dart --packages=/tmp/flutter/packages/flutter_tools/.dart_tool/package_config.json /tmp/flutter/bin/cache/flutter_tools.snapshot test test/tooltip_test.dart --no-pub (http block)
  • https://storage.googleapis.com/flutter_infra_release/gradle-wrapper/fd5c1f2c013565a3bea56ada6df9d2b8e96d56aa/gradle-wrapper.tgz
    • Triggering command: /tmp/flutter/bin/cache/dart-sdk/bin/dart /tmp/flutter/bin/cache/dart-sdk/bin/dart --packages=/tmp/flutter/packages/flutter_tools/.dart_tool/package_config.json /tmp/flutter/bin/cache/flutter_tools.snapshot test test/tooltip_test.dart --no-pub (http block)
    • Triggering command: /tmp/flutter/bin/cache/dart-sdk/bin/dart /tmp/flutter/bin/cache/dart-sdk/bin/dart --packages=/tmp/flutter/packages/flutter_tools/.dart_tool/package_config.json /tmp/flutter/bin/cache/flutter_tools.snapshot test test/tooltip_test.dart --no-pub credential.username /usr/local/bin/git (http block)
  • https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json
    • Triggering command: /tmp/flutter/bin/cache/dart-sdk/bin/dart /tmp/flutter/bin/cache/dart-sdk/bin/dart pub --suppress-analytics get --example --directory . (http block)
  • https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.32.0-stable.tar.xz
    • Triggering command: /usr/bin/curl curl -fsSL REDACTED -o /tmp/flutter.tar.xz (http block)
  • storage.flutter-io.cn
    • Triggering command: /usr/bin/curl curl -sI REDACTED (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

This section details on the original issue you should resolve

<issue_title>🐛 Tooltip widget replaces child when cursor enter/leaves window</issue_title>
<issue_description>This seems to have been an issue in flutter material too, fixed here:

When the mouse leaves/enters the window, the child of Tooltip is created with an entirely new state. Repro:

import 'package:fluent_ui/fluent_ui.dart';

void main() {
  runApp(FluentApp(
    home: ColoredBox(
      color: Colors.white,
      child: Center(
        child: Tooltip(
          message: 'if this string is empty, issue does not occur',
          child: StatefulText(),
        ),
      ),
    ),
  ));
}

class StatefulText extends StatefulWidget {
  const StatefulText({super.key});

  @override
  State<StatefulText> createState() => _StatefulTextState();
}

class _StatefulTextState extends State<StatefulText> {
  static int count = 1;

  @override
  void initState() {
    super.initState();
    count++;
  }

  @override
  Widget build(BuildContext context) {
    return Text(
      'this initState ran $count times (on window cursor enter/exit)',
    );
  }
}
```</issue_description>

<agent_instructions>Check the fix made in the material library: https://github.com/flutter/flutter/pull/119199 and apply a similar fix in this project.

Additionally, make feature-parity with the Flutter material built in tooltip, while maintaining the extra functionality of this fluent_ui package.

Make sure performance is maintained.</agent_instructions>

## Comments on the Issue (you are @copilot in this section)

<comments>
</comments>

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Remove the _handleMouseTrackerChange listener that caused setState to be
called when a mouse device connects/disconnects. This setState caused the
build method to conditionally add/remove a MouseRegion wrapper, which
changed the widget tree depth and caused the child widget to lose its state.

Now the MouseRegion is always included in the widget tree (matching the
fix in flutter/flutter#119199), and _mouseIsConnected is read directly
from RendererBinding as a getter instead of being cached as state.

Co-authored-by: bdlukaa <45696119+bdlukaa@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix tooltip widget replacing child on cursor enter/leave Fix Tooltip child state loss when cursor enters/leaves window Mar 3, 2026
@bdlukaa bdlukaa marked this pull request as ready for review March 3, 2026 15:58
@bdlukaa
Copy link
Copy Markdown
Owner

bdlukaa commented Mar 5, 2026

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request addresses a bug causing state loss in a Tooltip's child widget when the mouse enters or leaves the window. The changes remove the stateful tracking of mouse connection and instead make the MouseRegion a permanent part of the widget tree, which stabilizes the tree structure and preserves the child's state. This is achieved by removing the _mouseIsConnected state field and its update logic, replacing it with a getter, and refactoring the build method to always include the MouseRegion. A regression test is also added to verify that the child's initState is called only once during mouse connection changes.

@bdlukaa bdlukaa merged commit 55d2cc0 into master Mar 6, 2026
1 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐛 Tooltip widget replaces child when cursor enter/leaves window

2 participants