Wrap lines in `Text` element

Antonio Scandurra created

Change summary

gpui/src/elements/text.rs | 59 ++++++++++++++++++++++++++--------------
gpui/src/presenter.rs     |  4 ++
gpui/src/text_layout.rs   | 48 +++++++++++++++++++++++++++++++++
3 files changed, 89 insertions(+), 22 deletions(-)

Detailed changes

gpui/src/elements/text.rs 🔗

@@ -1,19 +1,16 @@
 use crate::{
     color::Color,
     font_cache::FamilyId,
-    fonts::{FontId, TextStyle},
+    fonts::TextStyle,
     geometry::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
     json::{ToJson, Value},
-    text_layout::Line,
-    DebugContext, Element, Event, EventContext, FontCache, LayoutContext, PaintContext,
-    SizeConstraint,
+    text_layout::{Line, LineWrapper, ShapedBoundary},
+    DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
 };
-use serde::Deserialize;
 use serde_json::json;
-use smallvec::{smallvec, SmallVec};
 
 pub struct Text {
     text: String,
@@ -22,6 +19,12 @@ pub struct Text {
     style: TextStyle,
 }
 
+pub struct LayoutState {
+    line: Line,
+    wrap_boundaries: Vec<ShapedBoundary>,
+    line_height: f32,
+}
+
 impl Text {
     pub fn new(text: String, family_id: FamilyId, font_size: f32) -> Self {
         Self {
@@ -44,7 +47,7 @@ impl Text {
 }
 
 impl Element for Text {
-    type LayoutState = Line;
+    type LayoutState = LayoutState;
     type PaintState = ();
 
     fn layout(
@@ -56,30 +59,44 @@ impl Element for Text {
             .font_cache
             .select_font(self.family_id, &self.style.font_properties)
             .unwrap();
-        todo!()
-        // let line =
-        //     cx.text_layout_cache
-        //         .layout_str(self.text.as_str(), self.font_size, runs.as_slice());
-
-        // let size = vec2f(
-        //     line.width().max(constraint.min.x()).min(constraint.max.x()),
-        //     cx.font_cache.line_height(font_id, self.font_size).ceil(),
-        // );
+        let line_height = cx.font_cache.line_height(font_id, self.font_size);
+        let line = cx.text_layout_cache.layout_str(
+            self.text.as_str(),
+            self.font_size,
+            &[(self.text.len(), font_id, self.style.color)],
+        );
+        let mut wrapper = LineWrapper::acquire(font_id, self.font_size, cx.font_system.clone());
+        let wrap_boundaries = wrapper
+            .wrap_shaped_line(&self.text, &line, constraint.max.x())
+            .collect::<Vec<_>>();
+        let size = vec2f(
+            line.width()
+                .ceil()
+                .max(constraint.min.x())
+                .min(constraint.max.x()),
+            (line_height * (wrap_boundaries.len() + 1) as f32).ceil(),
+        );
+        let layout = LayoutState {
+            line,
+            wrap_boundaries,
+            line_height,
+        };
 
-        // (size, line)
+        (size, layout)
     }
 
     fn paint(
         &mut self,
         bounds: RectF,
-        line: &mut Self::LayoutState,
+        layout: &mut Self::LayoutState,
         cx: &mut PaintContext,
     ) -> Self::PaintState {
-        line.paint(
+        layout.line.paint_wrapped(
             bounds.origin(),
-            RectF::new(vec2f(0., 0.), bounds.size()),
+            layout.line_height,
+            layout.wrap_boundaries.iter().copied(),
             cx,
-        )
+        );
     }
 
     fn dispatch_event(

gpui/src/presenter.rs 🔗

@@ -5,7 +5,7 @@ use crate::{
     json::{self, ToJson},
     platform::Event,
     text_layout::TextLayoutCache,
-    Action, AnyAction, AssetCache, ElementBox, Scene,
+    Action, AnyAction, AssetCache, ElementBox, FontSystem, Scene,
 };
 use pathfinder_geometry::vector::{vec2f, Vector2F};
 use serde_json::json;
@@ -114,6 +114,7 @@ impl Presenter {
             rendered_views: &mut self.rendered_views,
             parents: &mut self.parents,
             font_cache: &self.font_cache,
+            font_system: cx.platform().fonts(),
             text_layout_cache: &self.text_layout_cache,
             asset_cache: &self.asset_cache,
             view_stack: Vec::new(),
@@ -173,6 +174,7 @@ pub struct LayoutContext<'a> {
     parents: &'a mut HashMap<usize, usize>,
     view_stack: Vec<usize>,
     pub font_cache: &'a FontCache,
+    pub font_system: Arc<dyn FontSystem>,
     pub text_layout_cache: &'a TextLayoutCache,
     pub asset_cache: &'a AssetCache,
     pub app: &'a mut MutableAppContext,

gpui/src/text_layout.rs 🔗

@@ -251,6 +251,54 @@ impl Line {
             }
         }
     }
+
+    pub fn paint_wrapped(
+        &self,
+        origin: Vector2F,
+        line_height: f32,
+        boundaries: impl IntoIterator<Item = ShapedBoundary>,
+        cx: &mut PaintContext,
+    ) {
+        let padding_top = (line_height - self.layout.ascent - self.layout.descent) / 2.;
+        let baseline_origin = vec2f(0., padding_top + self.layout.ascent);
+
+        let mut boundaries = boundaries.into_iter().peekable();
+        let mut color_runs = self.color_runs.iter();
+        let mut color_end = 0;
+        let mut color = Color::black();
+
+        let mut glyph_origin = baseline_origin;
+        let mut prev_position = 0.;
+        for run in &self.layout.runs {
+            for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
+                if boundaries.peek().map_or(false, |b| b.glyph_ix == glyph_ix) {
+                    boundaries.next();
+                    glyph_origin = vec2f(0., glyph_origin.y() + line_height);
+                } else {
+                    glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position);
+                }
+                prev_position = glyph.position.x();
+
+                if glyph.index >= color_end {
+                    if let Some(next_run) = color_runs.next() {
+                        color_end += next_run.0 as usize;
+                        color = next_run.1;
+                    } else {
+                        color_end = self.layout.len;
+                        color = Color::black();
+                    }
+                }
+
+                cx.scene.push_glyph(scene::Glyph {
+                    font_id: run.font_id,
+                    font_size: self.layout.font_size,
+                    id: glyph.id,
+                    origin: origin + glyph_origin,
+                    color,
+                });
+            }
+        }
+    }
 }
 
 impl Run {