Skip to main content

laser_pdf/elements/
row.rs

1use crate::{
2    flex::{DrawLayout, MeasureLayout},
3    utils::max_optional_size,
4    *,
5};
6
7use super::none::NoneElement;
8
9/// A container that arranges child elements horizontally with flexible sizing.
10///
11/// Elements can be sized as self-sized, fixed width, or expanding to fill available space.
12/// The `expand` option makes all children the same height by passing the maximum height
13/// and break count as `preferred_height` and `preferred_height_break_count`, enabling
14/// features like bottom alignment and background fills.
15///
16/// The `preferred_height_break_count` represents the number of page breaks, with
17/// `preferred_height` being the height on the final page. For example, if
18/// `preferred_height_break_count = 2` and `preferred_height = 15.0`, it means
19/// \"break twice, then use 15mm on the final page\".
20///
21/// ## Flex System
22///
23/// - `Flex::SelfSized`: Element uses its natural width
24/// - `Flex::Fixed(width)`: Element uses the specified width  
25/// - `Flex::Expand(weight)`: Element gets a portion of remaining space based on weight
26///
27/// Remaining space is calculated as: total_width - sum(self_sized_widths) - sum(fixed_widths)
28/// Then distributed proportionally: element_width = remaining_space * (weight / total_weights)
29///
30/// ## Performance Note
31///
32/// When `expand: false`, only self-sized elements are measured in the first pass.
33/// When `expand: true`, all elements must be measured before drawing to determine
34/// the maximum height, which requires an additional measurement pass.
35pub struct Row<F: Fn(&mut RowContent)> {
36    /// Horizontal spacing between elements in millimeters
37    pub gap: f32,
38    /// Whether to expand all children to the same height by passing preferred_height
39    pub expand: bool,
40    /// Whether to collapse when all children have None height/width
41    pub collapse: bool,
42    /// Closure that gets called for adding the content.
43    ///
44    /// The closure is basically an internal iterator that produces elements by calling
45    /// [RowContent::add].
46    ///
47    /// This closure will be called at least twice because the non-expanded elements need to be
48    /// measured first. Depending on the surrounding context it could be called more than that
49    /// (though in real world layouts this effect should be minimal as not all containers need a
50    /// measure pass before drawing). Because of this it's beneficial to keep expensive computations
51    /// and allocations outside of this closure.
52    pub content: F,
53}
54
55impl<F: Fn(&mut RowContent)> Row<F> {
56    pub fn new(content: F) -> Self {
57        Row {
58            gap: 0.,
59            expand: false,
60            collapse: true,
61            content,
62        }
63    }
64
65    pub fn with_gap(self, gap: f32) -> Self {
66        Row { gap, ..self }
67    }
68
69    pub fn with_expand(self, expand: bool) -> Self {
70        Row { expand, ..self }
71    }
72
73    pub fn with_collapse(self, collapse: bool) -> Self {
74        Row { collapse, ..self }
75    }
76
77    pub fn expand(self) -> Self {
78        Row {
79            expand: true,
80            ..self
81        }
82    }
83}
84
85impl<F: Fn(&mut RowContent)> Element for Row<F> {
86    fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
87        FirstLocationUsage::WillUse
88    }
89
90    fn measure(&self, mut ctx: MeasureCtx) -> ElementSize {
91        let mut measure_layout = MeasureLayout::new(ctx.width.max, self.gap);
92
93        let mut max_height = None;
94
95        (self.content)(&mut RowContent {
96            text_pieces_cache: ctx.text_pieces_cache,
97            width: ctx.width,
98            first_height: ctx.first_height,
99            pass: Pass::MeasureNonExpanded {
100                layout: &mut measure_layout,
101                max_height: Some(&mut max_height),
102                breakable: ctx.breakable.as_mut(),
103            },
104        });
105
106        let mut width = measure_layout.no_expand_width();
107
108        let draw_layout = measure_layout.build();
109
110        (self.content)(&mut RowContent {
111            text_pieces_cache: ctx.text_pieces_cache,
112            width: ctx.width,
113            first_height: ctx.first_height,
114            pass: Pass::MeasureExpanded {
115                layout: &draw_layout,
116                max_height: &mut max_height,
117                width: if ctx.width.expand {
118                    None
119                } else {
120                    Some(&mut width)
121                },
122                width_expand: ctx.width.expand,
123                gap: self.gap,
124                breakable: ctx.breakable.as_mut(),
125            },
126        });
127
128        if !self.collapse {
129            if width.is_none() {
130                width = Some(0.);
131            }
132
133            if max_height.is_none() {
134                max_height = Some(0.);
135            }
136        }
137
138        ElementSize {
139            width: if ctx.width.expand {
140                Some(width.map(|w| w.max(ctx.width.max)).unwrap_or(ctx.width.max))
141            } else {
142                width
143            },
144            height: max_height,
145        }
146    }
147
148    fn draw(&self, mut ctx: DrawCtx) -> ElementSize {
149        let mut measure_layout = MeasureLayout::new(ctx.width.max, self.gap);
150
151        let mut max_height = None;
152
153        let mut break_count = 0;
154        let mut extra_location_min_height = None;
155
156        (self.content)(&mut RowContent {
157            text_pieces_cache: ctx.text_pieces_cache,
158            width: ctx.width,
159            first_height: ctx.first_height,
160            pass: Pass::MeasureNonExpanded {
161                layout: &mut measure_layout,
162                max_height: if self.expand {
163                    Some(&mut max_height)
164                } else {
165                    None
166                },
167                breakable: ctx
168                    .breakable
169                    .as_ref()
170                    .map(|b| BreakableMeasure {
171                        full_height: b.full_height,
172
173                        // in the non-expand case these will just be ignored
174                        break_count: &mut break_count,
175                        extra_location_min_height: &mut extra_location_min_height,
176                    })
177                    .as_mut(),
178            },
179        });
180
181        let draw_layout = measure_layout.build();
182
183        // If we want to expand all of the children to the same size we need an additional pass here
184        // to figure out the maximum height & break count of all of the children. This is part of
185        // the reason why expanding isn't just what Row always does.
186        if self.expand {
187            (self.content)(&mut RowContent {
188                text_pieces_cache: ctx.text_pieces_cache,
189                width: ctx.width,
190                first_height: ctx.first_height,
191                pass: Pass::MeasureExpanded {
192                    layout: &draw_layout,
193                    max_height: &mut max_height,
194                    width_expand: ctx.width.expand,
195                    width: None, // We'll get that from draw. No point in getting it twice.
196                    gap: self.gap,
197                    breakable: ctx
198                        .breakable
199                        .as_ref()
200                        .map(|b| BreakableMeasure {
201                            full_height: b.full_height,
202
203                            // in the non-expand case these will just be ignored
204                            break_count: &mut break_count,
205                            extra_location_min_height: &mut extra_location_min_height,
206                        })
207                        .as_mut(),
208                },
209            });
210
211            if let Some(ref mut b) = ctx.breakable {
212                match break_count.cmp(&b.preferred_height_break_count) {
213                    std::cmp::Ordering::Less => (),
214                    std::cmp::Ordering::Equal => {
215                        ctx.preferred_height = max_optional_size(ctx.preferred_height, max_height);
216                    }
217                    std::cmp::Ordering::Greater => {
218                        b.preferred_height_break_count = break_count;
219                        ctx.preferred_height = max_height;
220                    }
221                }
222            } else {
223                ctx.preferred_height = max_optional_size(ctx.preferred_height, max_height);
224            }
225        }
226
227        let mut width = None;
228
229        (self.content)(&mut RowContent {
230            text_pieces_cache: ctx.text_pieces_cache,
231            width: ctx.width,
232            first_height: ctx.first_height,
233            pass: Pass::Draw {
234                layout: &draw_layout,
235                max_height: &mut max_height,
236                width: &mut width,
237                width_expand: ctx.width.expand,
238                gap: self.gap,
239                pdf: ctx.pdf,
240                location: ctx.location,
241                preferred_height: ctx.preferred_height,
242                break_count: 0,
243                breakable: ctx.breakable.as_mut(),
244            },
245        });
246
247        if !self.collapse {
248            if width.is_none() {
249                width = Some(0.);
250            }
251
252            if max_height.is_none() {
253                max_height = Some(0.);
254            }
255        }
256
257        ElementSize {
258            width: if ctx.width.expand {
259                Some(width.map(|w| w.max(ctx.width.max)).unwrap_or(ctx.width.max))
260            } else {
261                width
262            },
263            height: max_height,
264        }
265    }
266}
267
268pub struct RowContent<'a, 'b, 'c> {
269    text_pieces_cache: &'a TextPiecesCache,
270    width: WidthConstraint,
271    first_height: f32,
272    pass: Pass<'a, 'b, 'c>,
273}
274
275enum Pass<'a, 'b, 'c> {
276    MeasureNonExpanded {
277        layout: &'a mut MeasureLayout,
278        max_height: Option<&'a mut Option<f32>>,
279        breakable: Option<&'a mut BreakableMeasure<'b>>,
280    },
281
282    FirstLocationUsage {},
283
284    MeasureExpanded {
285        layout: &'a DrawLayout,
286        max_height: &'a mut Option<f32>,
287        width: Option<&'a mut Option<f32>>,
288        width_expand: bool,
289        gap: f32,
290        breakable: Option<&'a mut BreakableMeasure<'b>>,
291    },
292
293    Draw {
294        layout: &'a DrawLayout,
295        max_height: &'a mut Option<f32>,
296        width: &'a mut Option<f32>,
297        width_expand: bool,
298
299        gap: f32,
300
301        pdf: &'c mut Pdf,
302        location: Location,
303
304        preferred_height: Option<f32>,
305        break_count: u32,
306        breakable: Option<&'a mut BreakableDraw<'b>>,
307    },
308}
309
310/// Flex behavior determining how row elements are sized horizontally.
311#[derive(Copy, Clone, Serialize, Deserialize)]
312pub enum Flex {
313    /// Expand to fill available space proportionally based on weight.
314    /// Remaining space after self-sized and fixed elements is distributed
315    /// proportionally: weight / sum_of_all_weights.
316    Expand(u8),
317    /// Use the element's natural width
318    SelfSized,
319    /// Use a fixed width in millimeters
320    Fixed(f32),
321}
322
323fn add_height(
324    max_height: &mut Option<f32>,
325    breakable: Option<&mut BreakableMeasure>,
326    size: ElementSize,
327    break_count: u32,
328    extra_location_min_height: Option<f32>,
329) {
330    if let Some(b) = breakable {
331        *b.extra_location_min_height =
332            max_optional_size(extra_location_min_height, *b.extra_location_min_height);
333
334        match break_count.cmp(b.break_count) {
335            std::cmp::Ordering::Less => (),
336            std::cmp::Ordering::Equal => {
337                *max_height = max_optional_size(*max_height, size.height);
338            }
339            std::cmp::Ordering::Greater => {
340                *b.break_count = break_count;
341                *max_height = size.height;
342            }
343        }
344    } else {
345        *max_height = max_optional_size(*max_height, size.height);
346    }
347}
348
349impl<'a, 'b, 'c> RowContent<'a, 'b, 'c> {
350    /// Add a flexible gap that expands to fill space.
351    ///
352    /// This is equivalent to adding a `NoneElement` with `Flex::Expand(gap)`.
353    /// Useful for pushing elements apart or centering them with flexible spacing.
354    pub fn flex_gap(&mut self, gap: u8) {
355        self.add(&NoneElement, Flex::Expand(gap));
356    }
357
358    pub fn add<E: Element>(&mut self, element: &E, flex: Flex) {
359        match self.pass {
360            Pass::MeasureNonExpanded {
361                layout: &mut ref mut layout,
362                ref mut max_height,
363                ref mut breakable,
364            } => match flex {
365                Flex::Expand(fraction) => {
366                    layout.add_expand(fraction);
367                }
368                Flex::SelfSized => {
369                    let mut break_count = 0;
370                    let mut extra_location_min_height = None;
371
372                    let size = element.measure(MeasureCtx {
373                        text_pieces_cache: self.text_pieces_cache,
374                        width: WidthConstraint {
375                            expand: false,
376                            ..self.width
377                        },
378                        first_height: self.first_height,
379                        breakable: breakable.as_deref_mut().map(|b| BreakableMeasure {
380                            full_height: b.full_height,
381                            break_count: &mut break_count,
382                            extra_location_min_height: &mut extra_location_min_height,
383                        }),
384                    });
385
386                    if let Some(max_height) = max_height {
387                        // if max_height is None we're not interested in height or breaks
388                        add_height(
389                            max_height,
390                            breakable.as_deref_mut(),
391                            size,
392                            break_count,
393                            extra_location_min_height,
394                        );
395                    }
396
397                    // elements with no width are collapsed
398                    if let Some(w) = size.width {
399                        layout.add_fixed(w);
400                    }
401                }
402                Flex::Fixed(width) => {
403                    layout.add_fixed(width);
404
405                    if let Some(max_height) = max_height {
406                        let mut break_count = 0;
407                        let mut extra_location_min_height = None;
408
409                        let size = element.measure(MeasureCtx {
410                            text_pieces_cache: self.text_pieces_cache,
411                            width: WidthConstraint {
412                                max: width,
413                                expand: true,
414                            },
415                            first_height: self.first_height,
416                            breakable: breakable.as_mut().map(|b| BreakableMeasure {
417                                full_height: b.full_height,
418                                break_count: &mut break_count,
419                                extra_location_min_height: &mut extra_location_min_height,
420                            }),
421                        });
422
423                        add_height(
424                            max_height,
425                            breakable.as_deref_mut(),
426                            size,
427                            break_count,
428                            extra_location_min_height,
429                        );
430                    }
431                }
432            },
433
434            Pass::MeasureExpanded {
435                layout,
436                max_height: &mut ref mut max_height,
437                ref mut width,
438                width_expand,
439                gap,
440                ref mut breakable,
441            } => match flex {
442                Flex::Expand(fraction) => {
443                    let element_width = layout.expand_width(fraction);
444
445                    let mut break_count = 0;
446                    let mut extra_location_min_height = None;
447
448                    let size = element.measure(MeasureCtx {
449                        text_pieces_cache: self.text_pieces_cache,
450                        width: WidthConstraint {
451                            max: element_width,
452                            expand: width_expand,
453                        },
454                        first_height: self.first_height,
455                        breakable: breakable.as_deref_mut().map(|b| BreakableMeasure {
456                            full_height: b.full_height,
457                            break_count: &mut break_count,
458                            extra_location_min_height: &mut extra_location_min_height,
459                        }),
460                    });
461
462                    add_height(
463                        max_height,
464                        breakable.as_deref_mut(),
465                        size,
466                        break_count,
467                        extra_location_min_height,
468                    );
469
470                    if let &mut Some(&mut ref mut width) = width {
471                        if let Some(w) = size.width {
472                            if let Some(width) = width {
473                                *width += gap + w;
474                            } else {
475                                *width = Some(w);
476                            }
477                        }
478                    }
479                }
480                Flex::SelfSized => (),
481                Flex::Fixed(_) => (),
482            },
483
484            Pass::Draw {
485                layout,
486                max_height: &mut ref mut max_height,
487                width: &mut ref mut width,
488                width_expand,
489                gap,
490                pdf: &mut ref mut pdf,
491                ref location,
492                preferred_height,
493                ref mut break_count,
494                ref mut breakable,
495            } => {
496                let width_constraint = match flex {
497                    Flex::Expand(fraction) => WidthConstraint {
498                        max: layout.expand_width(fraction),
499                        expand: width_expand,
500                    },
501                    Flex::SelfSized => WidthConstraint {
502                        max: self.width.max,
503                        expand: false,
504                    },
505                    Flex::Fixed(width) => WidthConstraint {
506                        max: width,
507                        expand: true,
508                    },
509                };
510
511                let mut element_break_count = 0;
512
513                let x_offset = if let &mut Some(width) = width {
514                    width + gap
515                } else {
516                    0.
517                };
518
519                let size = element.draw(DrawCtx {
520                    pdf,
521                    text_pieces_cache: self.text_pieces_cache,
522                    location: Location {
523                        pos: (location.pos.0 + x_offset, location.pos.1),
524                        ..location.clone()
525                    },
526
527                    width: width_constraint,
528                    first_height: self.first_height,
529                    preferred_height,
530
531                    // some trickery to get rust to make a temporary option that owns the closure
532                    breakable: breakable
533                        .as_deref_mut()
534                        .map(|b| {
535                            (
536                                b.full_height,
537                                b.preferred_height_break_count,
538                                |pdf: &mut Pdf, location_idx: u32, _| {
539                                    element_break_count = element_break_count.max(location_idx + 1);
540
541                                    let mut new_location = (b.do_break)(
542                                        pdf,
543                                        location_idx,
544                                        Some(if location_idx == 0 {
545                                            self.first_height
546                                        } else {
547                                            b.full_height
548                                        }),
549                                    );
550                                    new_location.pos.0 += x_offset;
551                                    new_location
552                                },
553                            )
554                        })
555                        .as_mut()
556                        .map(
557                            |&mut (
558                                full_height,
559                                preferred_height_break_count,
560                                ref mut get_location,
561                            )| {
562                                BreakableDraw {
563                                    full_height,
564                                    preferred_height_break_count,
565                                    do_break: get_location,
566                                }
567                            },
568                        ),
569                });
570
571                if breakable.is_some() {
572                    match element_break_count.cmp(break_count) {
573                        std::cmp::Ordering::Less => (),
574                        std::cmp::Ordering::Equal => {
575                            *max_height = max_optional_size(*max_height, size.height);
576                        }
577                        std::cmp::Ordering::Greater => {
578                            *break_count = element_break_count;
579                            *max_height = size.height;
580                        }
581                    }
582                } else {
583                    *max_height = max_optional_size(*max_height, size.height);
584                }
585
586                let mut width_add = |w| {
587                    if let Some(width) = width {
588                        *width += gap + w;
589                    } else {
590                        *width = Some(w);
591                    }
592                };
593
594                match (flex, width_expand) {
595                    (Flex::Expand(_), true) | (Flex::Fixed(_), _) => {
596                        width_add(width_constraint.max);
597                    }
598                    (Flex::Expand(_), false) | (Flex::SelfSized, _) => {
599                        if let Some(w) = size.width {
600                            width_add(w);
601                        }
602                    }
603                }
604            }
605
606            _ => todo!(),
607        }
608    }
609}
610
611#[cfg(test)]
612mod tests {
613    use super::*;
614    use crate::{
615        elements::{force_break::ForceBreak, none::NoneElement, rectangle::Rectangle},
616        test_utils::{build_element::BuildElementCtx, *},
617    };
618
619    #[test]
620    fn test_empty_row() {
621        let element = Row {
622            gap: 12.,
623            expand: true,
624            collapse: true,
625            content: |_content| {},
626        };
627
628        for output in ElementTestParams::default().run(&element) {
629            output.assert_size(ElementSize {
630                width: if output.width.expand {
631                    Some(output.width.max)
632                } else {
633                    None
634                },
635                height: None,
636            });
637
638            if let Some(b) = output.breakable {
639                b.assert_break_count(0);
640                b.assert_extra_location_min_height(None);
641            }
642        }
643
644        let element = Row {
645            gap: 12.,
646            expand: false,
647            collapse: true,
648            content: |_content| {},
649        };
650
651        for output in ElementTestParams::default().run(&element) {
652            output.assert_size(ElementSize {
653                width: if output.width.expand {
654                    Some(output.width.max)
655                } else {
656                    None
657                },
658                height: None,
659            });
660
661            if let Some(b) = output.breakable {
662                b.assert_break_count(0);
663                b.assert_extra_location_min_height(None);
664            }
665        }
666    }
667
668    #[test]
669    fn test_row_expand() {
670        test_row(true);
671    }
672
673    #[test]
674    fn test_row_no_expand() {
675        test_row(false);
676    }
677
678    fn test_row(expand: bool) {
679        use assert_passes::*;
680
681        let gap = 1.;
682
683        let element = BuildElement(
684            |BuildElementCtx {
685                 width,
686                 first_height,
687                 mut pass,
688             },
689             callback| {
690                let less_first_height = first_height == 6.;
691
692                // This way we don't have to duplicate the logic for this in every child. They
693                // should all get the same preferred height.
694                if let build_element::Pass::Draw {
695                    preferred_height,
696                    breakable,
697                } = &mut pass
698                {
699                    if preferred_height.is_some() || expand {
700                        if let Some(breakable) = breakable {
701                            let (height, break_count) =
702                                if less_first_height { (6., 2) } else { (12., 1) };
703
704                            *preferred_height = Some(height);
705                            breakable.preferred_height_break_count = break_count;
706                        } else {
707                            *preferred_height = Some(24.);
708                        }
709                    }
710                }
711
712                let child_0 = AssertPasses::new(
713                    NoneElement,
714                    match pass {
715                        build_element::Pass::FirstLocationUsage { .. } => todo!(),
716                        build_element::Pass::Measure { full_height } => vec![Pass::Measure {
717                            width: WidthConstraint {
718                                max: width.max,
719                                expand: false,
720                            },
721                            first_height,
722                            full_height,
723                        }],
724                        build_element::Pass::Draw {
725                            ref breakable,
726                            preferred_height,
727                        } => vec![
728                            Pass::Measure {
729                                width: WidthConstraint {
730                                    max: width.max,
731                                    expand: false,
732                                },
733                                first_height: first_height,
734                                full_height: breakable.as_ref().map(|b| b.full_height),
735                            },
736                            Pass::Draw {
737                                width: WidthConstraint {
738                                    max: width.max,
739                                    expand: false,
740                                },
741                                first_height: first_height,
742                                preferred_height,
743
744                                page: 0,
745                                layer: 0,
746                                pos: (12., 14.),
747
748                                breakable: breakable.as_ref().map(|b| BreakableDraw {
749                                    full_height: b.full_height,
750                                    preferred_height_break_count: b.preferred_height_break_count,
751                                    breaks: Vec::new(),
752                                }),
753                            },
754                        ],
755                    },
756                );
757
758                let child_1 = {
759                    let width = WidthConstraint {
760                        max: 6.,
761                        expand: width.expand,
762                    };
763
764                    AssertPasses::new(
765                        Rectangle {
766                            size: (5., 5.),
767                            fill: None,
768                            outline: None,
769                        },
770                        match pass {
771                            build_element::Pass::FirstLocationUsage { .. } => todo!(),
772                            build_element::Pass::Measure { full_height } => vec![Pass::Measure {
773                                width,
774                                first_height,
775                                full_height,
776                            }],
777                            build_element::Pass::Draw {
778                                ref breakable,
779                                preferred_height,
780                            } => {
781                                let mut r = Vec::new();
782
783                                if expand {
784                                    r.push(Pass::Measure {
785                                        width,
786                                        first_height,
787                                        full_height: breakable.as_ref().map(|b| b.full_height),
788                                    });
789                                }
790
791                                r.push(Pass::Draw {
792                                    width,
793                                    first_height,
794                                    preferred_height,
795
796                                    page: 0,
797                                    layer: 0,
798                                    pos: (12., 14.),
799
800                                    breakable: breakable.as_ref().map(|b| BreakableDraw {
801                                        full_height: b.full_height,
802                                        preferred_height_break_count: b
803                                            .preferred_height_break_count,
804                                        breaks: Vec::new(),
805                                    }),
806                                });
807
808                                r
809                            }
810                        },
811                    )
812                };
813
814                let child_1_width = if width.expand { 6. } else { 5. } + gap;
815
816                let child_2 = {
817                    let x = 12. + child_1_width;
818
819                    AssertPasses::new(
820                        FakeText {
821                            lines: 12,
822                            line_height: 2.,
823                            width: 500.,
824                        },
825                        match pass {
826                            build_element::Pass::FirstLocationUsage { .. } => todo!(),
827                            build_element::Pass::Measure { full_height } => vec![Pass::Measure {
828                                width: WidthConstraint {
829                                    max: 3.,
830                                    expand: true,
831                                },
832                                first_height,
833                                full_height,
834                            }],
835                            build_element::Pass::Draw {
836                                ref breakable,
837                                preferred_height,
838                            } => {
839                                let mut r = Vec::new();
840
841                                if expand {
842                                    r.push(Pass::Measure {
843                                        width: WidthConstraint {
844                                            max: 3.,
845                                            expand: true,
846                                        },
847                                        first_height,
848                                        full_height: breakable.as_ref().map(|b| b.full_height),
849                                    });
850                                }
851
852                                r.push(Pass::Draw {
853                                    width: WidthConstraint {
854                                        max: 3.,
855                                        expand: true,
856                                    },
857                                    first_height,
858                                    preferred_height,
859
860                                    page: 0,
861                                    layer: 0,
862                                    pos: (x, 14.),
863
864                                    breakable: breakable.as_ref().map(|b| BreakableDraw {
865                                        full_height: b.full_height,
866                                        preferred_height_break_count: b
867                                            .preferred_height_break_count,
868                                        breaks: if less_first_height {
869                                            vec![
870                                                Break {
871                                                    page: 1,
872                                                    layer: 0,
873                                                    pos: (x, 14.),
874                                                },
875                                                Break {
876                                                    page: 2,
877                                                    layer: 0,
878                                                    pos: (x, 14.),
879                                                },
880                                            ]
881                                        } else {
882                                            vec![Break {
883                                                page: 1,
884                                                layer: 0,
885                                                pos: (x, 14.),
886                                            }]
887                                        },
888                                    }),
889                                });
890
891                                r
892                            }
893                        },
894                    )
895                };
896
897                let child_2_width = 3. + gap;
898
899                let child_3 = {
900                    let x = 12. + child_1_width + child_2_width;
901                    let width = WidthConstraint {
902                        max: 13.,
903                        expand: width.expand,
904                    };
905
906                    AssertPasses::new(
907                        ForceBreak,
908                        match pass {
909                            build_element::Pass::FirstLocationUsage { .. } => todo!(),
910                            build_element::Pass::Measure { full_height } => vec![Pass::Measure {
911                                width,
912                                first_height,
913                                full_height,
914                            }],
915                            build_element::Pass::Draw {
916                                ref breakable,
917                                preferred_height,
918                            } => {
919                                let mut r = Vec::new();
920
921                                if expand {
922                                    r.push(Pass::Measure {
923                                        width,
924                                        first_height,
925                                        full_height: breakable.as_ref().map(|b| b.full_height),
926                                    });
927                                }
928
929                                r.push(Pass::Draw {
930                                    width,
931                                    first_height,
932                                    preferred_height,
933
934                                    page: 0,
935                                    layer: 0,
936                                    pos: (x, 14.),
937
938                                    breakable: breakable.as_ref().map(|b| BreakableDraw {
939                                        full_height: b.full_height,
940                                        preferred_height_break_count: b
941                                            .preferred_height_break_count,
942                                        breaks: vec![Break {
943                                            page: 1,
944                                            layer: 0,
945                                            pos: (x, 14.),
946                                        }],
947                                    }),
948                                });
949
950                                r
951                            }
952                        },
953                    )
954                };
955
956                let element = Row {
957                    gap,
958                    expand,
959                    collapse: false,
960                    content: |content| {
961                        content.add(&child_0, Flex::SelfSized);
962                        content.add(&child_1, Flex::Expand(1));
963                        content.add(&child_2, Flex::Fixed(3.));
964                        content.add(&child_3, Flex::Expand(2));
965                    },
966                };
967
968                callback.call(element)
969            },
970        );
971
972        for output in (ElementTestParams {
973            width: 24.,
974            first_height: 6.,
975            full_height: 12.,
976            pos: (12., 14.),
977            ..Default::default()
978        })
979        .run(&element)
980        {
981            let (height, breaks) = if output.breakable.is_none() {
982                (24., 0)
983            } else if output.first_height == 6. {
984                (6., 2)
985            } else {
986                (12., 1)
987            };
988
989            output.assert_size(ElementSize {
990                width: if output.width.expand {
991                    Some(20. + gap + 3.)
992                } else {
993                    Some(5. + gap + 3.)
994                },
995                height: Some(height),
996            });
997
998            if let Some(b) = output.breakable {
999                b.assert_break_count(breaks);
1000
1001                // TODO
1002                b.assert_extra_location_min_height(None);
1003            }
1004        }
1005    }
1006}