gpui: Fix `text-align` with nowrap mode (#24116)

Jason Lee and Owen Law created

Release Notes:

- N/A


------

- Continue #24090 to fix text align for when used `whitespace_nowrap`.
- Fix wrapped line length calculation.

And add example

```
cargo run -p gpui --example text_layout
```

<img width="760" alt="image"
src="https://github.com/user-attachments/assets/a087c300-0e0e-4a80-98c6-90161a9b0905"
/>

---------

Co-authored-by: Owen Law <owenlaw222@gmail.com>

Change summary

crates/gpui/examples/text_layout.rs | 64 +++++++++++++++++++++++++++++++
crates/gpui/src/elements/div.rs     |  2 
crates/gpui/src/elements/text.rs    | 11 ++++
crates/gpui/src/text_system/line.rs | 22 ++++------
4 files changed, 83 insertions(+), 16 deletions(-)

Detailed changes

crates/gpui/examples/text_layout.rs 🔗

@@ -0,0 +1,64 @@
+use gpui::{
+    div, prelude::*, px, size, App, Application, Bounds, Context, Window, WindowBounds,
+    WindowOptions,
+};
+
+struct HelloWorld {}
+
+impl Render for HelloWorld {
+    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+        div()
+            .bg(gpui::white())
+            .flex()
+            .flex_col()
+            .gap_3()
+            .p_4()
+            .size_full()
+            .child(div().child("Text left"))
+            .child(div().text_center().child("Text center"))
+            .child(div().text_right().child("Text right"))
+            .child(
+                div()
+                    .flex()
+                    .gap_2()
+                    .justify_between()
+                    .child(
+                        div()
+                            .w(px(400.))
+                            .border_1()
+                            .border_color(gpui::blue())
+                            .p_1()
+                            .whitespace_nowrap()
+                            .overflow_hidden()
+                            .text_center()
+                            .child("A long non-wrapping text align center"),
+                    )
+                    .child(
+                        div()
+                            .w_32()
+                            .border_1()
+                            .border_color(gpui::blue())
+                            .p_1()
+                            .whitespace_nowrap()
+                            .overflow_hidden()
+                            .text_right()
+                            .child("100%"),
+                    ),
+            )
+    }
+}
+
+fn main() {
+    Application::new().run(|cx: &mut App| {
+        let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx);
+        cx.open_window(
+            WindowOptions {
+                window_bounds: Some(WindowBounds::Windowed(bounds)),
+                ..Default::default()
+            },
+            |_, cx| cx.new(|_| HelloWorld {}),
+        )
+        .unwrap();
+        cx.activate(true);
+    });
+}

crates/gpui/src/elements/div.rs 🔗

@@ -1684,7 +1684,7 @@ impl Interactivity {
                     .ok()
                     .and_then(|mut text| text.pop())
                 {
-                    text.paint(hitbox.origin, FONT_SIZE, TextAlign::Left, window, cx)
+                    text.paint(hitbox.origin, FONT_SIZE, TextAlign::Left, None, window, cx)
                         .ok();
 
                     let text_bounds = crate::Bounds {

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

@@ -392,8 +392,15 @@ impl TextLayout {
         let mut line_origin = bounds.origin;
         let text_style = window.text_style();
         for line in &element_state.lines {
-            line.paint(line_origin, line_height, text_style.text_align, window, cx)
-                .log_err();
+            line.paint(
+                line_origin,
+                line_height,
+                text_style.text_align,
+                Some(bounds),
+                window,
+                cx,
+            )
+            .log_err();
             line_origin.y += line.size(line_height).height;
         }
     }

crates/gpui/src/text_system/line.rs 🔗

@@ -107,15 +107,21 @@ impl WrappedLine {
         origin: Point<Pixels>,
         line_height: Pixels,
         align: TextAlign,
+        bounds: Option<Bounds<Pixels>>,
         window: &mut Window,
         cx: &mut App,
     ) -> Result<()> {
+        let align_width = match bounds {
+            Some(bounds) => Some(bounds.size.width),
+            None => self.layout.wrap_width,
+        };
+
         paint_line(
             origin,
             &self.layout.unwrapped_layout,
             line_height,
             align,
-            self.layout.wrap_width,
+            align_width,
             &self.decoration_runs,
             &self.wrap_boundaries,
             window,
@@ -222,7 +228,7 @@ fn paint_line(
                     glyph_origin.x = aligned_origin_x(
                         origin,
                         align_width.unwrap_or(layout.width),
-                        prev_glyph_position.x,
+                        glyph.position.x,
                         &align,
                         layout,
                         wraps.peek(),
@@ -426,17 +432,7 @@ fn aligned_origin_x(
     wrap_boundary: Option<&&WrapBoundary>,
 ) -> Pixels {
     let end_of_line = if let Some(WrapBoundary { run_ix, glyph_ix }) = wrap_boundary {
-        if layout.runs[*run_ix].glyphs.len() == glyph_ix + 1 {
-            // Next glyph is in next run
-            layout
-                .runs
-                .get(run_ix + 1)
-                .and_then(|run| run.glyphs.first())
-                .map_or(layout.width, |glyph| glyph.position.x)
-        } else {
-            // Get next glyph
-            layout.runs[*run_ix].glyphs[*glyph_ix + 1].position.x
-        }
+        layout.runs[*run_ix].glyphs[*glyph_ix].position.x
     } else {
         layout.width
     };