summaryrefslogtreecommitdiffstats
path: root/native/src
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón <hector0193@gmail.com>2023-01-13 20:33:59 +0100
committerLibravatar GitHub <noreply@github.com>2023-01-13 20:33:59 +0100
commit597af315af714f3ecadd195516e80506504dcb26 (patch)
tree200d0447efc60c9066b762c9f1a00ce55d66def7 /native/src
parentd2b66805b350b4bd2ca5cb2818a6c05e3cfb5235 (diff)
parent507820a8438cec25074f92b72e118e0931fa7f9f (diff)
downloadiced-597af315af714f3ecadd195516e80506504dcb26.tar.gz
iced-597af315af714f3ecadd195516e80506504dcb26.tar.bz2
iced-597af315af714f3ecadd195516e80506504dcb26.zip
Merge pull request #1647 from iced-rs/feature/widget-request-redraw
Widget-driven animations
Diffstat (limited to 'native/src')
-rw-r--r--native/src/renderer.rs6
-rw-r--r--native/src/shell.rs60
-rw-r--r--native/src/subscription.rs33
-rw-r--r--native/src/user_interface.rs43
-rw-r--r--native/src/widget/text_input.rs103
-rw-r--r--native/src/window.rs20
-rw-r--r--native/src/window/event.rs7
-rw-r--r--native/src/window/redraw_request.rs38
8 files changed, 259 insertions, 51 deletions
diff --git a/native/src/renderer.rs b/native/src/renderer.rs
index 5e776be6..d5329acd 100644
--- a/native/src/renderer.rs
+++ b/native/src/renderer.rs
@@ -36,11 +36,11 @@ pub trait Renderer: Sized {
f: impl FnOnce(&mut Self),
);
- /// Clears all of the recorded primitives in the [`Renderer`].
- fn clear(&mut self);
-
/// Fills a [`Quad`] with the provided [`Background`].
fn fill_quad(&mut self, quad: Quad, background: impl Into<Background>);
+
+ /// Clears all of the recorded primitives in the [`Renderer`].
+ fn clear(&mut self);
}
/// A polygon with four sides.
diff --git a/native/src/shell.rs b/native/src/shell.rs
index b96d23e5..f1ddb48e 100644
--- a/native/src/shell.rs
+++ b/native/src/shell.rs
@@ -1,3 +1,5 @@
+use crate::window;
+
/// A connection to the state of a shell.
///
/// A [`Widget`] can leverage a [`Shell`] to trigger changes in an application,
@@ -7,6 +9,7 @@
#[derive(Debug)]
pub struct Shell<'a, Message> {
messages: &'a mut Vec<Message>,
+ redraw_request: Option<window::RedrawRequest>,
is_layout_invalid: bool,
are_widgets_invalid: bool,
}
@@ -16,31 +19,40 @@ impl<'a, Message> Shell<'a, Message> {
pub fn new(messages: &'a mut Vec<Message>) -> Self {
Self {
messages,
+ redraw_request: None,
is_layout_invalid: false,
are_widgets_invalid: false,
}
}
- /// Triggers the given function if the layout is invalid, cleaning it in the
- /// process.
- pub fn revalidate_layout(&mut self, f: impl FnOnce()) {
- if self.is_layout_invalid {
- self.is_layout_invalid = false;
+ /// Publish the given `Message` for an application to process it.
+ pub fn publish(&mut self, message: Message) {
+ self.messages.push(message);
+ }
- f()
+ /// Requests a new frame to be drawn at the given [`Instant`].
+ pub fn request_redraw(&mut self, request: window::RedrawRequest) {
+ match self.redraw_request {
+ None => {
+ self.redraw_request = Some(request);
+ }
+ Some(current) if request < current => {
+ self.redraw_request = Some(request);
+ }
+ _ => {}
}
}
+ /// Returns the requested [`Instant`] a redraw should happen, if any.
+ pub fn redraw_request(&self) -> Option<window::RedrawRequest> {
+ self.redraw_request
+ }
+
/// Returns whether the current layout is invalid or not.
pub fn is_layout_invalid(&self) -> bool {
self.is_layout_invalid
}
- /// Publish the given `Message` for an application to process it.
- pub fn publish(&mut self, message: Message) {
- self.messages.push(message);
- }
-
/// Invalidates the current application layout.
///
/// The shell will relayout the application widgets.
@@ -48,6 +60,22 @@ impl<'a, Message> Shell<'a, Message> {
self.is_layout_invalid = true;
}
+ /// Triggers the given function if the layout is invalid, cleaning it in the
+ /// process.
+ pub fn revalidate_layout(&mut self, f: impl FnOnce()) {
+ if self.is_layout_invalid {
+ self.is_layout_invalid = false;
+
+ f()
+ }
+ }
+
+ /// Returns whether the widgets of the current application have been
+ /// invalidated.
+ pub fn are_widgets_invalid(&self) -> bool {
+ self.are_widgets_invalid
+ }
+
/// Invalidates the current application widgets.
///
/// The shell will rebuild and relayout the widget tree.
@@ -62,16 +90,14 @@ impl<'a, Message> Shell<'a, Message> {
pub fn merge<B>(&mut self, other: Shell<'_, B>, f: impl Fn(B) -> Message) {
self.messages.extend(other.messages.drain(..).map(f));
+ if let Some(at) = other.redraw_request {
+ self.request_redraw(at);
+ }
+
self.is_layout_invalid =
self.is_layout_invalid || other.is_layout_invalid;
self.are_widgets_invalid =
self.are_widgets_invalid || other.are_widgets_invalid;
}
-
- /// Returns whether the widgets of the current application have been
- /// invalidated.
- pub fn are_widgets_invalid(&self) -> bool {
- self.are_widgets_invalid
- }
}
diff --git a/native/src/subscription.rs b/native/src/subscription.rs
index c60b1281..4c0d80a7 100644
--- a/native/src/subscription.rs
+++ b/native/src/subscription.rs
@@ -1,5 +1,6 @@
//! Listen to external events in your application.
use crate::event::{self, Event};
+use crate::window;
use crate::Hasher;
use iced_futures::futures::{self, Future, Stream};
@@ -33,7 +34,7 @@ pub type Tracker =
pub use iced_futures::subscription::Recipe;
-/// Returns a [`Subscription`] to all the runtime events.
+/// Returns a [`Subscription`] to all the ignored runtime events.
///
/// This subscription will notify your application of any [`Event`] that was
/// not captured by any widget.
@@ -58,8 +59,36 @@ pub fn events_with<Message>(
where
Message: 'static + MaybeSend,
{
+ #[derive(Hash)]
+ struct EventsWith;
+
+ Subscription::from_recipe(Runner {
+ id: (EventsWith, f),
+ spawn: move |events| {
+ use futures::future;
+ use futures::stream::StreamExt;
+
+ events.filter_map(move |(event, status)| {
+ future::ready(match event {
+ Event::Window(window::Event::RedrawRequested(_)) => None,
+ _ => f(event, status),
+ })
+ })
+ },
+ })
+}
+
+pub(crate) fn raw_events<Message>(
+ f: fn(Event, event::Status) -> Option<Message>,
+) -> Subscription<Message>
+where
+ Message: 'static + MaybeSend,
+{
+ #[derive(Hash)]
+ struct RawEvents;
+
Subscription::from_recipe(Runner {
- id: f,
+ id: (RawEvents, f),
spawn: move |events| {
use futures::future;
use futures::stream::StreamExt;
diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs
index 2b43829d..025f28a1 100644
--- a/native/src/user_interface.rs
+++ b/native/src/user_interface.rs
@@ -5,6 +5,7 @@ use crate::layout;
use crate::mouse;
use crate::renderer;
use crate::widget;
+use crate::window;
use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size};
/// A set of interactive graphical elements with a specific [`Layout`].
@@ -188,7 +189,9 @@ where
) -> (State, Vec<event::Status>) {
use std::mem::ManuallyDrop;
- let mut state = State::Updated;
+ let mut outdated = false;
+ let mut redraw_request = None;
+
let mut manual_overlay =
ManuallyDrop::new(self.root.as_widget_mut().overlay(
&mut self.state,
@@ -217,6 +220,16 @@ where
event_statuses.push(event_status);
+ match (redraw_request, shell.redraw_request()) {
+ (None, Some(at)) => {
+ redraw_request = Some(at);
+ }
+ (Some(current), Some(new)) if new < current => {
+ redraw_request = Some(new);
+ }
+ _ => {}
+ }
+
if shell.is_layout_invalid() {
let _ = ManuallyDrop::into_inner(manual_overlay);
@@ -244,7 +257,7 @@ where
}
if shell.are_widgets_invalid() {
- state = State::Outdated;
+ outdated = true;
}
}
@@ -289,6 +302,16 @@ where
self.overlay = None;
}
+ match (redraw_request, shell.redraw_request()) {
+ (None, Some(at)) => {
+ redraw_request = Some(at);
+ }
+ (Some(current), Some(new)) if new < current => {
+ redraw_request = Some(new);
+ }
+ _ => {}
+ }
+
shell.revalidate_layout(|| {
self.base = renderer.layout(
&self.root,
@@ -299,14 +322,21 @@ where
});
if shell.are_widgets_invalid() {
- state = State::Outdated;
+ outdated = true;
}
event_status.merge(overlay_status)
})
.collect();
- (state, event_statuses)
+ (
+ if outdated {
+ State::Outdated
+ } else {
+ State::Updated { redraw_request }
+ },
+ event_statuses,
+ )
}
/// Draws the [`UserInterface`] with the provided [`Renderer`].
@@ -559,5 +589,8 @@ pub enum State {
/// The [`UserInterface`] is up-to-date and can be reused without
/// rebuilding.
- Updated,
+ Updated {
+ /// The [`Instant`] when a redraw should be performed.
+ redraw_request: Option<window::RedrawRequest>,
+ },
}
diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs
index 8b4514e3..8755b85d 100644
--- a/native/src/widget/text_input.rs
+++ b/native/src/widget/text_input.rs
@@ -18,10 +18,12 @@ use crate::layout;
use crate::mouse::{self, click};
use crate::renderer;
use crate::text::{self, Text};
+use crate::time::{Duration, Instant};
use crate::touch;
use crate::widget;
use crate::widget::operation::{self, Operation};
use crate::widget::tree::{self, Tree};
+use crate::window;
use crate::{
Clipboard, Color, Command, Element, Layout, Length, Padding, Point,
Rectangle, Shell, Size, Vector, Widget,
@@ -425,7 +427,18 @@ where
let state = state();
let is_clicked = layout.bounds().contains(cursor_position);
- state.is_focused = is_clicked;
+ state.is_focused = if is_clicked {
+ state.is_focused.or_else(|| {
+ let now = Instant::now();
+
+ Some(Focus {
+ updated_at: now,
+ now,
+ })
+ })
+ } else {
+ None
+ };
if is_clicked {
let text_layout = layout.children().next().unwrap();
@@ -541,26 +554,30 @@ where
Event::Keyboard(keyboard::Event::CharacterReceived(c)) => {
let state = state();
- if state.is_focused
- && state.is_pasting.is_none()
- && !state.keyboard_modifiers.command()
- && !c.is_control()
- {
- let mut editor = Editor::new(value, &mut state.cursor);
+ if let Some(focus) = &mut state.is_focused {
+ if state.is_pasting.is_none()
+ && !state.keyboard_modifiers.command()
+ && !c.is_control()
+ {
+ let mut editor = Editor::new(value, &mut state.cursor);
- editor.insert(c);
+ editor.insert(c);
- let message = (on_change)(editor.contents());
- shell.publish(message);
+ let message = (on_change)(editor.contents());
+ shell.publish(message);
- return event::Status::Captured;
+ focus.updated_at = Instant::now();
+
+ return event::Status::Captured;
+ }
}
}
Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => {
let state = state();
- if state.is_focused {
+ if let Some(focus) = &mut state.is_focused {
let modifiers = state.keyboard_modifiers;
+ focus.updated_at = Instant::now();
match key_code {
keyboard::KeyCode::Enter
@@ -721,7 +738,7 @@ where
state.cursor.select_all(value);
}
keyboard::KeyCode::Escape => {
- state.is_focused = false;
+ state.is_focused = None;
state.is_dragging = false;
state.is_pasting = None;
@@ -742,7 +759,7 @@ where
Event::Keyboard(keyboard::Event::KeyReleased { key_code, .. }) => {
let state = state();
- if state.is_focused {
+ if state.is_focused.is_some() {
match key_code {
keyboard::KeyCode::V => {
state.is_pasting = None;
@@ -765,6 +782,21 @@ where
state.keyboard_modifiers = modifiers;
}
+ Event::Window(window::Event::RedrawRequested(now)) => {
+ let state = state();
+
+ 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),
+ ));
+ }
+ }
_ => {}
}
@@ -820,7 +852,7 @@ pub fn draw<Renderer>(
let text = value.to_string();
let size = size.unwrap_or_else(|| renderer.default_size());
- let (cursor, offset) = if state.is_focused() {
+ let (cursor, offset) = if let Some(focus) = &state.is_focused {
match state.cursor.state(value) {
cursor::State::Index(position) => {
let (text_value_width, offset) =
@@ -833,7 +865,13 @@ 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 {
@@ -847,9 +885,12 @@ pub fn draw<Renderer>(
border_color: Color::TRANSPARENT,
},
theme.value_color(style),
- )),
- offset,
- )
+ ))
+ } else {
+ None
+ };
+
+ (cursor, offset)
}
cursor::State::Selection { start, end } => {
let left = start.min(end);
@@ -958,7 +999,7 @@ pub fn mouse_interaction(
/// The state of a [`TextInput`].
#[derive(Debug, Default, Clone)]
pub struct State {
- is_focused: bool,
+ is_focused: Option<Focus>,
is_dragging: bool,
is_pasting: Option<Value>,
last_click: Option<mouse::Click>,
@@ -967,6 +1008,12 @@ pub struct State {
// TODO: Add stateful horizontal scrolling offset
}
+#[derive(Debug, Clone, Copy)]
+struct Focus {
+ updated_at: Instant,
+ now: Instant,
+}
+
impl State {
/// Creates a new [`State`], representing an unfocused [`TextInput`].
pub fn new() -> Self {
@@ -976,7 +1023,7 @@ impl State {
/// Creates a new [`State`], representing a focused [`TextInput`].
pub fn focused() -> Self {
Self {
- is_focused: true,
+ is_focused: None,
is_dragging: false,
is_pasting: None,
last_click: None,
@@ -987,7 +1034,7 @@ impl State {
/// Returns whether the [`TextInput`] is currently focused or not.
pub fn is_focused(&self) -> bool {
- self.is_focused
+ self.is_focused.is_some()
}
/// Returns the [`Cursor`] of the [`TextInput`].
@@ -997,13 +1044,19 @@ impl State {
/// Focuses the [`TextInput`].
pub fn focus(&mut self) {
- self.is_focused = true;
+ let now = Instant::now();
+
+ self.is_focused = Some(Focus {
+ updated_at: now,
+ now,
+ });
+
self.move_cursor_to_end();
}
/// Unfocuses the [`TextInput`].
pub fn unfocus(&mut self) {
- self.is_focused = false;
+ self.is_focused = None;
}
/// Moves the [`Cursor`] of the [`TextInput`] to the front of the input text.
@@ -1156,3 +1209,5 @@ where
)
.map(text::Hit::cursor)
}
+
+const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;
diff --git a/native/src/window.rs b/native/src/window.rs
index 1b97e655..a5cdc8ce 100644
--- a/native/src/window.rs
+++ b/native/src/window.rs
@@ -2,9 +2,29 @@
mod action;
mod event;
mod mode;
+mod redraw_request;
mod user_attention;
pub use action::Action;
pub use event::Event;
pub use mode::Mode;
+pub use redraw_request::RedrawRequest;
pub use user_attention::UserAttention;
+
+use crate::subscription::{self, Subscription};
+use crate::time::Instant;
+
+/// Subscribes to the frames of the window of the running application.
+///
+/// The resulting [`Subscription`] will produce items at a rate equal to the
+/// refresh rate of the window. Note that this rate may be variable, as it is
+/// normally managed by the graphics driver and/or the OS.
+///
+/// In any case, this [`Subscription`] is useful to smoothly draw application-driven
+/// animations without missing any frames.
+pub fn frames() -> Subscription<Instant> {
+ subscription::raw_events(|event, _status| match event {
+ crate::Event::Window(Event::RedrawRequested(at)) => Some(at),
+ _ => None,
+ })
+}
diff --git a/native/src/window/event.rs b/native/src/window/event.rs
index 86321ac0..e2fb5e66 100644
--- a/native/src/window/event.rs
+++ b/native/src/window/event.rs
@@ -1,3 +1,5 @@
+use crate::time::Instant;
+
use std::path::PathBuf;
/// A window-related event.
@@ -19,6 +21,11 @@ pub enum Event {
height: u32,
},
+ /// A window redraw was requested.
+ ///
+ /// The [`Instant`] contains the current time.
+ RedrawRequested(Instant),
+
/// The user has requested for the window to close.
///
/// Usually, you will want to terminate the execution whenever this event
diff --git a/native/src/window/redraw_request.rs b/native/src/window/redraw_request.rs
new file mode 100644
index 00000000..3b4f0fd3
--- /dev/null
+++ b/native/src/window/redraw_request.rs
@@ -0,0 +1,38 @@
+use crate::time::Instant;
+
+/// A request to redraw a window.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub enum RedrawRequest {
+ /// Redraw the next frame.
+ NextFrame,
+
+ /// Redraw at the given time.
+ At(Instant),
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::time::{Duration, Instant};
+
+ #[test]
+ fn ordering() {
+ let now = Instant::now();
+ let later = now + Duration::from_millis(10);
+
+ assert_eq!(RedrawRequest::NextFrame, RedrawRequest::NextFrame);
+ assert_eq!(RedrawRequest::At(now), RedrawRequest::At(now));
+
+ assert!(RedrawRequest::NextFrame < RedrawRequest::At(now));
+ assert!(RedrawRequest::At(now) > RedrawRequest::NextFrame);
+ assert!(RedrawRequest::At(now) < RedrawRequest::At(later));
+ assert!(RedrawRequest::At(later) > RedrawRequest::At(now));
+
+ assert!(RedrawRequest::NextFrame <= RedrawRequest::NextFrame);
+ assert!(RedrawRequest::NextFrame <= RedrawRequest::At(now));
+ assert!(RedrawRequest::At(now) >= RedrawRequest::NextFrame);
+ assert!(RedrawRequest::At(now) <= RedrawRequest::At(now));
+ assert!(RedrawRequest::At(now) <= RedrawRequest::At(later));
+ assert!(RedrawRequest::At(later) >= RedrawRequest::At(now));
+ }
+}