Draw tabs with svg icons in editor code only

Kirill Bulatov created

Change summary

crates/editor/src/element.rs           | 197 +++++++++++++++++++++++----
crates/editor/src/scroll/autoscroll.rs |  11 
crates/gpui/src/elements/text.rs       |  49 +-----
crates/gpui/src/text_layout.rs         |  33 ----
4 files changed, 186 insertions(+), 104 deletions(-)

Detailed changes

crates/editor/src/element.rs 🔗

@@ -21,7 +21,7 @@ use git::diff::DiffHunkStatus;
 use gpui::{
     color::Color,
     elements::*,
-    fonts::{HighlightStyle, Underline},
+    fonts::{HighlightStyle, TextStyle, Underline},
     geometry::{
         rect::RectF,
         vector::{vec2f, Vector2F},
@@ -29,17 +29,18 @@ use gpui::{
     },
     json::{self, ToJson},
     platform::{CursorStyle, Modifiers, MouseButton, MouseButtonEvent, MouseMovedEvent},
-    text_layout::{self, Line, RunStyle, TextLayoutCache},
-    AnyElement, Axis, Border, CursorRegion, Element, EventContext, LayoutContext, MouseRegion,
-    Quad, SceneBuilder, SizeConstraint, ViewContext, WindowContext,
+    text_layout::{self, Invisible, Line, RunStyle, TextLayoutCache},
+    AnyElement, Axis, Border, CursorRegion, Element, EventContext, FontCache, LayoutContext,
+    MouseRegion, Quad, SceneBuilder, SizeConstraint, ViewContext, WindowContext,
 };
 use itertools::Itertools;
 use json::json;
 use language::{Bias, CursorShape, DiagnosticSeverity, OffsetUtf16, Selection};
 use project::ProjectPath;
-use settings::{GitGutter, Settings};
+use settings::{GitGutter, Settings, ShowInvisibles};
 use smallvec::SmallVec;
 use std::{
+    borrow::Cow,
     cmp::{self, Ordering},
     fmt::Write,
     iter,
@@ -808,7 +809,8 @@ impl EditorElement {
                         .contains(&cursor_position.row())
                     {
                         let cursor_row_layout = &layout.position_map.line_layouts
-                            [(cursor_position.row() - start_row) as usize];
+                            [(cursor_position.row() - start_row) as usize]
+                            .line;
                         let cursor_column = cursor_position.column() as usize;
 
                         let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
@@ -863,9 +865,9 @@ impl EditorElement {
 
         if let Some(visible_text_bounds) = bounds.intersection(visible_bounds) {
             // Draw glyphs
-            for (ix, line) in layout.position_map.line_layouts.iter().enumerate() {
+            for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() {
                 let row = start_row + ix as u32;
-                line.paint(
+                line_with_invisibles.line.paint(
                     scene,
                     content_origin
                         + vec2f(
@@ -876,6 +878,53 @@ impl EditorElement {
                     layout.position_map.line_height,
                     cx,
                 );
+
+                let settings = cx.global::<Settings>();
+                match settings
+                    .editor_overrides
+                    .show_invisibles
+                    .or(settings.editor_defaults.show_invisibles)
+                    .unwrap_or_default()
+                {
+                    ShowInvisibles::None => {}
+                    ShowInvisibles::All => {
+                        for invisible in &line_with_invisibles.invisibles {
+                            match invisible {
+                                Invisible::Tab { line_start_offset } => {
+                                    // TODO kb cache, deduplicate
+                                    let x_offset =
+                                        line_with_invisibles.line.x_for_index(*line_start_offset);
+                                    let font_size = line_with_invisibles.line.font_size();
+                                    let max_size = vec2f(font_size, font_size);
+                                    let origin = content_origin
+                                        + vec2f(
+                                            -scroll_left + x_offset,
+                                            row as f32 * layout.position_map.line_height
+                                                - scroll_top,
+                                        );
+
+                                    let mut test_svg = Svg::new("icons/arrow_right_16.svg")
+                                        .with_color(Color::red());
+                                    let (_, mut layout_state) = test_svg.layout(
+                                        SizeConstraint::new(origin, max_size),
+                                        editor,
+                                        cx,
+                                    );
+                                    test_svg.paint(
+                                        scene,
+                                        RectF::new(origin, max_size),
+                                        visible_bounds,
+                                        &mut layout_state,
+                                        editor,
+                                        cx,
+                                    );
+                                }
+                                // TODO kb draw whitespaces too
+                                Invisible::Whitespace { .. } => {}
+                            }
+                        }
+                    }
+                }
             }
         }
 
@@ -888,7 +937,7 @@ impl EditorElement {
         if let Some((position, context_menu)) = layout.context_menu.as_mut() {
             scene.push_stacking_context(None, None);
             let cursor_row_layout =
-                &layout.position_map.line_layouts[(position.row() - start_row) as usize];
+                &layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
             let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
             let y = (position.row() + 1) as f32 * layout.position_map.line_height - scroll_top;
             let mut list_origin = content_origin + vec2f(x, y);
@@ -921,7 +970,7 @@ impl EditorElement {
 
             // This is safe because we check on layout whether the required row is available
             let hovered_row_layout =
-                &layout.position_map.line_layouts[(position.row() - start_row) as usize];
+                &layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
 
             // Minimum required size: Take the first popover, and add 1.5 times the minimum popover
             // height. This is the size we will use to decide whether to render popovers above or below
@@ -1118,7 +1167,7 @@ impl EditorElement {
                     .into_iter()
                     .map(|row| {
                         let line_layout =
-                            &layout.position_map.line_layouts[(row - start_row) as usize];
+                            &layout.position_map.line_layouts[(row - start_row) as usize].line;
                         HighlightedRangeLine {
                             start_x: if row == range.start.row() {
                                 content_origin.x()
@@ -1282,7 +1331,7 @@ impl EditorElement {
         rows: Range<u32>,
         snapshot: &EditorSnapshot,
         cx: &ViewContext<Editor>,
-    ) -> Vec<text_layout::Line> {
+    ) -> Vec<LineWithInvisibles> {
         if rows.start >= rows.end {
             return Vec::new();
         }
@@ -1317,6 +1366,10 @@ impl EditorElement {
                         )],
                     )
                 })
+                .map(|line| LineWithInvisibles {
+                    line,
+                    invisibles: Vec::new(),
+                })
                 .collect()
         } else {
             let style = &self.style;
@@ -1366,13 +1419,6 @@ impl EditorElement {
                     }
                 });
 
-            let settings = cx.global::<Settings>();
-            let show_invisibles = settings
-                .editor_overrides
-                .show_invisibles
-                .or(settings.editor_defaults.show_invisibles)
-                .unwrap_or_default()
-                == settings::ShowInvisibles::All;
             layout_highlighted_chunks(
                 chunks,
                 &style.text,
@@ -1380,7 +1426,6 @@ impl EditorElement {
                 cx.font_cache(),
                 MAX_LINE_LEN,
                 rows.len() as usize,
-                show_invisibles,
             )
         }
     }
@@ -1398,7 +1443,7 @@ impl EditorElement {
         text_x: f32,
         line_height: f32,
         style: &EditorStyle,
-        line_layouts: &[text_layout::Line],
+        line_layouts: &[LineWithInvisibles],
         include_root: bool,
         editor: &mut Editor,
         cx: &mut LayoutContext<Editor>,
@@ -1421,6 +1466,7 @@ impl EditorElement {
                     let anchor_x = text_x
                         + if rows.contains(&align_to.row()) {
                             line_layouts[(align_to.row() - rows.start) as usize]
+                                .line
                                 .x_for_index(align_to.column() as usize)
                         } else {
                             layout_line(align_to.row(), snapshot, style, cx.text_layout_cache())
@@ -1599,6 +1645,93 @@ impl EditorElement {
     }
 }
 
+struct HighlightedChunk<'a> {
+    chunk: &'a str,
+    style: Option<HighlightStyle>,
+    is_tab: bool,
+}
+
+pub struct LineWithInvisibles {
+    pub line: Line,
+    invisibles: Vec<Invisible>,
+}
+
+fn layout_highlighted_chunks<'a>(
+    chunks: impl Iterator<Item = HighlightedChunk<'a>>,
+    text_style: &TextStyle,
+    text_layout_cache: &TextLayoutCache,
+    font_cache: &Arc<FontCache>,
+    max_line_len: usize,
+    max_line_count: usize,
+) -> Vec<LineWithInvisibles> {
+    let mut layouts = Vec::with_capacity(max_line_count);
+    let mut line = String::new();
+    let mut invisibles = Vec::new();
+    let mut styles = Vec::new();
+    let mut row = 0;
+    let mut line_exceeded_max_len = false;
+    for highlighted_chunk in chunks.chain([HighlightedChunk {
+        chunk: "\n",
+        style: None,
+        is_tab: false,
+    }]) {
+        for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() {
+            if ix > 0 {
+                layouts.push(LineWithInvisibles {
+                    line: text_layout_cache.layout_str(&line, text_style.font_size, &styles),
+                    invisibles: invisibles.drain(..).collect(),
+                });
+
+                line.clear();
+                styles.clear();
+                row += 1;
+                line_exceeded_max_len = false;
+                if row == max_line_count {
+                    return layouts;
+                }
+            }
+
+            if !line_chunk.is_empty() && !line_exceeded_max_len {
+                let text_style = if let Some(style) = highlighted_chunk.style {
+                    text_style
+                        .clone()
+                        .highlight(style, font_cache)
+                        .map(Cow::Owned)
+                        .unwrap_or_else(|_| Cow::Borrowed(text_style))
+                } else {
+                    Cow::Borrowed(text_style)
+                };
+
+                if line.len() + line_chunk.len() > max_line_len {
+                    let mut chunk_len = max_line_len - line.len();
+                    while !line_chunk.is_char_boundary(chunk_len) {
+                        chunk_len -= 1;
+                    }
+                    line_chunk = &line_chunk[..chunk_len];
+                    line_exceeded_max_len = true;
+                }
+
+                styles.push((
+                    line_chunk.len(),
+                    RunStyle {
+                        font_id: text_style.font_id,
+                        color: text_style.color,
+                        underline: text_style.underline,
+                    },
+                ));
+                if highlighted_chunk.is_tab {
+                    invisibles.push(Invisible::Tab {
+                        line_start_offset: line.len(),
+                    });
+                }
+                line.push_str(line_chunk);
+            }
+        }
+    }
+
+    layouts
+}
+
 impl Element<Editor> for EditorElement {
     type LayoutState = LayoutState;
     type PaintState = ();
@@ -1825,9 +1958,9 @@ impl Element<Editor> for EditorElement {
 
         let mut max_visible_line_width = 0.0;
         let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx);
-        for line in &line_layouts {
-            if line.width() > max_visible_line_width {
-                max_visible_line_width = line.width();
+        for line_with_invisibles in &line_layouts {
+            if line_with_invisibles.line.width() > max_visible_line_width {
+                max_visible_line_width = line_with_invisibles.line.width();
             }
         }
 
@@ -2087,10 +2220,11 @@ impl Element<Editor> for EditorElement {
             return None;
         }
 
-        let line = layout
+        let line = &layout
             .position_map
             .line_layouts
-            .get((range_start.row() - start_row) as usize)?;
+            .get((range_start.row() - start_row) as usize)?
+            .line;
         let range_start_x = line.x_for_index(range_start.column() as usize);
         let range_start_y = range_start.row() as f32 * layout.position_map.line_height;
         Some(RectF::new(
@@ -2149,13 +2283,13 @@ pub struct LayoutState {
     fold_indicators: Vec<Option<AnyElement<Editor>>>,
 }
 
-pub struct PositionMap {
+struct PositionMap {
     size: Vector2F,
     line_height: f32,
     scroll_max: Vector2F,
     em_width: f32,
     em_advance: f32,
-    line_layouts: Vec<text_layout::Line>,
+    line_layouts: Vec<LineWithInvisibles>,
     snapshot: EditorSnapshot,
 }
 
@@ -2177,6 +2311,7 @@ impl PositionMap {
         let (column, x_overshoot) = if let Some(line) = self
             .line_layouts
             .get(row as usize - scroll_position.y() as usize)
+            .map(|line_with_spaces| &line_with_spaces.line)
         {
             if let Some(ix) = line.index_for_x(x) {
                 (ix as u32, 0.0)
@@ -2445,7 +2580,7 @@ impl HighlightedRange {
     }
 }
 
-pub fn position_to_display_point(
+fn position_to_display_point(
     position: Vector2F,
     text_bounds: RectF,
     position_map: &PositionMap,
@@ -2462,7 +2597,7 @@ pub fn position_to_display_point(
     }
 }
 
-pub fn range_to_bounds(
+fn range_to_bounds(
     range: &Range<DisplayPoint>,
     content_origin: Vector2F,
     scroll_left: f32,
@@ -2490,7 +2625,7 @@ pub fn range_to_bounds(
         content_origin.y() + row_range.start as f32 * position_map.line_height - scroll_top;
 
     for (idx, row) in row_range.enumerate() {
-        let line_layout = &position_map.line_layouts[(row - start_row) as usize];
+        let line_layout = &position_map.line_layouts[(row - start_row) as usize].line;
 
         let start_x = if row == range.start.row() {
             content_origin.x() + line_layout.x_for_index(range.start.column() as usize)

crates/editor/src/scroll/autoscroll.rs 🔗

@@ -1,9 +1,9 @@
 use std::cmp;
 
-use gpui::{text_layout, ViewContext};
+use gpui::ViewContext;
 use language::Point;
 
-use crate::{display_map::ToDisplayPoint, Editor, EditorMode};
+use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles};
 
 #[derive(PartialEq, Eq)]
 pub enum Autoscroll {
@@ -172,7 +172,7 @@ impl Editor {
         viewport_width: f32,
         scroll_width: f32,
         max_glyph_width: f32,
-        layouts: &[text_layout::Line],
+        layouts: &[LineWithInvisibles],
         cx: &mut ViewContext<Self>,
     ) -> bool {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
@@ -194,10 +194,13 @@ impl Editor {
                     let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3);
                     target_left = target_left.min(
                         layouts[(head.row() - start_row) as usize]
+                            .line
                             .x_for_index(start_column as usize),
                     );
                     target_right = target_right.max(
-                        layouts[(head.row() - start_row) as usize].x_for_index(end_column as usize)
+                        layouts[(head.row() - start_row) as usize]
+                            .line
+                            .x_for_index(end_column as usize)
                             + max_glyph_width,
                     );
                 }

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

@@ -6,7 +6,7 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     json::{ToJson, Value},
-    text_layout::{Invisible, Line, RunStyle, ShapedBoundary},
+    text_layout::{Line, RunStyle, ShapedBoundary},
     AppContext, Element, FontCache, LayoutContext, SceneBuilder, SizeConstraint, TextLayoutCache,
     View, ViewContext,
 };
@@ -114,11 +114,7 @@ impl<V: View> Element<V> for Text {
             } else {
                 result = None;
             }
-            result.map(|(chunk, style)| HighlightedChunk {
-                chunk,
-                style,
-                is_tab: false,
-            })
+            result
         });
 
         // Perform shaping on these highlighted chunks
@@ -129,7 +125,6 @@ impl<V: View> Element<V> for Text {
             &cx.font_cache,
             usize::MAX,
             self.text.matches('\n').count() + 1,
-            false,
         );
 
         // If line wrapping is enabled, wrap each of the shaped lines.
@@ -342,45 +337,24 @@ impl<V: View> Element<V> for Text {
     }
 }
 
-pub struct HighlightedChunk<'a> {
-    pub chunk: &'a str,
-    pub style: Option<HighlightStyle>,
-    pub is_tab: bool,
-}
-
-impl<'a> HighlightedChunk<'a> {
-    fn plain_str(str_symbols: &'a str) -> Self {
-        Self {
-            chunk: str_symbols,
-            style: None,
-            is_tab: str_symbols == "\t",
-        }
-    }
-}
-
 /// Perform text layout on a series of highlighted chunks of text.
-pub fn layout_highlighted_chunks<'a>(
-    chunks: impl Iterator<Item = HighlightedChunk<'a>>,
+fn layout_highlighted_chunks<'a>(
+    chunks: impl Iterator<Item = (&'a str, Option<HighlightStyle>)>,
     text_style: &TextStyle,
     text_layout_cache: &TextLayoutCache,
     font_cache: &Arc<FontCache>,
     max_line_len: usize,
     max_line_count: usize,
-    show_invisibles: bool,
 ) -> Vec<Line> {
     let mut layouts = Vec::with_capacity(max_line_count);
     let mut line = String::new();
-    let mut invisibles = Vec::new();
     let mut styles = Vec::new();
     let mut row = 0;
     let mut line_exceeded_max_len = false;
-    for highlighted_chunk in chunks.chain(std::iter::once(HighlightedChunk::plain_str("\n"))) {
-        for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() {
+    for (chunk, highlight_style) in chunks.chain([("\n", Default::default())]) {
+        for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
             if ix > 0 {
-                let mut laid_out_line =
-                    text_layout_cache.layout_str(&line, text_style.font_size, &styles);
-                laid_out_line.invisibles.extend(invisibles.drain(..));
-                layouts.push(laid_out_line);
+                layouts.push(text_layout_cache.layout_str(&line, text_style.font_size, &styles));
                 line.clear();
                 styles.clear();
                 row += 1;
@@ -391,7 +365,7 @@ pub fn layout_highlighted_chunks<'a>(
             }
 
             if !line_chunk.is_empty() && !line_exceeded_max_len {
-                let text_style = if let Some(style) = highlighted_chunk.style {
+                let text_style = if let Some(style) = highlight_style {
                     text_style
                         .clone()
                         .highlight(style, font_cache)
@@ -410,6 +384,7 @@ pub fn layout_highlighted_chunks<'a>(
                     line_exceeded_max_len = true;
                 }
 
+                line.push_str(line_chunk);
                 styles.push((
                     line_chunk.len(),
                     RunStyle {
@@ -418,12 +393,6 @@ pub fn layout_highlighted_chunks<'a>(
                         underline: text_style.underline,
                     },
                 ));
-                if show_invisibles && highlighted_chunk.is_tab {
-                    invisibles.push(Invisible::Tab {
-                        range: line.len()..line.len() + line_chunk.len(),
-                    });
-                }
-                line.push_str(line_chunk);
             }
         }
     }

crates/gpui/src/text_layout.rs 🔗

@@ -11,7 +11,6 @@ use crate::{
     window::WindowContext,
     SceneBuilder,
 };
-use itertools::Itertools;
 use ordered_float::OrderedFloat;
 use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
 use smallvec::SmallVec;
@@ -179,7 +178,6 @@ impl<'a> Hash for CacheKeyRef<'a> {
 pub struct Line {
     layout: Arc<LineLayout>,
     style_runs: SmallVec<[StyleRun; 32]>,
-    pub invisibles: SmallVec<[Invisible; 32]>,
 }
 
 #[derive(Debug, Clone, Copy)]
@@ -215,8 +213,8 @@ pub struct Glyph {
 
 #[derive(Debug, Clone)]
 pub enum Invisible {
-    Tab { range: std::ops::Range<usize> },
-    Whitespace { range: std::ops::Range<usize> },
+    Tab { line_start_offset: usize },
+    Whitespace { line_range: std::ops::Range<usize> },
 }
 
 impl Line {
@@ -229,11 +227,7 @@ impl Line {
                 underline: style.underline,
             });
         }
-        Self {
-            layout,
-            style_runs,
-            invisibles: SmallVec::new(),
-        }
+        Self { layout, style_runs }
     }
 
     pub fn runs(&self) -> &[Run] {
@@ -310,16 +304,6 @@ impl Line {
         let mut color = Color::black();
         let mut underline = None;
 
-        let tab_ranges = self
-            .invisibles
-            .iter()
-            .filter_map(|invisible| match invisible {
-                Invisible::Tab { range } => Some(range),
-                Invisible::Whitespace { .. } => None,
-            })
-            .sorted_by(|tab_range_1, tab_range_2| tab_range_1.start.cmp(&tab_range_2.start))
-            .collect::<Vec<_>>();
-
         for run in &self.layout.runs {
             let max_glyph_width = cx
                 .font_cache
@@ -386,19 +370,10 @@ impl Line {
                         origin: glyph_origin,
                     });
                 } else {
-                    let id = if tab_ranges.iter().any(|tab_range| {
-                        tab_range.start <= glyph.index && glyph.index < tab_range.end
-                    }) {
-                        // TODO kb get a proper (cached) glyph
-                        glyph.id + 100
-                    } else {
-                        glyph.id
-                    };
-
                     scene.push_glyph(scene::Glyph {
                         font_id: run.font_id,
                         font_size: self.layout.font_size,
-                        id,
+                        id: glyph.id,
                         origin: glyph_origin,
                         color,
                     });