ZIm/crates/gpui/src/taffy.rs
Michael Sloan 297fd858e8
Code for debugging issue with no width available
This commit enables extremely verbose taffy logging and adds output of the layout ids for the Open Router settings container ID and the ID for the text input that has the issue.  https://gist.github.com/mgsloan/28a2d8d9c9c803fc60898eae9db1ec8e is the subsets of the logs starting with the first line with the container ID and ending with the last line that has the container ID (see the end of the logs for these IDs)

Repro for the issue:

* Set ui_font_size to 20 in user settings.json
* Open zed agent settings
* Expand Open Router settings
* Resize panel until the API key input field becomes small
2025-08-14 19:10:19 -06:00

550 lines
17 KiB
Rust

use crate::{
AbsoluteLength, App, Bounds, DefiniteLength, Edges, Length, Pixels, Point, Size, Style, Window,
};
use collections::{FxHashMap, FxHashSet};
use smallvec::SmallVec;
use std::{cell::RefCell, fmt::Debug, ops::Range};
use taffy::{
TaffyTree, TraversePartialTree as _,
geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize},
style::AvailableSpace as TaffyAvailableSpace,
tree::NodeId,
};
thread_local! {
pub static LAYOUT_ID_TO_DEBUG: RefCell<Option<LayoutId>> = const { RefCell::new(None) };
}
thread_local! {
pub static CONTAINER_LAYOUT_ID_TO_DEBUG: RefCell<Option<LayoutId>> = const { RefCell::new(None) };
}
type NodeMeasureFn = Box<
dyn FnMut(Size<Option<Pixels>>, Size<AvailableSpace>, &mut Window, &mut App) -> Size<Pixels>,
>;
struct NodeContext {
measure: NodeMeasureFn,
}
pub struct TaffyLayoutEngine {
taffy: TaffyTree<NodeContext>,
absolute_layout_bounds: FxHashMap<LayoutId, Bounds<Pixels>>,
computed_layouts: FxHashSet<LayoutId>,
}
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(),
computed_layouts: FxHashSet::default(),
}
}
pub fn clear(&mut self) {
self.taffy.clear();
self.absolute_layout_bounds.clear();
self.computed_layouts.clear();
}
pub fn request_layout(
&mut self,
style: Style,
rem_size: Pixels,
children: &[LayoutId],
) -> LayoutId {
let taffy_style = style.to_taffy(rem_size);
let layout_id = if children.is_empty() {
self.taffy
.new_leaf(taffy_style)
.expect(EXPECT_MESSAGE)
.into()
} else {
let parent_id = self
.taffy
// This is safe because LayoutId is repr(transparent) to taffy::tree::NodeId.
.new_with_children(taffy_style, unsafe {
std::mem::transmute::<&[LayoutId], &[taffy::NodeId]>(children)
})
.expect(EXPECT_MESSAGE)
.into();
parent_id
};
layout_id
}
pub fn request_measured_layout(
&mut self,
style: Style,
rem_size: Pixels,
measure: impl FnMut(
Size<Option<Pixels>>,
Size<AvailableSpace>,
&mut Window,
&mut App,
) -> Size<Pixels>
+ 'static,
) -> LayoutId {
let taffy_style = style.to_taffy(rem_size);
let layout_id = self
.taffy
.new_leaf_with_context(
taffy_style,
NodeContext {
measure: Box::new(measure),
},
)
.expect(EXPECT_MESSAGE)
.into();
layout_id
}
// Used to understand performance
#[allow(dead_code)]
fn count_all_children(&self, parent: LayoutId) -> anyhow::Result<u32> {
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<u32> {
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<Vec<(LayoutId, LayoutId)>> {
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)
}
pub fn compute_layout(
&mut self,
id: LayoutId,
available_space: Size<AvailableSpace>,
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));
// }
// println!("");
//
if !self.computed_layouts.insert(id) {
let mut stack = SmallVec::<[LayoutId; 64]>::new();
stack.push(id);
while let Some(id) = stack.pop() {
self.absolute_layout_bounds.remove(&id);
stack.extend(
self.taffy
.children(id.into())
.expect(EXPECT_MESSAGE)
.into_iter()
.map(Into::into),
);
}
}
// let started_at = std::time::Instant::now();
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(Pixels),
height: known_dimensions.height.map(Pixels),
};
(node_context.measure)(known_dimensions, available_space.into(), window, cx)
.into()
},
)
.expect(EXPECT_MESSAGE);
LAYOUT_ID_TO_DEBUG.with_borrow(|layout_id_to_debug| {
println!("Layout ID Debug: {:?}", layout_id_to_debug);
});
CONTAINER_LAYOUT_ID_TO_DEBUG.with_borrow(|layout_id| {
println!("Container Layout ID Debug: {:?}\n", layout_id);
});
// println!("compute_layout took {:?}", started_at.elapsed());
}
pub fn layout_bounds(&mut self, id: LayoutId) -> Bounds<Pixels> {
if let Some(layout) = self.absolute_layout_bounds.get(&id).cloned() {
return layout;
}
let layout = self.taffy.layout(id.into()).expect(EXPECT_MESSAGE);
let mut bounds = Bounds {
origin: layout.location.into(),
size: layout.size.into(),
};
if let Some(parent_id) = self.taffy.parent(id.0) {
let parent_bounds = self.layout_bounds(parent_id.into());
bounds.origin += parent_bounds.origin;
}
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 std::hash::Hash for LayoutId {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
u64::from(self.0).hash(state);
}
}
impl From<NodeId> for LayoutId {
fn from(node_id: NodeId) -> Self {
Self(node_id)
}
}
impl From<LayoutId> for NodeId {
fn from(layout_id: LayoutId) -> NodeId {
layout_id.0
}
}
trait ToTaffy<Output> {
fn to_taffy(&self, rem_size: Pixels) -> Output;
}
impl ToTaffy<taffy::style::Style> for Style {
fn to_taffy(&self, rem_size: Pixels) -> taffy::style::Style {
use taffy::style_helpers::{fr, length, minmax, repeat};
fn to_grid_line(
placement: &Range<crate::GridPlacement>,
) -> taffy::Line<taffy::GridPlacement> {
taffy::Line {
start: placement.start.into(),
end: placement.end.into(),
}
}
fn to_grid_repeat<T: taffy::style::CheapCloneStr>(
unit: &Option<u16>,
) -> Vec<taffy::GridTemplateComponent<T>> {
// grid-template-columns: repeat(<number>, minmax(0, 1fr));
unit.map(|count| vec![repeat(count, vec![minmax(length(0.0), fr(1.0))])])
.unwrap_or_default()
}
taffy::style::Style {
display: self.display.into(),
overflow: self.overflow.into(),
scrollbar_width: self.scrollbar_width,
position: self.position.into(),
inset: self.inset.to_taffy(rem_size),
size: self.size.to_taffy(rem_size),
min_size: self.min_size.to_taffy(rem_size),
max_size: self.max_size.to_taffy(rem_size),
aspect_ratio: self.aspect_ratio,
margin: self.margin.to_taffy(rem_size),
padding: self.padding.to_taffy(rem_size),
border: self.border_widths.to_taffy(rem_size),
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),
flex_direction: self.flex_direction.into(),
flex_wrap: self.flex_wrap.into(),
flex_basis: self.flex_basis.to_taffy(rem_size),
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<taffy::style::LengthPercentageAuto> for Length {
fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::LengthPercentageAuto {
match self {
Length::Definite(length) => length.to_taffy(rem_size),
Length::Auto => taffy::prelude::LengthPercentageAuto::auto(),
}
}
}
impl ToTaffy<taffy::style::Dimension> for Length {
fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::Dimension {
match self {
Length::Definite(length) => length.to_taffy(rem_size),
Length::Auto => taffy::prelude::Dimension::auto(),
}
}
}
impl ToTaffy<taffy::style::LengthPercentage> for DefiniteLength {
fn to_taffy(&self, rem_size: Pixels) -> taffy::style::LengthPercentage {
match self {
DefiniteLength::Absolute(length) => match length {
AbsoluteLength::Pixels(pixels) => {
taffy::style::LengthPercentage::length(pixels.into())
}
AbsoluteLength::Rems(rems) => {
taffy::style::LengthPercentage::length((*rems * rem_size).into())
}
},
DefiniteLength::Fraction(fraction) => {
taffy::style::LengthPercentage::percent(*fraction)
}
}
}
}
impl ToTaffy<taffy::style::LengthPercentageAuto> for DefiniteLength {
fn to_taffy(&self, rem_size: Pixels) -> taffy::style::LengthPercentageAuto {
match self {
DefiniteLength::Absolute(length) => match length {
AbsoluteLength::Pixels(pixels) => {
taffy::style::LengthPercentageAuto::length(pixels.into())
}
AbsoluteLength::Rems(rems) => {
taffy::style::LengthPercentageAuto::length((*rems * rem_size).into())
}
},
DefiniteLength::Fraction(fraction) => {
taffy::style::LengthPercentageAuto::percent(*fraction)
}
}
}
}
impl ToTaffy<taffy::style::Dimension> for DefiniteLength {
fn to_taffy(&self, rem_size: Pixels) -> taffy::style::Dimension {
match self {
DefiniteLength::Absolute(length) => match length {
AbsoluteLength::Pixels(pixels) => taffy::style::Dimension::length(pixels.into()),
AbsoluteLength::Rems(rems) => {
taffy::style::Dimension::length((*rems * rem_size).into())
}
},
DefiniteLength::Fraction(fraction) => taffy::style::Dimension::percent(*fraction),
}
}
}
impl ToTaffy<taffy::style::LengthPercentage> for AbsoluteLength {
fn to_taffy(&self, rem_size: Pixels) -> taffy::style::LengthPercentage {
match self {
AbsoluteLength::Pixels(pixels) => taffy::style::LengthPercentage::length(pixels.into()),
AbsoluteLength::Rems(rems) => {
taffy::style::LengthPercentage::length((*rems * rem_size).into())
}
}
}
}
impl<T, T2> From<TaffyPoint<T>> for Point<T2>
where
T: Into<T2>,
T2: Clone + Debug + Default + PartialEq,
{
fn from(point: TaffyPoint<T>) -> Point<T2> {
Point {
x: point.x.into(),
y: point.y.into(),
}
}
}
impl<T, T2> From<Point<T>> for TaffyPoint<T2>
where
T: Into<T2> + Clone + Debug + Default + PartialEq,
{
fn from(val: Point<T>) -> Self {
TaffyPoint {
x: val.x.into(),
y: val.y.into(),
}
}
}
impl<T, U> ToTaffy<TaffySize<U>> for Size<T>
where
T: ToTaffy<U> + Clone + Debug + Default + PartialEq,
{
fn to_taffy(&self, rem_size: Pixels) -> TaffySize<U> {
TaffySize {
width: self.width.to_taffy(rem_size),
height: self.height.to_taffy(rem_size),
}
}
}
impl<T, U> ToTaffy<TaffyRect<U>> for Edges<T>
where
T: ToTaffy<U> + Clone + Debug + Default + PartialEq,
{
fn to_taffy(&self, rem_size: Pixels) -> TaffyRect<U> {
TaffyRect {
top: self.top.to_taffy(rem_size),
right: self.right.to_taffy(rem_size),
bottom: self.bottom.to_taffy(rem_size),
left: self.left.to_taffy(rem_size),
}
}
}
impl<T, U> From<TaffySize<T>> for Size<U>
where
T: Into<U>,
U: Clone + Debug + Default + PartialEq,
{
fn from(taffy_size: TaffySize<T>) -> Self {
Size {
width: taffy_size.width.into(),
height: taffy_size.height.into(),
}
}
}
impl<T, U> From<Size<T>> for TaffySize<U>
where
T: Into<U> + Clone + Debug + Default + PartialEq,
{
fn from(size: Size<T>) -> 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
///
/// ```
/// 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<Self> {
Size {
width: Self::MinContent,
height: Self::MinContent,
}
}
}
impl From<AvailableSpace> 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<TaffyAvailableSpace> 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<Pixels> for AvailableSpace {
fn from(pixels: Pixels) -> Self {
AvailableSpace::Definite(pixels)
}
}
impl From<Size<Pixels>> for Size<AvailableSpace> {
fn from(size: Size<Pixels>) -> Self {
Size {
width: AvailableSpace::Definite(size.width),
height: AvailableSpace::Definite(size.height),
}
}
}