flip cursor implementation to have an element and a state-entity

This commit is contained in:
temportalflux
2026-07-11 09:31:07 -04:00
parent 6ebe87025b
commit b3ab491240
5 changed files with 111 additions and 73 deletions
+2 -7
View File
@@ -11,7 +11,7 @@ mod storage;
pub(self) mod unicode;
pub use colors::*;
pub(self) use cursor::*;
pub use cursor::*;
pub use element::*;
pub(self) use history::*;
pub use layout::*;
@@ -22,10 +22,5 @@ pub use storage::*;
fn make_element(app: &mut gpui::App) -> impl gpui::IntoElement {
use gpui::AppContext;
let state = app.new(|cx| InputState::new(cx));
let cursor = app.new(|cx| {
let mut cursor = Cursor::new(None);
cursor.subscribe_to(&state, cx);
cursor
});
input(&state, app).cursor(cursor)
input(&state, app).text_cursor(default_cursor(&state, app))
}
+77 -30
View File
@@ -1,5 +1,5 @@
use gpui::{
Bounds, Context, Element, Entity, EventEmitter, Hsla, IntoElement, Pixels, Point, Render,
App, Bounds, Context, Element, Entity, EventEmitter, Hsla, IntoElement, Pixels, Point, Render,
Subscription,
};
use smallvec::SmallVec;
@@ -13,35 +13,63 @@ pub const DEFAULT_BLINK_INTERVAL: Duration = Duration::from_millis(500);
/// The state of an input's cursor blinking. While active, the cursor's visibility changes at some interval.
/// This blinking can be temporarily paused (e.g. during typing).
pub struct Cursor {
interval: Duration,
generation: usize,
visible: bool,
active: bool,
paused: bool,
state: Entity<CursorState>,
color: Hsla,
/// Tracks whether we were focused on the last update.
was_focused: bool,
point: Point<Pixels>,
height: Pixels,
}
pub struct CursorState {
interval: Duration,
generation: usize,
visible: bool,
active: bool,
paused: bool,
#[allow(dead_code)]
subscriptions: SmallVec<[Subscription; 2]>,
}
impl Default for CursorState {
fn default() -> Self {
Self {
interval: Duration::ZERO,
generation: Default::default(),
visible: true,
active: Default::default(),
paused: Default::default(),
subscriptions: SmallVec::new(),
}
}
}
#[track_caller]
pub fn cursor(state: Entity<CursorState>) -> Cursor {
Cursor::new(state)
}
#[track_caller]
pub fn default_cursor<E>(emitter: &Entity<E>, cx: &mut App) -> Cursor
where
E: EventEmitter<CursorTrigger>,
{
use gpui::AppContext;
cursor(cx.new(|cx| {
let mut cursor = CursorState::default().blink_interval_default();
cursor.subscribe_to(emitter, cx);
cursor
}))
}
impl Cursor {
/// Initializes the cursor blinking with the cursor already being visible.
#[track_caller]
pub fn new(interval: Option<Duration>) -> Self {
fn new(state: Entity<CursorState>) -> Self {
Self {
interval: interval.unwrap_or_default(),
generation: 0,
visible: true,
active: false,
paused: false,
state,
color: Hsla::white(),
was_focused: false,
point: Point::default(),
height: Pixels::ZERO,
subscriptions: SmallVec::new(),
}
}
@@ -49,29 +77,38 @@ impl Cursor {
self.color = color;
self
}
}
impl CursorState {
pub fn blink_interval_default(mut self) -> Self {
self.interval = DEFAULT_BLINK_INTERVAL;
self
}
pub fn blink_interval(mut self, interval: Duration) -> Self {
self.interval = interval;
self
}
pub fn subscribe_to<E>(&mut self, emitter: &Entity<E>, cx: &mut Context<Self>)
where
E: EventEmitter<CursorTrigger>,
{
let handle = cx.subscribe(emitter, |cursor, _emitter, event, cx| match event {
let handle = cx.subscribe(emitter, |state, _emitter, event, cx| match event {
CursorTrigger::PauseBlinkingForUserAction => {
cursor.pause_blinking(cx);
cx.notify();
if !state.interval.is_zero() {
state.pause_blinking(cx);
cx.notify();
}
}
});
self.subscriptions.push(handle);
}
/// Returns whether the cursor should currently be rendered.
pub fn visible(&self) -> bool {
self.visible
}
/// Activates cursor blinking.
///
/// While active, the cursor will alternate between visible and hidden states at the configured interval. Has no effect if already active.
pub fn enable(&mut self, cx: &mut Context<Self>) {
fn enable(&mut self, cx: &mut Context<Self>) {
if self.active {
return;
}
@@ -86,7 +123,7 @@ impl Cursor {
///
/// Marks the cursor as invisible and pauses blinking indefinitely. `enable` must be called explicitly to resume visibility and blinking.
/// Call `pause_blinking` instead if you want to temporarily stop blinking while keeping the cursor visible.
pub fn disable(&mut self, cx: &mut Context<Self>) {
fn disable(&mut self, cx: &mut Context<Self>) {
self.active = false;
self.visible = false;
self.paused = false;
@@ -94,7 +131,7 @@ impl Cursor {
}
/// Temporarily pauses blinking and leaves the cursor visible. Blinking will resume after the pre-established interval elapses from the time this is called.
pub fn pause_blinking(&mut self, cx: &mut Context<Self>) {
fn pause_blinking(&mut self, cx: &mut Context<Self>) {
if !self.visible {
self.visible = true;
cx.notify();
@@ -142,13 +179,15 @@ impl Cursor {
})
.detach();
}
}
impl Cursor {
pub fn update_input(
&mut self,
is_focused: bool,
pos: Point<Pixels>,
line_height: Pixels,
cx: &mut Context<Self>,
cx: &mut App,
) -> bool {
let was_focused = self.was_focused;
self.was_focused = is_focused;
@@ -156,17 +195,25 @@ impl Cursor {
self.point = pos;
self.height = line_height;
match (self.interval.is_zero(), is_focused, was_focused) {
match (
self.state.read(cx).interval.is_zero(),
is_focused,
was_focused,
) {
(true, _, _) => true,
(false, true, false) => {
self.enable(cx);
self.state.update(cx, |state, cx| {
state.enable(cx);
});
true
}
(false, false, true) => {
self.disable(cx);
self.state.update(cx, |state, cx| {
state.disable(cx);
});
false
}
(false, _, _) => self.visible,
(false, _, _) => self.state.read(cx).visible,
}
}
}
+3 -3
View File
@@ -15,7 +15,7 @@ pub struct Input {
pub(super) interactivity: Interactivity,
pub(super) placeholder: Option<SharedString>,
pub(super) colors: InputColors,
pub(super) cursor: Option<Entity<Cursor>>,
pub(super) cursor: Option<Cursor>,
}
impl Input {
@@ -159,8 +159,8 @@ impl Input {
self
}
pub fn cursor(mut self, entity: Entity<Cursor>) -> Self {
self.cursor = Some(entity);
pub fn text_cursor(mut self, cursor: Cursor) -> Self {
self.cursor = Some(cursor);
self
}
}
+27 -32
View File
@@ -67,10 +67,9 @@ impl Element for Input {
}
}
if let Some(cursor) = &self.cursor {
let (layout_id, layout) = cursor.update(cx, |cursor, cx| {
cursor.request_layout(global_id, inspector_id, window, cx)
});
if let Some(cursor) = &mut self.cursor {
let (layout_id, layout) =
cursor.request_layout(global_id, inspector_id, window, cx);
child_layout_ids.push(layout_id);
cursor_layout = Some(layout);
}
@@ -133,16 +132,14 @@ impl Element for Input {
window.with_element_offset(scroll_offset, |window| {
match (&mut self.cursor, &mut layout_state.cursor_layout) {
(Some(cursor), Some(layout)) => {
let prepaint = cursor.update(cx, |cursor, cx| {
cursor.prepaint(
global_id,
inspector_id,
bounds,
layout,
window,
cx,
)
});
let prepaint = cursor.prepaint(
global_id,
inspector_id,
bounds,
layout,
window,
cx,
);
cursor_prepaint = Some(prepaint);
}
_ => {}
@@ -211,26 +208,24 @@ impl Element for Input {
&mut prepaint_state.cursor_prepaint,
) {
(Some(cursor), Some(layout), Some(prepaint)) => {
cursor.update(cx, |cursor, cx| {
let cursor_pos = context.find_cursor_position_in_layouts();
let visible = cursor.update_input(
is_focused,
cursor_pos,
context.snapshot.line_height,
let cursor_pos = context.find_cursor_position_in_layouts();
let visible = cursor.update_input(
is_focused,
cursor_pos,
context.snapshot.line_height,
cx,
);
if is_focused && visible && context.snapshot.selected_range.is_empty() {
cursor.paint(
global_id,
inspector_id,
bounds,
layout,
prepaint,
window,
cx,
);
if is_focused && visible && context.snapshot.selected_range.is_empty() {
cursor.paint(
global_id,
inspector_id,
bounds,
layout,
prepaint,
window,
cx,
);
}
});
}
}
_ => {}
}
+2 -1
View File
@@ -16,8 +16,10 @@ use unicode_segmentation::UnicodeSegmentation;
#[derive(Clone, Debug)]
pub enum InputStateEvent {
/// Emitted when the input gains focus.
/// TODO: an emit was removed from element painting
Focus,
/// Emitted when the input loses focus.
/// TODO: an emit was removed from element painting
Blur,
/// Emitted when the text content changes.
TextChanged,
@@ -30,7 +32,6 @@ impl EventEmitter<InputStateEvent> for InputState {}
#[derive(Clone, Debug)]
pub enum CursorTrigger {
// TODO: cursor needs to receive this
PauseBlinkingForUserAction,
}
impl EventEmitter<CursorTrigger> for InputState {}