Draft Shell:request_redraw API

... and implement `TextInput` cursor blink 🎉
This commit is contained in:
Héctor Ramón Jiménez 2023-01-12 02:59:08 +01:00
parent 7ccd87c36b
commit 7354f68b3c
No known key found for this signature in database
GPG key ID: 140CC052C94F138E
12 changed files with 267 additions and 111 deletions

View file

@ -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.

View file

@ -1,3 +1,5 @@
use std::time::Instant;
/// 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_requested_at: Option<Instant>,
is_layout_invalid: bool,
are_widgets_invalid: bool,
}
@ -16,11 +19,47 @@ impl<'a, Message> Shell<'a, Message> {
pub fn new(messages: &'a mut Vec<Message>) -> Self {
Self {
messages,
redraw_requested_at: None,
is_layout_invalid: false,
are_widgets_invalid: false,
}
}
/// Publish the given `Message` for an application to process it.
pub fn publish(&mut self, message: Message) {
self.messages.push(message);
}
/// Requests a new frame to be drawn at the given [`Instant`].
pub fn request_redraw(&mut self, at: Instant) {
match self.redraw_requested_at {
None => {
self.redraw_requested_at = Some(at);
}
Some(current) if at < current => {
self.redraw_requested_at = Some(at);
}
_ => {}
}
}
/// Returns the requested [`Instant`] a redraw should happen, if any.
pub fn redraw_requested_at(&self) -> Option<Instant> {
self.redraw_requested_at
}
/// Returns whether the current layout is invalid or not.
pub fn is_layout_invalid(&self) -> bool {
self.is_layout_invalid
}
/// Invalidates the current application layout.
///
/// The shell will relayout the application widgets.
pub fn invalidate_layout(&mut self) {
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()) {
@ -31,21 +70,10 @@ impl<'a, Message> Shell<'a, Message> {
}
}
/// 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.
pub fn invalidate_layout(&mut self) {
self.is_layout_invalid = true;
/// 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.
@ -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_requested_at {
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
}
}

View file

@ -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.
@ -65,7 +66,10 @@ where
use futures::stream::StreamExt;
events.filter_map(move |(event, status)| {
future::ready(f(event, status))
future::ready(match event {
Event::Window(window::Event::RedrawRequested(_)) => None,
_ => f(event, status),
})
})
},
})

View file

@ -7,6 +7,8 @@ use crate::renderer;
use crate::widget;
use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size};
use std::time::Instant;
/// A set of interactive graphical elements with a specific [`Layout`].
///
/// It can be updated and drawn.
@ -188,7 +190,9 @@ where
) -> (State, Vec<event::Status>) {
use std::mem::ManuallyDrop;
let mut state = State::Updated;
let mut outdated = false;
let mut redraw_requested_at = None;
let mut manual_overlay =
ManuallyDrop::new(self.root.as_widget_mut().overlay(
&mut self.state,
@ -217,6 +221,16 @@ where
event_statuses.push(event_status);
match (redraw_requested_at, shell.redraw_requested_at()) {
(None, Some(at)) => {
redraw_requested_at = Some(at);
}
(Some(current), Some(new)) if new < current => {
redraw_requested_at = Some(new);
}
_ => {}
}
if shell.is_layout_invalid() {
let _ = ManuallyDrop::into_inner(manual_overlay);
@ -244,7 +258,7 @@ where
}
if shell.are_widgets_invalid() {
state = State::Outdated;
outdated = true;
}
}
@ -289,6 +303,16 @@ where
self.overlay = None;
}
match (redraw_requested_at, shell.redraw_requested_at()) {
(None, Some(at)) => {
redraw_requested_at = Some(at);
}
(Some(current), Some(new)) if new < current => {
redraw_requested_at = Some(new);
}
_ => {}
}
shell.revalidate_layout(|| {
self.base = renderer.layout(
&self.root,
@ -299,14 +323,23 @@ 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_requested_at,
}
},
event_statuses,
)
}
/// Draws the [`UserInterface`] with the provided [`Renderer`].
@ -559,5 +592,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_requested_at: Option<Instant>,
},
}

View file

@ -22,11 +22,14 @@ 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,
};
use std::time::{Duration, Instant};
pub use iced_style::text_input::{Appearance, StyleSheet};
/// A field that can be filled with text.
@ -425,7 +428,16 @@ where
let state = state();
let is_clicked = layout.bounds().contains(cursor_position);
state.is_focused = is_clicked;
state.is_focused = if is_clicked {
let now = Instant::now();
Some(Focus {
at: now,
last_draw: now,
})
} else {
None
};
if is_clicked {
let text_layout = layout.children().next().unwrap();
@ -541,26 +553,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.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.at = Instant::now();
match key_code {
keyboard::KeyCode::Enter
@ -721,7 +737,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 +758,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 +781,21 @@ where
state.keyboard_modifiers = modifiers;
}
Event::Window(window::Event::RedrawRequested(now)) => {
let state = state();
if let Some(focus) = &mut state.is_focused {
focus.last_draw = now;
let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS
- (now - focus.at).as_millis()
% CURSOR_BLINK_INTERVAL_MILLIS;
shell.request_redraw(
now + Duration::from_millis(millis_until_redraw as u64),
);
}
}
_ => {}
}
@ -820,7 +851,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 +864,13 @@ pub fn draw<Renderer>(
font.clone(),
);
(
let is_cursor_visible = ((focus.last_draw - focus.at)
.as_millis()
/ CURSOR_BLINK_INTERVAL_MILLIS)
% 2
== 0;
let cursor = if is_cursor_visible {
Some((
renderer::Quad {
bounds: Rectangle {
@ -847,9 +884,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 +998,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 +1007,12 @@ pub struct State {
// TODO: Add stateful horizontal scrolling offset
}
#[derive(Debug, Clone, Copy)]
struct Focus {
at: Instant,
last_draw: Instant,
}
impl State {
/// Creates a new [`State`], representing an unfocused [`TextInput`].
pub fn new() -> Self {
@ -976,7 +1022,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 +1033,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 +1043,19 @@ impl State {
/// Focuses the [`TextInput`].
pub fn focus(&mut self) {
self.is_focused = true;
let now = Instant::now();
self.is_focused = Some(Focus {
at: now,
last_draw: 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 +1208,5 @@ where
)
.map(text::Hit::cursor)
}
const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;

View file

@ -1,4 +1,5 @@
use std::path::PathBuf;
use std::time::Instant;
/// A window-related event.
#[derive(PartialEq, Eq, Clone, Debug)]
@ -19,6 +20,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