begin integrating paint implementations

This commit is contained in:
temportalflux
2026-07-11 09:31:07 -04:00
parent 3255bb544e
commit 8bec8f3031
+333 -319
View File
@@ -3,10 +3,10 @@ use gpui::{
Along, App, Bounds, ContentMask, CursorStyle, DispatchPhase, Element, ElementId,
ElementInputHandler, Entity, FocusHandle, Focusable, GlobalElementId, Hitbox, HitboxBehavior,
Hsla, InspectorElementId, LayoutId, Length, MouseButton, MouseDownEvent, MouseMoveEvent,
MouseUpEvent, Pixels, Point, ScrollWheelEvent, SharedString, TextAlign, TextRun, TextStyle,
MouseUpEvent, Pixels, ScrollWheelEvent, SharedString, Style, TextAlign, TextRun, TextStyle,
Window, WrappedLine, fill, point, px, relative, size,
};
use std::sync::Arc;
use std::{ops::Range, sync::Arc};
const CURSOR_WIDTH: f32 = 2.0;
const MARKED_TEXT_UNDERLINE_THICKNESS: f32 = 2.0;
@@ -133,16 +133,32 @@ impl Element for Input {
cx,
);
let input = self.input.clone();
let snapshot = InputStateSnapshot::new(&self.input, cx);
let placeholder = self.placeholder.clone();
let text_style = layout_state.text_style.clone();
let layout = input.read(cx).get_layout();
let is_focused = focus_handle.is_focused(window);
let colors = self.colors;
// TODO: refactor cursor_visible so it is clear that it is called on_paint
let cursor_visible = self
.input
.update(cx, |input, cx| input.cursor_visible(is_focused, cx));
let colors = self.colors;
let perform_paint = |_style: &Style, window: &mut Window, cx: &mut App| {
let context = PaintContext {
snapshot,
focus_handle: &focus_handle,
bounds,
text_style: &text_style,
placeholder: placeholder.as_ref(),
colors: &colors,
cursor_visible,
};
context.process_mouse_events(&self.input, window, cx);
window.with_content_mask(Some(ContentMask { bounds }), |window| {
context.paint(window, cx);
});
};
self.interactivity.paint(
global_id,
inspector_id,
@@ -150,256 +166,286 @@ impl Element for Input {
prepaint_state.hitbox.as_ref(),
window,
cx,
|_style, window, cx| {
handle_mouse(&input, bounds, layout.axis(), window, cx);
window.with_content_mask(Some(ContentMask { bounds }), |window| match layout {
super::InputLayout::SingleLine => paint_singleline(
&input,
&focus_handle,
bounds,
&text_style,
placeholder.as_ref(),
&colors,
cursor_visible,
window,
cx,
),
super::InputLayout::MultiLine => paint_multiline(
&input,
&focus_handle,
bounds,
&text_style,
placeholder.as_ref(),
&colors,
cursor_visible,
window,
cx,
),
});
},
perform_paint,
);
}
}
/// Registers all mouse event handlers for the input.
fn handle_mouse(
input: &Entity<InputState>,
bounds: Bounds<Pixels>,
axis: gpui::Axis,
window: &mut Window,
cx: &App,
) {
mouse_down(input.clone(), bounds, axis, window);
mouse_up(input.clone(), window);
mouse_move(input.clone(), bounds, axis, window);
handle_scroll(input.clone(), bounds, axis, window, cx);
struct InputStateSnapshot {
layout: super::InputLayout,
content: SharedString,
selected_range: Range<usize>,
marked_range: Option<Range<usize>>,
cursor_offset: usize,
line_layouts: Vec<InputLineLayout>,
scroll_offset: Pixels,
line_height: Pixels,
}
impl InputStateSnapshot {
fn new(entity: &Entity<InputState>, cx: &App) -> Self {
let input_state = entity.read(cx);
let selected_range = input_state.selected_range().clone();
let marked_range = input_state.marked_range().cloned();
let cursor_offset = input_state.cursor_offset();
let line_layouts = input_state.line_layouts.clone();
let scroll_offset = input_state.scroll_offset;
let line_height = input_state.line_height;
Self {
layout: input_state.get_layout(),
content: input_state.content().clone(),
selected_range,
marked_range,
cursor_offset,
line_layouts,
scroll_offset,
line_height,
}
}
}
fn mouse_down(
input: Entity<InputState>,
struct PaintContext<'app> {
snapshot: InputStateSnapshot,
focus_handle: &'app FocusHandle,
bounds: Bounds<Pixels>,
axis: gpui::Axis,
window: &mut Window,
) {
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
if phase != DispatchPhase::Bubble {
return;
}
if !bounds.contains(&event.position) {
return;
}
if event.button != MouseButton::Left {
return;
}
text_style: &'app TextStyle,
placeholder: Option<&'app SharedString>,
colors: &'app PaintColors,
cursor_visible: bool,
}
input.update(cx, |input, cx| {
let text_position =
screen_to_text_position(event.position, bounds, input.scroll_offset, axis);
input.on_mouse_down(
text_position,
event.click_count,
event.modifiers.shift,
window,
cx,
);
impl<'app> PaintContext<'app> {
pub fn process_mouse_events(
&self,
entity: &Entity<InputState>,
window: &mut Window,
cx: &mut App,
) {
let axis = self.snapshot.layout.axis();
let bounds = self.bounds;
window.on_mouse_event({
let input = entity.clone();
move |event: &MouseDownEvent, phase, window, cx| {
if phase != DispatchPhase::Bubble {
return;
}
if !bounds.contains(&event.position) {
return;
}
if event.button != MouseButton::Left {
return;
}
input.update(cx, |input, cx| {
// Converts a screen position to a position relative to the text area origin, adjusted for scroll offset.
let text_position = (event.position - bounds.origin)
.apply_along(axis, |pos| pos + input.scroll_offset);
input.on_mouse_down(
text_position,
event.click_count,
event.modifiers.shift,
window,
cx,
);
});
}
});
});
}
window.on_mouse_event({
let input = entity.clone();
move |event: &MouseUpEvent, phase, _window, cx| {
if phase != DispatchPhase::Bubble {
return;
}
if event.button != MouseButton::Left {
return;
}
fn mouse_up(input: Entity<InputState>, window: &mut Window) {
window.on_mouse_event(move |event: &MouseUpEvent, phase, _window, cx| {
if phase != DispatchPhase::Bubble {
return;
}
if event.button != MouseButton::Left {
return;
}
input.update(cx, |input, cx| {
input.on_mouse_up(cx);
input.update(cx, |input, cx| {
input.on_mouse_up(cx);
});
}
});
});
}
window.on_mouse_event({
let input = entity.clone();
move |event: &MouseMoveEvent, phase, _window, cx| {
if phase != DispatchPhase::Bubble {
return;
}
fn mouse_move(
input: Entity<InputState>,
bounds: Bounds<Pixels>,
axis: gpui::Axis,
window: &mut Window,
) {
window.on_mouse_event(move |event: &MouseMoveEvent, phase, _window, cx| {
if phase != DispatchPhase::Bubble {
return;
}
input.update(cx, |input, cx| {
let text_position =
screen_to_text_position(event.position, bounds, input.scroll_offset, axis);
input.on_mouse_move(text_position, cx);
input.update(cx, |input, cx| {
// Converts a screen position to a position relative to the text area origin, adjusted for scroll offset.
let text_position = (event.position - bounds.origin)
.apply_along(axis, |pos| pos + input.scroll_offset);
input.on_mouse_move(text_position, cx);
});
}
});
});
}
window.on_mouse_event({
let input = entity.clone();
let content_size = match axis {
gpui::Axis::Horizontal => {
let state = input.read(cx);
let line = state.line_layouts.first();
let line = line.and_then(|l| l.wrapped_line.as_ref());
line.map(|w| w.width()).unwrap_or(px(0.))
}
gpui::Axis::Vertical => input.read(cx).total_content_height(),
};
let max_scroll = (content_size - bounds.size.along(axis)).max(px(0.));
move |event: &ScrollWheelEvent, phase, _window, cx| {
if phase != DispatchPhase::Bubble {
return;
}
if !bounds.contains(&event.position) {
return;
}
fn handle_scroll(
input: Entity<InputState>,
bounds: Bounds<Pixels>,
axis: gpui::Axis,
window: &mut Window,
cx: &App,
) {
let content_size = match axis {
gpui::Axis::Horizontal => {
let state = input.read(cx);
let line = state.line_layouts.first();
let line = line.and_then(|l| l.wrapped_line.as_ref());
line.map(|w| w.width()).unwrap_or(px(0.))
}
gpui::Axis::Vertical => input.read(cx).total_content_height(),
};
let max_scroll = (content_size - bounds.size.along(axis)).max(px(0.));
let pixel_delta = event.delta.pixel_delta(px(20.));
input.update(cx, |input, cx| {
let delta = match axis {
gpui::Axis::Horizontal => pixel_delta.y,
gpui::Axis::Vertical => {
if pixel_delta.x.abs() > pixel_delta.y.abs() {
pixel_delta.x
} else {
pixel_delta.y
}
}
};
input.scroll_offset = (input.scroll_offset - delta).clamp(px(0.), max_scroll);
cx.notify();
});
}
});
}
window.on_mouse_event(move |event: &ScrollWheelEvent, phase, _window, cx| {
if phase != DispatchPhase::Bubble {
return;
}
if !bounds.contains(&event.position) {
return;
}
pub fn paint(&self, window: &mut Window, cx: &mut App) {
match self.snapshot.layout {
super::InputLayout::MultiLine => {
let is_focused = self.focus_handle.is_focused(window);
let pixel_delta = event.delta.pixel_delta(px(20.));
input.update(cx, |input, cx| {
let delta = match axis {
gpui::Axis::Horizontal => pixel_delta.y,
gpui::Axis::Vertical => {
if pixel_delta.x.abs() > pixel_delta.y.abs() {
pixel_delta.x
} else {
pixel_delta.y
if !self.snapshot.selected_range.is_empty() {
paint_multiline_selection(
&self.snapshot.line_layouts,
&self.snapshot.selected_range,
self.bounds,
self.snapshot.scroll_offset,
self.snapshot.line_height,
self.colors.selection,
window,
);
}
if self.snapshot.content.is_empty() {
if let Some(placeholder_str) = self.placeholder {
if !placeholder_str.is_empty() {
paint_placeholder(
placeholder_str,
self.bounds,
self.text_style,
self.colors.placeholder,
window,
cx,
false,
);
}
}
} else {
paint_multiline_text(
&self.snapshot.line_layouts,
self.bounds,
self.snapshot.scroll_offset,
self.snapshot.line_height,
window,
cx,
);
}
if let Some(marked_range) = &self.snapshot.marked_range {
if !marked_range.is_empty() {
paint_multiline_marked_underline(
&self.snapshot.line_layouts,
marked_range,
self.bounds,
self.snapshot.scroll_offset,
self.snapshot.line_height,
self.colors.cursor,
window,
);
}
}
};
input.scroll_offset = (input.scroll_offset - delta).clamp(px(0.), max_scroll);
cx.notify();
});
});
}
/// Converts a screen position to a position relative to the text area origin,
/// adjusted for scroll offset.
fn screen_to_text_position(
screen_position: Point<Pixels>,
bounds: Bounds<Pixels>,
scroll_offset: Pixels,
axis: gpui::Axis,
) -> Point<Pixels> {
let point = screen_position - bounds.origin;
point.apply_along(axis, |pos| pos + scroll_offset)
}
if is_focused && self.snapshot.selected_range.is_empty() && self.cursor_visible {
paint_multiline_cursor(
&self.snapshot.line_layouts,
self.snapshot.cursor_offset,
&self.snapshot.content,
self.bounds,
self.snapshot.scroll_offset,
self.snapshot.line_height,
self.colors.cursor,
window,
);
}
}
super::InputLayout::SingleLine => {
let state =
SingleLinePaintState::from_input(&self.snapshot, self.focus_handle, window);
fn paint_multiline(
input: &Entity<InputState>,
focus_handle: &FocusHandle,
bounds: Bounds<Pixels>,
text_style: &TextStyle,
placeholder: Option<&SharedString>,
colors: &PaintColors,
cursor_visible: bool,
window: &mut Window,
cx: &mut App,
) {
let input_state = input.read(cx);
let content = input_state.content().to_string();
let selected_range = input_state.selected_range().clone();
let marked_range = input_state.marked_range().cloned();
let cursor_offset = input_state.cursor_offset();
let line_layouts = input_state.line_layouts.clone();
let scroll_offset = input_state.scroll_offset;
let line_height = input_state.line_height;
let is_focused = focus_handle.is_focused(window);
if !self.snapshot.selected_range.is_empty() {
paint_singleline_selection(
&self.snapshot,
&state,
self.bounds,
self.colors.selection,
window,
);
}
if !selected_range.is_empty() {
paint_multiline_selection(
&line_layouts,
&selected_range,
bounds,
scroll_offset,
line_height,
colors.selection,
window,
);
}
if self.snapshot.content.is_empty() {
if let Some(placeholder_str) = self.placeholder {
if !placeholder_str.is_empty() {
paint_placeholder(
placeholder_str,
self.bounds,
self.text_style,
self.colors.placeholder,
window,
cx,
true,
);
}
}
} else {
paint_singleline_text(&self.snapshot, &state, self.bounds, window, cx);
}
if content.is_empty() {
if let Some(placeholder_str) = placeholder {
if !placeholder_str.is_empty() {
paint_placeholder(
placeholder_str,
bounds,
text_style,
colors.placeholder,
window,
cx,
false,
);
if let Some(marked_range) = &self.snapshot.marked_range {
if !marked_range.is_empty() {
paint_singleline_marked_underline(
&self.snapshot,
&state,
marked_range,
self.bounds,
self.colors.cursor,
window,
);
}
}
if state.is_focused
&& self.snapshot.selected_range.is_empty()
&& self.cursor_visible
{
paint_singleline_cursor(
&self.snapshot,
&state,
self.bounds,
self.colors.cursor,
window,
);
}
}
}
} else {
paint_multiline_text(
&line_layouts,
bounds,
scroll_offset,
line_height,
window,
cx,
);
}
if let Some(marked_range) = &marked_range {
if !marked_range.is_empty() {
paint_multiline_marked_underline(
&line_layouts,
marked_range,
bounds,
scroll_offset,
line_height,
colors.cursor,
window,
);
}
}
if is_focused && selected_range.is_empty() && cursor_visible {
paint_multiline_cursor(
&line_layouts,
cursor_offset,
&content,
bounds,
scroll_offset,
line_height,
colors.cursor,
window,
);
}
}
@@ -756,12 +802,6 @@ fn paint_multiline_cursor(
/// State for single-line painting that pre-computes character positions.
struct SingleLinePaintState {
content: String,
selected_range: std::ops::Range<usize>,
marked_range: Option<std::ops::Range<usize>>,
cursor_offset: usize,
scroll_offset: Pixels,
line_height: Pixels,
text_width: Pixels,
is_focused: bool,
char_positions: Vec<Pixels>,
@@ -770,23 +810,20 @@ struct SingleLinePaintState {
impl SingleLinePaintState {
fn from_input(
input: &Entity<InputState>,
snapshot: &InputStateSnapshot,
focus_handle: &FocusHandle,
window: &Window,
cx: &App,
) -> Self {
let input_state = input.read(cx);
let mut char_positions = Vec::new();
let mut text_width = px(0.);
if let Some(line) = input_state.line_layouts.first() {
if let Some(line) = snapshot.line_layouts.first() {
if let Some(wrapped) = &line.wrapped_line {
text_width = wrapped.width();
let content = input_state.content();
let content = &snapshot.content;
let mut idx = 0;
for ch in content.chars() {
if let Some(pos) = wrapped.position_for_index(idx, input_state.line_height) {
if let Some(pos) = wrapped.position_for_index(idx, snapshot.line_height) {
char_positions.push(pos.x);
} else {
char_positions.push(text_width);
@@ -797,99 +834,58 @@ impl SingleLinePaintState {
}
}
let wrapped_line = input_state
let wrapped_line = snapshot
.line_layouts
.first()
.and_then(|l| l.wrapped_line.clone());
Self {
content: input_state.content().to_string(),
selected_range: input_state.selected_range().clone(),
marked_range: input_state.marked_range().cloned(),
cursor_offset: input_state.cursor_offset(),
scroll_offset: input_state.scroll_offset,
line_height: input_state.line_height,
text_width,
is_focused: focus_handle.is_focused(window),
char_positions,
wrapped_line,
}
}
fn x_for_index(&self, index: usize) -> Pixels {
let char_index = self.content[..index.min(self.content.len())]
.chars()
.count();
self.char_positions
.get(char_index)
.copied()
.unwrap_or(self.text_width)
}
}
fn paint_singleline(
input: &Entity<InputState>,
focus_handle: &FocusHandle,
bounds: Bounds<Pixels>,
text_style: &TextStyle,
placeholder: Option<&SharedString>,
colors: &PaintColors,
cursor_visible: bool,
window: &mut Window,
cx: &mut App,
) {
let state = SingleLinePaintState::from_input(input, focus_handle, window, cx);
if !state.selected_range.is_empty() {
paint_singleline_selection(&state, bounds, colors.selection, window);
}
if state.content.is_empty() {
if let Some(placeholder_str) = placeholder {
if !placeholder_str.is_empty() {
paint_placeholder(
placeholder_str,
bounds,
text_style,
colors.placeholder,
window,
cx,
true,
);
}
}
} else {
paint_singleline_text(&state, bounds, window, cx);
}
if let Some(marked_range) = &state.marked_range {
if !marked_range.is_empty() {
paint_singleline_marked_underline(&state, marked_range, bounds, colors.cursor, window);
}
}
if state.is_focused && state.selected_range.is_empty() && cursor_visible {
paint_singleline_cursor(&state, bounds, colors.cursor, window);
}
fn x_for_index<'chars>(
content: &SharedString,
char_positions: &'chars Vec<Pixels>,
index: usize,
default: &Pixels,
) -> Pixels {
let char_index = content[..index.min(content.len())].chars().count();
char_positions.get(char_index).unwrap_or(default).clone()
}
fn paint_singleline_selection(
snapshot: &InputStateSnapshot,
state: &SingleLinePaintState,
bounds: Bounds<Pixels>,
selection_color: Hsla,
window: &mut Window,
) {
let start_x = state.x_for_index(state.selected_range.start) - state.scroll_offset;
let end_x = state.x_for_index(state.selected_range.end) - state.scroll_offset;
let start_x = x_for_index(
&snapshot.content,
&state.char_positions,
snapshot.selected_range.start,
&state.text_width,
) - snapshot.scroll_offset;
let end_x = x_for_index(
&snapshot.content,
&state.char_positions,
snapshot.selected_range.end,
&state.text_width,
) - snapshot.scroll_offset;
let y_offset = (bounds.size.height - state.line_height).max(px(0.)) / 2.0;
let y_offset = (bounds.size.height - snapshot.line_height).max(px(0.)) / 2.0;
window.paint_quad(fill(
Bounds::from_corners(
point(bounds.left() + start_x, bounds.top() + y_offset),
point(
bounds.left() + end_x,
bounds.top() + y_offset + state.line_height,
bounds.top() + y_offset + snapshot.line_height,
),
),
selection_color,
@@ -930,6 +926,7 @@ fn paint_placeholder(
}
fn paint_singleline_text(
snapshot: &InputStateSnapshot,
state: &SingleLinePaintState,
bounds: Bounds<Pixels>,
window: &mut Window,
@@ -939,15 +936,15 @@ fn paint_singleline_text(
return;
};
let y_offset = (bounds.size.height - state.line_height).max(px(0.)) / 2.0;
let y_offset = (bounds.size.height - snapshot.line_height).max(px(0.)) / 2.0;
let paint_origin = point(
bounds.origin.x - state.scroll_offset,
bounds.origin.x - snapshot.scroll_offset,
bounds.origin.y + y_offset,
);
let _ = wrapped_line.paint(
paint_origin,
state.line_height,
snapshot.line_height,
TextAlign::Left,
Some(bounds),
window,
@@ -956,18 +953,29 @@ fn paint_singleline_text(
}
fn paint_singleline_marked_underline(
snapshot: &InputStateSnapshot,
state: &SingleLinePaintState,
marked_range: &std::ops::Range<usize>,
bounds: Bounds<Pixels>,
underline_color: Hsla,
window: &mut Window,
) {
let start_x = state.x_for_index(marked_range.start) - state.scroll_offset;
let end_x = state.x_for_index(marked_range.end) - state.scroll_offset;
let start_x = x_for_index(
&snapshot.content,
&state.char_positions,
marked_range.start,
&state.text_width,
) - snapshot.scroll_offset;
let end_x = x_for_index(
&snapshot.content,
&state.char_positions,
marked_range.end,
&state.text_width,
) - snapshot.scroll_offset;
let underline_thickness = px(MARKED_TEXT_UNDERLINE_THICKNESS);
let y_offset = (bounds.size.height - state.line_height).max(px(0.)) / 2.0;
let underline_y = bounds.top() + y_offset + state.line_height - underline_thickness;
let y_offset = (bounds.size.height - snapshot.line_height).max(px(0.)) / 2.0;
let underline_y = bounds.top() + y_offset + snapshot.line_height - underline_thickness;
window.paint_quad(fill(
Bounds::from_corners(
@@ -979,19 +987,25 @@ fn paint_singleline_marked_underline(
}
fn paint_singleline_cursor(
snapshot: &InputStateSnapshot,
state: &SingleLinePaintState,
bounds: Bounds<Pixels>,
cursor_color: Hsla,
window: &mut Window,
) {
let cursor_x = state.x_for_index(state.cursor_offset) - state.scroll_offset;
let cursor_x = x_for_index(
&snapshot.content,
&state.char_positions,
snapshot.cursor_offset,
&state.text_width,
) - snapshot.scroll_offset;
let y_offset = (bounds.size.height - state.line_height).max(px(0.)) / 2.0;
let y_offset = (bounds.size.height - snapshot.line_height).max(px(0.)) / 2.0;
window.paint_quad(fill(
Bounds::new(
point(bounds.left() + cursor_x, bounds.top() + y_offset),
size(px(CURSOR_WIDTH), state.line_height),
size(px(CURSOR_WIDTH), snapshot.line_height),
),
cursor_color,
));