1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:flutter/material.dart';
6library;
7
8import 'package:flutter/foundation.dart';
9import 'package:flutter/rendering.dart';
10
11import 'framework.dart';
12
13/// A superclass for [RenderObjectWidget]s that configure [RenderObject]
14/// subclasses that organize their children in different slots.
15///
16/// Implementers of this mixin have to provide the list of available slots by
17/// overriding [slots]. The list of slots must never change for a given class
18/// implementing this mixin. In the common case, [Enum] values are used as slots
19/// and [slots] is typically implemented to return the value of the enum's
20/// `values` getter.
21///
22/// Furthermore, [childForSlot] must be implemented to return the current
23/// widget configuration for a given slot.
24///
25/// The [RenderObject] returned by [createRenderObject] and updated by
26/// [updateRenderObject] must implement [SlottedContainerRenderObjectMixin].
27///
28/// The type parameter `SlotType` is the type for the slots to be used by this
29/// [RenderObjectWidget] and the [RenderObject] it configures. In the typical
30/// case, `SlotType` is an [Enum] type.
31///
32/// The type parameter `ChildType` is the type used for the [RenderObject] children
33/// (e.g. [RenderBox] or [RenderSliver]). In the typical case, `ChildType` is
34/// [RenderBox]. This class does not support having different kinds of children
35/// for different slots.
36///
37/// {@tool dartpad}
38/// This example uses the [SlottedMultiChildRenderObjectWidget] in
39/// combination with the [SlottedContainerRenderObjectMixin] to implement a
40/// widget that provides two slots: topLeft and bottomRight. The widget arranges
41/// the children in those slots diagonally.
42///
43/// ** See code in examples/api/lib/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0.dart **
44/// {@end-tool}
45///
46/// See also:
47///
48/// * [MultiChildRenderObjectWidget], which configures a [RenderObject]
49/// with a single list of children.
50/// * [ListTile], which uses [SlottedMultiChildRenderObjectWidget] in its
51/// internal (private) implementation.
52abstract class SlottedMultiChildRenderObjectWidget<SlotType, ChildType extends RenderObject>
53 extends RenderObjectWidget
54 with SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType> {
55 /// Abstract const constructor. This constructor enables subclasses to provide
56 /// const constructors so that they can be used in const expressions.
57 const SlottedMultiChildRenderObjectWidget({super.key});
58}
59
60/// A mixin version of [SlottedMultiChildRenderObjectWidget].
61///
62/// This mixin provides the same logic as extending
63/// [SlottedMultiChildRenderObjectWidget] directly.
64///
65/// It was deprecated to simplify the process of creating slotted widgets.
66@Deprecated(
67 'Extend SlottedMultiChildRenderObjectWidget instead of mixing in SlottedMultiChildRenderObjectWidgetMixin. '
68 'This feature was deprecated after v3.10.0-1.5.pre.',
69)
70mixin SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType extends RenderObject>
71 on RenderObjectWidget {
72 /// Returns a list of all available slots.
73 ///
74 /// The list of slots must be static and must never change for a given class
75 /// implementing this mixin.
76 ///
77 /// Typically, an [Enum] is used to identify the different slots. In that case
78 /// this getter can be implemented by returning what the `values` getter
79 /// of the enum used returns.
80 @protected
81 Iterable<SlotType> get slots;
82
83 /// Returns the widget that is currently occupying the provided `slot`.
84 ///
85 /// The [RenderObject] configured by this class will be configured to have
86 /// the [RenderObject] produced by the returned [Widget] in the provided
87 /// `slot`.
88 @protected
89 Widget? childForSlot(SlotType slot);
90
91 @override
92 SlottedContainerRenderObjectMixin<SlotType, ChildType> createRenderObject(BuildContext context);
93
94 @override
95 void updateRenderObject(
96 BuildContext context,
97 SlottedContainerRenderObjectMixin<SlotType, ChildType> renderObject,
98 );
99
100 @override
101 SlottedRenderObjectElement<SlotType, ChildType> createElement() =>
102 SlottedRenderObjectElement<SlotType, ChildType>(this);
103}
104
105/// Mixin for a [RenderObject] configured by a [SlottedMultiChildRenderObjectWidget].
106///
107/// The [RenderObject] child currently occupying a given slot can be obtained by
108/// calling [childForSlot].
109///
110/// Implementers may consider overriding [children] to return the children
111/// of this render object in a consistent order (e.g. hit test order).
112///
113/// The type parameter `SlotType` is the type for the slots to be used by this
114/// [RenderObject] and the [SlottedMultiChildRenderObjectWidget] it was
115/// configured by. In the typical case, `SlotType` is an [Enum] type.
116///
117/// The type parameter `ChildType` is the type of [RenderObject] used for the children
118/// (e.g. [RenderBox] or [RenderSliver]). In the typical case, `ChildType` is
119/// [RenderBox]. This mixin does not support having different kinds of children
120/// for different slots.
121///
122/// See [SlottedMultiChildRenderObjectWidget] for example code showcasing how
123/// this mixin is used in combination with [SlottedMultiChildRenderObjectWidget].
124///
125/// See also:
126///
127/// * [ContainerRenderObjectMixin], which organizes its children in a single
128/// list.
129mixin SlottedContainerRenderObjectMixin<SlotType, ChildType extends RenderObject> on RenderObject {
130 /// Returns the [RenderObject] child that is currently occupying the provided
131 /// `slot`.
132 ///
133 /// Returns null if no [RenderObject] is configured for the given slot.
134 @protected
135 ChildType? childForSlot(SlotType slot) => _slotToChild[slot];
136
137 /// Returns an [Iterable] of all non-null children.
138 ///
139 /// This getter is used by the default implementation of [attach], [detach],
140 /// [redepthChildren], [visitChildren], and [debugDescribeChildren] to iterate
141 /// over the children of this [RenderObject]. The base implementation makes no
142 /// guarantee about the order in which the children are returned. Subclasses
143 /// for which the child order is important should override this getter and
144 /// return the children in the desired order.
145 @protected
146 Iterable<ChildType> get children => _slotToChild.values;
147
148 /// Returns the debug name for a given `slot`.
149 ///
150 /// This method is called by [debugDescribeChildren] for each slot that is
151 /// currently occupied by a child to obtain a name for that slot for debug
152 /// outputs.
153 ///
154 /// The default implementation calls [EnumName.name] on `slot` if it is an
155 /// [Enum] value and `toString` if it is not.
156 @protected
157 String debugNameForSlot(SlotType slot) {
158 if (slot is Enum) {
159 return slot.name;
160 }
161 return slot.toString();
162 }
163
164 @override
165 void attach(PipelineOwner owner) {
166 super.attach(owner);
167 for (final ChildType child in children) {
168 child.attach(owner);
169 }
170 }
171
172 @override
173 void detach() {
174 super.detach();
175 for (final ChildType child in children) {
176 child.detach();
177 }
178 }
179
180 @override
181 void redepthChildren() {
182 children.forEach(redepthChild);
183 }
184
185 @override
186 void visitChildren(RenderObjectVisitor visitor) {
187 children.forEach(visitor);
188 }
189
190 @override
191 List<DiagnosticsNode> debugDescribeChildren() {
192 final List<DiagnosticsNode> value = <DiagnosticsNode>[];
193 final Map<ChildType, SlotType> childToSlot = Map<ChildType, SlotType>.fromIterables(
194 _slotToChild.values,
195 _slotToChild.keys,
196 );
197 for (final ChildType child in children) {
198 _addDiagnostics(child, value, debugNameForSlot(childToSlot[child] as SlotType));
199 }
200 return value;
201 }
202
203 void _addDiagnostics(ChildType child, List<DiagnosticsNode> value, String name) {
204 value.add(child.toDiagnosticsNode(name: name));
205 }
206
207 final Map<SlotType, ChildType> _slotToChild = <SlotType, ChildType>{};
208
209 void _setChild(ChildType? child, SlotType slot) {
210 final ChildType? oldChild = _slotToChild[slot];
211 if (oldChild != null) {
212 dropChild(oldChild);
213 _slotToChild.remove(slot);
214 }
215 if (child != null) {
216 _slotToChild[slot] = child;
217 adoptChild(child);
218 }
219 }
220
221 void _moveChild(ChildType child, SlotType slot, SlotType oldSlot) {
222 assert(slot != oldSlot);
223 final ChildType? oldChild = _slotToChild[oldSlot];
224 if (oldChild == child) {
225 _setChild(null, oldSlot);
226 }
227 _setChild(child, slot);
228 }
229}
230
231/// Element used by the [SlottedMultiChildRenderObjectWidget].
232class SlottedRenderObjectElement<SlotType, ChildType extends RenderObject>
233 extends RenderObjectElement {
234 /// Creates an element that uses the given widget as its configuration.
235 SlottedRenderObjectElement(
236 SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType> super.widget,
237 );
238
239 Map<SlotType, Element> _slotToChild = <SlotType, Element>{};
240 Map<Key, Element> _keyedChildren = <Key, Element>{};
241
242 @override
243 SlottedContainerRenderObjectMixin<SlotType, ChildType> get renderObject =>
244 super.renderObject as SlottedContainerRenderObjectMixin<SlotType, ChildType>;
245
246 @override
247 void visitChildren(ElementVisitor visitor) {
248 _slotToChild.values.forEach(visitor);
249 }
250
251 @override
252 void forgetChild(Element child) {
253 assert(_slotToChild.containsValue(child));
254 assert(child.slot is SlotType);
255 assert(_slotToChild.containsKey(child.slot));
256 _slotToChild.remove(child.slot);
257 super.forgetChild(child);
258 }
259
260 @override
261 void mount(Element? parent, Object? newSlot) {
262 super.mount(parent, newSlot);
263 _updateChildren();
264 }
265
266 @override
267 void update(SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType> newWidget) {
268 super.update(newWidget);
269 assert(widget == newWidget);
270 _updateChildren();
271 }
272
273 List<SlotType>? _debugPreviousSlots;
274
275 void _updateChildren() {
276 final SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType>
277 slottedMultiChildRenderObjectWidgetMixin =
278 widget as SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType>;
279 assert(() {
280 _debugPreviousSlots ??= slottedMultiChildRenderObjectWidgetMixin.slots.toList();
281 return listEquals(
282 _debugPreviousSlots,
283 slottedMultiChildRenderObjectWidgetMixin.slots.toList(),
284 );
285 }(), '${widget.runtimeType}.slots must not change.');
286 assert(
287 slottedMultiChildRenderObjectWidgetMixin.slots.toSet().length ==
288 slottedMultiChildRenderObjectWidgetMixin.slots.length,
289 'slots must be unique',
290 );
291
292 final Map<Key, Element> oldKeyedElements = _keyedChildren;
293 _keyedChildren = <Key, Element>{};
294 final Map<SlotType, Element> oldSlotToChild = _slotToChild;
295 _slotToChild = <SlotType, Element>{};
296
297 Map<Key, List<Element>>? debugDuplicateKeys;
298
299 for (final SlotType slot in slottedMultiChildRenderObjectWidgetMixin.slots) {
300 final Widget? widget = slottedMultiChildRenderObjectWidgetMixin.childForSlot(slot);
301 final Key? newWidgetKey = widget?.key;
302
303 final Element? oldSlotChild = oldSlotToChild[slot];
304 final Element? oldKeyChild = oldKeyedElements[newWidgetKey];
305
306 // Try to find the slot for the correct Element that `widget` should update.
307 // If key matching fails, resort to `oldSlotChild` from the same slot.
308 final Element? fromElement;
309 if (oldKeyChild != null) {
310 fromElement = oldSlotToChild.remove(oldKeyChild.slot as SlotType);
311 } else if (oldSlotChild?.widget.key == null) {
312 fromElement = oldSlotToChild.remove(slot);
313 } else {
314 // The only case we can't use `oldSlotChild` is when its widget has a key.
315 assert(oldSlotChild!.widget.key != newWidgetKey);
316 fromElement = null;
317 }
318 final Element? newChild = updateChild(fromElement, widget, slot);
319
320 if (newChild != null) {
321 _slotToChild[slot] = newChild;
322
323 if (newWidgetKey != null) {
324 assert(() {
325 final Element? existingElement = _keyedChildren[newWidgetKey];
326 if (existingElement != null) {
327 (debugDuplicateKeys ??= <Key, List<Element>>{})
328 .putIfAbsent(newWidgetKey, () => <Element>[existingElement])
329 .add(newChild);
330 }
331 return true;
332 }());
333 _keyedChildren[newWidgetKey] = newChild;
334 }
335 }
336 }
337 oldSlotToChild.values.forEach(deactivateChild);
338 assert(_debugDuplicateKeys(debugDuplicateKeys));
339 assert(
340 _keyedChildren.values.every(_slotToChild.values.contains),
341 '_keyedChildren ${_keyedChildren.values} should be a subset of ${_slotToChild.values}',
342 );
343 }
344
345 bool _debugDuplicateKeys(Map<Key, List<Element>>? debugDuplicateKeys) {
346 if (debugDuplicateKeys == null) {
347 return true;
348 }
349 for (final MapEntry<Key, List<Element>> duplicateKey in debugDuplicateKeys.entries) {
350 throw FlutterError.fromParts(<DiagnosticsNode>[
351 ErrorSummary('Multiple widgets used the same key in ${widget.runtimeType}.'),
352 ErrorDescription(
353 'The key ${duplicateKey.key} was used by multiple widgets. The offending widgets were:\n',
354 ),
355 for (final Element element in duplicateKey.value) ErrorDescription(' - $element\n'),
356 ErrorDescription(
357 'A key can only be specified on one widget at a time in the same parent widget.',
358 ),
359 ]);
360 }
361 return true;
362 }
363
364 @override
365 void insertRenderObjectChild(ChildType child, SlotType slot) {
366 renderObject._setChild(child, slot);
367 assert(renderObject._slotToChild[slot] == child);
368 }
369
370 @override
371 void removeRenderObjectChild(ChildType child, SlotType slot) {
372 if (renderObject._slotToChild[slot] == child) {
373 renderObject._setChild(null, slot);
374 assert(renderObject._slotToChild[slot] == null);
375 }
376 }
377
378 @override
379 void moveRenderObjectChild(ChildType child, SlotType oldSlot, SlotType newSlot) {
380 renderObject._moveChild(child, newSlot, oldSlot);
381 }
382}
383