Skip to main content

nifty_runtime/reactive/
mod.rs

1//! Shows an app's windows from templates and keeps them in step with its state.
2//!
3//! Each dynamic part of a template records which state it reads. After every event the
4//! engine updates only the widgets whose state changed, and rebuilds regions in place when
5//! the state deciding their contents changed. The [`Logic`] gives everything its values,
6//! so the interpreter and compiled apps share this engine.
7
8mod template;
9mod windows;
10
11use std::cell::RefCell;
12use std::fmt;
13use std::rc::{Rc, Weak};
14
15pub use template::{
16    AppTemplate, Logic, MenuBar, Shown, Template, Templates, Widget, WindowAction, WindowRequest,
17    WindowTemplate,
18};
19
20use crate::backend::{
21    Backend, ClickHandler, DropHandler, MenuHandler, NumberHandler, TextHandler, ToggleHandler,
22};
23use crate::menu::{Menu, MenuEntry};
24use crate::state::StateSet;
25use crate::ui::{Node, Position, Ui};
26use windows::{MountedWindow, ReusedWindow};
27
28pub type ErrorHandler<E> = Rc<dyn Fn(E)>;
29
30/// A running app: its logic, its windows, and what each widget shows.
31pub struct Engine<L: Logic, B: Backend> {
32    logic: L,
33    ui: Ui<B>,
34    /// The app's windows, which code refers to by their position.
35    templates: Vec<WindowTemplate<L>>,
36    /// What each window shows, by its template's position; `None` while it is closed.
37    windows: RefCell<Vec<Option<MountedWindow<L>>>>,
38    on_error: ErrorHandler<L::Error>,
39    this: Weak<Self>,
40}
41
42/// A template and the list holding it, so mounted UI can re-read it later.
43struct TemplateRef<L: Logic> {
44    templates: Templates<L>,
45    index: usize,
46}
47
48impl<L: Logic> TemplateRef<L> {
49    fn template(&self) -> &Template<L> {
50        &self.templates[self.index]
51    }
52}
53
54impl<L: Logic> Clone for TemplateRef<L> {
55    fn clone(&self) -> Self {
56        Self {
57            templates: Rc::clone(&self.templates),
58            index: self.index,
59        }
60    }
61}
62
63enum Mounted<L: Logic> {
64    /// A widget; `live` is kept when what it shows can change.
65    Widget {
66        node: Node,
67        live: Option<(TemplateRef<L>, Rc<L::Frame>)>,
68    },
69    Stack {
70        node: Node,
71        children: Vec<Mounted<L>>,
72    },
73    /// What a region shows, placed directly among its parent's children.
74    Region {
75        template: TemplateRef<L>,
76        frame: Rc<L::Frame>,
77        children: Vec<Mounted<L>>,
78    },
79}
80
81impl<L: Logic, B: Backend> Engine<L, B> {
82    /// Shows the windows of `app` that start open, with `make_logic` giving their dynamic
83    /// parts values. It receives the UI and the engine-to-be, for logic that wakes the app
84    /// later.
85    ///
86    /// # Errors
87    /// When building the first UI fails.
88    pub fn start(
89        ui: Ui<B>,
90        app: AppTemplate<L>,
91        on_error: ErrorHandler<L::Error>,
92        make_logic: impl FnOnce(&Ui<B>, Weak<Self>) -> L,
93    ) -> Result<Rc<Self>, L::Error> {
94        Self::open(ui, app, on_error, make_logic, &[])
95    }
96
97    /// Shows `app`'s windows and menus in place of this app's. A window open now stays
98    /// open with its new content, one closed now stays closed, and windows this app
99    /// didn't have open if they start open; windows it has beyond `app`'s close.
100    ///
101    /// # Errors
102    /// When building the new UI fails. Nothing has changed then, and this app keeps running.
103    pub fn reload(
104        &self,
105        app: AppTemplate<L>,
106        make_logic: impl FnOnce(&Ui<B>, Weak<Self>) -> L,
107    ) -> Result<Rc<Self>, L::Error> {
108        let reused: Vec<Option<ReusedWindow>> = self
109            .windows
110            .borrow()
111            .iter()
112            .map(|mounted| mounted.as_ref().map(ReusedWindow::of))
113            .collect();
114        let next = Self::open(
115            self.ui.clone(),
116            app,
117            Rc::clone(&self.on_error),
118            make_logic,
119            &reused,
120        )?;
121
122        // The new app has replaced this one's content, so this one must not touch it again.
123        self.windows.borrow_mut().clear();
124        let kept = next.templates.len();
125        for surplus in reused.iter().skip(kept).flatten() {
126            self.ui.close(surplus.window());
127        }
128        Ok(next)
129    }
130
131    #[must_use]
132    pub fn logic(&self) -> &L {
133        &self.logic
134    }
135
136    /// Runs one event, then updates the UI; errors go to the app's error handler.
137    pub fn handle_event(&self, event: impl FnOnce(&L) -> Result<(), L::Error>) {
138        if let Err(error) = event(&self.logic).and_then(|()| self.refresh()) {
139            (self.on_error)(error);
140        }
141    }
142
143    /// Runs the event loop until the app quits.
144    pub fn run(&self) {
145        self.ui.run();
146    }
147
148    /// Builds the content of every window to open, reusing `reused` by position, and
149    /// shows it all only once everything is built, so a failure leaves the windows as
150    /// they were. The menus change with them.
151    fn open(
152        ui: Ui<B>,
153        AppTemplate { windows, menu_bar }: AppTemplate<L>,
154        on_error: ErrorHandler<L::Error>,
155        make_logic: impl FnOnce(&Ui<B>, Weak<Self>) -> L,
156        reused: &[Option<ReusedWindow>],
157    ) -> Result<Rc<Self>, L::Error> {
158        let engine = Rc::new_cyclic(|this: &Weak<Self>| Self {
159            logic: make_logic(&ui, this.clone()),
160            ui,
161            windows: RefCell::new(windows.iter().map(|_| None).collect()),
162            templates: windows,
163            on_error,
164            this: this.clone(),
165        });
166
167        let mut prepared = Vec::new();
168        for (index, opens) in engine.windows_to_open(reused).into_iter().enumerate() {
169            if !opens {
170                continue;
171            }
172            let reused = reused.get(index).copied().flatten();
173            match engine.prepare_window(index, reused) {
174                Ok(window) => prepared.push(window),
175                Err(error) => {
176                    engine.discard(prepared);
177                    return Err(error);
178                }
179            }
180        }
181
182        // Menus come before windows show, since a window's menu bar can change its size.
183        engine.show_menus(menu_bar);
184        for window in prepared {
185            engine.show_prepared(window);
186        }
187        engine.ui.set_close_handler(engine.on_close());
188        engine.ui.flush();
189        Ok(engine)
190    }
191
192    /// Gives the app `menu_bar`'s menus, whose items run their actions as events.
193    fn show_menus(&self, menu_bar: MenuBar<L>) {
194        let mut actions = Vec::new();
195        let menus: Vec<Menu<usize>> = menu_bar
196            .menus
197            .into_iter()
198            .map(|menu| menu.number(&mut actions))
199            .collect();
200        let on_select = self.on_menu(actions, Rc::new(menu_bar.frame));
201        self.ui.set_menu_bar(&menus, on_select);
202    }
203
204    /// Runs the numbered item's action, with `frame` as its locals, as one event.
205    fn on_menu(&self, actions: Vec<L::Action>, frame: Rc<L::Frame>) -> MenuHandler {
206        let engine = self.this.clone();
207        Box::new(move |item| send(&engine, |logic| logic.run(&actions[item], &frame)))
208    }
209
210    fn mount_list(
211        &self,
212        templates: &Templates<L>,
213        frame: &Rc<L::Frame>,
214        parent: Node,
215        offset: &mut usize,
216    ) -> Result<Vec<Mounted<L>>, L::Error> {
217        (0..templates.len())
218            .map(|index| {
219                let template = TemplateRef {
220                    templates: Rc::clone(templates),
221                    index,
222                };
223                self.mount(template, frame, parent, offset)
224            })
225            .collect()
226    }
227
228    /// Creates the UI for `template` at position `offset` in `parent`, advancing `offset`
229    /// past what it placed there.
230    fn mount(
231        &self,
232        template: TemplateRef<L>,
233        frame: &Rc<L::Frame>,
234        parent: Node,
235        offset: &mut usize,
236    ) -> Result<Mounted<L>, L::Error> {
237        match template.template() {
238            Template::Stack {
239                style,
240                children,
241                scrolls,
242            } => {
243                let at = next(parent, offset);
244                let node = if *scrolls {
245                    self.ui.scroll(at, style)
246                } else {
247                    self.ui.stack(at, style)
248                };
249                let children = self.mount_list(children, frame, node, &mut 0)?;
250                Ok(Mounted::Stack { node, children })
251            }
252            Template::Region { region, .. } => {
253                let children = self.mount_region(region, frame, parent, offset)?;
254                Ok(Mounted::Region {
255                    template,
256                    frame: Rc::clone(frame),
257                    children,
258                })
259            }
260            Template::Widget { widget, deps } => {
261                let node = self.create_widget(next(parent, offset), widget, frame)?;
262                let live = (!deps.is_empty()).then(|| (template, Rc::clone(frame)));
263                Ok(Mounted::Widget { node, live })
264            }
265        }
266    }
267
268    fn create_widget(
269        &self,
270        at: Position,
271        widget: &Widget<L>,
272        frame: &Rc<L::Frame>,
273    ) -> Result<Node, L::Error> {
274        Ok(match widget {
275            Widget::Label { text, style } => {
276                self.ui.label(at, &self.logic.text(text, frame)?, *style)
277            }
278            Widget::Button {
279                title,
280                style,
281                icon,
282                selected,
283                action,
284            } => {
285                let title = self.logic.text(title, frame)?;
286                let selected = selected
287                    .as_ref()
288                    .map(|selected| self.logic.flag(selected, frame))
289                    .transpose()?;
290                let on_click = self.on_click(action.clone(), Rc::clone(frame));
291                self.ui
292                    .button(at, &title, (*style, *icon), selected, on_click)
293            }
294            Widget::TextField {
295                value,
296                placeholder,
297                lines,
298                action,
299            } => {
300                let text = self.logic.read_text(value, frame)?;
301                let on_change = self.on_edit(value.clone(), Rc::clone(frame));
302                let on_submit = action
303                    .as_ref()
304                    .map(|action| self.on_click(action.clone(), Rc::clone(frame)));
305                self.ui
306                    .text_field(at, &text, (placeholder, *lines), on_change, on_submit)
307            }
308            Widget::Checkbox {
309                checked,
310                title,
311                style,
312            } => {
313                let is_checked = self.logic.read_checked(checked, frame)?;
314                let title = match title {
315                    Some(title) => self.logic.text(title, frame)?,
316                    None => String::new(),
317                };
318                let on_toggle = self.on_toggle(checked.clone(), Rc::clone(frame));
319                self.ui.checkbox(at, &title, is_checked, *style, on_toggle)
320            }
321            Widget::Slider { value, min, max } => {
322                let current = self.logic.read_number(value, frame)?;
323                let on_change = self.on_number(value.clone(), Rc::clone(frame));
324                self.ui.slider(at, current, (*min, *max), on_change)
325            }
326            Widget::Picker { selection, options } => {
327                let options = self.logic.texts(options, frame)?;
328                let selected = self.logic.read_text(selection, frame)?;
329                let on_pick = self.on_edit(selection.clone(), Rc::clone(frame));
330                self.ui.picker(at, &options, &selected, on_pick)
331            }
332            Widget::NavItem {
333                title,
334                value,
335                selection,
336                icon,
337                badge,
338            } => {
339                let title = self.logic.text(title, frame)?;
340                let badge = self.badge(badge.as_ref(), frame)?;
341                let selected = self.is_selected(value, selection, frame)?;
342                let on_click = self.on_select(selection.clone(), value.clone(), Rc::clone(frame));
343                self.ui
344                    .nav_item(at, &title, *icon, &badge, selected, on_click)
345            }
346            Widget::Image { path, size } => {
347                self.ui.image(at, &self.logic.text(path, frame)?, *size)
348            }
349            Widget::ContextMenu { entries } => {
350                let mut actions = Vec::new();
351                let entries = MenuEntry::number_all(entries.clone(), &mut actions);
352                let on_select = self.on_menu(actions, Rc::clone(frame));
353                self.ui.context_menu(at, &entries, on_select)
354            }
355            Widget::DropTarget { dropped, action } => {
356                let on_drop = self.on_drop(dropped.clone(), action.clone(), Rc::clone(frame));
357                self.ui.drop_target(at, on_drop)
358            }
359            Widget::Progress { value } => {
360                let percent = value
361                    .as_ref()
362                    .map(|value| self.logic.number(value, frame))
363                    .transpose()?;
364                self.ui.progress(at, percent)
365            }
366            Widget::Spacer => self.ui.spacer(at),
367            Widget::Divider => self.ui.divider(at),
368        })
369    }
370
371    /// Creates what a region currently shows.
372    fn mount_region(
373        &self,
374        region: &L::Region,
375        frame: &Rc<L::Frame>,
376        parent: Node,
377        offset: &mut usize,
378    ) -> Result<Vec<Mounted<L>>, L::Error> {
379        let mut children = Vec::new();
380        for shown in self.logic.expand(region, frame)? {
381            children.extend(self.mount_list(&shown.templates, &shown.frame, parent, offset)?);
382        }
383        Ok(children)
384    }
385
386    /// Brings every window up to date with the state changed by the last event, then
387    /// opens and closes the windows it asked for.
388    fn refresh(&self) -> Result<(), L::Error> {
389        let changed = self.logic.take_changed();
390        if !changed.is_empty() {
391            let mut windows = self.windows.borrow_mut();
392            for mounted in windows.iter_mut().flatten() {
393                self.refresh_list(&mut mounted.children, mounted.content, &changed, &mut 0)?;
394            }
395        }
396        self.apply_window_requests(&self.logic.take_window_requests())?;
397        self.ui.flush();
398        Ok(())
399    }
400
401    fn refresh_list(
402        &self,
403        items: &mut [Mounted<L>],
404        parent: Node,
405        changed: &StateSet,
406        offset: &mut usize,
407    ) -> Result<(), L::Error> {
408        for item in items {
409            match item {
410                Mounted::Widget { node, live } => {
411                    if let Some((template, frame)) = live
412                        && let Template::Widget { widget, deps } = template.template()
413                        && deps.intersects(changed)
414                    {
415                        self.update_widget(*node, widget, frame)?;
416                    }
417                    *offset += 1;
418                }
419                Mounted::Stack { node, children } => {
420                    self.refresh_list(children, *node, changed, &mut 0)?;
421                    *offset += 1;
422                }
423                Mounted::Region {
424                    template,
425                    frame,
426                    children,
427                } => match template.template() {
428                    Template::Region { region, deps } if deps.intersects(changed) => {
429                        for child in children.drain(..) {
430                            self.unmount(child);
431                        }
432                        *children = self.mount_region(region, frame, parent, offset)?;
433                    }
434                    _ => self.refresh_list(children, parent, changed, offset)?,
435                },
436            }
437        }
438        Ok(())
439    }
440
441    fn unmount(&self, mounted: Mounted<L>) {
442        match mounted {
443            Mounted::Widget { node, .. } | Mounted::Stack { node, .. } => self.ui.remove(node),
444            Mounted::Region { children, .. } => {
445                for child in children {
446                    self.unmount(child);
447                }
448            }
449        }
450    }
451
452    fn update_widget(
453        &self,
454        node: Node,
455        widget: &Widget<L>,
456        frame: &L::Frame,
457    ) -> Result<(), L::Error> {
458        match widget {
459            Widget::Label { text, .. } => {
460                self.ui.set_text(node, &self.logic.text(text, frame)?);
461            }
462            Widget::Button {
463                title, selected, ..
464            } => {
465                self.ui.set_text(node, &self.logic.text(title, frame)?);
466                if let Some(selected) = selected {
467                    self.ui.set_checked(node, self.logic.flag(selected, frame)?);
468                }
469            }
470            Widget::TextField { value, .. } => {
471                self.ui.set_text(node, &self.logic.read_text(value, frame)?);
472            }
473            Widget::Checkbox { checked, title, .. } => {
474                self.ui
475                    .set_checked(node, self.logic.read_checked(checked, frame)?);
476                if let Some(title) = title {
477                    self.ui.set_text(node, &self.logic.text(title, frame)?);
478                }
479            }
480            Widget::Slider { value, .. } => {
481                self.ui
482                    .set_value(node, self.logic.read_number(value, frame)?);
483            }
484            Widget::Picker { selection, options } => {
485                self.ui
486                    .set_options(node, &self.logic.texts(options, frame)?);
487                self.ui
488                    .set_text(node, &self.logic.read_text(selection, frame)?);
489            }
490            Widget::NavItem {
491                title,
492                value,
493                selection,
494                badge,
495                ..
496            } => {
497                self.ui.set_text(node, &self.logic.text(title, frame)?);
498                self.ui.set_badge(node, &self.badge(badge.as_ref(), frame)?);
499                self.ui
500                    .set_checked(node, self.is_selected(value, selection, frame)?);
501            }
502            Widget::Image { path, .. } => {
503                self.ui.set_image(node, &self.logic.text(path, frame)?);
504            }
505            Widget::Progress { value } => {
506                if let Some(value) = value {
507                    self.ui.set_progress(node, self.logic.number(value, frame)?);
508                }
509            }
510            Widget::Spacer
511            | Widget::Divider
512            | Widget::ContextMenu { .. }
513            | Widget::DropTarget { .. } => {}
514        }
515        Ok(())
516    }
517
518    fn badge(&self, badge: Option<&L::Value>, frame: &L::Frame) -> Result<String, L::Error> {
519        badge.map_or_else(|| Ok(String::new()), |badge| self.logic.text(badge, frame))
520    }
521
522    fn is_selected(
523        &self,
524        value: &L::Value,
525        selection: &L::Binding,
526        frame: &L::Frame,
527    ) -> Result<bool, L::Error> {
528        Ok(self.logic.read_text(selection, frame)? == self.logic.text(value, frame)?)
529    }
530
531    fn on_click(&self, action: L::Action, frame: Rc<L::Frame>) -> ClickHandler {
532        let engine = self.this.clone();
533        Box::new(move || send(&engine, |logic| logic.run(&action, &frame)))
534    }
535
536    fn on_edit(&self, binding: L::Binding, frame: Rc<L::Frame>) -> TextHandler {
537        let engine = self.this.clone();
538        Box::new(move |text| send(&engine, |logic| logic.write_text(&binding, &frame, text)))
539    }
540
541    /// Writes the item's value as it is when picked, which may differ from when it was shown.
542    fn on_select(
543        &self,
544        selection: L::Binding,
545        value: L::Value,
546        frame: Rc<L::Frame>,
547    ) -> ClickHandler {
548        let engine = self.this.clone();
549        Box::new(move || {
550            send(&engine, |logic| {
551                let value = logic.text(&value, &frame)?;
552                logic.write_text(&selection, &frame, &value)
553            });
554        })
555    }
556
557    fn on_number(&self, binding: L::Binding, frame: Rc<L::Frame>) -> NumberHandler {
558        let engine = self.this.clone();
559        Box::new(move |number| {
560            send(&engine, |logic| {
561                logic.write_number(&binding, &frame, number)
562            });
563        })
564    }
565
566    /// Writes what was dropped to `dropped`, then runs `action`, as one event.
567    fn on_drop(
568        &self,
569        dropped: L::Binding,
570        action: Option<L::Action>,
571        frame: Rc<L::Frame>,
572    ) -> DropHandler {
573        let engine = self.this.clone();
574        Box::new(move |items| {
575            send(&engine, |logic| {
576                logic.write_texts(&dropped, &frame, items)?;
577                action
578                    .as_ref()
579                    .map_or(Ok(()), |action| logic.run(action, &frame))
580            });
581        })
582    }
583
584    fn on_toggle(&self, binding: L::Binding, frame: Rc<L::Frame>) -> ToggleHandler {
585        let engine = self.this.clone();
586        Box::new(move |checked| {
587            send(&engine, |logic| {
588                logic.write_checked(&binding, &frame, checked)
589            });
590        })
591    }
592}
593
594/// Runs `event` as one event of `engine`, if the app is still running. Handlers hold
595/// their engine weakly, since the engine owns the widgets that own them.
596fn send<L: Logic, B: Backend>(
597    engine: &Weak<Engine<L, B>>,
598    event: impl FnOnce(&L) -> Result<(), L::Error>,
599) {
600    if let Some(engine) = engine.upgrade() {
601        engine.handle_event(event);
602    }
603}
604
605/// The next position in `parent`, advancing `offset` past it.
606fn next(parent: Node, offset: &mut usize) -> Position {
607    let at = Position {
608        parent,
609        index: *offset,
610    };
611    *offset += 1;
612    at
613}
614
615impl<L: Logic, B: Backend> fmt::Debug for Engine<L, B> {
616    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617        f.debug_struct("Engine")
618            .field("windows", &self.templates.len())
619            .finish_non_exhaustive()
620    }
621}
622
623#[cfg(test)]
624mod tests;