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 dialog-widgets.
17///
18/// Renders them and integrates them into event-handling.
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 mutable reference to the state at index n.
225    ///
226    /// Panic
227    ///
228    /// Panics when out-of-bounds.
229    ///
230    /// Fails
231    ///
232    /// Fails when recursively accessing the same state. Accessing a
233    /// *different* window-state is fine.
234    /// Fails when the types don't match.
235    pub fn get_mut<'a, S: 'static>(&'a self, n: usize) -> Option<RefMut<'a, S>> {
236        let state = self.core.state.borrow_mut();
237
238        RefMut::filter_map(state, |v| {
239            let state = &mut v[n];
240            if let Some(state) = state.as_mut() {
241                if let Some(state) = state.downcast_mut::<S>() {
242                    Some(state)
243                } else {
244                    None
245                }
246            } else {
247                None
248            }
249        })
250        .ok()
251    }
252
253    /// Get a reference to the state at index n.
254    ///
255    /// Panic
256    ///
257    /// Panics when out-of-bounds.
258    ///
259    /// Fails
260    ///
261    /// Fails when recursively accessing the same state. Accessing a
262    /// *different* window-state is fine.
263    /// Fails when the types don't match.
264    pub fn get<'a, S: 'static>(&'a self, n: usize) -> Option<Ref<'a, S>> {
265        let state = self.core.state.borrow();
266
267        Ref::filter_map(state, |v| {
268            let state = &v[n];
269            if let Some(state) = state.as_ref() {
270                if let Some(state) = state.downcast_ref::<S>() {
271                    Some(state)
272                } else {
273                    None
274                }
275            } else {
276                None
277            }
278        })
279        .ok()
280    }
281}
282
283/// Handle events from top to bottom of the stack.
284///
285/// crossterm events will only be passed on to the first
286/// dialog window. Other application events will go through
287/// all the windows on the stack until they are consumed.
288///
289/// Panic
290///
291/// This function is not reentrant, it will panic when called from within it's call-stack.
292impl<Event, Context, Error> HandleEvent<Event, &mut Context, Result<WindowControl<Event>, Error>>
293    for DialogStack<Event, Context, Error>
294where
295    Event: TryAsRef<crossterm::event::Event>,
296    Error: 'static,
297    Event: 'static,
298{
299    fn handle(&mut self, event: &Event, ctx: &mut Context) -> Result<WindowControl<Event>, Error> {
300        for n in (0..self.core.len.get()).rev() {
301            let Some(mut state) = self.core.state.borrow_mut()[n].take() else {
302                panic!("state is gone");
303            };
304            let event_fn = mem::replace(
305                &mut self.core.event.borrow_mut()[n],
306                Box::new(|_, _, _| Ok(WindowControl::Continue)),
307            );
308
309            let r = event_fn(event, state.as_mut(), ctx);
310
311            self.core.event.borrow_mut()[n] = event_fn;
312            self.core.state.borrow_mut()[n] = Some(state);
313
314            match r {
315                Ok(r) => match r {
316                    WindowControl::Close(_) => {
317                        self.remove(n);
318                        return Ok(r);
319                    }
320                    WindowControl::Event(_) => {
321                        return Ok(r);
322                    }
323                    WindowControl::Unchanged => {
324                        return Ok(r);
325                    }
326                    WindowControl::Changed => {
327                        return Ok(r);
328                    }
329                    WindowControl::Continue => {
330                        // next
331                    }
332                },
333                Err(e) => return Err(e),
334            }
335
336            // Block all crossterm events.
337            let event: Option<&crossterm::event::Event> = event.try_as_ref();
338            if event.is_some() {
339                return Ok(WindowControl::Unchanged);
340            }
341        }
342
343        Ok(WindowControl::Continue)
344    }
345}
346
347/// Handle events from top to bottom of the stack.
348///
349/// Panic
350///
351/// This function is not reentrant, it will panic when called from within it's call-stack.
352pub fn handle_dialog_stack<Event, Context, Error>(
353    mut dialog_stack: DialogStack<Event, Context, Error>,
354    event: &Event,
355    ctx: &mut Context,
356) -> Result<WindowControl<Event>, Error>
357where
358    Event: TryAsRef<crossterm::event::Event>,
359    Error: 'static,
360    Event: 'static,
361{
362    dialog_stack.handle(event, ctx)
363}