Merge pull request #1647 from iced-rs/feature/widget-request-redraw

Widget-driven animations
This commit is contained in:
Héctor Ramón 2023-01-13 20:33:59 +01:00 committed by GitHub
commit 597af315af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 435 additions and 130 deletions

View file

@ -15,4 +15,4 @@ version = "0.6"
optional = true
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-timer = { version = "0.2" }
instant = "0.1"

View file

@ -1,9 +1,13 @@
//! Keep track of time, both in native and web platforms!
#[cfg(target_arch = "wasm32")]
pub use wasm_timer::Instant;
pub use instant::Instant;
#[cfg(target_arch = "wasm32")]
pub use instant::Duration;
#[cfg(not(target_arch = "wasm32"))]
pub use std::time::Instant;
#[cfg(not(target_arch = "wasm32"))]
pub use std::time::Duration;

View file

@ -6,5 +6,5 @@ edition = "2021"
publish = false
[dependencies]
iced = { path = "../.." }
iced = { path = "../..", features = ["debug"] }
iced_native = { path = "../../native" }

View file

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

View file

@ -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()
}
}
}

View file

@ -9,7 +9,6 @@
use iced::application;
use iced::executor;
use iced::theme::{self, Theme};
use iced::time;
use iced::widget::canvas;
use iced::widget::canvas::gradient::{self, Gradient};
use iced::widget::canvas::stroke::{self, Stroke};
@ -90,7 +89,7 @@ impl Application for SolarSystem {
}
fn subscription(&self) -> Subscription<Message> {
time::every(time::Duration::from_millis(10)).map(Message::Tick)
window::frames().map(Message::Tick)
}
}

View file

@ -15,8 +15,8 @@ trace = ["iced_winit/trace"]
debug = ["iced_winit/debug"]
system = ["iced_winit/system"]
[dependencies.log]
version = "0.4"
[dependencies]
log = "0.4"
[dependencies.glutin]
version = "0.29"
@ -39,4 +39,4 @@ features = ["opengl"]
[dependencies.tracing]
version = "0.1.6"
optional = true
optional = true

View file

@ -11,8 +11,9 @@ use iced_winit::conversion;
use iced_winit::futures;
use iced_winit::futures::channel::mpsc;
use iced_winit::renderer;
use iced_winit::time::Instant;
use iced_winit::user_interface;
use iced_winit::{Clipboard, Command, Debug, Proxy, Settings};
use iced_winit::{Clipboard, Command, Debug, Event, Proxy, Settings};
use glutin::window::Window;
use std::mem::ManuallyDrop;
@ -131,7 +132,8 @@ where
})?
};
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>(
@ -141,7 +143,8 @@ where
runtime,
proxy,
debug,
receiver,
event_receiver,
control_sender,
context,
init_command,
settings.exit_on_close_request,
@ -179,14 +182,20 @@ 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;
}
}
}
});
@ -200,7 +209,10 @@ async fn run_instance<A, E, C>(
mut runtime: Runtime<E, Proxy<A::Message>, A::Message>,
mut proxy: glutin::event_loop::EventLoopProxy<A::Message>,
mut debug: Debug,
mut receiver: mpsc::UnboundedReceiver<glutin::event::Event<'_, A::Message>>,
mut event_receiver: mpsc::UnboundedReceiver<
glutin::event::Event<'_, A::Message>,
>,
mut control_sender: mpsc::UnboundedSender<glutin::event_loop::ControlFlow>,
mut context: glutin::ContextWrapper<glutin::PossiblyCurrent, Window>,
init_command: Command<A::Message>,
exit_on_close_request: bool,
@ -211,6 +223,7 @@ async fn run_instance<A, E, C>(
<A::Renderer as iced_native::Renderer>::Theme: StyleSheet,
{
use glutin::event;
use glutin::event_loop::ControlFlow;
use iced_winit::futures::stream::StreamExt;
let mut clipboard = Clipboard::connect(context.window());
@ -247,13 +260,22 @@ 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::Poll
| event::StartCause::ResumeTimeReached { .. }
);
}
event::Event::MainEventsCleared => {
if events.is_empty() && messages.is_empty() {
if !redraw_pending && events.is_empty() && messages.is_empty() {
continue;
}
@ -315,6 +337,23 @@ 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 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,
@ -335,6 +374,24 @@ async fn run_instance<A, E, C>(
}
context.window().request_redraw();
runtime
.broadcast((redraw_event, crate::event::Status::Ignored));
let _ = control_sender.start_send(match interface_state {
user_interface::State::Updated {
redraw_request: Some(redraw_request),
} => match redraw_request {
crate::window::RedrawRequest::NextFrame => {
ControlFlow::Poll
}
crate::window::RedrawRequest::At(at) => {
ControlFlow::WaitUntil(at)
}
},
_ => ControlFlow::Wait,
});
redraw_pending = false;
}
event::Event::PlatformSpecific(event::PlatformSpecific::MacOS(
event::MacOS::ReceivedUrl(url),

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 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,11 +19,47 @@ 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,
}
}
/// 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, 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
}
/// 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_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
}
}

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.
@ -58,8 +59,36 @@ pub fn events_with<Message>(
where
Message: 'static + MaybeSend,
{
#[derive(Hash)]
struct EventsWith;
Subscription::from_recipe(Runner {
id: f,
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: (RawEvents, f),
spawn: move |events| {
use futures::future;
use futures::stream::StreamExt;

View file

@ -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>,
},
}

View file

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

View file

@ -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,
})
}

View file

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

View file

@ -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));
}
}

View file

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

View file

@ -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)
}
}

View file

@ -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,
};
@ -20,6 +20,7 @@ use iced_futures::futures::channel::mpsc;
use iced_graphics::compositor;
use iced_graphics::window;
use iced_native::program::Program;
use iced_native::time::Instant;
use iced_native::user_interface::{self, UserInterface};
pub use iced_native::application::{Appearance, StyleSheet};
@ -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,22 @@ 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::Poll
| 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 +360,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 +398,23 @@ 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 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,24 @@ 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_request: Some(redraw_request),
} => match redraw_request {
crate::window::RedrawRequest::NextFrame => {
ControlFlow::Poll
}
crate::window::RedrawRequest::At(at) => {
ControlFlow::WaitUntil(at)
}
},
_ => ControlFlow::Wait,
});
redraw_pending = false;
}
event::Event::PlatformSpecific(event::PlatformSpecific::MacOS(
event::MacOS::ReceivedUrl(url),

View file

@ -2,7 +2,7 @@
use crate::command::{self, Command};
use iced_native::window;
pub use window::{Event, Mode, UserAttention};
pub use window::{frames, Event, Mode, RedrawRequest, UserAttention};
/// Closes the current window and exits the application.
pub fn close<Message>() -> Command<Message> {