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/widgets.dart'; |
6 | library; |
7 | |
8 | import 'dart:math' as math; |
9 | |
10 | import 'package:flutter/foundation.dart'; |
11 | |
12 | import 'box.dart'; |
13 | import 'layer.dart'; |
14 | import 'layout_helper.dart'; |
15 | import 'object.dart'; |
16 | |
17 | typedef _NextChild = RenderBox? Function(RenderBox child); |
18 | typedef _PositionChild = void Function(Offset offset, RenderBox child); |
19 | typedef _GetChildSize = Size Function(RenderBox child); |
20 | // A 2D vector that uses a [RenderWrap]'s main axis and cross axis as its first and second coordinate axes. |
21 | // It represents the same vector as (double mainAxisExtent, double crossAxisExtent). |
22 | extension type const _AxisSize._(Size _size) { |
23 | _AxisSize({ required double mainAxisExtent, required double crossAxisExtent }) : this._(Size(mainAxisExtent, crossAxisExtent)); |
24 | _AxisSize.fromSize({ required Size size, required Axis direction }) : this._(_convert(size, direction)); |
25 | |
26 | static const _AxisSize empty = _AxisSize._(Size.zero); |
27 | |
28 | static Size _convert(Size size, Axis direction) { |
29 | return switch (direction) { |
30 | Axis.horizontal => size, |
31 | Axis.vertical => size.flipped, |
32 | }; |
33 | } |
34 | double get mainAxisExtent => _size.width; |
35 | double get crossAxisExtent => _size.height; |
36 | |
37 | Size toSize(Axis direction) => _convert(_size, direction); |
38 | |
39 | _AxisSize applyConstraints(BoxConstraints constraints, Axis direction) { |
40 | final BoxConstraints effectiveConstraints = switch (direction) { |
41 | Axis.horizontal => constraints, |
42 | Axis.vertical => constraints.flipped, |
43 | }; |
44 | return _AxisSize._(effectiveConstraints.constrain(_size)); |
45 | } |
46 | |
47 | _AxisSize get flipped => _AxisSize._(_size.flipped); |
48 | _AxisSize operator +(_AxisSize other) => _AxisSize._(Size(_size.width + other._size.width, math.max(_size.height, other._size.height))); |
49 | _AxisSize operator -(_AxisSize other) => _AxisSize._(Size(_size.width - other._size.width, _size.height - other._size.height)); |
50 | } |
51 | |
52 | /// How [Wrap] should align objects. |
53 | /// |
54 | /// Used both to align children within a run in the main axis as well as to |
55 | /// align the runs themselves in the cross axis. |
56 | enum WrapAlignment { |
57 | /// Place the objects as close to the start of the axis as possible. |
58 | /// |
59 | /// If this value is used in a horizontal direction, a [TextDirection] must be |
60 | /// available to determine if the start is the left or the right. |
61 | /// |
62 | /// If this value is used in a vertical direction, a [VerticalDirection] must be |
63 | /// available to determine if the start is the top or the bottom. |
64 | start, |
65 | |
66 | /// Place the objects as close to the end of the axis as possible. |
67 | /// |
68 | /// If this value is used in a horizontal direction, a [TextDirection] must be |
69 | /// available to determine if the end is the left or the right. |
70 | /// |
71 | /// If this value is used in a vertical direction, a [VerticalDirection] must be |
72 | /// available to determine if the end is the top or the bottom. |
73 | end, |
74 | |
75 | /// Place the objects as close to the middle of the axis as possible. |
76 | center, |
77 | |
78 | /// Place the free space evenly between the objects. |
79 | spaceBetween, |
80 | |
81 | /// Place the free space evenly between the objects as well as half of that |
82 | /// space before and after the first and last objects. |
83 | spaceAround, |
84 | |
85 | /// Place the free space evenly between the objects as well as before and |
86 | /// after the first and last objects. |
87 | spaceEvenly; |
88 | |
89 | (double leadingSpace, double betweenSpace) _distributeSpace(double freeSpace, double itemSpacing, int itemCount, bool flipped) { |
90 | assert(itemCount > 0); |
91 | return switch (this) { |
92 | WrapAlignment.start => (flipped ? freeSpace : 0.0, itemSpacing), |
93 | |
94 | WrapAlignment.end => WrapAlignment.start._distributeSpace(freeSpace, itemSpacing, itemCount, !flipped), |
95 | WrapAlignment.spaceBetween when itemCount < 2 => WrapAlignment.start._distributeSpace(freeSpace, itemSpacing, itemCount, flipped), |
96 | |
97 | WrapAlignment.center => (freeSpace / 2.0, itemSpacing), |
98 | WrapAlignment.spaceBetween => (0, freeSpace / (itemCount - 1) + itemSpacing), |
99 | WrapAlignment.spaceAround => (freeSpace / itemCount / 2, freeSpace / itemCount + itemSpacing), |
100 | WrapAlignment.spaceEvenly => (freeSpace / (itemCount + 1), freeSpace / (itemCount + 1) + itemSpacing), |
101 | }; |
102 | } |
103 | } |
104 | |
105 | /// Who [Wrap] should align children within a run in the cross axis. |
106 | enum WrapCrossAlignment { |
107 | /// Place the children as close to the start of the run in the cross axis as |
108 | /// possible. |
109 | /// |
110 | /// If this value is used in a horizontal direction, a [TextDirection] must be |
111 | /// available to determine if the start is the left or the right. |
112 | /// |
113 | /// If this value is used in a vertical direction, a [VerticalDirection] must be |
114 | /// available to determine if the start is the top or the bottom. |
115 | start, |
116 | |
117 | /// Place the children as close to the end of the run in the cross axis as |
118 | /// possible. |
119 | /// |
120 | /// If this value is used in a horizontal direction, a [TextDirection] must be |
121 | /// available to determine if the end is the left or the right. |
122 | /// |
123 | /// If this value is used in a vertical direction, a [VerticalDirection] must be |
124 | /// available to determine if the end is the top or the bottom. |
125 | end, |
126 | |
127 | /// Place the children as close to the middle of the run in the cross axis as |
128 | /// possible. |
129 | center; |
130 | |
131 | // TODO(ianh): baseline. |
132 | |
133 | WrapCrossAlignment get _flipped => switch (this) { |
134 | WrapCrossAlignment.start => WrapCrossAlignment.end, |
135 | WrapCrossAlignment.end => WrapCrossAlignment.start, |
136 | WrapCrossAlignment.center => WrapCrossAlignment.center, |
137 | }; |
138 | |
139 | double get _alignment => switch (this) { |
140 | WrapCrossAlignment.start => 0, |
141 | WrapCrossAlignment.end => 1, |
142 | WrapCrossAlignment.center => 0.5, |
143 | }; |
144 | } |
145 | |
146 | class _RunMetrics { |
147 | _RunMetrics(this.leadingChild, this.axisSize); |
148 | |
149 | _AxisSize axisSize; |
150 | int childCount = 1; |
151 | RenderBox leadingChild; |
152 | |
153 | // Look ahead, creates a new run if incorporating the child would exceed the allowed line width. |
154 | _RunMetrics? tryAddingNewChild(RenderBox child, _AxisSize childSize, bool flipMainAxis, double spacing, double maxMainExtent) { |
155 | final bool needsNewRun = axisSize.mainAxisExtent + childSize.mainAxisExtent + spacing - maxMainExtent > precisionErrorTolerance; |
156 | if (needsNewRun) { |
157 | return _RunMetrics(child, childSize); |
158 | } else { |
159 | axisSize += childSize + _AxisSize(mainAxisExtent: spacing, crossAxisExtent: 0.0); |
160 | childCount += 1; |
161 | if (flipMainAxis) { |
162 | leadingChild = child; |
163 | } |
164 | return null; |
165 | } |
166 | } |
167 | } |
168 | |
169 | /// Parent data for use with [RenderWrap]. |
170 | class WrapParentData extends ContainerBoxParentData<RenderBox> { } |
171 | |
172 | /// Displays its children in multiple horizontal or vertical runs. |
173 | /// |
174 | /// A [RenderWrap] lays out each child and attempts to place the child adjacent |
175 | /// to the previous child in the main axis, given by [direction], leaving |
176 | /// [spacing] space in between. If there is not enough space to fit the child, |
177 | /// [RenderWrap] creates a new _run_ adjacent to the existing children in the |
178 | /// cross axis. |
179 | /// |
180 | /// After all the children have been allocated to runs, the children within the |
181 | /// runs are positioned according to the [alignment] in the main axis and |
182 | /// according to the [crossAxisAlignment] in the cross axis. |
183 | /// |
184 | /// The runs themselves are then positioned in the cross axis according to the |
185 | /// [runSpacing] and [runAlignment]. |
186 | class RenderWrap extends RenderBox |
187 | with ContainerRenderObjectMixin<RenderBox, WrapParentData>, |
188 | RenderBoxContainerDefaultsMixin<RenderBox, WrapParentData> { |
189 | /// Creates a wrap render object. |
190 | /// |
191 | /// By default, the wrap layout is horizontal and both the children and the |
192 | /// runs are aligned to the start. |
193 | RenderWrap({ |
194 | List<RenderBox>? children, |
195 | Axis direction = Axis.horizontal, |
196 | WrapAlignment alignment = WrapAlignment.start, |
197 | double spacing = 0.0, |
198 | WrapAlignment runAlignment = WrapAlignment.start, |
199 | double runSpacing = 0.0, |
200 | WrapCrossAlignment crossAxisAlignment = WrapCrossAlignment.start, |
201 | TextDirection? textDirection, |
202 | VerticalDirection verticalDirection = VerticalDirection.down, |
203 | Clip clipBehavior = Clip.none, |
204 | }) : _direction = direction, |
205 | _alignment = alignment, |
206 | _spacing = spacing, |
207 | _runAlignment = runAlignment, |
208 | _runSpacing = runSpacing, |
209 | _crossAxisAlignment = crossAxisAlignment, |
210 | _textDirection = textDirection, |
211 | _verticalDirection = verticalDirection, |
212 | _clipBehavior = clipBehavior { |
213 | addAll(children); |
214 | } |
215 | |
216 | /// The direction to use as the main axis. |
217 | /// |
218 | /// For example, if [direction] is [Axis.horizontal], the default, the |
219 | /// children are placed adjacent to one another in a horizontal run until the |
220 | /// available horizontal space is consumed, at which point a subsequent |
221 | /// children are placed in a new run vertically adjacent to the previous run. |
222 | Axis get direction => _direction; |
223 | Axis _direction; |
224 | set direction (Axis value) { |
225 | if (_direction == value) { |
226 | return; |
227 | } |
228 | _direction = value; |
229 | markNeedsLayout(); |
230 | } |
231 | |
232 | /// How the children within a run should be placed in the main axis. |
233 | /// |
234 | /// For example, if [alignment] is [WrapAlignment.center], the children in |
235 | /// each run are grouped together in the center of their run in the main axis. |
236 | /// |
237 | /// Defaults to [WrapAlignment.start]. |
238 | /// |
239 | /// See also: |
240 | /// |
241 | /// * [runAlignment], which controls how the runs are placed relative to each |
242 | /// other in the cross axis. |
243 | /// * [crossAxisAlignment], which controls how the children within each run |
244 | /// are placed relative to each other in the cross axis. |
245 | WrapAlignment get alignment => _alignment; |
246 | WrapAlignment _alignment; |
247 | set alignment (WrapAlignment value) { |
248 | if (_alignment == value) { |
249 | return; |
250 | } |
251 | _alignment = value; |
252 | markNeedsLayout(); |
253 | } |
254 | |
255 | /// How much space to place between children in a run in the main axis. |
256 | /// |
257 | /// For example, if [spacing] is 10.0, the children will be spaced at least |
258 | /// 10.0 logical pixels apart in the main axis. |
259 | /// |
260 | /// If there is additional free space in a run (e.g., because the wrap has a |
261 | /// minimum size that is not filled or because some runs are longer than |
262 | /// others), the additional free space will be allocated according to the |
263 | /// [alignment]. |
264 | /// |
265 | /// Defaults to 0.0. |
266 | double get spacing => _spacing; |
267 | double _spacing; |
268 | set spacing (double value) { |
269 | if (_spacing == value) { |
270 | return; |
271 | } |
272 | _spacing = value; |
273 | markNeedsLayout(); |
274 | } |
275 | |
276 | /// How the runs themselves should be placed in the cross axis. |
277 | /// |
278 | /// For example, if [runAlignment] is [WrapAlignment.center], the runs are |
279 | /// grouped together in the center of the overall [RenderWrap] in the cross |
280 | /// axis. |
281 | /// |
282 | /// Defaults to [WrapAlignment.start]. |
283 | /// |
284 | /// See also: |
285 | /// |
286 | /// * [alignment], which controls how the children within each run are placed |
287 | /// relative to each other in the main axis. |
288 | /// * [crossAxisAlignment], which controls how the children within each run |
289 | /// are placed relative to each other in the cross axis. |
290 | WrapAlignment get runAlignment => _runAlignment; |
291 | WrapAlignment _runAlignment; |
292 | set runAlignment (WrapAlignment value) { |
293 | if (_runAlignment == value) { |
294 | return; |
295 | } |
296 | _runAlignment = value; |
297 | markNeedsLayout(); |
298 | } |
299 | |
300 | /// How much space to place between the runs themselves in the cross axis. |
301 | /// |
302 | /// For example, if [runSpacing] is 10.0, the runs will be spaced at least |
303 | /// 10.0 logical pixels apart in the cross axis. |
304 | /// |
305 | /// If there is additional free space in the overall [RenderWrap] (e.g., |
306 | /// because the wrap has a minimum size that is not filled), the additional |
307 | /// free space will be allocated according to the [runAlignment]. |
308 | /// |
309 | /// Defaults to 0.0. |
310 | double get runSpacing => _runSpacing; |
311 | double _runSpacing; |
312 | set runSpacing (double value) { |
313 | if (_runSpacing == value) { |
314 | return; |
315 | } |
316 | _runSpacing = value; |
317 | markNeedsLayout(); |
318 | } |
319 | |
320 | /// How the children within a run should be aligned relative to each other in |
321 | /// the cross axis. |
322 | /// |
323 | /// For example, if this is set to [WrapCrossAlignment.end], and the |
324 | /// [direction] is [Axis.horizontal], then the children within each |
325 | /// run will have their bottom edges aligned to the bottom edge of the run. |
326 | /// |
327 | /// Defaults to [WrapCrossAlignment.start]. |
328 | /// |
329 | /// See also: |
330 | /// |
331 | /// * [alignment], which controls how the children within each run are placed |
332 | /// relative to each other in the main axis. |
333 | /// * [runAlignment], which controls how the runs are placed relative to each |
334 | /// other in the cross axis. |
335 | WrapCrossAlignment get crossAxisAlignment => _crossAxisAlignment; |
336 | WrapCrossAlignment _crossAxisAlignment; |
337 | set crossAxisAlignment (WrapCrossAlignment value) { |
338 | if (_crossAxisAlignment == value) { |
339 | return; |
340 | } |
341 | _crossAxisAlignment = value; |
342 | markNeedsLayout(); |
343 | } |
344 | |
345 | /// Determines the order to lay children out horizontally and how to interpret |
346 | /// `start` and `end` in the horizontal direction. |
347 | /// |
348 | /// If the [direction] is [Axis.horizontal], this controls the order in which |
349 | /// children are positioned (left-to-right or right-to-left), and the meaning |
350 | /// of the [alignment] property's [WrapAlignment.start] and |
351 | /// [WrapAlignment.end] values. |
352 | /// |
353 | /// If the [direction] is [Axis.horizontal], and either the |
354 | /// [alignment] is either [WrapAlignment.start] or [WrapAlignment.end], or |
355 | /// there's more than one child, then the [textDirection] must not be null. |
356 | /// |
357 | /// If the [direction] is [Axis.vertical], this controls the order in |
358 | /// which runs are positioned, the meaning of the [runAlignment] property's |
359 | /// [WrapAlignment.start] and [WrapAlignment.end] values, as well as the |
360 | /// [crossAxisAlignment] property's [WrapCrossAlignment.start] and |
361 | /// [WrapCrossAlignment.end] values. |
362 | /// |
363 | /// If the [direction] is [Axis.vertical], and either the |
364 | /// [runAlignment] is either [WrapAlignment.start] or [WrapAlignment.end], the |
365 | /// [crossAxisAlignment] is either [WrapCrossAlignment.start] or |
366 | /// [WrapCrossAlignment.end], or there's more than one child, then the |
367 | /// [textDirection] must not be null. |
368 | TextDirection? get textDirection => _textDirection; |
369 | TextDirection? _textDirection; |
370 | set textDirection(TextDirection? value) { |
371 | if (_textDirection != value) { |
372 | _textDirection = value; |
373 | markNeedsLayout(); |
374 | } |
375 | } |
376 | |
377 | /// Determines the order to lay children out vertically and how to interpret |
378 | /// `start` and `end` in the vertical direction. |
379 | /// |
380 | /// If the [direction] is [Axis.vertical], this controls which order children |
381 | /// are painted in (down or up), the meaning of the [alignment] property's |
382 | /// [WrapAlignment.start] and [WrapAlignment.end] values. |
383 | /// |
384 | /// If the [direction] is [Axis.vertical], and either the [alignment] |
385 | /// is either [WrapAlignment.start] or [WrapAlignment.end], or there's |
386 | /// more than one child, then the [verticalDirection] must not be null. |
387 | /// |
388 | /// If the [direction] is [Axis.horizontal], this controls the order in which |
389 | /// runs are positioned, the meaning of the [runAlignment] property's |
390 | /// [WrapAlignment.start] and [WrapAlignment.end] values, as well as the |
391 | /// [crossAxisAlignment] property's [WrapCrossAlignment.start] and |
392 | /// [WrapCrossAlignment.end] values. |
393 | /// |
394 | /// If the [direction] is [Axis.horizontal], and either the |
395 | /// [runAlignment] is either [WrapAlignment.start] or [WrapAlignment.end], the |
396 | /// [crossAxisAlignment] is either [WrapCrossAlignment.start] or |
397 | /// [WrapCrossAlignment.end], or there's more than one child, then the |
398 | /// [verticalDirection] must not be null. |
399 | VerticalDirection get verticalDirection => _verticalDirection; |
400 | VerticalDirection _verticalDirection; |
401 | set verticalDirection(VerticalDirection value) { |
402 | if (_verticalDirection != value) { |
403 | _verticalDirection = value; |
404 | markNeedsLayout(); |
405 | } |
406 | } |
407 | |
408 | /// {@macro flutter.material.Material.clipBehavior} |
409 | /// |
410 | /// Defaults to [Clip.none]. |
411 | Clip get clipBehavior => _clipBehavior; |
412 | Clip _clipBehavior = Clip.none; |
413 | set clipBehavior(Clip value) { |
414 | if (value != _clipBehavior) { |
415 | _clipBehavior = value; |
416 | markNeedsPaint(); |
417 | markNeedsSemanticsUpdate(); |
418 | } |
419 | } |
420 | |
421 | bool get _debugHasNecessaryDirections { |
422 | if (firstChild != null && lastChild != firstChild) { |
423 | // i.e. there's more than one child |
424 | switch (direction) { |
425 | case Axis.horizontal: |
426 | assert(textDirection != null, 'Horizontal $runtimeType with multiple children has a null textDirection, so the layout order is undefined.' ); |
427 | case Axis.vertical: |
428 | break; |
429 | } |
430 | } |
431 | if (alignment == WrapAlignment.start || alignment == WrapAlignment.end) { |
432 | switch (direction) { |
433 | case Axis.horizontal: |
434 | assert(textDirection != null, 'Horizontal $runtimeType with alignment $alignment has a null textDirection, so the alignment cannot be resolved.' ); |
435 | case Axis.vertical: |
436 | break; |
437 | } |
438 | } |
439 | if (runAlignment == WrapAlignment.start || runAlignment == WrapAlignment.end) { |
440 | switch (direction) { |
441 | case Axis.horizontal: |
442 | break; |
443 | case Axis.vertical: |
444 | assert(textDirection != null, 'Vertical $runtimeType with runAlignment $runAlignment has a null textDirection, so the alignment cannot be resolved.' ); |
445 | } |
446 | } |
447 | if (crossAxisAlignment == WrapCrossAlignment.start || crossAxisAlignment == WrapCrossAlignment.end) { |
448 | switch (direction) { |
449 | case Axis.horizontal: |
450 | break; |
451 | case Axis.vertical: |
452 | assert(textDirection != null, 'Vertical $runtimeType with crossAxisAlignment $crossAxisAlignment has a null textDirection, so the alignment cannot be resolved.' ); |
453 | } |
454 | } |
455 | return true; |
456 | } |
457 | |
458 | @override |
459 | void setupParentData(RenderBox child) { |
460 | if (child.parentData is! WrapParentData) { |
461 | child.parentData = WrapParentData(); |
462 | } |
463 | } |
464 | |
465 | @override |
466 | double computeMinIntrinsicWidth(double height) { |
467 | switch (direction) { |
468 | case Axis.horizontal: |
469 | double width = 0.0; |
470 | RenderBox? child = firstChild; |
471 | while (child != null) { |
472 | width = math.max(width, child.getMinIntrinsicWidth(double.infinity)); |
473 | child = childAfter(child); |
474 | } |
475 | return width; |
476 | case Axis.vertical: |
477 | return getDryLayout(BoxConstraints(maxHeight: height)).width; |
478 | } |
479 | } |
480 | |
481 | @override |
482 | double computeMaxIntrinsicWidth(double height) { |
483 | switch (direction) { |
484 | case Axis.horizontal: |
485 | double width = 0.0; |
486 | RenderBox? child = firstChild; |
487 | while (child != null) { |
488 | width += child.getMaxIntrinsicWidth(double.infinity); |
489 | child = childAfter(child); |
490 | } |
491 | return width; |
492 | case Axis.vertical: |
493 | return getDryLayout(BoxConstraints(maxHeight: height)).width; |
494 | } |
495 | } |
496 | |
497 | @override |
498 | double computeMinIntrinsicHeight(double width) { |
499 | switch (direction) { |
500 | case Axis.horizontal: |
501 | return getDryLayout(BoxConstraints(maxWidth: width)).height; |
502 | case Axis.vertical: |
503 | double height = 0.0; |
504 | RenderBox? child = firstChild; |
505 | while (child != null) { |
506 | height = math.max(height, child.getMinIntrinsicHeight(double.infinity)); |
507 | child = childAfter(child); |
508 | } |
509 | return height; |
510 | } |
511 | } |
512 | |
513 | @override |
514 | double computeMaxIntrinsicHeight(double width) { |
515 | switch (direction) { |
516 | case Axis.horizontal: |
517 | return getDryLayout(BoxConstraints(maxWidth: width)).height; |
518 | case Axis.vertical: |
519 | double height = 0.0; |
520 | RenderBox? child = firstChild; |
521 | while (child != null) { |
522 | height += child.getMaxIntrinsicHeight(double.infinity); |
523 | child = childAfter(child); |
524 | } |
525 | return height; |
526 | } |
527 | } |
528 | |
529 | @override |
530 | double? computeDistanceToActualBaseline(TextBaseline baseline) { |
531 | return defaultComputeDistanceToHighestActualBaseline(baseline); |
532 | } |
533 | |
534 | double _getMainAxisExtent(Size childSize) { |
535 | return switch (direction) { |
536 | Axis.horizontal => childSize.width, |
537 | Axis.vertical => childSize.height, |
538 | }; |
539 | } |
540 | |
541 | double _getCrossAxisExtent(Size childSize) { |
542 | return switch (direction) { |
543 | Axis.horizontal => childSize.height, |
544 | Axis.vertical => childSize.width, |
545 | }; |
546 | } |
547 | |
548 | Offset _getOffset(double mainAxisOffset, double crossAxisOffset) { |
549 | return switch (direction) { |
550 | Axis.horizontal => Offset(mainAxisOffset, crossAxisOffset), |
551 | Axis.vertical => Offset(crossAxisOffset, mainAxisOffset), |
552 | }; |
553 | } |
554 | |
555 | (bool flipHorizontal, bool flipVertical) get _areAxesFlipped { |
556 | final bool flipHorizontal = switch (textDirection ?? TextDirection.ltr) { |
557 | TextDirection.ltr => false, |
558 | TextDirection.rtl => true, |
559 | }; |
560 | final bool flipVertical = switch (verticalDirection) { |
561 | VerticalDirection.down => false, |
562 | VerticalDirection.up => true, |
563 | }; |
564 | return switch (direction) { |
565 | Axis.horizontal => (flipHorizontal, flipVertical), |
566 | Axis.vertical => (flipVertical, flipHorizontal), |
567 | }; |
568 | } |
569 | |
570 | @override |
571 | double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { |
572 | if (firstChild == null) { |
573 | return null; |
574 | } |
575 | final BoxConstraints childConstraints = switch (direction) { |
576 | Axis.horizontal => BoxConstraints(maxWidth: constraints.maxWidth), |
577 | Axis.vertical => BoxConstraints(maxHeight: constraints.maxHeight), |
578 | }; |
579 | |
580 | final (_AxisSize childrenAxisSize, List<_RunMetrics> runMetrics) = _computeRuns(constraints, ChildLayoutHelper.dryLayoutChild); |
581 | final _AxisSize containerAxisSize = childrenAxisSize.applyConstraints(constraints, direction); |
582 | |
583 | BaselineOffset baselineOffset = BaselineOffset.noBaseline; |
584 | void findHighestBaseline(Offset offset, RenderBox child) { |
585 | baselineOffset = baselineOffset.minOf(BaselineOffset(child.getDryBaseline(childConstraints, baseline)) + offset.dy); |
586 | } |
587 | Size getChildSize(RenderBox child) => child.getDryLayout(childConstraints); |
588 | _positionChildren(runMetrics, childrenAxisSize, containerAxisSize, findHighestBaseline, getChildSize); |
589 | return baselineOffset.offset; |
590 | } |
591 | |
592 | @override |
593 | @protected |
594 | Size computeDryLayout(covariant BoxConstraints constraints) { |
595 | return _computeDryLayout(constraints); |
596 | } |
597 | |
598 | Size _computeDryLayout(BoxConstraints constraints, [ChildLayouter layoutChild = ChildLayoutHelper.dryLayoutChild]) { |
599 | final (BoxConstraints childConstraints, double mainAxisLimit) = switch (direction) { |
600 | Axis.horizontal => (BoxConstraints(maxWidth: constraints.maxWidth), constraints.maxWidth), |
601 | Axis.vertical => (BoxConstraints(maxHeight: constraints.maxHeight), constraints.maxHeight), |
602 | }; |
603 | |
604 | double mainAxisExtent = 0.0; |
605 | double crossAxisExtent = 0.0; |
606 | double runMainAxisExtent = 0.0; |
607 | double runCrossAxisExtent = 0.0; |
608 | int childCount = 0; |
609 | RenderBox? child = firstChild; |
610 | while (child != null) { |
611 | final Size childSize = layoutChild(child, childConstraints); |
612 | final double childMainAxisExtent = _getMainAxisExtent(childSize); |
613 | final double childCrossAxisExtent = _getCrossAxisExtent(childSize); |
614 | // There must be at least one child before we move on to the next run. |
615 | if (childCount > 0 && runMainAxisExtent + childMainAxisExtent + spacing > mainAxisLimit) { |
616 | mainAxisExtent = math.max(mainAxisExtent, runMainAxisExtent); |
617 | crossAxisExtent += runCrossAxisExtent + runSpacing; |
618 | runMainAxisExtent = 0.0; |
619 | runCrossAxisExtent = 0.0; |
620 | childCount = 0; |
621 | } |
622 | runMainAxisExtent += childMainAxisExtent; |
623 | runCrossAxisExtent = math.max(runCrossAxisExtent, childCrossAxisExtent); |
624 | if (childCount > 0) { |
625 | runMainAxisExtent += spacing; |
626 | } |
627 | childCount += 1; |
628 | child = childAfter(child); |
629 | } |
630 | crossAxisExtent += runCrossAxisExtent; |
631 | mainAxisExtent = math.max(mainAxisExtent, runMainAxisExtent); |
632 | |
633 | return constraints.constrain(switch (direction) { |
634 | Axis.horizontal => Size(mainAxisExtent, crossAxisExtent), |
635 | Axis.vertical => Size(crossAxisExtent, mainAxisExtent), |
636 | }); |
637 | } |
638 | |
639 | static Size _getChildSize(RenderBox child) => child.size; |
640 | static void _setChildPosition(Offset offset, RenderBox child) { |
641 | (child.parentData! as WrapParentData).offset = offset; |
642 | } |
643 | |
644 | bool _hasVisualOverflow = false; |
645 | |
646 | @override |
647 | void performLayout() { |
648 | final BoxConstraints constraints = this.constraints; |
649 | assert(_debugHasNecessaryDirections); |
650 | if (firstChild == null) { |
651 | size = constraints.smallest; |
652 | _hasVisualOverflow = false; |
653 | return; |
654 | } |
655 | |
656 | final (_AxisSize childrenAxisSize, List<_RunMetrics> runMetrics) = _computeRuns(constraints, ChildLayoutHelper.layoutChild); |
657 | final _AxisSize containerAxisSize = childrenAxisSize.applyConstraints(constraints, direction); |
658 | size = containerAxisSize.toSize(direction); |
659 | final _AxisSize freeAxisSize = containerAxisSize - childrenAxisSize; |
660 | _hasVisualOverflow = freeAxisSize.mainAxisExtent < 0.0 || freeAxisSize.crossAxisExtent < 0.0; |
661 | _positionChildren(runMetrics, freeAxisSize, containerAxisSize, _setChildPosition, _getChildSize); |
662 | } |
663 | |
664 | (_AxisSize childrenSize, List<_RunMetrics> runMetrics) _computeRuns(BoxConstraints constraints, ChildLayouter layoutChild) { |
665 | assert(firstChild != null); |
666 | final (BoxConstraints childConstraints, double mainAxisLimit) = switch (direction) { |
667 | Axis.horizontal => (BoxConstraints(maxWidth: constraints.maxWidth), constraints.maxWidth), |
668 | Axis.vertical => (BoxConstraints(maxHeight: constraints.maxHeight), constraints.maxHeight), |
669 | }; |
670 | |
671 | final (bool flipMainAxis, _) = _areAxesFlipped; |
672 | final double spacing = this.spacing; |
673 | final List<_RunMetrics> runMetrics = <_RunMetrics>[]; |
674 | |
675 | _RunMetrics? currentRun; |
676 | _AxisSize childrenAxisSize = _AxisSize.empty; |
677 | for (RenderBox? child = firstChild; child != null; child = childAfter(child)) { |
678 | final _AxisSize childSize = _AxisSize.fromSize(size: layoutChild(child, childConstraints), direction: direction); |
679 | final _RunMetrics? newRun = currentRun == null |
680 | ? _RunMetrics(child, childSize) |
681 | : currentRun.tryAddingNewChild(child, childSize, flipMainAxis, spacing, mainAxisLimit); |
682 | if (newRun != null) { |
683 | runMetrics.add(newRun); |
684 | childrenAxisSize += currentRun?.axisSize.flipped ?? _AxisSize.empty; |
685 | currentRun = newRun; |
686 | } |
687 | } |
688 | assert(runMetrics.isNotEmpty); |
689 | final double totalRunSpacing = runSpacing * (runMetrics.length - 1); |
690 | childrenAxisSize += _AxisSize(mainAxisExtent: totalRunSpacing, crossAxisExtent: 0.0) + currentRun!.axisSize.flipped; |
691 | return (childrenAxisSize.flipped, runMetrics); |
692 | } |
693 | |
694 | void _positionChildren(List<_RunMetrics> runMetrics, _AxisSize freeAxisSize, _AxisSize containerAxisSize, _PositionChild positionChild, _GetChildSize getChildSize) { |
695 | assert(runMetrics.isNotEmpty); |
696 | |
697 | final double spacing = this.spacing; |
698 | |
699 | final double crossAxisFreeSpace = math.max(0.0, freeAxisSize.crossAxisExtent); |
700 | |
701 | final (bool flipMainAxis, bool flipCrossAxis) = _areAxesFlipped; |
702 | final WrapCrossAlignment effectiveCrossAlignment = flipCrossAxis ? crossAxisAlignment._flipped : crossAxisAlignment; |
703 | final (double runLeadingSpace, double runBetweenSpace) = runAlignment._distributeSpace( |
704 | crossAxisFreeSpace, |
705 | runSpacing, |
706 | runMetrics.length, |
707 | flipCrossAxis, |
708 | ); |
709 | final _NextChild nextChild = flipMainAxis ? childBefore : childAfter; |
710 | |
711 | double runCrossAxisOffset = runLeadingSpace; |
712 | final Iterable<_RunMetrics> runs = flipCrossAxis ? runMetrics.reversed : runMetrics; |
713 | for (final _RunMetrics run in runs) { |
714 | final double runCrossAxisExtent = run.axisSize.crossAxisExtent; |
715 | final int childCount = run.childCount; |
716 | |
717 | final double mainAxisFreeSpace = math.max(0.0, containerAxisSize.mainAxisExtent - run.axisSize.mainAxisExtent); |
718 | final (double childLeadingSpace, double childBetweenSpace) = alignment._distributeSpace(mainAxisFreeSpace, spacing, childCount, flipMainAxis); |
719 | |
720 | double childMainAxisOffset = childLeadingSpace; |
721 | |
722 | int remainingChildCount = run.childCount; |
723 | for (RenderBox? child = run.leadingChild; child != null && remainingChildCount > 0; child = nextChild(child), remainingChildCount -= 1) { |
724 | final _AxisSize(mainAxisExtent: double childMainAxisExtent, crossAxisExtent: double childCrossAxisExtent) = _AxisSize.fromSize(size: getChildSize(child), direction: direction); |
725 | final double childCrossAxisOffset = effectiveCrossAlignment._alignment * (runCrossAxisExtent - childCrossAxisExtent); |
726 | positionChild(_getOffset(childMainAxisOffset, runCrossAxisOffset + childCrossAxisOffset), child); |
727 | childMainAxisOffset += childMainAxisExtent + childBetweenSpace; |
728 | } |
729 | runCrossAxisOffset += runCrossAxisExtent + runBetweenSpace; |
730 | } |
731 | } |
732 | |
733 | @override |
734 | bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { |
735 | return defaultHitTestChildren(result, position: position); |
736 | } |
737 | |
738 | @override |
739 | void paint(PaintingContext context, Offset offset) { |
740 | // TODO(ianh): move the debug flex overflow paint logic somewhere common so |
741 | // it can be reused here |
742 | if (_hasVisualOverflow && clipBehavior != Clip.none) { |
743 | _clipRectLayer.layer = context.pushClipRect( |
744 | needsCompositing, |
745 | offset, |
746 | Offset.zero & size, |
747 | defaultPaint, |
748 | clipBehavior: clipBehavior, |
749 | oldLayer: _clipRectLayer.layer, |
750 | ); |
751 | } else { |
752 | _clipRectLayer.layer = null; |
753 | defaultPaint(context, offset); |
754 | } |
755 | } |
756 | |
757 | final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>(); |
758 | |
759 | @override |
760 | void dispose() { |
761 | _clipRectLayer.layer = null; |
762 | super.dispose(); |
763 | } |
764 | |
765 | @override |
766 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
767 | super.debugFillProperties(properties); |
768 | properties.add(EnumProperty<Axis>('direction' , direction)); |
769 | properties.add(EnumProperty<WrapAlignment>('alignment' , alignment)); |
770 | properties.add(DoubleProperty('spacing' , spacing)); |
771 | properties.add(EnumProperty<WrapAlignment>('runAlignment' , runAlignment)); |
772 | properties.add(DoubleProperty('runSpacing' , runSpacing)); |
773 | properties.add(DoubleProperty('crossAxisAlignment' , runSpacing)); |
774 | properties.add(EnumProperty<TextDirection>('textDirection' , textDirection, defaultValue: null)); |
775 | properties.add(EnumProperty<VerticalDirection>('verticalDirection' , verticalDirection, defaultValue: VerticalDirection.down)); |
776 | } |
777 | } |
778 | |