// 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 superclass for [RenderObjectWidget]s that configure [RenderObject] /// subclasses that organize their 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 [SlottedContainerRenderObjectMixin]. /// /// The type parameter `SlotType` is the type for the slots to be used by this /// [RenderObjectWidget] and the [RenderObject] it configures. In the typical /// case, `SlotType` is an [Enum] type. /// /// The type parameter `ChildType` is the type used for the [RenderObject] children /// (e.g. [RenderBox] or [RenderSliver]). In the typical case, `ChildType` is /// [RenderBox]. This class does not support having different kinds of children /// for different slots. /// /// {@tool dartpad} /// This example uses the [SlottedMultiChildRenderObjectWidget] 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 [SlottedMultiChildRenderObjectWidget] in its /// internal (private) implementation. abstract class SlottedMultiChildRenderObjectWidget<SlotType, ChildType extends RenderObject> extends RenderObjectWidget with SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType> { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. const SlottedMultiChildRenderObjectWidget({ super.key }); } /// A mixin version of [SlottedMultiChildRenderObjectWidget]. /// /// This mixin provides the same logic as extending /// [SlottedMultiChildRenderObjectWidget] directly. /// /// It was deprecated to simplify the process of creating slotted widgets. @Deprecated( 'Extend SlottedMultiChildRenderObjectWidget instead of mixing in SlottedMultiChildRenderObjectWidgetMixin. ' 'This feature was deprecated after v3.10.0-1.5.pre.' ) mixin SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType extends RenderObject> 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<SlotType> 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(SlotType slot); @override SlottedContainerRenderObjectMixin<SlotType, ChildType> createRenderObject(BuildContext context); @override void updateRenderObject(BuildContext context, SlottedContainerRenderObjectMixin<SlotType, ChildType> renderObject); @override SlottedRenderObjectElement<SlotType, ChildType> createElement() => SlottedRenderObjectElement<SlotType, ChildType>(this); } /// Mixin for a [RenderObject] configured by a [SlottedMultiChildRenderObjectWidget]. /// /// The [RenderObject] 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 `SlotType` is the type for the slots to be used by this /// [RenderObject] and the [SlottedMultiChildRenderObjectWidget] it was /// configured by. In the typical case, `SlotType` is an [Enum] type. /// /// The type parameter `ChildType` is the type of [RenderObject] used for the children /// (e.g. [RenderBox] or [RenderSliver]). In the typical case, `ChildType` is /// [RenderBox]. This mixin does not support having different kinds of children /// for different slots. /// /// See [SlottedMultiChildRenderObjectWidget] for example code showcasing how /// this mixin is used in combination with [SlottedMultiChildRenderObjectWidget]. /// /// See also: /// /// * [ContainerRenderObjectMixin], which organizes its children in a single /// list. mixin SlottedContainerRenderObjectMixin<SlotType, ChildType extends RenderObject> on RenderObject { /// Returns the [RenderObject] child that is currently occupying the provided /// `slot`. /// /// Returns null if no [RenderObject] is configured for the given slot. @protected ChildType? childForSlot(SlotType 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 [RenderObject]. 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<ChildType> 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` if it is an /// [Enum] value and `toString` if it is not. @protected String debugNameForSlot(SlotType slot) { if (slot is Enum) { return slot.name; } return slot.toString(); } @override void attach(PipelineOwner owner) { super.attach(owner); for (final ChildType child in children) { child.attach(owner); } } @override void detach() { super.detach(); for (final ChildType child in children) { child.detach(); } } @override void redepthChildren() { children.forEach(redepthChild); } @override void visitChildren(RenderObjectVisitor visitor) { children.forEach(visitor); } @override List<DiagnosticsNode> debugDescribeChildren() { final List<DiagnosticsNode> value = <DiagnosticsNode>[]; final Map<ChildType, SlotType> childToSlot = Map<ChildType, SlotType>.fromIterables( _slotToChild.values, _slotToChild.keys, ); for (final ChildType child in children) { _addDiagnostics(child, value, debugNameForSlot(childToSlot[child] as SlotType)); } return value; } void _addDiagnostics(ChildType child, List<DiagnosticsNode> value, String name) { value.add(child.toDiagnosticsNode(name: name)); } final Map<SlotType, ChildType> _slotToChild = <SlotType, ChildType>{}; void _setChild(ChildType? child, SlotType slot) { final ChildType? oldChild = _slotToChild[slot]; if (oldChild != null) { dropChild(oldChild); _slotToChild.remove(slot); } if (child != null) { _slotToChild[slot] = child; adoptChild(child); } } void _moveChild(ChildType child, SlotType slot, SlotType oldSlot) { assert(slot != oldSlot); final ChildType? oldChild = _slotToChild[oldSlot]; if (oldChild == child) { _setChild(null, oldSlot); } _setChild(child, slot); } } /// Element used by the [SlottedMultiChildRenderObjectWidget]. class SlottedRenderObjectElement<SlotType, ChildType extends RenderObject> extends RenderObjectElement { /// Creates an element that uses the given widget as its configuration. SlottedRenderObjectElement(SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType> super.widget); Map<SlotType, Element> _slotToChild = <SlotType, Element>{}; Map<Key, Element> _keyedChildren = <Key, Element>{}; @override SlottedContainerRenderObjectMixin<SlotType, ChildType> get renderObject => super.renderObject as SlottedContainerRenderObjectMixin<SlotType, ChildType>; @override void visitChildren(ElementVisitor visitor) { _slotToChild.values.forEach(visitor); } @override void forgetChild(Element child) { assert(_slotToChild.containsValue(child)); assert(child.slot is SlotType); 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<SlotType, ChildType> newWidget) { super.update(newWidget); assert(widget == newWidget); _updateChildren(); } List<SlotType>? _debugPreviousSlots; void _updateChildren() { final SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType> slottedMultiChildRenderObjectWidgetMixin = widget as SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType>; assert(() { _debugPreviousSlots ??= slottedMultiChildRenderObjectWidgetMixin.slots.toList(); return listEquals(_debugPreviousSlots, slottedMultiChildRenderObjectWidgetMixin.slots.toList()); }(), '${widget.runtimeType}.slots must not change.'); assert(slottedMultiChildRenderObjectWidgetMixin.slots.toSet().length == slottedMultiChildRenderObjectWidgetMixin.slots.length, 'slots must be unique'); final Map<Key, Element> oldKeyedElements = _keyedChildren; _keyedChildren = <Key, Element>{}; final Map<SlotType, Element> oldSlotToChild = _slotToChild; _slotToChild = <SlotType, Element>{}; Map<Key, List<Element>>? debugDuplicateKeys; for (final SlotType slot in slottedMultiChildRenderObjectWidgetMixin.slots) { final Widget? widget = slottedMultiChildRenderObjectWidgetMixin.childForSlot(slot); final Key? newWidgetKey = widget?.key; final Element? oldSlotChild = oldSlotToChild[slot]; final Element? oldKeyChild = oldKeyedElements[newWidgetKey]; // Try to find the slot for the correct Element that `widget` should update. // If key matching fails, resort to `oldSlotChild` from the same slot. final Element? fromElement; if (oldKeyChild != null) { fromElement = oldSlotToChild.remove(oldKeyChild.slot as SlotType); } else if (oldSlotChild?.widget.key == null) { fromElement = oldSlotToChild.remove(slot); } else { // The only case we can't use `oldSlotChild` is when its widget has a key. assert(oldSlotChild!.widget.key != newWidgetKey); fromElement = null; } final Element? newChild = updateChild(fromElement, widget, slot); if (newChild != null) { _slotToChild[slot] = newChild; if (newWidgetKey != null) { assert(() { final Element? existingElement = _keyedChildren[newWidgetKey]; if (existingElement != null) { (debugDuplicateKeys ??= <Key, List<Element>>{}) .putIfAbsent(newWidgetKey, () => <Element>[existingElement]) .add(newChild); } return true; }()); _keyedChildren[newWidgetKey] = newChild; } } } oldSlotToChild.values.forEach(deactivateChild); assert(_debugDuplicateKeys(debugDuplicateKeys)); assert(_keyedChildren.values.every(_slotToChild.values.contains), '_keyedChildren ${_keyedChildren.values} should be a subset of ${_slotToChild.values}'); } bool _debugDuplicateKeys(Map<Key, List<Element>>? debugDuplicateKeys) { if (debugDuplicateKeys == null) { return true; } for (final MapEntry<Key, List<Element>> duplicateKey in debugDuplicateKeys.entries) { throw FlutterError.fromParts(<DiagnosticsNode>[ ErrorSummary('Multiple widgets used the same key in ${widget.runtimeType}.'), ErrorDescription( 'The key ${duplicateKey.key} was used by multiple widgets. The offending widgets were:\n' ), for (final Element element in duplicateKey.value) ErrorDescription(' - $element\n'), ErrorDescription( 'A key can only be specified on one widget at a time in the same parent widget.', ), ]); } return true; } @override void insertRenderObjectChild(ChildType child, SlotType slot) { renderObject._setChild(child, slot); assert(renderObject._slotToChild[slot] == child); } @override void removeRenderObjectChild(ChildType child, SlotType slot) { if (renderObject._slotToChild[slot] == child) { renderObject._setChild(null, slot); assert(renderObject._slotToChild[slot] == null); } } @override void moveRenderObjectChild(ChildType child, SlotType oldSlot, SlotType newSlot) { renderObject._moveChild(child, newSlot, oldSlot); } }