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::{
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::<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() {
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<u32>,
snapshot: &EditorSnapshot,
cx: &ViewContext<Editor>,
) -> Vec<text_layout::Line> {
) -> Vec<LineWithInvisibles> {
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::<Settings>();
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<Editor>,
@ -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<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 {
type LayoutState = LayoutState;
type PaintState = ();
@ -1825,9 +1958,9 @@ impl Element<Editor> 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<Editor> 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<Option<AnyElement<Editor>>>,
}
pub struct PositionMap {
struct PositionMap {
size: Vector2F,
line_height: f32,
scroll_max: Vector2F,
em_width: f32,
em_advance: f32,
line_layouts: Vec<text_layout::Line>,
line_layouts: Vec<LineWithInvisibles>,
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<DisplayPoint>,
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)

View file

@ -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<Self>,
) -> 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,
);
}

View file

@ -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<V: View> Element<V> 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<V: View> Element<V> 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<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.
pub fn layout_highlighted_chunks<'a>(
chunks: impl Iterator<Item = HighlightedChunk<'a>>,
fn layout_highlighted_chunks<'a>(
chunks: impl Iterator<Item = (&'a str, Option<HighlightStyle>)>,
text_style: &TextStyle,
text_layout_cache: &TextLayoutCache,
font_cache: &Arc<FontCache>,
max_line_len: usize,
max_line_count: usize,
show_invisibles: bool,
) -> Vec<Line> {
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);
}
}
}

View file

@ -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<LineLayout>,
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<usize> },
Whitespace { range: std::ops::Range<usize> },
Tab { line_start_offset: usize },
Whitespace { line_range: std::ops::Range<usize> },
}
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::<Vec<_>>();
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,
});