From 9558da868193670a58b593b2908dda264a035bfd Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Nov 2023 20:11:55 -0700 Subject: [PATCH] Separate WrappedLines from ShapedLines 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. --- 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 | 259 +++++++++++-------- crates/gpui2/src/text_system/line_layout.rs | 136 +++++++--- 12 files changed, 563 insertions(+), 330 deletions(-) diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index e64d5e301c..533abcd871 100644 --- a/crates/editor2/src/display_map.rs +++ b/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 { 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( diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index af7da8e837..fff78ee2c5 100644 --- a/crates/editor2/src/editor.rs +++ b/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 { diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index de1b6f0622..782410f5f3 100644 --- a/crates/editor2/src/element.rs +++ b/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) -> Pixels { @@ -1338,7 +1338,7 @@ impl EditorElement { relative_rows } - fn layout_line_numbers( + fn shape_line_numbers( &self, rows: Range, active_rows: &BTreeMap, @@ -1347,12 +1347,12 @@ impl EditorElement { snapshot: &EditorSnapshot, cx: &ViewContext, ) -> ( - Vec>, + Vec>, Vec>, ) { 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, - line_number_layouts: &[Option], + line_number_layouts: &[Option], snapshot: &EditorSnapshot, cx: &ViewContext, ) -> Vec { @@ -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, } @@ -2209,7 +2202,7 @@ impl LineWithInvisibles { text_style: &TextStyle, max_line_len: usize, max_line_count: usize, - line_number_layouts: &[Option], + line_number_layouts: &[Option], editor_mode: EditorMode, cx: &WindowContext, ) -> Vec { @@ -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, active_rows: BTreeMap, highlighted_rows: Option>, - line_number_layouts: Vec>, + line_numbers: Vec>, display_hunks: Vec, blocks: Vec, highlighted_ranges: Vec<(Range, Hsla)>, @@ -3100,8 +3095,8 @@ pub struct LayoutState { code_actions_indicator: Option, // hover_popovers: Option<(DisplayPoint, Vec>)>, fold_indicators: Vec>>, - 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 { +) -> Result { 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, + block_text: Option, } impl Cursor { @@ -3247,7 +3238,7 @@ impl Cursor { line_height: Pixels, color: Hsla, shape: CursorShape, - block_text: Option, + block_text: Option, ) -> Cursor { Cursor { origin, diff --git a/crates/editor2/src/movement.rs b/crates/editor2/src/movement.rs index b28af681e0..1414ae702d 100644 --- a/crates/editor2/src/movement.rs +++ b/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); diff --git a/crates/editor2/src/selections_collection.rs b/crates/editor2/src/selections_collection.rs index 01e241c830..bcf41f135b 100644 --- a/crates/editor2/src/selections_collection.rs +++ b/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); diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 221eb903fd..b4b1af630e 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -13,7 +13,7 @@ pub trait Element { fn layout( &mut self, view_state: &mut V, - previous_element_state: Option, + element_state: Option, cx: &mut ViewContext, ) -> (LayoutId, Self::ElementState); diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 1081154e7d..6849a89711 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/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 Component for SharedString { - fn render(self) -> AnyElement { - Text { - text: self, - runs: None, - state_type: PhantomData, - } - .render() - } -} - -impl Component for &'static str { - fn render(self) -> AnyElement { - 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 Component for String { - fn render(self) -> AnyElement { - Text { - text: self.into(), - runs: None, - state_type: PhantomData, - } - .render() - } -} - -pub struct Text { +pub struct Text { text: SharedString, runs: Option>, - state_type: PhantomData, } -impl Text { - /// 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) -> Self { Text { text, runs: Some(runs), - state_type: Default::default(), } } } -impl Component for Text { +impl Component for Text { fn render(self) -> AnyElement { AnyElement::new(self) } } -impl Element for Text { - type ElementState = Arc>>; +impl Element for Text { + type ElementState = TextState; fn element_id(&self) -> Option { None @@ -103,7 +66,7 @@ impl Element for Text { 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 Element for Text { ) .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::(); - let size = Size { - width: lines - .iter() - .map(|line| line.layout.width) - .max() - .unwrap() - .ceil(), - height: line_height * line_count, - }; + let mut size: Size = 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 Element for Text { } } -pub struct TextElementState { - lines: SmallVec<[Line; 1]>, +#[derive(Default, Clone)] +pub struct TextState(Arc>>); + +impl TextState { + fn lock(&self) -> MutexGuard> { + 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>>, +} + +impl Element for InteractiveText { + type ElementState = InteractiveTextState; + + fn element_id(&self) -> Option { + Some(self.id.clone()) + } + + fn layout( + &mut self, + view_state: &mut V, + element_state: Option, + cx: &mut ViewContext, + ) -> (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, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + self.text + .paint(bounds, view_state, &mut element_state.text_state, cx) + } +} + +impl Component for SharedString { + fn render(self) -> AnyElement { + Text { + text: self, + runs: None, + } + .render() + } +} + +impl Component for &'static str { + fn render(self) -> AnyElement { + 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 Component for String { + fn render(self) -> AnyElement { + Text { + text: self.into(), + runs: None, + } + .render() + } +} diff --git a/crates/gpui2/src/platform/mac/text_system.rs b/crates/gpui2/src/platform/mac/text_system.rs index 155f3097fe..9ef0f321b6 100644 --- a/crates/gpui2/src/platform/mac/text_system.rs +++ b/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(), } } diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 5d9dd5d804..1b0cabb401 100644 --- a/crates/gpui2/src/style.rs +++ b/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(), } } diff --git a/crates/gpui2/src/text_system.rs b/crates/gpui2/src/text_system.rs index c7031fcb4d..b3d7a96aff 100644 --- a/crates/gpui2/src/text_system.rs +++ b/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> { + 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 { + 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, - ) -> Result> { + ) -> Result> { 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, pub underline: Option, } diff --git a/crates/gpui2/src/text_system/line.rs b/crates/gpui2/src/text_system/line.rs index 707274ad33..d05ae9468d 100644 --- a/crates/gpui2/src/text_system/line.rs +++ b/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,17 @@ pub struct DecorationRun { } #[derive(Clone, Default, Debug, Deref, DerefMut)] -pub struct Line { +pub struct ShapedLine { #[deref] #[deref_mut] - pub(crate) layout: Arc, - pub(crate) decorations: SmallVec<[DecorationRun; 32]>, + pub(crate) layout: Arc, + pub text: SharedString, + pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>, } -impl Line { - pub fn size(&self, line_height: Pixels) -> Size { - size( - self.layout.width, - line_height * (self.layout.wrap_boundaries.len() + 1), - ) - } - - pub fn wrap_count(&self) -> usize { - self.layout.wrap_boundaries.len() +impl ShapedLine { + pub fn len(&self) -> usize { + self.layout.len } pub fn paint( @@ -39,75 +33,84 @@ 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); + paint_line( + origin, + &self.layout, + line_height, + &self.decoration_runs, + None, + &[], + cx, + )?; - 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, UnderlineStyle)> = None; - let text_system = cx.text_system().clone(); + Ok(()) + } +} - 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; +#[derive(Clone, Default, Debug, Deref, DerefMut)] +pub struct WrappedLine { + #[deref] + #[deref_mut] + pub(crate) layout: Arc, + pub text: SharedString, + pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>, +} - for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { - glyph_origin.x += glyph.position.x - prev_glyph_position.x; +impl WrappedLine { + pub fn len(&self) -> usize { + self.layout.len() + } - 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, - )?; - } + pub fn paint( + &self, + origin: Point, + line_height: Pixels, + cx: &mut WindowContext, + ) -> Result<()> { + paint_line( + origin, + &self.layout.unwrapped_layout, + line_height, + &self.decoration_runs, + self.wrap_width, + &self.wrap_boundaries, + cx, + )?; - glyph_origin.x = origin.x; - glyph_origin.y += line_height; - } - prev_glyph_position = glyph.position; + Ok(()) + } +} - let mut finished_underline: Option<(Point, 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, - }, - )); - } +fn paint_line( + origin: Point, + layout: &LineLayout, + line_height: Pixels, + decoration_runs: &[DecorationRun], + wrap_width: Option, + 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, 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; - run_end += style_run.len as usize; - color = style_run.color; - } else { - run_end = self.layout.text.len(); - finished_underline = current_underline.take(); - } - } + for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { + glyph_origin.x += glyph.position.x - prev_glyph_position.x; - if let Some((underline_origin, underline_style)) = finished_underline { + 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, - }; + glyph_origin.x = origin.x; + glyph_origin.y += line_height; + } + prev_glyph_position = glyph.position; - 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, - )?; + let mut finished_underline: Option<(Point, 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_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, + )?; } } } - - 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, - )?; - } - - 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(()) } diff --git a/crates/gpui2/src/text_system/line_layout.rs b/crates/gpui2/src/text_system/line_layout.rs index 7e9176caca..a5cf814a8c 100644 --- a/crates/gpui2/src/text_system/line_layout.rs +++ b/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, pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>, + pub wrap_width: Option, } #[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 { + 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>>, - curr_frame: RwLock>>, + previous_frame: Mutex>>, + current_frame: RwLock>>, + previous_frame_wrapped: Mutex>>, + current_frame_wrapped: RwLock>>, platform_text_system: Arc, } impl LineLayoutCache { pub fn new(platform_text_system: Arc) -> 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, @@ -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 { + 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,