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