Skip to main content

demo_common/
scene.rs

1//! The `Demo` trait and its type-erased `Scene` form.
2//!
3//! A demo crate implements [`Demo`]; the gallery runs several of them side by
4//! side in one `iced::daemon`, which needs a single message type. [`erase`]
5//! boxes a demo behind [`Scene`] and wraps its messages in [`SceneMessage`],
6//! so the runtime never names the concrete demo.
7//!
8//! ```no_run
9//! use demo_common::{Demo, Scene, SceneDef, SceneMessage, erase};
10//! use iced::{Element, Task, Theme, widget::text};
11//!
12//! #[derive(Default)]
13//! struct App;
14//!
15//! #[derive(Clone, Debug)]
16//! enum Message {
17//!     Tick,
18//! }
19//!
20//! impl Demo for App {
21//!     type Message = Message;
22//!
23//!     fn boot() -> (Self, Task<Message>) {
24//!         (Self, Task::none())
25//!     }
26//!
27//!     fn update(&mut self, _message: Message) -> Task<Message> {
28//!         Task::none()
29//!     }
30//!
31//!     fn view(&self) -> Element<'_, Message> {
32//!         text("hello").into()
33//!     }
34//!
35//!     fn theme(&self) -> Theme {
36//!         Theme::CatppuccinFrappe
37//!     }
38//!
39//!     fn set_theme(&mut self, _theme: Theme) {}
40//! }
41//!
42//! pub fn scene() -> (Box<dyn Scene>, Task<SceneMessage>) {
43//!     erase::<App>()
44//! }
45//!
46//! static SCENES: &[SceneDef] = &[SceneDef {
47//!     name: "example",
48//!     boot: scene,
49//! }];
50//! ```
51
52use std::any::Any;
53use std::fmt;
54
55use iced::{Element, Subscription, Task, Theme};
56
57/// One demo application: the five methods `iced::application` takes, on a type.
58///
59/// The trait exists so the gallery can boot any demo through one signature.
60/// Native `main` functions keep calling `iced::application` with the same
61/// methods directly.
62pub trait Demo: Sized + 'static {
63    type Message: Any + Clone + Send + fmt::Debug;
64
65    /// Builds the pristine scene the documentation shows.
66    ///
67    /// Never loads persisted state: an embedded demo must look the same for
68    /// every visitor, and the screenshot must match what the canvas renders.
69    fn boot() -> (Self, Task<Self::Message>);
70
71    fn update(&mut self, message: Self::Message) -> Task<Self::Message>;
72
73    fn view(&self) -> Element<'_, Self::Message>;
74
75    fn theme(&self) -> Theme;
76
77    /// Switches the demo onto `theme`, the way an embed follows the page
78    /// theme of the site it sits in; the native binaries never call it.
79    fn set_theme(&mut self, theme: Theme);
80
81    fn subscription(&self) -> Subscription<Self::Message> {
82        Subscription::none()
83    }
84}
85
86/// A demo message with its type erased, so scenes of different demos share one
87/// runtime.
88pub struct SceneMessage(Box<dyn ErasedMessage>);
89
90trait ErasedMessage: Any + Send {
91    fn clone_box(&self) -> Box<dyn ErasedMessage>;
92    fn into_any(self: Box<Self>) -> Box<dyn Any>;
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result;
94}
95
96impl<M: Any + Clone + Send + fmt::Debug> ErasedMessage for M {
97    fn clone_box(&self) -> Box<dyn ErasedMessage> {
98        Box::new(self.clone())
99    }
100
101    fn into_any(self: Box<Self>) -> Box<dyn Any> {
102        self
103    }
104
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        fmt::Debug::fmt(self, f)
107    }
108}
109
110impl SceneMessage {
111    pub fn new<M: Any + Clone + Send + fmt::Debug>(message: M) -> Self {
112        Self(Box::new(message))
113    }
114
115    /// Recovers the concrete message.
116    ///
117    /// # Errors
118    ///
119    /// Returns `Err(self)` when the payload is another demo's message.
120    pub fn downcast<M: Any>(self) -> Result<M, Self> {
121        if (*self.0).type_id() != std::any::TypeId::of::<M>() {
122            return Err(self);
123        }
124
125        match self.0.into_any().downcast::<M>() {
126            Ok(message) => Ok(*message),
127            Err(_) => unreachable!("type id was checked above"),
128        }
129    }
130}
131
132impl Clone for SceneMessage {
133    fn clone(&self) -> Self {
134        Self(self.0.clone_box())
135    }
136}
137
138impl fmt::Debug for SceneMessage {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        self.0.fmt(f)
141    }
142}
143
144/// Object-safe view of a running demo.
145pub trait Scene {
146    fn update(&mut self, message: SceneMessage) -> Task<SceneMessage>;
147    fn view(&self) -> Element<'_, SceneMessage>;
148    fn theme(&self) -> Theme;
149    fn set_theme(&mut self, theme: Theme);
150    fn subscription(&self) -> Subscription<SceneMessage>;
151}
152
153struct Erased<A>(A);
154
155impl<A: Demo> Scene for Erased<A> {
156    fn update(&mut self, message: SceneMessage) -> Task<SceneMessage> {
157        match message.downcast::<A::Message>() {
158            Ok(message) => self.0.update(message).map(SceneMessage::new::<A::Message>),
159            Err(_) => Task::none(),
160        }
161    }
162
163    fn view(&self) -> Element<'_, SceneMessage> {
164        self.0.view().map(SceneMessage::new::<A::Message>)
165    }
166
167    fn theme(&self) -> Theme {
168        self.0.theme()
169    }
170
171    fn set_theme(&mut self, theme: Theme) {
172        self.0.set_theme(theme);
173    }
174
175    fn subscription(&self) -> Subscription<SceneMessage> {
176        // `Subscription::map` is compile-time checked to be zero-sized: pass
177        // the fn item, never a closure.
178        self.0.subscription().map(SceneMessage::new::<A::Message>)
179    }
180}
181
182/// Boots a demo behind the object-safe [`Scene`] interface.
183pub fn erase<A: Demo>() -> (Box<dyn Scene>, Task<SceneMessage>) {
184    let (app, task) = A::boot();
185
186    (
187        Box::new(Erased(app)),
188        task.map(SceneMessage::new::<A::Message>),
189    )
190}
191
192/// A demo the gallery can open by name.
193pub struct SceneDef {
194    /// Stable name used by `data-scene`, the PNG file and the JS API.
195    pub name: &'static str,
196    pub boot: fn() -> (Box<dyn Scene>, Task<SceneMessage>),
197}