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