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}