zi/
app.rs

1//! The application runtime. This is low-level module useful when you are
2//! implementing a backend, but otherwise not meant to be used directly by an
3//! end application.
4
5use smallvec::SmallVec;
6use std::{collections::HashMap, fmt::Debug, time::Instant};
7
8use crate::{
9    component::{
10        bindings::{BindingQuery, DynamicBindings, KeySequenceSlice, NamedBindingQuery},
11        layout::{LaidCanvas, LaidComponent, Layout},
12        template::{ComponentId, DynamicMessage, DynamicProperties, Renderable},
13        LinkMessage, ShouldRender,
14    },
15    terminal::{Canvas, Event, Key, Position, Rect, Size},
16};
17
18pub trait MessageSender: Debug + Send + 'static {
19    fn send(&self, message: ComponentMessage);
20
21    fn clone_box(&self) -> Box<dyn MessageSender>;
22}
23
24#[derive(Debug)]
25pub struct ComponentMessage(pub(crate) LinkMessage);
26
27#[derive(Copy, Clone, Debug, PartialEq)]
28pub enum PollState {
29    Clean,
30    Dirty(Option<Size>),
31    Exit,
32}
33
34#[derive(Debug)]
35struct AppRuntime {
36    screen: Canvas,
37    poll_state: PollState,
38    num_frame: usize,
39}
40
41impl AppRuntime {
42    fn new(size: Size) -> Self {
43        Self {
44            screen: Canvas::new(size),
45            poll_state: PollState::Dirty(None),
46            num_frame: 0,
47        }
48    }
49}
50
51/// The application runtime.
52///
53/// The runtime encapsulates the whole state of the application. It performs
54/// layout calculations and draws the component tree to a canvas. The runtime
55/// owns all the components and manages their lifetime. It creates components
56/// when first mounted and it is responsible for delivering messages, input and
57/// layout events.
58///
59/// The runtime itself does not run an event loop. This is delegated to a
60/// particular backend implementation to allow for maximum flexibility. For
61/// instance, the `crossterm` terminal backend implements an event loop using
62/// tokio channels for component message + mio for stdin input and resize
63/// events.
64///
65/// Note: This is a low-level struct useful when you are implementing a backend
66/// or for testing your application. For an end user application, you would
67/// normally use a backend that wraps an App in an event loop, see the examples.
68pub struct App {
69    root: Layout,
70    components: HashMap<ComponentId, MountedComponent>,
71    layouts: HashMap<ComponentId, Layout>,
72    subscriptions: ComponentSubscriptions,
73    controller: InputController,
74    runtime: AppRuntime,
75    sender: Box<dyn MessageSender>,
76}
77
78impl App {
79    /// Creates a new application runtime
80    ///
81    /// To instantiate a new runtime you need three things
82    ///
83    /// - a [`MessageSender`](trait.MessageSender.html) responsible for delivering
84    ///   messages sent by components using [`ComponentLink`](../struct.ComponentLink.html)
85    /// - the `size` of the initial canvas
86    /// - the root [`Layout`](../struct.Layout.html) that will be rendered
87    ///
88    /// ```no_run
89    /// use std::sync::mpsc;
90    /// use zi::{
91    ///     app::{App, ComponentMessage, MessageSender},
92    ///     components::text::{Text, TextProperties},
93    ///     prelude::*,
94    /// };
95    ///
96    /// #[derive(Clone, Debug)]
97    /// struct MessageQueue(mpsc::Sender<ComponentMessage>);
98    ///
99    /// impl MessageSender for MessageQueue {
100    ///     fn send(&self, message: ComponentMessage) {
101    ///         self.0.send(message).unwrap();
102    ///     }
103    ///
104    ///     fn clone_box(&self) -> Box<dyn MessageSender> {
105    ///         Box::new(self.clone())
106    ///     }
107    /// }
108    ///
109    /// # fn main() {
110    /// let (sender, receiver) = mpsc::channel();
111    /// let message_queue = MessageQueue(sender);
112    /// let mut app = App::new(
113    ///     message_queue,
114    ///     Size::new(10, 10),
115    ///     Text::with(TextProperties::new().content("Hello")),
116    /// );
117    ///
118    /// loop {
119    ///     // Deliver component messages. This would block forever as no component
120    ///     // sends any messages.
121    ///     let message = receiver.recv().unwrap();
122    ///
123    ///     app.handle_message(message);
124    ///     app.handle_resize(Size::new(20, 20));
125    ///
126    ///     // Draw
127    ///     let canvas = app.draw();
128    ///     eprintln!("{}", canvas);
129    /// }
130    /// # }
131    /// ```
132
133    pub fn new(sender: impl MessageSender, size: Size, root: Layout) -> Self {
134        Self {
135            root,
136            components: HashMap::new(),
137            layouts: HashMap::new(),
138            subscriptions: ComponentSubscriptions::new(),
139            controller: InputController::new(),
140            runtime: AppRuntime::new(size),
141            sender: Box::new(sender),
142        }
143    }
144
145    /// Return the application's poll state
146    #[inline]
147    pub fn poll_state(&self) -> PollState {
148        self.runtime.poll_state
149    }
150
151    /// Return `true` if any components currently mounted are tickable
152    #[inline]
153    pub fn is_tickable(&mut self) -> bool {
154        !self.subscriptions.tickable.is_empty()
155    }
156
157    /// Resizes the application's canvas lazily
158    #[inline]
159    pub fn tick(&mut self) {
160        for TickSubscription {
161            component_id,
162            message,
163        } in self.subscriptions.tickable.drain(..)
164        {
165            match self.components.get_mut(&component_id) {
166                Some(component) => {
167                    if component.update(message) {
168                        self.runtime.poll_state.merge(PollState::Dirty(None));
169                    }
170                }
171                None => {
172                    log::debug!(
173                        "Received message for nonexistent component (id: {}).",
174                        component_id,
175                    );
176                }
177            }
178        }
179    }
180
181    /// Compute component layout and draw the application to a canvas
182    ///
183    /// This function flushes all pending changes to the component tree,
184    /// computes the layout and redraws components where needed. After calling this
185    /// function `poll_state()` will be `PollState::Clean`
186    #[inline]
187    pub fn draw(&mut self) -> &Canvas {
188        match self.runtime.poll_state {
189            PollState::Dirty(maybe_new_size) => {
190                // Draw
191                let now = Instant::now();
192                if let Some(new_size) = maybe_new_size {
193                    log::debug!(
194                        "Screen resized {}x{} -> {}x{}",
195                        self.runtime.screen.size().width,
196                        self.runtime.screen.size().height,
197                        new_size.width,
198                        new_size.height
199                    );
200                    self.runtime.screen.resize(new_size);
201                }
202
203                let frame = Rect::new(Position::new(0, 0), self.runtime.screen.size());
204                let statistics = self.draw_tree(frame, self.runtime.num_frame);
205                let drawn_time = now.elapsed();
206
207                // Present
208                // let now = Instant::now();
209                // let num_bytes_presented = backend.present(&self.runtime.screen)?;
210                // let presented_time = now.elapsed();
211
212                log::debug!(
213                    "Frame {}: {} comps [{}] draw {:.1}ms",
214                    self.runtime.num_frame,
215                    self.components.len(),
216                    statistics,
217                    drawn_time.as_secs_f64() * 1000.0,
218                    // presented_time.as_secs_f64() * 1000.0,
219                    // num_bytes_presented,
220                );
221                self.runtime.num_frame += 1;
222            }
223            PollState::Exit => {
224                panic!("tried drawing while the app is exiting");
225            }
226            _ => {}
227        }
228        self.runtime.poll_state = PollState::Clean;
229        &self.runtime.screen
230    }
231
232    /// Resizes the application canvas. This operation is lazy and the mounted
233    /// components won't be notified until [`draw`](method.draw.html) is called.
234    pub fn handle_resize(&mut self, size: Size) {
235        self.runtime.poll_state.merge(PollState::Dirty(Some(size)));
236    }
237
238    #[inline]
239    pub fn handle_message(&mut self, message: ComponentMessage) {
240        match message.0 {
241            LinkMessage::Component(component_id, dyn_message) => {
242                let should_render = self
243                    .components
244                    .get_mut(&component_id)
245                    .map(|component| component.update(dyn_message))
246                    .unwrap_or_else(|| {
247                        log::debug!(
248                            "Received message for nonexistent component (id: {}).",
249                            component_id,
250                        );
251                        false
252                    });
253                self.runtime.poll_state.merge(if should_render {
254                    PollState::Dirty(None)
255                } else {
256                    PollState::Clean
257                });
258            }
259            LinkMessage::Exit => {
260                self.runtime.poll_state.merge(PollState::Exit);
261            }
262        }
263    }
264
265    #[inline]
266    pub fn handle_input(&mut self, event: Event) {
267        match event {
268            Event::KeyPress(key) => {
269                self.handle_key(key);
270                // todo: handle_event should return whether we need to rerender
271                self.runtime.poll_state.merge(PollState::Dirty(None));
272            }
273        }
274    }
275
276    #[inline]
277    fn handle_key(&mut self, key: Key) {
278        let Self {
279            ref mut components,
280            ref subscriptions,
281            controller: ref mut input_controller,
282            ..
283        } = *self;
284        let mut clear_controller = true;
285        let mut binding_queries = SmallVec::<[_; 4]>::with_capacity(subscriptions.focused.len());
286
287        input_controller.push(key);
288        for component_id in subscriptions.focused.iter() {
289            let focused_component = components
290                .get_mut(component_id)
291                .expect("focused component to be mounted");
292
293            let binding_query = focused_component
294                .bindings
295                .keymap()
296                .check_sequence(&input_controller.keys);
297            binding_queries.push(binding_query.map(|binding_query| {
298                NamedBindingQuery::new(focused_component.bindings.keymap(), binding_query)
299            }));
300            match focused_component
301                .bindings
302                .keymap()
303                .check_sequence(&input_controller.keys)
304            {
305                Some(BindingQuery::Match(command_id)) => {
306                    if let Some(message) = focused_component.renderable.run_command(
307                        &focused_component.bindings,
308                        *command_id,
309                        &input_controller.keys,
310                    ) {
311                        focused_component.update(message);
312                    }
313                }
314                Some(BindingQuery::PrefixOf(prefix_of)) => {
315                    log::info!(
316                        "{} ({} commands)",
317                        KeySequenceSlice::from(input_controller.keys.as_slice()),
318                        prefix_of.len()
319                    );
320                    clear_controller = false;
321                }
322                None => {}
323            }
324        }
325
326        for component_id in subscriptions.notify.iter() {
327            let notify_component = components
328                .get_mut(component_id)
329                .expect("component to be mounted");
330            notify_component
331                .renderable
332                .notify_binding_queries(&binding_queries, &input_controller.keys);
333        }
334
335        // If any component returned `BindingTransition::Clear`, we clear the controller.
336        if clear_controller {
337            input_controller.keys.clear();
338        }
339    }
340
341    #[inline]
342    fn draw_tree(&mut self, frame: Rect, generation: Generation) -> DrawStatistics {
343        let Self {
344            ref mut components,
345            ref mut layouts,
346            ref mut runtime,
347            ref mut subscriptions,
348            ref sender,
349            ..
350        } = *self;
351
352        subscriptions.clear();
353
354        let mut first = true;
355        let mut pending = Vec::new();
356        let mut statistics = DrawStatistics::default();
357        loop {
358            let (layout, frame2, position_hash, parent_changed) = if first {
359                first = false;
360                (&mut self.root, frame, 0, false)
361            } else if let Some((component_id, frame, position_hash)) = pending.pop() {
362                let component = components
363                    .get_mut(&component_id)
364                    .expect("Layout is cached only for mounted components");
365                let layout = layouts
366                    .entry(component_id)
367                    .or_insert_with(|| component.view());
368                let changed = component.should_render;
369                if changed {
370                    *layout = component.view()
371                }
372                component.set_generation(generation);
373                (layout, frame, position_hash, changed)
374            } else {
375                break;
376            };
377
378            layout.0.crawl(
379                frame2,
380                position_hash,
381                &mut |LaidComponent {
382                          frame,
383                          position_hash,
384                          template,
385                      }| {
386                    let component_id = template.generate_id(position_hash);
387                    let mut new_component = false;
388                    let component = components.entry(component_id).or_insert_with(|| {
389                        new_component = true;
390                        let (renderable, bindings) =
391                            template.create(component_id, frame, sender.clone_box());
392                        MountedComponent {
393                            renderable,
394                            frame,
395                            bindings,
396                            should_render: ShouldRender::Yes.into(),
397                            generation,
398                        }
399                    });
400
401                    if !new_component {
402                        let mut changed =
403                            parent_changed && component.change(template.dynamic_properties());
404                        if frame != component.frame {
405                            changed = component.resize(frame) || changed;
406                        }
407                        if changed {
408                            statistics.changed += 1;
409                        } else {
410                            statistics.nop += 1;
411                        }
412                    } else {
413                        statistics.new += 1;
414                    }
415
416                    component.update_bindings();
417                    if component.bindings.focused() {
418                        subscriptions.add_focused(component_id);
419                    }
420
421                    if component.bindings.notify() {
422                        subscriptions.add_notify(component_id);
423                    }
424
425                    if let Some(message) = component.tick() {
426                        subscriptions.add_tickable(component_id, message);
427                    }
428
429                    pending.push((component_id, frame, position_hash));
430                },
431                &mut |LaidCanvas { frame, canvas, .. }| {
432                    runtime.screen.copy_region(canvas, frame);
433                },
434            );
435        }
436
437        // Drop components that are not part of the current layout tree, i.e. do
438        // not appear on the screen.
439        components.retain(
440            |component_id,
441             &mut MountedComponent {
442                 generation: component_generation,
443                 ..
444             }| {
445                if component_generation < generation {
446                    statistics.deleted += 1;
447                    layouts.remove(component_id);
448                    false
449                } else {
450                    true
451                }
452            },
453        );
454
455        statistics
456    }
457}
458
459struct ComponentSubscriptions {
460    focused: SmallVec<[ComponentId; 2]>,
461    notify: SmallVec<[ComponentId; 2]>,
462    tickable: SmallVec<[TickSubscription; 2]>,
463}
464
465impl ComponentSubscriptions {
466    fn new() -> Self {
467        Self {
468            focused: SmallVec::new(),
469            notify: SmallVec::new(),
470            tickable: SmallVec::new(),
471        }
472    }
473
474    #[inline]
475    fn clear(&mut self) {
476        self.focused.clear();
477        self.notify.clear();
478        self.tickable.clear();
479    }
480
481    #[inline]
482    fn add_focused(&mut self, component_id: ComponentId) {
483        self.focused.push(component_id);
484    }
485
486    #[inline]
487    fn add_notify(&mut self, component_id: ComponentId) {
488        self.notify.push(component_id);
489    }
490
491    #[inline]
492    fn add_tickable(&mut self, component_id: ComponentId, message: DynamicMessage) {
493        self.tickable.push(TickSubscription {
494            component_id,
495            message,
496        });
497    }
498}
499
500struct TickSubscription {
501    component_id: ComponentId,
502    message: DynamicMessage,
503}
504
505impl PollState {
506    pub fn dirty(&self) -> bool {
507        matches!(*self, Self::Dirty(_))
508    }
509
510    pub fn resized(&self) -> bool {
511        matches!(*self, Self::Dirty(Some(_)))
512    }
513
514    pub fn exit(&self) -> bool {
515        matches!(*self, Self::Exit)
516    }
517
518    pub fn merge(&mut self, poll_state: PollState) {
519        *self = match (*self, poll_state) {
520            (Self::Exit, _) | (_, Self::Exit) => Self::Exit,
521            (Self::Clean, other) | (other, Self::Clean) => other,
522            (Self::Dirty(_), resized @ Self::Dirty(Some(_))) => resized,
523            (resized @ Self::Dirty(Some(_)), Self::Dirty(None)) => resized,
524            (Self::Dirty(None), Self::Dirty(None)) => Self::Dirty(None),
525        }
526    }
527}
528
529type Generation = usize;
530
531struct MountedComponent {
532    renderable: Box<dyn Renderable>,
533    frame: Rect,
534    bindings: DynamicBindings,
535    generation: Generation,
536    should_render: bool,
537}
538
539impl MountedComponent {
540    #[inline]
541    fn change(&mut self, properties: DynamicProperties) -> bool {
542        self.should_render = self.renderable.change(properties).into() || self.should_render;
543        self.should_render
544    }
545
546    #[inline]
547    fn resize(&mut self, frame: Rect) -> bool {
548        self.should_render = self.renderable.resize(frame).into() || self.should_render;
549        self.frame = frame;
550        self.should_render
551    }
552
553    #[inline]
554    fn update(&mut self, message: DynamicMessage) -> bool {
555        self.should_render = self.renderable.update(message).into() || self.should_render;
556        self.should_render
557    }
558
559    #[inline]
560    fn view(&mut self) -> Layout {
561        self.should_render = false;
562        self.renderable.view()
563    }
564
565    #[inline]
566    fn update_bindings(&mut self) {
567        self.renderable.bindings(&mut self.bindings)
568    }
569
570    #[inline]
571    fn tick(&self) -> Option<DynamicMessage> {
572        self.renderable.tick()
573    }
574
575    #[inline]
576    fn set_generation(&mut self, generation: Generation) {
577        self.generation = generation;
578    }
579}
580
581struct InputController {
582    keys: SmallVec<[Key; 8]>,
583}
584
585impl InputController {
586    fn new() -> Self {
587        Self {
588            keys: SmallVec::new(),
589        }
590    }
591
592    fn push(&mut self, key: Key) {
593        self.keys.push(key);
594    }
595}
596
597impl std::fmt::Display for InputController {
598    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
599        for key in self.keys.iter() {
600            match key {
601                Key::Char(' ') => write!(formatter, "SPC ")?,
602                Key::Char('\n') => write!(formatter, "RET ")?,
603                Key::Char('\t') => write!(formatter, "TAB ")?,
604                Key::Char(char) => write!(formatter, "{} ", char)?,
605                Key::Ctrl(char) => write!(formatter, "C-{} ", char)?,
606                Key::Alt(char) => write!(formatter, "A-{} ", char)?,
607                Key::F(number) => write!(formatter, "F{} ", number)?,
608                Key::Esc => write!(formatter, "ESC ")?,
609                key => write!(formatter, "{:?} ", key)?,
610            }
611        }
612        Ok(())
613    }
614}
615
616#[derive(Default)]
617struct DrawStatistics {
618    new: usize,
619    changed: usize,
620    deleted: usize,
621    nop: usize,
622}
623
624impl std::fmt::Display for DrawStatistics {
625    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
626        write!(
627            formatter,
628            "{} new {} upd {} del {} nop",
629            self.new, self.changed, self.deleted, self.nop
630        )
631    }
632}
633
634#[cfg(test)]
635mod tests {
636    use std::sync::mpsc;
637
638    use super::*;
639    use crate::{
640        component::ComponentExt,
641        components::text::{Text, TextProperties},
642    };
643
644    #[derive(Clone, Debug)]
645    struct MessageQueue(mpsc::Sender<ComponentMessage>);
646
647    impl MessageSender for MessageQueue {
648        fn send(&self, message: ComponentMessage) {
649            self.0.send(message).unwrap();
650        }
651
652        fn clone_box(&self) -> Box<dyn MessageSender> {
653            Box::new(self.clone())
654        }
655    }
656
657    impl MessageQueue {
658        fn new(sender: mpsc::Sender<ComponentMessage>) -> Self {
659            Self(sender)
660        }
661    }
662
663    #[test]
664    fn trivial_message_queue() {
665        let (sender, _receiver) = mpsc::channel();
666        let message_queue = MessageQueue::new(sender);
667
668        let mut app = App::new(
669            message_queue,
670            Size::new(10, 10),
671            Text::with(TextProperties::new().content("Hello")),
672        );
673
674        #[allow(clippy::never_loop)]
675        loop {
676            // Deliver component messages. This would block forever as no component
677            // sends any messages.
678            // let message = receiver
679            //     .recv_timeout(Duration::new(1, 0))
680            //     .expect_err("received an unexpected component message");
681
682            // app.handle_message(message);
683            app.handle_resize(Size::new(20, 20));
684
685            // Draw
686            let canvas = app.draw();
687            eprintln!("{}", canvas);
688
689            break;
690        }
691    }
692
693    #[test]
694    fn sizes() {
695        eprintln!(
696            "std::mem::size_of::<(ComponentId, DynamicMessage)>() == {}",
697            std::mem::size_of::<(ComponentId, DynamicMessage)>()
698        );
699        eprintln!(
700            "std::mem::size_of::<LinkMessage>() == {}",
701            std::mem::size_of::<LinkMessage>()
702        );
703    }
704}