diff options
29 files changed, 977 insertions, 100 deletions
diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index c9c1be7c..b7b3dedc 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -1,8 +1,9 @@ +use iced::executor; use iced::widget::{ button, column, container, horizontal_rule, progress_bar, radio, scrollable, text, vertical_space, Row, }; -use iced::{Element, Length, Sandbox, Settings, Theme}; +use iced::{Application, Command, Element, Length, Settings, Theme}; pub fn main() -> iced::Result { ScrollableDemo::run(Settings::default()) @@ -21,43 +22,57 @@ enum Message { Scrolled(usize, f32), } -impl Sandbox for ScrollableDemo { +impl Application for ScrollableDemo { type Message = Message; - - fn new() -> Self { - ScrollableDemo { - theme: Default::default(), - variants: Variant::all(), - } + type Theme = Theme; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: Self::Flags) -> (Self, Command<Message>) { + ( + ScrollableDemo { + theme: Default::default(), + variants: Variant::all(), + }, + Command::none(), + ) } fn title(&self) -> String { String::from("Scrollable - Iced") } - fn update(&mut self, message: Message) { + fn update(&mut self, message: Message) -> Command<Message> { match message { - Message::ThemeChanged(theme) => self.theme = theme, + Message::ThemeChanged(theme) => { + self.theme = theme; + + Command::none() + } Message::ScrollToTop(i) => { if let Some(variant) = self.variants.get_mut(i) { - // TODO - // variant.scrollable.snap_to(0.0); - variant.latest_offset = 0.0; + + scrollable::snap_to(Variant::id(i), 0.0) + } else { + Command::none() } } Message::ScrollToBottom(i) => { if let Some(variant) = self.variants.get_mut(i) { - // TODO - // variant.scrollable.snap_to(1.0); - variant.latest_offset = 1.0; + + scrollable::snap_to(Variant::id(i), 1.0) + } else { + Command::none() } } Message::Scrolled(i, offset) => { if let Some(variant) = self.variants.get_mut(i) { variant.latest_offset = offset; } + + Command::none() } } } @@ -136,6 +151,7 @@ impl Sandbox for ScrollableDemo { ); let mut scrollable = scrollable(contents) + .id(Variant::id(i)) .height(Length::Fill) .on_scroll(move |offset| Message::Scrolled(i, offset)); @@ -228,4 +244,8 @@ impl Variant { }, ] } + + pub fn id(i: usize) -> scrollable::Id { + scrollable::Id::new(format!("scrollable-{}", i)) + } } diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 5b068c78..2326ffc6 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -9,6 +9,7 @@ publish = false iced = { path = "../..", features = ["async-std", "debug"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +lazy_static = "1.4" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] async-std = "1.0" diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 7dde235a..bb00aac6 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -1,14 +1,23 @@ use iced::alignment::{self, Alignment}; +use iced::event::{self, Event}; +use iced::keyboard; +use iced::subscription; use iced::theme::{self, Theme}; use iced::widget::{ - button, checkbox, column, container, row, scrollable, text, text_input, - Text, + self, button, checkbox, column, container, row, scrollable, text, + text_input, Text, }; use iced::window; use iced::{Application, Element}; -use iced::{Color, Command, Font, Length, Settings}; +use iced::{Color, Command, Font, Length, Settings, Subscription}; + +use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; +lazy_static! { + static ref INPUT_ID: text_input::Id = text_input::Id::unique(); +} + pub fn main() -> iced::Result { Todos::run(Settings { window: window::Settings { @@ -42,6 +51,7 @@ enum Message { CreateTask, FilterChanged(Filter), TaskMessage(usize, TaskMessage), + TabPressed { shift: bool }, } impl Application for Todos { @@ -84,14 +94,16 @@ impl Application for Todos { _ => {} } - Command::none() + text_input::focus(INPUT_ID.clone()) } Todos::Loaded(state) => { let mut saved = false; - match message { + let command = match message { Message::InputChanged(value) => { state.input_value = value; + + Command::none() } Message::CreateTask => { if !state.input_value.is_empty() { @@ -100,30 +112,56 @@ impl Application for Todos { .push(Task::new(state.input_value.clone())); state.input_value.clear(); } + + Command::none() } Message::FilterChanged(filter) => { state.filter = filter; + + Command::none() } Message::TaskMessage(i, TaskMessage::Delete) => { state.tasks.remove(i); + + Command::none() } Message::TaskMessage(i, task_message) => { if let Some(task) = state.tasks.get_mut(i) { + let should_focus = + matches!(task_message, TaskMessage::Edit); + task.update(task_message); + + if should_focus { + text_input::focus(Task::text_input_id(i)) + } else { + Command::none() + } + } else { + Command::none() } } Message::Saved(_) => { state.saving = false; saved = true; + + Command::none() } - _ => {} - } + Message::TabPressed { shift } => { + if shift { + widget::focus_previous() + } else { + widget::focus_next() + } + } + _ => Command::none(), + }; if !saved { state.dirty = true; } - if state.dirty && !state.saving { + let save = if state.dirty && !state.saving { state.dirty = false; state.saving = true; @@ -138,7 +176,9 @@ impl Application for Todos { ) } else { Command::none() - } + }; + + Command::batch(vec![command, save]) } } } @@ -163,6 +203,7 @@ impl Application for Todos { input_value, Message::InputChanged, ) + .id(INPUT_ID.clone()) .padding(15) .size(30) .on_submit(Message::CreateTask); @@ -178,12 +219,13 @@ impl Application for Todos { .enumerate() .filter(|(_, task)| filter.matches(task)) .map(|(i, task)| { - task.view().map(move |message| { + task.view(i).map(move |message| { Message::TaskMessage(i, message) }) }) .collect(), ) + .spacing(10) .into() } else { empty_message(match filter { @@ -209,6 +251,22 @@ impl Application for Todos { } } } + + fn subscription(&self) -> Subscription<Message> { + subscription::events_with(|event, status| match (event, status) { + ( + Event::Keyboard(keyboard::Event::KeyPressed { + key_code: keyboard::KeyCode::Tab, + modifiers, + .. + }), + event::Status::Ignored, + ) => Some(Message::TabPressed { + shift: modifiers.shift(), + }), + _ => None, + }) + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -242,6 +300,10 @@ pub enum TaskMessage { } impl Task { + fn text_input_id(i: usize) -> text_input::Id { + text_input::Id::new(format!("task-{}", i)) + } + fn new(description: String) -> Self { Task { description, @@ -270,7 +332,7 @@ impl Task { } } - fn view(&self) -> Element<TaskMessage> { + fn view(&self, i: usize) -> Element<TaskMessage> { match &self.state { TaskState::Idle => { let checkbox = checkbox( @@ -297,6 +359,7 @@ impl Task { &self.description, TaskMessage::DescriptionEdited, ) + .id(Self::text_input_id(i)) .on_submit(TaskMessage::FinishEdition) .padding(10); diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 75d2ce08..82dac0cd 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -452,6 +452,7 @@ impl<'a> Step { .map(Element::from) .collect() ) + .spacing(10) ] .padding(20) .spacing(10); @@ -594,10 +595,7 @@ fn ferris<'a>(width: u16) -> Container<'a, StepMessage> { if cfg!(target_arch = "wasm32") { image("tour/images/ferris.png") } else { - image(format!( - "{}/../../tour/images/ferris.png", - env!("CARGO_MANIFEST_DIR") - )) + image(format!("{}/images/ferris.png", env!("CARGO_MANIFEST_DIR"))) } .width(Length::Units(width)), ) diff --git a/examples/websocket/Cargo.toml b/examples/websocket/Cargo.toml index db131dd7..c582733f 100644 --- a/examples/websocket/Cargo.toml +++ b/examples/websocket/Cargo.toml @@ -9,6 +9,7 @@ publish = false iced = { path = "../..", features = ["tokio", "debug"] } iced_native = { path = "../../native" } iced_futures = { path = "../../futures" } +lazy_static = "1.4" [dependencies.async-tungstenite] version = "0.16" diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 28a9de37..3902e04c 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -49,37 +49,42 @@ impl Application for WebSocket { match message { Message::NewMessageChanged(new_message) => { self.new_message = new_message; + + Command::none() } Message::Send(message) => match &mut self.state { State::Connected(connection) => { self.new_message.clear(); connection.send(message); + + Command::none() } - State::Disconnected => {} + State::Disconnected => Command::none(), }, Message::Echo(event) => match event { echo::Event::Connected(connection) => { self.state = State::Connected(connection); self.messages.push(echo::Message::connected()); + + Command::none() } echo::Event::Disconnected => { self.state = State::Disconnected; self.messages.push(echo::Message::disconnected()); + + Command::none() } echo::Event::MessageReceived(message) => { self.messages.push(message); - // TODO - // self.message_log.snap_to(1.0); + scrollable::snap_to(MESSAGE_LOG.clone(), 1.0) } }, - Message::Server => {} + Message::Server => Command::none(), } - - Command::none() } fn subscription(&self) -> Subscription<Message> { @@ -110,6 +115,7 @@ impl Application for WebSocket { .width(Length::Fill) .spacing(10), ) + .id(MESSAGE_LOG.clone()) .height(Length::Fill) .into() }; @@ -158,3 +164,7 @@ impl Default for State { Self::Disconnected } } + +lazy_static::lazy_static! { + static ref MESSAGE_LOG: scrollable::Id = scrollable::Id::unique(); +} diff --git a/glutin/src/application.rs b/glutin/src/application.rs index dddf0067..24f315fb 100644 --- a/glutin/src/application.rs +++ b/glutin/src/application.rs @@ -12,7 +12,7 @@ use iced_winit::futures; use iced_winit::futures::channel::mpsc; use iced_winit::renderer; use iced_winit::user_interface; -use iced_winit::{Clipboard, Debug, Proxy, Settings}; +use iced_winit::{Clipboard, Command, Debug, Proxy, Settings}; use glutin::window::Window; use std::mem::ManuallyDrop; @@ -39,9 +39,9 @@ where debug.startup_started(); let mut event_loop = EventLoop::with_user_event(); - let mut proxy = event_loop.create_proxy(); + let proxy = event_loop.create_proxy(); - let mut runtime = { + let runtime = { let executor = E::new().map_err(Error::ExecutorCreationFailed)?; let proxy = Proxy::new(event_loop.create_proxy()); @@ -54,8 +54,6 @@ where runtime.enter(|| A::new(flags)) }; - let subscription = application.subscription(); - let context = { let builder = settings.window.into_builder( &application.title(), @@ -125,18 +123,6 @@ where })? }; - let mut clipboard = Clipboard::connect(context.window()); - - application::run_command( - init_command, - &mut runtime, - &mut clipboard, - &mut proxy, - context.window(), - || compositor.fetch_information(), - ); - runtime.track(subscription); - let (mut sender, receiver) = mpsc::unbounded(); let mut instance = Box::pin(run_instance::<A, E, C>( @@ -144,11 +130,11 @@ where compositor, renderer, runtime, - clipboard, proxy, debug, receiver, context, + init_command, settings.exit_on_close_request, )); @@ -196,11 +182,11 @@ async fn run_instance<A, E, C>( mut compositor: C, mut renderer: A::Renderer, mut runtime: Runtime<E, Proxy<A::Message>, A::Message>, - mut clipboard: Clipboard, mut proxy: glutin::event_loop::EventLoopProxy<A::Message>, mut debug: Debug, mut receiver: mpsc::UnboundedReceiver<glutin::event::Event<'_, A::Message>>, mut context: glutin::ContextWrapper<glutin::PossiblyCurrent, Window>, + init_command: Command<A::Message>, exit_on_close_request: bool, ) where A: Application + 'static, @@ -211,12 +197,29 @@ async fn run_instance<A, E, C>( use glutin::event; use iced_winit::futures::stream::StreamExt; + let mut clipboard = Clipboard::connect(context.window()); + let mut cache = user_interface::Cache::default(); let mut state = application::State::new(&application, context.window()); let mut viewport_version = state.viewport_version(); + application::run_command( + &application, + &mut cache, + &state, + &mut renderer, + init_command, + &mut runtime, + &mut clipboard, + &mut proxy, + &mut debug, + context.window(), + || compositor.fetch_information(), + ); + runtime.track(application.subscription()); + let mut user_interface = ManuallyDrop::new(application::build_user_interface( - &mut application, + &application, user_interface::Cache::default(), &mut renderer, state.logical_size(), @@ -258,12 +261,15 @@ async fn run_instance<A, E, C>( user_interface::State::Outdated ) { - let cache = + let mut cache = ManuallyDrop::into_inner(user_interface).into_cache(); // Update application application::update( &mut application, + &mut cache, + &state, + &mut renderer, &mut runtime, &mut clipboard, &mut proxy, @@ -280,7 +286,7 @@ async fn run_instance<A, E, C>( user_interface = ManuallyDrop::new(application::build_user_interface( - &mut application, + &application, cache, &mut renderer, state.logical_size(), diff --git a/native/src/command.rs b/native/src/command.rs index 89d0f045..b0b12805 100644 --- a/native/src/command.rs +++ b/native/src/command.rs @@ -3,6 +3,8 @@ mod action; pub use action::Action; +use crate::widget; + use iced_futures::MaybeSend; use std::fmt; @@ -24,6 +26,13 @@ impl<T> Command<T> { Self(iced_futures::Command::single(action)) } + /// Creates a [`Command`] that performs a [`widget::Operation`]. + pub fn widget(operation: impl widget::Operation<T> + 'static) -> Self { + Self(iced_futures::Command::single(Action::Widget( + widget::Action::new(operation), + ))) + } + /// Creates a [`Command`] that performs the action of the given future. pub fn perform<A>( future: impl Future<Output = T> + 'static + MaybeSend, @@ -51,6 +60,7 @@ impl<T> Command<T> { ) -> Command<A> where T: 'static, + A: 'static, { let Command(command) = self; diff --git a/native/src/command/action.rs b/native/src/command/action.rs index 1bb03cef..3fb02899 100644 --- a/native/src/command/action.rs +++ b/native/src/command/action.rs @@ -1,5 +1,6 @@ use crate::clipboard; use crate::system; +use crate::widget; use crate::window; use iced_futures::MaybeSend; @@ -23,6 +24,9 @@ pub enum Action<T> { /// Run a system action. System(system::Action<T>), + + /// Run a widget action. + Widget(widget::Action<T>), } impl<T> Action<T> { @@ -34,6 +38,7 @@ impl<T> Action<T> { f: impl Fn(T) -> A + 'static + MaybeSend + Sync, ) -> Action<A> where + A: 'static, T: 'static, { use iced_futures::futures::FutureExt; @@ -43,6 +48,7 @@ impl<T> Action<T> { Self::Clipboard(action) => Action::Clipboard(action.map(f)), Self::Window(window) => Action::Window(window), Self::System(system) => Action::System(system.map(f)), + Self::Widget(widget) => Action::Widget(widget.map(f)), } } } @@ -56,6 +62,7 @@ impl<T> fmt::Debug for Action<T> { } Self::Window(action) => write!(f, "Action::Window({:?})", action), Self::System(action) => write!(f, "Action::System({:?})", action), + Self::Widget(_action) => write!(f, "Action::Widget"), } } } diff --git a/native/src/element.rs b/native/src/element.rs index cc74035e..8b994d73 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -3,6 +3,7 @@ use crate::layout; use crate::mouse; use crate::overlay; use crate::renderer; +use crate::widget; use crate::widget::tree::{self, Tree}; use crate::{Clipboard, Layout, Length, Point, Rectangle, Shell, Widget}; @@ -248,6 +249,42 @@ where self.widget.layout(renderer, limits) } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn widget::Operation<B>, + ) { + struct MapOperation<'a, B> { + operation: &'a mut dyn widget::Operation<B>, + } + + impl<'a, T, B> widget::Operation<T> for MapOperation<'a, B> { + fn container( + &mut self, + id: Option<&widget::Id>, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation<T>, + ), + ) { + self.operation.container(id, &mut |operation| { + operate_on_children(&mut MapOperation { operation }); + }); + } + + fn focusable( + &mut self, + state: &mut dyn widget::operation::Focusable, + id: Option<&widget::Id>, + ) { + self.operation.focusable(state, id); + } + } + + self.widget + .operate(tree, layout, &mut MapOperation { operation }); + } + fn on_event( &mut self, tree: &mut Tree, diff --git a/native/src/overlay.rs b/native/src/overlay.rs index c2a98693..905d3389 100644 --- a/native/src/overlay.rs +++ b/native/src/overlay.rs @@ -10,6 +10,7 @@ use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::renderer; +use crate::widget; use crate::widget::tree::{self, Tree}; use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size}; @@ -63,6 +64,14 @@ where /// Reconciliates the [`Widget`] with the provided [`Tree`]. fn diff(&self, _tree: &mut Tree) {} + /// Applies an [`Operation`] to the [`Widget`]. + fn operate( + &self, + _layout: Layout<'_>, + _operation: &mut dyn widget::Operation<Message>, + ) { + } + /// Processes a runtime [`Event`]. /// /// It receives: diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs index de2e1f37..b919c221 100644 --- a/native/src/overlay/element.rs +++ b/native/src/overlay/element.rs @@ -4,6 +4,7 @@ use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::renderer; +use crate::widget; use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector}; /// A generic [`Overlay`]. @@ -102,6 +103,15 @@ where self.overlay .draw(renderer, theme, style, layout, cursor_position) } + + /// Applies an [`Operation`] to the [`Element`]. + pub fn operate( + &self, + layout: Layout<'_>, + operation: &mut dyn widget::Operation<Message>, + ) { + self.overlay.operate(layout, operation); + } } struct Map<'a, A, B, Renderer> { diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 9f3a8e21..42669f95 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -479,6 +479,29 @@ where .unwrap_or(base_interaction) } + /// Applies a [`widget::Operation`] to the [`UserInterface`]. + pub fn operate( + &mut self, + renderer: &Renderer, + operation: &mut dyn widget::Operation<Message>, + ) { + self.root.as_widget().operate( + &mut self.state, + Layout::new(&self.base), + operation, + ); + + if let Some(layout) = self.overlay.as_ref() { + if let Some(overlay) = self.root.as_widget().overlay( + &mut self.state, + Layout::new(&self.base), + renderer, + ) { + overlay.operate(Layout::new(layout), operation); + } + } + } + /// Relayouts and returns a new [`UserInterface`] using the provided /// bounds. pub fn relayout(self, bounds: Size, renderer: &mut Renderer) -> Self { diff --git a/native/src/widget.rs b/native/src/widget.rs index 9a4f373a..8890b8e7 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -17,6 +17,7 @@ pub mod column; pub mod container; pub mod helpers; pub mod image; +pub mod operation; pub mod pane_grid; pub mod pick_list; pub mod progress_bar; @@ -33,6 +34,9 @@ pub mod toggler; pub mod tooltip; pub mod tree; +mod action; +mod id; + #[doc(no_inline)] pub use button::Button; #[doc(no_inline)] @@ -76,6 +80,10 @@ pub use tooltip::Tooltip; #[doc(no_inline)] pub use tree::Tree; +pub use action::Action; +pub use id::Id; +pub use operation::Operation; + use crate::event::{self, Event}; use crate::layout; use crate::mouse; @@ -159,6 +167,15 @@ where /// Reconciliates the [`Widget`] with the provided [`Tree`]. fn diff(&self, _tree: &mut Tree) {} + /// Applies an [`Operation`] to the [`Widget`]. + fn operate( + &self, + _state: &mut Tree, + _layout: Layout<'_>, + _operation: &mut dyn Operation<Message>, + ) { + } + /// Processes a runtime [`Event`]. /// /// By default, it does nothing. diff --git a/native/src/widget/action.rs b/native/src/widget/action.rs new file mode 100644 index 00000000..766e902b --- /dev/null +++ b/native/src/widget/action.rs @@ -0,0 +1,88 @@ +use crate::widget::operation::{self, Operation}; +use crate::widget::Id; + +use iced_futures::MaybeSend; + +/// An operation to be performed on the widget tree. +#[allow(missing_debug_implementations)] +pub struct Action<T>(Box<dyn Operation<T>>); + +impl<T> Action<T> { + /// Creates a new [`Action`] with the given [`Operation`]. + pub fn new(operation: impl Operation<T> + 'static) -> Self { + Self(Box::new(operation)) + } + + /// Maps the output of an [`Action`] using the given function. + pub fn map<A>( + self, + f: impl Fn(T) -> A + 'static + MaybeSend + Sync, + ) -> Action<A> + where + T: 'static, + A: 'static, + { + Action(Box::new(Map { + operation: self.0, + f: Box::new(f), + })) + } + + /// Consumes the [`Action`] and returns the internal [`Operation`]. + pub fn into_operation(self) -> Box<dyn Operation<T>> { + self.0 + } +} + +#[allow(missing_debug_implementations)] +struct Map<A, B> { + operation: Box<dyn Operation<A>>, + f: Box<dyn Fn(A) -> B>, +} + +impl<A, B> Operation<B> for Map<A, B> +where + A: 'static, + B: 'static, +{ + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>), + ) { + struct MapRef<'a, A, B> { + operation: &'a mut dyn Operation<A>, + f: &'a dyn Fn(A) -> B, + } + + impl<'a, A, B> Operation<B> for MapRef<'a, A, B> { + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>), + ) { + let Self { operation, f } = self; + + operation.container(id, &mut |operation| { + operate_on_children(&mut MapRef { operation, f }); + }); + } + } + + let Self { operation, f } = self; + + MapRef { + operation: operation.as_mut(), + f, + } + .container(id, operate_on_children); + } + + fn focusable( + &mut self, + state: &mut dyn operation::Focusable, + id: Option<&Id>, + ) { + self.operation.focusable(state, id); + } +} diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index 6eac6c1b..6c0b8f6e 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -8,6 +8,7 @@ use crate::overlay; use crate::renderer; use crate::touch; use crate::widget::tree::{self, Tree}; +use crate::widget::Operation; use crate::{ Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget, @@ -164,6 +165,21 @@ where ) } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation<Message>, + ) { + operation.container(None, &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + operation, + ); + }); + } + fn on_event( &mut self, tree: &mut Tree, diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index 834f9858..a8b0f183 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -4,7 +4,7 @@ use crate::layout; use crate::mouse; use crate::overlay; use crate::renderer; -use crate::widget::Tree; +use crate::widget::{Operation, Tree}; use crate::{ Alignment, Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget, @@ -143,6 +143,23 @@ where ) } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation<Message>, + ) { + operation.container(None, &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child.as_widget().operate(state, layout, operation); + }) + }); + } + fn on_event( &mut self, tree: &mut Tree, diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index b0fa0315..2afad3f2 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -5,7 +5,7 @@ use crate::layout; use crate::mouse; use crate::overlay; use crate::renderer; -use crate::widget::Tree; +use crate::widget::{Operation, Tree}; use crate::{ Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget, @@ -165,6 +165,21 @@ where ) } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation<Message>, + ) { + operation.container(None, &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + operation, + ); + }); + } + fn on_event( &mut self, tree: &mut Tree, diff --git a/native/src/widget/helpers.rs b/native/src/widget/helpers.rs index 518ac23b..a62448e9 100644 --- a/native/src/widget/helpers.rs +++ b/native/src/widget/helpers.rs @@ -49,8 +49,8 @@ where /// [`Column`]: widget::Column pub fn column<Message, Renderer>( children: Vec<Element<'_, Message, Renderer>>, -) -> widget::Row<'_, Message, Renderer> { - widget::Row::with_children(children) +) -> widget::Column<'_, Message, Renderer> { + widget::Column::with_children(children) } /// Creates a new [`Row`] with the given children. diff --git a/native/src/widget/id.rs b/native/src/widget/id.rs new file mode 100644 index 00000000..4b8fedf1 --- /dev/null +++ b/native/src/widget/id.rs @@ -0,0 +1,43 @@ +use std::borrow; +use std::sync::atomic::{self, AtomicUsize}; + +static NEXT_ID: AtomicUsize = AtomicUsize::new(0); + +/// The identifier of a generic widget. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(Internal); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into<borrow::Cow<'static, str>>) -> Self { + Self(Internal::Custom(id.into())) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + let id = NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed); + + Self(Internal::Unique(id)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Internal { + Unique(usize), + Custom(borrow::Cow<'static, str>), +} + +#[cfg(test)] +mod tests { + use super::Id; + + #[test] + fn unique_generates_different_ids() { + let a = Id::unique(); + let b = Id::unique(); + + assert_ne!(a, b); + } +} diff --git a/native/src/widget/operation.rs b/native/src/widget/operation.rs new file mode 100644 index 00000000..ef636aa2 --- /dev/null +++ b/native/src/widget/operation.rs @@ -0,0 +1,60 @@ +//! Query or update internal widget state. +pub mod focusable; +pub mod scrollable; + +pub use focusable::Focusable; +pub use scrollable::Scrollable; + +use crate::widget::Id; + +use std::fmt; + +/// A piece of logic that can traverse the widget tree of an application in +/// order to query or update some widget state. +pub trait Operation<T> { + /// Operates on a widget that contains other widgets. + /// + /// The `operate_on_children` function can be called to return control to + /// the widget tree and keep traversing it. + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + ); + + /// Operates on a widget that can be focused. + fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {} + + /// Operates on a widget that can be scrolled. + fn scrollable(&mut self, _state: &mut dyn Scrollable, _id: Option<&Id>) {} + + /// Finishes the [`Operation`] and returns its [`Outcome`]. + fn finish(&self) -> Outcome<T> { + Outcome::None + } +} + +/// The result of an [`Operation`]. +pub enum Outcome<T> { + /// The [`Operation`] produced no result. + None, + + /// The [`Operation`] produced some result. + Some(T), + + /// The [`Operation`] needs to be followed by another [`Operation`]. + Chain(Box<dyn Operation<T>>), +} + +impl<T> fmt::Debug for Outcome<T> +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "Outcome::None"), + Self::Some(output) => write!(f, "Outcome::Some({:?})", output), + Self::Chain(_) => write!(f, "Outcome::Chain(...)"), + } + } +} diff --git a/native/src/widget/operation/focusable.rs b/native/src/widget/operation/focusable.rs new file mode 100644 index 00000000..f17bf178 --- /dev/null +++ b/native/src/widget/operation/focusable.rs @@ -0,0 +1,169 @@ +//! Operate on widgets that can be focused. +use crate::widget::operation::{Operation, Outcome}; +use crate::widget::Id; + +/// The internal state of a widget that can be focused. +pub trait Focusable { + /// Returns whether the widget is focused or not. + fn is_focused(&self) -> bool; + + /// Focuses the widget. + fn focus(&mut self); + + /// Unfocuses the widget. + fn unfocus(&mut self); +} + +/// A summary of the focusable widgets present on a widget tree. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Count { + /// The index of the current focused widget, if any. + focused: Option<usize>, + + /// The total amount of focusable widgets. + total: usize, +} + +/// Produces an [`Operation`] that focuses the widget with the given [`Id`]. +pub fn focus<T>(target: Id) -> impl Operation<T> { + struct Focus { + target: Id, + } + + impl<T> Operation<T> for Focus { + fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { + match id { + Some(id) if id == &self.target => { + state.focus(); + } + _ => { + state.unfocus(); + } + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + ) { + operate_on_children(self) + } + } + + Focus { target } +} + +/// Produces an [`Operation`] that generates a [`Count`] and chains it with the +/// provided function to build a new [`Operation`]. +pub fn count<T, O>(f: fn(Count) -> O) -> impl Operation<T> +where + O: Operation<T> + 'static, +{ + struct CountFocusable<O> { + count: Count, + next: fn(Count) -> O, + } + + impl<T, O> Operation<T> for CountFocusable<O> + where + O: Operation<T> + 'static, + { + fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { + if state.is_focused() { + self.count.focused = Some(self.count.total); + } + + self.count.total += 1; + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + ) { + operate_on_children(self) + } + + fn finish(&self) -> Outcome<T> { + Outcome::Chain(Box::new((self.next)(self.count))) + } + } + + CountFocusable { + count: Count::default(), + next: f, + } +} + +/// Produces an [`Operation`] that searches for the current focused widget, and +/// - if found, focuses the previous focusable widget. +/// - if not found, focuses the last focusable widget. +pub fn focus_previous<T>() -> impl Operation<T> { + struct FocusPrevious { + count: Count, + current: usize, + } + + impl<T> Operation<T> for FocusPrevious { + fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { + if self.count.total == 0 { + return; + } + + match self.count.focused { + None if self.current == self.count.total - 1 => state.focus(), + Some(0) if self.current == 0 => state.unfocus(), + Some(0) => {} + Some(focused) if focused == self.current => state.unfocus(), + Some(focused) if focused - 1 == self.current => state.focus(), + _ => {} + } + + self.current += 1; + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + ) { + operate_on_children(self) + } + } + + count(|count| FocusPrevious { count, current: 0 }) +} + +/// Produces an [`Operation`] that searches for the current focused widget, and +/// - if found, focuses the next focusable widget. +/// - if not found, focuses the first focusable widget. +pub fn focus_next<T>() -> impl Operation<T> { + struct FocusNext { + count: Count, + current: usize, + } + + impl<T> Operation<T> for FocusNext { + fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { + match self.count.focused { + None if self.current == 0 => state.focus(), + Some(focused) if focused == self.current => state.unfocus(), + Some(focused) if focused + 1 == self.current => state.focus(), + _ => {} + } + + self.current += 1; + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + ) { + operate_on_children(self) + } + } + + count(|count| FocusNext { count, current: 0 }) +} diff --git a/native/src/widget/operation/scrollable.rs b/native/src/widget/operation/scrollable.rs new file mode 100644 index 00000000..2210137d --- /dev/null +++ b/native/src/widget/operation/scrollable.rs @@ -0,0 +1,35 @@ +//! Operate on widgets that can be scrolled. +use crate::widget::{Id, Operation}; + +/// The internal state of a widget that can be scrolled. +pub trait Scrollable { + /// Snaps the scroll of the widget to the given `percentage`. + fn snap_to(&mut self, percentage: f32); +} + +/// Produces an [`Operation`] that snaps the widget with the given [`Id`] to +/// the provided `percentage`. +pub fn snap_to<T>(target: Id, percentage: f32) -> impl Operation<T> { + struct SnapTo { + target: Id, + percentage: f32, + } + + impl<T> Operation<T> for SnapTo { + fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) { + if Some(&self.target) == id { + state.snap_to(self.percentage); + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + ) { + operate_on_children(self) + } + } + + SnapTo { target, percentage } +} diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs index c342c277..eda7c2d3 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -4,7 +4,7 @@ use crate::layout::{self, Layout}; use crate::mouse; use crate::overlay; use crate::renderer; -use crate::widget::Tree; +use crate::widget::{Operation, Tree}; use crate::{ Alignment, Clipboard, Element, Length, Padding, Point, Rectangle, Shell, Widget, @@ -130,6 +130,23 @@ where ) } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation<Message>, + ) { + operation.container(None, &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child.as_widget().operate(state, layout, operation); + }) + }); + } + fn on_event( &mut self, tree: &mut Tree, diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index b40c3743..4ebb07a0 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -5,10 +5,12 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::touch; +use crate::widget; +use crate::widget::operation::{self, Operation}; use crate::widget::tree::{self, Tree}; use crate::{ - Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle, - Shell, Size, Vector, Widget, + Background, Clipboard, Color, Command, Element, Layout, Length, Point, + Rectangle, Shell, Size, Vector, Widget, }; use std::{f32, u32}; @@ -30,6 +32,7 @@ where Renderer: crate::Renderer, Renderer::Theme: StyleSheet, { + id: Option<Id>, height: Length, scrollbar_width: u16, scrollbar_margin: u16, @@ -47,6 +50,7 @@ where /// Creates a new [`Scrollable`]. pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self { Scrollable { + id: None, height: Length::Shrink, scrollbar_width: 10, scrollbar_margin: 0, @@ -57,6 +61,12 @@ where } } + /// Sets the [`Id`] of the [`Scrollable`]. + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } + /// Sets the height of the [`Scrollable`]. pub fn height(mut self, height: Length) -> Self { self.height = height; @@ -150,6 +160,25 @@ where ) } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation<Message>, + ) { + let state = tree.state.downcast_mut::<State>(); + + operation.scrollable(state, self.id.as_ref().map(|id| &id.0)); + + operation.container(None, &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + operation, + ); + }); + } + fn on_event( &mut self, tree: &mut Tree, @@ -287,6 +316,30 @@ where } } +/// The identifier of a [`Scrollable`]. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(widget::Id); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self { + Self(widget::Id::new(id)) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + Self(widget::Id::unique()) + } +} + +/// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`Id`] +/// to the provided `percentage`. +pub fn snap_to<Message: 'static>(id: Id, percentage: f32) -> Command<Message> { + Command::widget(operation::scrollable::snap_to(id.0, percentage)) +} + /// Computes the layout of a [`Scrollable`]. pub fn layout<Renderer>( renderer: &Renderer, @@ -774,6 +827,12 @@ impl Default for State { } } +impl operation::Scrollable for State { + fn snap_to(&mut self, percentage: f32) { + State::snap_to(self, percentage); + } +} + /// The local state of a [`Scrollable`]. #[derive(Debug, Clone, Copy)] enum Offset { diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 4ef9e11b..8ddbc734 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -19,10 +19,12 @@ use crate::mouse::{self, click}; use crate::renderer; use crate::text::{self, Text}; use crate::touch; +use crate::widget; +use crate::widget::operation::{self, Operation}; use crate::widget::tree::{self, Tree}; use crate::{ - Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, - Shell, Size, Vector, Widget, + Clipboard, Color, Command, Element, Layout, Length, Padding, Point, + Rectangle, Shell, Size, Vector, Widget, }; pub use iced_style::text_input::{Appearance, StyleSheet}; @@ -53,6 +55,7 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet, { + id: Option<Id>, placeholder: String, value: Value, is_secure: bool, @@ -83,6 +86,7 @@ where F: 'a + Fn(String) -> Message, { TextInput { + id: None, placeholder: String::from(placeholder), value: Value::new(value), is_secure: false, @@ -97,6 +101,12 @@ where } } + /// Sets the [`Id`] of the [`TextInput`]. + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } + /// Converts the [`TextInput`] into a secure password input. pub fn password(mut self) -> Self { self.is_secure = true; @@ -214,6 +224,17 @@ where layout(renderer, limits, self.width, self.padding, self.size) } + fn operate( + &self, + tree: &mut Tree, + _layout: Layout<'_>, + operation: &mut dyn Operation<Message>, + ) { + let state = tree.state.downcast_mut::<State>(); + + operation.focusable(state, self.id.as_ref().map(|id| &id.0)); + } + fn on_event( &mut self, tree: &mut Tree, @@ -293,6 +314,29 @@ where } } +/// The identifier of a [`TextInput`]. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(widget::Id); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self { + Self(widget::Id::new(id)) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + Self(widget::Id::unique()) + } +} + +/// Produces a [`Command`] that focuses the [`TextInput`] with the given [`Id`]. +pub fn focus<Message: 'static>(id: Id) -> Command<Message> { + Command::widget(operation::focusable::focus(id.0)) +} + /// Computes the layout of a [`TextInput`]. pub fn layout<Renderer>( renderer: &Renderer, @@ -914,6 +958,7 @@ impl State { /// Focuses the [`TextInput`]. pub fn focus(&mut self) { self.is_focused = true; + self.move_cursor_to_end(); } /// Unfocuses the [`TextInput`]. @@ -942,6 +987,20 @@ impl State { } } +impl operation::Focusable for State { + fn is_focused(&self) -> bool { + State::is_focused(self) + } + + fn focus(&mut self) { + State::focus(self) + } + + fn unfocus(&mut self) { + State::unfocus(self) + } +} + mod platform { use crate::keyboard; @@ -192,22 +192,26 @@ use iced_wgpu as renderer; use iced_glow as renderer; pub use iced_native::theme; +pub use runtime::event; +pub use runtime::subscription; pub use application::Application; pub use element::Element; pub use error::Error; +pub use event::Event; pub use executor::Executor; pub use renderer::Renderer; pub use result::Result; pub use sandbox::Sandbox; pub use settings::Settings; +pub use subscription::Subscription; pub use theme::Theme; pub use runtime::alignment; pub use runtime::futures; pub use runtime::{ Alignment, Background, Color, Command, ContentFit, Font, Length, Padding, - Point, Rectangle, Size, Subscription, Vector, + Point, Rectangle, Size, Vector, }; #[cfg(feature = "system")] diff --git a/src/widget.rs b/src/widget.rs index 4ddf0566..817d2d33 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -99,7 +99,7 @@ pub mod radio { pub mod scrollable { //! Navigate an endless amount of content with a scrollbar. pub use iced_native::widget::scrollable::{ - style::Scrollbar, style::Scroller, StyleSheet, + snap_to, style::Scrollbar, style::Scroller, Id, StyleSheet, }; /// A widget that can vertically display an infinite amount of content @@ -119,7 +119,9 @@ pub mod toggler { pub mod text_input { //! Display fields that can be filled with text. - pub use iced_native::widget::text_input::{Appearance, StyleSheet}; + pub use iced_native::widget::text_input::{ + focus, Appearance, Id, StyleSheet, + }; /// A field that can be filled with text. pub type TextInput<'a, Message, Renderer = crate::Renderer> = @@ -209,3 +211,22 @@ pub use qr_code::QRCode; #[cfg(feature = "svg")] #[cfg_attr(docsrs, doc(cfg(feature = "svg")))] pub use svg::Svg; + +use crate::Command; +use iced_native::widget::operation; + +/// Focuses the previous focusable widget. +pub fn focus_previous<Message>() -> Command<Message> +where + Message: 'static, +{ + Command::widget(operation::focusable::focus_previous()) +} + +/// Focuses the next focusable widget. +pub fn focus_next<Message>() -> Command<Message> +where + Message: 'static, +{ + Command::widget(operation::focusable::focus_next()) +} diff --git a/winit/src/application.rs b/winit/src/application.rs index 99402cf5..3a5c3dac 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -7,6 +7,7 @@ use crate::clipboard::{self, Clipboard}; use crate::conversion; use crate::mouse; use crate::renderer; +use crate::widget::operation; use crate::{ Command, Debug, Error, Executor, Mode, Proxy, Runtime, Settings, Size, Subscription, @@ -131,9 +132,9 @@ where debug.startup_started(); let event_loop = EventLoop::with_user_event(); - let mut proxy = event_loop.create_proxy(); + let proxy = event_loop.create_proxy(); - let mut runtime = { + let runtime = { let proxy = Proxy::new(event_loop.create_proxy()); let executor = E::new().map_err(Error::ExecutorCreationFailed)?; @@ -146,8 +147,6 @@ where runtime.enter(|| A::new(flags)) }; - let subscription = application.subscription(); - let builder = settings.window.into_builder( &application.title(), application.mode(), @@ -176,20 +175,8 @@ where .expect("Append canvas to HTML body"); } - let mut clipboard = Clipboard::connect(&window); - let (compositor, renderer) = C::new(compositor_settings, Some(&window))?; - run_command( - init_command, - &mut runtime, - &mut clipboard, - &mut proxy, - &window, - || compositor.fetch_information(), - ); - runtime.track(subscription); - let (mut sender, receiver) = mpsc::unbounded(); let mut instance = Box::pin(run_instance::<A, E, C>( @@ -197,10 +184,10 @@ where compositor, renderer, runtime, - clipboard, proxy, debug, receiver, + init_command, window, settings.exit_on_close_request, )); @@ -247,10 +234,10 @@ async fn run_instance<A, E, C>( mut compositor: C, mut renderer: A::Renderer, mut runtime: Runtime<E, Proxy<A::Message>, A::Message>, - mut clipboard: Clipboard, mut proxy: winit::event_loop::EventLoopProxy<A::Message>, mut debug: Debug, mut receiver: mpsc::UnboundedReceiver<winit::event::Event<'_, A::Message>>, + init_command: Command<A::Message>, window: winit::window::Window, exit_on_close_request: bool, ) where @@ -262,6 +249,8 @@ async fn run_instance<A, E, C>( use iced_futures::futures::stream::StreamExt; use winit::event; + let mut clipboard = Clipboard::connect(&window); + let mut cache = user_interface::Cache::default(); let mut surface = compositor.create_surface(&window); let mut state = State::new(&application, &window); @@ -275,9 +264,24 @@ async fn run_instance<A, E, C>( physical_size.height, ); + run_command( + &application, + &mut cache, + &state, + &mut renderer, + init_command, + &mut runtime, + &mut clipboard, + &mut proxy, + &mut debug, + &window, + || compositor.fetch_information(), + ); + runtime.track(application.subscription()); + let mut user_interface = ManuallyDrop::new(build_user_interface( - &mut application, - user_interface::Cache::default(), + &application, + cache, &mut renderer, state.logical_size(), &mut debug, @@ -318,12 +322,15 @@ async fn run_instance<A, E, C>( user_interface::State::Outdated, ) { - let cache = + let mut cache = ManuallyDrop::into_inner(user_interface).into_cache(); // Update application update( &mut application, + &mut cache, + &state, + &mut renderer, &mut runtime, &mut clipboard, &mut proxy, @@ -339,7 +346,7 @@ async fn run_instance<A, E, C>( let should_exit = application.should_exit(); user_interface = ManuallyDrop::new(build_user_interface( - &mut application, + &application, cache, &mut renderer, state.logical_size(), @@ -515,7 +522,7 @@ pub fn requests_exit( /// Builds a [`UserInterface`] for the provided [`Application`], logging /// [`struct@Debug`] information accordingly. pub fn build_user_interface<'a, A: Application>( - application: &'a mut A, + application: &'a A, cache: user_interface::Cache, renderer: &mut A::Renderer, size: Size, @@ -539,6 +546,9 @@ where /// resulting [`Command`], and tracking its [`Subscription`]. pub fn update<A: Application, E: Executor>( application: &mut A, + cache: &mut user_interface::Cache, + state: &State<A>, + renderer: &mut A::Renderer, runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, clipboard: &mut Clipboard, proxy: &mut winit::event_loop::EventLoopProxy<A::Message>, @@ -556,7 +566,19 @@ pub fn update<A: Application, E: Executor>( let command = runtime.enter(|| application.update(message)); debug.update_finished(); - run_command(command, runtime, clipboard, proxy, window, graphics_info); + run_command( + application, + cache, + state, + renderer, + command, + runtime, + clipboard, + proxy, + debug, + window, + graphics_info, + ); } let subscription = application.subscription(); @@ -564,14 +586,23 @@ pub fn update<A: Application, E: Executor>( } /// Runs the actions of a [`Command`]. -pub fn run_command<Message: 'static + std::fmt::Debug + Send, E: Executor>( - command: Command<Message>, - runtime: &mut Runtime<E, Proxy<Message>, Message>, +pub fn run_command<A, E>( + application: &A, + cache: &mut user_interface::Cache, + state: &State<A>, + renderer: &mut A::Renderer, + command: Command<A::Message>, + runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, clipboard: &mut Clipboard, - proxy: &mut winit::event_loop::EventLoopProxy<Message>, + proxy: &mut winit::event_loop::EventLoopProxy<A::Message>, + debug: &mut Debug, window: &winit::window::Window, _graphics_info: impl FnOnce() -> compositor::Information + Copy, -) { +) where + A: Application, + E: Executor, + <A::Renderer as crate::Renderer>::Theme: StyleSheet, +{ use iced_native::command; use iced_native::system; use iced_native::window; @@ -627,6 +658,37 @@ pub fn run_command<Message: 'static + std::fmt::Debug + Send, E: Executor>( } } }, + command::Action::Widget(action) => { + let mut current_cache = std::mem::take(cache); + let mut current_operation = Some(action.into_operation()); + + let mut user_interface = build_user_interface( + application, + current_cache, + renderer, + state.logical_size(), + debug, + ); + + while let Some(mut operation) = current_operation.take() { + user_interface.operate(renderer, operation.as_mut()); + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(message) => { + proxy + .send_event(message) + .expect("Send message to event loop"); + } + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } + } + } + + current_cache = user_interface.into_cache(); + *cache = current_cache; + } } } } |