Skip to content

Commit

Permalink
wip: add cursor animation
Browse files Browse the repository at this point in the history
  • Loading branch information
lazytanuki committed May 15, 2023
1 parent 5eb2cbf commit 315408b
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 29 deletions.
178 changes: 149 additions & 29 deletions native/src/widget/text_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod value;
pub mod cursor;

pub use cursor::Cursor;
use iced_style::animation::AnimationDirection;
pub use value::Value;

use editor::Editor;
Expand All @@ -31,6 +32,8 @@ use crate::{

pub use iced_style::text_input::{Appearance, StyleSheet};

use self::cursor::CursorAnimation;

/// A field that can be filled with text.
///
/// # Example
Expand Down Expand Up @@ -69,6 +72,7 @@ where
on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_submit: Option<Message>,
style: <Renderer::Theme as StyleSheet>::Style,
cursor_animation_style: CursorAnimation,
}

impl<'a, Message, Renderer> TextInput<'a, Message, Renderer>
Expand Down Expand Up @@ -100,6 +104,7 @@ where
on_paste: None,
on_submit: None,
style: Default::default(),
cursor_animation_style: Default::default(),
}
}

Expand Down Expand Up @@ -166,6 +171,15 @@ where
self
}

/// Sets the animation style of the [`Cursor`].
pub fn set_cursor_animation_style(
mut self,
cursor_animation_style: CursorAnimation,
) -> Self {
self.cursor_animation_style = cursor_animation_style;
self
}

/// Draws the [`TextInput`] with the given [`Renderer`], overriding its
/// [`Value`] if provided.
///
Expand All @@ -191,6 +205,7 @@ where
&self.font,
self.is_secure,
&self.style,
self.cursor_animation_style,
)
}
}
Expand Down Expand Up @@ -264,6 +279,7 @@ where
self.on_paste.as_deref(),
&self.on_submit,
|| tree.state.downcast_mut::<State>(),
self.cursor_animation_style,
)
}

Expand All @@ -289,6 +305,7 @@ where
&self.font,
self.is_secure,
&self.style,
self.cursor_animation_style,
)
}

Expand Down Expand Up @@ -412,6 +429,7 @@ pub fn update<'a, Message, Renderer>(
on_paste: Option<&dyn Fn(String) -> Message>,
on_submit: &Option<Message>,
state: impl FnOnce() -> &'a mut State,
cursor_animation_style: CursorAnimation,
) -> event::Status
where
Message: Clone,
Expand Down Expand Up @@ -564,6 +582,8 @@ where

focus.updated_at = Instant::now();

reset_cursor(cursor_animation_style, state);

return event::Status::Captured;
}
}
Expand All @@ -575,6 +595,8 @@ where
let modifiers = state.keyboard_modifiers;
focus.updated_at = Instant::now();

reset_cursor(cursor_animation_style, state);

match key_code {
keyboard::KeyCode::Enter
| keyboard::KeyCode::NumpadEnter => {
Expand Down Expand Up @@ -784,13 +806,73 @@ where
if let Some(focus) = &mut state.is_focused {
focus.now = now;

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

shell.request_redraw(window::RedrawRequest::At(
now + Duration::from_millis(millis_until_redraw as u64),
));
match cursor_animation_style {
CursorAnimation::Blink => {
let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS
- (now - focus.updated_at).as_millis()
% CURSOR_BLINK_INTERVAL_MILLIS;

shell.request_redraw(window::RedrawRequest::At(
now + Duration::from_millis(
millis_until_redraw as u64,
),
));
}
CursorAnimation::Fade => {
if let Some(last_direction_change) =
&mut state.cursor_animation_last_direction_change
{
match state.cursor_animation_direction {
AnimationDirection::Forward => {
let interval_ms = (now
- *last_direction_change)
.as_millis()
as f32;
let opacity_diff = interval_ms
/ (CURSOR_FADE_INTERVAL_MILLIS as f32);
state.cursor_opacity = opacity_diff;
if state.cursor_opacity >= 1.0 {
state.cursor_animation_direction =
AnimationDirection::Backward;
state.cursor_opacity = 1.0;
*last_direction_change = now;
}
}
AnimationDirection::Backward => {
let interval_ms = (now
- *last_direction_change)
.as_millis()
as f32;

// Make a pause at full opacity
if interval_ms
> CURSOR_FADE_INTERVAL_MILLIS_PAUSE
{
let opacity_diff = (interval_ms
- CURSOR_FADE_INTERVAL_MILLIS_PAUSE)
/ (CURSOR_FADE_INTERVAL_MILLIS
as f32);
state.cursor_opacity =
1.0 - opacity_diff;
}
if state.cursor_opacity <= 0.0 {
state.cursor_animation_direction =
AnimationDirection::Forward;
state.cursor_opacity = 0.0;
*last_direction_change = now;
}
}
}
} else {
state.cursor_animation_last_direction_change =
Some(now);
state.cursor_opacity = 1.0;
state.cursor_animation_direction =
AnimationDirection::Backward;
}
shell.request_redraw(window::RedrawRequest::NextFrame);
}
}
}
}
_ => {}
Expand All @@ -799,6 +881,14 @@ where
event::Status::Ignored
}

fn reset_cursor(cursor_animation_style: CursorAnimation, state: &mut State) {
if let CursorAnimation::Fade = cursor_animation_style {
state.cursor_animation_direction = AnimationDirection::Backward;
state.cursor_opacity = 1.0;
state.cursor_animation_last_direction_change = None;
}
}

/// Draws the [`TextInput`] with the given [`Renderer`], overriding its
/// [`Value`] if provided.
///
Expand All @@ -815,6 +905,7 @@ pub fn draw<Renderer>(
font: &Renderer::Font,
is_secure: bool,
style: &<Renderer::Theme as StyleSheet>::Style,
cursor_animation_style: CursorAnimation,
) where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
Expand Down Expand Up @@ -861,29 +952,50 @@ pub fn draw<Renderer>(
font.clone(),
);

let is_cursor_visible = ((focus.now - focus.updated_at)
.as_millis()
/ CURSOR_BLINK_INTERVAL_MILLIS)
% 2
== 0;

let cursor = if is_cursor_visible {
Some((
renderer::Quad {
bounds: Rectangle {
x: text_bounds.x + text_value_width,
y: text_bounds.y,
width: 1.0,
height: text_bounds.height,
let cursor = match cursor_animation_style {
CursorAnimation::Blink => {
// Check if cursor is visible
if ((focus.now - focus.updated_at).as_millis()
/ CURSOR_BLINK_INTERVAL_MILLIS)
% 2
== 0
{
Some((
renderer::Quad {
bounds: Rectangle {
x: text_bounds.x + text_value_width,
y: text_bounds.y,
width: 1.0,
height: text_bounds.height,
},
border_radius: 0.0.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
theme.value_color(style),
))
} else {
None
}
}
CursorAnimation::Fade => {
let mut color = theme.value_color(style);
color.a = state.cursor_opacity;
Some((
renderer::Quad {
bounds: Rectangle {
x: text_bounds.x + text_value_width,
y: text_bounds.y,
width: 1.0,
height: text_bounds.height,
},
border_radius: 0.0.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
border_radius: 0.0.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
theme.value_color(style),
))
} else {
None
color,
))
}
};

(cursor, offset)
Expand Down Expand Up @@ -1001,6 +1113,9 @@ pub struct State {
last_click: Option<mouse::Click>,
cursor: Cursor,
keyboard_modifiers: keyboard::Modifiers,
cursor_opacity: f32,
cursor_animation_direction: AnimationDirection,
cursor_animation_last_direction_change: Option<Instant>,
// TODO: Add stateful horizontal scrolling offset
}

Expand All @@ -1025,6 +1140,9 @@ impl State {
last_click: None,
cursor: Cursor::default(),
keyboard_modifiers: keyboard::Modifiers::default(),
cursor_opacity: 1.0,
cursor_animation_direction: AnimationDirection::Backward,
cursor_animation_last_direction_change: None,
}
}

Expand Down Expand Up @@ -1207,3 +1325,5 @@ where
}

const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;
const CURSOR_FADE_INTERVAL_MILLIS: u128 = 600;
const CURSOR_FADE_INTERVAL_MILLIS_PAUSE: f32 = 400.0;
10 changes: 10 additions & 0 deletions native/src/widget/text_input/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ pub struct Cursor {
state: State,
}

/// The animation styles for the [`Cursor`].
#[derive(Debug, Clone, Copy, Default)]
pub enum CursorAnimation {
/// Blink the cursor (visible then not visible in a loop)
Blink,
/// Fade from visible to invisible in a loop
#[default]
Fade,
}

/// The state of a [`Cursor`].
#[derive(Debug, Copy, Clone)]
pub enum State {
Expand Down

0 comments on commit 315408b

Please sign in to comment.