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#[derive(Debug)]
17pub enum CustomData {
18 Number(u64),
19 String(String),
20 StringRef(&'static str),
21 Unit,
22}
23
24#[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#[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 pub fn title(title: &str) -> Slide {
53 Slide::Title(String::from(title))
54 }
55
56 pub fn image(resource: &'static str, text: &str) -> Slide {
58 Slide::Image(resource, None, String::from(text))
59 }
60
61 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 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 pub fn code(title: &str, code: &str) -> Slide {
74 Slide::Code(String::from(title), String::from(code))
75 }
76
77 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
90pub 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(¤t_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
305pub 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}