diff --git a/Cargo.lock b/Cargo.lock index 9d7a5ed3..dd3236ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2654,6 +2654,7 @@ dependencies = [ name = "iced_test" version = "0.14.0-dev" dependencies = [ + "iced_program", "iced_renderer", "iced_runtime", "nom 8.0.0", diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index fc6d1cdb..e8dcfa71 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -60,6 +60,10 @@ where P::name() } + fn settings(&self) -> core::Settings { + self.program.settings() + } + fn boot(&self) -> (Self::State, Task) { let (state, boot) = self.program.boot(); let (devtools, task) = DevTools::new(state); diff --git a/devtools/src/widget/recorder.rs b/devtools/src/widget/recorder.rs index 3fd88436..bae43478 100644 --- a/devtools/src/widget/recorder.rs +++ b/devtools/src/widget/recorder.rs @@ -68,8 +68,7 @@ where mouse::Event::ButtonPressed(_) | mouse::Event::ButtonReleased(_) | mouse::Event::WheelScrolled { .. } => { - shell - .publish(on_event(Event::Mouse(event.clone()))); + shell.publish(on_event(Event::Mouse(*event))); } mouse::Event::CursorMoved { position } => { shell.publish(on_event(Event::Mouse( diff --git a/program/src/lib.rs b/program/src/lib.rs index 80e59330..c75b02a1 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -4,10 +4,11 @@ pub use iced_runtime as runtime; pub use iced_runtime::core; pub use iced_runtime::futures; +use crate::core::renderer; use crate::core::text; use crate::core::theme; use crate::core::window; -use crate::core::{Element, Font}; +use crate::core::{Element, Font, Settings}; use crate::futures::{Executor, Subscription}; use crate::graphics::compositor; use crate::runtime::Task; @@ -36,6 +37,8 @@ pub trait Program: Sized { /// Returns the unique name of the [`Program`]. fn name() -> &'static str; + fn settings(&self) -> Settings; + fn boot(&self) -> (Self::State, Task); fn update( @@ -128,6 +131,10 @@ pub fn with_title( P::name() } + fn settings(&self) -> Settings { + self.program.settings() + } + fn boot(&self) -> (Self::State, Task) { self.program.boot() } @@ -210,6 +217,10 @@ pub fn with_subscription( P::name() } + fn settings(&self) -> Settings { + self.program.settings() + } + fn boot(&self) -> (Self::State, Task) { self.program.boot() } @@ -293,6 +304,10 @@ pub fn with_theme( P::name() } + fn settings(&self) -> Settings { + self.program.settings() + } + fn boot(&self) -> (Self::State, Task) { self.program.boot() } @@ -372,6 +387,10 @@ pub fn with_style( P::name() } + fn settings(&self) -> Settings { + self.program.settings() + } + fn boot(&self) -> (Self::State, Task) { self.program.boot() } @@ -447,6 +466,10 @@ pub fn with_scale_factor( P::name() } + fn settings(&self) -> Settings { + self.program.settings() + } + fn boot(&self) -> (Self::State, Task) { self.program.boot() } @@ -530,6 +553,10 @@ pub fn with_executor( P::name() } + fn settings(&self) -> Settings { + self.program.settings() + } + fn boot(&self) -> (Self::State, Task) { self.program.boot() } @@ -585,10 +612,15 @@ pub fn with_executor( } /// The renderer of some [`Program`]. -pub trait Renderer: text::Renderer + compositor::Default {} +pub trait Renderer: + text::Renderer + compositor::Default + renderer::Headless +{ +} -impl Renderer for T where T: text::Renderer + compositor::Default -{} +impl Renderer for T where + T: text::Renderer + compositor::Default + renderer::Headless +{ +} /// A particular instance of a running [`Program`]. #[allow(missing_debug_implementations)] diff --git a/src/application.rs b/src/application.rs index e87d89a2..8c8917fc 100644 --- a/src/application.rs +++ b/src/application.rs @@ -136,6 +136,10 @@ where ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { self.view.view(state) } + + fn settings(&self) -> Settings { + Settings::default() + } } Application { @@ -173,6 +177,9 @@ impl Application

{ where Self: 'static, { + let settings = self.settings.clone(); + let window = self.window.clone(); + #[cfg(all(feature = "debug", not(target_arch = "wasm32")))] let program = { iced_debug::init(iced_debug::Metadata { @@ -181,13 +188,13 @@ impl Application

{ can_time_travel: cfg!(feature = "time-travel"), }); - iced_devtools::attach(self.raw) + iced_devtools::attach(self) }; #[cfg(any(not(feature = "debug"), target_arch = "wasm32"))] - let program = self.raw; + let program = self; - Ok(shell::run(program, self.settings, Some(self.window))?) + Ok(shell::run(program, settings, Some(window))?) } /// Sets the [`Settings`] that will be used to run the [`Application`]. @@ -409,6 +416,66 @@ impl Application

{ } } +impl Program for Application

{ + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Renderer = P::Renderer; + type Executor = P::Executor; + + fn name() -> &'static str { + P::name() + } + + fn settings(&self) -> Settings { + self.settings.clone() + } + + fn boot(&self) -> (Self::State, Task) { + self.raw.boot() + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task { + self.raw.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + window: window::Id, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.raw.view(state, window) + } + + fn title(&self, state: &Self::State, window: window::Id) -> String { + self.raw.title(state, window) + } + + fn subscription(&self, state: &Self::State) -> Subscription { + self.raw.subscription(state) + } + + fn theme( + &self, + state: &Self::State, + window: iced_core::window::Id, + ) -> Self::Theme { + self.raw.theme(state, window) + } + + fn style(&self, state: &Self::State, theme: &Self::Theme) -> theme::Style { + self.raw.style(state, theme) + } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + self.raw.scale_factor(state, window) + } +} + /// The logic to initialize the `State` of some [`Application`]. /// /// This trait is implemented for both `Fn() -> State` and diff --git a/src/application/timed.rs b/src/application/timed.rs index 606273c8..5ca45ec4 100644 --- a/src/application/timed.rs +++ b/src/application/timed.rs @@ -86,6 +86,10 @@ where name.split("::").next().unwrap_or("a_cool_application") } + fn settings(&self) -> Settings { + Settings::default() + } + fn boot(&self) -> (State, Task) { let (state, task) = self.boot.boot(); diff --git a/src/daemon.rs b/src/daemon.rs index 1b99da30..b1f6e5ab 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -63,6 +63,10 @@ where name.split("::").next().unwrap_or("a_cool_daemon") } + fn settings(&self) -> Settings { + Settings::default() + } + fn boot(&self) -> (Self::State, Task) { self.boot.boot() } @@ -117,6 +121,8 @@ impl Daemon

{ where Self: 'static, { + let settings = self.settings.clone(); + #[cfg(all(feature = "debug", not(target_arch = "wasm32")))] let program = { iced_debug::init(iced_debug::Metadata { @@ -125,13 +131,13 @@ impl Daemon

{ can_time_travel: cfg!(feature = "time-travel"), }); - iced_devtools::attach(self.raw) + iced_devtools::attach(self) }; #[cfg(any(not(feature = "debug"), target_arch = "wasm32"))] - let program = self.raw; + let program = self; - Ok(shell::run(program, self.settings, None)?) + Ok(shell::run(program, settings, None)?) } /// Sets the [`Settings`] that will be used to run the [`Daemon`]. @@ -250,6 +256,66 @@ impl Daemon

{ } } +impl Program for Daemon

{ + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Renderer = P::Renderer; + type Executor = P::Executor; + + fn name() -> &'static str { + P::name() + } + + fn settings(&self) -> Settings { + self.settings.clone() + } + + fn boot(&self) -> (Self::State, Task) { + self.raw.boot() + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task { + self.raw.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + window: window::Id, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.raw.view(state, window) + } + + fn title(&self, state: &Self::State, window: window::Id) -> String { + self.raw.title(state, window) + } + + fn subscription(&self, state: &Self::State) -> Subscription { + self.raw.subscription(state) + } + + fn theme( + &self, + state: &Self::State, + window: iced_core::window::Id, + ) -> Self::Theme { + self.raw.theme(state, window) + } + + fn style(&self, state: &Self::State, theme: &Self::Theme) -> theme::Style { + self.raw.style(state, theme) + } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + self.raw.scale_factor(state, window) + } +} + /// The title logic of some [`Daemon`]. /// /// This trait is implemented both for `&static str` and diff --git a/test/Cargo.toml b/test/Cargo.toml index af5795ed..132a913b 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -15,6 +15,7 @@ workspace = true [dependencies] iced_runtime.workspace = true +iced_program.workspace = true iced_renderer.workspace = true iced_renderer.features = ["fira-sans"] diff --git a/test/src/emulator.rs b/test/src/emulator.rs new file mode 100644 index 00000000..9b2d2549 --- /dev/null +++ b/test/src/emulator.rs @@ -0,0 +1,169 @@ +use crate::Instruction; +use crate::core; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::window; +use crate::core::{Element, Size}; +use crate::program::Program; +use crate::runtime::futures::futures::StreamExt; +use crate::runtime::futures::futures::channel::mpsc; +use crate::runtime::futures::{Executor, Runtime}; +use crate::runtime::task; +use crate::runtime::user_interface; +use crate::runtime::{Action, UserInterface}; + +#[allow(missing_debug_implementations)] +pub struct Emulator { + state: P::State, + runtime: Runtime>, Event

>, + renderer: P::Renderer, + size: Size, + window: window::Id, + cursor: mouse::Cursor, + clipboard: Clipboard, + cache: Option, +} + +#[allow(missing_debug_implementations)] +pub enum Event { + Action(Action), +} + +impl Emulator

{ + pub fn new( + program: &P, + size: Size, + sender: mpsc::Sender>, + ) -> Emulator

{ + use renderer::Headless; + + let settings = program.settings(); + + // TODO: Error handling + let executor = P::Executor::new().expect("Create emulator executor"); + + let renderer = executor + .block_on(P::Renderer::new( + settings.default_font, + settings.default_text_size, + None, + )) + .expect("Create emulator renderer"); + + let mut runtime = Runtime::new(executor, sender); + + let (state, task) = program.boot(); + + if let Some(stream) = task::into_stream(task) { + runtime.run(stream.map(Event::Action).boxed()); + } + + Self { + state, + runtime, + renderer, + size, + clipboard: Clipboard { content: None }, + cursor: mouse::Cursor::Unavailable, + window: window::Id::unique(), + cache: Some(user_interface::Cache::default()), + } + } + + pub fn update(&mut self, program: &P, message: P::Message) { + let task = program.update(&mut self.state, message); + + if let Some(stream) = task::into_stream(task) { + self.runtime.run(stream.map(Event::Action).boxed()); + } + } + + pub fn perform(&mut self, program: &P, action: Action) { + match action { + Action::Output(message) => { + self.update(program, message); + } + Action::LoadFont { .. } => { + // TODO + } + Action::Widget(_operation) => { + // TODO + } + Action::Clipboard(action) => { + // TODO + dbg!(action); + } + Action::Window(_action) => { + // TODO + } + Action::System(action) => { + // TODO + dbg!(action); + } + Action::Exit => { + // TODO + } + } + } + + pub fn run(&mut self, program: &P, instruction: Instruction) { + let mut user_interface = UserInterface::build( + program.view(&self.state, self.window), + self.size, + self.cache.take().unwrap(), + &mut self.renderer, + ); + + let mut messages = Vec::new(); + + match instruction { + Instruction::Interact(interaction) => { + let events = interaction.events(); + + for event in &events { + if let core::Event::Mouse(mouse::Event::CursorMoved { + position, + }) = event + { + self.cursor = mouse::Cursor::Available(*position); + } + } + + let (_state, _status) = user_interface.update( + &events, + self.cursor, + &mut self.renderer, + &mut self.clipboard, + &mut messages, + ); + } + } + + self.cache = Some(user_interface.into_cache()); + + for message in messages { + self.update(program, message); + } + } + + pub fn view( + &self, + program: &P, + ) -> Element<'_, P::Message, P::Theme, P::Renderer> { + program.view(&self.state, self.window) + } +} + +struct Clipboard { + content: Option, +} + +impl core::Clipboard for Clipboard { + fn read(&self, _kind: core::clipboard::Kind) -> Option { + self.content.clone() + } + + fn write(&mut self, _kind: core::clipboard::Kind, contents: String) { + self.content = Some(contents); + } +} diff --git a/test/src/instruction.rs b/test/src/instruction.rs index 1f8455f8..d1323bcf 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -1,6 +1,7 @@ use crate::core::keyboard; use crate::core::mouse; use crate::core::{Event, Point}; +use crate::simulator; use std::fmt; @@ -145,6 +146,62 @@ impl Interaction { (current, next) => (current, Some(next)), } } + + pub fn events(&self) -> Vec { + let mouse_move_ = + |to| Event::Mouse(mouse::Event::CursorMoved { position: to }); + + let mouse_press = + |button| Event::Mouse(mouse::Event::ButtonPressed(button)); + + let mouse_release = + |button| Event::Mouse(mouse::Event::ButtonReleased(button)); + + let key_press = |key| simulator::press_key(key, None); + + let key_release = |key| simulator::release_key(key); + + match self { + Interaction::Mouse(mouse) => match mouse { + Mouse::Move(to) => vec![mouse_move_(*to)], + Mouse::Press { + button, + at: Some(at), + } => vec![mouse_move_(*at), mouse_press(*button)], + Mouse::Press { button, at: None } => { + vec![mouse_press(*button)] + } + Mouse::Release { + button, + at: Some(at), + } => vec![mouse_move_(*at), mouse_release(*button)], + Mouse::Release { button, at: None } => { + vec![mouse_release(*button)] + } + Mouse::Click { + button, + at: Some(at), + } => { + vec![ + mouse_move_(*at), + mouse_press(*button), + mouse_release(*button), + ] + } + Mouse::Click { button, at: None } => { + vec![mouse_press(*button), mouse_release(*button)] + } + }, + Interaction::Keyboard(keyboard) => match keyboard { + Keyboard::Press(key) => vec![key_press(*key)], + Keyboard::Release(key) => vec![key_release(*key)], + Keyboard::Type(key) => vec![key_press(*key), key_release(*key)], + Keyboard::Typewrite(text) => { + simulator::typewrite(text).collect() + } + }, + } + } } impl fmt::Display for Interaction { @@ -200,14 +257,6 @@ pub enum Keyboard { Typewrite(String), } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Key { - Enter, - Escape, - Tab, - Backspace, -} - impl fmt::Display for Keyboard { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -227,6 +276,25 @@ impl fmt::Display for Keyboard { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Key { + Enter, + Escape, + Tab, + Backspace, +} + +impl From for keyboard::Key { + fn from(key: Key) -> Self { + match key { + Key::Enter => Self::Named(keyboard::key::Named::Enter), + Key::Escape => Self::Named(keyboard::key::Named::Escape), + Key::Tab => Self::Named(keyboard::key::Named::Tab), + Key::Backspace => Self::Named(keyboard::key::Named::Backspace), + } + } +} + mod format { use super::*; diff --git a/test/src/lib.rs b/test/src/lib.rs index 4ec0d8dd..1aa69334 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -84,10 +84,12 @@ //! //! [the classical counter interface]: https://book.iced.rs/architecture.html#dissecting-an-interface #![allow(missing_docs)] +use iced_program as program; use iced_renderer as renderer; use iced_runtime as runtime; use iced_runtime::core; +pub mod emulator; pub mod instruction; pub mod selector; pub mod simulator; @@ -98,8 +100,3 @@ pub use error::Error; pub use instruction::Instruction; pub use selector::Selector; pub use simulator::{Simulator, simulator}; - -#[derive(Debug, Clone)] -pub struct Test { - instructions: Vec, -} diff --git a/test/src/simulator.rs b/test/src/simulator.rs index e638e9a0..cdff5040 100644 --- a/test/src/simulator.rs +++ b/test/src/simulator.rs @@ -483,6 +483,38 @@ pub fn click() -> impl Iterator { .into_iter() } +pub fn press_key( + key: impl Into, + text: Option, +) -> Event { + let key = key.into(); + + Event::Keyboard(keyboard::Event::KeyPressed { + key: key.clone(), + modified_key: key, + physical_key: keyboard::key::Physical::Unidentified( + keyboard::key::NativeCode::Unidentified, + ), + location: keyboard::Location::Standard, + modifiers: keyboard::Modifiers::default(), + text, + }) +} + +pub fn release_key(key: impl Into) -> Event { + let key = key.into(); + + Event::Keyboard(keyboard::Event::KeyReleased { + key: key.clone(), + modified_key: key, + physical_key: keyboard::key::Physical::Unidentified( + keyboard::key::NativeCode::Unidentified, + ), + location: keyboard::Location::Standard, + modifiers: keyboard::Modifiers::default(), + }) +} + /// Returns the sequence of events of a "key tap" (i.e. pressing and releasing a key). pub fn tap_key( key: impl Into, @@ -490,28 +522,7 @@ pub fn tap_key( ) -> impl Iterator { let key = key.into(); - [ - Event::Keyboard(keyboard::Event::KeyPressed { - key: key.clone(), - modified_key: key.clone(), - physical_key: keyboard::key::Physical::Unidentified( - keyboard::key::NativeCode::Unidentified, - ), - location: keyboard::Location::Standard, - modifiers: keyboard::Modifiers::default(), - text, - }), - Event::Keyboard(keyboard::Event::KeyReleased { - key: key.clone(), - modified_key: key, - physical_key: keyboard::key::Physical::Unidentified( - keyboard::key::NativeCode::Unidentified, - ), - location: keyboard::Location::Standard, - modifiers: keyboard::Modifiers::default(), - }), - ] - .into_iter() + [press_key(key.clone(), text), release_key(key)].into_iter() } /// Returns the sequence of events of typewriting the given text in a keyboard.