Skip to content

feat: Add WidgetComponent to render Flutter widgets in the component tree - #4053

Open
spydon wants to merge 4 commits into
mainfrom
feat/widget-component
Open

spydon wants to merge 4 commits into
mainfrom
feat/widget-component

Conversation

@spydon

@spydon spydon commented Sep 23, 2026

Copy link
Copy Markdown
Member

Description

Adds WidgetComponent, a PositionComponent that hosts a Flutter widget inside the Flame component tree.

The widget becomes a real part of the Flutter element tree under the GameWidget, so it is laid out, painted, hit tested and focused like any other widget: buttons respond to taps, text fields receive keyboard input, and inherited widgets such as Theme, MediaQuery and Directionality are available to it. At the same time it is rendered in the middle of the Flame render pass, so it respects the component priority, the camera transform and the position, angle, scale and anchor of the component and all of its ancestors. This is different from overlays, which sit in a Stack above the whole game and are not affected by the camera or component transforms.

world.add(
  WidgetComponent(
    position: Vector2(100, 100),
    size: Vector2(200, 60),
    anchor: Anchor.center,
    widget: ElevatedButton(
      onPressed: () => print('pressed'),
      child: const Text('Play'),
    ),
  ),
);

When size is given the widget gets tight constraints of that size. When it is omitted the component adopts the widget's size, laying it out with the optional constraints or, by default, loose constraints bounded by the game canvas in the component's local units.

How it works

  • RenderGameWidget is now a MultiChildRenderObjectWidget and GameRenderBox mixes in ContainerRenderObjectMixin. Each mounted WidgetComponent registers itself on the Game, and the GameWidget builds the hosted widgets as children of the game render box, each in a small WidgetComponentHost that links the child to its component, rebuilds only the hosted subtree when widget is reassigned, and excludes the subtree from focus and semantics while the component is not being rendered (for example under a hidden ancestor).
  • GameRenderBox.performLayout lays the children out with the constraints derived from their component, and WidgetComponent.render asks the render box to paint its child at the current canvas transform.
  • A widget that does not need compositing is painted straight into the game's picture. One that does (for example anything containing a RepaintBoundary) is painted through a TransformLayer, which requires ending the picture that is being recorded. The new ProxyCanvas makes this transparent to the rest of the component tree: it forwards every call to the current underlying canvas and, when the game's picture is split, replays the save stack, transforms and clips onto the new canvas so that pending restore calls keep balancing. Games without widget components keep using the plain canvas, so there is no overhead for them.
  • Hit testing uses the transforms captured during paint through BoxHitTestResult.addWithPaintTransform, and applyPaintTransform reports the same transforms so that localToGlobal lookups made by Flutter (text input, tooltips, finders) are correct. Rectangular Flame clips, such as the camera viewport, are applied to composited widgets and to hit testing.

Limitations

  • The widget is only rendered by the GameWidget render pass. It is not included when the component tree is rendered into a Picture or Image elsewhere, such as by the Snapshot mixin, PostProcesses or the devtools component snapshot.
  • Flame paints are not applied to the widget, so paint based effects like OpacityEffect do not affect it. Transforms and rectangular clips do.
  • A widget that needs its own compositing layer splits the game's picture, so any saveLayer an ancestor component has active at that point is closed and reopened around the widget.
  • A widget can only be painted once per frame. When the same component is rendered several times in one frame, for example because its world is viewed by several cameras, only the first render paints the widget.

Checklist

  • I have followed the Contributor Guide when preparing my PR.
  • I have updated/added tests for ALL new/updated/fixed functionality.
  • I have updated/added relevant documentation in docs and added dartdoc comments with ///.
  • I have updated/added relevant examples in examples or docs.

Breaking Change?

  • Yes, this PR is a breaking change.
  • No, this PR is not a breaking change.

@spydon
spydon requested a balanced review from Copilot September 23, 2026 15:51

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

This branch has not been deployed

No deployments
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.

2 participants