Last active
March 8, 2026 02:45
-
-
Save davidhicks980/291e413a4b4ea4ee93d71050d72f1416 to your computer and use it in GitHub Desktop.
StackPortal
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'dart:ui' as ui; | |
| import 'package:flutter/foundation.dart'; | |
| import 'package:flutter/rendering.dart'; | |
| import 'package:flutter/widgets.dart'; | |
| extension type OutletPositioner._(RenderBox renderBox) { | |
| StackOutletParentData get _parentData => renderBox.parentData! as StackOutletParentData; | |
| Size get size => renderBox.size; | |
| Size get anchorSize => _parentData.anchorSize ?? Size.zero; | |
| Matrix4 get ancestorAnchorTransform { | |
| return _parentData.ancestorAnchorTransform ?? Matrix4.identity(); | |
| } | |
| // ignore: use_setters_to_change_properties | |
| void setPosition(ui.Offset position) { | |
| _parentData.offset = position; | |
| } | |
| } | |
| abstract class OutletPositioningDelegate { | |
| const OutletPositioningDelegate(); | |
| /// Override to control the layout of the portal children of the outlet. | |
| void positionChildren(Size size, List<OutletPositioner> children); | |
| /// Override to return true when the layout needs to be updated. | |
| bool shouldRelayout(covariant OutletPositioningDelegate oldDelegate) => true; | |
| @override | |
| String toString() => objectRuntimeType(this, 'OutletPositioningDelegate'); | |
| } | |
| class StackOutletParentData extends ContainerBoxParentData<RenderBox> { | |
| Matrix4? ancestorAnchorTransform; | |
| Size? anchorSize; | |
| } | |
| /// A widget that acts as a container for a main child and allows [StackPortal]s | |
| /// deeper in the tree to project content on top of it. | |
| class StackOutlet extends StatelessWidget { | |
| const StackOutlet({super.key, required this.child, required this.positioningDelegate}); | |
| /// The main content of the application or section. | |
| final Widget child; | |
| final OutletPositioningDelegate positioningDelegate; | |
| @override | |
| Widget build(BuildContext context) { | |
| return _Outlet( | |
| positioningDelegate: positioningDelegate, | |
| child: Builder(builder: _buildScope), | |
| ); | |
| } | |
| Widget _buildScope(BuildContext context) { | |
| final _RenderOutlet stackOutlet = context.findAncestorRenderObjectOfType<_RenderOutlet>()!; | |
| return _StackOutletScope(renderOutlet: stackOutlet, child: child); | |
| } | |
| } | |
| class _StackOutletScope extends InheritedWidget { | |
| const _StackOutletScope({required this.renderOutlet, required super.child}); | |
| final _RenderOutlet renderOutlet; | |
| static _RenderOutlet? of(BuildContext context) { | |
| return context.dependOnInheritedWidgetOfExactType<_StackOutletScope>()?.renderOutlet; | |
| } | |
| @override | |
| bool updateShouldNotify(_StackOutletScope oldWidget) => renderOutlet != oldWidget.renderOutlet; | |
| } | |
| class _Outlet extends SingleChildRenderObjectWidget { | |
| const _Outlet({required super.child, required this.positioningDelegate}); | |
| final OutletPositioningDelegate positioningDelegate; | |
| @override | |
| _RenderOutlet createRenderObject(BuildContext context) { | |
| return _RenderOutlet(positioningDelegate: positioningDelegate); | |
| } | |
| @override | |
| void updateRenderObject(BuildContext context, _RenderOutlet renderObject) { | |
| renderObject.positioningDelegate = positioningDelegate; | |
| } | |
| } | |
| class _RenderOutlet extends RenderBox with RenderObjectWithChildMixin<RenderBox> { | |
| _RenderOutlet({required OutletPositioningDelegate positioningDelegate}) | |
| : _positioningDelegate = positioningDelegate; | |
| OutletPositioningDelegate get positioningDelegate => _positioningDelegate; | |
| OutletPositioningDelegate _positioningDelegate; | |
| set positioningDelegate(OutletPositioningDelegate newDelegate) { | |
| if (newDelegate == _positioningDelegate) { | |
| return; | |
| } | |
| final OutletPositioningDelegate oldDelegate = _positioningDelegate; | |
| _positioningDelegate = newDelegate; | |
| if (newDelegate.runtimeType != oldDelegate.runtimeType || | |
| newDelegate.shouldRelayout(oldDelegate)) { | |
| markNeedsLayout(); | |
| } | |
| } | |
| final List<_RenderPortalContents> _portalChildren = []; | |
| bool _isMarkNeedsLayoutSkipped = false; | |
| void addPortalChild(_RenderPortalContents portalItem) { | |
| assert(!_isMarkNeedsLayoutSkipped); | |
| _isMarkNeedsLayoutSkipped = true; | |
| _portalChildren.add(portalItem); | |
| adoptChild(portalItem); | |
| _isMarkNeedsLayoutSkipped = false; | |
| markNeedsLayout(); | |
| } | |
| void removePortalChild(_RenderPortalContents portalItem) { | |
| assert(!_isMarkNeedsLayoutSkipped); | |
| assert(portalItem.parent == this); | |
| assert(_portalChildren.contains(portalItem)); | |
| _isMarkNeedsLayoutSkipped = true; | |
| dropChild(portalItem); | |
| // Remove the item from the tracking list. | |
| _portalChildren.remove(portalItem); | |
| _isMarkNeedsLayoutSkipped = false; | |
| markNeedsLayout(); | |
| } | |
| @override | |
| void markNeedsLayout() { | |
| if (!_isMarkNeedsLayoutSkipped) { | |
| super.markNeedsLayout(); | |
| } | |
| } | |
| @override | |
| void setupParentData(RenderBox child) { | |
| if (child.parentData is! StackOutletParentData) { | |
| child.parentData = StackOutletParentData(); | |
| } | |
| } | |
| @override | |
| void attach(PipelineOwner owner) { | |
| super.attach(owner); | |
| for (final _RenderPortalContents child in _portalChildren) { | |
| child.attach(owner); | |
| } | |
| } | |
| @override | |
| void detach() { | |
| super.detach(); | |
| for (final _RenderPortalContents child in _portalChildren) { | |
| child.detach(); | |
| } | |
| } | |
| @override | |
| void redepthChildren() { | |
| super.redepthChildren(); | |
| _portalChildren.forEach(redepthChild); | |
| } | |
| @override | |
| void visitChildren(RenderObjectVisitor visitor) { | |
| super.visitChildren(visitor); | |
| _portalChildren.forEach(visitor); | |
| } | |
| void _updatePositioning() { | |
| final RenderBox? main = child; | |
| if (main == null) { | |
| assert(_portalChildren.isEmpty, 'Portal children should not exist when main child is null.'); | |
| return; | |
| } | |
| for (final _RenderPortalContents child in _portalChildren) { | |
| child.anchor!.updateAnchorParentData(child); | |
| } | |
| final List<OutletPositioner> children = [ | |
| OutletPositioner._(main), | |
| for (final child in _portalChildren) OutletPositioner._(child), | |
| ]; | |
| _positioningDelegate.positionChildren(size, children); | |
| } | |
| @override | |
| void performLayout() { | |
| size = constraints.biggest; | |
| final RenderBox? main = child; | |
| if (main == null) { | |
| assert(_portalChildren.isEmpty, 'Portal children should not exist when main child is null.'); | |
| return; | |
| } | |
| main.layout(constraints, parentUsesSize: true); | |
| // Layout Portals | |
| final portalConstraints = BoxConstraints.loose(size); | |
| for (final _RenderPortalContents child in _portalChildren) { | |
| child.layout(portalConstraints, parentUsesSize: true); | |
| } | |
| } | |
| @override | |
| bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { | |
| assert(_portalChildren.isEmpty || child != null); | |
| for (final _RenderPortalContents child in _portalChildren.reversed) { | |
| final StackOutletParentData childParentData = child.stackParentData; | |
| final bool isHit = result.addWithPaintOffset( | |
| offset: childParentData.offset, | |
| position: position, | |
| hitTest: (BoxHitTestResult result, Offset transformed) { | |
| return child.hitTest(result, position: transformed); | |
| }, | |
| ); | |
| if (isHit) { | |
| return true; | |
| } | |
| } | |
| final RenderBox? main = child; | |
| if (main == null) { | |
| return false; | |
| } | |
| final mainParentData = main.parentData! as StackOutletParentData; | |
| return result.addWithPaintOffset( | |
| offset: mainParentData.offset, | |
| position: position, | |
| hitTest: (BoxHitTestResult result, Offset transformed) { | |
| return main.hitTest(result, position: transformed); | |
| }, | |
| ); | |
| } | |
| @override | |
| void paint(PaintingContext context, Offset offset) { | |
| _updatePositioning(); | |
| final RenderBox? main = child; | |
| if (main != null) { | |
| final mainParentData = main.parentData! as StackOutletParentData; | |
| context.paintChild(main, mainParentData.offset + offset); | |
| } | |
| for (final _RenderPortalContents child in _portalChildren) { | |
| context.paintChild(child, child.stackParentData.offset + offset); | |
| } | |
| } | |
| @override | |
| void applyPaintTransform(RenderBox child, Matrix4 transform) { | |
| final childParentData = child.parentData! as BoxParentData; | |
| final Offset offset = childParentData.offset; | |
| transform.translateByDouble(offset.dx, offset.dy, 0, 1); | |
| } | |
| } | |
| /// A widget that keeps its [child] in the normal tree (the "anchor") but | |
| /// projects [portalBuilder] content into the nearest ancestor [StackOutlet]. | |
| class StackPortal extends StatelessWidget { | |
| const StackPortal({ | |
| super.key, | |
| required this.child, | |
| required this.portalBuilder, | |
| this.isShowing = false, | |
| }); | |
| final Widget child; | |
| final WidgetBuilder portalBuilder; | |
| final bool isShowing; | |
| @override | |
| Widget build(BuildContext context) { | |
| return _StackPortalContentWrapper( | |
| portal: isShowing | |
| ? _PortalContents( | |
| childIdentifier: this, | |
| child: Builder(builder: portalBuilder), | |
| ) | |
| : null, | |
| child: Semantics(traversalParentIdentifier: this, child: child), | |
| ); | |
| } | |
| } | |
| class _StackPortalContentWrapper extends RenderObjectWidget { | |
| const _StackPortalContentWrapper({required this.child, required this.portal}); | |
| final Widget child; | |
| final Widget? portal; | |
| @override | |
| RenderObjectElement createElement() => _StackPortalElement(this); | |
| @override | |
| RenderObject createRenderObject(BuildContext context) => _RenderAnchor(); | |
| } | |
| enum _Slot { anchor, portal } | |
| class _StackPortalElement extends RenderObjectElement { | |
| _StackPortalElement(_StackPortalContentWrapper super.widget); | |
| Element? _anchorElement; | |
| Element? _portalElement; // The content projected to the outlet | |
| @override | |
| _StackPortalContentWrapper get widget => super.widget as _StackPortalContentWrapper; | |
| @override | |
| _RenderAnchor get renderObject => super.renderObject as _RenderAnchor; | |
| _RenderOutlet? _currentOutlet; | |
| _RenderPortalContents? _portalRenderObject; | |
| void _attachToOutlet() { | |
| if (_portalRenderObject != null) { | |
| _currentOutlet = findAncestorRenderObjectOfType<_RenderOutlet>(); | |
| assert(_currentOutlet != null, 'StackPortal must be a descendant of a StackOutlet.'); | |
| _currentOutlet!.addPortalChild(_portalRenderObject!); | |
| } | |
| } | |
| void _detachFromOutlet() { | |
| if (_portalRenderObject != null && _currentOutlet != null) { | |
| _currentOutlet!.removePortalChild(_portalRenderObject!); | |
| _currentOutlet = null; | |
| } | |
| } | |
| @override | |
| void mount(Element? parent, Object? newSlot) { | |
| super.mount(parent, newSlot); | |
| _anchorElement = updateChild(_anchorElement, widget.child, _Slot.anchor); | |
| _portalElement = updateChild(_portalElement, widget.portal, _Slot.portal); | |
| (_portalElement?.renderObject as _RenderPortalContents?)?.anchor = renderObject; | |
| } | |
| @override | |
| void update(_StackPortalContentWrapper newWidget) { | |
| super.update(newWidget); | |
| _anchorElement = updateChild(_anchorElement, widget.child, _Slot.anchor); | |
| _portalElement = updateChild(_portalElement, widget.portal, _Slot.portal); | |
| (_portalElement?.renderObject as _RenderPortalContents?)?.anchor = renderObject; | |
| } | |
| @override | |
| void forgetChild(Element child) { | |
| assert(child == _anchorElement || child == _portalElement); | |
| if (child == _anchorElement) { | |
| _anchorElement = null; | |
| } else if (child == _portalElement) { | |
| _portalElement = null; | |
| } | |
| super.forgetChild(child); | |
| } | |
| @override | |
| void activate() { | |
| super.activate(); | |
| _attachToOutlet(); | |
| } | |
| @override | |
| void deactivate() { | |
| _detachFromOutlet(); | |
| super.deactivate(); | |
| } | |
| @override | |
| void insertRenderObjectChild(RenderObject child, Object? slot) { | |
| assert(child.parent == null, "$child's parent is not null: ${child.parent}"); | |
| switch (slot) { | |
| case _Slot.portal: | |
| assert(child is _RenderPortalContents); | |
| final portalChild = child as _RenderPortalContents; | |
| _portalRenderObject = portalChild; | |
| portalChild.anchor = renderObject; | |
| renderObject._portalContents = portalChild; | |
| _attachToOutlet(); | |
| renderObject.markNeedsSemanticsUpdate(); | |
| case _Slot.anchor: | |
| renderObject.child = child as RenderBox; | |
| } | |
| } | |
| @override | |
| void removeRenderObjectChild(RenderObject child, Object? slot) { | |
| switch (slot) { | |
| case _Slot.portal: | |
| _detachFromOutlet(); | |
| _portalRenderObject?.anchor = null; | |
| renderObject._portalContents = null; | |
| renderObject.markNeedsSemanticsUpdate(); | |
| case _Slot.anchor: | |
| renderObject.child = null; | |
| } | |
| } | |
| @override | |
| void moveRenderObjectChild(RenderObject child, Object? oldSlot, Object? newSlot) { | |
| assert(false, 'Reparenting of StackPortal content is not supported.'); | |
| } | |
| @override | |
| void visitChildren(ElementVisitor visitor) { | |
| if (_anchorElement != null) { | |
| visitor(_anchorElement!); | |
| } | |
| if (_portalElement != null) { | |
| visitor(_portalElement!); | |
| } | |
| } | |
| } | |
| class _PortalContents extends SingleChildRenderObjectWidget { | |
| const _PortalContents({required Widget super.child, this.childIdentifier}); | |
| final Object? childIdentifier; | |
| _RenderAnchor getLayoutParent(BuildContext context) { | |
| return context.findAncestorRenderObjectOfType<_RenderAnchor>()!; | |
| } | |
| @override | |
| _RenderPortalContents createRenderObject(BuildContext context) { | |
| final _RenderAnchor parent = getLayoutParent(context); | |
| final renderObject = _RenderPortalContents(parent, childIdentifier: childIdentifier); | |
| parent._portalContents = renderObject; | |
| return renderObject; | |
| } | |
| @override | |
| void updateRenderObject(BuildContext context, _RenderPortalContents renderObject) { | |
| assert(renderObject.anchor == getLayoutParent(context)); | |
| assert(getLayoutParent(context)._portalContents == renderObject); | |
| renderObject.childIdentifier = childIdentifier; | |
| } | |
| } | |
| /// A RenderProxyBox that wraps the portal content in the outlet's render tree. | |
| /// It holds a reference to the [RenderObject] of its anchor so that it can | |
| /// calculate a stable transform to the previous [_RenderPortalContents] or | |
| /// [_RenderOutlet] ancestor. | |
| /// | |
| /// Unlike the [RenderObject] backing [OverlayPortal], this RenderObject is not | |
| /// a relayout boundary. Since [_RenderOutlet] passes the size of each portal | |
| /// child to the [OutletPositioningDelegate], [_RenderOutlet] needs to rebuild | |
| /// whenever the size of a portal changes. | |
| final class _RenderPortalContents extends RenderProxyBox { | |
| _RenderPortalContents(this.anchor, {required this.childIdentifier}); | |
| StackOutletParentData get stackParentData => parentData! as StackOutletParentData; | |
| _RenderAnchor? anchor; | |
| Object? childIdentifier; | |
| @override | |
| void redepthChildren() { | |
| anchor!.redepthChild(this); | |
| super.redepthChildren(); | |
| } | |
| @override | |
| double? computeDryBaseline(BoxConstraints constraints, TextBaseline baseline) { | |
| return child?.getDryBaseline(constraints.loosen(), baseline); | |
| } | |
| @override | |
| ui.Size computeDryLayout(BoxConstraints constraints) { | |
| return child?.getDryLayout(constraints.loosen()) ?? constraints.smallest; | |
| } | |
| @override | |
| RenderObject? get debugLayoutParent => anchor; | |
| @override | |
| void performLayout() { | |
| assert(parent is _RenderOutlet); | |
| final RenderBox? child = this.child; | |
| if (child == null) { | |
| size = constraints.smallest; | |
| return; | |
| } | |
| child.layout(constraints.loosen(), parentUsesSize: true); | |
| size = child.size; | |
| } | |
| @override | |
| void describeSemanticsConfiguration(SemanticsConfiguration config) { | |
| super.describeSemanticsConfiguration(config); | |
| if (childIdentifier != null) { | |
| config.traversalChildIdentifier = childIdentifier; | |
| } | |
| } | |
| @override | |
| void setupParentData(RenderBox child) { | |
| if (child.parentData is! StackOutletParentData) { | |
| child.parentData = StackOutletParentData(); | |
| } | |
| } | |
| @override | |
| void applyPaintTransform(RenderBox child, Matrix4 transform) { | |
| final childParentData = child.parentData! as StackOutletParentData; | |
| final Offset offset = childParentData.offset; | |
| transform.translateByDouble(offset.dx, offset.dy, 0, 1); | |
| } | |
| } | |
| // A RenderProxyBox that makes sure its `_portalContents` has a greater | |
| // depth than itself, and triggers updates when it moves. | |
| class _RenderAnchor extends RenderProxyBox { | |
| _RenderPortalContents? _portalContents; | |
| _RenderOutlet get outlet { | |
| RenderObject? ancestor = parent; | |
| while (ancestor != null) { | |
| if (ancestor is _RenderOutlet) { | |
| return ancestor; | |
| } | |
| ancestor = ancestor.parent; | |
| } | |
| throw FlutterError('No ancestor _RenderOutlet found for _RenderAnchor.'); | |
| } | |
| @override | |
| void redepthChildren() { | |
| super.redepthChildren(); | |
| final _RenderPortalContents? child = _portalContents; | |
| // If child is not attached yet, this method will be invoked by child's real | |
| // parent (the outlet) when it becomes attached. | |
| if (child != null && child.attached) { | |
| redepthChild(child); | |
| } | |
| } | |
| void updateAnchorParentData(_RenderPortalContents portal) { | |
| RenderObject? ancestor = parent; | |
| while (ancestor != null) { | |
| // Stop when a parent Portal (nested case) or an Outlet (root case) is found. | |
| if (ancestor.parent is _RenderPortalContents || ancestor.parent is _RenderOutlet) { | |
| break; | |
| } | |
| ancestor = ancestor.parent; | |
| } | |
| if (ancestor == null) { | |
| assert(false, 'No ancestor Portal or Outlet found for StackPortal content.'); | |
| return; | |
| } | |
| // Calculate the anchor rect transform relative to the panel. | |
| final Matrix4 transform = getTransformTo(ancestor); | |
| portal.stackParentData | |
| ..ancestorAnchorTransform = transform | |
| ..anchorSize = size; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment