rat_dialog/
dialog_stack.rs

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