Move cursor entity to input element and set it up as an Element so it can render independently

This commit is contained in:
temportalflux
2026-07-11 09:31:07 -04:00
parent 7affa0393f
commit 454b3210fb
6 changed files with 61 additions and 40 deletions
Generated
+1
View File
@@ -2427,6 +2427,7 @@ version = "0.1.0"
dependencies = [
"async-io",
"gpui",
"smallvec",
"unicode-segmentation",
]
+1
View File
@@ -15,6 +15,7 @@ ignored = ["gpui"]
gpui.workspace = true
async-io.workspace = true
unicode-segmentation.workspace = true
smallvec.workspace = true
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
+2
View File
@@ -4,6 +4,8 @@ use std::time::Duration;
/// Default interval for cursor blinking.
pub const DEFAULT_BLINK_INTERVAL: Duration = Duration::from_millis(500);
pub trait Cursor {}
/// Configuration for cursor blinking, to be provided to InputState.
pub enum CursorBlinkType<'app> {
/// The cursor will not blink.
+14 -3
View File
@@ -1,7 +1,7 @@
use crate::input::{InputColors, InputState};
use crate::input::{Cursor, InputColors, InputState};
use gpui::{
Action, App, Context, Entity, FocusHandle, Focusable, Hsla, InteractiveElement, Interactivity,
IntoElement, SharedString, StyleRefinement, Styled, Window,
Action, AnyElement, App, Context, Entity, FocusHandle, Focusable, Hsla, InteractiveElement,
Interactivity, IntoElement, SharedString, StyleRefinement, Styled, Window,
};
#[track_caller]
@@ -15,6 +15,7 @@ pub struct Input {
pub(super) interactivity: Interactivity,
pub(super) placeholder: Option<SharedString>,
pub(super) colors: InputColors,
pub(super) cursor: Option<AnyElement>,
}
impl Input {
@@ -26,6 +27,7 @@ impl Input {
interactivity: Interactivity::new(),
placeholder: None,
colors: InputColors::default(),
cursor: None,
};
input.register_actions();
input
@@ -163,6 +165,15 @@ impl Input {
self.colors.marked = color;
self
}
pub fn cursor<T>(mut self, entity: Entity<T>) -> Self
where
T: Cursor,
Entity<T>: Into<AnyElement>,
{
self.cursor = Some(entity.into());
self
}
}
fn register_action<A: Action>(
+39 -7
View File
@@ -1,11 +1,12 @@
use crate::input::{Input, InputColors, InputLayoutData, InputLogicalLine, InputState};
use gpui::{
Along, App, Axis, Bounds, ContentMask, CursorStyle, DispatchPhase, Element, ElementId,
Along, App, Axis, Bounds, ContentMask, CursorStyle, DispatchPhase, Display, Element, ElementId,
ElementInputHandler, Entity, Focusable, GlobalElementId, Hitbox, HitboxBehavior, Hsla,
InspectorElementId, LayoutId, Length, MouseButton, MouseDownEvent, MouseMoveEvent,
MouseUpEvent, Pixels, Point, ScrollWheelEvent, SharedString, Style, TextAlign, TextRun,
TextStyle, Window, fill, point, px, relative, size,
};
use smallvec::SmallVec;
use std::ops::Range;
const CURSOR_WIDTH: f32 = 2.0;
@@ -13,6 +14,8 @@ const MARKED_TEXT_UNDERLINE_THICKNESS: f32 = 2.0;
pub struct InputLayoutState {
text_style: TextStyle,
#[allow(dead_code)]
child_layout_ids: SmallVec<[LayoutId; 2]>,
}
pub struct InputPrepaintState {
@@ -39,6 +42,7 @@ impl Element for Input {
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut resolved_text_style = None;
let mut child_layout_ids = SmallVec::new();
let layout_id = self.interactivity.request_layout(
global_id,
@@ -46,12 +50,13 @@ impl Element for Input {
window,
cx,
|element_style, window, cx| {
let layout = self.input.read(cx).layout_style();
window.with_text_style(element_style.text_style().cloned(), |window| {
let state = self.input.read(cx);
resolved_text_style = Some(window.text_style());
let mut layout_style = element_style.clone();
if matches!(layout, super::InputLayoutStyle::MultiLine) {
if matches!(state.layout_style(), super::InputLayoutStyle::MultiLine) {
if let Length::Auto = layout_style.size.width {
layout_style.size.width = relative(1.).into();
}
@@ -59,13 +64,21 @@ impl Element for Input {
layout_style.size.height = relative(1.).into();
}
}
window.request_layout(layout_style, None, cx)
child_layout_ids = self
.cursor
.iter_mut()
.map(|cursor| cursor.request_layout(window, cx))
.collect::<SmallVec<_>>();
window.request_layout(layout_style, child_layout_ids.iter().copied(), cx)
})
},
);
let layout_state = InputLayoutState {
text_style: resolved_text_style.unwrap_or_else(|| window.text_style()),
child_layout_ids,
};
(layout_id, layout_state)
}
@@ -106,8 +119,19 @@ impl Element for Input {
bounds.size,
window,
cx,
|_style, _point, hitbox, window, _cx| {
hitbox.or_else(|| Some(window.insert_hitbox(bounds, HitboxBehavior::Normal)))
|style, scroll_offset, hitbox, window, cx| {
let hitbox =
hitbox.or_else(|| Some(window.insert_hitbox(bounds, HitboxBehavior::Normal)));
if style.display != Display::None {
window.with_element_offset(scroll_offset, |window| {
if let Some(cursor) = &mut self.cursor {
cursor.prepaint(window, cx);
}
});
}
hitbox
},
);
@@ -146,7 +170,11 @@ impl Element for Input {
input.toggle_cursor_on_focus_change(is_focused, cx)
});
let perform_paint = |_style: &Style, window: &mut Window, cx: &mut App| {
let perform_paint = |style: &Style, window: &mut Window, cx: &mut App| {
if style.display == Display::None {
return;
}
let context = PaintContext {
snapshot,
is_focused,
@@ -159,6 +187,10 @@ impl Element for Input {
context.process_mouse_events(&self.input, window, cx);
window.with_content_mask(Some(ContentMask { bounds }), |window| {
context.paint(window, cx);
if let Some(cursor) = &mut self.cursor {
cursor.paint(window, cx);
}
});
};
self.interactivity.paint(
+4 -30
View File
@@ -2,8 +2,8 @@ use super::actions::*;
use crate::input::{CursorBlinkType, InputLayoutStyle, InputStorage};
use gpui::{
App, AppContext, ClipboardItem, Context, Entity, EntityId, EntityInputHandler, EventEmitter,
FocusHandle, Focusable, NavigationDirection, Pixels, Point, SharedString, Size, Subscription,
TextRun, TextStyle, Window, WrappedLine, point, px,
FocusHandle, Focusable, NavigationDirection, Pixels, Point, Render, SharedString, Size,
Subscription, TextRun, TextStyle, Window, WrappedLine, point, px,
};
use std::{
ops::Range,
@@ -74,9 +74,6 @@ pub struct InputState {
history_undo_stack: Vec<super::HistoryEntry>,
/// Stack of undone states for redo.
history_redo_stack: Vec<super::HistoryEntry>,
/// Optional entity and subscription tracking the blinking of the text cursor.
cursor_blink: Option<(Entity<super::CursorBlink>, Subscription)>,
}
/// Data built during element prepaint that is stored in InputState for conveinence
@@ -126,7 +123,7 @@ impl InputState {
/// Creates a new `Input` with the specified multiline setting.
/// Cursor blinking is enabled by default.
pub fn new(cx: &mut Context<Self>) -> Self {
let mut this = Self {
Self {
entity_id: cx.entity_id(),
focus_handle: cx.focus_handle(),
content: Box::new(super::Standard::default()),
@@ -148,30 +145,7 @@ impl InputState {
history_grouping_interval: super::DEFAULT_GROUP_INTERVAL,
history_undo_stack: Vec::new(),
history_redo_stack: Vec::new(),
cursor_blink: None,
};
// TODO: This is unoptimal for non-blinking cases, since the entity is generated and then discarded.
this = this.cursor_blink(CursorBlinkType::Enabled {
app: cx,
interval: None,
});
this
}
/// Configure how often the cursor should blink when the input element has focus.
pub fn cursor_blink<'app>(mut self, args: CursorBlinkType<'app>) -> Self {
self.cursor_blink = match args {
CursorBlinkType::Disabled => None,
CursorBlinkType::Enabled { app: cx, interval } => {
let interval = interval.unwrap_or(super::DEFAULT_BLINK_INTERVAL);
let cursor_blink = cx.new(|_cx| super::CursorBlink::new(interval));
let entity_id = self.entity_id;
let subscription = cx.observe(&cursor_blink, move |_, cx| cx.notify(entity_id));
Some((cursor_blink, subscription))
}
};
self
}
}
/// Returns the current text content.