Merge pull request #576 from zed-industries/cursor-shape

Keith Simmons and Max Brunsfeld created

Add support for rendering the cursor as a Block and Underscore

Co-authored-by: Max Brunsfeld <max@zed.dev>

Change summary

crates/editor/src/editor.rs    |  9 ++
crates/editor/src/element.rs   | 98 ++++++++++++++++++++++++++++++++---
crates/gpui/src/text_layout.rs | 20 ++++++
3 files changed, 114 insertions(+), 13 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -450,6 +450,7 @@ pub struct Editor {
     document_highlights_task: Option<Task<()>>,
     pending_rename: Option<RenameState>,
     searchable: bool,
+    cursor_shape: CursorShape,
 }
 
 pub struct EditorSnapshot {
@@ -930,6 +931,7 @@ impl Editor {
             document_highlights_task: Default::default(),
             pending_rename: Default::default(),
             searchable: true,
+            cursor_shape: Default::default(),
         };
         this.end_selection(cx);
         this
@@ -1021,6 +1023,11 @@ impl Editor {
         cx.notify();
     }
 
+    pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext<Self>) {
+        self.cursor_shape = cursor_shape;
+        cx.notify();
+    }
+
     pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor)
@@ -5584,7 +5591,7 @@ impl View for Editor {
         self.display_map.update(cx, |map, cx| {
             map.set_font(style.text.font_id, style.text.font_size, cx)
         });
-        EditorElement::new(self.handle.clone(), style.clone()).boxed()
+        EditorElement::new(self.handle.clone(), style.clone(), self.cursor_shape).boxed()
     }
 
     fn ui_name() -> &'static str {

crates/editor/src/element.rs 🔗

@@ -16,7 +16,7 @@ use gpui::{
         PathBuilder,
     },
     json::{self, ToJson},
-    text_layout::{self, RunStyle, TextLayoutCache},
+    text_layout::{self, Line, RunStyle, TextLayoutCache},
     AppContext, Axis, Border, Element, ElementBox, Event, EventContext, LayoutContext,
     MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
 };
@@ -32,11 +32,20 @@ use std::{
 pub struct EditorElement {
     view: WeakViewHandle<Editor>,
     style: EditorStyle,
+    cursor_shape: CursorShape,
 }
 
 impl EditorElement {
-    pub fn new(view: WeakViewHandle<Editor>, style: EditorStyle) -> Self {
-        Self { view, style }
+    pub fn new(
+        view: WeakViewHandle<Editor>,
+        style: EditorStyle,
+        cursor_shape: CursorShape,
+    ) -> Self {
+        Self {
+            view,
+            style,
+            cursor_shape,
+        }
     }
 
     fn view<'a>(&self, cx: &'a AppContext) -> &'a Editor {
@@ -338,7 +347,7 @@ impl EditorElement {
 
         let mut cursors = SmallVec::<[Cursor; 32]>::new();
         for (replica_id, selections) in &layout.selections {
-            let style = style.replica_selection_style(*replica_id);
+            let selection_style = style.replica_selection_style(*replica_id);
             let corner_radius = 0.15 * layout.line_height;
 
             for selection in selections {
@@ -346,7 +355,7 @@ impl EditorElement {
                     selection.start..selection.end,
                     start_row,
                     end_row,
-                    style.selection,
+                    selection_style.selection,
                     corner_radius,
                     corner_radius * 2.,
                     layout,
@@ -362,13 +371,50 @@ impl EditorElement {
                     if (start_row..end_row).contains(&cursor_position.row()) {
                         let cursor_row_layout =
                             &layout.line_layouts[(cursor_position.row() - start_row) as usize];
-                        let x = cursor_row_layout.x_for_index(cursor_position.column() as usize)
-                            - scroll_left;
+                        let cursor_column = cursor_position.column() as usize;
+
+                        let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
+                        let mut block_width =
+                            cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x;
+                        if block_width == 0.0 {
+                            block_width = layout.em_width;
+                        }
+
+                        let block_text =
+                            if matches!(self.cursor_shape, CursorShape::Block) {
+                                layout.snapshot.chars_at(cursor_position).next().and_then(
+                                    |character| {
+                                        let font_id =
+                                            cursor_row_layout.font_for_index(cursor_column)?;
+                                        let text = character.to_string();
+
+                                        Some(cx.text_layout_cache.layout_str(
+                                            &text,
+                                            cursor_row_layout.font_size(),
+                                            &[(
+                                                text.len(),
+                                                RunStyle {
+                                                    font_id,
+                                                    color: style.background,
+                                                    underline: None,
+                                                },
+                                            )],
+                                        ))
+                                    },
+                                )
+                            } else {
+                                None
+                            };
+
+                        let x = cursor_character_x - scroll_left;
                         let y = cursor_position.row() as f32 * layout.line_height - scroll_top;
                         cursors.push(Cursor {
-                            color: style.cursor,
+                            color: selection_style.cursor,
+                            block_width,
                             origin: content_origin + vec2f(x, y),
                             line_height: layout.line_height,
+                            shape: self.cursor_shape,
+                            block_text,
                         });
                     }
                 }
@@ -1161,6 +1207,7 @@ fn layout_line(
         while !line.is_char_boundary(len) {
             len -= 1;
         }
+
         line.truncate(len);
     }
 
@@ -1212,20 +1259,51 @@ impl PaintState {
     }
 }
 
+#[derive(Copy, Clone)]
+pub enum CursorShape {
+    Bar,
+    Block,
+    Underscore,
+}
+
+impl Default for CursorShape {
+    fn default() -> Self {
+        CursorShape::Bar
+    }
+}
+
 struct Cursor {
     origin: Vector2F,
+    block_width: f32,
     line_height: f32,
     color: Color,
+    shape: CursorShape,
+    block_text: Option<Line>,
 }
 
 impl Cursor {
     fn paint(&self, cx: &mut PaintContext) {
+        let bounds = match self.shape {
+            CursorShape::Bar => RectF::new(self.origin, vec2f(2.0, self.line_height)),
+            CursorShape::Block => {
+                RectF::new(self.origin, vec2f(self.block_width, self.line_height))
+            }
+            CursorShape::Underscore => RectF::new(
+                self.origin + Vector2F::new(0.0, self.line_height - 2.0),
+                vec2f(self.block_width, 2.0),
+            ),
+        };
+
         cx.scene.push_quad(Quad {
-            bounds: RectF::new(self.origin, vec2f(2.0, self.line_height)),
+            bounds,
             background: Some(self.color),
             border: Border::new(0., Color::black()),
             corner_radius: 0.,
         });
+
+        if let Some(block_text) = &self.block_text {
+            block_text.paint(self.origin, bounds, self.line_height, cx);
+        }
     }
 }
 
@@ -1389,7 +1467,7 @@ mod tests {
         let (window_id, editor) = cx.add_window(Default::default(), |cx| {
             Editor::new(EditorMode::Full, buffer, None, settings.1, None, cx)
         });
-        let element = EditorElement::new(editor.downgrade(), editor.read(cx).style(cx));
+        let element = EditorElement::new(editor.downgrade(), editor.read(cx).style(cx), CursorShape::Bar);
 
         let layouts = editor.update(cx, |editor, cx| {
             let snapshot = editor.snapshot(cx);

crates/gpui/src/text_layout.rs 🔗

@@ -186,7 +186,7 @@ pub struct Run {
     pub glyphs: Vec<Glyph>,
 }
 
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 pub struct Glyph {
     pub id: GlyphId,
     pub position: Vector2F,
@@ -210,10 +210,14 @@ impl Line {
         self.layout.width
     }
 
+    pub fn font_size(&self) -> f32 {
+        self.layout.font_size
+    }
+
     pub fn x_for_index(&self, index: usize) -> f32 {
         for run in &self.layout.runs {
             for glyph in &run.glyphs {
-                if glyph.index == index {
+                if glyph.index >= index {
                     return glyph.position.x();
                 }
             }
@@ -221,6 +225,18 @@ impl Line {
         self.layout.width
     }
 
+    pub fn font_for_index(&self, index: usize) -> Option<FontId> {
+        for run in &self.layout.runs {
+            for glyph in &run.glyphs {
+                if glyph.index >= index {
+                    return Some(run.font_id);
+                }
+            }
+        }
+
+        None
+    }
+
     pub fn index_for_x(&self, x: f32) -> Option<usize> {
         if x >= self.layout.width {
             None