blob: 1318c046ce3a2359f5c4d55f1a1d95e442b54f9f [file] [log] [blame] [edit]
// 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);
}
}