Refactor `LayoutTree` + add examples of custom trees (#564)
1//! Computes size using styles and measure functions
2
3use crate::geometry::{Point, Size};
4use crate::style::{AvailableSpace, Display, Overflow, Position, Style};
5use crate::tree::CollapsibleMarginSet;
6use crate::tree::{LayoutInput, LayoutOutput, SizingMode};
7use crate::util::debug::debug_log;
8use crate::util::sys::f32_max;
9use crate::util::MaybeMath;
10use crate::util::{MaybeResolve, ResolveOrZero};
11
12/// Compute the size of a leaf node (node with no children)
13pub fn compute_leaf_layout<MeasureFunction>(
14 inputs: LayoutInput,
15 style: &Style,
16 measure_function: Option<MeasureFunction>,
17) -> LayoutOutput
18where
19 MeasureFunction: FnOnce(Size<Option<f32>>, Size<AvailableSpace>) -> Size<f32>,
20{
21 let LayoutInput { known_dimensions, parent_size, available_space, sizing_mode, .. } = inputs;
22
23 // Resolve node's preferred/min/max sizes (width/heights) against the available space (percentages resolve to pixel values)
24 // For ContentSize mode, we pretend that the node has no size styles as these should be ignored.
25 let (node_size, node_min_size, node_max_size, aspect_ratio) = match sizing_mode {
26 SizingMode::ContentSize => {
27 let node_size = known_dimensions;
28 let node_min_size = Size::NONE;
29 let node_max_size = Size::NONE;
30 (node_size, node_min_size, node_max_size, None)
31 }
32 SizingMode::InherentSize => {
33 let aspect_ratio = style.aspect_ratio;
34 let style_size = style.size.maybe_resolve(parent_size).maybe_apply_aspect_ratio(aspect_ratio);
35 let style_min_size = style.min_size.maybe_resolve(parent_size).maybe_apply_aspect_ratio(aspect_ratio);
36 let style_max_size = style.max_size.maybe_resolve(parent_size);
37
38 let node_size = known_dimensions.or(style_size);
39 (node_size, style_min_size, style_max_size, aspect_ratio)
40 }
41 };
42
43 // Note: both horizontal and vertical percentage padding/borders are resolved against the container's inline size (i.e. width).
44 // This is not a bug, but is how CSS is specified (see: https://developer.mozilla.org/en-US/docs/Web/CSS/padding#values)
45 let margin = style.margin.resolve_or_zero(parent_size.width);
46 let padding = style.padding.resolve_or_zero(parent_size.width);
47 let border = style.border.resolve_or_zero(parent_size.width);
48 let padding_border = padding + border;
49
50 // Scrollbar gutters are reserved when the `overflow` property is set to `Overflow::Scroll`.
51 // However, the axis are switched (transposed) because a node that scrolls vertically needs
52 // *horizontal* space to be reserved for a scrollbar
53 let scrollbar_gutter = style.overflow.transpose().map(|overflow| match overflow {
54 Overflow::Scroll => style.scrollbar_width,
55 _ => 0.0,
56 });
57 // TODO: make side configurable based on the `direction` property
58 let mut content_box_inset = padding_border;
59 content_box_inset.right += scrollbar_gutter.x;
60 content_box_inset.bottom += scrollbar_gutter.y;
61
62 #[cfg(feature = "block_layout")]
63 let is_block = style.display == Display::Block;
64 #[cfg(not(feature = "block_layout"))]
65 let is_block = false;
66
67 let has_styles_preventing_being_collapsed_through = !is_block
68 || style.overflow.x.is_scroll_container()
69 || style.overflow.y.is_scroll_container()
70 || style.position == Position::Absolute
71 || padding.top > 0.0
72 || padding.bottom > 0.0
73 || border.top > 0.0
74 || border.bottom > 0.0;
75
76 debug_log!("LEAF");
77 debug_log!("node_size", dbg:node_size);
78 debug_log!("min_size ", dbg:node_min_size);
79 debug_log!("max_size ", dbg:node_max_size);
80
81 // Return early if both width and height are known
82 if let Size { width: Some(width), height: Some(height) } = node_size {
83 let size = Size { width, height }
84 .maybe_clamp(node_min_size, node_max_size)
85 .maybe_max(padding_border.sum_axes().map(Some));
86 return LayoutOutput {
87 size,
88 first_baselines: Point::NONE,
89 top_margin: CollapsibleMarginSet::ZERO,
90 bottom_margin: CollapsibleMarginSet::ZERO,
91 margins_can_collapse_through: !has_styles_preventing_being_collapsed_through
92 && size.height == 0.0
93 && measure_function.is_none(),
94 };
95 };
96
97 if let Some(measure_function) = measure_function {
98 // Compute available space
99 let available_space = Size {
100 width: available_space
101 .width
102 .maybe_sub(margin.horizontal_axis_sum())
103 .maybe_set(node_size.width)
104 .maybe_set(node_max_size.width)
105 .map_definite_value(|size| {
106 size.maybe_clamp(node_min_size.width, node_max_size.width) - content_box_inset.horizontal_axis_sum()
107 }),
108 height: available_space
109 .height
110 .maybe_sub(margin.vertical_axis_sum())
111 .maybe_set(node_size.height)
112 .maybe_set(node_max_size.height)
113 .map_definite_value(|size| {
114 size.maybe_clamp(node_min_size.height, node_max_size.height) - content_box_inset.vertical_axis_sum()
115 }),
116 };
117
118 // Measure node
119 let measured_size = measure_function(known_dimensions, available_space);
120 let clamped_size =
121 node_size.unwrap_or(measured_size + content_box_inset.sum_axes()).maybe_clamp(node_min_size, node_max_size);
122 let size = Size {
123 width: clamped_size.width,
124 height: f32_max(clamped_size.height, aspect_ratio.map(|ratio| clamped_size.width / ratio).unwrap_or(0.0)),
125 };
126 let size = size.maybe_max(padding_border.sum_axes().map(Some));
127
128 return LayoutOutput {
129 size,
130 first_baselines: Point::NONE,
131 top_margin: CollapsibleMarginSet::ZERO,
132 bottom_margin: CollapsibleMarginSet::ZERO,
133 margins_can_collapse_through: !has_styles_preventing_being_collapsed_through
134 && size.height == 0.0
135 && measured_size.height == 0.0,
136 };
137 }
138
139 let size = Size {
140 width: node_size
141 .width
142 // .unwrap_or(0.0) + padding.horizontal_axis_sum() + border.horizontal_axis_sum(), // content-box
143 .unwrap_or(content_box_inset.horizontal_axis_sum()) // border-box
144 .maybe_clamp(node_min_size.width, node_max_size.width)
145 .maybe_max(padding_border.horizontal_axis_sum().into()),
146 height: node_size
147 .height
148 // .unwrap_or(0.0) + padding.vertical_axis_sum() + border.vertical_axis_sum(), // content-box
149 .unwrap_or(content_box_inset.vertical_axis_sum()) // border-box
150 .maybe_clamp(node_min_size.height, node_max_size.height)
151 .maybe_max(padding_border.vertical_axis_sum().into()),
152 };
153
154 let size = Size {
155 width: f32_max(size.width, aspect_ratio.map(|ratio| size.height * ratio).unwrap_or(0.0)),
156 height: f32_max(size.height, aspect_ratio.map(|ratio| size.width / ratio).unwrap_or(0.0)),
157 };
158
159 LayoutOutput {
160 size,
161 first_baselines: Point::NONE,
162 top_margin: CollapsibleMarginSet::ZERO,
163 bottom_margin: CollapsibleMarginSet::ZERO,
164 margins_can_collapse_through: !has_styles_preventing_being_collapsed_through && size.height == 0.0,
165 }
166}