editor: Fix text selection not visible on text background (#26454)

Smit Barmase created

Closes #25014

Previously, we painted in the order: highlights -> text background ->
text -> etc. This caused text selection to be invisible when the text
had a background.

This PR changes the painting order to: text background -> highlights ->
text -> etc.

Before:


https://github.com/user-attachments/assets/5d9647c4-3ab2-4960-b6b9-e399882a0c50

After:


https://github.com/user-attachments/assets/c699f5b9-4077-45f8-85e5-86c89130eb71

Release Notes:

- Fixed an issue where text selection was not visible on top of a text
background in the editor.

Change summary

crates/editor/src/element.rs        |  42 ++++++
crates/gpui/src/text_system/line.rs | 215 ++++++++++++++++++++++--------
2 files changed, 200 insertions(+), 57 deletions(-)

Detailed changes

crates/editor/src/element.rs 🔗

@@ -4653,6 +4653,7 @@ impl EditorElement {
                 };
                 window.set_cursor_style(cursor_style, &layout.position_map.text_hitbox);
 
+                self.paint_lines_background(layout, window, cx);
                 let invisible_display_ranges = self.paint_highlights(layout, window);
                 self.paint_lines(&invisible_display_ranges, layout, window, cx);
                 self.paint_redactions(layout, window);
@@ -4743,6 +4744,18 @@ impl EditorElement {
         }
     }
 
+    fn paint_lines_background(
+        &mut self,
+        layout: &mut EditorLayout,
+        window: &mut Window,
+        cx: &mut App,
+    ) {
+        for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() {
+            let row = DisplayRow(layout.visible_display_row_range.start.0 + ix as u32);
+            line_with_invisibles.draw_background(layout, row, layout.content_origin, window, cx);
+        }
+    }
+
     fn paint_redactions(&mut self, layout: &EditorLayout, window: &mut Window) {
         if layout.redacted_ranges.is_empty() {
             return;
@@ -6237,6 +6250,35 @@ impl LineWithInvisibles {
         );
     }
 
+    fn draw_background(
+        &self,
+        layout: &EditorLayout,
+        row: DisplayRow,
+        content_origin: gpui::Point<Pixels>,
+        window: &mut Window,
+        cx: &mut App,
+    ) {
+        let line_height = layout.position_map.line_height;
+        let line_y = line_height
+            * (row.as_f32() - layout.position_map.scroll_pixel_position.y / line_height);
+
+        let mut fragment_origin =
+            content_origin + gpui::point(-layout.position_map.scroll_pixel_position.x, line_y);
+
+        for fragment in &self.fragments {
+            match fragment {
+                LineFragment::Text(line) => {
+                    line.paint_background(fragment_origin, line_height, window, cx)
+                        .log_err();
+                    fragment_origin.x += line.width;
+                }
+                LineFragment::Element { size, .. } => {
+                    fragment_origin.x += size.width;
+                }
+            }
+        }
+    }
+
     fn draw_invisibles(
         &self,
         selection_ranges: &[Range<DisplayPoint>],

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

@@ -81,6 +81,29 @@ impl ShapedLine {
 
         Ok(())
     }
+
+    /// Paint the background of the line to the window.
+    pub fn paint_background(
+        &self,
+        origin: Point<Pixels>,
+        line_height: Pixels,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> Result<()> {
+        paint_line_background(
+            origin,
+            &self.layout,
+            line_height,
+            TextAlign::default(),
+            None,
+            &self.decoration_runs,
+            &[],
+            window,
+            cx,
+        )?;
+
+        Ok(())
+    }
 }
 
 /// A line of text that has been shaped, decorated, and wrapped by the text layout system.
@@ -159,7 +182,6 @@ fn paint_line(
         let mut color = black();
         let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
         let mut current_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
-        let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
         let text_system = cx.text_system().clone();
         let mut glyph_origin = point(
             aligned_origin_x(
@@ -182,21 +204,6 @@ fn paint_line(
 
                 if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
                     wraps.next();
-                    if let Some((background_origin, background_color)) = current_background.as_mut()
-                    {
-                        if glyph_origin.x == background_origin.x {
-                            background_origin.x -= max_glyph_size.width.half()
-                        }
-                        window.paint_quad(fill(
-                            Bounds {
-                                origin: *background_origin,
-                                size: size(glyph_origin.x - background_origin.x, line_height),
-                            },
-                            *background_color,
-                        ));
-                        background_origin.x = origin.x;
-                        background_origin.y += line_height;
-                    }
                     if let Some((underline_origin, underline_style)) = current_underline.as_mut() {
                         if glyph_origin.x == underline_origin.x {
                             underline_origin.x -= max_glyph_size.width.half();
@@ -236,7 +243,6 @@ 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;
                 let mut finished_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
                 if glyph.index >= run_end {
@@ -252,18 +258,6 @@ fn paint_line(
                     }
 
                     if let Some(style_run) = style_run {
-                        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, glyph_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();
@@ -305,26 +299,11 @@ fn paint_line(
                         color = style_run.color;
                     } else {
                         run_end = layout.len;
-                        finished_background = current_background.take();
                         finished_underline = current_underline.take();
                         finished_strikethrough = current_strikethrough.take();
                     }
                 }
 
-                if let Some((mut background_origin, background_color)) = finished_background {
-                    let mut width = glyph_origin.x - background_origin.x;
-                    if background_origin.x == glyph_origin.x {
-                        background_origin.x -= max_glyph_size.width.half();
-                    };
-                    window.paint_quad(fill(
-                        Bounds {
-                            origin: background_origin,
-                            size: size(width, line_height),
-                        },
-                        background_color,
-                    ));
-                }
-
                 if let Some((mut underline_origin, underline_style)) = finished_underline {
                     if underline_origin.x == glyph_origin.x {
                         underline_origin.x -= max_glyph_size.width.half();
@@ -383,19 +362,6 @@ fn paint_line(
             last_line_end_x -= glyph.position.x;
         }
 
-        if let Some((mut background_origin, background_color)) = current_background.take() {
-            if last_line_end_x == background_origin.x {
-                background_origin.x -= max_glyph_size.width.half()
-            };
-            window.paint_quad(fill(
-                Bounds {
-                    origin: background_origin,
-                    size: size(last_line_end_x - background_origin.x, line_height),
-                },
-                background_color,
-            ));
-        }
-
         if let Some((mut underline_start, underline_style)) = current_underline.take() {
             if last_line_end_x == underline_start.x {
                 underline_start.x -= max_glyph_size.width.half()
@@ -422,6 +388,141 @@ fn paint_line(
     })
 }
 
+fn paint_line_background(
+    origin: Point<Pixels>,
+    layout: &LineLayout,
+    line_height: Pixels,
+    align: TextAlign,
+    align_width: Option<Pixels>,
+    decoration_runs: &[DecorationRun],
+    wrap_boundaries: &[WrapBoundary],
+    window: &mut Window,
+    cx: &mut App,
+) -> Result<()> {
+    let line_bounds = Bounds::new(
+        origin,
+        size(
+            layout.width,
+            line_height * (wrap_boundaries.len() as f32 + 1.),
+        ),
+    );
+    window.paint_layer(line_bounds, |window| {
+        let mut decoration_runs = decoration_runs.iter();
+        let mut wraps = wrap_boundaries.iter().peekable();
+        let mut run_end = 0;
+        let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
+        let text_system = cx.text_system().clone();
+        let mut glyph_origin = point(
+            aligned_origin_x(
+                origin,
+                align_width.unwrap_or(layout.width),
+                px(0.0),
+                &align,
+                layout,
+                wraps.peek(),
+            ),
+            origin.y,
+        );
+        let mut prev_glyph_position = Point::default();
+        let mut max_glyph_size = size(px(0.), px(0.));
+        for (run_ix, run) in layout.runs.iter().enumerate() {
+            max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
+
+            for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
+                glyph_origin.x += glyph.position.x - prev_glyph_position.x;
+
+                if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
+                    wraps.next();
+                    if let Some((background_origin, background_color)) = current_background.as_mut()
+                    {
+                        if glyph_origin.x == background_origin.x {
+                            background_origin.x -= max_glyph_size.width.half()
+                        }
+                        window.paint_quad(fill(
+                            Bounds {
+                                origin: *background_origin,
+                                size: size(glyph_origin.x - background_origin.x, line_height),
+                            },
+                            *background_color,
+                        ));
+                        background_origin.x = origin.x;
+                        background_origin.y += line_height;
+                    }
+                }
+                prev_glyph_position = glyph.position;
+
+                let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
+                if glyph.index >= run_end {
+                    let mut style_run = decoration_runs.next();
+
+                    // ignore style runs that apply to a partial glyph
+                    while let Some(run) = style_run {
+                        if glyph.index < run_end + (run.len as usize) {
+                            break;
+                        }
+                        run_end += run.len as usize;
+                        style_run = decoration_runs.next();
+                    }
+
+                    if let Some(style_run) = style_run {
+                        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, glyph_origin.y),
+                                run_background,
+                            ));
+                        }
+                        run_end += style_run.len as usize;
+                    } else {
+                        run_end = layout.len;
+                        finished_background = current_background.take();
+                    }
+                }
+
+                if let Some((mut background_origin, background_color)) = finished_background {
+                    let mut width = glyph_origin.x - background_origin.x;
+                    if background_origin.x == glyph_origin.x {
+                        background_origin.x -= max_glyph_size.width.half();
+                    };
+                    window.paint_quad(fill(
+                        Bounds {
+                            origin: background_origin,
+                            size: size(width, line_height),
+                        },
+                        background_color,
+                    ));
+                }
+            }
+        }
+
+        let mut last_line_end_x = origin.x + layout.width;
+        if let Some(boundary) = wrap_boundaries.last() {
+            let run = &layout.runs[boundary.run_ix];
+            let glyph = &run.glyphs[boundary.glyph_ix];
+            last_line_end_x -= glyph.position.x;
+        }
+
+        if let Some((mut background_origin, background_color)) = current_background.take() {
+            if last_line_end_x == background_origin.x {
+                background_origin.x -= max_glyph_size.width.half()
+            };
+            window.paint_quad(fill(
+                Bounds {
+                    origin: background_origin,
+                    size: size(last_line_end_x - background_origin.x, line_height),
+                },
+                background_color,
+            ));
+        }
+
+        Ok(())
+    })
+}
+
 fn aligned_origin_x(
     origin: Point<Pixels>,
     align_width: Pixels,