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
5import 'package:flutter/rendering.dart';
6
7import 'framework.dart';
8
9/// A container that handles [SelectionEvent]s for the [Selectable]s in
10/// the subtree.
11///
12/// This widget is useful when one wants to customize selection behaviors for
13/// a group of [Selectable]s
14///
15/// The state of this container is a single selectable and will register
16/// itself to the [registrar] if provided. Otherwise, it will register to the
17/// [SelectionRegistrar] from the context. Consider using a [SelectionArea]
18/// widget to provide a root registrar.
19///
20/// The containers handle the [SelectionEvent]s from the registered
21/// [SelectionRegistrar] and delegate the events to the [delegate].
22///
23/// This widget uses [SelectionRegistrarScope] to host the [delegate] as the
24/// [SelectionRegistrar] for the subtree to collect the [Selectable]s, and
25/// [SelectionEvent]s received by this container are sent to the [delegate] using
26/// the [SelectionHandler] API of the delegate.
27///
28/// {@tool dartpad}
29/// This sample demonstrates how to create a [SelectionContainer] that only
30/// allows selecting everything or nothing with no partial selection.
31///
32/// ** See code in examples/api/lib/material/selection_container/selection_container.0.dart **
33/// {@end-tool}
34///
35/// See also:
36/// * [SelectableRegion], which provides an overview of the selection system.
37/// * [SelectionContainer.disabled], which disable selection for a
38/// subtree.
39class SelectionContainer extends StatefulWidget {
40 /// Creates a selection container to collect the [Selectable]s in the subtree.
41 ///
42 /// If [registrar] is not provided, this selection container gets the
43 /// [SelectionRegistrar] from the context instead.
44 const SelectionContainer({
45 super.key,
46 this.registrar,
47 required SelectionContainerDelegate this.delegate,
48 required this.child,
49 });
50
51 /// Creates a selection container that disables selection for the
52 /// subtree.
53 ///
54 /// {@tool dartpad}
55 /// This sample demonstrates how to disable selection for a Text under a
56 /// SelectionArea.
57 ///
58 /// ** See code in examples/api/lib/material/selection_container/selection_container_disabled.0.dart **
59 /// {@end-tool}
60 const SelectionContainer.disabled({
61 super.key,
62 required this.child,
63 }) : registrar = null,
64 delegate = null;
65
66 /// The [SelectionRegistrar] this container is registered to.
67 ///
68 /// If null, this widget gets the [SelectionRegistrar] from the current
69 /// context.
70 final SelectionRegistrar? registrar;
71
72 /// {@macro flutter.widgets.ProxyWidget.child}
73 final Widget child;
74
75 /// The delegate for [SelectionEvent]s sent to this selection container.
76 ///
77 /// The [Selectable]s in the subtree are added or removed from this delegate
78 /// using [SelectionRegistrar] API.
79 ///
80 /// This delegate is responsible for updating the selections for the selectables
81 /// under this widget.
82 final SelectionContainerDelegate? delegate;
83
84 /// Gets the immediate ancestor [SelectionRegistrar] of the [BuildContext].
85 ///
86 /// If this returns null, either there is no [SelectionContainer] above
87 /// the [BuildContext] or the immediate [SelectionContainer] is not
88 /// enabled.
89 static SelectionRegistrar? maybeOf(BuildContext context) {
90 final SelectionRegistrarScope? scope = context.dependOnInheritedWidgetOfExactType<SelectionRegistrarScope>();
91 return scope?.registrar;
92 }
93
94 bool get _disabled => delegate == null;
95
96 @override
97 State<SelectionContainer> createState() => _SelectionContainerState();
98}
99
100class _SelectionContainerState extends State<SelectionContainer> with Selectable, SelectionRegistrant {
101 final Set<VoidCallback> _listeners = <VoidCallback>{};
102
103 static const SelectionGeometry _disabledGeometry = SelectionGeometry(
104 status: SelectionStatus.none,
105 hasContent: true,
106 );
107
108 @override
109 void initState() {
110 super.initState();
111 if (!widget._disabled) {
112 widget.delegate!._selectionContainerContext = context;
113 if (widget.registrar != null) {
114 registrar = widget.registrar;
115 }
116 }
117 }
118
119 @override
120 void didUpdateWidget(SelectionContainer oldWidget) {
121 super.didUpdateWidget(oldWidget);
122 if (oldWidget.delegate != widget.delegate) {
123 if (!oldWidget._disabled) {
124 oldWidget.delegate!._selectionContainerContext = null;
125 _listeners.forEach(oldWidget.delegate!.removeListener);
126 }
127 if (!widget._disabled) {
128 widget.delegate!._selectionContainerContext = context;
129 _listeners.forEach(widget.delegate!.addListener);
130 }
131 if (oldWidget.delegate?.value != widget.delegate?.value) {
132 // Avoid concurrent modification.
133 for (final VoidCallback listener in _listeners.toList(growable: false)) {
134 listener();
135 }
136 }
137 }
138 if (widget._disabled) {
139 registrar = null;
140 } else if (widget.registrar != null) {
141 registrar = widget.registrar;
142 }
143 assert(!widget._disabled || registrar == null);
144 }
145
146 @override
147 void didChangeDependencies() {
148 super.didChangeDependencies();
149 if (widget.registrar == null && !widget._disabled) {
150 registrar = SelectionContainer.maybeOf(context);
151 }
152 assert(!widget._disabled || registrar == null);
153 }
154
155 @override
156 void addListener(VoidCallback listener) {
157 assert(!widget._disabled);
158 widget.delegate!.addListener(listener);
159 _listeners.add(listener);
160 }
161
162 @override
163 void removeListener(VoidCallback listener) {
164 widget.delegate?.removeListener(listener);
165 _listeners.remove(listener);
166 }
167
168 @override
169 void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) {
170 assert(!widget._disabled);
171 widget.delegate!.pushHandleLayers(startHandle, endHandle);
172 }
173
174 @override
175 SelectedContent? getSelectedContent() {
176 assert(!widget._disabled);
177 return widget.delegate!.getSelectedContent();
178 }
179
180 @override
181 SelectionResult dispatchSelectionEvent(SelectionEvent event) {
182 assert(!widget._disabled);
183 return widget.delegate!.dispatchSelectionEvent(event);
184 }
185
186 @override
187 SelectionGeometry get value {
188 if (widget._disabled) {
189 return _disabledGeometry;
190 }
191 return widget.delegate!.value;
192 }
193
194 @override
195 Matrix4 getTransformTo(RenderObject? ancestor) {
196 assert(!widget._disabled);
197 return context.findRenderObject()!.getTransformTo(ancestor);
198 }
199
200 @override
201 Size get size => (context.findRenderObject()! as RenderBox).size;
202
203 @override
204 List<Rect> get boundingBoxes => <Rect>[(context.findRenderObject()! as RenderBox).paintBounds];
205
206 @override
207 void dispose() {
208 if (!widget._disabled) {
209 widget.delegate!._selectionContainerContext = null;
210 _listeners.forEach(widget.delegate!.removeListener);
211 }
212 super.dispose();
213 }
214
215 @override
216 Widget build(BuildContext context) {
217 if (widget._disabled) {
218 return SelectionRegistrarScope._disabled(child: widget.child);
219 }
220 return SelectionRegistrarScope(
221 registrar: widget.delegate!,
222 child: widget.child,
223 );
224 }
225}
226
227/// An inherited widget to host a [SelectionRegistrar] for the subtree.
228///
229/// Use [SelectionContainer.maybeOf] to get the SelectionRegistrar from
230/// a context.
231///
232/// This widget is automatically created as part of [SelectionContainer] and
233/// is generally not used directly, except for disabling selection for a part
234/// of subtree. In that case, one can wrap the subtree with
235/// [SelectionContainer.disabled].
236class SelectionRegistrarScope extends InheritedWidget {
237 /// Creates a selection registrar scope that host the [registrar].
238 const SelectionRegistrarScope({
239 super.key,
240 required SelectionRegistrar this.registrar,
241 required super.child,
242 });
243
244 /// Creates a selection registrar scope that disables selection for the
245 /// subtree.
246 const SelectionRegistrarScope._disabled({
247 required super.child,
248 }) : registrar = null;
249
250 /// The [SelectionRegistrar] hosted by this widget.
251 final SelectionRegistrar? registrar;
252
253 @override
254 bool updateShouldNotify(SelectionRegistrarScope oldWidget) {
255 return oldWidget.registrar != registrar;
256 }
257}
258
259/// A delegate to handle [SelectionEvent]s for a [SelectionContainer].
260///
261/// This delegate needs to implement [SelectionRegistrar] to register
262/// [Selectable]s in the [SelectionContainer] subtree.
263abstract class SelectionContainerDelegate implements SelectionHandler, SelectionRegistrar {
264 BuildContext? _selectionContainerContext;
265
266 /// Gets the paint transform from the [Selectable] child to
267 /// [SelectionContainer] of this delegate.
268 ///
269 /// Returns a matrix that maps the [Selectable] paint coordinate system to the
270 /// coordinate system of [SelectionContainer].
271 ///
272 /// Can only be called after [SelectionContainer] is laid out.
273 Matrix4 getTransformFrom(Selectable child) {
274 assert(
275 _selectionContainerContext?.findRenderObject() != null,
276 'getTransformFrom cannot be called before SelectionContainer is laid out.',
277 );
278 return child.getTransformTo(_selectionContainerContext!.findRenderObject()! as RenderBox);
279 }
280
281 /// Gets the paint transform from the [SelectionContainer] of this delegate to
282 /// the `ancestor`.
283 ///
284 /// Returns a matrix that maps the [SelectionContainer] paint coordinate
285 /// system to the coordinate system of `ancestor`.
286 ///
287 /// If `ancestor` is null, this method returns a matrix that maps from the
288 /// local paint coordinate system to the coordinate system of the
289 /// [PipelineOwner.rootNode].
290 ///
291 /// Can only be called after [SelectionContainer] is laid out.
292 Matrix4 getTransformTo(RenderObject? ancestor) {
293 assert(
294 _selectionContainerContext?.findRenderObject() != null,
295 'getTransformTo cannot be called before SelectionContainer is laid out.',
296 );
297 final RenderBox box = _selectionContainerContext!.findRenderObject()! as RenderBox;
298 return box.getTransformTo(ancestor);
299 }
300
301 /// Whether the [SelectionContainer] has undergone layout and has a size.
302 ///
303 /// See also:
304 ///
305 /// * [RenderBox.hasSize], which is used internally by this method.
306 bool get hasSize {
307 assert(
308 _selectionContainerContext?.findRenderObject() != null,
309 'The _selectionContainerContext must have a renderObject, such as after the first build has completed.',
310 );
311 final RenderBox box = _selectionContainerContext!.findRenderObject()! as RenderBox;
312 return box.hasSize;
313 }
314
315 /// Gets the size of the [SelectionContainer] of this delegate.
316 ///
317 /// Can only be called after [SelectionContainer] is laid out.
318 Size get containerSize {
319 assert(
320 hasSize,
321 'containerSize cannot be called before SelectionContainer is laid out.',
322 );
323 final RenderBox box = _selectionContainerContext!.findRenderObject()! as RenderBox;
324 return box.size;
325 }
326}
327