Skip to main content

textual_rs/widget/
screen.rs

1//! Screen types for the widget stack, including the modal screen wrapper.
2use std::cell::RefCell;
3
4use ratatui::{buffer::Buffer, layout::Rect};
5
6use super::{context::AppContext, Widget, WidgetId};
7
8/// A screen that blocks all keyboard and mouse input to screens beneath it
9/// while it is on top of the screen stack.
10///
11/// `ModalScreen` is a transparent wrapper — it owns one inner widget that
12/// becomes its only child. Size the inner widget with CSS (width, height,
13/// margin, align) to position it on screen.
14///
15/// Input blocking is guaranteed by the framework: keyboard focus is always
16/// scoped to the top screen, and the mouse hit-map is built from the top
17/// screen only. Screens below a modal are frozen but not unmounted.
18///
19/// # Usage
20///
21/// Push a modal from any `on_action` handler using
22/// [`AppContext::push_screen_deferred`](crate::widget::context::AppContext::push_screen_deferred):
23///
24/// ```no_run
25/// # use textual_rs::widget::screen::ModalScreen;
26/// # use textual_rs::widget::context::AppContext;
27/// # use textual_rs::Widget;
28/// # use ratatui::{buffer::Buffer, layout::Rect};
29/// struct ConfirmDialog;
30/// impl Widget for ConfirmDialog {
31///     fn widget_type_name(&self) -> &'static str { "ConfirmDialog" }
32///     fn render(&self, _ctx: &AppContext, _area: Rect, _buf: &mut Buffer) {}
33///     fn on_action(&self, action: &str, ctx: &AppContext) {
34///         if action == "ok" || action == "cancel" {
35///             ctx.pop_screen_deferred(); // dismiss the modal
36///         }
37///     }
38/// }
39///
40/// struct MainScreen;
41/// impl Widget for MainScreen {
42///     fn widget_type_name(&self) -> &'static str { "MainScreen" }
43///     fn render(&self, _ctx: &AppContext, _area: Rect, _buf: &mut Buffer) {}
44///     fn on_action(&self, action: &str, ctx: &AppContext) {
45///         if action == "open_confirm" {
46///             ctx.push_screen_deferred(Box::new(ModalScreen::new(Box::new(ConfirmDialog))));
47///         }
48///     }
49/// }
50/// ```
51///
52/// # Dismissing a modal
53///
54/// Call [`AppContext::pop_screen_deferred`](crate::widget::context::AppContext::pop_screen_deferred)
55/// from within the inner widget's `on_action`. Focus automatically returns to
56/// the widget that was focused before the modal was opened.
57pub struct ModalScreen {
58    /// Inner screen widget. Moved into compose() on first call via RefCell.
59    inner: RefCell<Option<Box<dyn Widget>>>,
60    own_id: std::cell::Cell<Option<WidgetId>>,
61}
62
63impl ModalScreen {
64    /// Create a new ModalScreen wrapping the given inner widget.
65    pub fn new(inner: Box<dyn Widget>) -> Self {
66        Self {
67            inner: RefCell::new(Some(inner)),
68            own_id: std::cell::Cell::new(None),
69        }
70    }
71}
72
73impl Widget for ModalScreen {
74    fn widget_type_name(&self) -> &'static str {
75        "ModalScreen"
76    }
77
78    fn is_modal(&self) -> bool {
79        true
80    }
81
82    fn on_mount(&self, id: WidgetId) {
83        self.own_id.set(Some(id));
84    }
85
86    fn on_unmount(&self, _id: WidgetId) {
87        self.own_id.set(None);
88    }
89
90    /// Returns the inner widget as a child. Called once at mount time.
91    fn compose(&self) -> Vec<Box<dyn Widget>> {
92        if let Some(inner) = self.inner.borrow_mut().take() {
93            vec![inner]
94        } else {
95            vec![]
96        }
97    }
98
99    fn render(&self, _ctx: &AppContext, _area: Rect, _buf: &mut Buffer) {
100        // ModalScreen is a transparent container — layout and rendering happen
101        // in the inner widget returned from compose().
102    }
103}