Checkpoint

Nathan Sobo created

Change summary

crates/gpui/src/fonts.rs                          |   5 
crates/gpui3/src/elements/text.rs                 |  43 +++---
crates/gpui3/src/geometry.rs                      |   8 +
crates/gpui3/src/text_system.rs                   | 102 +++++++++-------
crates/gpui3/src/text_system/line.rs              |  81 ++++++-------
crates/gpui3/src/text_system/line_wrapper.rs      |   3 
crates/gpui3/src/text_system/text_layout_cache.rs |   4 
7 files changed, 130 insertions(+), 116 deletions(-)

Detailed changes

crates/gpui/src/fonts.rs 🔗

@@ -155,8 +155,9 @@ impl Refineable for TextStyleRefinement {
         }
     }
 
-    fn refined(self, refinement: Self::Refinement) -> Self {
-        todo!()
+    fn refined(mut self, refinement: Self::Refinement) -> Self {
+        self.refine(&refinement);
+        self
     }
 }
 

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

@@ -1,7 +1,8 @@
 use crate::{
-    AnyElement, Bounds, Element, IntoAnyElement, LayoutId, Line, Pixels, Size, ViewContext,
+    size, AnyElement, Bounds, Element, IntoAnyElement, LayoutId, Line, Pixels, Size, ViewContext,
 };
 use parking_lot::Mutex;
+use smallvec::SmallVec;
 use std::{marker::PhantomData, sync::Arc};
 use util::{arc_cow::ArcCow, ResultExt};
 
@@ -75,7 +76,7 @@ impl<S: 'static + Send + Sync> Element for Text<S> {
         let layout_id = cx.request_measured_layout(Default::default(), rem_size, {
             let element_state = element_state.clone();
             move |known_dimensions, _| {
-                let Some(line_layout) = text_system
+                let Some(lines) = text_system
                     .layout_text(
                         text.as_ref(),
                         font_size,
@@ -88,14 +89,13 @@ impl<S: 'static + Send + Sync> Element for Text<S> {
                 };
 
                 let size = Size {
-                    width: line_layout.width(),
-                    height: line_height,
+                    width: lines.iter().map(|line| line.width()).max().unwrap(),
+                    height: line_height * lines.len(),
                 };
 
-                element_state.lock().replace(TextElementState {
-                    line: Arc::new(line_layout),
-                    line_height,
-                });
+                element_state
+                    .lock()
+                    .replace(TextElementState { lines, line_height });
 
                 size
             }
@@ -111,22 +111,25 @@ impl<S: 'static + Send + Sync> Element for Text<S> {
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<S>,
     ) {
-        let line;
-        let line_height;
-        {
-            let element_state = element_state.lock();
-            let element_state = element_state
-                .as_ref()
-                .expect("measurement has not been performed");
-            line = element_state.line.clone();
-            line_height = element_state.line_height;
+        let element_state = element_state.lock();
+        let element_state = element_state
+            .as_ref()
+            .expect("measurement has not been performed");
+        let line_height = element_state.line_height;
+        let mut line_origin = bounds.origin;
+        for line in &element_state.lines {
+            let line_bounds = Bounds {
+                origin: line_origin,
+                size: size(line.width(), line_height),
+            };
+            line.paint(line_bounds, line_bounds, line_height, cx)
+                .log_err();
+            line_origin.y += line_height;
         }
-
-        line.paint(bounds, bounds, line_height, cx).log_err();
     }
 }
 
 pub struct TextElementState {
-    line: Arc<Line>,
+    lines: SmallVec<[Arc<Line>; 1]>,
     line_height: Pixels,
 }

crates/gpui3/src/geometry.rs 🔗

@@ -656,6 +656,14 @@ impl Mul<f32> for Pixels {
     }
 }
 
+impl Mul<usize> for Pixels {
+    type Output = Pixels;
+
+    fn mul(self, other: usize) -> Pixels {
+        Pixels(self.0 * other as f32)
+    }
+}
+
 impl Mul<Pixels> for f32 {
     type Output = Pixels;
 

crates/gpui3/src/text_system.rs 🔗

@@ -18,6 +18,7 @@ use collections::HashMap;
 use core::fmt;
 use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
 use std::{
+    cmp,
     fmt::{Debug, Display, Formatter},
     hash::{Hash, Hasher},
     ops::{Deref, DerefMut},
@@ -150,63 +151,72 @@ impl TextSystem {
         font_size: Pixels,
         runs: &[(usize, RunStyle)],
         wrap_width: Option<Pixels>,
-    ) -> Result<SmallVec<[Line; 1]>> {
+    ) -> Result<SmallVec<[Arc<Line>; 1]>> {
+        let mut runs = runs
+            .iter()
+            .map(|(run_len, style)| (*run_len, style))
+            .peekable();
         let mut font_runs: Vec<(usize, FontId)> =
             self.font_runs_pool.lock().pop().unwrap_or_default();
 
-        let mut last_font: Option<&Font> = None;
-        for (len, style) in runs {
-            if let Some(last_font) = last_font.as_ref() {
-                if **last_font == style.font {
-                    font_runs.last_mut().unwrap().0 += len;
-                    continue;
-                }
-            }
-            last_font = Some(&style.font);
-            font_runs.push((*len, self.font_id(&style.font)?));
-        }
+        let mut lines = SmallVec::new();
+        let mut line_start = 0;
+        for line in text.split('\n') {
+            let line_end = line_start + line.len();
 
-        let mut layouts = SmallVec::new();
-        let mut start = 0;
-        let mut run_start = 0;
-        for line in text.lines() {
-            let end = start + line.len();
-            let mut run_end = run_start;
-            let mut line_length = 0;
-            for (len, _) in font_runs[run_start..].iter() {
-                line_length += len;
-                if *len >= line_length {
+            let mut last_font: Option<&Font> = None;
+            let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
+            let mut run_start = line_start;
+            while run_start < line_end {
+                let Some((run_len, run_style)) = runs.peek_mut() else {
                     break;
+                };
+
+                let run_len_within_line = cmp::min(line_end, run_start + *run_len) - run_start;
+
+                if last_font == Some(&run_style.font) {
+                    font_runs.last_mut().unwrap().0 += run_len_within_line;
+                } else {
+                    last_font = Some(&run_style.font);
+                    font_runs.push((
+                        run_len_within_line,
+                        self.platform_text_system.font_id(&run_style.font)?,
+                    ));
                 }
-                run_end += 1;
-            }
-            // If a run lands in the middle of a line, create an additional run for the remaining characters.
-            if line_length < end - start {
-                // Create a run for the part that fits this line.
-                let partial_run = font_runs[run_end];
-                partial_run.0 = line_length;
-                layouts.push(self.text_layout_cache.layout_line(
-                    &text[start..start + line_length],
-                    font_size,
-                    &font_runs[run_start..=run_end],
-                ));
-                // Update the original run to only include the part that does not fit this line.
-                font_runs[run_end].0 -= line_length;
-            } else {
-                layouts.push(self.text_layout_cache.layout_line(
-                    &text[start..end],
-                    font_size,
-                    &font_runs[run_start..run_end],
-                ));
-                run_start = run_end;
+
+                if decoration_runs.last().map_or(false, |last_run| {
+                    last_run.color == run_style.color && last_run.underline == run_style.underline
+                }) {
+                    decoration_runs.last_mut().unwrap().len += run_len_within_line as u32;
+                } else {
+                    decoration_runs.push(DecorationRun {
+                        len: run_len_within_line as u32,
+                        color: run_style.color,
+                        underline: run_style.underline.clone(),
+                    });
+                }
+
+                if run_len_within_line == *run_len {
+                    runs.next();
+                } else {
+                    // Preserve the remainder of the run for the next line
+                    *run_len -= run_len_within_line;
+                }
+                run_start += run_len_within_line;
             }
-            start = end + 1;
+
+            let layout = self
+                .text_layout_cache
+                .layout_line(line, font_size, &font_runs);
+            lines.push(Arc::new(Line::new(layout, decoration_runs)));
+
+            line_start = line_end + 1; // Skip `\n` character.
+            font_runs.clear();
         }
 
-        font_runs.clear();
         self.font_runs_pool.lock().push(font_runs);
 
-        Ok(layouts)
+        Ok(lines)
     }
 
     pub fn end_frame(&self) {

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

@@ -1,6 +1,6 @@
 use crate::{
-    black, point, px, Bounds, FontId, Hsla, LineLayout, Pixels, Point, RunStyle, ShapedBoundary,
-    ShapedRun, UnderlineStyle, WindowContext,
+    black, point, px, Bounds, FontId, Hsla, LineLayout, Pixels, Point, ShapedBoundary, ShapedRun,
+    UnderlineStyle, WindowContext,
 };
 use anyhow::Result;
 use smallvec::SmallVec;
@@ -9,27 +9,22 @@ use std::sync::Arc;
 #[derive(Default, Debug, Clone)]
 pub struct Line {
     layout: Arc<LineLayout>,
-    style_runs: SmallVec<[StyleRun; 32]>,
+    decoration_runs: SmallVec<[DecorationRun; 32]>,
 }
 
 #[derive(Debug, Clone)]
-struct StyleRun {
-    len: u32,
-    color: Hsla,
-    underline: UnderlineStyle,
+pub struct DecorationRun {
+    pub len: u32,
+    pub color: Hsla,
+    pub underline: Option<UnderlineStyle>,
 }
 
 impl Line {
-    pub fn new(layout: Arc<LineLayout>, runs: &[(usize, RunStyle)]) -> Self {
-        let mut style_runs = SmallVec::new();
-        for (len, style) in runs {
-            style_runs.push(StyleRun {
-                len: *len as u32,
-                color: style.color,
-                underline: style.underline.clone().unwrap_or_default(),
-            });
+    pub fn new(layout: Arc<LineLayout>, decoration_runs: SmallVec<[DecorationRun; 32]>) -> Self {
+        Self {
+            layout,
+            decoration_runs,
         }
-        Self { layout, style_runs }
     }
 
     pub fn runs(&self) -> &[ShapedRun] {
@@ -101,10 +96,10 @@ impl Line {
         let padding_top = (line_height - self.layout.ascent - self.layout.descent) / 2.;
         let baseline_offset = point(px(0.), padding_top + self.layout.ascent);
 
-        let mut style_runs = self.style_runs.iter();
+        let mut style_runs = self.decoration_runs.iter();
         let mut run_end = 0;
         let mut color = black();
-        let mut underline = None;
+        let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
         let text_system = cx.text_system().clone();
 
         for run in &self.layout.runs {
@@ -122,23 +117,21 @@ impl Line {
                 let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
                 if glyph.index >= run_end {
                     if let Some(style_run) = style_runs.next() {
-                        if let Some((_, underline_style)) = &mut underline {
-                            if style_run.underline != *underline_style {
-                                finished_underline = underline.take();
+                        if let Some((_, underline_style)) = &mut current_underline {
+                            if style_run.underline.as_ref() != Some(underline_style) {
+                                finished_underline = current_underline.take();
                             }
                         }
-                        if style_run.underline.thickness > px(0.) {
-                            underline.get_or_insert((
+                        if let Some(run_underline) = style_run.underline.as_ref() {
+                            current_underline.get_or_insert((
                                 point(
                                     glyph_origin.x,
                                     origin.y + baseline_offset.y + (self.layout.descent * 0.618),
                                 ),
                                 UnderlineStyle {
-                                    color: Some(
-                                        style_run.underline.color.unwrap_or(style_run.color),
-                                    ),
-                                    thickness: style_run.underline.thickness,
-                                    wavy: style_run.underline.wavy,
+                                    color: Some(run_underline.color.unwrap_or(style_run.color)),
+                                    thickness: run_underline.thickness,
+                                    wavy: run_underline.wavy,
                                 },
                             ));
                         }
@@ -147,7 +140,7 @@ impl Line {
                         color = style_run.color;
                     } else {
                         run_end = self.layout.len;
-                        finished_underline = underline.take();
+                        finished_underline = current_underline.take();
                     }
                 }
 
@@ -177,7 +170,7 @@ impl Line {
             }
         }
 
-        if let Some((underline_start, underline_style)) = underline.take() {
+        if let Some((underline_start, underline_style)) = current_underline.take() {
             let line_end_x = origin.x + self.layout.width;
             cx.paint_underline(
                 underline_start,
@@ -201,10 +194,10 @@ impl Line {
         let baseline_offset = point(px(0.), padding_top + self.layout.ascent);
 
         let mut boundaries = boundaries.into_iter().peekable();
-        let mut color_runs = self.style_runs.iter();
+        let mut color_runs = self.decoration_runs.iter();
         let mut style_run_end = 0;
         let mut _color = black(); // todo!
-        let mut underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
+        let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
 
         let mut glyph_origin = origin;
         let mut prev_position = px(0.);
@@ -217,7 +210,7 @@ impl Line {
                     .map_or(false, |b| b.run_ix == run_ix && b.glyph_ix == glyph_ix)
                 {
                     boundaries.next();
-                    if let Some((underline_origin, underline_style)) = underline.take() {
+                    if let Some((underline_origin, underline_style)) = current_underline.take() {
                         cx.paint_underline(
                             underline_origin,
                             glyph_origin.x - underline_origin.x,
@@ -234,31 +227,29 @@ impl Line {
                     if let Some(style_run) = color_runs.next() {
                         style_run_end += style_run.len as usize;
                         _color = style_run.color;
-                        if let Some((_, underline_style)) = &mut underline {
-                            if style_run.underline != *underline_style {
-                                finished_underline = underline.take();
+                        if let Some((_, underline_style)) = &mut current_underline {
+                            if style_run.underline.as_ref() != Some(underline_style) {
+                                finished_underline = current_underline.take();
                             }
                         }
-                        if style_run.underline.thickness > px(0.) {
-                            underline.get_or_insert((
+                        if let Some(underline_style) = style_run.underline.as_ref() {
+                            current_underline.get_or_insert((
                                 glyph_origin
                                     + point(
                                         px(0.),
                                         baseline_offset.y + (self.layout.descent * 0.618),
                                     ),
                                 UnderlineStyle {
-                                    color: Some(
-                                        style_run.underline.color.unwrap_or(style_run.color),
-                                    ),
-                                    thickness: style_run.underline.thickness,
-                                    wavy: style_run.underline.wavy,
+                                    color: Some(underline_style.color.unwrap_or(style_run.color)),
+                                    thickness: underline_style.thickness,
+                                    wavy: underline_style.wavy,
                                 },
                             ));
                         }
                     } else {
                         style_run_end = self.layout.len;
                         _color = black();
-                        finished_underline = underline.take();
+                        finished_underline = current_underline.take();
                     }
                 }
 
@@ -298,7 +289,7 @@ impl Line {
             }
         }
 
-        if let Some((underline_origin, underline_style)) = underline.take() {
+        if let Some((underline_origin, underline_style)) = current_underline.take() {
             let line_end_x = glyph_origin.x + self.layout.width - prev_position;
             cx.paint_underline(
                 underline_origin,

crates/gpui3/src/text_system/line_wrapper.rs 🔗

@@ -286,7 +286,7 @@ mod tests {
             };
 
             let text = "aa bbb cccc ddddd eeee";
-            let line = text_system
+            let lines = text_system
                 .layout_text(
                     text,
                     px(16.),
@@ -300,6 +300,7 @@ mod tests {
                     None,
                 )
                 .unwrap();
+            let line = &lines[0];
 
             let mut wrapper = LineWrapper::new(
                 text_system.font_id(&normal.font).unwrap(),

crates/gpui3/src/text_system/text_layout_cache.rs 🔗

@@ -15,11 +15,11 @@ pub(crate) struct TextLayoutCache {
 }
 
 impl TextLayoutCache {
-    pub fn new(fonts: Arc<dyn PlatformTextSystem>) -> Self {
+    pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
         Self {
             prev_frame: Mutex::new(HashMap::new()),
             curr_frame: RwLock::new(HashMap::new()),
-            platform_text_system: fonts,
+            platform_text_system,
         }
     }