Skip to content

Commit

Permalink
Implement blinking cursor for text_editor
Browse files Browse the repository at this point in the history
  • Loading branch information
hecrj committed Jul 28, 2024
1 parent d1fa953 commit a6e1cba
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 28 deletions.
103 changes: 90 additions & 13 deletions widget/src/text_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ use crate::core::renderer;
use crate::core::text::editor::{Cursor, Editor as _};
use crate::core::text::highlighter::{self, Highlighter};
use crate::core::text::{self, LineHeight, Text};
use crate::core::time::{Duration, Instant};
use crate::core::widget::operation;
use crate::core::widget::{self, Widget};
use crate::core::window;
use crate::core::{
Background, Border, Color, Element, Length, Padding, Pixels, Point,
Rectangle, Shell, Size, SmolStr, Theme, Vector,
Expand Down Expand Up @@ -369,7 +371,7 @@ where
/// The state of a [`TextEditor`].
#[derive(Debug)]
pub struct State<Highlighter: text::Highlighter> {
is_focused: bool,
focus: Option<Focus>,
last_click: Option<mouse::Click>,
drag_click: Option<mouse::click::Kind>,
partial_scroll: f32,
Expand All @@ -378,26 +380,61 @@ pub struct State<Highlighter: text::Highlighter> {
highlighter_format_address: usize,
}

#[derive(Debug, Clone, Copy)]
struct Focus {
updated_at: Instant,
now: Instant,
is_window_focused: bool,
}

impl Focus {
const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;

fn now() -> Self {
let now = Instant::now();

Self {
updated_at: now,
now,
is_window_focused: true,
}
}

fn is_cursor_visible(&self) -> bool {
self.is_window_focused
&& ((self.now - self.updated_at).as_millis()
/ Self::CURSOR_BLINK_INTERVAL_MILLIS)
% 2
== 0
}
}

impl<Highlighter: text::Highlighter> State<Highlighter> {
/// Returns whether the [`TextEditor`] is currently focused or not.
pub fn is_focused(&self) -> bool {
self.is_focused
self.focus.is_some()
}
}

impl<Highlighter: text::Highlighter> operation::Focusable
for State<Highlighter>
{
fn is_focused(&self) -> bool {
self.is_focused
self.focus.is_some()
}

fn focus(&mut self) {
self.is_focused = true;
let now = Instant::now();

self.focus = Some(Focus {
updated_at: now,
now,
is_window_focused: true,
});
}

fn unfocus(&mut self) {
self.is_focused = false;
self.focus = None;
}
}

Expand All @@ -414,7 +451,7 @@ where

fn state(&self) -> widget::tree::State {
widget::tree::State::new(State {
is_focused: false,
focus: None,
last_click: None,
drag_click: None,
partial_scroll: 0.0,
Expand Down Expand Up @@ -502,6 +539,41 @@ where

let state = tree.state.downcast_mut::<State<Highlighter>>();

match event {
Event::Window(window::Event::Unfocused) => {
if let Some(focus) = &mut state.focus {
focus.is_window_focused = false;
}
}
Event::Window(window::Event::Focused) => {
if let Some(focus) = &mut state.focus {
focus.is_window_focused = true;
focus.updated_at = Instant::now();

shell.request_redraw(window::RedrawRequest::NextFrame);
}
}
Event::Window(window::Event::RedrawRequested(now)) => {
if let Some(focus) = &mut state.focus {
if focus.is_window_focused {
focus.now = now;

let millis_until_redraw =
Focus::CURSOR_BLINK_INTERVAL_MILLIS
- (now - focus.updated_at).as_millis()
% Focus::CURSOR_BLINK_INTERVAL_MILLIS;

shell.request_redraw(window::RedrawRequest::At(
now + Duration::from_millis(
millis_until_redraw as u64,
),
));
}
}
}
_ => {}
}

let Some(update) = Update::from_event(
event,
state,
Expand All @@ -523,7 +595,7 @@ where
mouse::click::Kind::Triple => Action::SelectLine,
};

state.is_focused = true;
state.focus = Some(Focus::now());
state.last_click = Some(click);
state.drag_click = Some(click.kind());

Expand Down Expand Up @@ -566,7 +638,7 @@ where

match binding {
Binding::Unfocus => {
state.is_focused = false;
state.focus = None;
state.drag_click = None;
}
Binding::Copy => {
Expand Down Expand Up @@ -645,6 +717,10 @@ where
clipboard,
shell,
);

if let Some(focus) = &mut state.focus {
focus.updated_at = Instant::now();
}
}
}

Expand Down Expand Up @@ -679,7 +755,7 @@ where

let status = if is_disabled {
Status::Disabled
} else if state.is_focused {
} else if state.focus.is_some() {
Status::Focused
} else if is_mouse_over {
Status::Hovered
Expand Down Expand Up @@ -740,9 +816,9 @@ where
bounds.y + self.padding.top,
);

if state.is_focused {
if let Some(focus) = state.focus.as_ref() {
match internal.editor.cursor() {
Cursor::Caret(position) => {
Cursor::Caret(position) if focus.is_cursor_visible() => {
let cursor =
Rectangle::new(
position + translation,
Expand Down Expand Up @@ -784,6 +860,7 @@ where
);
}
}
_ => {}

Check failure on line 863 in widget/src/text_editor.rs

View workflow job for this annotation

GitHub Actions / all

wildcard matches only a single variant and will also match any future added variants
}
}
}
Expand Down Expand Up @@ -990,7 +1067,7 @@ impl<Message> Update<Message> {
);

Some(Update::Click(click))
} else if state.is_focused {
} else if state.focus.is_some() {
binding(Binding::Unfocus)
} else {
None
Expand Down Expand Up @@ -1030,7 +1107,7 @@ impl<Message> Update<Message> {
text,
..
}) => {
let status = if state.is_focused {
let status = if state.focus.is_some() {
Status::Focused
} else {
Status::Active
Expand Down
15 changes: 0 additions & 15 deletions widget/src/text_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1210,21 +1210,6 @@ impl<P: text::Paragraph> State<P> {
Self::default()
}

/// Creates a new [`State`], representing a focused [`TextInput`].
pub fn focused() -> Self {
Self {
value: paragraph::Plain::default(),
placeholder: paragraph::Plain::default(),
icon: paragraph::Plain::default(),
is_focused: None,
is_dragging: false,
is_pasting: None,
last_click: None,
cursor: Cursor::default(),
keyboard_modifiers: keyboard::Modifiers::default(),
}
}

/// Returns whether the [`TextInput`] is currently focused or not.
pub fn is_focused(&self) -> bool {
self.is_focused.is_some()
Expand Down

0 comments on commit a6e1cba

Please sign in to comment.