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 'dart:math' as math;
6
7import 'package:flutter/rendering.dart';
8
9import 'basic.dart';
10import 'framework.dart';
11
12/// Defines the horizontal alignment of [OverflowBar] children
13/// when they're laid out in an overflow column.
14///
15/// This value must be interpreted relative to the ambient
16/// [TextDirection].
17enum OverflowBarAlignment {
18 /// Each child is left-aligned for [TextDirection.ltr],
19 /// right-aligned for [TextDirection.rtl].
20 start,
21
22 /// Each child is right-aligned for [TextDirection.ltr],
23 /// left-aligned for [TextDirection.rtl].
24 end,
25
26 /// Each child is horizontally centered.
27 center,
28}
29
30/// A widget that lays out its [children] in a row unless they
31/// "overflow" the available horizontal space, in which case it lays
32/// them out in a column instead.
33///
34/// This widget's width will expand to contain its children and the
35/// specified [spacing] until it overflows. The overflow column will
36/// consume all of the available width. The [overflowAlignment]
37/// defines how each child will be aligned within the overflow column
38/// and the [overflowSpacing] defines the gap between each child.
39///
40/// The order that the children appear in the horizontal layout
41/// is defined by the [textDirection], just like the [Row] widget.
42/// If the layout overflows, then children's order within their
43/// column is specified by [overflowDirection] instead.
44///
45/// {@tool dartpad}
46/// This example defines a simple approximation of a dialog
47/// layout, where the layout of the dialog's action buttons are
48/// defined by an [OverflowBar]. The content is wrapped in a
49/// [SingleChildScrollView], so that if overflow occurs, the
50/// action buttons will still be accessible by scrolling,
51/// no matter how much vertical space is available.
52///
53/// ** See code in examples/api/lib/widgets/overflow_bar/overflow_bar.0.dart **
54/// {@end-tool}
55class OverflowBar extends MultiChildRenderObjectWidget {
56 /// Constructs an OverflowBar.
57 const OverflowBar({
58 super.key,
59 this.spacing = 0.0,
60 this.alignment,
61 this.overflowSpacing = 0.0,
62 this.overflowAlignment = OverflowBarAlignment.start,
63 this.overflowDirection = VerticalDirection.down,
64 this.textDirection,
65 this.clipBehavior = Clip.none,
66 super.children,
67 });
68
69 /// The width of the gap between [children] for the default
70 /// horizontal layout.
71 ///
72 /// If the horizontal layout overflows, then [overflowSpacing] is
73 /// used instead.
74 ///
75 /// Defaults to 0.0.
76 final double spacing;
77
78 /// Defines the [children]'s horizontal layout according to the same
79 /// rules as for [Row.mainAxisAlignment].
80 ///
81 /// If this property is non-null, and the [children], separated by
82 /// [spacing], fit within the available width, then the overflow
83 /// bar will be as wide as possible. If the children do not fit
84 /// within the available width, then this property is ignored and
85 /// [overflowAlignment] applies instead.
86 ///
87 /// If this property is null (the default) then the overflow bar
88 /// will be no wider than needed to layout the [children] separated
89 /// by [spacing], modulo the incoming constraints.
90 ///
91 /// If [alignment] is one of [MainAxisAlignment.spaceAround],
92 /// [MainAxisAlignment.spaceBetween], or
93 /// [MainAxisAlignment.spaceEvenly], then the [spacing] parameter is
94 /// only used to see if the horizontal layout will overflow.
95 ///
96 /// Defaults to null.
97 ///
98 /// See also:
99 ///
100 /// * [overflowAlignment], the horizontal alignment of the [children] within
101 /// the vertical "overflow" layout.
102 ///
103 final MainAxisAlignment? alignment;
104
105 /// The height of the gap between [children] in the vertical
106 /// "overflow" layout.
107 ///
108 /// This parameter is only used if the horizontal layout overflows, i.e.
109 /// if there isn't enough horizontal room for the [children] and [spacing].
110 ///
111 /// Defaults to 0.0.
112 ///
113 /// See also:
114 ///
115 /// * [spacing], The width of the gap between each pair of children
116 /// for the default horizontal layout.
117 final double overflowSpacing;
118
119 /// The horizontal alignment of the [children] within the vertical
120 /// "overflow" layout.
121 ///
122 /// This parameter is only used if the horizontal layout overflows, i.e.
123 /// if there isn't enough horizontal room for the [children] and [spacing].
124 /// In that case the overflow bar will expand to fill the available
125 /// width and it will layout its [children] in a column. The
126 /// horizontal alignment of each child within that column is
127 /// defined by this parameter and the [textDirection]. If the
128 /// [textDirection] is [TextDirection.ltr] then each child will be
129 /// aligned with the left edge of the available space for
130 /// [OverflowBarAlignment.start], with the right edge of the
131 /// available space for [OverflowBarAlignment.end]. Similarly, if the
132 /// [textDirection] is [TextDirection.rtl] then each child will
133 /// be aligned with the right edge of the available space for
134 /// [OverflowBarAlignment.start], and with the left edge of the
135 /// available space for [OverflowBarAlignment.end]. For
136 /// [OverflowBarAlignment.center] each child is horizontally
137 /// centered within the available space.
138 ///
139 /// Defaults to [OverflowBarAlignment.start].
140 ///
141 /// See also:
142 ///
143 /// * [alignment], which defines the [children]'s horizontal layout
144 /// (according to the same rules as for [Row.mainAxisAlignment]) when
145 /// the children, separated by [spacing], fit within the available space.
146 /// * [overflowDirection], which defines the order that the
147 /// [OverflowBar]'s children appear in, if the horizontal layout
148 /// overflows.
149 final OverflowBarAlignment overflowAlignment;
150
151 /// Defines the order that the [children] appear in, if
152 /// the horizontal layout overflows.
153 ///
154 /// This parameter is only used if the horizontal layout overflows, i.e.
155 /// if there isn't enough horizontal room for the [children] and [spacing].
156 ///
157 /// If the children do not fit into a single row, then they
158 /// are arranged in a column. The first child is at the top of the
159 /// column if this property is set to [VerticalDirection.down], since it
160 /// "starts" at the top and "ends" at the bottom. On the other hand,
161 /// the first child will be at the bottom of the column if this
162 /// property is set to [VerticalDirection.up], since it "starts" at the
163 /// bottom and "ends" at the top.
164 ///
165 /// Defaults to [VerticalDirection.down].
166 ///
167 /// See also:
168 ///
169 /// * [overflowAlignment], which defines the horizontal alignment
170 /// of the children within the vertical "overflow" layout.
171 final VerticalDirection overflowDirection;
172
173 /// Determines the order that the [children] appear in for the default
174 /// horizontal layout, and the interpretation of
175 /// [OverflowBarAlignment.start] and [OverflowBarAlignment.end] for
176 /// the vertical overflow layout.
177 ///
178 /// For the default horizontal layout, if [textDirection] is
179 /// [TextDirection.rtl] then the last child is laid out first. If
180 /// [textDirection] is [TextDirection.ltr] then the first child is
181 /// laid out first.
182 ///
183 /// If this parameter is null, then the value of
184 /// `Directionality.of(context)` is used.
185 ///
186 /// See also:
187 ///
188 /// * [overflowDirection], which defines the order that the
189 /// [OverflowBar]'s children appear in, if the horizontal layout
190 /// overflows.
191 /// * [Directionality], which defines the ambient directionality of
192 /// text and text-direction-sensitive render objects.
193 final TextDirection? textDirection;
194
195 /// {@macro flutter.material.Material.clipBehavior}
196 ///
197 /// Defaults to [Clip.none].
198 final Clip clipBehavior;
199
200 @override
201 RenderObject createRenderObject(BuildContext context) {
202 return _RenderOverflowBar(
203 spacing: spacing,
204 alignment: alignment,
205 overflowSpacing: overflowSpacing,
206 overflowAlignment: overflowAlignment,
207 overflowDirection: overflowDirection,
208 textDirection: textDirection ?? Directionality.of(context),
209 clipBehavior: clipBehavior,
210 );
211 }
212
213 @override
214 void updateRenderObject(BuildContext context, RenderObject renderObject) {
215 (renderObject as _RenderOverflowBar)
216 ..spacing = spacing
217 ..alignment = alignment
218 ..overflowSpacing = overflowSpacing
219 ..overflowAlignment = overflowAlignment
220 ..overflowDirection = overflowDirection
221 ..textDirection = textDirection ?? Directionality.of(context)
222 ..clipBehavior = clipBehavior;
223 }
224
225 @override
226 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
227 super.debugFillProperties(properties);
228 properties.add(DoubleProperty('spacing', spacing, defaultValue: 0));
229 properties.add(EnumProperty<MainAxisAlignment>('alignment', alignment, defaultValue: null));
230 properties.add(DoubleProperty('overflowSpacing', overflowSpacing, defaultValue: 0));
231 properties.add(EnumProperty<OverflowBarAlignment>('overflowAlignment', overflowAlignment, defaultValue: OverflowBarAlignment.start));
232 properties.add(EnumProperty<VerticalDirection>('overflowDirection', overflowDirection, defaultValue: VerticalDirection.down));
233 properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
234 }
235}
236
237class _OverflowBarParentData extends ContainerBoxParentData<RenderBox> { }
238
239class _RenderOverflowBar extends RenderBox
240 with ContainerRenderObjectMixin<RenderBox, _OverflowBarParentData>,
241 RenderBoxContainerDefaultsMixin<RenderBox, _OverflowBarParentData> {
242 _RenderOverflowBar({
243 List<RenderBox>? children,
244 double spacing = 0.0,
245 MainAxisAlignment? alignment,
246 double overflowSpacing = 0.0,
247 OverflowBarAlignment overflowAlignment = OverflowBarAlignment.start,
248 VerticalDirection overflowDirection = VerticalDirection.down,
249 required TextDirection textDirection,
250 Clip clipBehavior = Clip.none,
251 }) : _spacing = spacing,
252 _alignment = alignment,
253 _overflowSpacing = overflowSpacing,
254 _overflowAlignment = overflowAlignment,
255 _overflowDirection = overflowDirection,
256 _textDirection = textDirection,
257 _clipBehavior = clipBehavior {
258 addAll(children);
259 }
260
261 double get spacing => _spacing;
262 double _spacing;
263 set spacing (double value) {
264 if (_spacing == value) {
265 return;
266 }
267 _spacing = value;
268 markNeedsLayout();
269 }
270
271 MainAxisAlignment? get alignment => _alignment;
272 MainAxisAlignment? _alignment;
273 set alignment (MainAxisAlignment? value) {
274 if (_alignment == value) {
275 return;
276 }
277 _alignment = value;
278 markNeedsLayout();
279 }
280
281 double get overflowSpacing => _overflowSpacing;
282 double _overflowSpacing;
283 set overflowSpacing (double value) {
284 if (_overflowSpacing == value) {
285 return;
286 }
287 _overflowSpacing = value;
288 markNeedsLayout();
289 }
290
291 OverflowBarAlignment get overflowAlignment => _overflowAlignment;
292 OverflowBarAlignment _overflowAlignment;
293 set overflowAlignment (OverflowBarAlignment value) {
294 if (_overflowAlignment == value) {
295 return;
296 }
297 _overflowAlignment = value;
298 markNeedsLayout();
299 }
300
301 VerticalDirection get overflowDirection => _overflowDirection;
302 VerticalDirection _overflowDirection;
303 set overflowDirection (VerticalDirection value) {
304 if (_overflowDirection == value) {
305 return;
306 }
307 _overflowDirection = value;
308 markNeedsLayout();
309 }
310
311 TextDirection get textDirection => _textDirection;
312 TextDirection _textDirection;
313 set textDirection(TextDirection value) {
314 if (_textDirection == value) {
315 return;
316 }
317 _textDirection = value;
318 markNeedsLayout();
319 }
320
321 Clip get clipBehavior => _clipBehavior;
322 Clip _clipBehavior = Clip.none;
323 set clipBehavior(Clip value) {
324 if (value == _clipBehavior) {
325 return;
326 }
327 _clipBehavior = value;
328 markNeedsPaint();
329 markNeedsSemanticsUpdate();
330 }
331
332 @override
333 void setupParentData(RenderBox child) {
334 if (child.parentData is! _OverflowBarParentData) {
335 child.parentData = _OverflowBarParentData();
336 }
337 }
338
339 @override
340 double computeMinIntrinsicHeight(double width) {
341 RenderBox? child = firstChild;
342 if (child == null) {
343 return 0;
344 }
345 double barWidth = 0.0;
346 while (child != null) {
347 barWidth += child.getMinIntrinsicWidth(double.infinity);
348 child = childAfter(child);
349 }
350 barWidth += spacing * (childCount - 1);
351
352 double height = 0.0;
353 if (barWidth > width) {
354 child = firstChild;
355 while (child != null) {
356 height += child.getMinIntrinsicHeight(width);
357 child = childAfter(child);
358 }
359 return height + overflowSpacing * (childCount - 1);
360 } else {
361 child = firstChild;
362 while (child != null) {
363 height = math.max(height, child.getMinIntrinsicHeight(width));
364 child = childAfter(child);
365 }
366 return height;
367 }
368 }
369
370 @override
371 double computeMaxIntrinsicHeight(double width) {
372 RenderBox? child = firstChild;
373 if (child == null) {
374 return 0;
375 }
376 double barWidth = 0.0;
377 while (child != null) {
378 barWidth += child.getMinIntrinsicWidth(double.infinity);
379 child = childAfter(child);
380 }
381 barWidth += spacing * (childCount - 1);
382
383 double height = 0.0;
384 if (barWidth > width) {
385 child = firstChild;
386 while (child != null) {
387 height += child.getMaxIntrinsicHeight(width);
388 child = childAfter(child);
389 }
390 return height + overflowSpacing * (childCount - 1);
391 } else {
392 child = firstChild;
393 while (child != null) {
394 height = math.max(height, child.getMaxIntrinsicHeight(width));
395 child = childAfter(child);
396 }
397 return height;
398 }
399 }
400
401 @override
402 double computeMinIntrinsicWidth(double height) {
403 RenderBox? child = firstChild;
404 if (child == null) {
405 return 0;
406 }
407 double width = 0.0;
408 while (child != null) {
409 width += child.getMinIntrinsicWidth(double.infinity);
410 child = childAfter(child);
411 }
412 return width + spacing * (childCount - 1);
413 }
414
415 @override
416 double computeMaxIntrinsicWidth(double height) {
417 RenderBox? child = firstChild;
418 if (child == null) {
419 return 0;
420 }
421 double width = 0.0;
422 while (child != null) {
423 width += child.getMaxIntrinsicWidth(double.infinity);
424 child = childAfter(child);
425 }
426 return width + spacing * (childCount - 1);
427 }
428
429 @override
430 double? computeDistanceToActualBaseline(TextBaseline baseline) {
431 return defaultComputeDistanceToHighestActualBaseline(baseline);
432 }
433
434 @override
435 Size computeDryLayout(BoxConstraints constraints) {
436 RenderBox? child = firstChild;
437 if (child == null) {
438 return constraints.smallest;
439 }
440 final BoxConstraints childConstraints = constraints.loosen();
441 double childrenWidth = 0.0;
442 double maxChildHeight = 0.0;
443 double y = 0.0;
444 while (child != null) {
445 final Size childSize = child.getDryLayout(childConstraints);
446 childrenWidth += childSize.width;
447 maxChildHeight = math.max(maxChildHeight, childSize.height);
448 y += childSize.height + overflowSpacing;
449 child = childAfter(child);
450 }
451 final double actualWidth = childrenWidth + spacing * (childCount - 1);
452 if (actualWidth > constraints.maxWidth) {
453 return constraints.constrain(Size(constraints.maxWidth, y - overflowSpacing));
454 } else {
455 final double overallWidth = alignment == null ? actualWidth : constraints.maxWidth;
456 return constraints.constrain(Size(overallWidth, maxChildHeight));
457 }
458 }
459
460 @override
461 void performLayout() {
462 RenderBox? child = firstChild;
463 if (child == null) {
464 size = constraints.smallest;
465 return;
466 }
467
468 final BoxConstraints childConstraints = constraints.loosen();
469 double childrenWidth = 0;
470 double maxChildHeight = 0;
471 double maxChildWidth = 0;
472
473 while (child != null) {
474 child.layout(childConstraints, parentUsesSize: true);
475 childrenWidth += child.size.width;
476 maxChildHeight = math.max(maxChildHeight, child.size.height);
477 maxChildWidth = math.max(maxChildWidth, child.size.width);
478 child = childAfter(child);
479 }
480
481 final bool rtl = textDirection == TextDirection.rtl;
482 final double actualWidth = childrenWidth + spacing * (childCount - 1);
483
484 if (actualWidth > constraints.maxWidth) {
485 // Overflow vertical layout
486 child = overflowDirection == VerticalDirection.down ? firstChild : lastChild;
487 RenderBox? nextChild() => overflowDirection == VerticalDirection.down ? childAfter(child!) : childBefore(child!);
488 double y = 0;
489 while (child != null) {
490 final _OverflowBarParentData childParentData = child.parentData! as _OverflowBarParentData;
491 double x = 0;
492 switch (overflowAlignment) {
493 case OverflowBarAlignment.start:
494 x = rtl ? constraints.maxWidth - child.size.width : 0;
495 case OverflowBarAlignment.center:
496 x = (constraints.maxWidth - child.size.width) / 2;
497 case OverflowBarAlignment.end:
498 x = rtl ? 0 : constraints.maxWidth - child.size.width;
499 }
500 childParentData.offset = Offset(x, y);
501 y += child.size.height + overflowSpacing;
502 child = nextChild();
503 }
504 size = constraints.constrain(Size(constraints.maxWidth, y - overflowSpacing));
505 } else {
506 // Default horizontal layout
507 child = firstChild;
508 final double firstChildWidth = child!.size.width;
509 final double overallWidth = alignment == null ? actualWidth : constraints.maxWidth;
510 size = constraints.constrain(Size(overallWidth, maxChildHeight));
511
512 late double x; // initial value: origin of the first child
513 double layoutSpacing = spacing; // space between children
514 switch (alignment) {
515 case null:
516 x = rtl ? size.width - firstChildWidth : 0;
517 case MainAxisAlignment.start:
518 x = rtl ? size.width - firstChildWidth : 0;
519 case MainAxisAlignment.center:
520 final double halfRemainingWidth = (size.width - actualWidth) / 2;
521 x = rtl ? size.width - halfRemainingWidth - firstChildWidth : halfRemainingWidth;
522 case MainAxisAlignment.end:
523 x = rtl ? actualWidth - firstChildWidth : size.width - actualWidth;
524 case MainAxisAlignment.spaceBetween:
525 layoutSpacing = (size.width - childrenWidth) / (childCount - 1);
526 x = rtl ? size.width - firstChildWidth : 0;
527 case MainAxisAlignment.spaceAround:
528 layoutSpacing = childCount > 0 ? (size.width - childrenWidth) / childCount : 0;
529 x = rtl ? size.width - layoutSpacing / 2 - firstChildWidth : layoutSpacing / 2;
530 case MainAxisAlignment.spaceEvenly:
531 layoutSpacing = (size.width - childrenWidth) / (childCount + 1);
532 x = rtl ? size.width - layoutSpacing - firstChildWidth : layoutSpacing;
533 }
534
535 while (child != null) {
536 final _OverflowBarParentData childParentData = child.parentData! as _OverflowBarParentData;
537 childParentData.offset = Offset(x, (maxChildHeight - child.size.height) / 2);
538 // x is the horizontal origin of child. To advance x to the next child's
539 // origin for LTR: add the width of the current child. To advance x to
540 // the origin of the next child for RTL: subtract the width of the next
541 // child (if there is one).
542 if (!rtl) {
543 x += child.size.width + layoutSpacing;
544 }
545 child = childAfter(child);
546 if (rtl && child != null) {
547 x -= child.size.width + layoutSpacing;
548 }
549 }
550 }
551 }
552
553 @override
554 bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
555 return defaultHitTestChildren(result, position: position);
556 }
557
558 @override
559 void paint(PaintingContext context, Offset offset) {
560 defaultPaint(context, offset);
561 }
562
563 @override
564 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
565 super.debugFillProperties(properties);
566 properties.add(DoubleProperty('spacing', spacing, defaultValue: 0));
567 properties.add(DoubleProperty('overflowSpacing', overflowSpacing, defaultValue: 0));
568 properties.add(EnumProperty<OverflowBarAlignment>('overflowAlignment', overflowAlignment, defaultValue: OverflowBarAlignment.start));
569 properties.add(EnumProperty<VerticalDirection>('overflowDirection', overflowDirection, defaultValue: VerticalDirection.down));
570 properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
571 }
572}
573