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::{AnyElement, Component, layout_style::LayoutStyle};
26
27#[derive(Default, Clone, Copy)]
28/// 弹窗位置枚举。
29pub enum Placement {
30    Top,
31    TopLeft,
32    TopRight,
33    Bottom,
34    BottomLeft,
35    BottomRight,
36    #[default]
37    Center,
38    Left,
39    Right,
40}
41
42impl Placement {
43    pub fn to_flex(&self) -> [Flex; 2] {
44        match self {
45            Placement::Top => [Flex::Start, Flex::Center],
46            Placement::TopLeft => [Flex::Start, Flex::Start],
47            Placement::TopRight => [Flex::Start, Flex::End],
48            Placement::Bottom => [Flex::End, Flex::Center],
49            Placement::BottomLeft => [Flex::End, Flex::Start],
50            Placement::BottomRight => [Flex::End, Flex::End],
51            Placement::Center => [Flex::Center, Flex::Center],
52            Placement::Left => [Flex::Center, Flex::Start],
53            Placement::Right => [Flex::Center, Flex::End],
54        }
55    }
56}
57
58#[with_layout_style(margin, offset, width, height)]
59#[derive(Default, Props)]
60/// Modal 组件属性。
61pub struct ModalProps<'a> {
62    /// 弹窗内容。
63    pub children: Vec<AnyElement<'a>>,
64    /// 弹窗样式。
65    pub style: Style,
66    /// 弹窗位置。
67    pub placement: Placement,
68    /// 是否显示弹窗。
69    pub open: bool,
70}
71
72/// Modal 组件实现。
73pub struct Modal {
74    pub open: bool,
75    pub margin: Margin,
76    pub offset: Offset,
77    pub width: Constraint,
78    pub height: Constraint,
79    pub placement: Placement,
80    pub style: Style,
81}
82
83impl Component for Modal {
84    type Props<'a> = ModalProps<'a>;
85    fn new(props: &Self::Props<'_>) -> Self {
86        Modal {
87            open: props.open,
88            margin: props.margin,
89            offset: props.offset,
90            width: props.width,
91            height: props.height,
92            style: props.style,
93            placement: props.placement,
94        }
95    }
96
97    fn update(
98        &mut self,
99        props: &mut Self::Props<'_>,
100        _hooks: crate::Hooks,
101        updater: &mut crate::ComponentUpdater,
102    ) {
103        self.open = props.open;
104        self.margin = props.margin;
105        self.offset = props.offset;
106        self.width = props.width;
107        self.height = props.height;
108        self.style = props.style;
109        self.placement = props.placement;
110
111        if self.open {
112            updater.update_children(props.children.iter_mut(), None);
113        }
114
115        updater.set_layout_style(LayoutStyle {
116            width: Constraint::Percentage(0),
117            height: Constraint::Percentage(0),
118            ..Default::default()
119        });
120    }
121
122    fn draw(&mut self, drawer: &mut crate::ComponentDrawer<'_, '_>) {
123        if self.open {
124            let area = drawer.buffer_mut().area();
125            let area = area.inner(self.margin).offset(self.offset);
126            let block = Block::default().style(self.style);
127            block.render(area, drawer.buffer_mut());
128
129            let [v, h] = self.placement.to_flex();
130
131            let vertical = Layout::vertical([self.height]).flex(v).split(area)[0];
132            let horizontal = Layout::horizontal([self.width]).flex(h).split(vertical)[0];
133
134            Clear.render(horizontal, drawer.buffer_mut());
135            drawer.area = horizontal;
136        }
137    }
138}