iced_test/
emulator.rs

1//! Run your application in a headless runtime.
2use crate::core;
3use crate::core::mouse;
4use crate::core::renderer;
5use crate::core::time::Instant;
6use crate::core::widget;
7use crate::core::window;
8use crate::core::{Bytes, Element, Point, Size};
9use crate::instruction;
10use crate::program;
11use crate::program::Program;
12use crate::runtime;
13use crate::runtime::futures::futures::StreamExt;
14use crate::runtime::futures::futures::channel::mpsc;
15use crate::runtime::futures::futures::stream;
16use crate::runtime::futures::subscription;
17use crate::runtime::futures::{Executor, Runtime};
18use crate::runtime::task;
19use crate::runtime::user_interface;
20use crate::runtime::{Task, UserInterface};
21use crate::{Instruction, Selector};
22
23use std::fmt;
24
25/// A headless runtime that can run iced applications and execute
26/// [instructions](crate::Instruction).
27///
28/// An [`Emulator`] runs its program as faithfully as possible to the real thing.
29/// It will run subscriptions and tasks with the [`Executor`](Program::Executor) of
30/// the [`Program`].
31///
32/// If you want to run a simulation without side effects, use a [`Simulator`](crate::Simulator)
33/// instead.
34pub struct Emulator<P: Program> {
35    state: P::State,
36    runtime: Runtime<P::Executor, mpsc::Sender<Event<P>>, Event<P>>,
37    renderer: P::Renderer,
38    mode: Mode,
39    size: Size,
40    window: core::window::Id,
41    cursor: mouse::Cursor,
42    clipboard: Clipboard,
43    cache: Option<user_interface::Cache>,
44    pending_tasks: usize,
45}
46
47/// An emulation event.
48pub enum Event<P: Program> {
49    /// An action that must be [performed](Emulator::perform) by the [`Emulator`].
50    Action(Action<P>),
51    /// An [`Instruction`] failed to be executed.
52    Failed(Instruction),
53    /// The [`Emulator`] is ready.
54    Ready,
55}
56
57/// An action that must be [performed](Emulator::perform) by the [`Emulator`].
58pub struct Action<P: Program>(Action_<P>);
59
60enum Action_<P: Program> {
61    Runtime(runtime::Action<P::Message>),
62    CountDown,
63}
64
65impl<P: Program + 'static> Emulator<P> {
66    /// Creates a new [`Emulator`] of the [`Program`] with the given [`Mode`] and [`Size`].
67    ///
68    /// The [`Emulator`] will send [`Event`] notifications through the provided [`mpsc::Sender`].
69    ///
70    /// When the [`Emulator`] has finished booting, an [`Event::Ready`] will be produced.
71    pub fn new(sender: mpsc::Sender<Event<P>>, program: &P, mode: Mode, size: Size) -> Emulator<P> {
72        Self::with_preset(sender, program, mode, size, None)
73    }
74
75    /// Creates a new [`Emulator`] analogously to [`new`](Self::new), but it also takes a
76    /// [`program::Preset`] that will be used as the initial state.
77    ///
78    /// When the [`Emulator`] has finished booting, an [`Event::Ready`] will be produced.
79    pub fn with_preset(
80        sender: mpsc::Sender<Event<P>>,
81        program: &P,
82        mode: Mode,
83        size: Size,
84        preset: Option<&program::Preset<P::State, P::Message>>,
85    ) -> Emulator<P> {
86        use renderer::Headless;
87
88        let settings = program.settings();
89
90        // TODO: Error handling
91        let executor = P::Executor::new().expect("Create emulator executor");
92
93        let renderer = executor
94            .block_on(P::Renderer::new(
95                settings.default_font,
96                settings.default_text_size,
97                None,
98            ))
99            .expect("Create emulator renderer");
100
101        let runtime = Runtime::new(executor, sender);
102
103        let (state, task) = runtime.enter(|| {
104            if let Some(preset) = preset {
105                preset.boot()
106            } else {
107                program.boot()
108            }
109        });
110
111        let mut emulator = Self {
112            state,
113            runtime,
114            renderer,
115            mode,
116            size,
117            clipboard: Clipboard { content: None },
118            cursor: mouse::Cursor::Unavailable,
119            window: core::window::Id::unique(),
120            cache: Some(user_interface::Cache::default()),
121            pending_tasks: 0,
122        };
123
124        emulator.resubscribe(program);
125        emulator.wait_for(task);
126
127        emulator
128    }
129
130    /// Updates the state of the [`Emulator`] program.
131    ///
132    /// This is equivalent to calling the [`Program::update`] function,
133    /// resubscribing to any subscriptions, and running the resulting tasks
134    /// concurrently.
135    pub fn update(&mut self, program: &P, message: P::Message) {
136        let task = self
137            .runtime
138            .enter(|| program.update(&mut self.state, message));
139
140        self.resubscribe(program);
141
142        match self.mode {
143            Mode::Zen if self.pending_tasks > 0 => self.wait_for(task),
144            _ => {
145                if let Some(stream) = task::into_stream(task) {
146                    self.runtime.run(
147                        stream
148                            .map(Action_::Runtime)
149                            .map(Action)
150                            .map(Event::Action)
151                            .boxed(),
152                    );
153                }
154            }
155        }
156    }
157
158    /// Performs an [`Action`].
159    ///
160    /// Whenever an [`Emulator`] sends an [`Event::Action`], this
161    /// method must be called to proceed with the execution.
162    pub fn perform(&mut self, program: &P, action: Action<P>) {
163        match action.0 {
164            Action_::CountDown => {
165                if self.pending_tasks > 0 {
166                    self.pending_tasks -= 1;
167
168                    if self.pending_tasks == 0 {
169                        self.runtime.send(Event::Ready);
170                    }
171                }
172            }
173            Action_::Runtime(action) => match action {
174                runtime::Action::Output(message) => {
175                    self.update(program, message);
176                }
177                runtime::Action::LoadFont { .. } => {
178                    // TODO
179                }
180                runtime::Action::Widget(operation) => {
181                    let mut user_interface = UserInterface::build(
182                        program.view(&self.state, self.window),
183                        self.size,
184                        self.cache.take().unwrap(),
185                        &mut self.renderer,
186                    );
187
188                    let mut operation = Some(operation);
189
190                    while let Some(mut current) = operation.take() {
191                        user_interface.operate(&self.renderer, &mut current);
192
193                        match current.finish() {
194                            widget::operation::Outcome::None => {}
195                            widget::operation::Outcome::Some(()) => {}
196                            widget::operation::Outcome::Chain(next) => {
197                                operation = Some(next);
198                            }
199                        }
200                    }
201
202                    self.cache = Some(user_interface.into_cache());
203                }
204                runtime::Action::Clipboard(action) => {
205                    // TODO
206                    dbg!(action);
207                }
208                runtime::Action::Window(action) => {
209                    use crate::runtime::window;
210
211                    match action {
212                        window::Action::Open(id, _settings, sender) => {
213                            self.window = id;
214
215                            let _ = sender.send(self.window);
216                        }
217                        window::Action::GetOldest(sender) | window::Action::GetLatest(sender) => {
218                            let _ = sender.send(Some(self.window));
219                        }
220                        window::Action::GetSize(id, sender) => {
221                            if id == self.window {
222                                let _ = sender.send(self.size);
223                            }
224                        }
225                        window::Action::GetMaximized(id, sender) => {
226                            if id == self.window {
227                                let _ = sender.send(false);
228                            }
229                        }
230                        window::Action::GetMinimized(id, sender) => {
231                            if id == self.window {
232                                let _ = sender.send(None);
233                            }
234                        }
235                        window::Action::GetPosition(id, sender) => {
236                            if id == self.window {
237                                let _ = sender.send(Some(Point::ORIGIN));
238                            }
239                        }
240                        window::Action::GetScaleFactor(id, sender) => {
241                            if id == self.window {
242                                let _ = sender.send(1.0);
243                            }
244                        }
245                        window::Action::GetMode(id, sender) => {
246                            if id == self.window {
247                                let _ = sender.send(core::window::Mode::Windowed);
248                            }
249                        }
250                        _ => {
251                            // Ignored
252                        }
253                    }
254                }
255                runtime::Action::System(action) => {
256                    // TODO
257                    dbg!(action);
258                }
259                iced_runtime::Action::Image(action) => {
260                    // TODO
261                    dbg!(action);
262                }
263                runtime::Action::Exit => {
264                    // TODO
265                }
266                runtime::Action::Reload => {
267                    // TODO
268                }
269            },
270        }
271    }
272
273    /// Runs an [`Instruction`].
274    ///
275    /// If the [`Instruction`] executes successfully, an [`Event::Ready`] will be
276    /// produced by the [`Emulator`].
277    ///
278    /// Otherwise, an [`Event::Failed`] will be triggered.
279    pub fn run(&mut self, program: &P, instruction: Instruction) {
280        let mut user_interface = UserInterface::build(
281            program.view(&self.state, self.window),
282            self.size,
283            self.cache.take().unwrap(),
284            &mut self.renderer,
285        );
286
287        let mut messages = Vec::new();
288
289        match &instruction {
290            Instruction::Interact(interaction) => {
291                let Some(events) = interaction.events(|target| match target {
292                    instruction::Target::Point(position) => Some(*position),
293                    instruction::Target::Text(text) => {
294                        use widget::Operation;
295
296                        let mut operation = Selector::find(text.as_str());
297
298                        user_interface.operate(
299                            &self.renderer,
300                            &mut widget::operation::black_box(&mut operation),
301                        );
302
303                        match operation.finish() {
304                            widget::operation::Outcome::Some(text) => {
305                                Some(text?.visible_bounds()?.center())
306                            }
307                            _ => None,
308                        }
309                    }
310                }) else {
311                    self.runtime.send(Event::Failed(instruction));
312                    self.cache = Some(user_interface.into_cache());
313                    return;
314                };
315
316                for event in &events {
317                    if let core::Event::Mouse(mouse::Event::CursorMoved { position }) = event {
318                        self.cursor = mouse::Cursor::Available(*position);
319                    }
320                }
321
322                let (_state, _status) = user_interface.update(
323                    &events,
324                    self.cursor,
325                    &mut self.renderer,
326                    &mut self.clipboard,
327                    &mut messages,
328                );
329
330                self.cache = Some(user_interface.into_cache());
331
332                let task = self.runtime.enter(|| {
333                    Task::batch(
334                        messages
335                            .into_iter()
336                            .map(|message| program.update(&mut self.state, message)),
337                    )
338                });
339
340                self.resubscribe(program);
341                self.wait_for(task);
342            }
343            Instruction::Expect(expectation) => match expectation {
344                instruction::Expectation::Text(text) => {
345                    use widget::Operation;
346
347                    let mut operation = Selector::find(text.as_str());
348
349                    user_interface.operate(
350                        &self.renderer,
351                        &mut widget::operation::black_box(&mut operation),
352                    );
353
354                    match operation.finish() {
355                        widget::operation::Outcome::Some(Some(_text)) => {
356                            self.runtime.send(Event::Ready);
357                        }
358                        _ => {
359                            self.runtime.send(Event::Failed(instruction));
360                        }
361                    }
362
363                    self.cache = Some(user_interface.into_cache());
364                }
365            },
366        }
367    }
368
369    fn wait_for(&mut self, task: Task<P::Message>) {
370        if let Some(stream) = task::into_stream(task) {
371            match self.mode {
372                Mode::Zen => {
373                    self.pending_tasks += 1;
374
375                    self.runtime.run(
376                        stream
377                            .map(Action_::Runtime)
378                            .map(Action)
379                            .map(Event::Action)
380                            .chain(stream::once(async {
381                                Event::Action(Action(Action_::CountDown))
382                            }))
383                            .boxed(),
384                    );
385                }
386                Mode::Patient => {
387                    self.runtime.run(
388                        stream
389                            .map(Action_::Runtime)
390                            .map(Action)
391                            .map(Event::Action)
392                            .chain(stream::once(async { Event::Ready }))
393                            .boxed(),
394                    );
395                }
396                Mode::Immediate => {
397                    self.runtime.run(
398                        stream
399                            .map(Action_::Runtime)
400                            .map(Action)
401                            .map(Event::Action)
402                            .boxed(),
403                    );
404                    self.runtime.send(Event::Ready);
405                }
406            }
407        } else if self.pending_tasks == 0 {
408            self.runtime.send(Event::Ready);
409        }
410    }
411
412    fn resubscribe(&mut self, program: &P) {
413        self.runtime
414            .track(subscription::into_recipes(self.runtime.enter(|| {
415                program.subscription(&self.state).map(|message| {
416                    Event::Action(Action(Action_::Runtime(runtime::Action::Output(message))))
417                })
418            })));
419    }
420
421    /// Returns the current view of the [`Emulator`].
422    pub fn view(&self, program: &P) -> Element<'_, P::Message, P::Theme, P::Renderer> {
423        program.view(&self.state, self.window)
424    }
425
426    /// Returns the current theme of the [`Emulator`].
427    pub fn theme(&self, program: &P) -> Option<P::Theme> {
428        program.theme(&self.state, self.window)
429    }
430
431    /// Takes a [`window::Screenshot`] of the current state of the [`Emulator`].
432    pub fn screenshot(
433        &mut self,
434        program: &P,
435        theme: &P::Theme,
436        scale_factor: f32,
437    ) -> window::Screenshot {
438        use core::renderer::Headless;
439
440        let style = program.style(&self.state, theme);
441
442        let mut user_interface = UserInterface::build(
443            program.view(&self.state, self.window),
444            self.size,
445            self.cache.take().unwrap(),
446            &mut self.renderer,
447        );
448
449        // TODO: Nested redraws!
450        let _ = user_interface.update(
451            &[core::Event::Window(window::Event::RedrawRequested(
452                Instant::now(),
453            ))],
454            mouse::Cursor::Unavailable,
455            &mut self.renderer,
456            &mut self.clipboard,
457            &mut Vec::new(),
458        );
459
460        user_interface.draw(
461            &mut self.renderer,
462            theme,
463            &renderer::Style {
464                text_color: style.text_color,
465            },
466            mouse::Cursor::Unavailable,
467        );
468
469        let physical_size = Size::new(
470            (self.size.width * scale_factor).round() as u32,
471            (self.size.height * scale_factor).round() as u32,
472        );
473
474        let rgba = self
475            .renderer
476            .screenshot(physical_size, scale_factor, style.background_color);
477
478        window::Screenshot {
479            rgba: Bytes::from(rgba),
480            size: physical_size,
481            scale_factor,
482        }
483    }
484
485    /// Turns the [`Emulator`] into its internal state.
486    pub fn into_state(self) -> (P::State, core::window::Id) {
487        (self.state, self.window)
488    }
489}
490
491/// The strategy used by an [`Emulator`] when waiting for tasks to finish.
492///
493/// A [`Mode`] can be used to make an [`Emulator`] wait for side effects to finish before
494/// continuing execution.
495#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
496pub enum Mode {
497    /// Waits for all tasks spawned by an [`Instruction`], as well as all tasks indirectly
498    /// spawned by the the results of those tasks.
499    ///
500    /// This is the default.
501    #[default]
502    Zen,
503    /// Waits only for the tasks directly spawned by an [`Instruction`].
504    Patient,
505    /// Never waits for any tasks to finish.
506    Immediate,
507}
508
509impl Mode {
510    /// A list of all the available modes.
511    pub const ALL: &[Self] = &[Self::Zen, Self::Patient, Self::Immediate];
512}
513
514impl fmt::Display for Mode {
515    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
516        f.write_str(match self {
517            Self::Zen => "Zen",
518            Self::Patient => "Patient",
519            Self::Immediate => "Immediate",
520        })
521    }
522}
523
524struct Clipboard {
525    content: Option<String>,
526}
527
528impl core::Clipboard for Clipboard {
529    fn read(&self, _kind: core::clipboard::Kind) -> Option<String> {
530        self.content.clone()
531    }
532
533    fn write(&mut self, _kind: core::clipboard::Kind, contents: String) {
534        self.content = Some(contents);
535    }
536}