diff --git a/examples/api/lib/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0.dart b/examples/api/lib/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0.dart new file mode 100644 index 00000000000..217544b3b6c --- /dev/null +++ b/examples/api/lib/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0.dart @@ -0,0 +1,289 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +/// Slots used for the children of [Diagonal] and [RenderDiagonal]. +enum DiagonalSlot { + topLeft, + bottomRight, +} + +/// A widget that demonstrates the usage of [SlottedMultiChildRenderObjectWidgetMixin] +/// by providing slots for two children that will be arranged diagonally. +class Diagonal extends RenderObjectWidget with SlottedMultiChildRenderObjectWidgetMixin { + const Diagonal({ + Key? key, + this.topLeft, + this.bottomRight, + this.backgroundColor, + }) : super(key: key); + + final Widget? topLeft; + final Widget? bottomRight; + final Color? backgroundColor; + + @override + Iterable get slots => DiagonalSlot.values; + + @override + Widget? childForSlot(DiagonalSlot slot) { + switch (slot) { + case DiagonalSlot.topLeft: + return topLeft; + case DiagonalSlot.bottomRight: + return bottomRight; + } + } + + // The [createRenderObject] and [updateRenderObject] methods configure the + // [RenderObject] backing this widget with the configuration of the widget. + // They do not need to do anything with the children of the widget, though. + // The children of the widget are automatically configured on the + // [RenderObject] by [SlottedRenderObjectElement.mount] and + // [SlottedRenderObjectElement.update]. + + @override + SlottedContainerRenderObjectMixin createRenderObject( + BuildContext context, + ) { + return RenderDiagonal( + backgroundColor: backgroundColor, + ); + } + + @override + void updateRenderObject( + BuildContext context, + SlottedContainerRenderObjectMixin renderObject, + ) { + (renderObject as RenderDiagonal).backgroundColor = backgroundColor; + } +} + +/// A render object that demonstrates the usage of [SlottedContainerRenderObjectMixin] +/// by providing slots for two children that will be arranged diagonally. +class RenderDiagonal extends RenderBox with SlottedContainerRenderObjectMixin, DebugOverflowIndicatorMixin { + RenderDiagonal({Color? backgroundColor}) : _backgroundColor = backgroundColor; + + // Getters and setters to configure the [RenderObject] with the configuration + // of the [Widget]. These mostly contain boilerplate code, but depending on + // where the configuration value is used, the setter has to call + // [markNeedsLayout], [markNeedsPaint], or [markNeedsSemanticsUpdate]. + Color? get backgroundColor => _backgroundColor; + Color? _backgroundColor; + set backgroundColor(Color? value) { + assert(value != null); + if (_backgroundColor == value) { + return; + } + _backgroundColor = value; + markNeedsPaint(); + } + + // Getters to simplify accessing the slotted children. + RenderBox? get _topLeft => childForSlot(DiagonalSlot.topLeft); + RenderBox? get _bottomRight => childForSlot(DiagonalSlot.bottomRight); + + // The size this render object would have if the incoming constraints were + // unconstrained; calculated during performLayout used during paint for an + // assertion that checks for unintended overflow. + late Size _childrenSize; + + // Returns children in hit test order. + @override + Iterable get children sync* { + if (_topLeft != null) { + yield _topLeft!; + } + if (_bottomRight != null) { + yield _bottomRight!; + } + } + + // LAYOUT + + @override + void performLayout() { + // Children are allowed to be as big as they want (= unconstrained). + const BoxConstraints childConstraints = BoxConstraints(); + + // Lay out the top left child and position it at offset zero. + Size topLeftSize = Size.zero; + final RenderBox? topLeft = _topLeft; + if (topLeft != null) { + topLeft.layout(childConstraints, parentUsesSize: true); + _positionChild(topLeft, Offset.zero); + topLeftSize = topLeft.size; + } + + // Lay out the bottom right child and position it at the bottom right corner + // of the top left child. + Size bottomRightSize = Size.zero; + final RenderBox? bottomRight = _bottomRight; + if (bottomRight != null) { + bottomRight.layout(childConstraints, parentUsesSize: true); + _positionChild( + bottomRight, + Offset(topLeftSize.width, topLeftSize.height), + ); + bottomRightSize = bottomRight.size; + } + + // Calculate the overall size and constrain it to the given constraints. + // Any overflow is marked (in debug mode) during paint. + _childrenSize = Size( + topLeftSize.width + bottomRightSize.width, + topLeftSize.height + bottomRightSize.height, + ); + size = constraints.constrain(_childrenSize); + } + + void _positionChild(RenderBox child, Offset offset) { + (child.parentData! as BoxParentData).offset = offset; + } + + // PAINT + + @override + void paint(PaintingContext context, Offset offset) { + // Paint the background. + if (backgroundColor != null) { + context.canvas.drawRect( + offset & size, + Paint() + ..color = backgroundColor!, + ); + } + + void paintChild(RenderBox child, PaintingContext context, Offset offset) { + final BoxParentData childParentData = child.parentData! as BoxParentData; + context.paintChild(child, childParentData.offset + offset); + } + + // Paint the children at the offset calculated during layout. + final RenderBox? topLeft = _topLeft; + if (topLeft != null) { + paintChild(topLeft, context, offset); + } + final RenderBox? bottomRight = _bottomRight; + if (bottomRight != null) { + paintChild(bottomRight, context, offset); + } + + // Paint an overflow indicator in debug mode if the children want to be + // larger than the incoming constraints allow. + assert(() { + paintOverflowIndicator( + context, + offset, + Offset.zero & size, + Offset.zero & _childrenSize, + ); + return true; + }()); + } + + // HIT TEST + + @override + bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { + for (final RenderBox child in children) { + final BoxParentData parentData = child.parentData! as BoxParentData; + final bool isHit = result.addWithPaintOffset( + offset: parentData.offset, + position: position, + hitTest: (BoxHitTestResult result, Offset transformed) { + assert(transformed == position - parentData.offset); + return child.hitTest(result, position: transformed); + }, + ); + if (isHit) { + return true; + } + } + return false; + } + + // INTRINSICS + + // Incoming height/width are ignored as children are always laid out unconstrained. + + @override + double computeMinIntrinsicWidth(double height) { + final double topLeftWidth = _topLeft?.getMinIntrinsicWidth(double.infinity) ?? 0; + final double bottomRightWith = _bottomRight?.getMinIntrinsicWidth(double.infinity) ?? 0; + return topLeftWidth + bottomRightWith; + } + + @override + double computeMaxIntrinsicWidth(double height) { + final double topLeftWidth = _topLeft?.getMaxIntrinsicWidth(double.infinity) ?? 0; + final double bottomRightWith = _bottomRight?.getMaxIntrinsicWidth(double.infinity) ?? 0; + return topLeftWidth + bottomRightWith; } + + @override + double computeMinIntrinsicHeight(double width) { + final double topLeftHeight = _topLeft?.getMinIntrinsicHeight(double.infinity) ?? 0; + final double bottomRightHeight = _bottomRight?.getMinIntrinsicHeight(double.infinity) ?? 0; + return topLeftHeight + bottomRightHeight; + } + + @override + double computeMaxIntrinsicHeight(double width) { + final double topLeftHeight = _topLeft?.getMaxIntrinsicHeight(double.infinity) ?? 0; + final double bottomRightHeight = _bottomRight?.getMaxIntrinsicHeight(double.infinity) ?? 0; + return topLeftHeight + bottomRightHeight; + } + + @override + Size computeDryLayout(BoxConstraints constraints) { + const BoxConstraints childConstraints = BoxConstraints(); + final Size topLeftSize = _topLeft?.computeDryLayout(childConstraints) ?? Size.zero; + final Size bottomRightSize = _bottomRight?.computeDryLayout(childConstraints) ?? Size.zero; + return constraints.constrain(Size( + topLeftSize.width + bottomRightSize.width, + topLeftSize.height + bottomRightSize.height, + )); + } +} + +class ExampleWidget extends StatelessWidget { + const ExampleWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar(title: const Text('Slotted RenderObject Example')), + body: Center( + child: Diagonal( + topLeft: Container( + color: Colors.green, + height: 100, + width: 200, + child: const Center( + child: Text('topLeft'), + ), + ), + bottomRight: Container( + color: Colors.yellow, + height: 60, + width: 30, + child: const Center( + child: Text('bottomRight'), + ), + ), + backgroundColor: Colors.blue, + ), + ), + ), + ); + } +} + +void main() { + runApp(const ExampleWidget()); +} diff --git a/examples/api/test/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0_test.dart b/examples/api/test/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0_test.dart new file mode 100644 index 00000000000..150d9e57fd1 --- /dev/null +++ b/examples/api/test/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0_test.dart @@ -0,0 +1,41 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; +import 'package:flutter_api_samples/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('shows two widgets arranged diagonally', (WidgetTester tester) async { + await tester.pumpWidget( + const example.ExampleWidget(), + ); + + expect(find.text('topLeft'), findsOneWidget); + expect(find.text('bottomRight'), findsOneWidget); + + expect( + tester.getBottomRight(findContainerWithText('topLeft')), + tester.getTopLeft(findContainerWithText('bottomRight')), + ); + + expect( + tester.getSize(findContainerWithText('topLeft')), + const Size(200, 100), + ); + expect( + tester.getSize(findContainerWithText('bottomRight')), + const Size(30, 60), + ); + + expect( + tester.getSize(find.byType(example.Diagonal)), + const Size(200 + 30, 100 + 60), + ); + }); +} + +Finder findContainerWithText(String text) { + return find.ancestor(of: find.text(text), matching: find.byType(Container)); +} diff --git a/packages/flutter/lib/src/material/chip.dart b/packages/flutter/lib/src/material/chip.dart index 82ff6c2fbb7..b720b856811 100644 --- a/packages/flutter/lib/src/material/chip.dart +++ b/packages/flutter/lib/src/material/chip.dart @@ -2059,7 +2059,7 @@ class _RenderChipRedirectingHitDetection extends RenderConstrainedBox { } } -class _ChipRenderWidget extends RenderObjectWidget { +class _ChipRenderWidget extends RenderObjectWidget with SlottedMultiChildRenderObjectWidgetMixin<_ChipSlot> { const _ChipRenderWidget({ Key? key, required this.theme, @@ -2083,7 +2083,19 @@ class _ChipRenderWidget extends RenderObjectWidget { final ShapeBorder? avatarBorder; @override - _RenderChipElement createElement() => _RenderChipElement(this); + Iterable<_ChipSlot> get slots => _ChipSlot.values; + + @override + Widget? childForSlot(_ChipSlot slot) { + switch (slot) { + case _ChipSlot.label: + return theme.label; + case _ChipSlot.avatar: + return theme.avatar; + case _ChipSlot.deleteIcon: + return theme.deleteIcon; + } + } @override void updateRenderObject(BuildContext context, _RenderChip renderObject) { @@ -2100,7 +2112,7 @@ class _ChipRenderWidget extends RenderObjectWidget { } @override - RenderObject createRenderObject(BuildContext context) { + SlottedContainerRenderObjectMixin<_ChipSlot> createRenderObject(BuildContext context) { return _RenderChip( theme: theme, textDirection: Directionality.of(context), @@ -2121,105 +2133,6 @@ enum _ChipSlot { deleteIcon, } -class _RenderChipElement extends RenderObjectElement { - _RenderChipElement(_ChipRenderWidget chip) : super(chip); - - final Map<_ChipSlot, Element> slotToChild = <_ChipSlot, Element>{}; - - @override - _ChipRenderWidget get widget => super.widget as _ChipRenderWidget; - - @override - _RenderChip get renderObject => super.renderObject as _RenderChip; - - @override - void visitChildren(ElementVisitor visitor) { - slotToChild.values.forEach(visitor); - } - - @override - void forgetChild(Element child) { - assert(slotToChild.containsValue(child)); - assert(child.slot is _ChipSlot); - assert(slotToChild.containsKey(child.slot)); - slotToChild.remove(child.slot); - super.forgetChild(child); - } - - void _mountChild(Widget widget, _ChipSlot slot) { - final Element? oldChild = slotToChild[slot]; - final Element? newChild = updateChild(oldChild, widget, slot); - if (oldChild != null) { - slotToChild.remove(slot); - } - if (newChild != null) { - slotToChild[slot] = newChild; - } - } - - @override - void mount(Element? parent, Object? newSlot) { - super.mount(parent, newSlot); - _mountChild(widget.theme.avatar, _ChipSlot.avatar); - _mountChild(widget.theme.deleteIcon, _ChipSlot.deleteIcon); - _mountChild(widget.theme.label, _ChipSlot.label); - } - - void _updateChild(Widget widget, _ChipSlot slot) { - final Element? oldChild = slotToChild[slot]; - final Element? newChild = updateChild(oldChild, widget, slot); - if (oldChild != null) { - slotToChild.remove(slot); - } - if (newChild != null) { - slotToChild[slot] = newChild; - } - } - - @override - void update(_ChipRenderWidget newWidget) { - super.update(newWidget); - assert(widget == newWidget); - _updateChild(widget.theme.label, _ChipSlot.label); - _updateChild(widget.theme.avatar, _ChipSlot.avatar); - _updateChild(widget.theme.deleteIcon, _ChipSlot.deleteIcon); - } - - void _updateRenderObject(RenderObject? child, _ChipSlot slot) { - switch (slot) { - case _ChipSlot.avatar: - renderObject.avatar = child as RenderBox?; - break; - case _ChipSlot.label: - renderObject.label = child as RenderBox?; - break; - case _ChipSlot.deleteIcon: - renderObject.deleteIcon = child as RenderBox?; - break; - } - } - - @override - void insertRenderObjectChild(RenderObject child, _ChipSlot slot) { - assert(child is RenderBox); - _updateRenderObject(child, slot); - assert(renderObject.children.keys.contains(slot)); - } - - @override - void removeRenderObjectChild(RenderObject child, _ChipSlot slot) { - assert(child is RenderBox); - assert(renderObject.children[slot] == child); - _updateRenderObject(null, slot); - assert(!renderObject.children.keys.contains(slot)); - } - - @override - void moveRenderObjectChild(RenderObject child, Object? oldSlot, Object? newSlot) { - assert(false, 'not reachable'); - } -} - @immutable class _ChipRenderTheme { const _ChipRenderTheme({ @@ -2286,7 +2199,7 @@ class _ChipRenderTheme { } } -class _RenderChip extends RenderBox { +class _RenderChip extends RenderBox with SlottedContainerRenderObjectMixin<_ChipSlot> { _RenderChip({ required _ChipRenderTheme theme, required TextDirection textDirection, @@ -2307,8 +2220,6 @@ class _RenderChip extends RenderBox { enableAnimation.addListener(markNeedsPaint); } - final Map<_ChipSlot, RenderBox> children = <_ChipSlot, RenderBox>{}; - bool? value; bool? isEnabled; late Rect _deleteButtonRect; @@ -2319,35 +2230,9 @@ class _RenderChip extends RenderBox { Animation enableAnimation; ShapeBorder? avatarBorder; - RenderBox? _updateChild(RenderBox? oldChild, RenderBox? newChild, _ChipSlot slot) { - if (oldChild != null) { - dropChild(oldChild); - children.remove(slot); - } - if (newChild != null) { - children[slot] = newChild; - adoptChild(newChild); - } - return newChild; - } - - RenderBox? _avatar; - RenderBox? get avatar => _avatar; - set avatar(RenderBox? value) { - _avatar = _updateChild(_avatar, value, _ChipSlot.avatar); - } - - RenderBox? _deleteIcon; - RenderBox? get deleteIcon => _deleteIcon; - set deleteIcon(RenderBox? value) { - _deleteIcon = _updateChild(_deleteIcon, value, _ChipSlot.deleteIcon); - } - - RenderBox? _label; - RenderBox? get label => _label; - set label(RenderBox? value) { - _label = _updateChild(_label, value, _ChipSlot.label); - } + RenderBox? get avatar => childForSlot(_ChipSlot.avatar); + RenderBox? get deleteIcon => childForSlot(_ChipSlot.deleteIcon); + RenderBox? get label => childForSlot(_ChipSlot.label); _ChipRenderTheme get theme => _theme; _ChipRenderTheme _theme; @@ -2370,7 +2255,8 @@ class _RenderChip extends RenderBox { } // The returned list is ordered for hit testing. - Iterable get _children sync* { + @override + Iterable get children sync* { if (avatar != null) { yield avatar!; } @@ -2385,47 +2271,6 @@ class _RenderChip extends RenderBox { bool get isDrawingCheckmark => theme.showCheckmark && !checkmarkAnimation.isDismissed; bool get deleteIconShowing => !deleteDrawerAnimation.isDismissed; - @override - void attach(PipelineOwner owner) { - super.attach(owner); - for (final RenderBox child in _children) { - child.attach(owner); - } - } - - @override - void detach() { - super.detach(); - for (final RenderBox child in _children) { - child.detach(); - } - } - - @override - void redepthChildren() { - _children.forEach(redepthChild); - } - - @override - void visitChildren(RenderObjectVisitor visitor) { - _children.forEach(visitor); - } - - @override - List debugDescribeChildren() { - final List value = []; - void add(RenderBox? child, String name) { - if (child != null) { - value.add(child.toDiagnosticsNode(name: name)); - } - } - - add(avatar, 'avatar'); - add(label, 'label'); - add(deleteIcon, 'deleteIcon'); - return value; - } - @override bool get sizedByParent => false; diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index ba9d7ece9b6..7e9d5e938ad 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -682,7 +682,7 @@ class _RenderDecorationLayout { } // The workhorse: layout and paint a _Decorator widget's _Decoration. -class _RenderDecoration extends RenderBox { +class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin<_DecorationSlot> { _RenderDecoration({ required _Decoration decoration, required TextDirection textDirection, @@ -702,88 +702,22 @@ class _RenderDecoration extends RenderBox { _expands = expands; static const double subtextGap = 8.0; - final Map<_DecorationSlot, RenderBox> children = <_DecorationSlot, RenderBox>{}; - RenderBox? _updateChild(RenderBox? oldChild, RenderBox? newChild, _DecorationSlot slot) { - if (oldChild != null) { - dropChild(oldChild); - children.remove(slot); - } - if (newChild != null) { - children[slot] = newChild; - adoptChild(newChild); - } - return newChild; - } - - RenderBox? _icon; - RenderBox? get icon => _icon; - set icon(RenderBox? value) { - _icon = _updateChild(_icon, value, _DecorationSlot.icon); - } - - RenderBox? _input; - RenderBox? get input => _input; - set input(RenderBox? value) { - _input = _updateChild(_input, value, _DecorationSlot.input); - } - - RenderBox? _label; - RenderBox? get label => _label; - set label(RenderBox? value) { - _label = _updateChild(_label, value, _DecorationSlot.label); - } - - RenderBox? _hint; - RenderBox? get hint => _hint; - set hint(RenderBox? value) { - _hint = _updateChild(_hint, value, _DecorationSlot.hint); - } - - RenderBox? _prefix; - RenderBox? get prefix => _prefix; - set prefix(RenderBox? value) { - _prefix = _updateChild(_prefix, value, _DecorationSlot.prefix); - } - - RenderBox? _suffix; - RenderBox? get suffix => _suffix; - set suffix(RenderBox? value) { - _suffix = _updateChild(_suffix, value, _DecorationSlot.suffix); - } - - RenderBox? _prefixIcon; - RenderBox? get prefixIcon => _prefixIcon; - set prefixIcon(RenderBox? value) { - _prefixIcon = _updateChild(_prefixIcon, value, _DecorationSlot.prefixIcon); - } - - RenderBox? _suffixIcon; - RenderBox? get suffixIcon => _suffixIcon; - set suffixIcon(RenderBox? value) { - _suffixIcon = _updateChild(_suffixIcon, value, _DecorationSlot.suffixIcon); - } - - RenderBox? _helperError; - RenderBox? get helperError => _helperError; - set helperError(RenderBox? value) { - _helperError = _updateChild(_helperError, value, _DecorationSlot.helperError); - } - - RenderBox? _counter; - RenderBox? get counter => _counter; - set counter(RenderBox? value) { - _counter = _updateChild(_counter, value, _DecorationSlot.counter); - } - - RenderBox? _container; - RenderBox? get container => _container; - set container(RenderBox? value) { - _container = _updateChild(_container, value, _DecorationSlot.container); - } + RenderBox? get icon => childForSlot(_DecorationSlot.icon); + RenderBox? get input => childForSlot(_DecorationSlot.input); + RenderBox? get label => childForSlot(_DecorationSlot.label); + RenderBox? get hint => childForSlot(_DecorationSlot.hint); + RenderBox? get prefix => childForSlot(_DecorationSlot.prefix); + RenderBox? get suffix => childForSlot(_DecorationSlot.suffix); + RenderBox? get prefixIcon => childForSlot(_DecorationSlot.prefixIcon); + RenderBox? get suffixIcon => childForSlot(_DecorationSlot.suffixIcon); + RenderBox? get helperError => childForSlot(_DecorationSlot.helperError); + RenderBox? get counter => childForSlot(_DecorationSlot.counter); + RenderBox? get container => childForSlot(_DecorationSlot.container); // The returned list is ordered for hit testing. - Iterable get _children sync* { + @override + Iterable get children sync* { if (icon != null) yield icon!; if (input != null) @@ -882,30 +816,6 @@ class _RenderDecoration extends RenderBox { return !decoration.isCollapsed && decoration.border!.isOutline; } - @override - void attach(PipelineOwner owner) { - super.attach(owner); - for (final RenderBox child in _children) - child.attach(owner); - } - - @override - void detach() { - super.detach(); - for (final RenderBox child in _children) - child.detach(); - } - - @override - void redepthChildren() { - _children.forEach(redepthChild); - } - - @override - void visitChildren(RenderObjectVisitor visitor) { - _children.forEach(visitor); - } - @override void visitChildrenForSemantics(RenderObjectVisitor visitor) { if (icon != null) @@ -940,27 +850,6 @@ class _RenderDecoration extends RenderBox { visitor(counter!); } - @override - List debugDescribeChildren() { - final List value = []; - void add(RenderBox? child, String name) { - if (child != null) - value.add(child.toDiagnosticsNode(name: name)); - } - add(icon, 'icon'); - add(input, 'input'); - add(label, 'label'); - add(hint, 'hint'); - add(prefix, 'prefix'); - add(suffix, 'suffix'); - add(prefixIcon, 'prefixIcon'); - add(suffixIcon, 'suffixIcon'); - add(helperError, 'helperError'); - add(counter, 'counter'); - add(container, 'container'); - return value; - } - @override bool get sizedByParent => false; @@ -1638,7 +1527,7 @@ class _RenderDecoration extends RenderBox { @override bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { assert(position != null); - for (final RenderBox child in _children) { + for (final RenderBox child in children) { // The label must be handled specially since we've transformed it. final Offset offset = _boxParentData(child).offset; final bool isHit = result.addWithPaintOffset( @@ -1667,146 +1556,7 @@ class _RenderDecoration extends RenderBox { } } -class _DecorationElement extends RenderObjectElement { - _DecorationElement(_Decorator widget) : super(widget); - - final Map<_DecorationSlot, Element> slotToChild = <_DecorationSlot, Element>{}; - - @override - _Decorator get widget => super.widget as _Decorator; - - @override - _RenderDecoration get renderObject => super.renderObject as _RenderDecoration; - - @override - void visitChildren(ElementVisitor visitor) { - slotToChild.values.forEach(visitor); - } - - @override - void forgetChild(Element child) { - assert(slotToChild.containsValue(child)); - assert(child.slot is _DecorationSlot); - assert(slotToChild.containsKey(child.slot)); - slotToChild.remove(child.slot); - super.forgetChild(child); - } - - void _mountChild(Widget? widget, _DecorationSlot slot) { - final Element? oldChild = slotToChild[slot]; - final Element? newChild = updateChild(oldChild, widget, slot); - if (oldChild != null) { - slotToChild.remove(slot); - } - if (newChild != null) { - slotToChild[slot] = newChild; - } - } - - @override - void mount(Element? parent, Object? newSlot) { - super.mount(parent, newSlot); - _mountChild(widget.decoration.icon, _DecorationSlot.icon); - _mountChild(widget.decoration.input, _DecorationSlot.input); - _mountChild(widget.decoration.label, _DecorationSlot.label); - _mountChild(widget.decoration.hint, _DecorationSlot.hint); - _mountChild(widget.decoration.prefix, _DecorationSlot.prefix); - _mountChild(widget.decoration.suffix, _DecorationSlot.suffix); - _mountChild(widget.decoration.prefixIcon, _DecorationSlot.prefixIcon); - _mountChild(widget.decoration.suffixIcon, _DecorationSlot.suffixIcon); - _mountChild(widget.decoration.helperError, _DecorationSlot.helperError); - _mountChild(widget.decoration.counter, _DecorationSlot.counter); - _mountChild(widget.decoration.container, _DecorationSlot.container); - } - - void _updateChild(Widget? widget, _DecorationSlot slot) { - final Element? oldChild = slotToChild[slot]; - final Element? newChild = updateChild(oldChild, widget, slot); - if (oldChild != null) { - slotToChild.remove(slot); - } - if (newChild != null) { - slotToChild[slot] = newChild; - } - } - - @override - void update(_Decorator newWidget) { - super.update(newWidget); - assert(widget == newWidget); - _updateChild(widget.decoration.icon, _DecorationSlot.icon); - _updateChild(widget.decoration.input, _DecorationSlot.input); - _updateChild(widget.decoration.label, _DecorationSlot.label); - _updateChild(widget.decoration.hint, _DecorationSlot.hint); - _updateChild(widget.decoration.prefix, _DecorationSlot.prefix); - _updateChild(widget.decoration.suffix, _DecorationSlot.suffix); - _updateChild(widget.decoration.prefixIcon, _DecorationSlot.prefixIcon); - _updateChild(widget.decoration.suffixIcon, _DecorationSlot.suffixIcon); - _updateChild(widget.decoration.helperError, _DecorationSlot.helperError); - _updateChild(widget.decoration.counter, _DecorationSlot.counter); - _updateChild(widget.decoration.container, _DecorationSlot.container); - } - - void _updateRenderObject(RenderBox? child, _DecorationSlot slot) { - switch (slot) { - case _DecorationSlot.icon: - renderObject.icon = child; - break; - case _DecorationSlot.input: - renderObject.input = child; - break; - case _DecorationSlot.label: - renderObject.label = child; - break; - case _DecorationSlot.hint: - renderObject.hint = child; - break; - case _DecorationSlot.prefix: - renderObject.prefix = child; - break; - case _DecorationSlot.suffix: - renderObject.suffix = child; - break; - case _DecorationSlot.prefixIcon: - renderObject.prefixIcon = child; - break; - case _DecorationSlot.suffixIcon: - renderObject.suffixIcon = child; - break; - case _DecorationSlot.helperError: - renderObject.helperError = child; - break; - case _DecorationSlot.counter: - renderObject.counter = child; - break; - case _DecorationSlot.container: - renderObject.container = child; - break; - } - } - - @override - void insertRenderObjectChild(RenderObject child, _DecorationSlot slot) { - assert(child is RenderBox); - _updateRenderObject(child as RenderBox, slot); - assert(renderObject.children.keys.contains(slot)); - } - - @override - void removeRenderObjectChild(RenderObject child, _DecorationSlot slot) { - assert(child is RenderBox); - assert(renderObject.children[slot] == child); - _updateRenderObject(null, slot); - assert(!renderObject.children.keys.contains(slot)); - } - - @override - void moveRenderObjectChild(RenderObject child, Object? oldSlot, Object? newSlot) { - assert(false, 'not reachable'); - } -} - -class _Decorator extends RenderObjectWidget { +class _Decorator extends RenderObjectWidget with SlottedMultiChildRenderObjectWidgetMixin<_DecorationSlot> { const _Decorator({ Key? key, required this.textAlignVertical, @@ -1829,7 +1579,35 @@ class _Decorator extends RenderObjectWidget { final bool expands; @override - _DecorationElement createElement() => _DecorationElement(this); + Iterable<_DecorationSlot> get slots => _DecorationSlot.values; + + @override + Widget? childForSlot(_DecorationSlot slot) { + switch (slot) { + case _DecorationSlot.icon: + return decoration.icon; + case _DecorationSlot.input: + return decoration.input; + case _DecorationSlot.label: + return decoration.label; + case _DecorationSlot.hint: + return decoration.hint; + case _DecorationSlot.prefix: + return decoration.prefix; + case _DecorationSlot.suffix: + return decoration.suffix; + case _DecorationSlot.prefixIcon: + return decoration.prefixIcon; + case _DecorationSlot.suffixIcon: + return decoration.suffixIcon; + case _DecorationSlot.helperError: + return decoration.helperError; + case _DecorationSlot.counter: + return decoration.counter; + case _DecorationSlot.container: + return decoration.container; + } + } @override _RenderDecoration createRenderObject(BuildContext context) { diff --git a/packages/flutter/lib/src/material/list_tile.dart b/packages/flutter/lib/src/material/list_tile.dart index 1c68b55c9c6..87a3c113019 100644 --- a/packages/flutter/lib/src/material/list_tile.dart +++ b/packages/flutter/lib/src/material/list_tile.dart @@ -1266,7 +1266,7 @@ enum _ListTileSlot { trailing, } -class _ListTile extends RenderObjectWidget { +class _ListTile extends RenderObjectWidget with SlottedMultiChildRenderObjectWidgetMixin<_ListTileSlot> { const _ListTile({ Key? key, this.leading, @@ -1307,7 +1307,21 @@ class _ListTile extends RenderObjectWidget { final double minLeadingWidth; @override - _ListTileElement createElement() => _ListTileElement(this); + Iterable<_ListTileSlot> get slots => _ListTileSlot.values; + + @override + Widget? childForSlot(_ListTileSlot slot) { + switch (slot) { + case _ListTileSlot.leading: + return leading; + case _ListTileSlot.title: + return title; + case _ListTileSlot.subtitle: + return subtitle; + case _ListTileSlot.trailing: + return trailing; + } + } @override _RenderListTile createRenderObject(BuildContext context) { @@ -1339,111 +1353,7 @@ class _ListTile extends RenderObjectWidget { } } -class _ListTileElement extends RenderObjectElement { - _ListTileElement(_ListTile widget) : super(widget); - - final Map<_ListTileSlot, Element> slotToChild = <_ListTileSlot, Element>{}; - - @override - _ListTile get widget => super.widget as _ListTile; - - @override - _RenderListTile get renderObject => super.renderObject as _RenderListTile; - - @override - void visitChildren(ElementVisitor visitor) { - slotToChild.values.forEach(visitor); - } - - @override - void forgetChild(Element child) { - assert(slotToChild.containsValue(child)); - assert(child.slot is _ListTileSlot); - assert(slotToChild.containsKey(child.slot)); - slotToChild.remove(child.slot); - super.forgetChild(child); - } - - void _mountChild(Widget? widget, _ListTileSlot slot) { - final Element? oldChild = slotToChild[slot]; - final Element? newChild = updateChild(oldChild, widget, slot); - if (oldChild != null) { - slotToChild.remove(slot); - } - if (newChild != null) { - slotToChild[slot] = newChild; - } - } - - @override - void mount(Element? parent, Object? newSlot) { - super.mount(parent, newSlot); - _mountChild(widget.leading, _ListTileSlot.leading); - _mountChild(widget.title, _ListTileSlot.title); - _mountChild(widget.subtitle, _ListTileSlot.subtitle); - _mountChild(widget.trailing, _ListTileSlot.trailing); - } - - void _updateChild(Widget? widget, _ListTileSlot slot) { - final Element? oldChild = slotToChild[slot]; - final Element? newChild = updateChild(oldChild, widget, slot); - if (oldChild != null) { - slotToChild.remove(slot); - } - if (newChild != null) { - slotToChild[slot] = newChild; - } - } - - @override - void update(_ListTile newWidget) { - super.update(newWidget); - assert(widget == newWidget); - _updateChild(widget.leading, _ListTileSlot.leading); - _updateChild(widget.title, _ListTileSlot.title); - _updateChild(widget.subtitle, _ListTileSlot.subtitle); - _updateChild(widget.trailing, _ListTileSlot.trailing); - } - - void _updateRenderObject(RenderBox? child, _ListTileSlot slot) { - switch (slot) { - case _ListTileSlot.leading: - renderObject.leading = child; - break; - case _ListTileSlot.title: - renderObject.title = child; - break; - case _ListTileSlot.subtitle: - renderObject.subtitle = child; - break; - case _ListTileSlot.trailing: - renderObject.trailing = child; - break; - } - } - - @override - void insertRenderObjectChild(RenderObject child, _ListTileSlot slot) { - assert(child is RenderBox); - _updateRenderObject(child as RenderBox, slot); - assert(renderObject.children.keys.contains(slot)); - } - - @override - void removeRenderObjectChild(RenderObject child, _ListTileSlot slot) { - assert(child is RenderBox); - assert(renderObject.children[slot] == child); - _updateRenderObject(null, slot); - assert(!renderObject.children.keys.contains(slot)); - } - - @override - void moveRenderObjectChild(RenderObject child, Object? oldSlot, Object? newSlot) { - assert(false, 'not reachable'); - } -} - -class _RenderListTile extends RenderBox { +class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_ListTileSlot> { _RenderListTile({ required bool isDense, required VisualDensity visualDensity, @@ -1472,46 +1382,14 @@ class _RenderListTile extends RenderBox { _minVerticalPadding = minVerticalPadding, _minLeadingWidth = minLeadingWidth; - final Map<_ListTileSlot, RenderBox> children = <_ListTileSlot, RenderBox>{}; - - RenderBox? _updateChild(RenderBox? oldChild, RenderBox? newChild, _ListTileSlot slot) { - if (oldChild != null) { - dropChild(oldChild); - children.remove(slot); - } - if (newChild != null) { - children[slot] = newChild; - adoptChild(newChild); - } - return newChild; - } - - RenderBox? _leading; - RenderBox? get leading => _leading; - set leading(RenderBox? value) { - _leading = _updateChild(_leading, value, _ListTileSlot.leading); - } - - RenderBox? _title; - RenderBox? get title => _title; - set title(RenderBox? value) { - _title = _updateChild(_title, value, _ListTileSlot.title); - } - - RenderBox? _subtitle; - RenderBox? get subtitle => _subtitle; - set subtitle(RenderBox? value) { - _subtitle = _updateChild(_subtitle, value, _ListTileSlot.subtitle); - } - - RenderBox? _trailing; - RenderBox? get trailing => _trailing; - set trailing(RenderBox? value) { - _trailing = _updateChild(_trailing, value, _ListTileSlot.trailing); - } + RenderBox? get leading => childForSlot(_ListTileSlot.leading); + RenderBox? get title => childForSlot(_ListTileSlot.title); + RenderBox? get subtitle => childForSlot(_ListTileSlot.subtitle); + RenderBox? get trailing => childForSlot(_ListTileSlot.trailing); // The returned list is ordered for hit testing. - Iterable get _children sync* { + @override + Iterable get children sync* { if (leading != null) yield leading!; if (title != null) @@ -1615,44 +1493,6 @@ class _RenderListTile extends RenderBox { markNeedsLayout(); } - @override - void attach(PipelineOwner owner) { - super.attach(owner); - for (final RenderBox child in _children) - child.attach(owner); - } - - @override - void detach() { - super.detach(); - for (final RenderBox child in _children) - child.detach(); - } - - @override - void redepthChildren() { - _children.forEach(redepthChild); - } - - @override - void visitChildren(RenderObjectVisitor visitor) { - _children.forEach(visitor); - } - - @override - List debugDescribeChildren() { - final List value = []; - void add(RenderBox? child, String name) { - if (child != null) - value.add(child.toDiagnosticsNode(name: name)); - } - add(leading, 'leading'); - add(title, 'title'); - add(subtitle, 'subtitle'); - add(trailing, 'trailing'); - return value; - } - @override bool get sizedByParent => false; @@ -1905,7 +1745,7 @@ class _RenderListTile extends RenderBox { @override bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { assert(position != null); - for (final RenderBox child in _children) { + for (final RenderBox child in children) { final BoxParentData parentData = child.parentData! as BoxParentData; final bool isHit = result.addWithPaintOffset( offset: parentData.offset, diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index dd4248f1cfc..cf95c132da3 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -3167,6 +3167,11 @@ mixin ContainerParentDataMixin on ParentData { /// parent data. /// /// Moreover, this is a required mixin for render objects returned to [MultiChildRenderObjectWidget]. +/// +/// See also: +/// +/// * [SlottedContainerRenderObjectMixin], which organizes its children +/// in different named slots. mixin ContainerRenderObjectMixin> on RenderObject { bool _debugUltimatePreviousSiblingOf(ChildType child, { ChildType? equals }) { ParentDataType childParentData = child.parentData! as ParentDataType; diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index ed0424f23c0..31cf0d0fb6d 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -1677,6 +1677,13 @@ abstract class InheritedWidget extends ProxyWidget { /// RenderObjectWidgets provide the configuration for [RenderObjectElement]s, /// which wrap [RenderObject]s, which provide the actual rendering of the /// application. +/// +/// See also: +/// +/// * [MultiChildRenderObjectWidget], which configures a [RenderObject] with +/// a single list of children. +/// * [SlottedMultiChildRenderObjectWidgetMixin], which configures a +/// [RenderObject] that organizes its children in different named slots. abstract class RenderObjectWidget extends Widget { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. @@ -1767,7 +1774,11 @@ abstract class SingleChildRenderObjectWidget extends RenderObjectWidget { /// See also: /// /// * [Stack], which uses [MultiChildRenderObjectWidget]. -/// * [RenderStack], for an example implementation of the associated render object. +/// * [RenderStack], for an example implementation of the associated render +/// object. +/// * [SlottedMultiChildRenderObjectWidgetMixin], which configures a +/// [RenderObject] that instead of having a single list of children organizes +/// its children in named slots. abstract class MultiChildRenderObjectWidget extends RenderObjectWidget { /// Initializes fields for subclasses. /// diff --git a/packages/flutter/lib/src/widgets/slotted_render_object_widget.dart b/packages/flutter/lib/src/widgets/slotted_render_object_widget.dart new file mode 100644 index 00000000000..25ae7b4d3b7 --- /dev/null +++ b/packages/flutter/lib/src/widgets/slotted_render_object_widget.dart @@ -0,0 +1,271 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; + +import 'framework.dart'; + +/// A mixin for a [RenderObjectWidget] that configures a [RenderObject] +/// subclass, which organizes its children in different slots. +/// +/// Implementers of this mixin have to provide the list of available slots by +/// overriding [slots]. The list of slots must never change for a given class +/// implementing this mixin. In the common case, [Enum] values are used as slots +/// and [slots] is typically implemented to return the value of the enum's +/// `values` getter. +/// +/// Furthermore, [childForSlot] must be implemented to return the current +/// widget configuration for a given slot. +/// +/// The [RenderObject] returned by [createRenderObject] and updated by +/// [updateRenderObject] must implement the [SlottedContainerRenderObjectMixin]. +/// +/// The type parameter `S` is the type for the slots to be used by this +/// [RenderObjectWidget] and the [RenderObject] it configures. In the typical +/// case, `S` is an [Enum] type. +/// +/// {@tool dartpad} +/// This example uses the [SlottedMultiChildRenderObjectWidgetMixin] in +/// combination with the [SlottedContainerRenderObjectMixin] to implement a +/// widget that provides two slots: topLeft and bottomRight. The widget arranges +/// the children in those slots diagonally. +/// +/// ** See code in examples/api/lib/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0.dart ** +/// {@end-tool} +/// +/// See also: +/// +/// * [MultiChildRenderObjectWidget], which configures a [RenderObject] +/// with a single list of children. +/// * [ListTile], which uses [SlottedMultiChildRenderObjectWidgetMixin] in its +/// internal (private) implementation. +mixin SlottedMultiChildRenderObjectWidgetMixin on RenderObjectWidget { + /// Returns a list of all available slots. + /// + /// The list of slots must be static and must never change for a given class + /// implementing this mixin. + /// + /// Typically, an [Enum] is used to identify the different slots. In that case + /// this getter can be implemented by returning what the `values` getter + /// of the enum used returns. + @protected + Iterable get slots; + + /// Returns the widget that is currently occupying the provided `slot`. + /// + /// The [RenderObject] configured by this class will be configured to have + /// the [RenderObject] produced by the returned [Widget] in the provided + /// `slot`. + @protected + Widget? childForSlot(S slot); + + @override + SlottedContainerRenderObjectMixin createRenderObject(BuildContext context); + + @override + void updateRenderObject(BuildContext context, SlottedContainerRenderObjectMixin renderObject); + + @override + SlottedRenderObjectElement createElement() => SlottedRenderObjectElement(this); +} + +/// Mixin for a [RenderBox] configured by a [SlottedMultiChildRenderObjectWidgetMixin]. +/// +/// The [RenderBox] child currently occupying a given slot can be obtained by +/// calling [childForSlot]. +/// +/// Implementers may consider overriding [children] to return the children +/// of this render object in a consistent order (e.g. hit test order). +/// +/// The type parameter `S` is the type for the slots to be used by this +/// [RenderObject] and the [SlottedMultiChildRenderObjectWidgetMixin] it was +/// configured by. In the typical case, `S` is an [Enum] type. +/// +/// See [SlottedMultiChildRenderObjectWidgetMixin] for example code showcasing +/// how this mixin is used in combination with the +/// [SlottedMultiChildRenderObjectWidgetMixin]. +/// +/// See also: +/// +/// * [ContainerRenderObjectMixin], which organizes its children in a single +/// list. +mixin SlottedContainerRenderObjectMixin on RenderBox { + /// Returns the [RenderBox] child that is currently occupying the provided + /// `slot`. + /// + /// Returns null if no [RenderBox] is configured for the given slot. + @protected + RenderBox? childForSlot(S slot) => _slotToChild[slot]; + + /// Returns an [Iterable] of all non-null children. + /// + /// This getter is used by the default implementation of [attach], [detach], + /// [redepthChildren], [visitChildren], and [debugDescribeChildren] to iterate + /// over the children of this [RenderBox]. The base implementation makes no + /// guarantee about the order in which the children are returned. Subclasses, + /// for which the child order is important should override this getter and + /// return the children in the desired order. + @protected + Iterable get children => _slotToChild.values; + + /// Returns the debug name for a given `slot`. + /// + /// This method is called by [debugDescribeChildren] for each slot that is + /// currently occupied by a child to obtain a name for that slot for debug + /// outputs. + /// + /// The default implementation calls [EnumName.name] on `slot` it it is an + /// [Enum] value and `toString` if it is not. + @protected + String debugNameForSlot(S slot) { + if (slot is Enum) { + return slot.name; + } + return slot.toString(); + } + + @override + void attach(PipelineOwner owner) { + super.attach(owner); + for (final RenderBox child in children) { + child.attach(owner); + } + } + + @override + void detach() { + super.detach(); + for (final RenderBox child in children) { + child.detach(); + } + } + + @override + void redepthChildren() { + children.forEach(redepthChild); + } + + @override + void visitChildren(RenderObjectVisitor visitor) { + children.forEach(visitor); + } + + @override + List debugDescribeChildren() { + final List value = []; + final Map childToSlot = Map.fromIterables( + _slotToChild.values, + _slotToChild.keys, + ); + for (final RenderBox child in children) { + _addDiagnostics(child, value, debugNameForSlot(childToSlot[child] as S)); + } + return value; + } + + void _addDiagnostics(RenderBox child, List value, String name) { + value.add(child.toDiagnosticsNode(name: name)); + } + + final Map _slotToChild = {}; + + void _setChild(RenderBox? child, S slot) { + final RenderBox? oldChild = _slotToChild[slot]; + if (oldChild != null) { + dropChild(oldChild); + _slotToChild.remove(slot); + } + if (child != null) { + _slotToChild[slot] = child; + adoptChild(child); + } + } +} + +/// Element used by the [SlottedMultiChildRenderObjectWidgetMixin]. +class SlottedRenderObjectElement extends RenderObjectElement { + /// Creates an element that uses the given widget as its configuration. + SlottedRenderObjectElement(SlottedMultiChildRenderObjectWidgetMixin widget) : super(widget); + + final Map _slotToChild = {}; + + @override + SlottedMultiChildRenderObjectWidgetMixin get widget => super.widget as SlottedMultiChildRenderObjectWidgetMixin; + + @override + SlottedContainerRenderObjectMixin get renderObject => super.renderObject as SlottedContainerRenderObjectMixin; + + @override + void visitChildren(ElementVisitor visitor) { + _slotToChild.values.forEach(visitor); + } + + @override + void forgetChild(Element child) { + assert(_slotToChild.containsValue(child)); + assert(child.slot is S); + assert(_slotToChild.containsKey(child.slot)); + _slotToChild.remove(child.slot); + super.forgetChild(child); + } + + @override + void mount(Element? parent, Object? newSlot) { + super.mount(parent, newSlot); + _updateChildren(); + } + + @override + void update(SlottedMultiChildRenderObjectWidgetMixin newWidget) { + super.update(newWidget); + assert(widget == newWidget); + _updateChildren(); + } + + List? _debugPreviousSlots; + + void _updateChildren() { + assert(() { + _debugPreviousSlots ??= widget.slots.toList(); + return listEquals(_debugPreviousSlots, widget.slots.toList()); + }(), '${widget.runtimeType}.slots must not change.'); + assert(widget.slots.toSet().length == widget.slots.length, 'slots must be unique'); + + for (final S slot in widget.slots) { + _updateChild(widget.childForSlot(slot), slot); + } + } + + void _updateChild(Widget? widget, S slot) { + final Element? oldChild = _slotToChild[slot]; + assert(oldChild == null || oldChild.slot == slot); // Reason why [moveRenderObjectChild] is not reachable. + final Element? newChild = updateChild(oldChild, widget, slot); + if (oldChild != null) { + _slotToChild.remove(slot); + } + if (newChild != null) { + _slotToChild[slot] = newChild; + } + } + + @override + void insertRenderObjectChild(RenderBox child, S slot) { + renderObject._setChild(child, slot); + assert(renderObject._slotToChild[slot] == child); + } + + @override + void removeRenderObjectChild(RenderBox child, S slot) { + assert(renderObject._slotToChild[slot] == child); + renderObject._setChild(null, slot); + assert(renderObject._slotToChild[slot] == null); + } + + @override + void moveRenderObjectChild(RenderBox child, Object? oldSlot, Object? newSlot) { + // Existing elements are never moved to a new slot, see assert in [_updateChild]. + assert(false, 'not reachable'); + } +} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index c424d6e9e30..62e702e79db 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -116,6 +116,7 @@ export 'src/widgets/sliver_fill.dart'; export 'src/widgets/sliver_layout_builder.dart'; export 'src/widgets/sliver_persistent_header.dart'; export 'src/widgets/sliver_prototype_extent_list.dart'; +export 'src/widgets/slotted_render_object_widget.dart'; export 'src/widgets/spacer.dart'; export 'src/widgets/status_transitions.dart'; export 'src/widgets/table.dart'; diff --git a/packages/flutter/test/material/chip_test.dart b/packages/flutter/test/material/chip_test.dart index 7362fde6854..60fd121fde3 100644 --- a/packages/flutter/test/material/chip_test.dart +++ b/packages/flutter/test/material/chip_test.dart @@ -15,7 +15,7 @@ import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; Finder findRenderChipElement() { - return find.byElementPredicate((Element e) => '${e.runtimeType}' == '_RenderChipElement'); + return find.byElementPredicate((Element e) => '${e.renderObject.runtimeType}' == '_RenderChip'); } RenderBox getMaterialBox(WidgetTester tester) { diff --git a/packages/flutter/test/widgets/slotted_render_object_widget_test.dart b/packages/flutter/test/widgets/slotted_render_object_widget_test.dart new file mode 100644 index 00000000000..7da4ae45a00 --- /dev/null +++ b/packages/flutter/test/widgets/slotted_render_object_widget_test.dart @@ -0,0 +1,292 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../rendering/mock_canvas.dart'; + +const Color green = Color(0xFF00FF00); +const Color yellow = Color(0xFFFFFF00); + +void main() { + testWidgets('SlottedRenderObjectWidget test', (WidgetTester tester) async { + await tester.pumpWidget(buildWidget( + topLeft: Container( + height: 100, + width: 80, + color: yellow, + child: const Text('topLeft'), + ), + bottomRight: Container( + height: 120, + width: 110, + color: green, + child: const Text('bottomRight'), + ), + )); + + expect(find.text('topLeft'), findsOneWidget); + expect(find.text('bottomRight'), findsOneWidget); + expect(tester.getSize(find.byType(_Diagonal)), const Size(80 + 110, 100 + 120)); + expect(find.byType(_Diagonal), paints + ..rect( + rect: const Rect.fromLTWH(0, 0, 80, 100), + color: yellow, + ) + ..rect( + rect: const Rect.fromLTWH(80, 100, 110, 120), + color: green, + ) + ); + + await tester.pumpWidget(buildWidget( + topLeft: Container( + height: 200, + width: 100, + color: yellow, + child: const Text('topLeft'), + ), + bottomRight: Container( + height: 220, + width: 210, + color: green, + child: const Text('bottomRight'), + ), + )); + + expect(find.text('topLeft'), findsOneWidget); + expect(find.text('bottomRight'), findsOneWidget); + expect(tester.getSize(find.byType(_Diagonal)), const Size(100 + 210, 200 + 220)); + expect(find.byType(_Diagonal), paints + ..rect( + rect: const Rect.fromLTWH(0, 0, 100, 200), + color: yellow, + ) + ..rect( + rect: const Rect.fromLTWH(100, 200, 210, 220), + color: green, + ) + ); + + await tester.pumpWidget(buildWidget( + topLeft: Container( + height: 200, + width: 100, + color: yellow, + child: const Text('topLeft'), + ), + bottomRight: Container( + key: UniqueKey(), + height: 230, + width: 220, + color: green, + child: const Text('bottomRight'), + ), + )); + + expect(find.text('topLeft'), findsOneWidget); + expect(find.text('bottomRight'), findsOneWidget); + expect(tester.getSize(find.byType(_Diagonal)), const Size(100 + 220, 200 + 230)); + expect(find.byType(_Diagonal), paints + ..rect( + rect: const Rect.fromLTWH(0, 0, 100, 200), + color: yellow, + ) + ..rect( + rect: const Rect.fromLTWH(100, 200, 220, 230), + color: green, + ) + ); + + await tester.pumpWidget(buildWidget( + topLeft: Container( + height: 200, + width: 100, + color: yellow, + child: const Text('topLeft'), + ), + )); + + expect(find.text('topLeft'), findsOneWidget); + expect(find.text('bottomRight'), findsNothing); + expect(tester.getSize(find.byType(_Diagonal)), const Size(100, 200)); + expect(find.byType(_Diagonal), paints + ..rect( + rect: const Rect.fromLTWH(0, 0, 100, 200), + color: yellow, + ) + ); + + await tester.pumpWidget(buildWidget()); + expect(find.text('topLeft'), findsNothing); + expect(find.text('bottomRight'), findsNothing); + expect(tester.getSize(find.byType(_Diagonal)), Size.zero); + expect(find.byType(_Diagonal), paintsNothing); + + await tester.pumpWidget(Container()); + expect(find.byType(_Diagonal), findsNothing); + }); + + test('nameForSlot', () { + expect(_RenderDiagonal().publicNameForSlot(_DiagonalSlot.bottomRight), 'bottomRight'); + expect(_RenderDiagonal().publicNameForSlot(_DiagonalSlot.topLeft), 'topLeft'); + final _Slot slot = _Slot(); + expect(_RenderTest().publicNameForSlot(slot), slot.toString()); + }); + + testWidgets('debugDescribeChildren', (WidgetTester tester) async { + await tester.pumpWidget(buildWidget( + topLeft: const SizedBox( + height: 100, + width: 80, + ), + bottomRight: const SizedBox( + height: 120, + width: 110, + ), + )); + + expect( + tester.renderObject(find.byType(_Diagonal)).toStringDeep(), + equalsIgnoringHashCodes(r''' +_RenderDiagonal#00000 relayoutBoundary=up1 + │ creator: _Diagonal ← Align ← Directionality ← [root] + │ parentData: offset=Offset(0.0, 0.0) (can use size) + │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0) + │ size: Size(190.0, 220.0) + │ + ├─topLeft: RenderConstrainedBox#00000 relayoutBoundary=up2 + │ creator: SizedBox ← _Diagonal ← Align ← Directionality ← [root] + │ parentData: offset=Offset(0.0, 0.0) (can use size) + │ constraints: BoxConstraints(unconstrained) + │ size: Size(80.0, 100.0) + │ additionalConstraints: BoxConstraints(w=80.0, h=100.0) + │ + └─bottomRight: RenderConstrainedBox#00000 relayoutBoundary=up2 + creator: SizedBox ← _Diagonal ← Align ← Directionality ← [root] + parentData: offset=Offset(80.0, 100.0) (can use size) + constraints: BoxConstraints(unconstrained) + size: Size(110.0, 120.0) + additionalConstraints: BoxConstraints(w=110.0, h=120.0) +''') + ); + }); +} + +Widget buildWidget({Widget? topLeft, Widget? bottomRight}) { + return Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: _Diagonal( + topLeft: topLeft, + bottomRight: bottomRight, + ), + ), + ); +} + +enum _DiagonalSlot { + topLeft, + bottomRight, +} + +class _Diagonal extends RenderObjectWidget with SlottedMultiChildRenderObjectWidgetMixin<_DiagonalSlot> { + const _Diagonal({ + Key? key, + this.topLeft, + this.bottomRight, + this.backgroundColor, + }) : super(key: key); + + final Widget? topLeft; + final Widget? bottomRight; + final Color? backgroundColor; + + @override + Iterable<_DiagonalSlot> get slots => _DiagonalSlot.values; + + @override + Widget? childForSlot(Object slot) { + switch (slot) { + case _DiagonalSlot.topLeft: + return topLeft; + case _DiagonalSlot.bottomRight: + return bottomRight; + } + } + + @override + SlottedContainerRenderObjectMixin<_DiagonalSlot> createRenderObject( + BuildContext context, + ) { + return _RenderDiagonal(); + } +} + +class _RenderDiagonal extends RenderBox with SlottedContainerRenderObjectMixin<_DiagonalSlot> { + RenderBox? get _topLeft => childForSlot(_DiagonalSlot.topLeft); + RenderBox? get _bottomRight => childForSlot(_DiagonalSlot.bottomRight); + + @override + void performLayout() { + const BoxConstraints childConstraints = BoxConstraints(); + + Size topLeftSize = Size.zero; + if (_topLeft != null) { + _topLeft!.layout(childConstraints, parentUsesSize: true); + _positionChild(_topLeft!, Offset.zero); + topLeftSize = _topLeft!.size; + } + + Size bottomRightSize = Size.zero; + if (_bottomRight != null) { + _bottomRight!.layout(childConstraints, parentUsesSize: true); + _positionChild( + _bottomRight!, + Offset(topLeftSize.width, topLeftSize.height), + ); + bottomRightSize = _bottomRight!.size; + } + + size = constraints.constrain(Size( + topLeftSize.width + bottomRightSize.width, + topLeftSize.height + bottomRightSize.height, + )); + } + + void _positionChild(RenderBox child, Offset offset) { + (child.parentData! as BoxParentData).offset = offset; + } + + @override + void paint(PaintingContext context, Offset offset) { + if (_topLeft != null) { + _paintChild(_topLeft!, context, offset); + } + if (_bottomRight != null) { + _paintChild(_bottomRight!, context, offset); + } + } + + void _paintChild(RenderBox child, PaintingContext context, Offset offset) { + final BoxParentData childParentData = child.parentData! as BoxParentData; + context.paintChild(child, childParentData.offset + offset); + } + + String publicNameForSlot(_DiagonalSlot slot) => debugNameForSlot(slot); +} + +class _Slot { + @override + String toString() => describeIdentity(this); +} + +class _RenderTest extends RenderBox with SlottedContainerRenderObjectMixin<_Slot> { + String publicNameForSlot(_Slot slot) => debugNameForSlot(slot); +}