use std::{ops::Range, rc::Rc};
use gpui::{
App, Bounds, Corners, Element, ElementId, ElementInputHandler, Entity, GlobalElementId, Half,
HighlightStyle, Hitbox, Hsla, IntoElement, LayoutId, MouseButton, MouseMoveEvent, Path, Pixels,
Point, ShapedLine, SharedString, Size, Style, TextRun, TextStyle, UnderlineStyle, Window, fill,
point, px, relative, size,
};
use ropey::Rope;
use smallvec::SmallVec;
use crate::{
ActiveTheme as _, Colorize, PixelsExt, Root,
input::{RopeExt as _, blink_cursor::CURSOR_WIDTH, text_wrapper::LineLayout},
};
use super::{InputState, LastLayout, mode::InputMode};
const BOTTOM_MARGIN_ROWS: usize = 3;
pub(super) const RIGHT_MARGIN: Pixels = px(10.);
pub(super) const LINE_NUMBER_RIGHT_MARGIN: Pixels = px(10.);
pub(super) struct TextElement {
pub(crate) state: Entity<InputState>,
placeholder: SharedString,
}
impl TextElement {
pub(super) fn new(state: Entity<InputState>) -> Self {
Self {
state,
placeholder: SharedString::default(),
}
}
pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
self.placeholder = placeholder.into();
self
}
fn paint_mouse_listeners(&mut self, window: &mut Window, _: &mut App) {
window.on_mouse_event({
let state = self.state.clone();
move |event: &MouseMoveEvent, _, window, cx| {
if event.pressed_button == Some(MouseButton::Left) {
state.update(cx, |state, cx| {
state.on_drag_move(event, window, cx);
});
}
}
});
}
fn layout_cursor(
&self,
last_layout: &LastLayout,
bounds: &mut Bounds<Pixels>,
_: &mut Window,
cx: &mut App,
) -> (Option<Bounds<Pixels>>, Point<Pixels>, Option<usize>) {
let state = self.state.read(cx);
let line_height = last_layout.line_height;
let visible_range = &last_layout.visible_range;
let lines = &last_layout.lines;
let text_wrapper = &state.text_wrapper;
let line_number_width = last_layout.line_number_width;
let mut selected_range = state.selected_range;
if let Some(ime_marked_range) = &state.ime_marked_range {
selected_range = (ime_marked_range.end..ime_marked_range.end).into();
}
let is_selected_all = selected_range.len() == state.text.len();
let mut cursor = state.cursor();
if state.masked {
selected_range.start = state.text.offset_to_char_index(selected_range.start);
selected_range.end = state.text.offset_to_char_index(selected_range.end);
cursor = state.text.offset_to_char_index(cursor);
}
let mut current_row = None;
let mut scroll_offset = state.scroll_handle.offset();
let mut cursor_bounds = None;
let top_bottom_margin = if state.mode.is_auto_grow() {
line_height
} else if visible_range.len() < BOTTOM_MARGIN_ROWS * 8 {
line_height
} else {
BOTTOM_MARGIN_ROWS * line_height
};
let mut cursor_pos = None;
let mut cursor_start = None;
let mut cursor_end = None;
let mut prev_lines_offset = 0;
let mut offset_y = px(0.);
for (ix, wrap_line) in text_wrapper.lines.iter().enumerate() {
let row = ix;
let line_origin = point(px(0.), offset_y);
if cursor_pos.is_some() && cursor_start.is_some() && cursor_end.is_some() {
break;
}
let in_visible_range = ix >= visible_range.start;
if let Some(line) = in_visible_range
.then(|| lines.get(ix.saturating_sub(visible_range.start)))
.flatten()
{
if cursor_pos.is_none() {
let offset = cursor.saturating_sub(prev_lines_offset);
if let Some(pos) = line.position_for_index(offset, line_height) {
current_row = Some(row);
cursor_pos = Some(line_origin + pos);
}
}
if cursor_start.is_none() {
let offset = selected_range.start.saturating_sub(prev_lines_offset);
if let Some(pos) = line.position_for_index(offset, line_height) {
cursor_start = Some(line_origin + pos);
}
}
if cursor_end.is_none() {
let offset = selected_range.end.saturating_sub(prev_lines_offset);
if let Some(pos) = line.position_for_index(offset, line_height) {
cursor_end = Some(line_origin + pos);
}
}
offset_y += line.size(line_height).height;
prev_lines_offset += line.len() + 1;
} else {
if prev_lines_offset >= cursor && cursor_pos.is_none() {
current_row = Some(row);
cursor_pos = Some(line_origin);
}
if prev_lines_offset >= selected_range.start && cursor_start.is_none() {
cursor_start = Some(line_origin);
}
if prev_lines_offset >= selected_range.end && cursor_end.is_none() {
cursor_end = Some(line_origin);
}
offset_y += wrap_line.height(line_height);
prev_lines_offset += wrap_line.len() + 1;
}
}
if let (Some(cursor_pos), Some(cursor_start), Some(cursor_end)) =
(cursor_pos, cursor_start, cursor_end)
{
let selection_changed = state.last_selected_range != Some(selected_range);
if selection_changed && !is_selected_all {
scroll_offset.x = if scroll_offset.x + cursor_pos.x
> (bounds.size.width - line_number_width - RIGHT_MARGIN)
{
bounds.size.width - line_number_width - RIGHT_MARGIN - cursor_pos.x
} else if scroll_offset.x + cursor_pos.x < px(0.) {
scroll_offset.x - cursor_pos.x
} else {
scroll_offset.x
};
scroll_offset.y =
if scroll_offset.y + cursor_pos.y > bounds.size.height - top_bottom_margin {
scroll_offset.y - line_height
} else if scroll_offset.y + cursor_pos.y < top_bottom_margin {
(scroll_offset.y + line_height).min(px(0.))
} else {
scroll_offset.y
};
if state.selection_reversed {
if scroll_offset.x + cursor_start.x < px(0.) {
scroll_offset.x = -cursor_start.x;
}
if scroll_offset.y + cursor_start.y < px(0.) {
scroll_offset.y = -cursor_start.y;
}
} else {
if scroll_offset.x + cursor_end.x <= px(0.) {
scroll_offset.x = -cursor_end.x;
}
if scroll_offset.y + cursor_end.y <= px(0.) {
scroll_offset.y = -cursor_end.y;
}
}
}
let cursor_height = match state.size {
crate::Size::Large => 1.,
crate::Size::Small => 0.75,
_ => 0.85,
} * line_height;
cursor_bounds = Some(Bounds::new(
point(
bounds.left() + cursor_pos.x + line_number_width + scroll_offset.x,
bounds.top() + cursor_pos.y + ((line_height - cursor_height) / 2.),
),
size(CURSOR_WIDTH, cursor_height),
));
}
if let Some(deferred_scroll_offset) = state.deferred_scroll_offset {
scroll_offset = deferred_scroll_offset;
}
bounds.origin = bounds.origin + scroll_offset;
(cursor_bounds, scroll_offset, current_row)
}
pub(crate) fn layout_match_range(
range: Range<usize>,
last_layout: &LastLayout,
bounds: &Bounds<Pixels>,
) -> Option<Path<Pixels>> {
if range.is_empty() {
return None;
}
if range.start < last_layout.visible_range_offset.start
|| range.end > last_layout.visible_range_offset.end
{
return None;
}
let line_height = last_layout.line_height;
let visible_top = last_layout.visible_top;
let visible_start_offset = last_layout.visible_range_offset.start;
let lines = &last_layout.lines;
let line_number_width = last_layout.line_number_width;
let start_ix = range.start;
let end_ix = range.end;
let mut prev_lines_offset = visible_start_offset;
let mut offset_y = visible_top;
let mut line_corners = vec![];
for line in lines.iter() {
let line_size = line.size(line_height);
let line_wrap_width = line_size.width;
let line_origin = point(px(0.), offset_y);
let line_cursor_start =
line.position_for_index(start_ix.saturating_sub(prev_lines_offset), line_height);
let line_cursor_end =
line.position_for_index(end_ix.saturating_sub(prev_lines_offset), line_height);
if line_cursor_start.is_some() || line_cursor_end.is_some() {
let start = line_cursor_start
.unwrap_or_else(|| line.position_for_index(0, line_height).unwrap());
let end = line_cursor_end
.unwrap_or_else(|| line.position_for_index(line.len(), line_height).unwrap());
let wrapped_lines =
(end.y / line_height).ceil() as usize - (start.y / line_height).ceil() as usize;
let mut end_x = end.x;
if wrapped_lines > 0 {
end_x = line_wrap_width;
}
end_x = end_x.max(start.x + px(6.));
line_corners.push(Corners {
top_left: line_origin + point(start.x, start.y),
top_right: line_origin + point(end_x, start.y),
bottom_left: line_origin + point(start.x, start.y + line_height),
bottom_right: line_origin + point(end_x, start.y + line_height),
});
for i in 1..=wrapped_lines {
let start = point(px(0.), start.y + i as f32 * line_height);
let mut end = point(end.x, end.y + i as f32 * line_height);
if i < wrapped_lines {
end.x = line_size.width;
}
line_corners.push(Corners {
top_left: line_origin + point(start.x, start.y),
top_right: line_origin + point(end.x, start.y),
bottom_left: line_origin + point(start.x, start.y + line_height),
bottom_right: line_origin + point(end.x, start.y + line_height),
});
}
}
if line_cursor_start.is_some() && line_cursor_end.is_some() {
break;
}
offset_y += line_size.height;
prev_lines_offset += line.len() + 1;
}
let mut points = vec![];
if line_corners.is_empty() {
return None;
}
for corners in &mut line_corners {
if corners.top_left.x > corners.top_right.x {
std::mem::swap(&mut corners.top_left, &mut corners.top_right);
std::mem::swap(&mut corners.bottom_left, &mut corners.bottom_right);
}
}
for corners in &line_corners {
points.push(corners.top_right);
points.push(corners.bottom_right);
points.push(corners.bottom_left);
}
let mut rev_line_corners = line_corners.iter().rev().peekable();
while let Some(corners) = rev_line_corners.next() {
points.push(corners.top_left);
if let Some(next) = rev_line_corners.peek() {
if next.top_left.x > corners.top_left.x {
points.push(point(next.top_left.x, corners.top_left.y));
}
}
}
let path_origin = bounds.origin + point(line_number_width, px(0.));
let first_p = *points.get(0).unwrap();
let mut builder = gpui::PathBuilder::fill();
builder.move_to(path_origin + first_p);
for p in points.iter().skip(1) {
builder.line_to(path_origin + *p);
}
builder.build().ok()
}
fn layout_search_matches(
&self,
last_layout: &LastLayout,
bounds: &Bounds<Pixels>,
cx: &mut App,
) -> Vec<(Path<Pixels>, bool)> {
let search_panel = self.state.read(cx).search_panel.clone();
let Some((ranges, current_match_ix)) = search_panel.and_then(|panel| {
if let Some(matcher) = panel.read(cx).matcher() {
Some((matcher.matched_ranges.clone(), matcher.current_match_ix))
} else {
None
}
}) else {
return vec![];
};
let mut paths = Vec::new();
for (index, range) in ranges.as_ref().iter().enumerate() {
if let Some(path) = Self::layout_match_range(range.clone(), last_layout, bounds) {
paths.push((path, current_match_ix == index));
}
}
paths
}
fn layout_hover_highlight(
&self,
last_layout: &LastLayout,
bounds: &Bounds<Pixels>,
cx: &mut App,
) -> Option<Path<Pixels>> {
let hover_popover = self.state.read(cx).hover_popover.clone();
let Some(symbol_range) = hover_popover.map(|popover| popover.read(cx).symbol_range.clone())
else {
return None;
};
Self::layout_match_range(symbol_range, last_layout, bounds)
}
fn layout_document_colors(
&self,
document_colors: &[(Range<usize>, Hsla)],
last_layout: &LastLayout,
bounds: &Bounds<Pixels>,
) -> Vec<(Path<Pixels>, Hsla)> {
let mut paths = vec![];
for (range, color) in document_colors.iter() {
if let Some(path) = Self::layout_match_range(range.clone(), last_layout, bounds) {
paths.push((path, *color));
}
}
paths
}
fn layout_selections(
&self,
last_layout: &LastLayout,
bounds: &mut Bounds<Pixels>,
cx: &mut App,
) -> Option<Path<Pixels>> {
let state = self.state.read(cx);
let mut selected_range = state.selected_range;
if let Some(ime_marked_range) = &state.ime_marked_range {
if !ime_marked_range.is_empty() {
selected_range = (ime_marked_range.end..ime_marked_range.end).into();
}
}
if selected_range.is_empty() {
return None;
}
if state.masked {
selected_range.start = state.text.offset_to_char_index(selected_range.start);
selected_range.end = state.text.offset_to_char_index(selected_range.end);
}
let (start_ix, end_ix) = if selected_range.start < selected_range.end {
(selected_range.start, selected_range.end)
} else {
(selected_range.end, selected_range.start)
};
let range = start_ix.max(last_layout.visible_range_offset.start)
..end_ix.min(last_layout.visible_range_offset.end);
Self::layout_match_range(range, &last_layout, bounds)
}
fn calculate_visible_range(
&self,
state: &InputState,
line_height: Pixels,
input_height: Pixels,
) -> (Range<usize>, Pixels) {
let extra_rows = 1;
let mut visible_top = px(0.);
if state.mode.is_single_line() {
return (0..1, visible_top);
}
let total_lines = state.text_wrapper.len();
let scroll_top = if let Some(deferred_scroll_offset) = state.deferred_scroll_offset {
deferred_scroll_offset.y
} else {
state.scroll_handle.offset().y
};
let mut visible_range = 0..total_lines;
let mut line_bottom = px(0.);
for (ix, line) in state.text_wrapper.lines.iter().enumerate() {
let wrapped_height = line.height(line_height);
line_bottom += wrapped_height;
if line_bottom < -scroll_top {
visible_top = line_bottom - wrapped_height;
visible_range.start = ix;
}
if line_bottom + scroll_top >= input_height {
visible_range.end = (ix + extra_rows).min(total_lines);
break;
}
}
(visible_range, visible_top)
}
fn layout_line_numbers(
state: &InputState,
text: &Rope,
font_size: Pixels,
style: &TextStyle,
window: &mut Window,
) -> (Pixels, usize) {
let total_lines = text.lines_len();
let line_number_len = match total_lines {
0..=9999 => 5,
10000..=99999 => 6,
100000..=999999 => 7,
_ => 8,
};
let line_number_width = if state.mode.line_number() {
let empty_line_number = window.text_system().shape_line(
"+".repeat(line_number_len).into(),
font_size,
&[TextRun {
len: line_number_len,
font: style.font(),
color: gpui::black(),
background_color: None,
underline: None,
strikethrough: None,
}],
None,
);
empty_line_number.width + px(6.) + LINE_NUMBER_RIGHT_MARGIN
} else {
px(0.)
};
(line_number_width, line_number_len)
}
fn layout_inline_completion(
state: &InputState,
visible_range: &Range<usize>,
font_size: Pixels,
window: &mut Window,
cx: &App,
) -> (Option<ShapedLine>, Vec<ShapedLine>) {
if !state.focus_handle.is_focused(window) {
return (None, vec![]);
}
let Some(completion_item) = state.inline_completion.item.as_ref() else {
return (None, vec![]);
};
let cursor_row = state.cursor_position().line as usize;
if cursor_row < visible_range.start || cursor_row >= visible_range.end {
return (None, vec![]);
}
let completion_text = &completion_item.insert_text;
let completion_color = cx.theme().muted_foreground.opacity(0.5);
let text_style = window.text_style();
let font = text_style.font();
let lines: Vec<&str> = completion_text.split('\n').collect();
if lines.is_empty() {
return (None, vec![]);
}
let first_text: SharedString = lines[0].to_string().into();
let first_line = if !first_text.is_empty() {
let first_run = TextRun {
len: first_text.len(),
font: font.clone(),
color: completion_color,
background_color: None,
underline: None,
strikethrough: None,
};
Some(
window
.text_system()
.shape_line(first_text, font_size, &[first_run], None),
)
} else {
None
};
let ghost_lines: Vec<ShapedLine> = lines[1..]
.iter()
.map(|line_text| {
let text: SharedString = line_text.to_string().into();
let len = text.len().max(1); let run = TextRun {
len,
font: font.clone(),
color: completion_color,
background_color: None,
underline: None,
strikethrough: None,
};
let shaped_text = if text.is_empty() { " ".into() } else { text };
window
.text_system()
.shape_line(shaped_text, font_size, &[run], None)
})
.collect();
(first_line, ghost_lines)
}
fn layout_lines(
state: &InputState,
display_text: &Rope,
last_layout: &LastLayout,
font_size: Pixels,
runs: &[TextRun],
bg_segments: &[(Range<usize>, Hsla)],
window: &mut Window,
) -> Vec<LineLayout> {
let is_single_line = state.mode.is_single_line();
let text_wrapper = &state.text_wrapper;
let visible_range = &last_layout.visible_range;
let visible_range_offset = &last_layout.visible_range_offset;
if is_single_line {
let shaped_line = window.text_system().shape_line(
display_text.to_string().into(),
font_size,
&runs,
None,
);
return vec![LineLayout::new().lines(smallvec::smallvec![shaped_line])];
}
if state.text.len() == 0 {
return display_text
.to_string()
.split("\n")
.map(|line| {
let shaped_line = window.text_system().shape_line(
line.to_string().into(),
font_size,
&runs,
None,
);
LineLayout::new().lines(smallvec::smallvec![shaped_line])
})
.collect();
}
let visible_text = display_text
.slice_lines(visible_range.start..visible_range.end)
.to_string();
let mut lines = vec![];
let mut offset = 0;
for (ix, line) in visible_text.split("\n").enumerate() {
let line_item = text_wrapper
.lines
.get(visible_range.start + ix)
.expect("line should exists in text_wrapper");
debug_assert_eq!(line_item.len(), line.len());
let mut line_layout = LineLayout::new();
let mut wrapped_lines = SmallVec::with_capacity(1);
for range in &line_item.wrapped_lines {
let line_runs = runs_for_range(runs, offset, &range);
let line_runs = if bg_segments.is_empty() {
line_runs
} else {
split_runs_by_bg_segments(
visible_range_offset.start + offset,
&line_runs,
bg_segments,
)
};
let sub_line: SharedString = line[range.clone()].to_string().into();
let shaped_line = window
.text_system()
.shape_line(sub_line, font_size, &line_runs, None);
wrapped_lines.push(shaped_line);
}
line_layout.set_wrapped_lines(wrapped_lines);
lines.push(line_layout);
offset += line.len() + 1;
}
lines
}
fn highlight_lines(
&mut self,
visible_range: &Range<usize>,
_visible_top: Pixels,
visible_byte_range: Range<usize>,
cx: &mut App,
) -> Option<Vec<(Range<usize>, HighlightStyle)>> {
let state = self.state.read(cx);
let text = &state.text;
let is_multi_line = state.mode.is_multi_line();
let (highlighter, diagnostics) = match &state.mode {
InputMode::CodeEditor {
highlighter,
diagnostics,
..
} => (highlighter.borrow(), diagnostics),
_ => return None,
};
let highlighter = highlighter.as_ref()?;
let mut offset = visible_byte_range.start;
let mut styles = vec![];
for line in text
.iter_lines()
.skip(visible_range.start)
.take(visible_range.len())
{
let line_len = if is_multi_line {
line.len() + 1
} else {
line.len()
};
let range = offset..offset + line_len;
let line_styles = highlighter.styles(&range, &cx.theme().highlight_theme);
styles = gpui::combine_highlights(styles, line_styles).collect();
offset = range.end;
}
let diagnostic_styles = diagnostics.styles_for_range(&visible_byte_range, cx);
if let Some(hover_style) = self.layout_hover_definition(cx) {
styles.push(hover_style);
}
styles = gpui::combine_highlights(diagnostic_styles, styles).collect();
Some(styles)
}
}
pub(super) struct PrepaintState {
last_layout: LastLayout,
line_numbers: Option<Vec<SmallVec<[ShapedLine; 1]>>>,
scroll_size: Size<Pixels>,
cursor_bounds: Option<Bounds<Pixels>>,
cursor_scroll_offset: Point<Pixels>,
current_row: Option<usize>,
selection_path: Option<Path<Pixels>>,
hover_highlight_path: Option<Path<Pixels>>,
search_match_paths: Vec<(Path<Pixels>, bool)>,
document_color_paths: Vec<(Path<Pixels>, Hsla)>,
hover_definition_hitbox: Option<Hitbox>,
indent_guides_path: Option<Path<Pixels>>,
bounds: Bounds<Pixels>,
ghost_lines: Vec<ShapedLine>,
ghost_first_line: Option<ShapedLine>,
ghost_lines_height: Pixels,
}
impl PrepaintState {
fn cursor_bounds_with_scroll(&self) -> Option<Bounds<Pixels>> {
self.cursor_bounds.map(|mut bounds| {
bounds.origin.y += self.cursor_scroll_offset.y;
bounds
})
}
}
impl IntoElement for TextElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
#[allow(unused)]
fn print_points_as_svg_path(
line_corners: &Vec<Corners<Point<Pixels>>>,
points: &Vec<Point<Pixels>>,
) {
for corners in line_corners {
println!(
"tl: ({}, {}), tr: ({}, {}), bl: ({}, {}), br: ({}, {})",
corners.top_left.x.as_f32() as i32,
corners.top_left.y.as_f32() as i32,
corners.top_right.x.as_f32() as i32,
corners.top_right.y.as_f32() as i32,
corners.bottom_left.x.as_f32() as i32,
corners.bottom_left.y.as_f32() as i32,
corners.bottom_right.x.as_f32() as i32,
corners.bottom_right.y.as_f32() as i32,
);
}
if points.len() > 0 {
println!(
"M{},{}",
points[0].x.as_f32() as i32,
points[0].y.as_f32() as i32
);
for p in points.iter().skip(1) {
println!("L{},{}", p.x.as_f32() as i32, p.y.as_f32() as i32);
}
}
}
impl Element for TextElement {
type RequestLayoutState = ();
type PrepaintState = PrepaintState;
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_: Option<&gpui::InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let state = self.state.read(cx);
let line_height = window.line_height();
let mut style = Style::default();
style.size.width = relative(1.).into();
if state.mode.is_multi_line() {
style.flex_grow = 1.0;
style.size.height = relative(1.).into();
if state.mode.is_auto_grow() {
let rows = state.mode.max_rows().min(state.mode.rows());
style.min_size.height = (rows * line_height).into();
} else {
style.min_size.height = line_height.into();
}
} else {
style.size.height = line_height.into();
};
(window.request_layout(style, [], cx), ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_: Option<&gpui::InspectorElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
let style = window.text_style();
let font = style.font();
let text_size = style.font_size.to_pixels(window.rem_size());
self.state.update(cx, |state, cx| {
state.text_wrapper.set_font(font, text_size, cx);
state.text_wrapper.prepare_if_need(&state.text, cx);
});
let state = self.state.read(cx);
let line_height = window.line_height();
let (visible_range, visible_top) =
self.calculate_visible_range(&state, line_height, bounds.size.height);
let visible_start_offset = state.text.line_start_offset(visible_range.start);
let visible_end_offset = state
.text
.line_end_offset(visible_range.end.saturating_sub(1));
let highlight_styles = self.highlight_lines(
&visible_range,
visible_top,
visible_start_offset..visible_end_offset,
cx,
);
let state = self.state.read(cx);
let multi_line = state.mode.is_multi_line();
let text = state.text.clone();
let is_empty = text.len() == 0;
let placeholder = self.placeholder.clone();
let mut bounds = bounds;
let (display_text, text_color) = if is_empty {
(
&Rope::from(placeholder.as_str()),
cx.theme().muted_foreground,
)
} else if state.masked {
(
&Rope::from("*".repeat(text.chars().count())),
cx.theme().foreground,
)
} else {
(&text, cx.theme().foreground)
};
let text_style = window.text_style();
let (line_number_width, line_number_len) =
Self::layout_line_numbers(&state, &text, text_size, &text_style, window);
let wrap_width = if multi_line && state.soft_wrap {
Some(bounds.size.width - line_number_width - RIGHT_MARGIN)
} else {
None
};
let mut last_layout = LastLayout {
visible_range,
visible_top,
visible_range_offset: visible_start_offset..visible_end_offset,
line_height,
wrap_width,
line_number_width,
lines: Rc::new(vec![]),
cursor_bounds: None,
};
let run = TextRun {
len: display_text.len(),
font: style.font(),
color: text_color,
background_color: None,
underline: None,
strikethrough: None,
};
let marked_run = TextRun {
len: 0,
font: style.font(),
color: text_color,
background_color: None,
underline: Some(UnderlineStyle {
thickness: px(1.),
color: Some(text_color),
wavy: false,
}),
strikethrough: None,
};
let runs = if !is_empty {
if let Some(highlight_styles) = highlight_styles {
let mut runs = vec![];
runs.extend(highlight_styles.iter().map(|(range, style)| {
let mut run = text_style.clone().highlight(*style).to_run(range.len());
if let Some(ime_marked_range) = &state.ime_marked_range {
if range.start >= ime_marked_range.start
&& range.end <= ime_marked_range.end
{
run.color = marked_run.color;
run.strikethrough = marked_run.strikethrough;
run.underline = marked_run.underline;
}
}
run
}));
runs.into_iter().filter(|run| run.len > 0).collect()
} else {
vec![run]
}
} else if let Some(ime_marked_range) = &state.ime_marked_range {
vec![
TextRun {
len: ime_marked_range.start,
..run.clone()
},
TextRun {
len: ime_marked_range.end - ime_marked_range.start,
underline: marked_run.underline,
..run.clone()
},
TextRun {
len: display_text.len() - ime_marked_range.end,
..run.clone()
},
]
.into_iter()
.filter(|run| run.len > 0)
.collect()
} else {
vec![run]
};
let document_colors = state
.lsp
.document_colors_for_range(&text, &last_layout.visible_range);
let lines = Self::layout_lines(
&state,
&display_text,
&last_layout,
text_size,
&runs,
&document_colors,
window,
);
let mut longest_line_width = wrap_width.unwrap_or(px(0.));
if state.mode.is_single_line() || !state.soft_wrap {
let longest_row = state.text_wrapper.longest_row.row;
let longest_line: SharedString = state.text.slice_line(longest_row).to_string().into();
longest_line_width = window
.text_system()
.shape_line(
longest_line.clone(),
text_size,
&[TextRun {
len: longest_line.len(),
font: style.font(),
color: gpui::black(),
background_color: None,
underline: None,
strikethrough: None,
}],
wrap_width,
)
.width;
}
last_layout.lines = Rc::new(lines);
let (ghost_first_line, ghost_lines) = Self::layout_inline_completion(
state,
&last_layout.visible_range,
text_size,
window,
cx,
);
let ghost_line_count = ghost_lines.len();
let ghost_lines_height = ghost_line_count as f32 * line_height;
let total_wrapped_lines = state.text_wrapper.len();
let empty_bottom_height = if state.mode.is_code_editor() {
bounds
.size
.height
.half()
.max(BOTTOM_MARGIN_ROWS * line_height)
} else {
px(0.)
};
let scroll_size = size(
if longest_line_width + line_number_width + RIGHT_MARGIN > bounds.size.width {
longest_line_width + line_number_width + RIGHT_MARGIN
} else {
longest_line_width
},
(total_wrapped_lines as f32 * line_height + empty_bottom_height + ghost_lines_height)
.max(bounds.size.height),
);
let (cursor_bounds, cursor_scroll_offset, current_row) =
self.layout_cursor(&last_layout, &mut bounds, window, cx);
last_layout.cursor_bounds = cursor_bounds;
let search_match_paths = self.layout_search_matches(&last_layout, &mut bounds, cx);
let selection_path = self.layout_selections(&last_layout, &mut bounds, cx);
let hover_highlight_path = self.layout_hover_highlight(&last_layout, &mut bounds, cx);
let document_color_paths =
self.layout_document_colors(&document_colors, &last_layout, &bounds);
let state = self.state.read(cx);
let line_numbers = if state.mode.line_number() {
let mut line_numbers = vec![];
let other_line_runs = vec![TextRun {
len: line_number_len,
font: style.font(),
color: cx.theme().muted_foreground,
background_color: None,
underline: None,
strikethrough: None,
}];
let current_line_runs = vec![TextRun {
len: line_number_len,
font: style.font(),
color: cx.theme().foreground,
background_color: None,
underline: None,
strikethrough: None,
}];
for (ix, line) in last_layout.lines.iter().enumerate() {
let ix = last_layout.visible_range.start + ix;
let line_no = format!("{:>width$}", ix + 1, width = line_number_len).into();
let runs = if current_row == Some(ix) {
¤t_line_runs
} else {
&other_line_runs
};
let mut sub_lines: SmallVec<[ShapedLine; 1]> = SmallVec::new();
sub_lines.push(
window
.text_system()
.shape_line(line_no, text_size, &runs, None),
);
for _ in 0..line.wrapped_lines.len().saturating_sub(1) {
sub_lines.push(ShapedLine::default());
}
line_numbers.push(sub_lines);
}
Some(line_numbers)
} else {
None
};
let hover_definition_hitbox = self.layout_hover_definition_hitbox(state, window, cx);
let indent_guides_path =
self.layout_indent_guides(state, &bounds, &last_layout, &text_style, window);
PrepaintState {
bounds,
last_layout,
scroll_size,
line_numbers,
cursor_bounds,
cursor_scroll_offset,
current_row,
selection_path,
search_match_paths,
hover_highlight_path,
hover_definition_hitbox,
document_color_paths,
indent_guides_path,
ghost_first_line,
ghost_lines,
ghost_lines_height,
}
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_: Option<&gpui::InspectorElementId>,
input_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
let focus_handle = self.state.read(cx).focus_handle.clone();
let show_cursor = self.state.read(cx).show_cursor(window, cx);
let focused = focus_handle.is_focused(window);
let bounds = prepaint.bounds;
let selected_range = self.state.read(cx).selected_range;
let visible_range = &prepaint.last_layout.visible_range;
window.handle_input(
&focus_handle,
ElementInputHandler::new(bounds, self.state.clone()),
cx,
);
if focused {
let state = self.state.clone();
if Root::read(window, cx).focused_input.as_ref() != Some(&state) {
Root::update(window, cx, |root, _, cx| {
root.focused_input = Some(state);
cx.notify();
});
}
}
window.on_next_frame({
let state = self.state.clone();
move |window, cx| {
if !focused && Root::read(window, cx).focused_input.as_ref() == Some(&state) {
Root::update(window, cx, |root, _, cx| {
root.focused_input = None;
cx.notify();
});
}
}
});
let line_height = window.line_height();
let origin = bounds.origin;
let invisible_top_padding = prepaint.last_layout.visible_top;
let mut mask_offset_y = px(0.);
let state = self.state.read(cx);
if state.masked && state.text.len() > 0 {
if cfg!(target_os = "macos") {
mask_offset_y = px(3.);
} else {
mask_offset_y = px(2.5);
}
}
let active_line_color = cx.theme().highlight_theme.style.editor_active_line;
let mut offset_y = px(0.);
if let Some(line_numbers) = prepaint.line_numbers.as_ref() {
offset_y += invisible_top_padding;
for (ix, lines) in line_numbers.iter().enumerate() {
let row = visible_range.start + ix;
let is_active = prepaint.current_row == Some(row);
let p = point(input_bounds.origin.x, origin.y + offset_y);
let height = line_height * lines.len() as f32;
if is_active {
if let Some(bg_color) = active_line_color {
window.paint_quad(fill(
Bounds::new(p, size(bounds.size.width, height)),
bg_color,
));
}
}
offset_y += height;
}
}
if let Some(path) = prepaint.indent_guides_path.take() {
window.paint_path(path, cx.theme().border.opacity(0.85));
}
if window.is_window_active() {
let secondary_selection = cx.theme().selection.saturation(0.1);
for (path, is_active) in prepaint.search_match_paths.iter() {
window.paint_path(path.clone(), secondary_selection);
if *is_active {
window.paint_path(path.clone(), cx.theme().selection);
}
}
if let Some(path) = prepaint.selection_path.take() {
window.paint_path(path, cx.theme().selection);
}
if let Some(path) = prepaint.hover_highlight_path.take() {
window.paint_path(path, secondary_selection);
}
}
for (path, color) in prepaint.document_color_paths.iter() {
window.paint_path(path.clone(), *color);
}
let mut offset_y = mask_offset_y + invisible_top_padding;
let ghost_lines = &prepaint.ghost_lines;
let has_ghost_lines = !ghost_lines.is_empty();
for (ix, line) in prepaint.last_layout.lines.iter().enumerate() {
let row = visible_range.start + ix;
let p = point(
origin.x + prepaint.last_layout.line_number_width,
origin.y + offset_y,
);
_ = line.paint(p, line_height, window, cx);
offset_y += line.size(line_height).height;
if has_ghost_lines && Some(row) == prepaint.current_row {
let ghost_x = origin.x + prepaint.last_layout.line_number_width;
for ghost_line in ghost_lines {
let ghost_p = point(ghost_x, origin.y + offset_y);
let ghost_bounds = Bounds::new(
ghost_p,
size(
bounds.size.width - prepaint.last_layout.line_number_width,
line_height,
),
);
window.paint_quad(fill(ghost_bounds, cx.theme().editor_background()));
_ = ghost_line.paint(ghost_p, line_height, window, cx);
offset_y += line_height;
}
}
}
if focused && show_cursor {
if let Some(cursor_bounds) = prepaint.cursor_bounds_with_scroll() {
window.paint_quad(fill(cursor_bounds, cx.theme().caret));
}
}
let mut offset_y = px(0.);
if let Some(line_numbers) = prepaint.line_numbers.as_ref() {
offset_y += invisible_top_padding;
window.paint_quad(fill(
Bounds {
origin: input_bounds.origin,
size: size(
prepaint.last_layout.line_number_width - LINE_NUMBER_RIGHT_MARGIN,
input_bounds.size.height + prepaint.ghost_lines_height,
),
},
cx.theme().editor_background(),
));
for (ix, lines) in line_numbers.iter().enumerate() {
let row = visible_range.start + ix;
let p = point(input_bounds.origin.x, origin.y + offset_y);
let is_active = prepaint.current_row == Some(row);
let height = line_height * lines.len() as f32;
if is_active {
if let Some(bg_color) = active_line_color {
window.paint_quad(fill(
Bounds::new(p, size(prepaint.last_layout.line_number_width, height)),
bg_color,
));
}
}
for line in lines {
_ = line.paint(p, line_height, window, cx);
offset_y += line_height;
}
if !prepaint.ghost_lines.is_empty() && prepaint.current_row.is_some() {
offset_y += prepaint.ghost_lines_height;
}
}
}
self.state.update(cx, |state, cx| {
state.last_layout = Some(prepaint.last_layout.clone());
state.last_bounds = Some(bounds);
state.last_cursor = Some(state.cursor());
state.set_input_bounds(input_bounds, cx);
state.last_selected_range = Some(selected_range);
state.scroll_size = prepaint.scroll_size;
state.update_scroll_offset(Some(prepaint.cursor_scroll_offset), cx);
state.deferred_scroll_offset = None;
cx.notify();
});
if let Some(hitbox) = prepaint.hover_definition_hitbox.as_ref() {
window.set_cursor_style(gpui::CursorStyle::PointingHand, &hitbox);
}
if focused {
if let Some(first_line) = &prepaint.ghost_first_line {
if let Some(cursor_bounds) = prepaint.cursor_bounds_with_scroll() {
let first_line_x = cursor_bounds.origin.x + cursor_bounds.size.width;
let p = point(first_line_x, cursor_bounds.origin.y);
let bg_bounds = Bounds::new(p, size(first_line.width + px(4.), line_height));
window.paint_quad(fill(bg_bounds, cx.theme().editor_background()));
_ = first_line.paint(p, line_height, window, cx);
}
}
}
self.paint_mouse_listeners(window, cx);
}
}
pub(super) fn runs_for_range(
runs: &[TextRun],
line_offset: usize,
range: &Range<usize>,
) -> Vec<TextRun> {
let mut result = vec![];
let range = (line_offset + range.start)..(line_offset + range.end);
let mut cursor = 0;
for run in runs {
let run_start = cursor;
let run_end = cursor + run.len;
if run_end <= range.start {
cursor = run_end;
continue;
}
if run_start >= range.end {
break;
}
let start = range.start.max(run_start) - run_start;
let end = range.end.min(run_end) - run_start;
let len = end - start;
if len > 0 {
result.push(TextRun { len, ..run.clone() });
}
cursor = run_end;
}
result
}
fn split_runs_by_bg_segments(
start_offset: usize,
runs: &[TextRun],
bg_segments: &[(Range<usize>, Hsla)],
) -> Vec<TextRun> {
let mut result = vec![];
let mut cursor = start_offset;
for run in runs {
let mut run_start = cursor;
let run_end = cursor + run.len;
for (bg_range, bg_color) in bg_segments {
if run_end <= bg_range.start || run_start >= bg_range.end {
continue;
}
if run_start < bg_range.start {
result.push(TextRun {
len: bg_range.start - run_start,
..run.clone()
});
}
let overlap_start = run_start.max(bg_range.start);
let overlap_end = run_end.min(bg_range.end);
let text_color = if bg_color.l >= 0.5 {
gpui::black()
} else {
gpui::white()
};
let run_len = overlap_end.saturating_sub(overlap_start);
if run_len > 0 {
result.push(TextRun {
len: run_len,
color: text_color,
..run.clone()
});
cursor = bg_range.end;
run_start = cursor;
}
}
if run_end > cursor {
result.push(TextRun {
len: run_end - cursor,
..run.clone()
});
}
cursor = run_end;
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_runs_for_range() {
let run = TextRun {
len: 0,
font: gpui::font(".SystemUIFont"),
color: gpui::black(),
background_color: None,
underline: None,
strikethrough: None,
};
let runs = vec![
TextRun {
len: 3,
..run.clone()
},
TextRun {
len: 1,
..run.clone()
},
TextRun {
len: 5,
..run.clone()
},
TextRun {
len: 1,
..run.clone()
},
TextRun {
len: 12,
..run.clone()
},
];
#[track_caller]
fn assert_runs(actual: Vec<TextRun>, expected: &[usize]) {
let left = actual.iter().map(|run| run.len).collect::<Vec<_>>();
assert_eq!(left, expected);
}
assert_runs(runs_for_range(&runs, 0, &(0..0)), &[]);
assert_runs(runs_for_range(&runs, 0, &(0..100)), &[3, 1, 5, 1, 12]);
assert_runs(runs_for_range(&runs, 0, &(0..6)), &[3, 1, 2]);
assert_runs(runs_for_range(&runs, 0, &(1..6)), &[2, 1, 2]);
assert_runs(runs_for_range(&runs, 0, &(3..10)), &[1, 5, 1]);
assert_runs(runs_for_range(&runs, 0, &(5..8)), &[3]);
assert_runs(runs_for_range(&runs, 3, &(0..3)), &[1, 2]);
assert_runs(runs_for_range(&runs, 3, &(2..10)), &[4, 1, 3]);
assert_runs(runs_for_range(&runs, 9, &(0..8)), &[1, 7]);
}
#[test]
fn test_split_runs_by_bg_segments() {
let run = TextRun {
len: 0,
font: gpui::font(".SystemUIFont"),
color: gpui::blue(),
background_color: None,
underline: None,
strikethrough: None,
};
let runs = vec![
TextRun {
len: 5,
..run.clone()
},
TextRun {
len: 7,
..run.clone()
},
TextRun {
len: 24,
..run.clone()
},
];
let bg_segments = vec![(8..12, gpui::red()), (12..18, gpui::blue())];
let result = split_runs_by_bg_segments(5, &runs, &bg_segments);
assert_eq!(
result.iter().map(|run| run.len).collect::<Vec<_>>(),
vec![3, 2, 2, 5, 1, 23]
);
assert_eq!(result[0].color, gpui::blue());
assert_eq!(result[1].color, gpui::black());
assert_eq!(result[2].color, gpui::black());
assert_eq!(result[3].color, gpui::black());
assert_eq!(result[4].color, gpui::black());
assert_eq!(result[5].color, gpui::blue());
}
}