Skip to content

Implement multi-window support for RawTooltip#184401

Draft
mattkae wants to merge 10 commits into
flutter:masterfrom
canonical:raw-tooltip-multi-window
Draft

Implement multi-window support for RawTooltip#184401
mattkae wants to merge 10 commits into
flutter:masterfrom
canonical:raw-tooltip-multi-window

Conversation

@mattkae
Copy link
Copy Markdown
Contributor

@mattkae mattkae commented Mar 31, 2026

What's new?

  • Updated RawTooltip to show a true window in a tooltip when one is available
  • Added preferBelow and verticalOffset as parameters to RawTooltip
  • Added the _TooltipShowController class so that the tooltip can decide between showing itself in an overlay vs a true window
  • Added a tooltip.4.dart example that uses runWidget with a RegularWindowController so that the tooltip example can have a parent

How to test

Run:

flutter config --enable-windowing
cd examples/api
flutter run lib/material/tooltip/tooltip.4.dart

The downside of this example is that the implicit view window opens (because the runner opens it).

Pre-launch Checklist

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

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

@github-actions github-actions Bot added framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos labels Mar 31, 2026
@mattkae mattkae added the CICD Run CI/CD label Mar 31, 2026
Comment thread packages/flutter/lib/src/widgets/raw_tooltip.dart
Comment thread packages/flutter/lib/src/widgets/raw_tooltip.dart Outdated
Comment thread packages/flutter/lib/src/widgets/raw_tooltip.dart
Comment thread packages/flutter/lib/src/widgets/raw_tooltip.dart
Copy link
Copy Markdown
Contributor

@robert-ancell robert-ancell left a comment

Choose a reason for hiding this comment

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

Some code style and documentation suggestions but otherwise looks good.

@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 1, 2026
@mattkae mattkae added the CICD Run CI/CD label Apr 1, 2026
@flutter-dashboard
Copy link
Copy Markdown

This pull request is not mergeable in its current state, likely because of a merge conflict. Pre-submit CI jobs were not triggered. Pushing a new commit to this branch that resolves the issue will result in pre-submit jobs being scheduled.

@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 1, 2026
@mattkae mattkae added the CICD Run CI/CD label Apr 1, 2026
@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 1, 2026
@mattkae mattkae added the CICD Run CI/CD label Apr 1, 2026
@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 1, 2026
@mattkae mattkae added the CICD Run CI/CD label Apr 1, 2026
@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 1, 2026
@mattkae mattkae requested a review from robert-ancell April 1, 2026 17:40
@mattkae mattkae marked this pull request as ready for review April 1, 2026 17:40
@mattkae mattkae added the CICD Run CI/CD label Apr 1, 2026
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 introduces multi-window tooltip support by abstracting display logic into a controller interface, allowing tooltips to render in overlays or separate windows. It adds preferBelow and verticalOffset properties to RawTooltip and Tooltip for improved positioning. A review comment suggests adding a mounted check before calling setState in a callback to avoid potential runtime errors.

parent: windowController,
anchorRectGetter: _getAnchorRect,
positionerBuilder: _buildWindowPositioner,
onChanged: () => setState(() {}),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Calling setState in an asynchronous callback like onChanged should be guarded by a mounted check to avoid errors if the widget is removed from the tree before the callback is executed.

          onChanged: () { if (mounted) { setState(() {}); } },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe not necessary because _showController is disposed in dispose?

Comment thread packages/flutter/lib/src/material/tooltip.dart Outdated
@victorsanni victorsanni self-requested a review April 1, 2026 21:21
Copy link
Copy Markdown
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.

This is super exciting. The approach looks good to me. It avoids the post-decoupling problem of importing windowing stuff into Material that we found in #181861 thanks to the raw widget architecture.

CC @victorsanni as the author of RawTooltip

Comment thread packages/flutter/lib/src/material/tooltip.dart Outdated
Comment thread packages/flutter/lib/src/widgets/raw_tooltip.dart Outdated
parent: windowController,
anchorRectGetter: _getAnchorRect,
positionerBuilder: _buildWindowPositioner,
onChanged: () => setState(() {}),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe not necessary because _showController is disposed in dispose?

Comment thread packages/flutter/lib/src/widgets/raw_tooltip.dart
Comment thread packages/flutter/lib/src/widgets/raw_tooltip.dart Outdated
Comment thread packages/flutter/lib/src/widgets/raw_tooltip.dart
Comment thread packages/flutter/test/widgets/raw_tooltip_test.dart
Comment thread packages/flutter/test/widgets/raw_tooltip_test.dart Outdated
@github-actions github-actions Bot removed d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos CICD Run CI/CD labels Apr 2, 2026
@mattkae mattkae requested review from justinmc and loic-sharma April 2, 2026 20:18
/// direction, the tooltip will be displayed in the opposite direction.
///
/// Defaults to true.
/// {@endtemplate}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

FYI, it looks like nothing is using this template currently

void dispose();
}

/// Shows the tooltip using an [OverlayPortalController] (the non-windowing path).
Copy link
Copy Markdown
Member

@loic-sharma loic-sharma Apr 2, 2026

Choose a reason for hiding this comment

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

"the non-windowing path" might not be obvious to a reader, consider clarifying this means the tooltip is within the current window:

Suggested change
/// Shows the tooltip using an [OverlayPortalController] (the non-windowing path).
/// Shows the tooltip within the current window using an [OverlayPortalController].

Comment on lines +610 to +611
/// Shows the tooltip using a [TooltipWindowController] and [WindowRegistry]
/// (the windowing path).
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Likewise "the windowing path" might not be obvious to a reader, consider explaining this creates a child window:

Suggested change
/// Shows the tooltip using a [TooltipWindowController] and [WindowRegistry]
/// (the windowing path).
/// Shows the tooltip in a child window using a [TooltipWindowController]
/// and the [WindowRegistry].

@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_showControllerInitialized) {
Copy link
Copy Markdown
Member

@loic-sharma loic-sharma Apr 2, 2026

Choose a reason for hiding this comment

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

I believe if a Tooltip has a GlobalKey, it can be reparented from one window to another. I suspect we need to handle this case by recreating the show controller.

Comment on lines +1083 to +1099
switch (_showController) {
case final _OverlayTooltipShowController overlayTooltipShowController:
assert(debugCheckHasOverlay(context));
return OverlayPortal.overlayChildLayoutBuilder(
controller: overlayTooltipShowController.overlayController,
overlayChildBuilder: _buildTooltipOverlay,
child: result,
);
case final _WindowTooltipShowController windowTooltipShowController:
return ViewAnchor(
view: TooltipWindow(
controller: windowTooltipShowController.windowController!,
child: widget.tooltipBuilder(context, _overlayAnimation),
),
child: result,
);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would it make sense to add a build method to _TooltipShowController? This would likely require being able to to move _buildTooltipOverlay down to _OverlayTooltipShowController. I don't feel strongly about this, just a thought in case it makes the code nicer.

Copy link
Copy Markdown
Member

@loic-sharma loic-sharma left a comment

Choose a reason for hiding this comment

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

Looks good! Love the ASCII art in tests :)

Consider getting a review from @victorsanni as well as they are our RawTooltip expert :)

Copy link
Copy Markdown
Contributor

@victorsanni victorsanni left a comment

Choose a reason for hiding this comment

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

Why are we exposing preferBelow and verticalOffset? @chunhtai, @dkwingsmt and I had detailed conversations about why we shouldn't:

TLDR we left them out of RawTooltip because they are Material-opinionated properties, and there are ways to work around them (i.e RawTooltip.positionDelegate). We also have logic that resolves them in Tooltip so that they don't have to be passed down to RawTooltip (see Tooltip._getDefaultPositionDelegate).

/// If there is insufficient space to display the tooltip in the preferred
/// direction, the tooltip will be displayed in the opposite direction.
///
/// Defaults to true.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we keep the default outside the template so it can be reused in Tooltip.preferBelow?

/// will position themselves above or below their corresponding widgets
/// depending on the value of [preferBelow]
///
/// Defaults to 0.0.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same here, Tooltip.verticalOffset defaults to the value set in the theme. If that is not provided, it defaults to 24.0. Removing the default from the template allows it to be reused in Tooltip.verticalOffset where defaults can be explained.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also why default to 0.0 instead of 24.0 as in Tooltip? 0.0 might cause visual overlap with the child. Tooltip uses 24.0 as a buffer for example.

/// When [preferBelow] is set to true and tooltips have sufficient space to
/// display themselves, this property defines how much vertical space tooltips
/// will position themselves above or below their corresponding widgets
/// depending on the value of [preferBelow]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// depending on the value of [preferBelow]
/// depending on the value of [preferBelow].

Comment on lines +444 to +447
/// When [preferBelow] is set to true and tooltips have sufficient space to
/// display themselves, this property defines how much vertical space tooltips
/// will position themselves above or below their corresponding widgets
/// depending on the value of [preferBelow]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This paragraph looks incoherent to me, seems like two separate sentences were merged at an arbitrary position.

@mattkae
Copy link
Copy Markdown
Contributor Author

mattkae commented Apr 6, 2026

Why are we exposing preferBelow and verticalOffset? @chunhtai, @dkwingsmt and I had detailed conversations about why we shouldn't:

* [#177678 (comment)](https://github.com/flutter/flutter/pull/177678#discussion_r2621145143)

* [#177678 (comment)](https://github.com/flutter/flutter/pull/177678#discussion_r2579108543)

TLDR we left them out of RawTooltip because they are Material-opinionated properties, and there are ways to work around them (i.e RawTooltip.positionDelegate). We also have logic that resolves them in Tooltip so that they don't have to be passed down to RawTooltip (see Tooltip._getDefaultPositionDelegate).

Understood!

I gave this a try this morning, and it isn't really possible with how the windowing API currently works. Rather than working with absolute positions and offsets, the windowing API expects to be given some semantic information about the placement of the window (e.g. its placement gravity). At runtime, the platform is able to reason about the desired behavior and do something smart in the event that it cannot perform it (e.g. if the tooltip was going to go off of the monitor, the platform will flip it to the other side in order to keep it on screen). Because of this, it makes sense to have some semantic information piped down to the RawTooltip, instead of just a resolver for a raw "offset".

On top of this, we don't yet have a way for the window to provide its size before being shown, but that is a minor point that can certainly be added on our end if need be :)

Let me know if you have any ideas on a path forward here. I attempted to "infer" the WindowPositioner from the provided offset, but I fear that it will be too fragile for my taste (and it doesn't exactly work), e.g.:

  WindowPositioner _buildWindowPositioner() {
    final TooltipPositionDelegate? delegate = widget.positionDelegate;
    if (delegate == null) {
      // Default: position below the target, matching the overlay path default.
      return const WindowPositioner(
        parentAnchor: WindowPositionerAnchor.bottom,
        childAnchor: WindowPositionerAnchor.top,
      );
    }

    // Probe the delegate with the real target rect and a zero-sized tooltip.
    // The returned point is where the tooltip's top-left corner would be
    // placed, which with Size.zero is effectively the anchor point.
    // The offset from the target center to that point becomes the
    // WindowPositioner offset with center/center anchors.
    final Rect? anchorRect = _getAnchorRect();
    if (anchorRect == null) {
      return const WindowPositioner(
        parentAnchor: WindowPositionerAnchor.bottom,
        childAnchor: WindowPositionerAnchor.top,
      );
    }

    final Offset result = delegate(
      TooltipPositionContext(
        target: anchorRect.center,
        targetSize: anchorRect.size,
        tooltipSize: Size.zero,
        verticalOffset: 0.0,
      ),
    );

    return WindowPositioner(offset: result - anchorRect.center);
  }

@victorsanni
Copy link
Copy Markdown
Contributor

Thanks for the reply. My understanding of your explanation (and this PR) is:

  • the RawTooltip will have a windowing path and a non-windowing path.
  • knowing the Offset is insufficient for the windowing path, since the OS needs a more dynamic ruleset.

From this understanding, my guess is exposing preferBelow and verticalOffset may not provide a robust-enough solution for the windowing path, because of their opinionated-ness (real word?).

The underlying positionDependentBox that _TooltipPositionDelegate uses under the hood is a trap IMO because it is (mostly) fixed on the X-axis, allowing configuration only on the Y-axis:

/// Position a child box within a container box, either above or below a target
/// point.
...
Offset positionDependentBox({

My hunch is windowing needs will be more complicated than positionDependentBox can support (the WindowPositionerAnchor enum has way more values than just .bottom and .top). preferBelow and verticalOffset might work for current use cases, but could there be a use case for prefer left/right? or horizontal offset? If that happens, we would need to increase the API surface for TooltipPositionContext/RawTooltip. positionDelegate encapsulates this solution for the non-windowing path, but adding preferBelow/verticalOffset to support the windowing path feels like the beginnings of a spiral towards API bloat.

Maybe the solution here instead is a new top-level property similar to positionDelegate that encapsulates the ruleset needed for the windowing path. What do you think @mattkae?

@mattkae
Copy link
Copy Markdown
Contributor Author

mattkae commented Apr 6, 2026

Thanks for the reply. My understanding of your explanation (and this PR) is:

* the RawTooltip will have a windowing path and a non-windowing path.

* knowing the `Offset` is insufficient for the windowing path, since the OS needs a more dynamic ruleset.

From this understanding, my guess is exposing preferBelow and verticalOffset may not provide a robust-enough solution for the windowing path, because of their opinionated-ness (real word?).

The underlying positionDependentBox that _TooltipPositionDelegate uses under the hood is a trap IMO because it is (mostly) fixed on the X-axis, allowing configuration only on the Y-axis:

/// Position a child box within a container box, either above or below a target
/// point.
...
Offset positionDependentBox({

My hunch is windowing needs will be more complicated than positionDependentBox can support (the WindowPositionerAnchor enum has way more values than just .bottom and .top). preferBelow and verticalOffset might work for current use cases, but could there be a use case for prefer left/right? or horizontal offset? If that happens, we would need to increase the API surface for TooltipPositionContext/RawTooltip. positionDelegate encapsulates this solution for the non-windowing path, but adding preferBelow/verticalOffset to support the windowing path feels like the beginnings of a spiral towards API bloat.

Maybe the solution here instead is a new top-level property similar to positionDelegate that encapsulates the ruleset needed for the windowing path. What do you think @mattkae?

I think that is an excellent idea! I can propose one in this PR, and I will re-request you when it's done. I need to think about what will work well across both use cases first.

@justinmc
Copy link
Copy Markdown
Contributor

justinmc commented Apr 8, 2026

a new top-level property similar to positionDelegate that encapsulates the ruleset needed for the windowing path

After reading through the thread here that's my best bet for how to resolve this too. Something like a windowPositioner parameter (or function that builds a WindowPositioner). It looks to me like if the user passes a WindowPositioner, then we can use that to produce an Offset similar to what's returned from positionDelegate.

@mattkae mattkae marked this pull request as draft April 10, 2026 18:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

5 participants