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