gioser

tawasuyu/taffy

A high performance rust-powered UI layout library

HTML 5364Rust 86Markdown 12TOML 91841 Definitions

⎇ d4374b93f0ec2c0ace42d3feafec22d3af1940d8 src/compute/grid/alignment.rs Rust · 256 lines · 10.6 KB History Who touched what raw

Refactor `LayoutTree` + add examples of custom trees (#564) d4374b93f · Nico Burns ·

1//! Alignment of tracks and final positioning of items
2use super::types::GridTrack;
3use crate::compute::common::alignment::compute_alignment_offset;
4use crate::geometry::{InBothAbsAxis, Line, Point, Rect, Size};
5use crate::style::{AlignContent, AlignItems, AlignSelf, AvailableSpace, Position};
6use crate::tree::{Layout, NodeId, PartialLayoutTree, PartialLayoutTreeExt, SizingMode};
7use crate::util::sys::f32_max;
8use crate::util::{MaybeMath, MaybeResolve, ResolveOrZero};
9
10/// Align the grid tracks within the grid according to the align-content (rows) or
11/// justify-content (columns) property. This only does anything if the size of the
12/// grid is not equal to the size of the grid container in the axis being aligned.
13pub(super) fn align_tracks(
14    grid_container_content_box_size: f32,
15    padding: Line<f32>,
16    border: Line<f32>,
17    tracks: &mut [GridTrack],
18    track_alignment_style: AlignContent,
19) {
20    let used_size: f32 = tracks.iter().map(|track| track.base_size).sum();
21    let free_space = grid_container_content_box_size - used_size;
22    let origin = padding.start + border.start;
23
24    // Count the number of non-collapsed tracks (not counting gutters)
25    let num_tracks = tracks.iter().skip(1).step_by(2).filter(|track| !track.is_collapsed).count();
26
27    // Grid layout treats gaps as full tracks rather than applying them at alignment so we
28    // simply pass zero here. Grid layout is never reversed.
29    let gap = 0.0;
30    let layout_is_reversed = false;
31
32    // Compute offsets
33    let mut total_offset = origin;
34    tracks.iter_mut().enumerate().for_each(|(i, track)| {
35        // Odd tracks are gutters (but slices are zero-indexed, so odd tracks have even indicies)
36        let is_gutter = i % 2 == 0;
37
38        // The first non-gutter track is index 1
39        let is_first = i == 1;
40
41        let offset = if is_gutter {
42            0.0
43        } else {
44            compute_alignment_offset(free_space, num_tracks, gap, track_alignment_style, layout_is_reversed, is_first)
45        };
46
47        track.offset = total_offset + offset;
48        total_offset = total_offset + offset + track.base_size;
49    });
50}
51
52/// Align and size a grid item into it's final position
53pub(super) fn align_and_position_item(
54    tree: &mut impl PartialLayoutTree,
55    node: NodeId,
56    order: u32,
57    grid_area: Rect<f32>,
58    container_alignment_styles: InBothAbsAxis<Option<AlignItems>>,
59    baseline_shim: f32,
60) {
61    let grid_area_size = Size { width: grid_area.right - grid_area.left, height: grid_area.bottom - grid_area.top };
62
63    let style = tree.get_style(node);
64    let aspect_ratio = style.aspect_ratio;
65    let justify_self = style.justify_self;
66    let align_self = style.align_self;
67
68    let position = style.position;
69    let inset_horizontal = style.inset.horizontal_components().map(|size| size.resolve_to_option(grid_area_size.width));
70    let inset_vertical = style.inset.vertical_components().map(|size| size.resolve_to_option(grid_area_size.height));
71    let padding = style.padding.map(|p| p.resolve_or_zero(Some(grid_area_size.width)));
72    let border = style.border.map(|p| p.resolve_or_zero(Some(grid_area_size.width)));
73    let padding_border_size = (padding + border).sum_axes();
74    let inherent_size = style.size.maybe_resolve(grid_area_size).maybe_apply_aspect_ratio(aspect_ratio);
75    let min_size = style
76        .min_size
77        .maybe_resolve(grid_area_size)
78        .or(padding_border_size.map(Some))
79        .maybe_max(padding_border_size)
80        .maybe_apply_aspect_ratio(aspect_ratio);
81    let max_size = style.max_size.maybe_resolve(grid_area_size).maybe_apply_aspect_ratio(aspect_ratio);
82
83    // Resolve default alignment styles if they are set on neither the parent or the node itself
84    // Note: if the child has a preferred aspect ratio but neither width or height are set, then the width is stretched
85    // and the then height is calculated from the width according the aspect ratio
86    // See: https://www.w3.org/TR/css-grid-1/#grid-item-sizing
87    let alignment_styles = InBothAbsAxis {
88        horizontal: justify_self.or(container_alignment_styles.horizontal).unwrap_or_else(|| {
89            if inherent_size.width.is_some() {
90                AlignSelf::Start
91            } else {
92                AlignSelf::Stretch
93            }
94        }),
95        vertical: align_self.or(container_alignment_styles.vertical).unwrap_or_else(|| {
96            if inherent_size.height.is_some() || aspect_ratio.is_some() {
97                AlignSelf::Start
98            } else {
99                AlignSelf::Stretch
100            }
101        }),
102    };
103
104    // Note: This is not a bug. It is part of the CSS spec that both horizontal and vertical margins
105    // resolve against the WIDTH of the grid area.
106    let margin = style.margin.map(|margin| margin.resolve_to_option(grid_area_size.width));
107
108    let grid_area_minus_item_margins_size = Size {
109        width: grid_area_size.width.maybe_sub(margin.left).maybe_sub(margin.right),
110        height: grid_area_size.height.maybe_sub(margin.top).maybe_sub(margin.bottom) - baseline_shim,
111    };
112
113    // If node is absolutely positioned and width is not set explicitly, then deduce it
114    // from left, right and container_content_box if both are set.
115    let width = inherent_size.width.or_else(|| {
116        // Apply width derived from both the left and right properties of an absolutely
117        // positioned element being set
118        if position == Position::Absolute {
119            if let (Some(left), Some(right)) = (inset_horizontal.start, inset_horizontal.end) {
120                return Some(f32_max(grid_area_minus_item_margins_size.width - left - right, 0.0));
121            }
122        }
123
124        // Apply width based on stretch alignment if:
125        //  - Alignment style is "stretch"
126        //  - The node is not absolutely positioned
127        //  - The node does not have auto margins in this axis.
128        if margin.left.is_some()
129            && margin.right.is_some()
130            && alignment_styles.horizontal == AlignSelf::Stretch
131            && position != Position::Absolute
132        {
133            return Some(grid_area_minus_item_margins_size.width);
134        }
135
136        None
137    });
138
139    // Reapply aspect ratio after stretch and absolute position width adjustments
140    let Size { width, height } = Size { width, height: inherent_size.height }.maybe_apply_aspect_ratio(aspect_ratio);
141
142    let height = height.or_else(|| {
143        if position == Position::Absolute {
144            if let (Some(top), Some(bottom)) = (inset_vertical.start, inset_vertical.end) {
145                return Some(f32_max(grid_area_minus_item_margins_size.height - top - bottom, 0.0));
146            }
147        }
148
149        // Apply height based on stretch alignment if:
150        //  - Alignment style is "stretch"
151        //  - The node is not absolutely positioned
152        //  - The node does not have auto margins in this axis.
153        if margin.top.is_some()
154            && margin.bottom.is_some()
155            && alignment_styles.vertical == AlignSelf::Stretch
156            && position != Position::Absolute
157        {
158            return Some(grid_area_minus_item_margins_size.height);
159        }
160
161        None
162    });
163    // Reapply aspect ratio after stretch and absolute position height adjustments
164    let Size { width, height } = Size { width, height }.maybe_apply_aspect_ratio(aspect_ratio);
165
166    // Clamp size by min and max width/height
167    let Size { width, height } = Size { width, height }.maybe_clamp(min_size, max_size);
168
169    // Layout node
170    let measured_size_and_baselines = tree.perform_child_layout(
171        node,
172        Size { width, height },
173        grid_area_size.map(Option::Some),
174        grid_area_minus_item_margins_size.map(AvailableSpace::Definite),
175        SizingMode::InherentSize,
176        Line::FALSE,
177    );
178
179    // Resolve final size
180    let Size { width, height } =
181        Size { width, height }.unwrap_or(measured_size_and_baselines.size).maybe_clamp(min_size, max_size);
182
183    let x = align_item_within_area(
184        Line { start: grid_area.left, end: grid_area.right },
185        justify_self.unwrap_or(alignment_styles.horizontal),
186        width,
187        position,
188        inset_horizontal,
189        margin.horizontal_components(),
190        0.0,
191    );
192    let y = align_item_within_area(
193        Line { start: grid_area.top, end: grid_area.bottom },
194        align_self.unwrap_or(alignment_styles.vertical),
195        height,
196        position,
197        inset_vertical,
198        margin.vertical_components(),
199        baseline_shim,
200    );
201
202    *tree.get_unrounded_layout_mut(node) = Layout { order, size: Size { width, height }, location: Point { x, y } };
203}
204
205/// Align and size a grid item along a single axis
206pub(super) fn align_item_within_area(
207    grid_area: Line<f32>,
208    alignment_style: AlignSelf,
209    resolved_size: f32,
210    position: Position,
211    inset: Line<Option<f32>>,
212    margin: Line<Option<f32>>,
213    baseline_shim: f32,
214) -> f32 {
215    // Calculate grid area dimension in the axis
216    let non_auto_margin = Line { start: margin.start.unwrap_or(0.0) + baseline_shim, end: margin.end.unwrap_or(0.0) };
217    let grid_area_size = f32_max(grid_area.end - grid_area.start, 0.0);
218    let free_space = f32_max(grid_area_size - resolved_size - non_auto_margin.sum(), 0.0);
219
220    // Expand auto margins to fill available space
221    let auto_margin_count = margin.start.is_none() as u8 + margin.end.is_none() as u8;
222    let auto_margin_size = if auto_margin_count > 0 { free_space / auto_margin_count as f32 } else { 0.0 };
223    let resolved_margin = Line {
224        start: margin.start.unwrap_or(auto_margin_size) + baseline_shim,
225        end: margin.end.unwrap_or(auto_margin_size),
226    };
227
228    // Compute offset in the axis
229    let alignment_based_offset = match alignment_style {
230        AlignSelf::Start | AlignSelf::FlexStart => resolved_margin.start,
231        AlignSelf::End | AlignSelf::FlexEnd => grid_area_size - resolved_size - resolved_margin.end,
232        AlignSelf::Center => (grid_area_size - resolved_size + resolved_margin.start - resolved_margin.end) / 2.0,
233        // TODO: Add support for baseline alignment. For now we treat it as "start".
234        AlignSelf::Baseline => resolved_margin.start,
235        AlignSelf::Stretch => resolved_margin.start,
236    };
237
238    let offset_within_area = if position == Position::Absolute {
239        if let Some(start) = inset.start {
240            start + non_auto_margin.start
241        } else if let Some(end) = inset.end {
242            grid_area_size - end - resolved_size - non_auto_margin.end
243        } else {
244            alignment_based_offset
245        }
246    } else {
247        alignment_based_offset
248    };
249
250    let mut start = grid_area.start + offset_within_area;
251    if position == Position::Relative {
252        start += inset.start.or(inset.end.map(|pos| -pos)).unwrap_or(0.0);
253    }
254
255    start
256}