Draw tabs with svg icons in editor code only

This commit is contained in:
Kirill Bulatov 2023-05-04 17:37:00 +03:00 committed by Kirill Bulatov
parent f0a88b3337
commit 2d8c88ad73
4 changed files with 186 additions and 104 deletions

View file

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

View file

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

View file

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

View file

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