add caret blinking which is handled via a separate entity
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
pub mod actions;
|
||||
mod caret;
|
||||
mod element;
|
||||
mod history;
|
||||
pub mod notify;
|
||||
mod state;
|
||||
mod storage;
|
||||
|
||||
@@ -11,7 +11,6 @@ pub use storage::*;
|
||||
|
||||
/* TODO list
|
||||
- remove gpuikit based input
|
||||
- cursor blinking
|
||||
- text sanitation
|
||||
- add page up/down actions to nav by an entire page or expand selection by an entire page
|
||||
- test IME (char palette only available on macos)
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{Context, Entity, EventEmitter, Subscription};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// Default interval for caret blinking.
|
||||
pub const DEFAULT_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
||||
|
||||
pub enum CaretNotify {
|
||||
PauseBlinking,
|
||||
}
|
||||
|
||||
pub struct Caret {
|
||||
/// The frequency at which the caret blinks
|
||||
interval: Duration,
|
||||
generation: usize,
|
||||
/// Whether the caret is presently visible in this frame
|
||||
visible: bool,
|
||||
/// Whether the caret is currently able to blink
|
||||
active: bool,
|
||||
/// true when blinking is active but paused for some number of frames
|
||||
paused: bool,
|
||||
#[allow(dead_code)]
|
||||
subscriptions: SmallVec<[Subscription; 2]>,
|
||||
/// Tracks whether we were focused on the last update.
|
||||
was_focused: bool,
|
||||
}
|
||||
impl Default for Caret {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
interval: Duration::ZERO,
|
||||
generation: Default::default(),
|
||||
visible: false,
|
||||
active: false,
|
||||
paused: false,
|
||||
subscriptions: SmallVec::new(),
|
||||
was_focused: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Caret {
|
||||
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<CaretNotify>,
|
||||
{
|
||||
let handle = cx.subscribe(emitter, |state, _emitter, event, cx| match event {
|
||||
CaretNotify::PauseBlinking => {
|
||||
if !state.interval.is_zero() {
|
||||
state.pause_blinking(cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
});
|
||||
self.subscriptions.push(handle);
|
||||
}
|
||||
|
||||
/// Processes updates during prepaint and returns whether the caret is currently visible.
|
||||
pub fn update_focus(&mut self, is_focused: bool, cx: &mut Context<Self>) -> bool {
|
||||
let was_focused = self.was_focused;
|
||||
self.was_focused = is_focused;
|
||||
|
||||
match (self.interval.is_zero(), is_focused, was_focused) {
|
||||
(true, _, _) => true,
|
||||
(false, true, false) => {
|
||||
self.enable(cx);
|
||||
true
|
||||
}
|
||||
(false, false, true) => {
|
||||
self.disable(cx);
|
||||
false
|
||||
}
|
||||
(false, _, _) => self.visible,
|
||||
}
|
||||
}
|
||||
|
||||
/// Activates caret blinking.
|
||||
///
|
||||
/// While active, the caret will alternate between visible and hidden states at the
|
||||
/// configured interval. Has no effect if already active.
|
||||
fn enable(&mut self, cx: &mut Context<Self>) {
|
||||
if self.active {
|
||||
return;
|
||||
}
|
||||
|
||||
self.active = true;
|
||||
self.visible = false;
|
||||
self.paused = false;
|
||||
self.spawn_ticker(cx);
|
||||
}
|
||||
|
||||
/// Deactivates caret blinking.
|
||||
///
|
||||
/// Marks the caret 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 caret visible.
|
||||
fn disable(&mut self, cx: &mut Context<Self>) {
|
||||
self.active = false;
|
||||
self.visible = false;
|
||||
self.paused = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Temporarily pauses blinking and leaves the caret visible. Blinking will resume after
|
||||
/// the pre-established interval elapses from the time this is called.
|
||||
fn pause_blinking(&mut self, cx: &mut Context<Self>) {
|
||||
if !self.visible {
|
||||
self.visible = true;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
self.paused = true;
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
|
||||
let generation = self.generation;
|
||||
let interval = self.interval;
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
async_io::Timer::after(interval).await;
|
||||
this.update(cx, |this, cx| {
|
||||
if this.generation == generation {
|
||||
this.paused = false;
|
||||
this.spawn_ticker(cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn spawn_ticker(&mut self, cx: &mut Context<Self>) {
|
||||
if !self.active || self.paused {
|
||||
return;
|
||||
}
|
||||
|
||||
self.visible = !self.visible;
|
||||
cx.notify();
|
||||
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
let generation = self.generation;
|
||||
let interval = self.interval;
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
async_io::Timer::after(interval).await;
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(cx, |this, cx| {
|
||||
if this.generation == generation {
|
||||
this.spawn_ticker(cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
@@ -197,6 +197,7 @@ struct InteractivityPrepaint {
|
||||
hitbox: Option<Hitbox>,
|
||||
scroll_offset: Point<Pixels>,
|
||||
inner_bounds: Bounds<Pixels>,
|
||||
caret_visible: bool,
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
@@ -424,10 +425,18 @@ impl Element for EditableTextElement {
|
||||
) -> Self::PrepaintState {
|
||||
// should reflect the text content layout size of the stored text,
|
||||
// so that scrolling can take it into account during prepaint.
|
||||
let content_size = {
|
||||
let content_size;
|
||||
let caret;
|
||||
let focus_handle;
|
||||
{
|
||||
let state = request_layout.state.read(cx);
|
||||
state.layout_data.size.unwrap_or_else(|| bounds.size)
|
||||
};
|
||||
content_size = state.layout_data.size.unwrap_or_else(|| bounds.size);
|
||||
caret = state.caret_entity().clone();
|
||||
focus_handle = state.focus_handle(cx);
|
||||
}
|
||||
|
||||
let is_focused = focus_handle.is_focused(window);
|
||||
let caret_visible = caret.update(cx, |caret, cx| caret.update_focus(is_focused, cx));
|
||||
|
||||
let prepaint = self.interactivity().prepaint(
|
||||
global_id,
|
||||
@@ -460,12 +469,12 @@ impl Element for EditableTextElement {
|
||||
hitbox,
|
||||
scroll_offset,
|
||||
inner_bounds,
|
||||
caret_visible,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let state = request_layout.state.read(cx);
|
||||
let focus_handle = state.focus_handle(cx);
|
||||
let elements = self.build_elements(state, &prepaint, window);
|
||||
|
||||
PrepaintState {
|
||||
@@ -586,6 +595,7 @@ impl EditableTextElement {
|
||||
hitbox: _,
|
||||
scroll_offset,
|
||||
inner_bounds,
|
||||
caret_visible,
|
||||
} = prepaint;
|
||||
|
||||
let caret_pos = state.caret_pos();
|
||||
@@ -691,9 +701,7 @@ impl EditableTextElement {
|
||||
}
|
||||
}
|
||||
|
||||
if state.is_caret_visible(window)
|
||||
&& let Some(carent_point) = caret_point
|
||||
{
|
||||
if *caret_visible && let Some(carent_point) = caret_point {
|
||||
const CURSOR_WIDTH: f32 = 2.0;
|
||||
let quad = fill(
|
||||
Bounds::new(
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
use std::{ops::Range, time::Instant};
|
||||
|
||||
use crate::editable_text::UnicodeTextStorage;
|
||||
|
||||
pub struct TextChanged;
|
||||
|
||||
pub struct TextHistoryPushed {
|
||||
pub timestamp: Instant,
|
||||
pub modified_range: Range<usize>,
|
||||
pub text_payload: String,
|
||||
pub new_length: usize,
|
||||
pub selected_range: Range<usize>,
|
||||
}
|
||||
impl TextHistoryPushed {
|
||||
pub fn new(
|
||||
range: Range<usize>,
|
||||
new_length: usize,
|
||||
storage: &dyn UnicodeTextStorage,
|
||||
selected_range: Range<usize>,
|
||||
) -> Self {
|
||||
let timestamp = Instant::now();
|
||||
let modified_range = range.start..range.start + new_length;
|
||||
// NOTE: not performant to allocate a new text payload if the event doesnt
|
||||
// need to be logged (based on timestamp). Should consider a more robust way to access
|
||||
// the storage only if it absolutely needs to be cloned from.
|
||||
let text_payload = storage.content_utf8()[range].to_string();
|
||||
Self {
|
||||
timestamp,
|
||||
modified_range,
|
||||
text_payload,
|
||||
new_length,
|
||||
selected_range,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_to_redo(self, content: &str) -> Self {
|
||||
let undo_start = self.modified_range.start;
|
||||
let undo_end = (self.modified_range.start + self.new_length).min(content.len());
|
||||
let text_payload = content[undo_start..undo_end].to_string();
|
||||
let new_length = self.text_payload.len();
|
||||
Self {
|
||||
timestamp: self.timestamp,
|
||||
modified_range: undo_start..undo_start + self.text_payload.len(),
|
||||
text_payload,
|
||||
new_length,
|
||||
selected_range: self.selected_range,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,21 @@
|
||||
use crate::editable_text::{
|
||||
TextBoundary, UnicodeTextStorage,
|
||||
actions::EditableTextActionHandler,
|
||||
caret::{Caret, CaretNotify},
|
||||
history::EditableTextHistory,
|
||||
notify::{TextChanged, TextHistoryPushed},
|
||||
};
|
||||
use gpui::{
|
||||
App, Bounds, ClipboardItem, Context, EntityInputHandler, EventEmitter, FocusHandle, Focusable,
|
||||
NavigationDirection, Pixels, Point, Size, UTF16Selection, Window, WrappedLine, point,
|
||||
App, Bounds, ClipboardItem, Context, Entity, EntityInputHandler, EventEmitter, FocusHandle,
|
||||
Focusable, NavigationDirection, Pixels, Point, Size, UTF16Selection, Window, WrappedLine,
|
||||
point,
|
||||
};
|
||||
use std::{borrow::Cow, ops::Range, sync::Arc};
|
||||
|
||||
pub struct TextChanged;
|
||||
|
||||
pub struct EditableTextState {
|
||||
storage: Box<dyn UnicodeTextStorage>,
|
||||
caret: Entity<Caret>,
|
||||
|
||||
/// The utf-8 character range that is currently selected by the user.
|
||||
/// Valid both when start < end and start > end (which dictates the direction of the selection). Empty when start==end.
|
||||
@@ -91,7 +95,7 @@ impl TextLineSegment {
|
||||
}
|
||||
|
||||
impl EventEmitter<TextChanged> for EditableTextState {}
|
||||
impl EventEmitter<TextHistoryPushed> for EditableTextState {}
|
||||
impl EventEmitter<CaretNotify> for EditableTextState {}
|
||||
|
||||
impl Focusable for EditableTextState {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
@@ -101,8 +105,18 @@ impl Focusable for EditableTextState {
|
||||
|
||||
impl EditableTextState {
|
||||
pub fn new(storage: impl Into<Box<dyn UnicodeTextStorage>>, cx: &mut Context<Self>) -> Self {
|
||||
use gpui::AppContext;
|
||||
let caret = cx.new({
|
||||
let state_entity = cx.entity();
|
||||
move |cx| {
|
||||
let mut caret = Caret::default().blink_interval_default();
|
||||
caret.subscribe_to(&state_entity, cx);
|
||||
caret
|
||||
}
|
||||
});
|
||||
Self {
|
||||
storage: storage.into(),
|
||||
caret,
|
||||
|
||||
selected_range: 0..0,
|
||||
marked_range: None,
|
||||
@@ -138,6 +152,10 @@ impl EditableTextState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn caret_entity(&self) -> &Entity<Caret> {
|
||||
&self.caret
|
||||
}
|
||||
|
||||
pub fn caret_pos(&self) -> usize {
|
||||
self.selected_range.start
|
||||
}
|
||||
@@ -310,7 +328,7 @@ impl EditableTextState {
|
||||
}
|
||||
|
||||
pub fn move_to(&mut self, caret_pos: usize, cx: &mut Context<Self>) {
|
||||
//cx.emit(CursorTrigger::PauseBlinkingForUserAction);
|
||||
cx.emit(CaretNotify::PauseBlinking);
|
||||
let caret_pos = caret_pos.min(self.storage.content_utf8().len());
|
||||
self.selected_range = caret_pos..caret_pos;
|
||||
self.scroll_to_caret();
|
||||
@@ -318,7 +336,7 @@ impl EditableTextState {
|
||||
}
|
||||
|
||||
pub fn select_to(&mut self, caret_pos: usize, cx: &mut Context<Self>) {
|
||||
//cx.emit(CursorTrigger::PauseBlinkingForUserAction);
|
||||
cx.emit(CaretNotify::PauseBlinking);
|
||||
let caret_pos = caret_pos.min(self.storage().content_utf8().len());
|
||||
self.selected_range.start = caret_pos;
|
||||
self.scroll_to_caret();
|
||||
@@ -528,13 +546,6 @@ impl EditableTextState {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditableTextState {
|
||||
pub fn is_caret_visible(&self, window: &Window) -> bool {
|
||||
// TODO: Cursor blinking
|
||||
self.focus_handle.is_focused(window)
|
||||
}
|
||||
}
|
||||
|
||||
impl EntityInputHandler for EditableTextState {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
@@ -587,7 +598,7 @@ impl EntityInputHandler for EditableTextState {
|
||||
) {
|
||||
let range_utf8 = self.ime_resolve_range(range_utf16);
|
||||
self.replace_text_in_range_bytes(range_utf8, text_to_insert, cx);
|
||||
//cx.emit(CursorTrigger::PauseBlinkingForUserAction);
|
||||
cx.emit(CaretNotify::PauseBlinking);
|
||||
cx.emit(TextChanged);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user