use crate::{ AbsoluteLength, App, Bounds, DefiniteLength, Edges, GridTemplate, Length, Pixels, Point, Size, Style, Window, size, util::{ ceil_to_device_pixel, round_half_toward_zero, round_stroke_to_device_pixel, round_to_device_pixel, }, }; use collections::{FxHashMap, FxHashSet}; use stacksafe::{StackSafe, stacksafe}; use std::{fmt::Debug, ops::Range}; use taffy::{ TaffyTree, TraversePartialTree as _, geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize}, prelude::{max_content, min_content}, style::AvailableSpace as TaffyAvailableSpace, tree::NodeId, }; type NodeMeasureFn = StackSafe< Box< dyn FnMut( Size>, Size, &mut Window, &mut App, ) -> Size, >, >; struct NodeContext { measure: NodeMeasureFn, } pub struct TaffyLayoutEngine { taffy: TaffyTree, absolute_layout_bounds: FxHashMap>, /// Unrounded absolute border-box top-left per-node coordinate in device pixels. absolute_outer_origins: FxHashMap>, computed_layouts: FxHashSet, layout_bounds_scratch_space: Vec, } const EXPECT_MESSAGE: &str = "we should avoid taffy layout errors by construction if possible"; impl TaffyLayoutEngine { pub fn new() -> Self { let mut taffy = TaffyTree::new(); taffy.disable_rounding(); TaffyLayoutEngine { taffy, absolute_layout_bounds: FxHashMap::default(), absolute_outer_origins: FxHashMap::default(), computed_layouts: FxHashSet::default(), layout_bounds_scratch_space: Vec::new(), } } pub fn clear(&mut self) { self.taffy.clear(); self.absolute_layout_bounds.clear(); self.absolute_outer_origins.clear(); self.computed_layouts.clear(); } pub fn request_layout( &mut self, style: Style, rem_size: Pixels, scale_factor: f32, children: &[LayoutId], ) -> LayoutId { let taffy_style = style.to_taffy(rem_size, scale_factor); if children.is_empty() { self.taffy .new_leaf(taffy_style) .expect(EXPECT_MESSAGE) .into() } else { self.taffy // This is safe because LayoutId is repr(transparent) to taffy::tree::NodeId. .new_with_children(taffy_style, LayoutId::to_taffy_slice(children)) .expect(EXPECT_MESSAGE) .into() } } pub fn request_measured_layout( &mut self, style: Style, rem_size: Pixels, scale_factor: f32, measure: impl FnMut( Size>, Size, &mut Window, &mut App, ) -> Size + 'static, ) -> LayoutId { let taffy_style = style.to_taffy(rem_size, scale_factor); self.taffy .new_leaf_with_context( taffy_style, NodeContext { measure: StackSafe::new(Box::new(measure)), }, ) .expect(EXPECT_MESSAGE) .into() } /// Treats any `auto` dimension of the given node's style as filling `size`. /// /// This is applied to window roots before layout so they behave like the /// root element on the web, which stretches to fill the initial containing /// block (the viewport) unless given an explicit size. Explicitly styled /// dimensions are preserved. pub fn stretch_auto_size_to_fill( &mut self, id: LayoutId, size: Size, scale_factor: f32, ) { let style = self.taffy.style(id.0).expect(EXPECT_MESSAGE); let stretch_width = style.size.width.is_auto(); let stretch_height = style.size.height.is_auto(); if !stretch_width && !stretch_height { return; } let mut style = style.clone(); if stretch_width { style.size.width = taffy::style::Dimension::length(round_to_device_pixel(size.width.0, scale_factor)); } if stretch_height { style.size.height = taffy::style::Dimension::length(round_to_device_pixel(size.height.0, scale_factor)); } self.taffy.set_style(id.0, style).expect(EXPECT_MESSAGE); } // Used to understand performance #[allow(dead_code)] fn count_all_children(&self, parent: LayoutId) -> anyhow::Result { let mut count = 0; for child in self.taffy.children(parent.0)? { // Count this child. count += 1; // Count all of this child's children. count += self.count_all_children(LayoutId(child))? } Ok(count) } // Used to understand performance #[allow(dead_code)] fn max_depth(&self, depth: u32, parent: LayoutId) -> anyhow::Result { println!( "{parent:?} at depth {depth} has {} children", self.taffy.child_count(parent.0) ); let mut max_child_depth = 0; for child in self.taffy.children(parent.0)? { max_child_depth = std::cmp::max(max_child_depth, self.max_depth(0, LayoutId(child))?); } Ok(depth + 1 + max_child_depth) } // Used to understand performance #[allow(dead_code)] fn get_edges(&self, parent: LayoutId) -> anyhow::Result> { let mut edges = Vec::new(); for child in self.taffy.children(parent.0)? { edges.push((parent, LayoutId(child))); edges.extend(self.get_edges(LayoutId(child))?); } Ok(edges) } #[stacksafe] pub fn compute_layout( &mut self, id: LayoutId, available_space: Size, window: &mut Window, cx: &mut App, ) { // Leaving this here until we have a better instrumentation approach. // println!("Laying out {} children", self.count_all_children(id)?); // println!("Max layout depth: {}", self.max_depth(0, id)?); // Output the edges (branches) of the tree in Mermaid format for visualization. // println!("Edges:"); // for (a, b) in self.get_edges(id)? { // println!("N{} --> N{}", u64::from(a), u64::from(b)); // } // if !self.computed_layouts.insert(id) { let stack = &mut self.layout_bounds_scratch_space; stack.push(id); while let Some(id) = stack.pop() { self.absolute_layout_bounds.remove(&id); self.absolute_outer_origins.remove(&id); stack.extend( self.taffy .children(id.into()) .expect(EXPECT_MESSAGE) .into_iter() .map(LayoutId::from), ); } } let scale_factor = window.scale_factor(); let transform = |v: AvailableSpace| match v { AvailableSpace::Definite(pixels) => { AvailableSpace::Definite(Pixels(pixels.0 * scale_factor)) } AvailableSpace::MinContent => AvailableSpace::MinContent, AvailableSpace::MaxContent => AvailableSpace::MaxContent, }; let available_space = size( transform(available_space.width), transform(available_space.height), ); self.taffy .compute_layout_with_measure( id.into(), available_space.into(), |known_dimensions, available_space, _id, node_context, _style| { let Some(node_context) = node_context else { return taffy::geometry::Size::default(); }; let known_dimensions = Size { width: known_dimensions.width.map(|e| Pixels(e / scale_factor)), height: known_dimensions.height.map(|e| Pixels(e / scale_factor)), }; let available_space: Size = available_space.into(); let untransform = |ev: AvailableSpace| match ev { AvailableSpace::Definite(pixels) => { AvailableSpace::Definite(Pixels(pixels.0 / scale_factor)) } AvailableSpace::MinContent => AvailableSpace::MinContent, AvailableSpace::MaxContent => AvailableSpace::MaxContent, }; let available_space = size( untransform(available_space.width), untransform(available_space.height), ); let measured_size: Size = (node_context.measure)(known_dimensions, available_space, window, cx); snap_measured_size_to_device_pixels(measured_size, scale_factor).into() }, ) .expect(EXPECT_MESSAGE); } // Pixel snapping // // Painting primitives at non-integer pixel coordinates produces blurry // output. Pixel snapping converts layout coordinates into integer // device-pixel coordinates so painted edges land exactly on physical // pixel boundaries. // // Non-integer coordinates can arise for several reasons, including: // - flex distribution, percentages, centering, and text measurement // can produce fractional element sizes and positions; // - at fractional scale factors (for example 125% or 150%), integer // logical-pixel values can map to non-integer device-pixel values. // // We pixel-snap by rounding in device-pixel space, after multiplying // by `scale_factor`, so that snapping targets physical pixels. Bounds // are divided by `scale_factor` before being returned to GPUI. // // Midpoints are rounded toward zero. This is a stylistic choice: a // 1-logical-pixel line at 150% scale should render as 1 dp rather than // 2 dp. // // Pixel snapping is done in two phases: // // 1. Pre-layout metric snapping. Before Taffy computes layout, all // authored absolute lengths are rounded in `to_taffy`. This // includes borders, padding, gaps, and explicit sizes. // Custom-measured leaf nodes have their measured sizes rounded up // to integer device-pixel lengths. // // 2. Post-layout edge snapping. After Taffy resolves the tree, layout // relationships such as flex shares, grid tracks, percentages, and // centering can produce new fractional edge positions. Boxes now // have edges in absolute coordinates, and snapping must decide // where those edges land on the device-pixel grid. // // Ideally, post-layout snapping would satisfy: // // - Edge closure. Two raw layout edges at the same absolute position // should snap to the same pixel column. // - Translation stability. A component's internal geometry should not // change when it moves to a new absolute position. // // These goals are in tension because rounding is not associative. // The simple local schemes make different tradeoffs: // // - Absolute edge rounding gives each window coordinate one answer, // so coincident edges always close globally. But a span's snapped // length is `round(far) - round(near)`, which may change by 1 dp // as its absolute origin moves. // // - Parent-relative edge rounding rounds each child inside its // parent's coordinate space. This guarantees translation stability, // but a shared edge reached through different parents can // accumulate different rounding, causing non-closure between // cousins. // // - Length rounding rounds each width, height, and thickness // independently and then places boxes from those rounded lengths. // Sizes stay stable under translation, but neighboring boxes derive // their shared boundary from different sources, so closure is not // guaranteed. // // We apply absolute edge rounding for each element's outer box in // post-layout rounding to preserve closure. Border and padding widths // are not touched by post-layout rounding; they keep their pre-layout // rounded value so that they remain stable under translation. // // This gives both closure and translation stability in the case that // all local metrics are integer device-pixel lengths. Pre-layout // rounding covers that in most cases. The exception is metrics // resolved by layout relationships, such as percentages. Outer box // edges will still close globally, and painted border widths are still // snapped independently, but the raw content-box origin can carry a // 1dp residual into descendants. pub fn layout_bounds(&mut self, id: LayoutId, scale_factor: f32) -> Bounds { if let Some(layout) = self.absolute_layout_bounds.get(&id).cloned() { return layout; } let layout = self.taffy.layout(id.into()).expect(EXPECT_MESSAGE); let layout_location = layout.location; let layout_size = layout.size; let parent = self.taffy.parent(id.0); let absolute_outer_origin = match parent { Some(parent_id) => { let parent_id = LayoutId::from(parent_id); self.layout_bounds(parent_id, scale_factor); let parent_origin = *self .absolute_outer_origins .get(&parent_id) .expect("parent absolute outer origin should be cached"); parent_origin + Point::from(layout_location) } None => Point::from(layout_location), }; self.absolute_outer_origins .insert(id, absolute_outer_origin); let absolute_far = absolute_outer_origin + Point::from(Size::from(layout_size)); let snapped_bounds = Bounds::from_corners( absolute_outer_origin.map(round_half_toward_zero), absolute_far.map(round_half_toward_zero), ); let bounds = (snapped_bounds / scale_factor).map(Pixels); self.absolute_layout_bounds.insert(id, bounds); bounds } } /// A unique identifier for a layout node, generated when requesting a layout from Taffy #[derive(Copy, Clone, Eq, PartialEq, Debug)] #[repr(transparent)] pub struct LayoutId(NodeId); impl LayoutId { fn to_taffy_slice(node_ids: &[Self]) -> &[taffy::NodeId] { // SAFETY: LayoutId is repr(transparent) to taffy::tree::NodeId. unsafe { std::mem::transmute::<&[LayoutId], &[taffy::NodeId]>(node_ids) } } } impl std::hash::Hash for LayoutId { fn hash(&self, state: &mut H) { u64::from(self.0).hash(state); } } impl From for LayoutId { fn from(node_id: NodeId) -> Self { Self(node_id) } } impl From for NodeId { fn from(layout_id: LayoutId) -> NodeId { layout_id.0 } } fn snap_measured_size_to_device_pixels(size: Size, scale_factor: f32) -> Size { size.map(|d| ceil_to_device_pixel(d.0.max(0.0), scale_factor)) } fn border_widths_to_taffy( widths: &Edges, rem_size: Pixels, scale_factor: f32, ) -> TaffyRect { let snap = |w: &AbsoluteLength| { taffy::style::LengthPercentage::length(round_stroke_to_device_pixel( w.to_pixels(rem_size).0, scale_factor, )) }; TaffyRect { top: snap(&widths.top), right: snap(&widths.right), bottom: snap(&widths.bottom), left: snap(&widths.left), } } trait ToTaffy { fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> Output; } impl ToTaffy for Style { fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::Style { use taffy::style_helpers::{fr, length, minmax, repeat}; fn to_grid_line( placement: &Range, ) -> taffy::Line { taffy::Line { start: placement.start.into(), end: placement.end.into(), } } fn to_grid_repeat( unit: &Option, ) -> Vec> { unit.map(|template| { match template.min_size { // grid-template-*: repeat(, minmax(0, 1fr)); crate::GridTemplateMinSize::Zero => { vec![repeat( template.repeat, vec![minmax(length(0.0_f32), fr(1.0_f32))], )] } // grid-template-*: repeat(, minmax(min-content, 1fr)); crate::GridTemplateMinSize::MinContent => { vec![repeat( template.repeat, vec![minmax(min_content(), fr(1.0_f32))], )] } // grid-template-*: repeat(, minmax(0, max-content)) crate::GridTemplateMinSize::MaxContent => { vec![repeat( template.repeat, vec![minmax(length(0.0_f32), max_content())], )] } } }) .unwrap_or_default() } taffy::style::Style { display: self.display.into(), overflow: self.overflow.into(), scrollbar_width: self.scrollbar_width.to_taffy(rem_size, scale_factor), position: self.position.into(), inset: self.inset.to_taffy(rem_size, scale_factor), size: self.size.to_taffy(rem_size, scale_factor), min_size: self.min_size.to_taffy(rem_size, scale_factor), max_size: self.max_size.to_taffy(rem_size, scale_factor), aspect_ratio: self.aspect_ratio, margin: self.margin.to_taffy(rem_size, scale_factor), padding: self.padding.to_taffy(rem_size, scale_factor), border: border_widths_to_taffy(&self.border_widths, rem_size, scale_factor), align_items: self.align_items.map(|x| x.into()), align_self: self.align_self.map(|x| x.into()), align_content: self.align_content.map(|x| x.into()), justify_content: self.justify_content.map(|x| x.into()), gap: self.gap.to_taffy(rem_size, scale_factor), flex_direction: self.flex_direction.into(), flex_wrap: self.flex_wrap.into(), flex_basis: self.flex_basis.to_taffy(rem_size, scale_factor), flex_grow: self.flex_grow, flex_shrink: self.flex_shrink, grid_template_rows: to_grid_repeat(&self.grid_rows), grid_template_columns: to_grid_repeat(&self.grid_cols), grid_row: self .grid_location .as_ref() .map(|location| to_grid_line(&location.row)) .unwrap_or_default(), grid_column: self .grid_location .as_ref() .map(|location| to_grid_line(&location.column)) .unwrap_or_default(), ..Default::default() } } } impl ToTaffy for AbsoluteLength { fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> f32 { round_to_device_pixel(self.to_pixels(rem_size).0, scale_factor) } } impl ToTaffy for Length { fn to_taffy( &self, rem_size: Pixels, scale_factor: f32, ) -> taffy::prelude::LengthPercentageAuto { match self { Length::Definite(length) => length.to_taffy(rem_size, scale_factor), Length::Auto => taffy::prelude::LengthPercentageAuto::auto(), } } } impl ToTaffy for Length { fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::prelude::Dimension { match self { Length::Definite(length) => length.to_taffy(rem_size, scale_factor), Length::Auto => taffy::prelude::Dimension::auto(), } } } impl ToTaffy for DefiniteLength { fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::LengthPercentage { match self { DefiniteLength::Absolute(length) => length.to_taffy(rem_size, scale_factor), DefiniteLength::Fraction(fraction) => { taffy::style::LengthPercentage::percent(*fraction) } } } } impl ToTaffy for DefiniteLength { fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::LengthPercentageAuto { match self { DefiniteLength::Absolute(length) => length.to_taffy(rem_size, scale_factor), DefiniteLength::Fraction(fraction) => { taffy::style::LengthPercentageAuto::percent(*fraction) } } } } impl ToTaffy for DefiniteLength { fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::Dimension { match self { DefiniteLength::Absolute(length) => length.to_taffy(rem_size, scale_factor), DefiniteLength::Fraction(fraction) => taffy::style::Dimension::percent(*fraction), } } } impl ToTaffy for AbsoluteLength { fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::LengthPercentage { taffy::style::LengthPercentage::length(self.to_taffy(rem_size, scale_factor)) } } impl ToTaffy for AbsoluteLength { fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::LengthPercentageAuto { taffy::style::LengthPercentageAuto::length(self.to_taffy(rem_size, scale_factor)) } } impl ToTaffy for AbsoluteLength { fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::Dimension { taffy::style::Dimension::length(self.to_taffy(rem_size, scale_factor)) } } impl From> for Point where T: Into, T2: Clone + Debug + Default + PartialEq, { fn from(point: TaffyPoint) -> Point { Point { x: point.x.into(), y: point.y.into(), } } } impl From> for TaffyPoint where T: Into + Clone + Debug + Default + PartialEq, { fn from(val: Point) -> Self { TaffyPoint { x: val.x.into(), y: val.y.into(), } } } impl ToTaffy> for Size where T: ToTaffy + Clone + Debug + Default + PartialEq, { fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> TaffySize { TaffySize { width: self.width.to_taffy(rem_size, scale_factor), height: self.height.to_taffy(rem_size, scale_factor), } } } impl ToTaffy> for Edges where T: ToTaffy + Clone + Debug + Default + PartialEq, { fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> TaffyRect { TaffyRect { top: self.top.to_taffy(rem_size, scale_factor), right: self.right.to_taffy(rem_size, scale_factor), bottom: self.bottom.to_taffy(rem_size, scale_factor), left: self.left.to_taffy(rem_size, scale_factor), } } } impl From> for Size where T: Into, U: Clone + Debug + Default + PartialEq, { fn from(taffy_size: TaffySize) -> Self { Size { width: taffy_size.width.into(), height: taffy_size.height.into(), } } } impl From> for TaffySize where T: Into + Clone + Debug + Default + PartialEq, { fn from(size: Size) -> Self { TaffySize { width: size.width.into(), height: size.height.into(), } } } /// The space available for an element to be laid out in #[derive(Copy, Clone, Default, Debug, Eq, PartialEq)] pub enum AvailableSpace { /// The amount of space available is the specified number of pixels Definite(Pixels), /// The amount of space available is indefinite and the node should be laid out under a min-content constraint #[default] MinContent, /// The amount of space available is indefinite and the node should be laid out under a max-content constraint MaxContent, } impl AvailableSpace { /// Returns a `Size` with both width and height set to `AvailableSpace::MinContent`. /// /// This function is useful when you want to create a `Size` with the minimum content constraints /// for both dimensions. /// /// # Examples /// /// ``` /// use gpui::AvailableSpace; /// let min_content_size = AvailableSpace::min_size(); /// assert_eq!(min_content_size.width, AvailableSpace::MinContent); /// assert_eq!(min_content_size.height, AvailableSpace::MinContent); /// ``` pub const fn min_size() -> Size { Size { width: Self::MinContent, height: Self::MinContent, } } } impl From for TaffyAvailableSpace { fn from(space: AvailableSpace) -> TaffyAvailableSpace { match space { AvailableSpace::Definite(Pixels(value)) => TaffyAvailableSpace::Definite(value), AvailableSpace::MinContent => TaffyAvailableSpace::MinContent, AvailableSpace::MaxContent => TaffyAvailableSpace::MaxContent, } } } impl From for AvailableSpace { fn from(space: TaffyAvailableSpace) -> AvailableSpace { match space { TaffyAvailableSpace::Definite(value) => AvailableSpace::Definite(Pixels(value)), TaffyAvailableSpace::MinContent => AvailableSpace::MinContent, TaffyAvailableSpace::MaxContent => AvailableSpace::MaxContent, } } } impl From for AvailableSpace { fn from(pixels: Pixels) -> Self { AvailableSpace::Definite(pixels) } } impl From> for Size { fn from(size: Size) -> Self { Size { width: AvailableSpace::Definite(size.width), height: AvailableSpace::Definite(size.height), } } } #[cfg(test)] mod tests { use super::*; #[test] fn border_widths_to_taffy_use_stroke_snapping() { let border_widths = Edges { top: Pixels(0.0).into(), right: Pixels(0.4).into(), bottom: Pixels(0.5).into(), left: Pixels(1.6).into(), }; let taffy_border = border_widths_to_taffy(&border_widths, Pixels(16.0), 1.0); assert_eq!( taffy_border.top, taffy::style::LengthPercentage::length(0.0) ); assert_eq!( taffy_border.right, taffy::style::LengthPercentage::length(1.0) ); assert_eq!( taffy_border.bottom, taffy::style::LengthPercentage::length(1.0) ); assert_eq!( taffy_border.left, taffy::style::LengthPercentage::length(2.0) ); } }