1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
//! Per-line placement: prepare items, distribute main-axis space, run
//! `justify-content` and `align-items`, recurse into the element registry.
use crate::{
html::box_edges::BoxEdges,
html::css_parse::{
AlignContent, AlignItems, FlexDirection, FlexWrap, GapSizes, JustifyContent, clamp_min_max,
},
layout::{
BoxConstraint, Cursor, LayoutBox, LayoutContext, OuterDisplay, PaintStyle, Rect,
layoutbox::{translate_subtree, wrap_runs},
},
tdom::node::{NodeArena, NodeId},
};
use super::algorithm::{
align_content_spacing, align_on_cross_axis, distribute_main_axis, effective_align,
justify_spacing, wrap_into_lines,
};
use super::container::{AnonymousTextItem, FLEX_ITEM_MIN_CROSS, FlexItemPrep, FlexItemSource};
use super::prepare::{natural_cross_axis_size_for_prep, resolve_item_cross_size};
/// Lay out every flex item across one or more lines and return the
/// positioned children plus the vertical extent they consume (used to size
/// the container).
#[allow(clippy::too_many_arguments)]
pub(super) fn layout_flex_items(
arena: &NodeArena,
container_node_id: NodeId,
mut preps: Vec<FlexItemPrep>,
direction: FlexDirection,
wrap: FlexWrap,
justify: JustifyContent,
align_items: AlignItems,
align_content: AlignContent,
gap: GapSizes,
content: BoxConstraint,
content_y: usize,
context: LayoutContext,
has_explicit_height: bool,
container_height: usize,
) -> (Vec<LayoutBox>, usize) {
// 1. Sort every prepared item by `order`.
preps.sort_by_key(|p| p.order);
// 2. Split into lines based on `wrap`. The wrap threshold is the main
// axis size: the container width for rows, the explicit height for
// columns (an indefinite column has no wrap threshold).
let available_main = match direction.is_row() {
true => content.width,
false if has_explicit_height => container_height,
false => usize::MAX,
};
let lines: Vec<Vec<usize>> = match wrap {
FlexWrap::NoWrap => vec![(0..preps.len()).collect()],
FlexWrap::Wrap | FlexWrap::WrapReverse => {
wrap_into_lines(&preps, direction, available_main, gap)
}
};
// 3. Lay out each line and stack them along the cross axis.
let mut placed: Vec<LayoutBox> = Vec::with_capacity(preps.len());
// `current_cross` is the cursor on the axis perpendicular to the
// flex main axis: vertical for rows, horizontal for columns.
let cross_origin = match direction.is_row() {
true => content_y,
false => content.x,
};
let mut current_cross: usize = cross_origin;
let mut total_main: usize = 0;
let line_count = lines.len();
let (cross_gap, main_gap) = match direction.is_row() {
true => (gap.row, gap.column),
false => (gap.column, gap.row),
};
// Actual cross-axis extent of each line once its items are laid out
// (relative to the line origin). An item can grow taller than the
// natural max-content measure assumed (text wrapping at the item's
// final width), so the line / container must expand to cover it.
let mut line_extents: Vec<usize> = Vec::with_capacity(line_count);
// Number of boxes each line actually contributed to `placed`. A line can
// place fewer boxes than it has preps (an item whose dispatch returns
// `None` — an unimplemented formatting context, a handlerless tag — only
// logs a warning), so the `align-content` pass must walk `placed` by
// these counts instead of assuming one box per prep.
let mut line_box_counts: Vec<usize> = Vec::with_capacity(line_count);
// Pre-measure every line's natural cross size so `align-content:
// stretch` can grow the lines (filling the container edge-to-edge — the
// browser semantics) instead of spacing them. Non-stretch values keep
// the classic leading / between offset computed from the same free
// space. A row container without an explicit height has no definite
// cross axis, so `stretch` is a no-op there (CSS).
let mut natural_crosses: Vec<usize> = Vec::with_capacity(line_count);
for line_indices in &lines {
let line_preps: Vec<&FlexItemPrep> = line_indices.iter().map(|&i| &preps[i]).collect();
natural_crosses.push(
line_preps
.iter()
.map(|p| {
natural_cross_axis_size_for_prep(p, direction)
.saturating_add(p.margin_cross_before)
.saturating_add(p.margin_cross_after)
})
.max()
.unwrap_or(FLEX_ITEM_MIN_CROSS)
.max(FLEX_ITEM_MIN_CROSS),
);
}
let stretch_cross_axis = match direction.is_row() {
true => has_explicit_height.then_some(container_height),
false => Some(content.width),
};
let total_natural_cross: usize = natural_crosses
.iter()
.enumerate()
.map(|(i, &n)| n + if i + 1 < line_count { cross_gap } else { 0 })
.sum();
let align_free_space = stretch_cross_axis
.map(|size| size.saturating_sub(total_natural_cross))
.unwrap_or(0);
// `stretch` grows every line equally; the remainder is left unused
// (the last line ends short of the container edge, matching the
// browser's float math once truncated to integer cells).
let stretch_extra = if matches!(align_content, AlignContent::Stretch) && line_count > 1 {
align_free_space / line_count
} else {
0
};
for (line_idx, line_indices) in lines.iter().enumerate() {
// 3a. Distribute the line's main-axis space.
let line_preps: Vec<&FlexItemPrep> = line_indices.iter().map(|&i| &preps[i]).collect();
// The main-axis size is definite for `row` (always `content.width`)
// and for `column` when the container has an explicit height (from
// CSS `height` or a preallocated rect). A `column` without an
// explicit height is *indefinite* — its main size is determined by
// the items themselves, so there is no free space for `flex-grow`
// to consume (CSS Flexbox §9.7). `indefinite` is forwarded to
// `distribute_main_axis` so it can skip grow/shrink rather than
// distribute an `usize::MAX` free space that overflows `isize`.
let indefinite = direction.is_column() && !has_explicit_height;
let line_main_size = match direction.is_row() {
true => content.width,
false if has_explicit_height => container_height,
false => usize::MAX,
};
let (main_sizes, free_for_justify) =
distribute_main_axis(&line_preps, line_main_size, main_gap, indefinite);
// 3b. Natural cross-axis size of this line (pre-measured above).
let natural_line_cross = natural_crosses[line_idx];
// 3c. Effective cross-axis size. The cross axis swaps between the
// two directions, so this is direction-aware:
//
// - `row` direction: cross axis is vertical. Explicit
// container height wins (and only for a single line —
// otherwise every line would overflow the container).
// Without an explicit height we use the natural line cross
// to avoid a circular dependency where stretching items
// would silently enlarge the container.
// - `column` direction: cross axis is horizontal. The
// container's content width is always known (it's
// `content.width`), so the line stretches to that
// regardless of an explicit height. This must NOT be
// `max`ed with the natural (max-content) line cross: the
// intrinsic width is width-independent, so a column whose
// content is wider than the viewport (e.g. a sidebar +
// main layout on a narrow terminal) would size every line
// to the intrinsic width and overflow the container. A
// multi-line column container has no concept of
// cross-axis stretching per line — lines stack along the
// main axis, so we keep the natural line cross for them.
let line_cross = match direction.is_row() {
true => {
if has_explicit_height && line_count == 1 {
// A single-line row container with an explicit height
// sizes its line to that height exactly (CSS Flexbox
// §9.4): `align-items: stretch` fills the container and
// must not grow past it — an item stretched to the
// container height whose content is taller keeps its
// assigned rect, and the content is bounded by that
// item's own clip / scroll handling (or spills past it
// browser-style when it does not clip; the item's own
// height resolution grows non-clipping items back to
// their content).
container_height
} else {
natural_line_cross.saturating_add(stretch_extra)
}
}
false => {
if line_count == 1 {
content.width
} else {
natural_line_cross.saturating_add(stretch_extra)
}
}
};
// 3d. Compute justify-content leading + between spacing.
//
// CSS Flexbox §8.1: `margin: auto` on a flex item absorbs the line's
// free space *before* `justify-content` applies — and any auto
// margin disables justify-content for that line entirely. Auto
// margins split the free space equally (integer cells; the earliest
// auto margins absorb the remainder).
let auto_count: usize = line_preps
.iter()
.map(|p| p.margin_auto_main.0 as usize + p.margin_auto_main.1 as usize)
.sum();
let (justify_leading, justify_between) = if auto_count > 0 {
(0, 0)
} else {
justify_spacing(justify, free_for_justify, line_preps.len())
};
let auto_shares: Vec<(usize, usize)> = match free_for_justify.checked_div(auto_count) {
None => Vec::new(),
Some(base) => {
let mut rem = free_for_justify % auto_count;
line_preps
.iter()
.map(|p| {
let mut before = if p.margin_auto_main.0 { base } else { 0 };
let mut after = if p.margin_auto_main.1 { base } else { 0 };
if rem > 0 && p.margin_auto_main.0 {
before += 1;
rem -= 1;
}
if rem > 0 && p.margin_auto_main.1 {
after += 1;
rem -= 1;
}
(before, after)
})
.collect()
}
};
// 3e. Per-line placement, factored so the line's cross size can be
// measured first and the line re-placed at the final size. An item's
// content can wrap taller than the natural max-content measure
// (e.g. `812 MB/s` in a narrow card), so the first pass measures the
// real line extent; the second pass stretches every `align-items:
// stretch` item to it — matching a browser where the line's cross
// size is the largest item's, and the items fill it evenly.
let line_origin = current_cross;
let place_line = |line_cross: usize| -> (Vec<LayoutBox>, usize, usize) {
let mut boxes: Vec<LayoutBox> = Vec::with_capacity(line_preps.len());
let mut line_bottom: usize = line_origin.saturating_add(line_cross);
let mut cursor_main: usize = match direction.is_row() {
true => content.x,
false => content_y,
};
cursor_main = cursor_main.saturating_add(justify_leading);
// Main-axis space the line actually occupies (row: sum of item
// widths; column: sum of item heights, which can exceed the
// distributed sizes when wrapped text grows an item).
let mut placed_main: usize = 0;
// The line's shared text baseline (row direction): the largest
// first-text-row offset among `baseline`-aligned items, so those
// items' first text rows coincide (the terminal cell-grid
// analogue of the CSS baseline).
let line_baseline = line_preps
.iter()
.filter(|p| {
direction.is_row()
&& matches!(
effective_align(p.align_self, align_items),
AlignItems::Baseline
)
})
.map(|p| p.baseline)
.max()
.unwrap_or(0);
for (prep_idx, prep) in line_preps.iter().enumerate() {
// CSS `min-width` / `max-width` (row) or `min-height` /
// `max-height` (column) clamp the final main-axis size. The
// automatic min-content floor (`min_main`) is already enforced
// by the shrink distribution; explicit constraints layer on top
// through the canonical clamp (min wins over max, CSS).
let lower = prep.min_main.max(prep.css_min_main.unwrap_or(0));
let main_size = clamp_min_max(main_sizes[prep_idx], Some(lower), prep.css_max_main);
// Natural cross-axis size of this item, measured once at prep
// time (used as the fallback for non-stretch alignments and
// for stretch when the container's cross-axis size is
// unknown).
let item_natural_cross = natural_cross_axis_size_for_prep(prep, direction);
let item_cross_size = resolve_item_cross_size(
prep.align_self,
line_cross,
item_natural_cross,
(prep.margin_cross_before, prep.margin_cross_after),
prep.css_cross,
);
let align = effective_align(prep.align_self, align_items);
// `margin: auto` shares are folded into the effective
// margins: they occupy the line exactly like concrete
// margins, pushing the item (and its siblings) into place.
let (auto_before, auto_after) =
auto_shares.get(prep_idx).copied().unwrap_or((0, 0));
let margin_before = prep.margin_main_before.saturating_add(auto_before);
let margin_after = prep.margin_main_after.saturating_add(auto_after);
// Cross-axis alignment operates on the item's *margin-box*
// (browsers align the margin box within the line). The
// leading margin then offsets the border-box from the
// margin-box top.
let margin_box = item_cross_size
.saturating_add(prep.margin_cross_before)
.saturating_add(prep.margin_cross_after);
let (item_x, item_y, item_w, item_h) = match direction.is_row() {
true => {
let y = if matches!(align, AlignItems::Baseline) {
// Baseline: the item's first text row sits on the
// line's shared baseline.
line_origin
.saturating_add(line_baseline)
.saturating_sub(prep.baseline)
} else {
align_on_cross_axis(line_origin, line_cross, margin_box, align)
.saturating_add(prep.margin_cross_before)
};
(
cursor_main.saturating_add(margin_before),
y,
main_size,
item_cross_size,
)
}
false => {
let x = align_on_cross_axis(line_origin, line_cross, margin_box, align)
.saturating_add(prep.margin_cross_before);
(
x,
cursor_main.saturating_add(margin_before),
item_cross_size,
main_size,
)
}
};
let item_rect = Rect::new(item_x, item_y, item_w.max(1), item_h.max(1));
let child_box = match &prep.source {
FlexItemSource::Node(item_id) => {
crate::layout::inner_formatting_context::flow::dispatch(
arena,
*item_id,
// Percentage margins / paddings (and `width` / `min-*` /
// `max-*` percentages) of a flex item resolve against
// the flex container's content-box width — the item's
// own allocated size is already fixed by
// `pre_allocated_rect` and must not feed back into
// percentage resolution.
BoxConstraint {
x: item_x,
width: content.width,
},
&mut Cursor {
y: item_y,
prev_bottom_margin: 0,
},
context,
Some(item_rect),
)
}
FlexItemSource::AnonymousText(text) => {
Some(layout_anonymous_text(text, item_rect, container_node_id))
}
};
// Main-axis extent the item actually occupies, measured from
// the placement origin. For `row` it equals the distributed
// width; for `column` the laid-out box can grow taller than
// the distributed main size (wrapped text), and the next item
// must start below the real extent instead of overlapping it.
let mut actual_main_extent: usize = 0;
if let Some(child_box) = child_box {
// A flex item keeps its own inner formatting context (a
// plain `<div>` item remains `Flow`). The flex container
// itself is what establishes `FormattingContext::Flex`
// for its children, not each child.
//
// The item's content can exceed the pre-allocated cross
// size (text wrapping at the final width makes a card
// taller than the max-content measure), so grow the line
// to the actual box extent — otherwise the container's
// paint clip cuts the item's bottom border and its last
// row bleeds into the border.
line_bottom = line_bottom.max(
child_box
.rect
.y
.saturating_add(child_box.rect.height.max(1))
// The item's trailing cross-axis margin sits below
// its border-box, so the line must cover it too
// (the line is the largest item's margin-box).
.saturating_add(prep.margin_cross_after),
);
if direction.is_column() {
actual_main_extent = child_box
.rect
.y
.saturating_add(child_box.rect.height)
.saturating_sub(cursor_main);
}
boxes.push(child_box);
} else if let FlexItemSource::Node(item_id) = &prep.source {
log::warn!(
"No layout handler for flex item {:?}",
arena[*item_id].node_type
);
}
let step = match direction.is_row() {
true => main_size,
false => main_size.max(actual_main_extent),
};
// The item's margins occupy the line on both sides of its
// border-box and advance the next item.
let step = step
.saturating_add(margin_before)
.saturating_add(margin_after);
cursor_main = cursor_main.saturating_add(step);
placed_main = placed_main.saturating_add(step);
if prep_idx + 1 < line_preps.len() {
cursor_main = cursor_main.saturating_add(main_gap);
cursor_main = cursor_main.saturating_add(justify_between);
placed_main = placed_main.saturating_add(main_gap);
placed_main = placed_main.saturating_add(justify_between);
}
}
// The line's cross-axis extent used to stack the next line:
// rows grow vertically (wrapped content), columns stack along
// the fixed content width (the line cross), never the item
// heights (which are the column *main* axis).
let extent = if direction.is_row() {
line_bottom.saturating_sub(line_origin)
} else {
line_cross
};
(boxes, extent, placed_main)
};
// Measurement pass at the tentative (natural) cross size, then a
// placement pass at the final size when items wrapped taller. The
// re-layout only happens when the final line cross differs.
let (first_boxes, first_extent, first_placed_main) = place_line(line_cross);
// For `row` direction the cross axis is vertical, so wrapped content
// can grow the line and the re-place stretches `align-items: stretch`
// items to the real height. For `column` direction the cross axis is
// the container's fixed content width: `first_extent` here is the
// line's *main-axis* (height) extent, so using it as the cross size
// would re-place every item WIDER than the container whenever the
// content is taller than the viewport is wide (a dashboard on a
// narrow terminal) — the overflow. A column's line cross is always
// the container width, so no re-place.
let final_line_cross = match direction.is_row() {
true => first_extent.max(line_cross),
false => line_cross,
};
let (line_boxes, actual_extent, placed_main) = if final_line_cross > line_cross {
place_line(final_line_cross)
} else {
(first_boxes, first_extent, first_placed_main)
};
let line_box_count = line_boxes.len();
placed.extend(line_boxes);
// The container height is the line's main-axis extent in column
// direction. `current_cross` tracks the perpendicular axis and is
// therefore not suitable for sizing a column's block-height. The
// actual `placed_main` is used so wrapped (grown) items extend the
// container instead of being clipped by it.
total_main = total_main.max(justify_leading.saturating_add(placed_main));
line_extents.push(actual_extent);
line_box_counts.push(line_box_count);
// 3f. Advance the cross-axis cursor for the next line.
current_cross = line_origin.saturating_add(actual_extent);
if line_idx + 1 < line_count {
current_cross = current_cross.saturating_add(cross_gap);
}
}
let total_cross = current_cross.saturating_sub(cross_origin);
// 4. Apply `align-content` to shift the stacked lines. We do this
// by re-translating each already-placed child's cross-axis
// coordinate by the same delta. The available cross-axis size is
// direction-aware: row containers may use an explicit height,
// while column containers use their known content width.
if line_count > 1 {
let cross_axis_size = match direction.is_row() {
true => {
if has_explicit_height {
container_height
} else {
total_cross
}
}
false => content.width,
};
let total_lines_cross: usize = line_extents
.iter()
.copied()
.enumerate()
.map(|(i, ext)| ext + if i + 1 < line_count { cross_gap } else { 0 })
.sum();
let free_space = cross_axis_size.saturating_sub(total_lines_cross);
let (leading, between) = align_content_spacing(align_content, free_space, line_count);
let mut shift: usize = leading;
let mut running_offset: usize = 0;
// Walk `placed` by the per-line box counts: a line whose item failed
// to lay out contributes fewer boxes than preps, and indexing by the
// prep grouping would run past the vec (or shift the wrong boxes).
for (i, count) in line_box_counts.iter().enumerate() {
if i > 0 {
shift = shift.saturating_add(between);
}
for child in &mut placed[running_offset..running_offset + count] {
let delta = match direction.is_row() {
true => (0, shift as i64),
false => (shift as i64, 0),
};
// Rects are absolute canvas coordinates: the item's laid-out
// subtree must move with its border-box, not just the
// border-box itself.
translate_subtree(child, delta.0, delta.1);
}
running_offset = running_offset.saturating_add(*count);
}
}
// 5. Reverse axes. `row-reverse` / `column-reverse` mirror each item
// along the main axis (free space moves to the other side, which is
// exactly what the reversed `justify-content` semantics need);
// `wrap-reverse` mirrors the stacked lines along the cross axis.
// Mirroring *after* `align-content` maps its top-anchored
// distribution to the bottom-anchored one wrap-reverse implies.
let main_origin = match direction.is_row() {
true => content.x,
false => content_y,
};
let main_end = match direction.is_row() {
true => content.x.saturating_add(content.width),
false => {
if has_explicit_height {
content_y.saturating_add(container_height)
} else {
content_y.saturating_add(total_main)
}
}
};
if direction.is_reverse() {
for child in &mut placed {
let (main_pos, main_size) = if direction.is_row() {
(child.rect.x, child.rect.width)
} else {
(child.rect.y, child.rect.height)
};
let new_main = main_end
.saturating_sub(main_pos.saturating_sub(main_origin))
.saturating_sub(main_size);
// Rects are absolute canvas coordinates: mirror the laid-out
// subtree together with the item's border-box.
if direction.is_row() {
translate_subtree(child, new_main as i64 - main_pos as i64, 0);
} else {
translate_subtree(child, 0, new_main as i64 - main_pos as i64);
}
}
}
if matches!(wrap, FlexWrap::WrapReverse) {
// The mirror axis is the container's cross-axis extent (a definite
// height for rows, the content width for columns), so wrapped lines
// anchor to the cross-end — matching a browser where `wrap-reverse`
// stacks lines from the cross-start (the bottom / right edge). Using
// the *content* extent instead would float the lines in the middle
// of the container whenever it has free cross space.
let cross_end = match direction.is_row() {
true if has_explicit_height => cross_origin.saturating_add(container_height),
false => cross_origin.saturating_add(content.width),
_ => cross_origin.saturating_add(total_cross),
};
for child in &mut placed {
let (cross_pos, cross_size) = if direction.is_row() {
(child.rect.y, child.rect.height)
} else {
(child.rect.x, child.rect.width)
};
let new_cross = cross_end
.saturating_sub(cross_pos.saturating_sub(cross_origin))
.saturating_sub(cross_size);
// Rects are absolute canvas coordinates: mirror the laid-out
// subtree together with the item's border-box.
if direction.is_row() {
translate_subtree(child, 0, new_cross as i64 - cross_pos as i64);
} else {
translate_subtree(child, new_cross as i64 - cross_pos as i64, 0);
}
}
}
let total_height = match direction.is_row() {
true => total_cross,
false => total_main,
};
(placed, total_height)
}
/// Lay out an anonymous text flex item: wrap its runs at the allocated width
/// and build a block box. Anonymous items have no DOM node of their own, so
/// the box reports the flex container's node id for hit-testing.
fn layout_anonymous_text(
text: &AnonymousTextItem,
rect: Rect,
container_node_id: NodeId,
) -> LayoutBox {
let wrapped = wrap_runs(
&text.runs,
rect.width.max(1),
text.font_size_cells,
text.white_space,
text.word_break,
text.overflow_wrap,
0,
);
let has_lines = !wrapped.is_empty() && wrapped.iter().any(|l| !l.is_empty());
let inline_rows = if has_lines {
wrapped.len().saturating_mul(text.line_height_cells.max(1))
} else {
0
};
let rect_height = rect.height.max(inline_rows.max(1));
let paint_style = PaintStyle {
box_edges: BoxEdges::default(),
runs: if has_lines { Some(wrapped) } else { None },
font_size_cells: text.font_size_cells,
line_height_cells: text.line_height_cells,
..PaintStyle::default()
};
LayoutBox::new_normal(
container_node_id,
Rect::new(rect.x, rect.y, rect.width, rect_height),
paint_style,
OuterDisplay::BlockLevel { container: true },
Vec::new(),
)
}