slides/
lib.rs

1#[macro_use]
2extern crate yew;
3#[macro_use]
4extern crate stdweb;
5
6use yew::prelude::*;
7use yew::services::console::ConsoleService;
8use stdweb::Value;
9use stdweb::unstable::TryInto;
10use stdweb::web::window;
11use yew::services::timeout::TimeoutService;
12
13static PREFIX: &str = "#slide-";
14
15/// Data format for storing user-defined component state
16#[derive(Debug)]
17pub enum CustomData {
18    Number(u64),
19    String(String),
20    StringRef(&'static str),
21    Unit,
22}
23
24/// User defined component slide
25#[derive(Clone)]
26pub struct Custom {
27    pub title: String,
28    pub init: Box<&'static Fn() -> CustomData>,
29    pub update: Box<&'static Fn(&mut CustomData, CustomData, &mut Env<Registry, RootModel>) -> bool>,
30    pub render: Box<&'static Fn(&CustomData) -> Html<Registry, RootModel>>,
31}
32
33/// Represents a single slide.
34///
35/// Available slide types:
36/// - Title: displays a string with big font
37/// - Image: displays an image and a caption string
38/// - List: a list of items with a title
39/// - Code: a code snipped with a title
40/// - Custom: a custom component with user-defined logic
41#[derive(Clone)]
42pub enum Slide {
43    Title(String),
44    Image(&'static str, Option<String>, String),
45    List(String, Vec<String>),
46    Code(String, String),
47    Custom(Custom),
48}
49
50impl Slide {
51    /// short-hand function for creating a title slide
52    pub fn title(title: &str) -> Slide {
53        Slide::Title(String::from(title))
54    }
55
56    /// short-hand function for creating a image slide
57    pub fn image(resource: &'static str, text: &str) -> Slide {
58        Slide::Image(resource, None, String::from(text))
59    }
60
61    /// short-hand function for creating a image slide
62    pub fn image_with_title(resource: &'static str, title: &str, text: &str) -> Slide {
63        Slide::Image(resource, Some(String::from(title)), String::from(text))
64    }
65
66    /// short-hand function for creating a list slide
67    pub fn list(title: &str, list: &[&'static str]) -> Slide {
68        let items = list.iter().map(|s| String::from(*s)).collect();
69        Slide::List(String::from(title), items)
70    }
71
72    /// short-hand function for creating a list slide
73    pub fn code(title: &str, code: &str) -> Slide {
74        Slide::Code(String::from(title), String::from(code))
75    }
76
77    /// short-hand function for creating a list slide
78    pub fn custom(title: &str, init: &'static Fn() -> CustomData, update: &'static Fn(&mut CustomData, CustomData, &mut Env<Registry, RootModel>) -> bool, render: &'static Fn(&CustomData) -> Html<Registry, RootModel>) -> Slide {
79        Slide::Custom(
80            Custom {
81                title: String::from(title),
82                init: Box::new(init),
83                update: Box::new(update),
84                render: Box::new(render),
85            }
86        )
87    }
88}
89
90/// Represents a story (list of slides).
91pub struct Story {
92    pub slides: Vec<Slide>,
93}
94
95pub struct Registry {
96    pub console: ConsoleService,
97    story: Option<Story>,
98    pub timeout: TimeoutService,
99}
100
101pub struct RootModel {
102    story: Story,
103    current_slide: usize,
104    current_hash: String,
105    #[allow(dead_code)]
106    handle: Value,
107    custom_data: CustomData,
108}
109
110pub enum RootMessage {
111    Keydown(u32),
112    Custom(CustomData),
113}
114
115impl Component<Registry> for RootModel {
116    type Message = RootMessage;
117    type Properties = ();
118
119    fn create(_props: Self::Properties, context: &mut Env<Registry, Self>) -> Self {
120        let callback = context.send_back(|code: u32| RootMessage::Keydown(code));
121        let js_callback = move |v: Value| { callback.emit(v.try_into().unwrap()) };
122        let handle = js! {
123          var cb = @{js_callback};
124          return document.addEventListener("keypress", function (e) {
125            console.log(e.keyCode);
126            cb(e.keyCode);
127          })
128        };
129        let story = context.story.take().unwrap_or_else(|| Story { slides: vec!() });
130        let current_slide = RootModel::get_location_slide().unwrap_or(0);
131        let current_hash = RootModel::get_slide_hash(current_slide);
132        RootModel::set_location_hash(&current_hash);
133        let custom_data = match &story.slides[current_slide] {
134            Slide::Custom(slide) => (*slide.init)(),
135            _ => CustomData::Unit,
136        };
137        RootModel {
138            story,
139            current_slide,
140            current_hash,
141            handle,
142            custom_data,
143        }
144    }
145
146    fn update(&mut self, msg: Self::Message, context: &mut Env<Registry, Self>) -> bool {
147        let slides_count = self.story.slides.len();
148        match msg {
149            RootMessage::Keydown(46) => {
150                self.current_slide = (slides_count + self.current_slide + 1).min(slides_count + slides_count - 1) % slides_count;
151                self.current_hash = RootModel::get_slide_hash(self.current_slide);
152                RootModel::set_location_hash(&self.current_hash);
153                let custom_data = match &self.story.slides[self.current_slide] {
154                    Slide::Custom(slide) => (*slide.init)(),
155                    _ => CustomData::Unit,
156                };
157                self.custom_data = custom_data;
158                true
159            }
160            RootMessage::Keydown(44) => {
161                self.current_slide = (slides_count + self.current_slide - 1).max(slides_count) % slides_count;
162                self.current_hash = RootModel::get_slide_hash(self.current_slide);
163                RootModel::set_location_hash(&self.current_hash);
164                let custom_data = match &self.story.slides[self.current_slide] {
165                    Slide::Custom(slide) => (*slide.init)(),
166                    _ => CustomData::Unit,
167                };
168                self.custom_data = custom_data;
169                true
170            }
171            RootMessage::Keydown(code) => {
172                context.console.log(&format!("Unhandled key {}", code));
173                false
174            }
175            RootMessage::Custom(data) => {
176                match &self.story.slides[self.current_slide] {
177                    Slide::Custom(slide) => {
178                        (*slide.update)(&mut self.custom_data, data, context)
179                    }
180                    _ => {
181                        false
182                    }
183                }
184            }
185        }
186    }
187}
188
189
190impl RootModel {
191    fn list_item_view(&self, string: &String) -> Html<Registry, RootModel> {
192        html! {
193          <li> { string } </li>
194        }
195    }
196
197    fn title_view(&self, string: &String) -> Html<Registry, RootModel> {
198        html! {
199          <h2> { string } </h2>
200        }
201    }
202}
203
204impl Renderable<Registry, RootModel> for RootModel {
205    fn view(&self) -> Html<Registry, RootModel> {
206        let current_slide = &self.story.slides[self.current_slide];
207        match (self.story.slides.len(), current_slide) {
208            (0, _) => {
209                html! {
210                <div class="slide-wrapper",>
211                  <div class="slide",class="empty",>
212                    { "Nothing to display" }
213                  </div>
214                </div>
215                }
216            }
217            (_, Slide::Title(string)) => {
218                html! {
219                <div class="slide-wrapper",>
220                  <div class="slide",class="text",>
221                    <div class="content",>
222                      <h2>
223                        { string }
224                      </h2>
225                    </div>
226                  </div>
227                </div>
228                }
229            }
230            (_, Slide::Image(resource, None, text)) => {
231                html! {
232                <div class="slide-wrapper",>
233                  <div class="slide",class="image",>
234                    <div class="content",>
235                      <img src=resource, />
236                      <p>
237                        <div>
238                          { text }
239                        </div>
240                      </p>
241                    </div>
242                  </div>
243                </div>
244                }
245            }
246            (_, Slide::Image(resource, Some(title), text)) => {
247                html! {
248                <div class="slide-wrapper",>
249                  <div class="slide",class="image",>
250                    <div class="content",>
251                      <p>
252                        { self.title_view(title) }
253                      </p>
254                      <img src=resource, />
255                      <p>
256                        <div>
257                          { text }
258                        </div>
259                      </p>
260                    </div>
261                  </div>
262                </div>
263                }
264            }
265            (_, Slide::List(title, list)) => {
266                html! {
267                <div class="slide-wrapper",>
268                  <div class="slide",class="list",>
269                    <div class="content",>
270                        { self.title_view(title) }
271                        <ul> { for list.iter().map(|i| self.list_item_view(i)) } </ul>
272                    </div>
273                  </div>
274                </div>
275                }
276            }
277            (_, Slide::Code(title, code)) => {
278                html! {
279                <div class="slide-wrapper",>
280                  <div class="slide",class="code",>
281                    <div class="content",>
282                      { self.title_view(title) }
283                      <pre><code> { code } </code></pre>
284                    </div>
285                  </div>
286                </div>
287                }
288            }
289            (_, Slide::Custom(Custom { render, title, .. })) => {
290                html! {
291                <div class="slide-wrapper",>
292                  <div class="slide",class="code",>
293                    <div class="content",>
294                      { self.title_view(title) }
295                      { (*render)(&self.custom_data) }
296                    </div>
297                  </div>
298                </div>
299                }
300            }
301        }
302    }
303}
304
305/// Run slides engine with provided story.
306pub fn run(story: Story) {
307    yew::initialize();
308    let registry = Registry {
309        console: ConsoleService::new(),
310        story: Some(story),
311        timeout: TimeoutService::new(),
312    };
313    let app = App::<Registry, RootModel>::new(registry);
314    app.mount_to_body();
315    yew::run_loop();
316}
317
318impl RootModel {
319    fn get_location_slide() -> Option<usize> {
320        window().location()
321            .and_then(|l| l.hash().ok())
322            .filter(|h| h.starts_with(PREFIX))
323            .and_then(|h| h[PREFIX.len()..].parse::<usize>().ok())
324    }
325
326    fn get_slide_hash(slide: usize) -> String {
327        format!("{}{}", PREFIX, slide)
328    }
329
330    fn set_location_hash(hash: &str) {
331        js! {
332          window.location.hash = @{hash};
333        }
334    }
335}