Skip to main content

ratatui_kit/components/
modal.rs

1// Modal 组件:模态弹窗,支持遮罩、居中/自定义位置、尺寸、样式等。
2//
3// ## 用法示例
4// ```rust
5// element!(Modal(
6//     open: open.get(),
7//     width: Constraint::Percentage(60),
8//     height: Constraint::Percentage(60),
9//     style: Style::default().dim(),
10// ){
11//     Border(top_title: Some(Line::from("弹窗内容"))) {
12//         // ...子内容
13//     }
14// })
15// ```
16// 通过 `open` 控制显示,`placement` 控制弹窗位置,`width/height` 控制尺寸。
17
18use ratatui::{
19    layout::{Constraint, Flex, Layout, Margin, Offset},
20    style::Style,
21    widgets::{Block, Clear, Widget},
22};
23use ratatui_kit_macros::{Props, with_layout_style};
24
25use crate::{
26    AnyElement, Component, Context, SystemContext,
27    input::{CurrentLayer, InputLayer},
28    layout_style::LayoutStyle,
29};
30
31#[derive(Default, Clone, Copy)]
32// 弹窗位置枚举。
33pub enum Placement {
34    Top,
35    TopLeft,
36    TopRight,
37    Bottom,
38    BottomLeft,
39    BottomRight,
40    #[default]
41    Center,
42    Left,
43    Right,
44}
45
46impl Placement {
47    pub fn to_flex(&self) -> [Flex; 2] {
48        match self {
49            Placement::Top => [Flex::Start, Flex::Center],
50            Placement::TopLeft => [Flex::Start, Flex::Start],
51            Placement::TopRight => [Flex::Start, Flex::End],
52            Placement::Bottom => [Flex::End, Flex::Center],
53            Placement::BottomLeft => [Flex::End, Flex::Start],
54            Placement::BottomRight => [Flex::End, Flex::End],
55            Placement::Center => [Flex::Center, Flex::Center],
56            Placement::Left => [Flex::Center, Flex::Start],
57            Placement::Right => [Flex::Center, Flex::End],
58        }
59    }
60}
61
62#[with_layout_style(margin, offset, width, height)]
63#[derive(Default, Props)]
64// Modal 组件属性。
65pub struct ModalProps<'a> {
66    // 弹窗内容。
67    pub children: Vec<AnyElement<'a>>,
68    // 弹窗样式。
69    pub style: Style,
70    // 弹窗位置。
71    pub placement: Placement,
72    // 是否显示弹窗。
73    pub open: bool,
74    // 外部注入的输入层句柄(父组件已 `use_input_layer` 时)。
75    //
76    // `None` → Modal 内部自开层(handler 全在 Modal 子树内的常见场景);
77    // `Some(h)` → 复用父级已登记的层(不重复登记),仅向子树注入 `CurrentLayer`——
78    // 用于「handler 注册在 Modal 父组件」的场景(父 `use_input_layer` + `use_event_handler(Layer(h))`)。
79    //
80    // **Footgun**:走 `Layer(h)` 路径时必须把 `h` 传进来,否则 Modal 自开新层会截断 `h` → 父级 handler 失聪。
81    pub layer: Option<InputLayer>,
82    // 是否截断更低层。`None` 视作 `true`(模态独占输入);非阻塞浮层可设 `Some(false)`。
83    pub blocks_lower: Option<bool>,
84}
85
86// Modal 组件实现。
87pub struct Modal {
88    pub open: bool,
89    pub margin: Margin,
90    pub offset: Offset,
91    pub width: Constraint,
92    pub height: Constraint,
93    pub placement: Placement,
94    pub style: Style,
95}
96
97impl Component for Modal {
98    type Props<'a> = ModalProps<'a>;
99    fn new(props: &Self::Props<'_>) -> Self {
100        Modal {
101            open: props.open,
102            margin: props.margin,
103            offset: props.offset,
104            width: props.width,
105            height: props.height,
106            style: props.style,
107            placement: props.placement,
108        }
109    }
110
111    fn update(
112        &mut self,
113        props: &mut Self::Props<'_>,
114        _hooks: crate::Hooks,
115        updater: &mut crate::ComponentUpdater,
116    ) {
117        self.open = props.open;
118        self.margin = props.margin;
119        self.offset = props.offset;
120        self.width = props.width;
121        self.height = props.height;
122        self.style = props.style;
123        self.placement = props.placement;
124
125        if self.open {
126            let blocks = props.blocks_lower.unwrap_or(true);
127            // 借用纪律:取 SystemContext 守卫拿 layer id 后【立即 drop】,再 update_children,
128            // 否则子树组件访问 SystemContext(use_input_layer / use_exit)会撞 AlreadyBorrowed。
129            let layer_id = match props.layer {
130                // 外部已登记该层(父级 use_input_layer):Modal 不重复 push,仅注入给子树。
131                Some(h) => h.id,
132                // 内部自开层(handler 全在 Modal 子树内):push 一个独占层。
133                None => {
134                    let mut sys = updater
135                        .get_context_mut::<SystemContext>()
136                        .expect("`SystemContext` missing (the root context always provides it)");
137                    sys.input.push_layer(true, blocks).id
138                }
139            };
140
141            // 给子树注入 CurrentLayer:子树内 use_event_handler(Current) 自动归属本层。
142            updater.update_children(
143                props.children.iter_mut(),
144                Some(Context::owned(CurrentLayer(layer_id))),
145            );
146        }
147
148        updater.set_layout_style(LayoutStyle {
149            width: Constraint::Length(0),
150            height: Constraint::Length(0),
151            ..Default::default()
152        });
153    }
154
155    fn draw(&mut self, drawer: &mut crate::ComponentDrawer<'_, '_>) {
156        if self.open {
157            // 根据终端尺寸计算弹窗尺寸和位置
158            let area = drawer.buffer_mut().area();
159            let area = area.inner(self.margin).offset(self.offset);
160
161            let block = Block::default().style(self.style);
162            block.render(area, drawer.buffer_mut());
163
164            let [v, h] = self.placement.to_flex();
165
166            let vertical = Layout::vertical([self.height]).flex(v).split(area)[0];
167            let horizontal = Layout::horizontal([self.width]).flex(h).split(vertical)[0];
168
169            // 清空弹窗区域
170            Clear.render(horizontal, drawer.buffer_mut());
171            drawer.area = horizontal;
172        }
173    }
174}