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