diff options
| author | 2023-01-13 20:33:59 +0100 | |
|---|---|---|
| committer | 2023-01-13 20:33:59 +0100 | |
| commit | 597af315af714f3ecadd195516e80506504dcb26 (patch) | |
| tree | 200d0447efc60c9066b762c9f1a00ce55d66def7 /native/src | |
| parent | d2b66805b350b4bd2ca5cb2818a6c05e3cfb5235 (diff) | |
| parent | 507820a8438cec25074f92b72e118e0931fa7f9f (diff) | |
| download | iced-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.rs | 6 | ||||
| -rw-r--r-- | native/src/shell.rs | 60 | ||||
| -rw-r--r-- | native/src/subscription.rs | 33 | ||||
| -rw-r--r-- | native/src/user_interface.rs | 43 | ||||
| -rw-r--r-- | native/src/widget/text_input.rs | 103 | ||||
| -rw-r--r-- | native/src/window.rs | 20 | ||||
| -rw-r--r-- | native/src/window/event.rs | 7 | ||||
| -rw-r--r-- | native/src/window/redraw_request.rs | 38 | 
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)); +    } +}  | 
