Skip to content

Instantly share code, notes, and snippets.

@davidhicks980
Last active March 8, 2026 02:45
Show Gist options
  • Select an option

  • Save davidhicks980/291e413a4b4ea4ee93d71050d72f1416 to your computer and use it in GitHub Desktop.

Select an option

Save davidhicks980/291e413a4b4ea4ee93d71050d72f1416 to your computer and use it in GitHub Desktop.
StackPortal
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