diff options
author | 2023-01-12 02:59:08 +0100 | |
---|---|---|
committer | 2023-01-12 02:59:08 +0100 | |
commit | 7354f68b3ca345767de3f09dccddf168493977bf (patch) | |
tree | a09626c11a25ab4260c576733f1700e9ad12894b | |
parent | 7ccd87c36b54e0d53f65f5774f140a0528ae4504 (diff) | |
download | iced-7354f68b3ca345767de3f09dccddf168493977bf.tar.gz iced-7354f68b3ca345767de3f09dccddf168493977bf.tar.bz2 iced-7354f68b3ca345767de3f09dccddf168493977bf.zip |
Draft `Shell:request_redraw` API
... and implement `TextInput` cursor blink :tada:
Diffstat (limited to '')
-rw-r--r-- | examples/events/Cargo.toml | 2 | ||||
-rw-r--r-- | examples/events/src/main.rs | 24 | ||||
-rw-r--r-- | examples/exit/src/main.rs | 26 | ||||
-rw-r--r-- | native/src/renderer.rs | 6 | ||||
-rw-r--r-- | native/src/shell.rs | 60 | ||||
-rw-r--r-- | native/src/subscription.rs | 8 | ||||
-rw-r--r-- | native/src/user_interface.rs | 46 | ||||
-rw-r--r-- | native/src/widget/text_input.rs | 102 | ||||
-rw-r--r-- | native/src/window/event.rs | 6 | ||||
-rw-r--r-- | src/application.rs | 7 | ||||
-rw-r--r-- | src/sandbox.rs | 11 | ||||
-rw-r--r-- | winit/src/application.rs | 72 |
12 files changed, 263 insertions, 107 deletions
diff --git a/examples/events/Cargo.toml b/examples/events/Cargo.toml index 8ad04a36..8c56e471 100644 --- a/examples/events/Cargo.toml +++ b/examples/events/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../.." } +iced = { path = "../..", features = ["debug"] } iced_native = { path = "../../native" } diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 234e1423..4ae8d6fb 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -1,11 +1,12 @@ use iced::alignment; use iced::executor; use iced::widget::{button, checkbox, container, text, Column}; +use iced::window; use iced::{ Alignment, Application, Command, Element, Length, Settings, Subscription, Theme, }; -use iced_native::{window, Event}; +use iced_native::Event; pub fn main() -> iced::Result { Events::run(Settings { @@ -18,7 +19,6 @@ pub fn main() -> iced::Result { struct Events { last: Vec<iced_native::Event>, enabled: bool, - should_exit: bool, } #[derive(Debug, Clone)] @@ -50,31 +50,29 @@ impl Application for Events { if self.last.len() > 5 { let _ = self.last.remove(0); } + + Command::none() } Message::EventOccurred(event) => { if let Event::Window(window::Event::CloseRequested) = event { - self.should_exit = true; + window::close() + } else { + Command::none() } } Message::Toggled(enabled) => { self.enabled = enabled; - } - Message::Exit => { - self.should_exit = true; - } - }; - Command::none() + Command::none() + } + Message::Exit => window::close(), + } } fn subscription(&self) -> Subscription<Message> { iced_native::subscription::events().map(Message::EventOccurred) } - fn should_exit(&self) -> bool { - self.should_exit - } - fn view(&self) -> Element<Message> { let events = Column::with_children( self.last diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index 5d518d2f..6152f627 100644 --- a/examples/exit/src/main.rs +++ b/examples/exit/src/main.rs @@ -1,5 +1,7 @@ +use iced::executor; use iced::widget::{button, column, container}; -use iced::{Alignment, Element, Length, Sandbox, Settings}; +use iced::window; +use iced::{Alignment, Application, Command, Element, Length, Settings, Theme}; pub fn main() -> iced::Result { Exit::run(Settings::default()) @@ -8,7 +10,6 @@ pub fn main() -> iced::Result { #[derive(Default)] struct Exit { show_confirm: bool, - exit: bool, } #[derive(Debug, Clone, Copy)] @@ -17,28 +18,27 @@ enum Message { Exit, } -impl Sandbox for Exit { +impl Application for Exit { + type Executor = executor::Default; type Message = Message; + type Theme = Theme; + type Flags = (); - fn new() -> Self { - Self::default() + fn new(_flags: ()) -> (Self, Command<Message>) { + (Self::default(), Command::none()) } fn title(&self) -> String { String::from("Exit - Iced") } - fn should_exit(&self) -> bool { - self.exit - } - - fn update(&mut self, message: Message) { + fn update(&mut self, message: Message) -> Command<Message> { match message { - Message::Confirm => { - self.exit = true; - } + Message::Confirm => window::close(), Message::Exit => { self.show_confirm = true; + + Command::none() } } } 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..81d2a0e6 100644 --- a/native/src/shell.rs +++ b/native/src/shell.rs @@ -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,31 +19,40 @@ 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, } } - /// 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, 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 } - /// 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_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 - } } diff --git a/native/src/subscription.rs b/native/src/subscription.rs index c60b1281..980a8116 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. @@ -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), + }) }) }, }) diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 2b43829d..49a6b00e 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -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>, + }, } diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 8b4514e3..9d5dd620 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -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; diff --git a/native/src/window/event.rs b/native/src/window/event.rs index 86321ac0..16684222 100644 --- a/native/src/window/event.rs +++ b/native/src/window/event.rs @@ -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 diff --git a/src/application.rs b/src/application.rs index f2b7c955..6d68779b 100644 --- a/src/application.rs +++ b/src/application.rs @@ -180,13 +180,6 @@ pub trait Application: Sized { 1.0 } - /// Returns whether the [`Application`] should be terminated. - /// - /// By default, it returns `false`. - fn should_exit(&self) -> bool { - false - } - /// Runs the [`Application`]. /// /// On native platforms, this method will take control of the current thread diff --git a/src/sandbox.rs b/src/sandbox.rs index 47bad831..40c699d9 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -140,13 +140,6 @@ pub trait Sandbox { 1.0 } - /// Returns whether the [`Sandbox`] should be terminated. - /// - /// By default, it returns `false`. - fn should_exit(&self) -> bool { - false - } - /// Runs the [`Sandbox`]. /// /// On native platforms, this method will take control of the current thread @@ -203,8 +196,4 @@ where fn scale_factor(&self) -> f64 { T::scale_factor(self) } - - fn should_exit(&self) -> bool { - T::should_exit(self) - } } diff --git a/winit/src/application.rs b/winit/src/application.rs index 74c73815..0f5309d2 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -11,7 +11,7 @@ use crate::mouse; use crate::renderer; use crate::widget::operation; use crate::{ - Command, Debug, Error, Executor, Proxy, Runtime, Settings, Size, + Command, Debug, Error, Event, Executor, Proxy, Runtime, Settings, Size, Subscription, }; @@ -25,6 +25,7 @@ use iced_native::user_interface::{self, UserInterface}; pub use iced_native::application::{Appearance, StyleSheet}; use std::mem::ManuallyDrop; +use std::time::Instant; #[cfg(feature = "trace")] pub use profiler::Profiler; @@ -186,7 +187,8 @@ where let (compositor, renderer) = C::new(compositor_settings, Some(&window))?; - let (mut sender, receiver) = mpsc::unbounded(); + let (mut event_sender, event_receiver) = mpsc::unbounded(); + let (control_sender, mut control_receiver) = mpsc::unbounded(); let mut instance = Box::pin({ let run_instance = run_instance::<A, E, C>( @@ -196,7 +198,8 @@ where runtime, proxy, debug, - receiver, + event_receiver, + control_sender, init_command, window, settings.exit_on_close_request, @@ -234,13 +237,19 @@ where }; if let Some(event) = event { - sender.start_send(event).expect("Send event"); + event_sender.start_send(event).expect("Send event"); let poll = instance.as_mut().poll(&mut context); - *control_flow = match poll { - task::Poll::Pending => ControlFlow::Wait, - task::Poll::Ready(_) => ControlFlow::Exit, + match poll { + task::Poll::Pending => { + if let Ok(Some(flow)) = control_receiver.try_next() { + *control_flow = flow; + } + } + task::Poll::Ready(_) => { + *control_flow = ControlFlow::Exit; + } }; } }) @@ -253,7 +262,10 @@ async fn run_instance<A, E, C>( mut runtime: Runtime<E, Proxy<A::Message>, A::Message>, mut proxy: winit::event_loop::EventLoopProxy<A::Message>, mut debug: Debug, - mut receiver: mpsc::UnboundedReceiver<winit::event::Event<'_, A::Message>>, + mut event_receiver: mpsc::UnboundedReceiver< + winit::event::Event<'_, A::Message>, + >, + mut control_sender: mpsc::UnboundedSender<winit::event_loop::ControlFlow>, init_command: Command<A::Message>, window: winit::window::Window, exit_on_close_request: bool, @@ -265,6 +277,7 @@ async fn run_instance<A, E, C>( { use iced_futures::futures::stream::StreamExt; use winit::event; + use winit::event_loop::ControlFlow; let mut clipboard = Clipboard::connect(&window); let mut cache = user_interface::Cache::default(); @@ -309,13 +322,21 @@ async fn run_instance<A, E, C>( let mut mouse_interaction = mouse::Interaction::default(); let mut events = Vec::new(); let mut messages = Vec::new(); + let mut redraw_pending = false; debug.startup_finished(); - while let Some(event) = receiver.next().await { + while let Some(event) = event_receiver.next().await { match event { + event::Event::NewEvents(start_cause) => { + redraw_pending = matches!( + start_cause, + event::StartCause::Init + | event::StartCause::ResumeTimeReached { .. } + ); + } event::Event::MainEventsCleared => { - if events.is_empty() && messages.is_empty() { + if !redraw_pending && events.is_empty() && messages.is_empty() { continue; } @@ -338,7 +359,7 @@ async fn run_instance<A, E, C>( if !messages.is_empty() || matches!( interface_state, - user_interface::State::Outdated, + user_interface::State::Outdated ) { let mut cache = @@ -376,6 +397,24 @@ async fn run_instance<A, E, C>( } } + // TODO: Avoid redrawing all the time by forcing widgets to + // request redraws on state changes + // + // Then, we can use the `interface_state` here to decide whether + // if a redraw is needed right away, or simply wait until a + // specific time. + let redraw_event = Event::Window( + crate::window::Event::RedrawRequested(Instant::now()), + ); + + let (interface_state, _) = user_interface.update( + &[redraw_event.clone()], + state.cursor_position(), + &mut renderer, + &mut clipboard, + &mut messages, + ); + debug.draw_started(); let new_mouse_interaction = user_interface.draw( &mut renderer, @@ -396,6 +435,17 @@ async fn run_instance<A, E, C>( } window.request_redraw(); + runtime + .broadcast((redraw_event, crate::event::Status::Ignored)); + + let _ = control_sender.start_send(match interface_state { + user_interface::State::Updated { + redraw_requested_at: Some(at), + } => ControlFlow::WaitUntil(at), + _ => ControlFlow::Wait, + }); + + redraw_pending = false; } event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( event::MacOS::ReceivedUrl(url), |