stub out prepaint and paint for <input>

This commit is contained in:
temportalflux
2026-07-11 09:31:07 -04:00
parent 3f6faa20a5
commit 19e4791a33
4 changed files with 228 additions and 10 deletions
@@ -1,9 +1,12 @@
use crate::editable_text::{
EditableInputActionElement, InitStorage, StateBackedElement, TextInputState,
EditableInputActionElement, InitStorage, StateBackedElement, TextInputLayoutData,
TextInputState,
};
use gpui::{
App, Element, ElementId, Entity, Hitbox, InteractiveElement, Interactivity, IntoElement,
SharedString, StyleRefinement, Styled, TextStyle, Window,
App, Bounds, ContentMask, CursorStyle, Display, Element, ElementId, ElementInputHandler,
Entity, FocusHandle, Focusable, Hitbox, HitboxBehavior, Hsla, InteractiveElement,
Interactivity, IntoElement, PaintQuad, Pixels, ShapedLine, SharedString, Style,
StyleRefinement, Styled, TextRun, TextStyle, Window, fill, point, size,
};
#[track_caller]
@@ -80,6 +83,13 @@ pub mod element {
#[doc(hidden)]
pub struct PrepaintState {
pub hitbox: Option<Hitbox>,
pub line: Option<ShapedLine>,
pub focus_handle: FocusHandle,
pub selection: Option<PaintQuad>,
pub caret_quad: Option<PaintQuad>,
pub scroll_x: Pixels,
pub display_text: SharedString,
pub caret_visible: bool,
}
}
@@ -122,7 +132,7 @@ impl Element for TextInputElement {
},
);
let layout_state = element::LayoutState {
let layout_state = Self::RequestLayoutState {
state,
text_style: resolved_text_style.unwrap_or_else(|| window.text_style()),
};
@@ -131,19 +141,119 @@ impl Element for TextInputElement {
fn prepaint(
&mut self,
id: Option<&gpui::GlobalElementId>,
global_id: Option<&gpui::GlobalElementId>,
inspector_id: Option<&gpui::InspectorElementId>,
bounds: gpui::Bounds<gpui::Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut gpui::Window,
cx: &mut gpui::App,
) -> Self::PrepaintState {
todo!()
let input = request_layout.state.read(cx);
let focus_handle = input.focus_handle(cx);
let caret_pos = input.caret_pos();
let selection = input.selected_range();
// TODO: horizontal scroll
let scroll_x_input = Pixels::ZERO; //input.scroll_x();
// TODO: Cursor blinking
let cursor_visible = true; // input.cursor_visible();
let text_color = Hsla::white(); // TODO: as an element param
let placeholder_color = Hsla::black().opacity(0.5); // TODO: as an element param
let selection_color = Hsla::blue().opacity(0.5); // TODO: as an element param
let caret_color = Hsla::white(); // TODO: as an element param
let text_value = input.storage().content_utf8();
let is_empty = text_value.is_empty();
let (display_text, run_color) = match is_empty {
// TODO: Can the SharedString allocation be avoided?
false => (SharedString::new(text_value), text_color),
true => {
let value = self.placeholder.as_ref().cloned().unwrap_or_default();
(value, placeholder_color)
}
};
let style = window.text_style();
let font_size = style.font_size.to_pixels(window.rem_size());
let run = TextRun {
len: display_text.len(),
font: style.font(),
color: run_color,
background_color: None,
underline: None,
strikethrough: None,
};
let line = window
.text_system()
.shape_line(display_text.clone(), font_size, &[run], None);
let caret_x_line = line.x_for_index(caret_pos);
let cursor_thickness = gpui::px(2.0);
let max_cursor_x = (bounds.size.width - cursor_thickness).max(Pixels::ZERO);
let max_scroll_x = (line.width - max_cursor_x).max(Pixels::ZERO);
let mut scroll_x = scroll_x_input.clamp(Pixels::ZERO, max_scroll_x);
if caret_x_line < scroll_x {
scroll_x = caret_x_line;
} else if caret_x_line > scroll_x + max_cursor_x {
scroll_x = caret_x_line - max_cursor_x;
}
scroll_x = scroll_x.clamp(Pixels::ZERO, max_scroll_x);
let (selection_quad, cursor_quad) = if !selection.is_empty() && !is_empty {
let start_x = line.x_for_index(selection.start);
let end_x = line.x_for_index(selection.end);
let quad = fill(
Bounds::from_corners(
point(bounds.left() + start_x.min(end_x) - scroll_x, bounds.top()),
point(
bounds.left() + start_x.max(end_x) - scroll_x,
bounds.bottom(),
),
),
selection_color,
);
(Some(quad), None)
} else {
let cursor_paint_x = bounds.left() + caret_x_line - scroll_x;
let quad = fill(
Bounds::new(
point(cursor_paint_x, bounds.top()),
size(cursor_thickness, bounds.bottom() - bounds.top()),
),
caret_color,
);
(None, Some(quad))
};
let hitbox = self.interactivity.prepaint(
global_id,
inspector_id,
bounds,
bounds.size,
window,
cx,
|style, scroll_offset, hitbox, window, cx| {
hitbox.or_else(|| Some(window.insert_hitbox(bounds, HitboxBehavior::Normal)))
},
);
Self::PrepaintState {
hitbox,
line: Some(line),
focus_handle,
selection: selection_quad,
caret_quad: cursor_quad,
scroll_x,
display_text,
caret_visible: cursor_visible,
}
}
fn paint(
&mut self,
id: Option<&gpui::GlobalElementId>,
global_id: Option<&gpui::GlobalElementId>,
inspector_id: Option<&gpui::InspectorElementId>,
bounds: gpui::Bounds<gpui::Pixels>,
request_layout: &mut Self::RequestLayoutState,
@@ -151,6 +261,66 @@ impl Element for TextInputElement {
window: &mut gpui::Window,
cx: &mut gpui::App,
) {
todo!()
if let Some(hitbox) = &prepaint.hitbox {
window.set_cursor_style(CursorStyle::IBeam, hitbox);
}
// NOTE: Skip when disabled
let ime_handler = ElementInputHandler::new(bounds, request_layout.state.clone());
window.handle_input(&prepaint.focus_handle, ime_handler, cx);
let mut layout_data = TextInputLayoutData::default();
let perform_paint = |style: &Style, window: &mut Window, cx: &mut App| {
if style.display == Display::None {
return;
}
layout_data = window.with_content_mask(Some(ContentMask { bounds }), |window| {
if let Some(sel) = prepaint.selection.take() {
window.paint_quad(sel);
}
let line = prepaint
.line
.take()
.expect("prepaint always produces a line");
let origin_x = bounds.left() - prepaint.scroll_x;
let _ = line.paint(
point(origin_x, bounds.top()),
window.line_height(),
gpui::TextAlign::Left,
None,
window,
cx,
);
// TODO: Render marked IME underlines
let is_focused = prepaint.focus_handle.is_focused(window);
if is_focused
&& prepaint.caret_visible
&& let Some(cur) = prepaint.caret_quad.take()
{
window.paint_quad(cur);
}
TextInputLayoutData {
lines: vec![line],
bounds,
}
});
};
self.interactivity.paint(
global_id,
inspector_id,
bounds,
prepaint.hitbox.as_ref(),
window,
cx,
perform_paint,
);
request_layout.state.update(cx, |state, _cx| {
*state.layout_data_mut() = layout_data;
});
}
}
@@ -16,6 +16,19 @@ pub struct TextInputState {
impl EventEmitter<TextChanged> for TextInputState {}
impl EventEmitter<TextHistoryPushed> for TextInputState {}
impl std::ops::Deref for TextInputState {
type Target = TextInputStateBase;
fn deref(&self) -> &Self::Target {
&self.internal
}
}
impl std::ops::DerefMut for TextInputState {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.internal
}
}
impl TextInputState {
pub fn new(storage: impl Into<Box<dyn UnicodeTextStorage>>, cx: &mut Context<Self>) -> Self {
let internal = TextInputStateBase::new(storage, cx);
@@ -3,8 +3,8 @@ use crate::editable_text::{
notify::{TextChanged, TextHistoryPushed},
};
use gpui::{
App, ClipboardItem, Entity, FocusHandle, Focusable, NavigationDirection, Pixels, Point,
UTF16Selection, Window,
App, Bounds, ClipboardItem, Entity, FocusHandle, Focusable, NavigationDirection, Pixels, Point,
ShapedLine, UTF16Selection, Window,
};
use std::ops::Range;
@@ -53,6 +53,18 @@ pub struct TextInputStateBase {
click_count: usize,
focus_handle: FocusHandle,
layout_data: TextInputLayoutData,
}
#[derive(Default)]
pub(super) struct TextInputLayoutData {
/// The `ShapedLine` produced by the painter's `prepaint`.
/// Cached so IME `bounds_for_range` / `character_index_for_point` can evaluate without re-shaping.
pub lines: Vec<ShapedLine>,
/// The bounds of the text area, in window coordinates.
/// Cached for IME operations.
pub bounds: Bounds<Pixels>,
}
impl Focusable for TextInputStateBase {
@@ -74,6 +86,8 @@ impl TextInputStateBase {
click_count: 0,
focus_handle: cx.focus_handle(),
layout_data: TextInputLayoutData::default(),
}
}
@@ -103,6 +117,14 @@ impl TextInputStateBase {
pub fn set_selected_range(&mut self, range: Range<usize>) {
self.selected_range = range;
}
pub(super) fn layout_data(&self) -> &TextInputLayoutData {
&self.layout_data
}
pub(super) fn layout_data_mut(&mut self) -> &mut TextInputLayoutData {
&mut self.layout_data
}
}
impl TextInputStateBase {
@@ -16,6 +16,19 @@ pub struct TextAreaState {
impl EventEmitter<TextChanged> for TextAreaState {}
impl EventEmitter<TextHistoryPushed> for TextAreaState {}
impl std::ops::Deref for TextAreaState {
type Target = TextInputStateBase;
fn deref(&self) -> &Self::Target {
&self.internal
}
}
impl std::ops::DerefMut for TextAreaState {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.internal
}
}
impl TextAreaState {
pub fn new(storage: impl Into<Box<dyn UnicodeTextStorage>>, cx: &mut Context<Self>) -> Self {
let internal = TextInputStateBase::new(storage, cx);