Allow providing a background color in a `TextRun`

Antonio Scandurra created

Change summary

crates/gpui2/src/text_system.rs       | 11 ++++
crates/gpui2/src/text_system/line.rs  | 64 ++++++++++++++++++++++++++--
crates/gpui2/src/window.rs            |  3 
crates/storybook2/src/stories/text.rs | 21 ++++++++-
4 files changed, 87 insertions(+), 12 deletions(-)

Detailed changes

crates/gpui2/src/text_system.rs 🔗

@@ -196,7 +196,10 @@ impl TextSystem {
         let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
         for run in runs {
             if let Some(last_run) = decoration_runs.last_mut() {
-                if last_run.color == run.color && last_run.underline == run.underline {
+                if last_run.color == run.color
+                    && last_run.underline == run.underline
+                    && last_run.background_color == run.background_color
+                {
                     last_run.len += run.len as u32;
                     continue;
                 }
@@ -204,6 +207,7 @@ impl TextSystem {
             decoration_runs.push(DecorationRun {
                 len: run.len as u32,
                 color: run.color,
+                background_color: run.background_color,
                 underline: run.underline.clone(),
             });
         }
@@ -254,13 +258,16 @@ impl TextSystem {
                 }
 
                 if decoration_runs.last().map_or(false, |last_run| {
-                    last_run.color == run.color && last_run.underline == run.underline
+                    last_run.color == run.color
+                        && last_run.underline == run.underline
+                        && last_run.background_color == run.background_color
                 }) {
                     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.color,
+                        background_color: run.background_color,
                         underline: run.underline.clone(),
                     });
                 }

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

@@ -1,6 +1,7 @@
 use crate::{
-    black, point, px, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString,
-    UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
+    black, point, px, size, transparent_black, BorrowWindow, Bounds, Corners, Edges, Hsla,
+    LineLayout, Pixels, Point, Result, SharedString, UnderlineStyle, WindowContext, WrapBoundary,
+    WrappedLineLayout,
 };
 use derive_more::{Deref, DerefMut};
 use smallvec::SmallVec;
@@ -10,6 +11,7 @@ use std::sync::Arc;
 pub struct DecorationRun {
     pub len: u32,
     pub color: Hsla,
+    pub background_color: Option<Hsla>,
     pub underline: Option<UnderlineStyle>,
 }
 
@@ -97,6 +99,7 @@ fn paint_line(
     let mut run_end = 0;
     let mut color = black();
     let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
+    let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
     let text_system = cx.text_system().clone();
     let mut glyph_origin = origin;
     let mut prev_glyph_position = Point::default();
@@ -110,12 +113,24 @@ fn paint_line(
 
             if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
                 wraps.next();
+                if let Some((background_origin, background_color)) = current_background.take() {
+                    cx.paint_quad(
+                        Bounds {
+                            origin: background_origin,
+                            size: size(glyph_origin.x - background_origin.x, line_height),
+                        },
+                        Corners::default(),
+                        background_color,
+                        Edges::default(),
+                        transparent_black(),
+                    );
+                }
                 if let Some((underline_origin, underline_style)) = current_underline.take() {
                     cx.paint_underline(
                         underline_origin,
                         glyph_origin.x - underline_origin.x,
                         &underline_style,
-                    )?;
+                    );
                 }
 
                 glyph_origin.x = origin.x;
@@ -123,9 +138,20 @@ fn paint_line(
             }
             prev_glyph_position = glyph.position;
 
+            let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
             let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
             if glyph.index >= run_end {
                 if let Some(style_run) = decoration_runs.next() {
+                    if let Some((_, background_color)) = &mut current_background {
+                        if style_run.background_color.as_ref() != Some(background_color) {
+                            finished_background = current_background.take();
+                        }
+                    }
+                    if let Some(run_background) = style_run.background_color {
+                        current_background
+                            .get_or_insert((point(glyph_origin.x, origin.y), run_background));
+                    }
+
                     if let Some((_, underline_style)) = &mut current_underline {
                         if style_run.underline.as_ref() != Some(underline_style) {
                             finished_underline = current_underline.take();
@@ -149,16 +175,30 @@ fn paint_line(
                     color = style_run.color;
                 } else {
                     run_end = layout.len;
+                    finished_background = current_background.take();
                     finished_underline = current_underline.take();
                 }
             }
 
+            if let Some((background_origin, background_color)) = finished_background {
+                cx.paint_quad(
+                    Bounds {
+                        origin: background_origin,
+                        size: size(glyph_origin.x - background_origin.x, line_height),
+                    },
+                    Corners::default(),
+                    background_color,
+                    Edges::default(),
+                    transparent_black(),
+                );
+            }
+
             if let Some((underline_origin, underline_style)) = finished_underline {
                 cx.paint_underline(
                     underline_origin,
                     glyph_origin.x - underline_origin.x,
                     &underline_style,
-                )?;
+                );
             }
 
             let max_glyph_bounds = Bounds {
@@ -188,13 +228,27 @@ fn paint_line(
         }
     }
 
+    if let Some((background_origin, background_color)) = current_background.take() {
+        let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width);
+        cx.paint_quad(
+            Bounds {
+                origin: background_origin,
+                size: size(line_end_x - background_origin.x, line_height),
+            },
+            Corners::default(),
+            background_color,
+            Edges::default(),
+            transparent_black(),
+        );
+    }
+
     if let Some((underline_start, underline_style)) = current_underline.take() {
         let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width);
         cx.paint_underline(
             underline_start,
             line_end_x - underline_start.x,
             &underline_style,
-        )?;
+        );
     }
 
     Ok(())

crates/gpui2/src/window.rs 🔗

@@ -881,7 +881,7 @@ impl<'a> WindowContext<'a> {
         origin: Point<Pixels>,
         width: Pixels,
         style: &UnderlineStyle,
-    ) -> Result<()> {
+    ) {
         let scale_factor = self.scale_factor();
         let height = if style.wavy {
             style.thickness * 3.
@@ -905,7 +905,6 @@ impl<'a> WindowContext<'a> {
                 wavy: style.wavy,
             },
         );
-        Ok(())
     }
 
     /// Paint a monochrome (non-emoji) glyph into the scene for the current frame at the current z-index.

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

@@ -1,6 +1,6 @@
 use gpui::{
-    blue, div, red, white, Div, InteractiveText, ParentElement, Render, Styled, StyledText, View,
-    VisualContext, WindowContext,
+    blue, div, green, red, white, Div, InteractiveText, ParentElement, Render, Styled, StyledText,
+    TextRun, View, VisualContext, WindowContext,
 };
 use ui::v_stack;
 
@@ -56,6 +56,21 @@ impl Render for TextStory {
                 "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.",
-            ))).child(InteractiveText::new("interactive", StyledText::new("Hello world, how is it going?")).on_click(vec![2..4], |event, cx| {dbg!(event);}))
+            ))).child(
+                InteractiveText::new(
+                    "interactive",
+                    StyledText::new("Hello world, how is it going?").with_runs(vec![
+                        cx.text_style().to_run(6),
+                        TextRun {
+                            background_color: Some(green()),
+                            ..cx.text_style().to_run(5)
+                        },
+                        cx.text_style().to_run(18),
+                    ]),
+                )
+                .on_click(vec![2..4, 1..3, 7..9], |range_ix, cx| {
+                    println!("Clicked range {range_ix}");
+                })
+            )
     }
 }