Fix text wrapping when a child of a v_stack() (#3362)

Conrad Irwin created

Previously text that was rendered in a flex-column would reserve the
correct
amount of space during layout, and then paint itself incorrectly.

Release Notes:

- N/A

Change summary

Cargo.lock                            | 23 +++++++++++-
crates/gpui2/Cargo.toml               |  2 
crates/gpui2/src/elements/text.rs     | 30 ++++++++++++++--
crates/gpui2/src/taffy.rs             | 46 ++++++++++++-------------
crates/storybook2/src/stories/text.rs | 51 +++++++++++++++++++++++++---
crates/storybook3/src/storybook3.rs   | 18 +++++++++-
6 files changed, 129 insertions(+), 41 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3811,7 +3811,7 @@ dependencies = [
  "smol",
  "sqlez",
  "sum_tree",
- "taffy",
+ "taffy 0.3.11 (git+https://github.com/DioxusLabs/taffy?rev=4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e)",
  "thiserror",
  "time",
  "tiny-skia",
@@ -3876,7 +3876,7 @@ dependencies = [
  "smol",
  "sqlez",
  "sum_tree",
- "taffy",
+ "taffy 0.3.11 (git+https://github.com/DioxusLabs/taffy?rev=1876f72bee5e376023eaa518aa7b8a34c769bd1b)",
  "thiserror",
  "time",
  "tiny-skia",
@@ -3911,6 +3911,12 @@ version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eec1c01eb1de97451ee0d60de7d81cf1e72aabefb021616027f3d1c3ec1c723c"
 
+[[package]]
+name = "grid"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1df00eed8d1f0db937f6be10e46e8072b0671accb504cf0f959c5c52c679f5b9"
+
 [[package]]
 name = "h2"
 version = "0.3.21"
@@ -9105,13 +9111,24 @@ dependencies = [
  "winx",
 ]
 
+[[package]]
+name = "taffy"
+version = "0.3.11"
+source = "git+https://github.com/DioxusLabs/taffy?rev=1876f72bee5e376023eaa518aa7b8a34c769bd1b#1876f72bee5e376023eaa518aa7b8a34c769bd1b"
+dependencies = [
+ "arrayvec 0.7.4",
+ "grid 0.11.0",
+ "num-traits",
+ "slotmap",
+]
+
 [[package]]
 name = "taffy"
 version = "0.3.11"
 source = "git+https://github.com/DioxusLabs/taffy?rev=4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e#4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e"
 dependencies = [
  "arrayvec 0.7.4",
- "grid",
+ "grid 0.10.0",
  "num-traits",
  "slotmap",
 ]

crates/gpui2/Cargo.toml 🔗

@@ -47,7 +47,7 @@ serde_derive.workspace = true
 serde_json.workspace = true
 smallvec.workspace = true
 smol.workspace = true
-taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e" }
+taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "1876f72bee5e376023eaa518aa7b8a34c769bd1b" }
 thiserror.workspace = true
 time.workspace = true
 tiny-skia = "0.5"

crates/gpui2/src/elements/text.rs 🔗

@@ -64,19 +64,34 @@ impl<V: 'static> Element<V> for Text {
 
         let layout_id = cx.request_measured_layout(Default::default(), rem_size, {
             let element_state = element_state.clone();
-            move |known_dimensions, _| {
+            move |known_dimensions, available_space| {
+                let wrap_width = known_dimensions.width.or(match available_space.width {
+                    crate::AvailableSpace::Definite(x) => Some(x),
+                    _ => None,
+                });
+
+                if let Some(text_state) = element_state.0.lock().as_ref() {
+                    if text_state.size.is_some()
+                        && (wrap_width.is_none() || wrap_width == text_state.wrap_width)
+                    {
+                        return text_state.size.unwrap();
+                    }
+                }
+
                 let Some(lines) = text_system
                     .shape_text(
                         &text,
                         font_size,
                         &runs[..],
-                        known_dimensions.width, // Wrap if we know the width.
+                        wrap_width, // Wrap if we know the width.
                     )
                     .log_err()
                 else {
                     element_state.lock().replace(TextStateInner {
                         lines: Default::default(),
                         line_height,
+                        wrap_width,
+                        size: Some(Size::default()),
                     });
                     return Size::default();
                 };
@@ -88,9 +103,12 @@ impl<V: 'static> Element<V> for Text {
                     size.width = size.width.max(line_size.width);
                 }
 
-                element_state
-                    .lock()
-                    .replace(TextStateInner { lines, line_height });
+                element_state.lock().replace(TextStateInner {
+                    lines,
+                    line_height,
+                    wrap_width,
+                    size: Some(size),
+                });
 
                 size
             }
@@ -133,6 +151,8 @@ impl TextState {
 struct TextStateInner {
     lines: SmallVec<[WrappedLine; 1]>,
     line_height: Pixels,
+    wrap_width: Option<Pixels>,
+    size: Option<Size<Pixels>>,
 }
 
 struct InteractiveText {

crates/gpui2/src/taffy.rs 🔗

@@ -5,12 +5,14 @@ use std::fmt::Debug;
 use taffy::{
     geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize},
     style::AvailableSpace as TaffyAvailableSpace,
-    tree::{Measurable, MeasureFunc, NodeId},
+    tree::NodeId,
     Taffy,
 };
 
+type Measureable = dyn Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels> + Send + Sync;
+
 pub struct TaffyLayoutEngine {
-    taffy: Taffy,
+    taffy: Taffy<Box<Measureable>>,
     children_to_parents: HashMap<LayoutId, LayoutId>,
     absolute_layout_bounds: HashMap<LayoutId, Bounds<Pixels>>,
     computed_layouts: HashSet<LayoutId>,
@@ -70,9 +72,9 @@ impl TaffyLayoutEngine {
     ) -> LayoutId {
         let style = style.to_taffy(rem_size);
 
-        let measurable = Box::new(Measureable(measure)) as Box<dyn Measurable>;
+        let measurable = Box::new(measure);
         self.taffy
-            .new_leaf_with_measure(style, MeasureFunc::Boxed(measurable))
+            .new_leaf_with_context(style, measurable)
             .expect(EXPECT_MESSAGE)
             .into()
     }
@@ -154,7 +156,22 @@ impl TaffyLayoutEngine {
 
         // let started_at = std::time::Instant::now();
         self.taffy
-            .compute_layout(id.into(), available_space.into())
+            .compute_layout_with_measure(
+                id.into(),
+                available_space.into(),
+                |known_dimensions, available_space, _node_id, context| {
+                    let Some(measure) = context else {
+                        return taffy::geometry::Size::default();
+                    };
+
+                    let known_dimensions = Size {
+                        width: known_dimensions.width.map(Pixels),
+                        height: known_dimensions.height.map(Pixels),
+                    };
+
+                    measure(known_dimensions, available_space.into()).into()
+                },
+            )
             .expect(EXPECT_MESSAGE);
         // println!("compute_layout took {:?}", started_at.elapsed());
     }
@@ -202,25 +219,6 @@ impl From<LayoutId> for NodeId {
     }
 }
 
-struct Measureable<F>(F);
-
-impl<F> taffy::tree::Measurable for Measureable<F>
-where
-    F: Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels> + Send + Sync,
-{
-    fn measure(
-        &self,
-        known_dimensions: TaffySize<Option<f32>>,
-        available_space: TaffySize<TaffyAvailableSpace>,
-    ) -> TaffySize<f32> {
-        let known_dimensions: Size<Option<f32>> = known_dimensions.into();
-        let known_dimensions: Size<Option<Pixels>> = known_dimensions.map(|d| d.map(Into::into));
-        let available_space = available_space.into();
-        let size = (self.0)(known_dimensions, available_space);
-        size.into()
-    }
-}
-
 trait ToTaffy<Output> {
     fn to_taffy(&self, rem_size: Pixels) -> Output;
 }

crates/storybook2/src/stories/text.rs 🔗

@@ -1,4 +1,7 @@
-use gpui::{div, white, Div, ParentComponent, Render, Styled, View, VisualContext, WindowContext};
+use gpui::{
+    blue, div, red, white, Div, ParentComponent, Render, Styled, View, VisualContext, WindowContext,
+};
+use ui::v_stack;
 
 pub struct TextStory;
 
@@ -12,10 +15,46 @@ impl Render for TextStory {
     type Element = Div<Self>;
 
     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
-        div().size_full().bg(white()).child(concat!(
-            "The quick brown fox jumps over the lazy dog. ",
-            "Meanwhile, the lazy dog decided it was time for a change. ",
-            "He started daily workout routines, ate healthier and became the fastest dog in town.",
-        ))
+        v_stack()
+            .bg(blue())
+            .child(
+                div()
+                    .flex()
+                    .child(div().max_w_96().bg(white()).child(concat!(
+        "max-width: 96. The quick brown fox jumps over the lazy dog. ",
+        "Meanwhile, the lazy dog decided it was time for a change. ",
+        "He started daily workout routines, ate healthier and became the fastest dog in town.",
+    ))),
+            )
+            .child(div().h_5())
+            .child(div().flex().flex_col().w_96().bg(white()).child(concat!(
+        "flex-col. width: 96; The quick brown fox jumps over the lazy dog. ",
+        "Meanwhile, the lazy dog decided it was time for a change. ",
+        "He started daily workout routines, ate healthier and became the fastest dog in town.",
+    )))
+            .child(div().h_5())
+            .child(
+                div()
+                    .flex()
+                    .child(div().min_w_96().bg(white()).child(concat!(
+    "min-width: 96. The quick brown fox jumps over the lazy dog. ",
+    "Meanwhile, the lazy dog decided it was time for a change. ",
+    "He started daily workout routines, ate healthier and became the fastest dog in town.",
+))))
+            .child(div().h_5())
+            .child(div().flex().w_96().bg(white()).child(div().overflow_hidden().child(concat!(
+        "flex-row. width 96. overflow-hidden. The quick brown fox jumps over the lazy dog. ",
+        "Meanwhile, the lazy dog decided it was time for a change. ",
+        "He started daily workout routines, ate healthier and became the fastest dog in town.",
+    ))))
+            // NOTE: When rendering text in a horizonal flex container,
+            // Taffy will not pass width constraints down from the parent.
+            // To fix this, render text in a praent with overflow: hidden, which
+                    .child(div().h_5())
+                    .child(div().flex().w_96().bg(red()).child(concat!(
+                "flex-row. width 96. The quick brown fox jumps over the lazy dog. ",
+                "Meanwhile, the lazy dog decided it was time for a change. ",
+                "He started daily workout routines, ate healthier and became the fastest dog in town.",
+            )))
     }
 }

crates/storybook3/src/storybook3.rs 🔗

@@ -1,9 +1,9 @@
 use anyhow::Result;
-use gpui::AssetSource;
 use gpui::{
     div, px, size, AnyView, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds,
     WindowOptions,
 };
+use gpui::{white, AssetSource};
 use settings::{default_settings, Settings, SettingsStore};
 use std::borrow::Cow;
 use std::sync::Arc;
@@ -56,6 +56,7 @@ fn main() {
 }
 
 struct TestView {
+    #[allow(unused)]
     story: AnyView,
 }
 
@@ -65,9 +66,22 @@ impl Render for TestView {
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         div()
             .flex()
+            .bg(gpui::blue())
             .flex_col()
             .size_full()
             .font("Helvetica")
-            .child(self.story.clone())
+            .child(div().h_5())
+            .child(
+                div()
+                    .flex()
+                    .w_96()
+                    .bg(white())
+                    .relative()
+                    .child(div().child(concat!(
+            "The quick brown fox jumps over the lazy dog. ",
+            "Meanwhile, the lazy dog decided it was time for a change. ",
+            "He started daily workout routines, ate healthier and became the fastest dog in town.",
+        ))),
+            )
     }
 }