rat_dialog/
lib.rs

1#![allow(clippy::question_mark)]
2#![allow(clippy::type_complexity)]
3use rat_event::{ConsumedEvent, HandleEvent, Outcome};
4use rat_salsa::{Control, SalsaContext};
5use ratatui::buffer::Buffer;
6use ratatui::layout::Rect;
7use std::any::{Any, TypeId, type_name};
8use std::cell::{Cell, RefCell};
9use std::cmp::max;
10use std::fmt::{Debug, Formatter};
11use std::mem;
12use std::rc::Rc;
13
14/// Extends rat-salsa::Control with some dialog specific options.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
16#[must_use]
17#[non_exhaustive]
18pub enum DialogControl<Event> {
19    /// Continue with event-handling.
20    /// In the event-loop this waits for the next event.
21    Continue,
22    /// Break event-handling without repaint.
23    /// In the event-loop this waits for the next event.
24    Unchanged,
25    /// Break event-handling and repaints/renders the application.
26    /// In the event-loop this calls `render`.
27    Changed,
28    /// Return back some application event.
29    Event(Event),
30    /// Close the dialog
31    Close(Option<Event>),
32    /// Move to front
33    ToFront,
34    /// Quit
35    Quit,
36}
37
38impl<Event> ConsumedEvent for DialogControl<Event> {
39    fn is_consumed(&self) -> bool {
40        !matches!(self, DialogControl::Continue)
41    }
42}
43
44impl<Event, T: Into<Outcome>> From<T> for DialogControl<Event> {
45    fn from(value: T) -> Self {
46        let r = value.into();
47        match r {
48            Outcome::Continue => DialogControl::Continue,
49            Outcome::Unchanged => DialogControl::Unchanged,
50            Outcome::Changed => DialogControl::Changed,
51        }
52    }
53}
54
55/// Hold a stack of widgets.
56///
57/// Renders the widgets and can handle events.
58///
59/// Hold the dialog-stack in your global state,
60/// call render() at the very end of rendering and
61/// handle() near the start of event-handling.
62///
63/// This will not handle modality, so make sure
64/// to consume all events you don't want to propagate.
65///
66pub struct DialogStack<Event, Context, Error> {
67    core: Rc<DialogStackCore<Event, Context, Error>>,
68}
69
70struct DialogStackCore<Event, Context, Error> {
71    len: Cell<usize>,
72    render: RefCell<Vec<Box<dyn Fn(Rect, &mut Buffer, &mut dyn Any, &mut Context) + 'static>>>,
73    event: RefCell<
74        Vec<
75            Box<
76                dyn Fn(&Event, &mut dyn Any, &mut Context) -> Result<DialogControl<Event>, Error>
77                    + 'static,
78            >,
79        >,
80    >,
81    type_id: RefCell<Vec<TypeId>>,
82    state: RefCell<Vec<Option<Box<dyn Any>>>>,
83}
84
85impl<Event, Context, Error> Clone for DialogStack<Event, Context, Error> {
86    fn clone(&self) -> Self {
87        Self {
88            core: self.core.clone(),
89        }
90    }
91}
92
93impl<Event, Context, Error> Default for DialogStack<Event, Context, Error> {
94    fn default() -> Self {
95        Self {
96            core: Rc::new(DialogStackCore {
97                len: Cell::new(0),
98                render: Default::default(),
99                event: Default::default(),
100                type_id: Default::default(),
101                state: Default::default(),
102            }),
103        }
104    }
105}
106
107impl<Event, Context, Error> Debug for DialogStack<Event, Context, Error> {
108    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
109        let state = self.core.state.borrow();
110        let is_proxy = state.iter().map(|v| v.is_none()).collect::<Vec<_>>();
111        let type_id = self.core.type_id.borrow();
112
113        f.debug_struct("DialogStackCore")
114            .field("len", &self.core.len.get())
115            .field("type_id", &type_id)
116            .field("is_proxy", &is_proxy)
117            .finish()
118    }
119}
120
121impl<Event, Context, Error> DialogStack<Event, Context, Error> {
122    /// Render all dialog-windows in stack-order.
123    pub fn render(self, area: Rect, buf: &mut Buffer, ctx: &mut Context) {
124        for n in 0..self.core.len.get() {
125            let Some(mut state) = self.core.state.borrow_mut()[n].take() else {
126                panic!("state is gone");
127            };
128            let render_fn = mem::replace(
129                &mut self.core.render.borrow_mut()[n],
130                Box::new(|_, _, _, _| {}),
131            );
132
133            render_fn(area, buf, state.as_mut(), ctx);
134
135            self.core.render.borrow_mut()[n] = render_fn;
136            self.core.state.borrow_mut()[n] = Some(state);
137        }
138    }
139}
140
141impl<Event, Context, Error> DialogStack<Event, Context, Error> {
142    pub fn new() -> Self {
143        Self::default()
144    }
145
146    /// Push a dialog-window on the stack.
147    /// - render is called in reverse stack order, to render bottom to top.
148    /// - event is called in stack-order to handle events.
149    ///   if you don't want events to propagate to dialog-windows in the
150    ///   background, you must consume them by returning StackControl::Unchanged.
151    /// - state as Any
152    pub fn push(
153        &self,
154        render: impl Fn(Rect, &mut Buffer, &mut dyn Any, &'_ mut Context) + 'static,
155        event: impl Fn(&Event, &mut dyn Any, &'_ mut Context) -> Result<DialogControl<Event>, Error>
156        + 'static,
157        state: impl Any,
158    ) {
159        self.core.len.update(|v| v + 1);
160        self.core.type_id.borrow_mut().push(state.type_id());
161        self.core.state.borrow_mut().push(Some(Box::new(state)));
162        self.core.event.borrow_mut().push(Box::new(event));
163        self.core.render.borrow_mut().push(Box::new(render));
164    }
165
166    /// Pop the top dialog-window from the stack.
167    ///
168    /// It will return None if the stack is empty.
169    ///
170    /// Panic
171    ///
172    /// This function is partially reentrant. When called during rendering/event-handling
173    /// it will panic when trying to pop your current dialog-window.
174    /// Return StackControl::Pop instead of calling this function.
175    pub fn pop(&self) -> Option<Box<dyn Any>> {
176        self.core.len.update(|v| v - 1);
177        self.core.type_id.borrow_mut().pop();
178        self.core.event.borrow_mut().pop();
179        self.core.render.borrow_mut().pop();
180        let Some(s) = self.core.state.borrow_mut().pop() else {
181            return None;
182        };
183        if s.is_none() {
184            panic!("state is gone");
185        }
186        s
187    }
188
189    /// Remove some dialog-window.
190    ///
191    /// Panic
192    ///
193    /// This function is not reentrant. It will panic when called during
194    /// rendering or event-handling of any dialog-window.
195    /// Panics when out-of-bounds.
196    pub fn remove(&self, n: usize) -> Box<dyn Any> {
197        for s in self.core.state.borrow().iter() {
198            if s.is_none() {
199                panic!("state is gone");
200            }
201        }
202
203        self.core.len.update(|v| v - 1);
204        self.core.type_id.borrow_mut().remove(n);
205        _ = self.core.event.borrow_mut().remove(n);
206        _ = self.core.render.borrow_mut().remove(n);
207
208        self.core
209            .state
210            .borrow_mut()
211            .remove(n)
212            .expect("state exists")
213    }
214
215    /// Move the given dialog-window to the top of the stack.
216    ///
217    /// Panic
218    ///
219    /// This function is not reentrant. It will panic when called during
220    /// rendering or event-handling of any dialog-window. Use StackControl::ToFront
221    /// for this.
222    ///
223    /// Panics when out-of-bounds.
224    pub fn to_front(&self, n: usize) {
225        for s in self.core.state.borrow().iter() {
226            if s.is_none() {
227                panic!("state is gone");
228            }
229        }
230
231        let type_id = self.core.type_id.borrow_mut().remove(n);
232        let state = self.core.state.borrow_mut().remove(n);
233        let event = self.core.event.borrow_mut().remove(n);
234        let render = self.core.render.borrow_mut().remove(n);
235
236        self.core.type_id.borrow_mut().push(type_id);
237        self.core.state.borrow_mut().push(state);
238        self.core.event.borrow_mut().push(event);
239        self.core.render.borrow_mut().push(render);
240    }
241
242    /// No windows.
243    pub fn is_empty(&self) -> bool {
244        self.core.type_id.borrow().is_empty()
245    }
246
247    /// Number of dialog-windows.
248    pub fn len(&self) -> usize {
249        self.core.len.get()
250    }
251
252    /// Typecheck the state.
253    pub fn state_is<S: 'static>(&self, n: usize) -> bool {
254        self.core.type_id.borrow()[n] == TypeId::of::<S>()
255    }
256
257    /// Find first state with this type.
258    #[allow(clippy::manual_find)]
259    pub fn first<S: 'static>(&self) -> Option<usize> {
260        for n in (0..self.core.len.get()).rev() {
261            if self.core.type_id.borrow()[n] == TypeId::of::<S>() {
262                return Some(n);
263            }
264        }
265        None
266    }
267
268    /// Find all states with this type.
269    pub fn find<S: 'static>(&self) -> Vec<usize> {
270        self.core
271            .type_id
272            .borrow()
273            .iter()
274            .enumerate()
275            .rev()
276            .filter_map(|(n, v)| {
277                if *v == TypeId::of::<S>() {
278                    Some(n)
279                } else {
280                    None
281                }
282            })
283            .collect()
284    }
285
286    /// Run f for the given instance of S.
287    ///
288    /// Panic
289    ///
290    /// Panics when out-of-bounds.
291    /// Panics when recursively accessing the same state. Accessing a
292    /// *different* window-state is fine.
293    /// Panics when the types don't match.
294    pub fn apply<S: 'static, R>(&self, n: usize, f: impl Fn(&S) -> R) -> R {
295        let Some(state) = self.core.state.borrow_mut()[n].take() else {
296            panic!("state is gone");
297        };
298
299        let r = if let Some(state) = state.as_ref().downcast_ref::<S>() {
300            f(state)
301        } else {
302            self.core.state.borrow_mut()[n] = Some(state);
303            panic!("state is not {:?}", type_name::<S>());
304        };
305
306        self.core.state.borrow_mut()[n] = Some(state);
307        r
308    }
309
310    /// Run f for the given instance of S with exclusive/mutabel access.
311    ///
312    /// Panic
313    ///
314    /// Panics when out-of-bounds.
315    /// Panics when recursively accessing the same state. Accessing a
316    /// *different* window-state is fine.
317    /// Panics when the types don't match.
318    pub fn apply_mut<S: 'static, R>(&mut self, n: usize, f: impl Fn(&mut S) -> R) -> R {
319        let Some(mut state) = self.core.state.borrow_mut()[n].take() else {
320            panic!("state is gone");
321        };
322
323        let r = if let Some(state) = state.as_mut().downcast_mut::<S>() {
324            f(state)
325        } else {
326            self.core.state.borrow_mut()[n] = Some(state);
327            panic!("state is not {:?}", type_name::<S>());
328        };
329
330        self.core.state.borrow_mut()[n] = Some(state);
331        r
332    }
333}
334
335/// Handle events from top to bottom of the stack.
336///
337/// Panic
338///
339/// This function is not reentrant, it will panic when called from within it's call-stack.
340impl<Event, Context, Error> HandleEvent<Event, &mut Context, Result<Control<Event>, Error>>
341    for DialogStack<Event, Context, Error>
342where
343    Context: SalsaContext<Event, Error>,
344    Error: 'static,
345    Event: 'static,
346{
347    fn handle(&mut self, event: &Event, ctx: &mut Context) -> Result<Control<Event>, Error> {
348        let mut rr = Control::Continue;
349
350        for n in (0..self.core.len.get()).rev() {
351            let Some(mut state) = self.core.state.borrow_mut()[n].take() else {
352                panic!("state is gone");
353            };
354
355            let event_fn = mem::replace(
356                &mut self.core.event.borrow_mut()[n],
357                Box::new(|_, _, _| Ok(DialogControl::Continue)),
358            );
359
360            let r = event_fn(event, state.as_mut(), ctx);
361
362            self.core.event.borrow_mut()[n] = event_fn;
363            self.core.state.borrow_mut()[n] = Some(state);
364
365            match r {
366                Ok(r) => match r {
367                    DialogControl::Close(event) => {
368                        self.remove(n);
369                        if let Some(event) = event {
370                            ctx.queue_event(event);
371                        }
372                        rr = max(rr, Control::Changed);
373                    }
374                    DialogControl::Event(event) => {
375                        ctx.queue_event(event);
376                        rr = max(rr, Control::Continue);
377                    }
378                    DialogControl::ToFront => {
379                        self.to_front(n);
380                        rr = max(rr, Control::Changed);
381                    }
382                    DialogControl::Continue => {
383                        rr = max(rr, Control::Continue);
384                    }
385                    DialogControl::Unchanged => {
386                        rr = max(rr, Control::Unchanged);
387                    }
388                    DialogControl::Changed => {
389                        rr = max(rr, Control::Changed);
390                    }
391                    DialogControl::Quit => {
392                        rr = max(rr, Control::Quit);
393                    }
394                },
395                Err(e) => return Err(e),
396            }
397        }
398
399        Ok(rr)
400    }
401}