Skip to main content

taffy/compute/
leaf.rs

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