Separate WrappedLines from ShapedLines

Nathan Sobo created

ShapedLines are never wrapped, whereas WrappedLines are optionally wrapped if
they are associated with a wrap width. I tried to combine everything because
wrapping is inherently optional for the Text element, but we have a bunch of
APIs that don't make sense on a line that may wrap, so we need a distinct type
for that case.

Change summary

crates/editor2/src/display_map.rs            |  29 +-
crates/editor2/src/editor.rs                 |  32 +-
crates/editor2/src/element.rs                | 133 +++++-----
crates/editor2/src/movement.rs               |  10 
crates/editor2/src/selections_collection.rs  |   8 
crates/gpui2/src/element.rs                  |   2 
crates/gpui2/src/elements/text.rs            | 191 ++++++++++-----
crates/gpui2/src/platform/mac/text_system.rs |  10 
crates/gpui2/src/style.rs                    |   1 
crates/gpui2/src/text_system.rs              |  82 ++++++
crates/gpui2/src/text_system/line.rs         | 265 ++++++++++++---------
crates/gpui2/src/text_system/line_layout.rs  | 136 ++++++++--
12 files changed, 566 insertions(+), 333 deletions(-)

Detailed changes

crates/editor2/src/display_map.rs 🔗

@@ -13,7 +13,8 @@ pub use block_map::{BlockMap, BlockPoint};
 use collections::{BTreeMap, HashMap, HashSet};
 use fold_map::FoldMap;
 use gpui::{
-    Font, FontId, HighlightStyle, Hsla, Line, Model, ModelContext, Pixels, TextRun, UnderlineStyle,
+    Font, FontId, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, ShapedLine,
+    TextRun, UnderlineStyle, WrappedLine,
 };
 use inlay_map::InlayMap;
 use language::{
@@ -561,7 +562,7 @@ impl DisplaySnapshot {
         })
     }
 
-    pub fn lay_out_line_for_row(
+    pub fn layout_row(
         &self,
         display_row: u32,
         TextLayoutDetails {
@@ -569,7 +570,7 @@ impl DisplaySnapshot {
             editor_style,
             rem_size,
         }: &TextLayoutDetails,
-    ) -> Line {
+    ) -> Arc<LineLayout> {
         let mut runs = Vec::new();
         let mut line = String::new();
 
@@ -598,29 +599,27 @@ impl DisplaySnapshot {
 
         let font_size = editor_style.text.font_size.to_pixels(*rem_size);
         text_system
-            .layout_text(&line, font_size, &runs, None)
-            .unwrap()
-            .pop()
-            .unwrap()
+            .layout_line(&line, font_size, &runs)
+            .expect("we expect the font to be loaded because it's rendered by the editor")
     }
 
-    pub fn x_for_point(
+    pub fn x_for_display_point(
         &self,
         display_point: DisplayPoint,
         text_layout_details: &TextLayoutDetails,
     ) -> Pixels {
-        let layout_line = self.lay_out_line_for_row(display_point.row(), text_layout_details);
-        layout_line.x_for_index(display_point.column() as usize)
+        let line = self.layout_row(display_point.row(), text_layout_details);
+        line.x_for_index(display_point.column() as usize)
     }
 
-    pub fn column_for_x(
+    pub fn display_column_for_x(
         &self,
         display_row: u32,
-        x_coordinate: Pixels,
-        text_layout_details: &TextLayoutDetails,
+        x: Pixels,
+        details: &TextLayoutDetails,
     ) -> u32 {
-        let layout_line = self.lay_out_line_for_row(display_row, text_layout_details);
-        layout_line.closest_index_for_x(x_coordinate) as u32
+        let layout_line = self.layout_row(display_row, details);
+        layout_line.closest_index_for_x(x) as u32
     }
 
     pub fn chars_at(

crates/editor2/src/editor.rs 🔗

@@ -5445,7 +5445,9 @@ impl Editor {
                     *head.column_mut() += 1;
                     head = display_map.clip_point(head, Bias::Right);
                     let goal = SelectionGoal::HorizontalPosition(
-                        display_map.x_for_point(head, &text_layout_details).into(),
+                        display_map
+                            .x_for_display_point(head, &text_layout_details)
+                            .into(),
                     );
                     selection.collapse_to(head, goal);
 
@@ -6391,8 +6393,8 @@ impl Editor {
             let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone();
             let range = oldest_selection.display_range(&display_map).sorted();
 
-            let start_x = display_map.x_for_point(range.start, &text_layout_details);
-            let end_x = display_map.x_for_point(range.end, &text_layout_details);
+            let start_x = display_map.x_for_display_point(range.start, &text_layout_details);
+            let end_x = display_map.x_for_display_point(range.end, &text_layout_details);
             let positions = start_x.min(end_x)..start_x.max(end_x);
 
             selections.clear();
@@ -6431,15 +6433,16 @@ impl Editor {
                     let range = selection.display_range(&display_map).sorted();
                     debug_assert_eq!(range.start.row(), range.end.row());
                     let mut row = range.start.row();
-                    let positions = if let SelectionGoal::HorizontalRange { start, end } =
-                        selection.goal
-                    {
-                        px(start)..px(end)
-                    } else {
-                        let start_x = display_map.x_for_point(range.start, &text_layout_details);
-                        let end_x = display_map.x_for_point(range.end, &text_layout_details);
-                        start_x.min(end_x)..start_x.max(end_x)
-                    };
+                    let positions =
+                        if let SelectionGoal::HorizontalRange { start, end } = selection.goal {
+                            px(start)..px(end)
+                        } else {
+                            let start_x =
+                                display_map.x_for_display_point(range.start, &text_layout_details);
+                            let end_x =
+                                display_map.x_for_display_point(range.end, &text_layout_details);
+                            start_x.min(end_x)..start_x.max(end_x)
+                        };
 
                     while row != end_row {
                         if above {
@@ -6992,7 +6995,7 @@ impl Editor {
                         let display_point = point.to_display_point(display_snapshot);
                         let goal = SelectionGoal::HorizontalPosition(
                             display_snapshot
-                                .x_for_point(display_point, &text_layout_details)
+                                .x_for_display_point(display_point, &text_layout_details)
                                 .into(),
                         );
                         (display_point, goal)
@@ -9755,7 +9758,8 @@ impl InputHandler for Editor {
         let scroll_left = scroll_position.x * em_width;
 
         let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot);
-        let x = snapshot.x_for_point(start, &text_layout_details) - scroll_left + self.gutter_width;
+        let x = snapshot.x_for_display_point(start, &text_layout_details) - scroll_left
+            + self.gutter_width;
         let y = line_height * (start.row() as f32 - scroll_position.y);
 
         Some(Bounds {

crates/editor2/src/element.rs 🔗

@@ -20,10 +20,10 @@ use collections::{BTreeMap, HashMap};
 use gpui::{
     div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace,
     BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element,
-    ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, Line,
+    ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, LineLayout,
     MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels,
-    ScrollWheelEvent, Size, StatefulInteractiveComponent, Style, Styled, TextRun, TextStyle, View,
-    ViewContext, WindowContext,
+    ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveComponent, Style, Styled,
+    TextRun, TextStyle, View, ViewContext, WindowContext, WrappedLine,
 };
 use itertools::Itertools;
 use language::language_settings::ShowWhitespaceSetting;
@@ -476,7 +476,7 @@ impl EditorElement {
             Self::paint_diff_hunks(bounds, layout, cx);
         }
 
-        for (ix, line) in layout.line_number_layouts.iter().enumerate() {
+        for (ix, line) in layout.line_numbers.iter().enumerate() {
             if let Some(line) = line {
                 let line_origin = bounds.origin
                     + point(
@@ -775,21 +775,21 @@ impl EditorElement {
                                         .chars_at(cursor_position)
                                         .next()
                                         .and_then(|(character, _)| {
-                                            let text = character.to_string();
+                                            let text = SharedString::from(character.to_string());
+                                            let len = text.len();
                                             cx.text_system()
-                                                .layout_text(
-                                                    &text,
+                                                .shape_line(
+                                                    text,
                                                     cursor_row_layout.font_size,
                                                     &[TextRun {
-                                                        len: text.len(),
+                                                        len,
                                                         font: self.style.text.font(),
                                                         color: self.style.background,
+                                                        background_color: None,
                                                         underline: None,
                                                     }],
-                                                    None,
                                                 )
-                                                .unwrap()
-                                                .pop()
+                                                .log_err()
                                         })
                                 } else {
                                     None
@@ -1244,20 +1244,20 @@ impl EditorElement {
         let font_size = style.text.font_size.to_pixels(cx.rem_size());
         let layout = cx
             .text_system()
-            .layout_text(
-                " ".repeat(column).as_str(),
+            .shape_line(
+                SharedString::from(" ".repeat(column)),
                 font_size,
                 &[TextRun {
                     len: column,
                     font: style.text.font(),
                     color: Hsla::default(),
+                    background_color: None,
                     underline: None,
                 }],
-                None,
             )
             .unwrap();
 
-        layout[0].width
+        layout.width
     }
 
     fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext<Editor>) -> Pixels {
@@ -1338,7 +1338,7 @@ impl EditorElement {
         relative_rows
     }
 
-    fn layout_line_numbers(
+    fn shape_line_numbers(
         &self,
         rows: Range<u32>,
         active_rows: &BTreeMap<u32, bool>,
@@ -1347,12 +1347,12 @@ impl EditorElement {
         snapshot: &EditorSnapshot,
         cx: &ViewContext<Editor>,
     ) -> (
-        Vec<Option<gpui::Line>>,
+        Vec<Option<ShapedLine>>,
         Vec<Option<(FoldStatus, BufferRow, bool)>>,
     ) {
         let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
         let include_line_numbers = snapshot.mode == EditorMode::Full;
-        let mut line_number_layouts = Vec::with_capacity(rows.len());
+        let mut shaped_line_numbers = Vec::with_capacity(rows.len());
         let mut fold_statuses = Vec::with_capacity(rows.len());
         let mut line_number = String::new();
         let is_relative = EditorSettings::get_global(cx).relative_line_numbers;
@@ -1387,15 +1387,14 @@ impl EditorElement {
                         len: line_number.len(),
                         font: self.style.text.font(),
                         color,
+                        background_color: None,
                         underline: None,
                     };
-                    let layout = cx
+                    let shaped_line = cx
                         .text_system()
-                        .layout_text(&line_number, font_size, &[run], None)
-                        .unwrap()
-                        .pop()
+                        .shape_line(line_number.clone().into(), font_size, &[run])
                         .unwrap();
-                    line_number_layouts.push(Some(layout));
+                    shaped_line_numbers.push(Some(shaped_line));
                     fold_statuses.push(
                         is_singleton
                             .then(|| {
@@ -1408,17 +1407,17 @@ impl EditorElement {
                 }
             } else {
                 fold_statuses.push(None);
-                line_number_layouts.push(None);
+                shaped_line_numbers.push(None);
             }
         }
 
-        (line_number_layouts, fold_statuses)
+        (shaped_line_numbers, fold_statuses)
     }
 
     fn layout_lines(
         &mut self,
         rows: Range<u32>,
-        line_number_layouts: &[Option<Line>],
+        line_number_layouts: &[Option<ShapedLine>],
         snapshot: &EditorSnapshot,
         cx: &ViewContext<Editor>,
     ) -> Vec<LineWithInvisibles> {
@@ -1439,18 +1438,17 @@ impl EditorElement {
                 .chain(iter::repeat(""))
                 .take(rows.len());
             placeholder_lines
-                .map(|line| {
+                .filter_map(move |line| {
                     let run = TextRun {
                         len: line.len(),
                         font: self.style.text.font(),
                         color: placeholder_color,
+                        background_color: None,
                         underline: Default::default(),
                     };
                     cx.text_system()
-                        .layout_text(line, font_size, &[run], None)
-                        .unwrap()
-                        .pop()
-                        .unwrap()
+                        .shape_line(line.to_string().into(), font_size, &[run])
+                        .log_err()
                 })
                 .map(|line| LineWithInvisibles {
                     line,
@@ -1726,7 +1724,7 @@ impl EditorElement {
             .head
         });
 
-        let (line_number_layouts, fold_statuses) = self.layout_line_numbers(
+        let (line_numbers, fold_statuses) = self.shape_line_numbers(
             start_row..end_row,
             &active_rows,
             head_for_relative,
@@ -1740,8 +1738,7 @@ impl EditorElement {
         let scrollbar_row_range = scroll_position.y..(scroll_position.y + height_in_lines);
 
         let mut max_visible_line_width = Pixels::ZERO;
-        let line_layouts =
-            self.layout_lines(start_row..end_row, &line_number_layouts, &snapshot, cx);
+        let line_layouts = self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx);
         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;
@@ -1879,35 +1876,31 @@ impl EditorElement {
         let invisible_symbol_font_size = font_size / 2.;
         let tab_invisible = cx
             .text_system()
-            .layout_text(
-                "→",
+            .shape_line(
+                "→".into(),
                 invisible_symbol_font_size,
                 &[TextRun {
                     len: "→".len(),
                     font: self.style.text.font(),
                     color: cx.theme().colors().editor_invisible,
+                    background_color: None,
                     underline: None,
                 }],
-                None,
             )
-            .unwrap()
-            .pop()
             .unwrap();
         let space_invisible = cx
             .text_system()
-            .layout_text(
-                "•",
+            .shape_line(
+                "•".into(),
                 invisible_symbol_font_size,
                 &[TextRun {
                     len: "•".len(),
                     font: self.style.text.font(),
                     color: cx.theme().colors().editor_invisible,
+                    background_color: None,
                     underline: None,
                 }],
-                None,
             )
-            .unwrap()
-            .pop()
             .unwrap();
 
         LayoutState {
@@ -1939,7 +1932,7 @@ impl EditorElement {
             active_rows,
             highlighted_rows,
             highlighted_ranges,
-            line_number_layouts,
+            line_numbers,
             display_hunks,
             blocks,
             selections,
@@ -2199,7 +2192,7 @@ impl EditorElement {
 
 #[derive(Debug)]
 pub struct LineWithInvisibles {
-    pub line: Line,
+    pub line: ShapedLine,
     invisibles: Vec<Invisible>,
 }
 
@@ -2209,7 +2202,7 @@ impl LineWithInvisibles {
         text_style: &TextStyle,
         max_line_len: usize,
         max_line_count: usize,
-        line_number_layouts: &[Option<Line>],
+        line_number_layouts: &[Option<ShapedLine>],
         editor_mode: EditorMode,
         cx: &WindowContext,
     ) -> Vec<Self> {
@@ -2229,11 +2222,12 @@ impl LineWithInvisibles {
         }]) {
             for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() {
                 if ix > 0 {
-                    let layout = cx
+                    let shaped_line = cx
                         .text_system()
-                        .layout_text(&line, font_size, &styles, None);
+                        .shape_line(line.clone().into(), font_size, &styles)
+                        .unwrap();
                     layouts.push(Self {
-                        line: layout.unwrap().pop().unwrap(),
+                        line: shaped_line,
                         invisibles: invisibles.drain(..).collect(),
                     });
 
@@ -2267,6 +2261,7 @@ impl LineWithInvisibles {
                         len: line_chunk.len(),
                         font: text_style.font(),
                         color: text_style.color,
+                        background_color: None,
                         underline: text_style.underline,
                     });
 
@@ -3087,7 +3082,7 @@ pub struct LayoutState {
     visible_display_row_range: Range<u32>,
     active_rows: BTreeMap<u32, bool>,
     highlighted_rows: Option<Range<u32>>,
-    line_number_layouts: Vec<Option<gpui::Line>>,
+    line_numbers: Vec<Option<ShapedLine>>,
     display_hunks: Vec<DisplayDiffHunk>,
     blocks: Vec<BlockLayout>,
     highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
@@ -3100,8 +3095,8 @@ pub struct LayoutState {
     code_actions_indicator: Option<CodeActionsIndicator>,
     // hover_popovers: Option<(DisplayPoint, Vec<AnyElement<Editor>>)>,
     fold_indicators: Vec<Option<AnyElement<Editor>>>,
-    tab_invisible: Line,
-    space_invisible: Line,
+    tab_invisible: ShapedLine,
+    space_invisible: ShapedLine,
 }
 
 struct CodeActionsIndicator {
@@ -3201,7 +3196,7 @@ fn layout_line(
     snapshot: &EditorSnapshot,
     style: &EditorStyle,
     cx: &WindowContext,
-) -> Result<Line> {
+) -> Result<ShapedLine> {
     let mut line = snapshot.line(row);
 
     if line.len() > MAX_LINE_LEN {
@@ -3213,21 +3208,17 @@ fn layout_line(
         line.truncate(len);
     }
 
-    Ok(cx
-        .text_system()
-        .layout_text(
-            &line,
-            style.text.font_size.to_pixels(cx.rem_size()),
-            &[TextRun {
-                len: snapshot.line_len(row) as usize,
-                font: style.text.font(),
-                color: Hsla::default(),
-                underline: None,
-            }],
-            None,
-        )?
-        .pop()
-        .unwrap())
+    cx.text_system().shape_line(
+        line.into(),
+        style.text.font_size.to_pixels(cx.rem_size()),
+        &[TextRun {
+            len: snapshot.line_len(row) as usize,
+            font: style.text.font(),
+            color: Hsla::default(),
+            background_color: None,
+            underline: None,
+        }],
+    )
 }
 
 #[derive(Debug)]
@@ -3237,7 +3228,7 @@ pub struct Cursor {
     line_height: Pixels,
     color: Hsla,
     shape: CursorShape,
-    block_text: Option<Line>,
+    block_text: Option<ShapedLine>,
 }
 
 impl Cursor {
@@ -3247,7 +3238,7 @@ impl Cursor {
         line_height: Pixels,
         color: Hsla,
         shape: CursorShape,
-        block_text: Option<Line>,
+        block_text: Option<ShapedLine>,
     ) -> Cursor {
         Cursor {
             origin,

crates/editor2/src/movement.rs 🔗

@@ -98,7 +98,7 @@ pub fn up_by_rows(
         SelectionGoal::HorizontalPosition(x) => x.into(), // todo!("Can the fields in SelectionGoal by Pixels? We should extract a geometry crate and depend on that.")
         SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
         SelectionGoal::HorizontalRange { end, .. } => end.into(),
-        _ => map.x_for_point(start, text_layout_details),
+        _ => map.x_for_display_point(start, text_layout_details),
     };
 
     let prev_row = start.row().saturating_sub(row_count);
@@ -107,7 +107,7 @@ pub fn up_by_rows(
         Bias::Left,
     );
     if point.row() < start.row() {
-        *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
+        *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
     } else if preserve_column_at_start {
         return (start, goal);
     } else {
@@ -137,18 +137,18 @@ pub fn down_by_rows(
         SelectionGoal::HorizontalPosition(x) => x.into(),
         SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
         SelectionGoal::HorizontalRange { end, .. } => end.into(),
-        _ => map.x_for_point(start, text_layout_details),
+        _ => map.x_for_display_point(start, text_layout_details),
     };
 
     let new_row = start.row() + row_count;
     let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
     if point.row() > start.row() {
-        *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
+        *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
     } else if preserve_column_at_end {
         return (start, goal);
     } else {
         point = map.max_point();
-        goal_x = map.x_for_point(point, text_layout_details)
+        goal_x = map.x_for_display_point(point, text_layout_details)
     }
 
     let mut clipped_point = map.clip_point(point, Bias::Right);

crates/editor2/src/selections_collection.rs 🔗

@@ -313,14 +313,14 @@ impl SelectionsCollection {
         let is_empty = positions.start == positions.end;
         let line_len = display_map.line_len(row);
 
-        let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details);
+        let line = display_map.layout_row(row, &text_layout_details);
 
         dbg!("****START COL****");
-        let start_col = layed_out_line.closest_index_for_x(positions.start) as u32;
-        if start_col < line_len || (is_empty && positions.start == layed_out_line.width) {
+        let start_col = line.closest_index_for_x(positions.start) as u32;
+        if start_col < line_len || (is_empty && positions.start == line.width) {
             let start = DisplayPoint::new(row, start_col);
             dbg!("****END COL****");
-            let end_col = layed_out_line.closest_index_for_x(positions.end) as u32;
+            let end_col = line.closest_index_for_x(positions.end) as u32;
             let end = DisplayPoint::new(row, end_col);
             dbg!(start_col, end_col);
 

crates/gpui2/src/element.rs 🔗

@@ -13,7 +13,7 @@ pub trait Element<V: 'static> {
     fn layout(
         &mut self,
         view_state: &mut V,
-        previous_element_state: Option<Self::ElementState>,
+        element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
     ) -> (LayoutId, Self::ElementState);
 

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

@@ -1,76 +1,39 @@
 use crate::{
-    AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, Line, Pixels, SharedString,
-    Size, TextRun, ViewContext,
+    AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, LayoutId, Pixels,
+    SharedString, Size, TextRun, ViewContext, WrappedLine,
 };
-use parking_lot::Mutex;
+use parking_lot::{Mutex, MutexGuard};
 use smallvec::SmallVec;
-use std::{marker::PhantomData, sync::Arc};
+use std::{cell::Cell, rc::Rc, sync::Arc};
 use util::ResultExt;
 
-impl<V: 'static> Component<V> for SharedString {
-    fn render(self) -> AnyElement<V> {
-        Text {
-            text: self,
-            runs: None,
-            state_type: PhantomData,
-        }
-        .render()
-    }
-}
-
-impl<V: 'static> Component<V> for &'static str {
-    fn render(self) -> AnyElement<V> {
-        Text {
-            text: self.into(),
-            runs: None,
-            state_type: PhantomData,
-        }
-        .render()
-    }
-}
-
-// TODO: Figure out how to pass `String` to `child` without this.
-// This impl doesn't exist in the `gpui2` crate.
-impl<V: 'static> Component<V> for String {
-    fn render(self) -> AnyElement<V> {
-        Text {
-            text: self.into(),
-            runs: None,
-            state_type: PhantomData,
-        }
-        .render()
-    }
-}
-
-pub struct Text<V> {
+pub struct Text {
     text: SharedString,
     runs: Option<Vec<TextRun>>,
-    state_type: PhantomData<V>,
 }
 
-impl<V: 'static> Text<V> {
-    /// styled renders text that has different runs of different styles.
-    /// callers are responsible for setting the correct style for each run.
-    ////
-    /// For uniform text you can usually just pass a string as a child, and
-    /// cx.text_style() will be used automatically.
+impl Text {
+    /// Renders text with runs of different styles.
+    ///
+    /// Callers are responsible for setting the correct style for each run.
+    /// For text with a uniform style, you can usually avoid calling this constructor
+    /// and just pass text directly.
     pub fn styled(text: SharedString, runs: Vec<TextRun>) -> Self {
         Text {
             text,
             runs: Some(runs),
-            state_type: Default::default(),
         }
     }
 }
 
-impl<V: 'static> Component<V> for Text<V> {
+impl<V: 'static> Component<V> for Text {
     fn render(self) -> AnyElement<V> {
         AnyElement::new(self)
     }
 }
 
-impl<V: 'static> Element<V> for Text<V> {
-    type ElementState = Arc<Mutex<Option<TextElementState>>>;
+impl<V: 'static> Element<V> for Text {
+    type ElementState = TextState;
 
     fn element_id(&self) -> Option<crate::ElementId> {
         None
@@ -103,7 +66,7 @@ impl<V: 'static> Element<V> for Text<V> {
             let element_state = element_state.clone();
             move |known_dimensions, _| {
                 let Some(lines) = text_system
-                    .layout_text(
+                    .shape_text(
                         &text,
                         font_size,
                         &runs[..],
@@ -111,30 +74,23 @@ impl<V: 'static> Element<V> for Text<V> {
                     )
                     .log_err()
                 else {
-                    element_state.lock().replace(TextElementState {
+                    element_state.lock().replace(TextStateInner {
                         lines: Default::default(),
                         line_height,
                     });
                     return Size::default();
                 };
 
-                let line_count = lines
-                    .iter()
-                    .map(|line| line.wrap_count() + 1)
-                    .sum::<usize>();
-                let size = Size {
-                    width: lines
-                        .iter()
-                        .map(|line| line.layout.width)
-                        .max()
-                        .unwrap()
-                        .ceil(),
-                    height: line_height * line_count,
-                };
+                let mut size: Size<Pixels> = Size::default();
+                for line in &lines {
+                    let line_size = line.size(line_height);
+                    size.height += line_size.height;
+                    size.width = size.width.max(line_size.width);
+                }
 
                 element_state
                     .lock()
-                    .replace(TextElementState { lines, line_height });
+                    .replace(TextStateInner { lines, line_height });
 
                 size
             }
@@ -165,7 +121,104 @@ impl<V: 'static> Element<V> for Text<V> {
     }
 }
 
-pub struct TextElementState {
-    lines: SmallVec<[Line; 1]>,
+#[derive(Default, Clone)]
+pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
+
+impl TextState {
+    fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
+        self.0.lock()
+    }
+}
+
+struct TextStateInner {
+    lines: SmallVec<[WrappedLine; 1]>,
     line_height: Pixels,
 }
+
+struct InteractiveText {
+    id: ElementId,
+    text: Text,
+}
+
+struct InteractiveTextState {
+    text_state: TextState,
+    clicked_range_ixs: Rc<Cell<SmallVec<[usize; 1]>>>,
+}
+
+impl<V: 'static> Element<V> for InteractiveText {
+    type ElementState = InteractiveTextState;
+
+    fn element_id(&self) -> Option<ElementId> {
+        Some(self.id.clone())
+    }
+
+    fn layout(
+        &mut self,
+        view_state: &mut V,
+        element_state: Option<Self::ElementState>,
+        cx: &mut ViewContext<V>,
+    ) -> (LayoutId, Self::ElementState) {
+        if let Some(InteractiveTextState {
+            text_state,
+            clicked_range_ixs,
+        }) = element_state
+        {
+            let (layout_id, text_state) = self.text.layout(view_state, Some(text_state), cx);
+            let element_state = InteractiveTextState {
+                text_state,
+                clicked_range_ixs,
+            };
+            (layout_id, element_state)
+        } else {
+            let (layout_id, text_state) = self.text.layout(view_state, None, cx);
+            let element_state = InteractiveTextState {
+                text_state,
+                clicked_range_ixs: Rc::default(),
+            };
+            (layout_id, element_state)
+        }
+    }
+
+    fn paint(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) {
+        self.text
+            .paint(bounds, view_state, &mut element_state.text_state, cx)
+    }
+}
+
+impl<V: 'static> Component<V> for SharedString {
+    fn render(self) -> AnyElement<V> {
+        Text {
+            text: self,
+            runs: None,
+        }
+        .render()
+    }
+}
+
+impl<V: 'static> Component<V> for &'static str {
+    fn render(self) -> AnyElement<V> {
+        Text {
+            text: self.into(),
+            runs: None,
+        }
+        .render()
+    }
+}
+
+// TODO: Figure out how to pass `String` to `child` without this.
+// This impl doesn't exist in the `gpui2` crate.
+impl<V: 'static> Component<V> for String {
+    fn render(self) -> AnyElement<V> {
+        Text {
+            text: self.into(),
+            runs: None,
+        }
+        .render()
+    }
+}

crates/gpui2/src/platform/mac/text_system.rs 🔗

@@ -343,10 +343,10 @@ impl MacTextSystemState {
         // Construct the attributed string, converting UTF8 ranges to UTF16 ranges.
         let mut string = CFMutableAttributedString::new();
         {
-            string.replace_str(&CFString::new(text), CFRange::init(0, 0));
+            string.replace_str(&CFString::new(text.as_ref()), CFRange::init(0, 0));
             let utf16_line_len = string.char_len() as usize;
 
-            let mut ix_converter = StringIndexConverter::new(text);
+            let mut ix_converter = StringIndexConverter::new(text.as_ref());
             for run in font_runs {
                 let utf8_end = ix_converter.utf8_ix + run.len;
                 let utf16_start = ix_converter.utf16_ix;
@@ -390,7 +390,7 @@ impl MacTextSystemState {
             };
             let font_id = self.id_for_native_font(font);
 
-            let mut ix_converter = StringIndexConverter::new(text);
+            let mut ix_converter = StringIndexConverter::new(text.as_ref());
             let mut glyphs = SmallVec::new();
             for ((glyph_id, position), glyph_utf16_ix) in run
                 .glyphs()
@@ -413,11 +413,11 @@ impl MacTextSystemState {
 
         let typographic_bounds = line.get_typographic_bounds();
         LineLayout {
+            runs,
+            font_size,
             width: typographic_bounds.width.into(),
             ascent: typographic_bounds.ascent.into(),
             descent: typographic_bounds.descent.into(),
-            runs,
-            font_size,
             len: text.len(),
         }
     }

crates/gpui2/src/style.rs 🔗

@@ -203,6 +203,7 @@ impl TextStyle {
                 style: self.font_style,
             },
             color: self.color,
+            background_color: None,
             underline: self.underline.clone(),
         }
     }

crates/gpui2/src/text_system.rs 🔗

@@ -3,20 +3,20 @@ mod line;
 mod line_layout;
 mod line_wrapper;
 
-use anyhow::anyhow;
 pub use font_features::*;
 pub use line::*;
 pub use line_layout::*;
 pub use line_wrapper::*;
-use smallvec::SmallVec;
 
 use crate::{
     px, Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size,
     UnderlineStyle,
 };
+use anyhow::anyhow;
 use collections::HashMap;
 use core::fmt;
 use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
+use smallvec::SmallVec;
 use std::{
     cmp,
     fmt::{Debug, Display, Formatter},
@@ -151,13 +151,79 @@ impl TextSystem {
         }
     }
 
-    pub fn layout_text(
+    pub fn layout_line(
         &self,
         text: &str,
         font_size: Pixels,
         runs: &[TextRun],
+    ) -> Result<Arc<LineLayout>> {
+        let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
+        for run in runs.iter() {
+            let font_id = self.font_id(&run.font)?;
+            if let Some(last_run) = font_runs.last_mut() {
+                if last_run.font_id == font_id {
+                    last_run.len += run.len;
+                    continue;
+                }
+            }
+            font_runs.push(FontRun {
+                len: run.len,
+                font_id,
+            });
+        }
+
+        let layout = self
+            .line_layout_cache
+            .layout_line(&text, font_size, &font_runs);
+
+        font_runs.clear();
+        self.font_runs_pool.lock().push(font_runs);
+
+        Ok(layout)
+    }
+
+    pub fn shape_line(
+        &self,
+        text: SharedString,
+        font_size: Pixels,
+        runs: &[TextRun],
+    ) -> Result<ShapedLine> {
+        debug_assert!(
+            text.find('\n').is_none(),
+            "text argument should not contain newlines"
+        );
+
+        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 {
+                    last_run.len += run.len as u32;
+                    continue;
+                }
+            }
+            decoration_runs.push(DecorationRun {
+                len: run.len as u32,
+                color: run.color,
+                underline: run.underline.clone(),
+            });
+        }
+
+        let layout = self.layout_line(text.as_ref(), font_size, runs)?;
+
+        Ok(ShapedLine {
+            layout,
+            text,
+            decoration_runs,
+        })
+    }
+
+    pub fn shape_text(
+        &self,
+        text: &str, // todo!("pass a SharedString and preserve it when passed a single line?")
+        font_size: Pixels,
+        runs: &[TextRun],
         wrap_width: Option<Pixels>,
-    ) -> Result<SmallVec<[Line; 1]>> {
+    ) -> Result<SmallVec<[WrappedLine; 1]>> {
         let mut runs = runs.iter().cloned().peekable();
         let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
 
@@ -210,10 +276,11 @@ impl TextSystem {
 
             let layout = self
                 .line_layout_cache
-                .layout_line(&line_text, font_size, &font_runs, wrap_width);
-            lines.push(Line {
+                .layout_wrapped_line(&line_text, font_size, &font_runs, wrap_width);
+            lines.push(WrappedLine {
                 layout,
-                decorations: decoration_runs,
+                decoration_runs,
+                text: SharedString::from(line_text),
             });
 
             line_start = line_end + 1; // Skip `\n` character.
@@ -384,6 +451,7 @@ pub struct TextRun {
     pub len: usize,
     pub font: Font,
     pub color: Hsla,
+    pub background_color: Option<Hsla>,
     pub underline: Option<UnderlineStyle>,
 }
 

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

@@ -1,5 +1,5 @@
 use crate::{
-    black, point, px, size, BorrowWindow, Bounds, Hsla, Pixels, Point, Result, Size,
+    black, point, px, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString,
     UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
 };
 use derive_more::{Deref, DerefMut};
@@ -14,23 +14,51 @@ pub struct DecorationRun {
 }
 
 #[derive(Clone, Default, Debug, Deref, DerefMut)]
-pub struct Line {
+pub struct ShapedLine {
     #[deref]
     #[deref_mut]
-    pub(crate) layout: Arc<WrappedLineLayout>,
-    pub(crate) decorations: SmallVec<[DecorationRun; 32]>,
+    pub(crate) layout: Arc<LineLayout>,
+    pub text: SharedString,
+    pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
 }
 
-impl Line {
-    pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
-        size(
-            self.layout.width,
-            line_height * (self.layout.wrap_boundaries.len() + 1),
-        )
+impl ShapedLine {
+    pub fn len(&self) -> usize {
+        self.layout.len
     }
 
-    pub fn wrap_count(&self) -> usize {
-        self.layout.wrap_boundaries.len()
+    pub fn paint(
+        &self,
+        origin: Point<Pixels>,
+        line_height: Pixels,
+        cx: &mut WindowContext,
+    ) -> Result<()> {
+        paint_line(
+            origin,
+            &self.layout,
+            line_height,
+            &self.decoration_runs,
+            None,
+            &[],
+            cx,
+        )?;
+
+        Ok(())
+    }
+}
+
+#[derive(Clone, Default, Debug, Deref, DerefMut)]
+pub struct WrappedLine {
+    #[deref]
+    #[deref_mut]
+    pub(crate) layout: Arc<WrappedLineLayout>,
+    pub text: SharedString,
+    pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
+}
+
+impl WrappedLine {
+    pub fn len(&self) -> usize {
+        self.layout.len()
     }
 
     pub fn paint(
@@ -39,75 +67,50 @@ impl Line {
         line_height: Pixels,
         cx: &mut WindowContext,
     ) -> Result<()> {
-        let padding_top =
-            (line_height - self.layout.layout.ascent - self.layout.layout.descent) / 2.;
-        let baseline_offset = point(px(0.), padding_top + self.layout.layout.ascent);
-
-        let mut style_runs = self.decorations.iter();
-        let mut wraps = self.layout.wrap_boundaries.iter().peekable();
-        let mut run_end = 0;
-        let mut color = black();
-        let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
-        let text_system = cx.text_system().clone();
-
-        let mut glyph_origin = origin;
-        let mut prev_glyph_position = Point::default();
-        for (run_ix, run) in self.layout.layout.runs.iter().enumerate() {
-            let max_glyph_size = text_system
-                .bounding_box(run.font_id, self.layout.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((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;
-                    glyph_origin.y += line_height;
-                }
-                prev_glyph_position = glyph.position;
-
-                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 current_underline {
-                            if style_run.underline.as_ref() != Some(underline_style) {
-                                finished_underline = current_underline.take();
-                            }
-                        }
-                        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.layout.descent * 0.618),
-                                ),
-                                UnderlineStyle {
-                                    color: Some(run_underline.color.unwrap_or(style_run.color)),
-                                    thickness: run_underline.thickness,
-                                    wavy: run_underline.wavy,
-                                },
-                            ));
-                        }
+        paint_line(
+            origin,
+            &self.layout.unwrapped_layout,
+            line_height,
+            &self.decoration_runs,
+            self.wrap_width,
+            &self.wrap_boundaries,
+            cx,
+        )?;
 
-                        run_end += style_run.len as usize;
-                        color = style_run.color;
-                    } else {
-                        run_end = self.layout.text.len();
-                        finished_underline = current_underline.take();
-                    }
-                }
+        Ok(())
+    }
+}
 
-                if let Some((underline_origin, underline_style)) = finished_underline {
+fn paint_line(
+    origin: Point<Pixels>,
+    layout: &LineLayout,
+    line_height: Pixels,
+    decoration_runs: &[DecorationRun],
+    wrap_width: Option<Pixels>,
+    wrap_boundaries: &[WrapBoundary],
+    cx: &mut WindowContext<'_>,
+) -> Result<()> {
+    let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
+    let baseline_offset = point(px(0.), padding_top + layout.ascent);
+    let mut decoration_runs = decoration_runs.iter();
+    let mut wraps = wrap_boundaries.iter().peekable();
+    let mut run_end = 0;
+    let mut color = black();
+    let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
+    let text_system = cx.text_system().clone();
+    let mut glyph_origin = origin;
+    let mut prev_glyph_position = Point::default();
+    for (run_ix, run) in layout.runs.iter().enumerate() {
+        let 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((underline_origin, underline_style)) = current_underline.take() {
                     cx.paint_underline(
                         underline_origin,
                         glyph_origin.x - underline_origin.x,
@@ -115,42 +118,84 @@ impl Line {
                     )?;
                 }
 
-                let max_glyph_bounds = Bounds {
-                    origin: glyph_origin,
-                    size: max_glyph_size,
-                };
-
-                let content_mask = cx.content_mask();
-                if max_glyph_bounds.intersects(&content_mask.bounds) {
-                    if glyph.is_emoji {
-                        cx.paint_emoji(
-                            glyph_origin + baseline_offset,
-                            run.font_id,
-                            glyph.id,
-                            self.layout.layout.font_size,
-                        )?;
-                    } else {
-                        cx.paint_glyph(
-                            glyph_origin + baseline_offset,
-                            run.font_id,
-                            glyph.id,
-                            self.layout.layout.font_size,
-                            color,
-                        )?;
+                glyph_origin.x = origin.x;
+                glyph_origin.y += line_height;
+            }
+            prev_glyph_position = glyph.position;
+
+            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((_, underline_style)) = &mut current_underline {
+                        if style_run.underline.as_ref() != Some(underline_style) {
+                            finished_underline = current_underline.take();
+                        }
                     }
+                    if let Some(run_underline) = style_run.underline.as_ref() {
+                        current_underline.get_or_insert((
+                            point(
+                                glyph_origin.x,
+                                origin.y + baseline_offset.y + (layout.descent * 0.618),
+                            ),
+                            UnderlineStyle {
+                                color: Some(run_underline.color.unwrap_or(style_run.color)),
+                                thickness: run_underline.thickness,
+                                wavy: run_underline.wavy,
+                            },
+                        ));
+                    }
+
+                    run_end += style_run.len as usize;
+                    color = style_run.color;
+                } else {
+                    run_end = layout.len;
+                    finished_underline = current_underline.take();
                 }
             }
-        }
 
-        if let Some((underline_start, underline_style)) = current_underline.take() {
-            let line_end_x = origin.x + self.layout.layout.width;
-            cx.paint_underline(
-                underline_start,
-                line_end_x - underline_start.x,
-                &underline_style,
-            )?;
+            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 {
+                origin: glyph_origin,
+                size: max_glyph_size,
+            };
+
+            let content_mask = cx.content_mask();
+            if max_glyph_bounds.intersects(&content_mask.bounds) {
+                if glyph.is_emoji {
+                    cx.paint_emoji(
+                        glyph_origin + baseline_offset,
+                        run.font_id,
+                        glyph.id,
+                        layout.font_size,
+                    )?;
+                } else {
+                    cx.paint_glyph(
+                        glyph_origin + baseline_offset,
+                        run.font_id,
+                        glyph.id,
+                        layout.font_size,
+                        color,
+                    )?;
+                }
+            }
         }
+    }
 
-        Ok(())
+    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/text_system/line_layout.rs 🔗

@@ -1,5 +1,4 @@
-use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, SharedString};
-use derive_more::{Deref, DerefMut};
+use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size};
 use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
 use smallvec::SmallVec;
 use std::{
@@ -149,13 +148,11 @@ impl LineLayout {
     }
 }
 
-#[derive(Deref, DerefMut, Default, Debug)]
+#[derive(Default, Debug)]
 pub struct WrappedLineLayout {
-    #[deref]
-    #[deref_mut]
-    pub layout: LineLayout,
-    pub text: SharedString,
+    pub unwrapped_layout: Arc<LineLayout>,
     pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>,
+    pub wrap_width: Option<Pixels>,
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
@@ -164,31 +161,74 @@ pub struct WrapBoundary {
     pub glyph_ix: usize,
 }
 
+impl WrappedLineLayout {
+    pub fn len(&self) -> usize {
+        self.unwrapped_layout.len
+    }
+
+    pub fn width(&self) -> Pixels {
+        self.wrap_width
+            .unwrap_or(Pixels::MAX)
+            .min(self.unwrapped_layout.width)
+    }
+
+    pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
+        Size {
+            width: self.width(),
+            height: line_height * (self.wrap_boundaries.len() + 1),
+        }
+    }
+
+    pub fn ascent(&self) -> Pixels {
+        self.unwrapped_layout.ascent
+    }
+
+    pub fn descent(&self) -> Pixels {
+        self.unwrapped_layout.descent
+    }
+
+    pub fn wrap_boundaries(&self) -> &[WrapBoundary] {
+        &self.wrap_boundaries
+    }
+
+    pub fn font_size(&self) -> Pixels {
+        self.unwrapped_layout.font_size
+    }
+
+    pub fn runs(&self) -> &[ShapedRun] {
+        &self.unwrapped_layout.runs
+    }
+}
+
 pub(crate) struct LineLayoutCache {
-    prev_frame: Mutex<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
-    curr_frame: RwLock<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
+    previous_frame: Mutex<HashMap<CacheKey, Arc<LineLayout>>>,
+    current_frame: RwLock<HashMap<CacheKey, Arc<LineLayout>>>,
+    previous_frame_wrapped: Mutex<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
+    current_frame_wrapped: RwLock<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
     platform_text_system: Arc<dyn PlatformTextSystem>,
 }
 
 impl LineLayoutCache {
     pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
         Self {
-            prev_frame: Mutex::new(HashMap::new()),
-            curr_frame: RwLock::new(HashMap::new()),
+            previous_frame: Mutex::default(),
+            current_frame: RwLock::default(),
+            previous_frame_wrapped: Mutex::default(),
+            current_frame_wrapped: RwLock::default(),
             platform_text_system,
         }
     }
 
     pub fn start_frame(&self) {
-        let mut prev_frame = self.prev_frame.lock();
-        let mut curr_frame = self.curr_frame.write();
+        let mut prev_frame = self.previous_frame.lock();
+        let mut curr_frame = self.current_frame.write();
         std::mem::swap(&mut *prev_frame, &mut *curr_frame);
         curr_frame.clear();
     }
 
-    pub fn layout_line(
+    pub fn layout_wrapped_line(
         &self,
-        text: &SharedString,
+        text: &str,
         font_size: Pixels,
         runs: &[FontRun],
         wrap_width: Option<Pixels>,
@@ -199,34 +239,66 @@ impl LineLayoutCache {
             runs,
             wrap_width,
         } as &dyn AsCacheKeyRef;
-        let curr_frame = self.curr_frame.upgradable_read();
-        if let Some(layout) = curr_frame.get(key) {
+
+        let current_frame = self.current_frame_wrapped.upgradable_read();
+        if let Some(layout) = current_frame.get(key) {
             return layout.clone();
         }
 
-        let mut curr_frame = RwLockUpgradableReadGuard::upgrade(curr_frame);
-        if let Some((key, layout)) = self.prev_frame.lock().remove_entry(key) {
-            curr_frame.insert(key, layout.clone());
+        let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
+        if let Some((key, layout)) = self.previous_frame_wrapped.lock().remove_entry(key) {
+            current_frame.insert(key, layout.clone());
             layout
         } else {
-            let layout = self.platform_text_system.layout_line(text, font_size, runs);
-            let wrap_boundaries = wrap_width
-                .map(|wrap_width| layout.compute_wrap_boundaries(text.as_ref(), wrap_width))
-                .unwrap_or_default();
-            let wrapped_line = Arc::new(WrappedLineLayout {
-                layout,
-                text: text.clone(),
+            let unwrapped_layout = self.layout_line(text, font_size, runs);
+            let wrap_boundaries = if let Some(wrap_width) = wrap_width {
+                unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width)
+            } else {
+                SmallVec::new()
+            };
+            let layout = Arc::new(WrappedLineLayout {
+                unwrapped_layout,
                 wrap_boundaries,
+                wrap_width,
             });
-
             let key = CacheKey {
-                text: text.clone(),
+                text: text.into(),
                 font_size,
                 runs: SmallVec::from(runs),
                 wrap_width,
             };
-            curr_frame.insert(key, wrapped_line.clone());
-            wrapped_line
+            current_frame.insert(key, layout.clone());
+            layout
+        }
+    }
+
+    pub fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> Arc<LineLayout> {
+        let key = &CacheKeyRef {
+            text,
+            font_size,
+            runs,
+            wrap_width: None,
+        } as &dyn AsCacheKeyRef;
+
+        let current_frame = self.current_frame.upgradable_read();
+        if let Some(layout) = current_frame.get(key) {
+            return layout.clone();
+        }
+
+        let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
+        if let Some((key, layout)) = self.previous_frame.lock().remove_entry(key) {
+            current_frame.insert(key, layout.clone());
+            layout
+        } else {
+            let layout = Arc::new(self.platform_text_system.layout_line(text, font_size, runs));
+            let key = CacheKey {
+                text: text.into(),
+                font_size,
+                runs: SmallVec::from(runs),
+                wrap_width: None,
+            };
+            current_frame.insert(key, layout.clone());
+            layout
         }
     }
 }
@@ -243,7 +315,7 @@ trait AsCacheKeyRef {
 
 #[derive(Eq)]
 struct CacheKey {
-    text: SharedString,
+    text: String,
     font_size: Pixels,
     runs: SmallVec<[FontRun; 1]>,
     wrap_width: Option<Pixels>,