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