rat_salsa/
lib.rs

1#![doc = include_str!("../readme.md")]
2#![allow(clippy::uninlined_format_args)]
3use crate::framework::control_queue::ControlQueue;
4use crate::tasks::{Cancel, Liveness};
5use crate::thread_pool::ThreadPool;
6use crate::timer::{TimerDef, TimerHandle, Timers};
7#[cfg(feature = "async")]
8use crate::tokio_tasks::TokioTasks;
9use crossbeam::channel::{SendError, Sender};
10use rat_event::{ConsumedEvent, HandleEvent, Outcome, Regular};
11use rat_focus::Focus;
12use ratatui::buffer::Buffer;
13use std::cell::{Cell, Ref, RefCell, RefMut};
14use std::cmp::Ordering;
15use std::fmt::{Debug, Formatter};
16#[cfg(feature = "async")]
17use std::future::Future;
18use std::mem;
19use std::rc::Rc;
20use std::time::Duration;
21#[cfg(feature = "async")]
22use tokio::task::AbortHandle;
23
24#[cfg(feature = "dialog")]
25pub use try_as_traits::{TryAsMut, TryAsRef, TypedContainer};
26
27#[cfg(feature = "dialog")]
28pub mod dialog_stack;
29mod framework;
30mod run_config;
31pub mod tasks;
32pub mod terminal;
33mod thread_pool;
34pub mod timer;
35#[cfg(feature = "async")]
36mod tokio_tasks;
37
38use crate::terminal::Terminal;
39pub use framework::run_tui;
40pub use run_config::RunConfig;
41
42/// Event types.
43pub mod event {
44    /// Timer event.
45    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
46    pub struct TimerEvent(pub crate::timer::TimeOut);
47
48    /// Event sent immediately after rendering.
49    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
50    pub struct RenderedEvent;
51
52    /// Event sent immediately before quitting the application.
53    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
54    pub struct QuitEvent;
55}
56
57/// Event sources.
58pub mod poll {
59    /// Trait for an event-source.
60    ///
61    /// If you need to add your own do the following:
62    ///
63    /// * Implement this trait for a struct that fits.
64    ///
65    pub trait PollEvents<Event, Error>: std::any::Any
66    where
67        Event: 'static,
68        Error: 'static,
69    {
70        fn as_any(&self) -> &dyn std::any::Any;
71
72        /// Poll for a new event.
73        ///
74        /// Events are not processed immediately when they occur. Instead,
75        /// all event sources are polled, the poll state is put into a queue.
76        /// Then the queue is emptied one by one and `read_execute()` is called.
77        ///
78        /// This prevents issues with poll-ordering of multiple sources, and
79        /// one source cannot just flood the app with events.
80        fn poll(&mut self) -> Result<bool, Error>;
81
82        /// Read the event and distribute it.
83        ///
84        /// If you add a new event, that doesn't fit into AppEvents, you'll
85        /// have to define a new trait for your AppState and use that.
86        fn read(&mut self) -> Result<crate::Control<Event>, Error>;
87    }
88
89    mod crossterm;
90    mod quit;
91    mod rendered;
92    mod thread_pool;
93    mod timer;
94    #[cfg(feature = "async")]
95    mod tokio_tasks;
96
97    pub use crossterm::PollCrossterm;
98    pub use quit::PollQuit;
99    pub use rendered::PollRendered;
100    pub use thread_pool::PollTasks;
101    pub use timer::PollTimers;
102    #[cfg(feature = "async")]
103    pub use tokio_tasks::PollTokio;
104}
105
106pub mod mock {
107    //! Provides dummy implementations for some functions.
108
109    /// Empty placeholder for [run_tui](crate::run_tui).
110    pub fn init<State, Global, Error>(
111        _state: &mut State, //
112        _ctx: &mut Global,
113    ) -> Result<(), Error> {
114        Ok(())
115    }
116
117    /// Empty placeholder for [run_tui](crate::run_tui).
118    pub fn error<Global, State, Event, Error>(
119        _error: Error,
120        _state: &mut State,
121        _ctx: &mut Global,
122    ) -> Result<crate::Control<Event>, Error> {
123        Ok(crate::Control::Continue)
124    }
125}
126
127/// Result enum for event handling.
128///
129/// The result of an event is processed immediately after the
130/// function returns, before polling new events. This way an action
131/// can trigger another action which triggers the repaint without
132/// other events intervening.
133///
134/// If you ever need to return more than one result from event-handling,
135/// you can hand it to AppContext/RenderContext::queue(). Events
136/// in the queue are processed in order, and the return value of
137/// the event-handler comes last. If an error is returned, everything
138/// send to the queue will be executed nonetheless.
139///
140/// __See__
141///
142/// - [flow!](rat_event::flow)
143/// - [try_flow!](rat_event::try_flow)
144/// - [ConsumedEvent]
145#[derive(Debug, Clone, Copy)]
146#[must_use]
147#[non_exhaustive]
148pub enum Control<Event> {
149    /// Continue with event-handling.
150    /// In the event-loop this waits for the next event.
151    Continue,
152    /// Break event-handling without repaint.
153    /// In the event-loop this waits for the next event.
154    Unchanged,
155    /// Break event-handling and repaints/renders the application.
156    /// In the event-loop this calls `render`.
157    Changed,
158    /// Eventhandling can cause secondary application specific events.
159    /// One common way is to return this `Control::Message(my_event)`
160    /// to reenter the event-loop with your own secondary event.
161    ///
162    /// This acts quite like a message-queue to communicate between
163    /// disconnected parts of your application. And indeed there is
164    /// a hidden message-queue as part of the event-loop.
165    ///
166    /// The other way is to call [SalsaAppContext::queue] to initiate such
167    /// events.
168    Event(Event),
169    /// A dialog close event. In the main loop it will be handled
170    /// just like [Control::Event]. But the DialogStack can react
171    /// separately and close the window.
172    #[cfg(feature = "dialog")]
173    Close(Event),
174    /// Quit the application.
175    Quit,
176}
177
178impl<Event> Eq for Control<Event> {}
179
180impl<Event> PartialEq for Control<Event> {
181    fn eq(&self, other: &Self) -> bool {
182        mem::discriminant(self) == mem::discriminant(other)
183    }
184}
185
186impl<Event> Ord for Control<Event> {
187    fn cmp(&self, other: &Self) -> Ordering {
188        match self {
189            Control::Continue => match other {
190                Control::Continue => Ordering::Equal,
191                Control::Unchanged => Ordering::Less,
192                Control::Changed => Ordering::Less,
193                Control::Event(_) => Ordering::Less,
194                #[cfg(feature = "dialog")]
195                Control::Close(_) => Ordering::Less,
196                Control::Quit => Ordering::Less,
197            },
198            Control::Unchanged => match other {
199                Control::Continue => Ordering::Greater,
200                Control::Unchanged => Ordering::Equal,
201                Control::Changed => Ordering::Less,
202                Control::Event(_) => Ordering::Less,
203                #[cfg(feature = "dialog")]
204                Control::Close(_) => Ordering::Less,
205                Control::Quit => Ordering::Less,
206            },
207            Control::Changed => match other {
208                Control::Continue => Ordering::Greater,
209                Control::Unchanged => Ordering::Greater,
210                Control::Changed => Ordering::Equal,
211                Control::Event(_) => Ordering::Less,
212                #[cfg(feature = "dialog")]
213                Control::Close(_) => Ordering::Less,
214                Control::Quit => Ordering::Less,
215            },
216            Control::Event(_) => match other {
217                Control::Continue => Ordering::Greater,
218                Control::Unchanged => Ordering::Greater,
219                Control::Changed => Ordering::Greater,
220                Control::Event(_) => Ordering::Equal,
221                #[cfg(feature = "dialog")]
222                Control::Close(_) => Ordering::Less,
223                Control::Quit => Ordering::Less,
224            },
225            #[cfg(feature = "dialog")]
226            Control::Close(_) => match other {
227                Control::Continue => Ordering::Greater,
228                Control::Unchanged => Ordering::Greater,
229                Control::Changed => Ordering::Greater,
230                Control::Event(_) => Ordering::Greater,
231                Control::Close(_) => Ordering::Equal,
232                Control::Quit => Ordering::Less,
233            },
234            Control::Quit => match other {
235                Control::Continue => Ordering::Greater,
236                Control::Unchanged => Ordering::Greater,
237                Control::Changed => Ordering::Greater,
238                Control::Event(_) => Ordering::Greater,
239                #[cfg(feature = "dialog")]
240                Control::Close(_) => Ordering::Greater,
241                Control::Quit => Ordering::Equal,
242            },
243        }
244    }
245}
246
247impl<Event> PartialOrd for Control<Event> {
248    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
249        Some(self.cmp(other))
250    }
251}
252
253impl<Event> ConsumedEvent for Control<Event> {
254    fn is_consumed(&self) -> bool {
255        !matches!(self, Control::Continue)
256    }
257}
258
259impl<Event, T: Into<Outcome>> From<T> for Control<Event> {
260    fn from(value: T) -> Self {
261        let r = value.into();
262        match r {
263            Outcome::Continue => Control::Continue,
264            Outcome::Unchanged => Control::Unchanged,
265            Outcome::Changed => Control::Changed,
266        }
267    }
268}
269
270/// This trait gives access to all facilities built into rat-salsa.
271///
272/// Your global state struct has to implement this trait. This allows
273/// rat-salsa to add its facilities to it.  
274///
275/// [run_tui] sets it during initialization, it will be up and
276/// running by the time init() is called.
277///
278pub trait SalsaContext<Event, Error>
279where
280    Event: 'static,
281    Error: 'static,
282{
283    /// The AppContext struct holds all the data for the rat-salsa
284    /// functionality. [run_tui] calls this to set the initialized
285    /// struct.
286    fn set_salsa_ctx(&mut self, app_ctx: SalsaAppContext<Event, Error>);
287
288    /// Access the AppContext previously set.
289    fn salsa_ctx(&self) -> &SalsaAppContext<Event, Error>;
290
291    /// Get the current frame/render-count.
292    fn count(&self) -> usize {
293        self.salsa_ctx().count.get()
294    }
295
296    /// Get the last render timing.
297    fn last_render(&self) -> Duration {
298        self.salsa_ctx().last_render.get()
299    }
300
301    /// Get the last event-handling timing.
302    fn last_event(&self) -> Duration {
303        self.salsa_ctx().last_event.get()
304    }
305
306    /// Set the cursor, if the given value is something,
307    /// hides it otherwise.
308    ///
309    /// This should only be set during rendering.
310    fn set_screen_cursor(&self, cursor: Option<(u16, u16)>) {
311        if let Some(c) = cursor {
312            self.salsa_ctx().cursor.set(Some(c));
313        }
314    }
315
316    /// Add a timer.
317    ///
318    /// __Panic__
319    ///
320    /// Panics if no timer support is configured.
321    #[inline]
322    fn add_timer(&self, t: TimerDef) -> TimerHandle {
323        self.salsa_ctx()
324            .timers
325            .as_ref()
326            .expect("No timers configured. In main() add RunConfig::default()?.poll(PollTimers)")
327            .add(t)
328    }
329
330    /// Remove a timer.
331    ///
332    /// __Panic__
333    ///
334    /// Panics if no timer support is configured.
335    #[inline]
336    fn remove_timer(&self, tag: TimerHandle) {
337        self.salsa_ctx()
338            .timers
339            .as_ref()
340            .expect("No timers configured. In main() add RunConfig::default()?.poll(PollTimers)")
341            .remove(tag);
342    }
343
344    /// Replace a timer.
345    /// Remove the old timer and create a new one.
346    /// If the old timer no longer exists it just creates the new one.
347    ///
348    /// __Panic__
349    ///
350    /// Panics if no timer support is configured.
351    #[inline]
352    fn replace_timer(&self, h: Option<TimerHandle>, t: TimerDef) -> TimerHandle {
353        if let Some(h) = h {
354            self.remove_timer(h);
355        }
356        self.add_timer(t)
357    }
358
359    /// Add a background worker task.
360    ///
361    /// ```rust ignore
362    /// let cancel = ctx.spawn(|cancel, send| {
363    ///     // ... do stuff
364    ///     if cancel.is_canceled() {
365    ///         return; // return early
366    ///     }
367    ///     Ok(Control::Continue)
368    /// });
369    /// ```
370    ///
371    /// - Cancel token
372    ///
373    /// The cancel token can be used by the application to signal an early
374    /// cancellation of a long-running task. This cancellation is cooperative,
375    /// the background task must regularly check for cancellation and quit
376    /// if needed.
377    ///
378    /// - Liveness token
379    ///
380    /// This token is set whenever the given task has finished, be it
381    /// regularly or by panicking.
382    ///
383    /// __Panic__
384    ///
385    /// Panics if no worker-thread support is configured.
386    #[inline]
387    fn spawn_ext(
388        &self,
389        task: impl FnOnce(Cancel, &Sender<Result<Control<Event>, Error>>) -> Result<Control<Event>, Error>
390            + Send
391            + 'static,
392    ) -> Result<(Cancel, Liveness), SendError<()>>
393    where
394        Event: 'static + Send,
395        Error: 'static + Send,
396    {
397        self.salsa_ctx()
398            .tasks
399            .as_ref()
400            .expect(
401                "No thread-pool configured. In main() add RunConfig::default()?.poll(PollTasks)",
402            )
403            .spawn(Box::new(task))
404    }
405
406    /// Add a background worker task.
407    ///
408    /// ```rust ignore
409    /// let cancel = ctx.spawn(|| {
410    ///     // ...
411    ///     Ok(Control::Continue)
412    /// });
413    /// ```
414    ///
415    /// __Panic__
416    ///
417    /// Panics if no worker-thread support is configured.
418    #[inline]
419    fn spawn(
420        &self,
421        task: impl FnOnce() -> Result<Control<Event>, Error> + Send + 'static,
422    ) -> Result<(), SendError<()>>
423    where
424        Event: 'static + Send,
425        Error: 'static + Send,
426    {
427        _ = self
428            .salsa_ctx()
429            .tasks
430            .as_ref()
431            .expect(
432                "No thread-pool configured. In main() add RunConfig::default()?.poll(PollTasks)",
433            )
434            .spawn(Box::new(|_, _| task()))?;
435        Ok(())
436    }
437
438    /// Spawn a future in the executor.
439    ///
440    /// Panic
441    ///
442    /// Panics if tokio is not configured.
443    #[inline]
444    #[cfg(feature = "async")]
445    fn spawn_async<F>(&self, future: F)
446    where
447        F: Future<Output = Result<Control<Event>, Error>> + Send + 'static,
448        Event: 'static + Send,
449        Error: 'static + Send,
450    {
451        _ = self.salsa_ctx() //
452            .tokio
453            .as_ref()
454            .expect("No tokio runtime is configured. In main() add RunConfig::default()?.poll(PollTokio::new(rt))")
455            .spawn(Box::new(future));
456    }
457
458    /// Spawn a future in the executor.
459    /// You get an extra channel to send back more than one result.
460    ///
461    /// - AbortHandle
462    ///
463    /// The tokio AbortHandle to abort a spawned task.
464    ///
465    /// - Liveness
466    ///
467    /// This token is set whenever the given task has finished, be it
468    /// regularly or by panicking.
469    ///
470    /// Panic
471    ///
472    /// Panics if tokio is not configured.
473    #[inline]
474    #[cfg(feature = "async")]
475    fn spawn_async_ext<C, F>(&self, cr_future: C) -> (AbortHandle, Liveness)
476    where
477        C: FnOnce(tokio::sync::mpsc::Sender<Result<Control<Event>, Error>>) -> F,
478        F: Future<Output = Result<Control<Event>, Error>> + Send + 'static,
479        Event: 'static + Send,
480        Error: 'static + Send,
481    {
482        let rt = self
483            .salsa_ctx()//
484            .tokio
485            .as_ref()
486            .expect("No tokio runtime is configured. In main() add RunConfig::default()?.poll(PollTokio::new(rt))");
487        let future = cr_future(rt.sender());
488        rt.spawn(Box::new(future))
489    }
490
491    /// Queue an application event.
492    #[inline]
493    fn queue_event(&self, event: Event) {
494        self.salsa_ctx().queue.push(Ok(Control::Event(event)));
495    }
496
497    /// Queue additional results.
498    #[inline]
499    fn queue(&self, ctrl: impl Into<Control<Event>>) {
500        self.salsa_ctx().queue.push(Ok(ctrl.into()));
501    }
502
503    /// Queue an error.
504    #[inline]
505    fn queue_err(&self, err: Error) {
506        self.salsa_ctx().queue.push(Err(err));
507    }
508
509    /// Set the `Focus`.
510    #[inline]
511    fn set_focus(&self, focus: Focus) {
512        self.salsa_ctx().focus.replace(Some(focus));
513    }
514
515    /// Take the `Focus` back from the Context.
516    #[inline]
517    fn take_focus(&self) -> Option<Focus> {
518        self.salsa_ctx().focus.take()
519    }
520
521    /// Clear the `Focus`.
522    #[inline]
523    fn clear_focus(&self) {
524        self.salsa_ctx().focus.replace(None);
525    }
526
527    /// Access the `Focus`.
528    ///
529    /// __Panic__
530    ///
531    /// Panics if no focus has been set.
532    #[inline]
533    fn focus<'a>(&'a self) -> Ref<'a, Focus> {
534        let borrow = self.salsa_ctx().focus.borrow();
535        Ref::map(borrow, |v| v.as_ref().expect("focus"))
536    }
537
538    /// Mutably access the focus-field.
539    ///
540    /// __Panic__
541    ///
542    /// Panics if no focus has been set.
543    #[inline]
544    fn focus_mut<'a>(&'a mut self) -> RefMut<'a, Focus> {
545        let borrow = self.salsa_ctx().focus.borrow_mut();
546        RefMut::map(borrow, |v| v.as_mut().expect("focus"))
547    }
548
549    /// Handle the focus-event and automatically queue the result.
550    ///
551    /// __Panic__
552    ///
553    /// Panics if no focus has been set.
554    #[inline]
555    fn handle_focus<E>(&mut self, event: &E) -> Outcome
556    where
557        Focus: HandleEvent<E, Regular, Outcome>,
558    {
559        let mut borrow = self.salsa_ctx().focus.borrow_mut();
560        let focus = borrow.as_mut().expect("focus");
561        let r = focus.handle(event, Regular);
562        if r.is_consumed() {
563            self.queue(r);
564        }
565        r
566    }
567
568    /// Access the terminal.
569    #[inline]
570    fn terminal(&mut self) -> Rc<RefCell<dyn Terminal<Error>>> {
571        self.salsa_ctx().term.clone().expect("terminal")
572    }
573
574    /// Clear the terminal and do a full redraw before the next draw.
575    #[inline]
576    fn clear_terminal(&mut self) {
577        self.salsa_ctx().clear_terminal.set(true);
578    }
579
580    /// Call insert_before() before the next draw.
581    #[inline]
582    fn insert_before(&mut self, height: u16, draw_fn: impl FnOnce(&mut Buffer) + 'static) {
583        self.salsa_ctx().insert_before.set(InsertBefore {
584            height,
585            draw_fn: Box::new(draw_fn),
586        });
587    }
588}
589
590///
591/// Application context for event handling.
592///
593/// Add this to your global state and implement [SalsaContext] to
594/// access the facilities of rat-salsa. You can Default::default()
595/// initialize this field with some dummy values. It will
596/// be set correctly when calling [run_tui].
597///
598pub struct SalsaAppContext<Event, Error>
599where
600    Event: 'static,
601    Error: 'static,
602{
603    /// Can be set to hold a Focus, if needed.
604    pub(crate) focus: RefCell<Option<Focus>>,
605    /// Last frame count rendered.
606    pub(crate) count: Cell<usize>,
607    /// Output cursor position. Set to Frame after rendering is complete.
608    pub(crate) cursor: Cell<Option<(u16, u16)>>,
609    /// Terminal area
610    pub(crate) term: Option<Rc<RefCell<dyn Terminal<Error>>>>,
611    /// Clear terminal before next draw.
612    pub(crate) clear_terminal: Cell<bool>,
613    /// Call insert_before before the next draw.
614    pub(crate) insert_before: Cell<InsertBefore>,
615    /// Last render time.
616    pub(crate) last_render: Cell<Duration>,
617    /// Last event time.
618    pub(crate) last_event: Cell<Duration>,
619
620    /// Application timers.
621    pub(crate) timers: Option<Rc<Timers>>,
622    /// Background tasks.
623    pub(crate) tasks: Option<Rc<ThreadPool<Event, Error>>>,
624    /// Background tasks.
625    #[cfg(feature = "async")]
626    pub(crate) tokio: Option<Rc<TokioTasks<Event, Error>>>,
627    /// Queue foreground tasks.
628    pub(crate) queue: ControlQueue<Event, Error>,
629}
630
631struct InsertBefore {
632    height: u16,
633    draw_fn: Box<dyn FnOnce(&mut Buffer)>,
634}
635
636impl Debug for InsertBefore {
637    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
638        f.debug_struct("InsertBefore")
639            .field("height", &self.height)
640            .field("draw_fn", &"dyn Fn()")
641            .finish()
642    }
643}
644
645impl Default for InsertBefore {
646    fn default() -> Self {
647        Self {
648            height: 0,
649            draw_fn: Box::new(|_| {}),
650        }
651    }
652}
653
654impl<Event, Error> Debug for SalsaAppContext<Event, Error> {
655    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
656        let mut ff = f.debug_struct("AppContext");
657        ff.field("focus", &self.focus)
658            .field("count", &self.count)
659            .field("cursor", &self.cursor)
660            .field("clear_terminal", &self.clear_terminal)
661            .field("insert_before", &"n/a")
662            .field("timers", &self.timers)
663            .field("tasks", &self.tasks)
664            .field("queue", &self.queue);
665        #[cfg(feature = "async")]
666        {
667            ff.field("tokio", &self.tokio);
668        }
669        ff.finish()
670    }
671}
672
673impl<Event, Error> Default for SalsaAppContext<Event, Error>
674where
675    Event: 'static,
676    Error: 'static,
677{
678    fn default() -> Self {
679        Self {
680            focus: Default::default(),
681            count: Default::default(),
682            cursor: Default::default(),
683            term: Default::default(),
684            clear_terminal: Default::default(),
685            insert_before: Default::default(),
686            last_render: Default::default(),
687            last_event: Default::default(),
688            timers: Default::default(),
689            tasks: Default::default(),
690            #[cfg(feature = "async")]
691            tokio: Default::default(),
692            queue: Default::default(),
693        }
694    }
695}
696
697impl<Event, Error> SalsaContext<Event, Error> for SalsaAppContext<Event, Error>
698where
699    Event: 'static,
700    Error: 'static,
701{
702    #[inline]
703    fn set_salsa_ctx(&mut self, app_ctx: SalsaAppContext<Event, Error>) {
704        *self = app_ctx;
705    }
706
707    #[inline]
708    fn salsa_ctx(&self) -> &SalsaAppContext<Event, Error> {
709        self
710    }
711}
712
713mod _private {
714    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
715    pub struct NonExhaustive;
716}