Skip to main content

gpui_engine_default/
layout.rs

1//! Taffy-backed layout evaluation.
2//!
3//! The engine owns the `taffy` tree and translates between [`LayoutId`] and
4//! `taffy::NodeId`. It receives the engine's layout style and converts it here,
5//! so the facade never names `taffy`. Custom measure callbacks are invoked with
6//! a type-erased [`MeasureContext`], so the engine never names the facade's
7//! window or application types.
8
9use collections::{FxHashMap, FxHashSet};
10use gpui_engine::{BoxedMeasureFn, EngineLayoutStyle, LayoutEngine, LayoutId, MeasureContext};
11use gpui_types::{
12    AvailableSpace, Bounds, Pixels, Point, Size, ceil_to_device_pixel, round_half_toward_zero,
13    round_to_device_pixel, size,
14};
15use taffy::{TaffyTree, TraversePartialTree as _, tree::NodeId};
16
17#[cfg(feature = "stacker")]
18type StackSafe<T> = stacksafe::StackSafe<T>;
19#[cfg(not(feature = "stacker"))]
20type StackSafe<T> = T;
21
22type NodeMeasureFn = StackSafe<BoxedMeasureFn>;
23
24struct NodeContext {
25    measure: NodeMeasureFn,
26}
27
28/// The `taffy`-backed layout tree for a window.
29pub struct TaffyLayoutEngine {
30    taffy: TaffyTree<NodeContext>,
31    absolute_layout_bounds: FxHashMap<LayoutId, Bounds<Pixels>>,
32    /// Unrounded absolute border-box top-left per-node coordinate in device pixels.
33    absolute_outer_origins: FxHashMap<LayoutId, Point<f32>>,
34    computed_layouts: FxHashSet<LayoutId>,
35    layout_bounds_scratch_space: Vec<LayoutId>,
36}
37
38const EXPECT_MESSAGE: &str = "we should avoid taffy layout errors by construction if possible";
39
40/// Creates the layout engine the default engine hands to each new window.
41pub fn default_layout_engine() -> Box<dyn LayoutEngine> {
42    Box::new(TaffyLayoutEngine::new())
43}
44
45fn taffy_id(id: LayoutId) -> NodeId {
46    NodeId::new(id.0)
47}
48
49fn layout_id(node_id: NodeId) -> LayoutId {
50    LayoutId(u64::from(node_id))
51}
52
53impl TaffyLayoutEngine {
54    /// Creates an empty layout engine with rounding disabled.
55    pub fn new() -> Self {
56        let mut taffy = TaffyTree::new();
57        taffy.disable_rounding();
58        TaffyLayoutEngine {
59            taffy,
60            absolute_layout_bounds: FxHashMap::default(),
61            absolute_outer_origins: FxHashMap::default(),
62            computed_layouts: FxHashSet::default(),
63            layout_bounds_scratch_space: Vec::new(),
64        }
65    }
66
67    /// Discards every node and cached bounds, ready for a fresh frame.
68    pub fn clear(&mut self) {
69        self.taffy.clear();
70        self.absolute_layout_bounds.clear();
71        self.absolute_outer_origins.clear();
72        self.computed_layouts.clear();
73    }
74
75    /// Adds a leaf or container node built from an already-converted taffy style.
76    fn request_layout_taffy(
77        &mut self,
78        taffy_style: taffy::style::Style,
79        children: &[LayoutId],
80    ) -> LayoutId {
81        if children.is_empty() {
82            let node = self.taffy.new_leaf(taffy_style).expect(EXPECT_MESSAGE);
83            layout_id(node)
84        } else {
85            let child_ids: Vec<NodeId> = children.iter().copied().map(taffy_id).collect();
86            let node = self
87                .taffy
88                .new_with_children(taffy_style, &child_ids)
89                .expect(EXPECT_MESSAGE);
90            layout_id(node)
91        }
92    }
93
94    /// Adds a leaf whose size is resolved by `measure` during layout.
95    fn request_measured_layout_taffy(
96        &mut self,
97        taffy_style: taffy::style::Style,
98        measure: BoxedMeasureFn,
99    ) -> LayoutId {
100        #[cfg(feature = "stacker")]
101        let measure = StackSafe::new(measure);
102
103        let node = self
104            .taffy
105            .new_leaf_with_context(taffy_style, NodeContext { measure })
106            .expect(EXPECT_MESSAGE);
107        layout_id(node)
108    }
109
110    /// Treats any `auto` dimension of the given node's style as filling `size`.
111    ///
112    /// This is applied to window roots before layout so they behave like the
113    /// root element on the web, which stretches to fill the initial containing
114    /// block (the viewport) unless given an explicit size. Explicitly styled
115    /// dimensions are preserved.
116    pub fn stretch_auto_size_to_fill(
117        &mut self,
118        id: LayoutId,
119        size: Size<Pixels>,
120        scale_factor: f32,
121    ) {
122        let style = self.taffy.style(taffy_id(id)).expect(EXPECT_MESSAGE);
123        let stretch_width = style.size.width.is_auto();
124        let stretch_height = style.size.height.is_auto();
125        if !stretch_width && !stretch_height {
126            return;
127        }
128        let mut style = style.clone();
129        if stretch_width {
130            style.size.width =
131                taffy::style::Dimension::length(round_to_device_pixel(size.width.0, scale_factor));
132        }
133        if stretch_height {
134            style.size.height =
135                taffy::style::Dimension::length(round_to_device_pixel(size.height.0, scale_factor));
136        }
137        self.taffy
138            .set_style(taffy_id(id), style)
139            .expect(EXPECT_MESSAGE);
140    }
141
142    // Used to understand performance
143    #[allow(dead_code)]
144    fn count_all_children(&self, parent: LayoutId) -> anyhow::Result<u32> {
145        let mut count = 0;
146
147        for child in self.taffy.children(taffy_id(parent))? {
148            // Count this child.
149            count += 1;
150
151            // Count all of this child's children.
152            count += self.count_all_children(layout_id(child))?
153        }
154
155        Ok(count)
156    }
157
158    // Used to understand performance
159    #[allow(dead_code)]
160    fn max_depth(&self, depth: u32, parent: LayoutId) -> anyhow::Result<u32> {
161        println!(
162            "{parent:?} at depth {depth} has {} children",
163            self.taffy.child_count(taffy_id(parent))
164        );
165
166        let mut max_child_depth = 0;
167
168        for child in self.taffy.children(taffy_id(parent))? {
169            max_child_depth = std::cmp::max(max_child_depth, self.max_depth(0, layout_id(child))?);
170        }
171
172        Ok(depth + 1 + max_child_depth)
173    }
174
175    // Used to understand performance
176    #[allow(dead_code)]
177    fn get_edges(&self, parent: LayoutId) -> anyhow::Result<Vec<(LayoutId, LayoutId)>> {
178        let mut edges = Vec::new();
179
180        for child in self.taffy.children(taffy_id(parent))? {
181            edges.push((parent, layout_id(child)));
182
183            edges.extend(self.get_edges(layout_id(child))?);
184        }
185
186        Ok(edges)
187    }
188
189    /// Computes the layout of `id` within `available_space`, invoking stored
190    /// measure callbacks with `context`.
191    #[cfg_attr(feature = "stacker", stacksafe::stacksafe)]
192    pub fn compute_layout(
193        &mut self,
194        id: LayoutId,
195        available_space: Size<AvailableSpace>,
196        scale_factor: f32,
197        context: &mut dyn MeasureContext,
198    ) {
199        if !self.computed_layouts.insert(id) {
200            let stack = &mut self.layout_bounds_scratch_space;
201            stack.push(id);
202            while let Some(id) = stack.pop() {
203                self.absolute_layout_bounds.remove(&id);
204                self.absolute_outer_origins.remove(&id);
205                stack.extend(
206                    self.taffy
207                        .children(taffy_id(id))
208                        .expect(EXPECT_MESSAGE)
209                        .into_iter()
210                        .map(layout_id),
211                );
212            }
213        }
214
215        let transform = |v: AvailableSpace| match v {
216            AvailableSpace::Definite(pixels) => {
217                AvailableSpace::Definite(Pixels(pixels.0 * scale_factor))
218            }
219            AvailableSpace::MinContent => AvailableSpace::MinContent,
220            AvailableSpace::MaxContent => AvailableSpace::MaxContent,
221        };
222        let available_space = size(
223            transform(available_space.width),
224            transform(available_space.height),
225        );
226
227        self.taffy
228            .compute_layout_with_measure(
229                taffy_id(id),
230                available_space.into(),
231                |known_dimensions, available_space, _id, node_context, _style| {
232                    let Some(node_context) = node_context else {
233                        return taffy::geometry::Size::default();
234                    };
235
236                    let known_dimensions = Size {
237                        width: known_dimensions.width.map(|e| Pixels(e / scale_factor)),
238                        height: known_dimensions.height.map(|e| Pixels(e / scale_factor)),
239                    };
240
241                    let available_space: Size<AvailableSpace> = available_space.into();
242                    let untransform = |ev: AvailableSpace| match ev {
243                        AvailableSpace::Definite(pixels) => {
244                            AvailableSpace::Definite(Pixels(pixels.0 / scale_factor))
245                        }
246                        AvailableSpace::MinContent => AvailableSpace::MinContent,
247                        AvailableSpace::MaxContent => AvailableSpace::MaxContent,
248                    };
249                    let available_space = size(
250                        untransform(available_space.width),
251                        untransform(available_space.height),
252                    );
253
254                    let measured_size: Size<Pixels> =
255                        (node_context.measure)(known_dimensions, available_space, context);
256                    snap_measured_size_to_device_pixels(measured_size, scale_factor).into()
257                },
258            )
259            .expect(EXPECT_MESSAGE);
260    }
261
262    /// Returns the bounds of `id` relative to the window, pixel-snapped.
263    ///
264    /// Pixel snapping
265    ///
266    /// Painting primitives at non-integer pixel coordinates produces blurry
267    /// output. Pixel snapping converts layout coordinates into integer
268    /// device-pixel coordinates so painted edges land exactly on physical
269    /// pixel boundaries.
270    ///
271    /// Non-integer coordinates can arise for several reasons, including:
272    ///   - flex distribution, percentages, centering, and text measurement
273    ///     can produce fractional element sizes and positions;
274    ///   - at fractional scale factors (for example 125% or 150%), integer
275    ///     logical-pixel values can map to non-integer device-pixel values.
276    ///
277    /// We pixel-snap by rounding in device-pixel space, after multiplying
278    /// by `scale_factor`, so that snapping targets physical pixels. Bounds
279    /// are divided by `scale_factor` before being returned to GPUI.
280    ///
281    /// Midpoints are rounded toward zero. This is a stylistic choice: a
282    /// 1-logical-pixel line at 150% scale should render as 1 dp rather than
283    /// 2 dp.
284    ///
285    /// Pixel snapping is done in two phases:
286    ///
287    ///  1. Pre-layout metric snapping. Before Taffy computes layout, all
288    ///     authored absolute lengths are rounded in `to_taffy`. This
289    ///     includes borders, padding, gaps, and explicit sizes.
290    ///     Custom-measured leaf nodes have their measured sizes rounded up
291    ///     to integer device-pixel lengths.
292    ///
293    ///  2. Post-layout edge snapping. After Taffy resolves the tree, layout
294    ///     relationships such as flex shares, grid tracks, percentages, and
295    ///     centering can produce new fractional edge positions. Boxes now
296    ///     have edges in absolute coordinates, and snapping must decide
297    ///     where those edges land on the device-pixel grid.
298    ///
299    /// Ideally, post-layout snapping would satisfy:
300    ///
301    ///  - Edge closure. Two raw layout edges at the same absolute position
302    ///    should snap to the same pixel column.
303    ///  - Translation stability. A component's internal geometry should not
304    ///    change when it moves to a new absolute position.
305    ///
306    /// These goals are in tension because rounding is not associative.
307    /// The simple local schemes make different tradeoffs:
308    ///
309    ///  - Absolute edge rounding gives each window coordinate one answer,
310    ///    so coincident edges always close globally. But a span's snapped
311    ///    length is `round(far) - round(near)`, which may change by 1 dp
312    ///    as its absolute origin moves.
313    ///
314    ///  - Parent-relative edge rounding rounds each child inside its
315    ///    parent's coordinate space. This guarantees translation stability,
316    ///    but a shared edge reached through different parents can
317    ///    accumulate different rounding, causing non-closure between
318    ///    cousins.
319    ///
320    ///  - Length rounding rounds each width, height, and thickness
321    ///    independently and then places boxes from those rounded lengths.
322    ///    Sizes stay stable under translation, but neighboring boxes derive
323    ///    their shared boundary from different sources, so closure is not
324    ///    guaranteed.
325    ///
326    /// We apply absolute edge rounding for each element's outer box in
327    /// post-layout rounding to preserve closure. Border and padding widths
328    /// are not touched by post-layout rounding; they keep their pre-layout
329    /// rounded value so that they remain stable under translation.
330    ///
331    /// This gives both closure and translation stability in the case that
332    /// all local metrics are integer device-pixel lengths. Pre-layout
333    /// rounding covers that in most cases. The exception is metrics
334    /// resolved by layout relationships, such as percentages. Outer box
335    /// edges will still close globally, and painted border widths are still
336    /// snapped independently, but the raw content-box origin can carry a
337    /// 1dp residual into descendants.
338    pub fn layout_bounds(&mut self, id: LayoutId, scale_factor: f32) -> Bounds<Pixels> {
339        if let Some(layout) = self.absolute_layout_bounds.get(&id).cloned() {
340            return layout;
341        }
342
343        let layout = self.taffy.layout(taffy_id(id)).expect(EXPECT_MESSAGE);
344        let layout_location = layout.location;
345        let layout_size = layout.size;
346        let parent = self.taffy.parent(taffy_id(id));
347
348        let absolute_outer_origin = match parent {
349            Some(parent_id) => {
350                let parent_id = layout_id(parent_id);
351                self.layout_bounds(parent_id, scale_factor);
352                let parent_origin = *self
353                    .absolute_outer_origins
354                    .get(&parent_id)
355                    .expect("parent absolute outer origin should be cached");
356                parent_origin + Point::from(layout_location)
357            }
358            None => Point::from(layout_location),
359        };
360        self.absolute_outer_origins
361            .insert(id, absolute_outer_origin);
362
363        let absolute_far = absolute_outer_origin + Point::from(Size::from(layout_size));
364        let snapped_bounds = Bounds::from_corners(
365            absolute_outer_origin.map(round_half_toward_zero),
366            absolute_far.map(round_half_toward_zero),
367        );
368
369        let bounds = (snapped_bounds / scale_factor).map(Pixels);
370        self.absolute_layout_bounds.insert(id, bounds);
371        bounds
372    }
373}
374
375impl LayoutEngine for TaffyLayoutEngine {
376    fn clear(&mut self) {
377        TaffyLayoutEngine::clear(self)
378    }
379
380    fn request_layout(
381        &mut self,
382        style: &EngineLayoutStyle,
383        rem_size: Pixels,
384        scale_factor: f32,
385        children: &[LayoutId],
386    ) -> LayoutId {
387        let taffy_style = crate::layout_style::to_taffy_style(style, rem_size, scale_factor);
388        TaffyLayoutEngine::request_layout_taffy(self, taffy_style, children)
389    }
390
391    fn request_measured_layout(
392        &mut self,
393        style: &EngineLayoutStyle,
394        rem_size: Pixels,
395        scale_factor: f32,
396        measure: BoxedMeasureFn,
397    ) -> LayoutId {
398        let taffy_style = crate::layout_style::to_taffy_style(style, rem_size, scale_factor);
399        TaffyLayoutEngine::request_measured_layout_taffy(self, taffy_style, measure)
400    }
401
402    fn stretch_auto_size_to_fill(&mut self, id: LayoutId, size: Size<Pixels>, scale_factor: f32) {
403        TaffyLayoutEngine::stretch_auto_size_to_fill(self, id, size, scale_factor)
404    }
405
406    fn compute_layout(
407        &mut self,
408        id: LayoutId,
409        available_space: Size<AvailableSpace>,
410        scale_factor: f32,
411        context: &mut dyn MeasureContext,
412    ) {
413        TaffyLayoutEngine::compute_layout(self, id, available_space, scale_factor, context)
414    }
415
416    fn layout_bounds(&mut self, id: LayoutId, scale_factor: f32) -> Bounds<Pixels> {
417        TaffyLayoutEngine::layout_bounds(self, id, scale_factor)
418    }
419}
420
421fn snap_measured_size_to_device_pixels(size: Size<Pixels>, scale_factor: f32) -> Size<f32> {
422    size.map(|d| ceil_to_device_pixel(d.0.max(0.0), scale_factor))
423}