taffy/compute/
leaf.rs

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