* update deps * clean up * add account crate * add person crate * add chat and chat ui crates * . * clean up the ui crate * . * .
891 lines
31 KiB
Rust
891 lines
31 KiB
Rust
use std::ops::Range;
|
|
use std::rc::Rc;
|
|
|
|
use gpui::{
|
|
fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler,
|
|
Entity, GlobalElementId, Hitbox, IntoElement, LayoutId, MouseButton, MouseMoveEvent, Path,
|
|
Pixels, Point, ShapedLine, SharedString, Size, Style, TextAlign, TextRun, UnderlineStyle,
|
|
Window,
|
|
};
|
|
use rope::Rope;
|
|
use smallvec::SmallVec;
|
|
use theme::ActiveTheme;
|
|
|
|
use super::blink_cursor::CURSOR_WIDTH;
|
|
use super::rope_ext::RopeExt;
|
|
use super::state::{InputState, LastLayout};
|
|
use crate::Root;
|
|
|
|
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(),
|
|
}
|
|
}
|
|
|
|
/// Set the placeholder text of the input field.
|
|
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);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Returns the:
|
|
///
|
|
/// - cursor bounds
|
|
/// - scroll offset
|
|
/// - current row index (No only the visible lines, but all lines)
|
|
///
|
|
/// This method also will update for track scroll to cursor.
|
|
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 cursor = state.cursor();
|
|
let mut current_row = None;
|
|
let mut scroll_offset = state.scroll_handle.offset();
|
|
let mut cursor_bounds = None;
|
|
|
|
// If the input has a fixed height (Otherwise is auto-grow), we need to add a bottom margin to the input.
|
|
let top_bottom_margin = if state.mode.is_auto_grow() {
|
|
#[allow(clippy::if_same_then_else)]
|
|
line_height
|
|
} else if visible_range.len() < BOTTOM_MARGIN_ROWS * 8 {
|
|
line_height
|
|
} else {
|
|
BOTTOM_MARGIN_ROWS * line_height
|
|
};
|
|
|
|
// The cursor corresponds to the current cursor position in the text no only the line.
|
|
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);
|
|
|
|
// break loop if all cursor positions are found
|
|
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 in visible range lines
|
|
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;
|
|
// +1 for the last `\n`
|
|
prev_lines_offset += line.len() + 1;
|
|
} else {
|
|
// If not in the visible range.
|
|
|
|
// Just increase the offset_y and prev_lines_offset.
|
|
// This will let the scroll_offset to track the cursor position correctly.
|
|
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);
|
|
// +1 for the last `\n`
|
|
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 {
|
|
scroll_offset.x = if scroll_offset.x + cursor_pos.x
|
|
> (bounds.size.width - line_number_width - RIGHT_MARGIN)
|
|
{
|
|
// cursor is out of right
|
|
bounds.size.width - line_number_width - RIGHT_MARGIN - cursor_pos.x
|
|
} else if scroll_offset.x + cursor_pos.x < px(0.) {
|
|
// cursor is out of left
|
|
scroll_offset.x - cursor_pos.x
|
|
} else {
|
|
scroll_offset.x
|
|
};
|
|
|
|
// If we change the scroll_offset.y, GPUI will render and trigger the next run loop.
|
|
// So, here we just adjust offset by `line_height` for move smooth.
|
|
scroll_offset.y =
|
|
if scroll_offset.y + cursor_pos.y > bounds.size.height - top_bottom_margin {
|
|
// cursor is out of bottom
|
|
scroll_offset.y - line_height
|
|
} else if scroll_offset.y + cursor_pos.y < top_bottom_margin {
|
|
// cursor is out of top
|
|
(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.) {
|
|
// selection start is out of left
|
|
scroll_offset.x = -cursor_start.x;
|
|
}
|
|
if scroll_offset.y + cursor_start.y < px(0.) {
|
|
// selection start is out of top
|
|
scroll_offset.y = -cursor_start.y;
|
|
}
|
|
} else {
|
|
if scroll_offset.x + cursor_end.x <= px(0.) {
|
|
// selection end is out of left
|
|
scroll_offset.x = -cursor_end.x;
|
|
}
|
|
if scroll_offset.y + cursor_end.y <= px(0.) {
|
|
// selection end is out of top
|
|
scroll_offset.y = -cursor_end.y;
|
|
}
|
|
}
|
|
}
|
|
|
|
// cursor bounds
|
|
let cursor_height = 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 += scroll_offset;
|
|
|
|
(cursor_bounds, scroll_offset, current_row)
|
|
}
|
|
|
|
/// Layout the match range to a Path.
|
|
pub(crate) fn layout_match_range(
|
|
range: Range<usize>,
|
|
last_layout: &LastLayout,
|
|
bounds: &mut 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());
|
|
|
|
// Split the selection into multiple items
|
|
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;
|
|
}
|
|
|
|
// Ensure at least 6px width for the selection for empty lines.
|
|
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),
|
|
});
|
|
|
|
// wrapped lines
|
|
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;
|
|
// +1 for skip the last `\n`
|
|
prev_lines_offset += line.len() + 1;
|
|
}
|
|
|
|
let mut points = vec![];
|
|
if line_corners.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
// Fix corners to make sure the left to right direction
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
|
|
// print_points_as_svg_path(&line_corners, &points);
|
|
|
|
let path_origin = bounds.origin + point(line_number_width, px(0.));
|
|
let first_p = *points.first().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_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;
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
/// Calculate the visible range of lines in the viewport.
|
|
///
|
|
/// Returns
|
|
///
|
|
/// - visible_range: The visible range is based on unwrapped lines (Zero based).
|
|
/// - visible_top: The top position of the first visible line in the scroll viewport.
|
|
fn calculate_visible_range(
|
|
&self,
|
|
state: &InputState,
|
|
line_height: Pixels,
|
|
input_height: Pixels,
|
|
) -> (Range<usize>, Pixels) {
|
|
// Add extra rows to avoid showing empty space when scroll to bottom.
|
|
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)
|
|
}
|
|
}
|
|
|
|
pub(super) struct PrepaintState {
|
|
/// The lines of entire lines.
|
|
last_layout: LastLayout,
|
|
/// The lines only contains the visible lines in the viewport, based on `visible_range`.
|
|
///
|
|
/// The child is the soft lines.
|
|
line_numbers: Option<Vec<SmallVec<[ShapedLine; 1]>>>,
|
|
/// Size of the scrollable area by entire lines.
|
|
scroll_size: Size<Pixels>,
|
|
cursor_bounds: Option<Bounds<Pixels>>,
|
|
cursor_scroll_offset: Point<Pixels>,
|
|
selection_path: Option<Path<Pixels>>,
|
|
hover_highlight_path: Option<Path<Pixels>>,
|
|
search_match_paths: Vec<(Path<Pixels>, bool)>,
|
|
hover_definition_hitbox: Option<Hitbox>,
|
|
bounds: Bounds<Pixels>,
|
|
}
|
|
|
|
impl IntoElement for TextElement {
|
|
type Element = Self;
|
|
|
|
fn into_element(self) -> Self::Element {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Element for TextElement {
|
|
type PrepaintState = PrepaintState;
|
|
type RequestLayoutState = ();
|
|
|
|
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() {
|
|
// Auto grow to let height match to rows, but not exceed max rows.
|
|
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 {
|
|
// For single-line inputs, the minimum height should be the line height
|
|
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 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 state = self.state.read(cx);
|
|
let multi_line = state.mode.is_multi_line();
|
|
let text = state.text.clone();
|
|
let is_empty = text.is_empty();
|
|
let placeholder = self.placeholder.clone();
|
|
let style = window.text_style();
|
|
let font_size = style.font_size.to_pixels(window.rem_size());
|
|
let mut bounds = bounds;
|
|
|
|
let (display_text, text_color) = if is_empty {
|
|
(
|
|
Rope::from_str_small(placeholder.as_str()),
|
|
cx.theme().text_muted,
|
|
)
|
|
} else if state.masked {
|
|
(
|
|
Rope::from_str_small("*".repeat(text.chars_count()).as_str()),
|
|
cx.theme().text,
|
|
)
|
|
} else {
|
|
(text.clone(), cx.theme().text)
|
|
};
|
|
|
|
let line_number_width = px(0.);
|
|
|
|
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 {
|
|
vec![run]
|
|
} else if let Some(ime_marked_range) = &state.ime_marked_range {
|
|
// IME marked text
|
|
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 wrap_width = if multi_line && state.soft_wrap {
|
|
Some(bounds.size.width - line_number_width - RIGHT_MARGIN)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// NOTE: Here 50 lines about 150µs
|
|
// let measure = crate::Measure::new("shape_text");
|
|
let visible_text = display_text
|
|
.slice_rows(visible_range.start as u32..visible_range.end as u32)
|
|
.to_string();
|
|
|
|
let lines = window
|
|
.text_system()
|
|
.shape_text(visible_text.into(), font_size, &runs, wrap_width, None)
|
|
.expect("failed to shape text");
|
|
// measure.end();
|
|
|
|
let mut longest_line_width = wrap_width.unwrap_or(px(0.));
|
|
if state.mode.is_multi_line() && !state.soft_wrap && lines.len() > 1 {
|
|
let longtest_line: SharedString = state
|
|
.text
|
|
.line(state.text.summary().longest_row as usize)
|
|
.to_string()
|
|
.into();
|
|
longest_line_width = window
|
|
.text_system()
|
|
.shape_line(
|
|
longtest_line.clone(),
|
|
font_size,
|
|
&[TextRun {
|
|
len: longtest_line.len(),
|
|
font: style.font(),
|
|
color: gpui::black(),
|
|
background_color: None,
|
|
underline: None,
|
|
strikethrough: None,
|
|
}],
|
|
wrap_width,
|
|
)
|
|
.width;
|
|
}
|
|
|
|
let total_wrapped_lines = state.text_wrapper.len();
|
|
let empty_bottom_height = 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)
|
|
.max(bounds.size.height),
|
|
);
|
|
|
|
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(lines),
|
|
cursor_bounds: None,
|
|
};
|
|
|
|
// `position_for_index` for example
|
|
//
|
|
// #### text
|
|
//
|
|
// Hello 世界,this is GPUI component.
|
|
// The GPUI Component is a collection of UI components for
|
|
// GPUI framework, including Button, Input, Checkbox, Radio,
|
|
// Dropdown, Tab, and more...
|
|
//
|
|
// wrap_width: 444px, line_height: 20px
|
|
//
|
|
// #### lines[0]
|
|
//
|
|
// | index | pos | line |
|
|
// |-------|------------------|------|
|
|
// | 5 | (37 px, 0.0) | 0 |
|
|
// | 38 | (261.7 px, 20.0) | 0 |
|
|
// | 40 | None | - |
|
|
//
|
|
// #### lines[1]
|
|
//
|
|
// | index | position | line |
|
|
// |-------|-----------------------|------|
|
|
// | 5 | (43.578125 px, 0.0) | 0 |
|
|
// | 56 | (422.21094 px, 0.0) | 0 |
|
|
// | 57 | (11.6328125 px, 20.0) | 1 |
|
|
// | 114 | (429.85938 px, 20.0) | 1 |
|
|
// | 115 | (11.3125 px, 40.0) | 2 |
|
|
|
|
// Calculate the scroll offset to keep the cursor in view
|
|
|
|
let (cursor_bounds, cursor_scroll_offset, _) =
|
|
self.layout_cursor(&last_layout, &mut bounds, window, cx);
|
|
last_layout.cursor_bounds = cursor_bounds;
|
|
|
|
let selection_path = self.layout_selections(&last_layout, &mut bounds, cx);
|
|
let search_match_paths = vec![];
|
|
let hover_highlight_path = None;
|
|
let line_numbers = None;
|
|
let hover_definition_hitbox = None;
|
|
|
|
PrepaintState {
|
|
bounds,
|
|
last_layout,
|
|
scroll_size,
|
|
line_numbers,
|
|
cursor_bounds,
|
|
cursor_scroll_offset,
|
|
selection_path,
|
|
search_match_paths,
|
|
hover_highlight_path,
|
|
hover_definition_hitbox,
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
window.handle_input(
|
|
&focus_handle,
|
|
ElementInputHandler::new(bounds, self.state.clone()),
|
|
cx,
|
|
);
|
|
|
|
// Set Root focused_input when self is focused
|
|
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();
|
|
});
|
|
}
|
|
}
|
|
|
|
// And reset focused_input when next_frame start
|
|
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();
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Paint multi line text
|
|
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.);
|
|
if self.state.read(cx).masked {
|
|
// Move down offset for vertical centering the *****
|
|
if cfg!(target_os = "macos") {
|
|
mask_offset_y = px(3.);
|
|
} else {
|
|
mask_offset_y = px(2.5);
|
|
}
|
|
}
|
|
|
|
// Paint active line
|
|
let mut offset_y = px(0.);
|
|
if let Some(line_numbers) = prepaint.line_numbers.as_ref() {
|
|
offset_y += invisible_top_padding;
|
|
|
|
// Each item is the normal lines.
|
|
for lines in line_numbers.iter() {
|
|
let height = line_height * lines.len() as f32;
|
|
offset_y += height;
|
|
}
|
|
}
|
|
|
|
// Paint selections
|
|
if window.is_window_active() {
|
|
let secondary_selection = cx.theme().selection;
|
|
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);
|
|
}
|
|
|
|
// Paint hover highlight
|
|
if let Some(path) = prepaint.hover_highlight_path.take() {
|
|
window.paint_path(path, secondary_selection);
|
|
}
|
|
}
|
|
|
|
// Paint text
|
|
let mut offset_y = mask_offset_y + invisible_top_padding;
|
|
for line in prepaint.last_layout.lines.iter() {
|
|
let p = point(
|
|
origin.x + prepaint.last_layout.line_number_width,
|
|
origin.y + offset_y,
|
|
);
|
|
_ = line.paint(p, line_height, TextAlign::Left, None, window, cx);
|
|
offset_y += line.size(line_height).height;
|
|
}
|
|
|
|
// Paint blinking cursor
|
|
if focused && show_cursor {
|
|
if let Some(mut cursor_bounds) = prepaint.cursor_bounds.take() {
|
|
cursor_bounds.origin.y += prepaint.cursor_scroll_offset.y;
|
|
window.paint_quad(fill(cursor_bounds, cx.theme().cursor));
|
|
}
|
|
}
|
|
|
|
// Paint line numbers
|
|
let mut offset_y = px(0.);
|
|
if let Some(line_numbers) = prepaint.line_numbers.as_ref() {
|
|
offset_y += invisible_top_padding;
|
|
|
|
// Paint line number background
|
|
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,
|
|
),
|
|
},
|
|
cx.theme().background,
|
|
));
|
|
|
|
// Each item is the normal lines.
|
|
for lines in line_numbers.iter() {
|
|
let p = point(input_bounds.origin.x, origin.y + offset_y);
|
|
|
|
for line in lines {
|
|
_ = line.paint(p, line_height, window, cx);
|
|
offset_y += line_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);
|
|
}
|
|
|
|
self.paint_mouse_listeners(window, cx);
|
|
}
|
|
}
|