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