improve input

This commit is contained in:
2026-10-29 14:12:23 +07:00
parent ad43867cb4
commit 5d29c61f39
2 changed files with 141 additions and 89 deletions
+120 -87
View File
@@ -12,7 +12,7 @@ use gpui::{
RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, TextAlign,
TextRun, UTF16Selection, Window, canvas, div, fill, point, px,
};
use gpui_native_theme::{ActiveTheme as _, Size};
use gpui_native_theme::{ActiveTheme as _, ColorTokens, Size};
use crate::common::{Sizable, h_flex};
@@ -39,6 +39,8 @@ struct InputLayout {
/// Handler for key down events.
type KeyDownHandler = std::rc::Rc<dyn Fn(&KeyDownEvent, &mut Window, &mut App)>;
/// Handler for mouse down events.
type MouseDownHandler = std::rc::Rc<dyn Fn(&MouseDownEvent, &mut Window, &mut App)>;
/// Input state for a single-line text field.
pub struct InputState {
@@ -54,6 +56,10 @@ pub struct InputState {
focus_handle: gpui::FocusHandle,
/// The key-down handler, bound to this entity with `cx.listener`.
key_down_handler: Option<KeyDownHandler>,
/// The mouse-down handler, bound to this entity with `cx.listener`.
mouse_down_handler: Option<MouseDownHandler>,
/// Whether a drag-select is in progress.
dragging: bool,
/// The shaped line and element bounds, recorded during each paint,
/// and shared with hit-testing and the IME geometry conversions.
layout: Rc<RefCell<Option<InputLayout>>>,
@@ -75,6 +81,12 @@ impl InputState {
state.handle_key_down(event, window, cx);
},
))),
mouse_down_handler: Some(Rc::new(cx.listener(
|state, event: &MouseDownEvent, window, cx| {
state.handle_mouse_down(event, window, cx);
},
))),
dragging: false,
layout: Rc::new(RefCell::new(None)),
}
}
@@ -393,6 +405,47 @@ impl InputState {
window.paint_quad(fill(caret, color));
}
/// Paints the field contents; returns whether a drag-select is in progress.
fn paint(
&mut self,
line: gpui::ShapedLine,
bounds: Bounds<Pixels>,
padding: Pixels,
colors: ColorTokens,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
let focused = self.focus_handle.is_focused(window);
self.sync_focus(focused, cx);
let text_style = window.text_style();
let font_size = text_style.font_size.to_pixels(window.rem_size());
let line_height = text_style
.line_height
.to_pixels(text_style.font_size, window.rem_size());
let (origin, caret_x) = self.store_layout(line, bounds, padding, line_height);
if focused && !self.selection().is_empty() {
self.paint_selection(origin, caret_x, bounds, colors.selection, window);
}
self.paint_text(origin, line_height, window, cx);
if focused && self.selection().is_empty() {
self.paint_caret(
origin,
caret_x,
font_size,
line_height,
colors.foreground,
window,
);
}
self.dragging
}
/// The text index at window x, using the last painted layout.
fn index_at_x(&self, x: Pixels) -> usize {
let layout = self.layout.borrow();
@@ -411,6 +464,57 @@ impl InputState {
self.notify(window, cx);
}
/// Registers the window-level listeners that continue a drag-select
/// while the pointer moves outside the field.
fn register_drag_listeners(entity: &Entity<Self>, window: &mut Window) {
window.on_mouse_event({
let state = entity.clone();
move |event: &MouseMoveEvent, phase, window, cx| {
if phase != DispatchPhase::Bubble || event.pressed_button != Some(MouseButton::Left)
{
return;
}
state.update(cx, |state, cx| {
state.drag_select_to(event.position.x, window, cx)
});
}
});
window.on_mouse_event({
let state = entity.clone();
move |event: &MouseUpEvent, phase, _window, cx| {
if phase != DispatchPhase::Bubble || event.button != MouseButton::Left {
return;
}
state.update(cx, |state, _| state.dragging = false);
}
});
}
/// The mouse-down handler: starts a drag and places or extends the selection.
fn handle_mouse_down(
&mut self,
event: &MouseDownEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.dragging = true;
let index = self.index_at_x(event.position.x);
if event.click_count == 2 {
let start = previous_word_start(&self.text, index);
let end = next_word_end(&self.text, index);
self.select_range(start..end, window, cx);
} else if event.modifiers.shift {
self.cursor = index;
self.notify(window, cx);
} else {
self.cursor = index;
self.anchor = index;
self.notify(window, cx);
}
}
/// The key-down handler: navigation, editing, clipboard, and submit.
///
/// Runs only while the field is focused.
@@ -774,16 +878,9 @@ impl RenderOnce for Input {
let metrics = theme.sizes.resolve(self.size);
let focus_handle = self.state.read(cx).focus_handle.clone();
// Drag selection crosses frames, so it lives in keyed element state.
let dragging = window.use_keyed_state("input-dragging", cx, |_, _| false);
let down_dragging = dragging.clone();
let paint_dragging = dragging.clone();
let focused = focus_handle.is_focused(window);
let is_empty = self.state.read(cx).text.is_empty();
let down_state = self.state.clone();
let fallback_label = self
.a11y_label
.clone()
@@ -826,43 +923,13 @@ impl RenderOnce for Input {
{
let state = self.state.clone();
let focus = focus_handle.clone();
let dragging = paint_dragging;
let padding = metrics.padding_x / 2.0;
let disabled = self.disabled;
// `line` comes from the prepaint closure above.
move |bounds, line, window, cx| {
state.update(cx, |state, cx| {
state.sync_focus(focused, cx);
let text_style = window.text_style();
let font_size = text_style.font_size.to_pixels(window.rem_size());
let line_height = text_style
.line_height
.to_pixels(text_style.font_size, window.rem_size());
let padding = metrics.padding_x / 2.0;
let (origin, caret_x) =
state.store_layout(line, bounds, padding, line_height);
if focused && !state.selection().is_empty() {
state.paint_selection(
origin,
caret_x,
bounds,
colors.selection,
window,
);
}
state.paint_text(origin, line_height, window, cx);
if focused && state.selection().is_empty() {
state.paint_caret(
origin,
caret_x,
font_size,
line_height,
colors.foreground,
window,
);
}
let dragging = state.update(cx, |state, cx| {
state.paint(line, bounds, padding, colors, window, cx)
});
if disabled {
@@ -875,32 +942,8 @@ impl RenderOnce for Input {
cx,
);
if *dragging.read(cx) {
let state = state.clone();
let dragging = dragging.clone();
window.on_mouse_event(
move |event: &MouseMoveEvent, phase, window, cx| {
if phase != DispatchPhase::Bubble
|| event.pressed_button != Some(MouseButton::Left)
{
return;
}
state.update(cx, |state, cx| {
state.drag_select_to(event.position.x, window, cx)
});
},
);
let dragging = dragging.clone();
window.on_mouse_event(
move |event: &MouseUpEvent, phase, _window, cx| {
if phase != DispatchPhase::Bubble
|| event.button != MouseButton::Left
{
return;
}
dragging.update(cx, |dragging, _| *dragging = false);
},
);
if dragging {
InputState::register_drag_listeners(&state, window);
}
}
},
@@ -911,26 +954,14 @@ impl RenderOnce for Input {
.when(!self.disabled, |input| {
input
.on_mouse_down(MouseButton::Left, {
let state = down_state;
let dragging = down_dragging;
let mouse_down_handler = self
.state
.read(cx)
.mouse_down_handler
.clone()
.expect("input state always carries its mouse-down handler");
move |event: &MouseDownEvent, window, cx| {
dragging.update(cx, |dragging, _| *dragging = true);
state.update(cx, |state, cx| {
let index = state.index_at_x(event.position.x);
if event.click_count == 2 {
let start = previous_word_start(&state.text, index);
let end = next_word_end(&state.text, index);
state.select_range(start..end, window, cx);
} else if event.modifiers.shift {
state.cursor = index;
state.notify(window, cx);
} else {
state.cursor = index;
state.anchor = index;
state.notify(window, cx);
}
});
mouse_down_handler(event, window, cx);
}
})
.on_key_down({
@@ -940,7 +971,9 @@ impl RenderOnce for Input {
.key_down_handler
.clone()
.expect("input state always carries its key handler");
move |event: &KeyDownEvent, window, cx| key_down_handler(event, window, cx)
move |event: &KeyDownEvent, window, cx| {
key_down_handler(event, window, cx);
}
})
})
}