Files
coop/crates/ui/src/input/element.rs
reya 7091fa1cab chore: restructure and refine the ui (#199)
* update deps

* clean up

* add account crate

* add person crate

* add chat and chat ui crates

* .

* clean up the ui crate

* .

* .
2025-11-01 09:16:02 +07:00

891 lines
31 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}