From 2d8c88ad73235e20fb0357c8f14e23ca82d1416b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 4 May 2023 17:37:00 +0300 Subject: [PATCH] Draw tabs with svg icons in editor code only --- crates/editor/src/element.rs | 197 +++++++++++++++++++++---- crates/editor/src/scroll/autoscroll.rs | 11 +- crates/gpui/src/elements/text.rs | 49 ++---- crates/gpui/src/text_layout.rs | 33 +---- 4 files changed, 186 insertions(+), 104 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 9a213be996..b5abcfa48e 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -21,7 +21,7 @@ use git::diff::DiffHunkStatus; use gpui::{ color::Color, elements::*, - fonts::{HighlightStyle, Underline}, + fonts::{HighlightStyle, TextStyle, Underline}, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, @@ -29,17 +29,18 @@ use gpui::{ }, json::{self, ToJson}, platform::{CursorStyle, Modifiers, MouseButton, MouseButtonEvent, MouseMovedEvent}, - text_layout::{self, Line, RunStyle, TextLayoutCache}, - AnyElement, Axis, Border, CursorRegion, Element, EventContext, LayoutContext, MouseRegion, - Quad, SceneBuilder, SizeConstraint, ViewContext, WindowContext, + text_layout::{self, Invisible, Line, RunStyle, TextLayoutCache}, + AnyElement, Axis, Border, CursorRegion, Element, EventContext, FontCache, LayoutContext, + MouseRegion, Quad, SceneBuilder, SizeConstraint, ViewContext, WindowContext, }; use itertools::Itertools; use json::json; use language::{Bias, CursorShape, DiagnosticSeverity, OffsetUtf16, Selection}; use project::ProjectPath; -use settings::{GitGutter, Settings}; +use settings::{GitGutter, Settings, ShowInvisibles}; use smallvec::SmallVec; use std::{ + borrow::Cow, cmp::{self, Ordering}, fmt::Write, iter, @@ -808,7 +809,8 @@ impl EditorElement { .contains(&cursor_position.row()) { let cursor_row_layout = &layout.position_map.line_layouts - [(cursor_position.row() - start_row) as usize]; + [(cursor_position.row() - start_row) as usize] + .line; let cursor_column = cursor_position.column() as usize; let cursor_character_x = cursor_row_layout.x_for_index(cursor_column); @@ -863,9 +865,9 @@ impl EditorElement { if let Some(visible_text_bounds) = bounds.intersection(visible_bounds) { // Draw glyphs - for (ix, line) in layout.position_map.line_layouts.iter().enumerate() { + for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() { let row = start_row + ix as u32; - line.paint( + line_with_invisibles.line.paint( scene, content_origin + vec2f( @@ -876,6 +878,53 @@ impl EditorElement { layout.position_map.line_height, cx, ); + + let settings = cx.global::(); + match settings + .editor_overrides + .show_invisibles + .or(settings.editor_defaults.show_invisibles) + .unwrap_or_default() + { + ShowInvisibles::None => {} + ShowInvisibles::All => { + for invisible in &line_with_invisibles.invisibles { + match invisible { + Invisible::Tab { line_start_offset } => { + // TODO kb cache, deduplicate + let x_offset = + line_with_invisibles.line.x_for_index(*line_start_offset); + let font_size = line_with_invisibles.line.font_size(); + let max_size = vec2f(font_size, font_size); + let origin = content_origin + + vec2f( + -scroll_left + x_offset, + row as f32 * layout.position_map.line_height + - scroll_top, + ); + + let mut test_svg = Svg::new("icons/arrow_right_16.svg") + .with_color(Color::red()); + let (_, mut layout_state) = test_svg.layout( + SizeConstraint::new(origin, max_size), + editor, + cx, + ); + test_svg.paint( + scene, + RectF::new(origin, max_size), + visible_bounds, + &mut layout_state, + editor, + cx, + ); + } + // TODO kb draw whitespaces too + Invisible::Whitespace { .. } => {} + } + } + } + } } } @@ -888,7 +937,7 @@ impl EditorElement { if let Some((position, context_menu)) = layout.context_menu.as_mut() { scene.push_stacking_context(None, None); let cursor_row_layout = - &layout.position_map.line_layouts[(position.row() - start_row) as usize]; + &layout.position_map.line_layouts[(position.row() - start_row) as usize].line; let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left; let y = (position.row() + 1) as f32 * layout.position_map.line_height - scroll_top; let mut list_origin = content_origin + vec2f(x, y); @@ -921,7 +970,7 @@ impl EditorElement { // This is safe because we check on layout whether the required row is available let hovered_row_layout = - &layout.position_map.line_layouts[(position.row() - start_row) as usize]; + &layout.position_map.line_layouts[(position.row() - start_row) as usize].line; // Minimum required size: Take the first popover, and add 1.5 times the minimum popover // height. This is the size we will use to decide whether to render popovers above or below @@ -1118,7 +1167,7 @@ impl EditorElement { .into_iter() .map(|row| { let line_layout = - &layout.position_map.line_layouts[(row - start_row) as usize]; + &layout.position_map.line_layouts[(row - start_row) as usize].line; HighlightedRangeLine { start_x: if row == range.start.row() { content_origin.x() @@ -1282,7 +1331,7 @@ impl EditorElement { rows: Range, snapshot: &EditorSnapshot, cx: &ViewContext, - ) -> Vec { + ) -> Vec { if rows.start >= rows.end { return Vec::new(); } @@ -1317,6 +1366,10 @@ impl EditorElement { )], ) }) + .map(|line| LineWithInvisibles { + line, + invisibles: Vec::new(), + }) .collect() } else { let style = &self.style; @@ -1366,13 +1419,6 @@ impl EditorElement { } }); - let settings = cx.global::(); - let show_invisibles = settings - .editor_overrides - .show_invisibles - .or(settings.editor_defaults.show_invisibles) - .unwrap_or_default() - == settings::ShowInvisibles::All; layout_highlighted_chunks( chunks, &style.text, @@ -1380,7 +1426,6 @@ impl EditorElement { cx.font_cache(), MAX_LINE_LEN, rows.len() as usize, - show_invisibles, ) } } @@ -1398,7 +1443,7 @@ impl EditorElement { text_x: f32, line_height: f32, style: &EditorStyle, - line_layouts: &[text_layout::Line], + line_layouts: &[LineWithInvisibles], include_root: bool, editor: &mut Editor, cx: &mut LayoutContext, @@ -1421,6 +1466,7 @@ impl EditorElement { let anchor_x = text_x + if rows.contains(&align_to.row()) { line_layouts[(align_to.row() - rows.start) as usize] + .line .x_for_index(align_to.column() as usize) } else { layout_line(align_to.row(), snapshot, style, cx.text_layout_cache()) @@ -1599,6 +1645,93 @@ impl EditorElement { } } +struct HighlightedChunk<'a> { + chunk: &'a str, + style: Option, + is_tab: bool, +} + +pub struct LineWithInvisibles { + pub line: Line, + invisibles: Vec, +} + +fn layout_highlighted_chunks<'a>( + chunks: impl Iterator>, + text_style: &TextStyle, + text_layout_cache: &TextLayoutCache, + font_cache: &Arc, + max_line_len: usize, + max_line_count: usize, +) -> Vec { + let mut layouts = Vec::with_capacity(max_line_count); + let mut line = String::new(); + let mut invisibles = Vec::new(); + let mut styles = Vec::new(); + let mut row = 0; + let mut line_exceeded_max_len = false; + for highlighted_chunk in chunks.chain([HighlightedChunk { + chunk: "\n", + style: None, + is_tab: false, + }]) { + for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() { + if ix > 0 { + layouts.push(LineWithInvisibles { + line: text_layout_cache.layout_str(&line, text_style.font_size, &styles), + invisibles: invisibles.drain(..).collect(), + }); + + line.clear(); + styles.clear(); + row += 1; + line_exceeded_max_len = false; + if row == max_line_count { + return layouts; + } + } + + if !line_chunk.is_empty() && !line_exceeded_max_len { + let text_style = if let Some(style) = highlighted_chunk.style { + text_style + .clone() + .highlight(style, font_cache) + .map(Cow::Owned) + .unwrap_or_else(|_| Cow::Borrowed(text_style)) + } else { + Cow::Borrowed(text_style) + }; + + if line.len() + line_chunk.len() > max_line_len { + let mut chunk_len = max_line_len - line.len(); + while !line_chunk.is_char_boundary(chunk_len) { + chunk_len -= 1; + } + line_chunk = &line_chunk[..chunk_len]; + line_exceeded_max_len = true; + } + + styles.push(( + line_chunk.len(), + RunStyle { + font_id: text_style.font_id, + color: text_style.color, + underline: text_style.underline, + }, + )); + if highlighted_chunk.is_tab { + invisibles.push(Invisible::Tab { + line_start_offset: line.len(), + }); + } + line.push_str(line_chunk); + } + } + } + + layouts +} + impl Element for EditorElement { type LayoutState = LayoutState; type PaintState = (); @@ -1825,9 +1958,9 @@ impl Element for EditorElement { let mut max_visible_line_width = 0.0; let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx); - for line in &line_layouts { - if line.width() > max_visible_line_width { - max_visible_line_width = line.width(); + for line_with_invisibles in &line_layouts { + if line_with_invisibles.line.width() > max_visible_line_width { + max_visible_line_width = line_with_invisibles.line.width(); } } @@ -2087,10 +2220,11 @@ impl Element for EditorElement { return None; } - let line = layout + let line = &layout .position_map .line_layouts - .get((range_start.row() - start_row) as usize)?; + .get((range_start.row() - start_row) as usize)? + .line; let range_start_x = line.x_for_index(range_start.column() as usize); let range_start_y = range_start.row() as f32 * layout.position_map.line_height; Some(RectF::new( @@ -2149,13 +2283,13 @@ pub struct LayoutState { fold_indicators: Vec>>, } -pub struct PositionMap { +struct PositionMap { size: Vector2F, line_height: f32, scroll_max: Vector2F, em_width: f32, em_advance: f32, - line_layouts: Vec, + line_layouts: Vec, snapshot: EditorSnapshot, } @@ -2177,6 +2311,7 @@ impl PositionMap { let (column, x_overshoot) = if let Some(line) = self .line_layouts .get(row as usize - scroll_position.y() as usize) + .map(|line_with_spaces| &line_with_spaces.line) { if let Some(ix) = line.index_for_x(x) { (ix as u32, 0.0) @@ -2445,7 +2580,7 @@ impl HighlightedRange { } } -pub fn position_to_display_point( +fn position_to_display_point( position: Vector2F, text_bounds: RectF, position_map: &PositionMap, @@ -2462,7 +2597,7 @@ pub fn position_to_display_point( } } -pub fn range_to_bounds( +fn range_to_bounds( range: &Range, content_origin: Vector2F, scroll_left: f32, @@ -2490,7 +2625,7 @@ pub fn range_to_bounds( content_origin.y() + row_range.start as f32 * position_map.line_height - scroll_top; for (idx, row) in row_range.enumerate() { - let line_layout = &position_map.line_layouts[(row - start_row) as usize]; + let line_layout = &position_map.line_layouts[(row - start_row) as usize].line; let start_x = if row == range.start.row() { content_origin.x() + line_layout.x_for_index(range.start.column() as usize) diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index 63ee7c56ca..ed098293f6 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -1,9 +1,9 @@ use std::cmp; -use gpui::{text_layout, ViewContext}; +use gpui::ViewContext; use language::Point; -use crate::{display_map::ToDisplayPoint, Editor, EditorMode}; +use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles}; #[derive(PartialEq, Eq)] pub enum Autoscroll { @@ -172,7 +172,7 @@ impl Editor { viewport_width: f32, scroll_width: f32, max_glyph_width: f32, - layouts: &[text_layout::Line], + layouts: &[LineWithInvisibles], cx: &mut ViewContext, ) -> bool { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -194,10 +194,13 @@ impl Editor { let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3); target_left = target_left.min( layouts[(head.row() - start_row) as usize] + .line .x_for_index(start_column as usize), ); target_right = target_right.max( - layouts[(head.row() - start_row) as usize].x_for_index(end_column as usize) + layouts[(head.row() - start_row) as usize] + .line + .x_for_index(end_column as usize) + max_glyph_width, ); } diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 5f7d1239ef..66654fbe93 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -6,7 +6,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{ToJson, Value}, - text_layout::{Invisible, Line, RunStyle, ShapedBoundary}, + text_layout::{Line, RunStyle, ShapedBoundary}, AppContext, Element, FontCache, LayoutContext, SceneBuilder, SizeConstraint, TextLayoutCache, View, ViewContext, }; @@ -114,11 +114,7 @@ impl Element for Text { } else { result = None; } - result.map(|(chunk, style)| HighlightedChunk { - chunk, - style, - is_tab: false, - }) + result }); // Perform shaping on these highlighted chunks @@ -129,7 +125,6 @@ impl Element for Text { &cx.font_cache, usize::MAX, self.text.matches('\n').count() + 1, - false, ); // If line wrapping is enabled, wrap each of the shaped lines. @@ -342,45 +337,24 @@ impl Element for Text { } } -pub struct HighlightedChunk<'a> { - pub chunk: &'a str, - pub style: Option, - pub is_tab: bool, -} - -impl<'a> HighlightedChunk<'a> { - fn plain_str(str_symbols: &'a str) -> Self { - Self { - chunk: str_symbols, - style: None, - is_tab: str_symbols == "\t", - } - } -} - /// Perform text layout on a series of highlighted chunks of text. -pub fn layout_highlighted_chunks<'a>( - chunks: impl Iterator>, +fn layout_highlighted_chunks<'a>( + chunks: impl Iterator)>, text_style: &TextStyle, text_layout_cache: &TextLayoutCache, font_cache: &Arc, max_line_len: usize, max_line_count: usize, - show_invisibles: bool, ) -> Vec { let mut layouts = Vec::with_capacity(max_line_count); let mut line = String::new(); - let mut invisibles = Vec::new(); let mut styles = Vec::new(); let mut row = 0; let mut line_exceeded_max_len = false; - for highlighted_chunk in chunks.chain(std::iter::once(HighlightedChunk::plain_str("\n"))) { - for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() { + for (chunk, highlight_style) in chunks.chain([("\n", Default::default())]) { + for (ix, mut line_chunk) in chunk.split('\n').enumerate() { if ix > 0 { - let mut laid_out_line = - text_layout_cache.layout_str(&line, text_style.font_size, &styles); - laid_out_line.invisibles.extend(invisibles.drain(..)); - layouts.push(laid_out_line); + layouts.push(text_layout_cache.layout_str(&line, text_style.font_size, &styles)); line.clear(); styles.clear(); row += 1; @@ -391,7 +365,7 @@ pub fn layout_highlighted_chunks<'a>( } if !line_chunk.is_empty() && !line_exceeded_max_len { - let text_style = if let Some(style) = highlighted_chunk.style { + let text_style = if let Some(style) = highlight_style { text_style .clone() .highlight(style, font_cache) @@ -410,6 +384,7 @@ pub fn layout_highlighted_chunks<'a>( line_exceeded_max_len = true; } + line.push_str(line_chunk); styles.push(( line_chunk.len(), RunStyle { @@ -418,12 +393,6 @@ pub fn layout_highlighted_chunks<'a>( underline: text_style.underline, }, )); - if show_invisibles && highlighted_chunk.is_tab { - invisibles.push(Invisible::Tab { - range: line.len()..line.len() + line_chunk.len(), - }); - } - line.push_str(line_chunk); } } } diff --git a/crates/gpui/src/text_layout.rs b/crates/gpui/src/text_layout.rs index 9e3f21a084..549f7e6d95 100644 --- a/crates/gpui/src/text_layout.rs +++ b/crates/gpui/src/text_layout.rs @@ -11,7 +11,6 @@ use crate::{ window::WindowContext, SceneBuilder, }; -use itertools::Itertools; use ordered_float::OrderedFloat; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use smallvec::SmallVec; @@ -179,7 +178,6 @@ impl<'a> Hash for CacheKeyRef<'a> { pub struct Line { layout: Arc, style_runs: SmallVec<[StyleRun; 32]>, - pub invisibles: SmallVec<[Invisible; 32]>, } #[derive(Debug, Clone, Copy)] @@ -215,8 +213,8 @@ pub struct Glyph { #[derive(Debug, Clone)] pub enum Invisible { - Tab { range: std::ops::Range }, - Whitespace { range: std::ops::Range }, + Tab { line_start_offset: usize }, + Whitespace { line_range: std::ops::Range }, } impl Line { @@ -229,11 +227,7 @@ impl Line { underline: style.underline, }); } - Self { - layout, - style_runs, - invisibles: SmallVec::new(), - } + Self { layout, style_runs } } pub fn runs(&self) -> &[Run] { @@ -310,16 +304,6 @@ impl Line { let mut color = Color::black(); let mut underline = None; - let tab_ranges = self - .invisibles - .iter() - .filter_map(|invisible| match invisible { - Invisible::Tab { range } => Some(range), - Invisible::Whitespace { .. } => None, - }) - .sorted_by(|tab_range_1, tab_range_2| tab_range_1.start.cmp(&tab_range_2.start)) - .collect::>(); - for run in &self.layout.runs { let max_glyph_width = cx .font_cache @@ -386,19 +370,10 @@ impl Line { origin: glyph_origin, }); } else { - let id = if tab_ranges.iter().any(|tab_range| { - tab_range.start <= glyph.index && glyph.index < tab_range.end - }) { - // TODO kb get a proper (cached) glyph - glyph.id + 100 - } else { - glyph.id - }; - scene.push_glyph(scene::Glyph { font_id: run.font_id, font_size: self.layout.font_size, - id, + id: glyph.id, origin: glyph_origin, color, });