Skip to main content

ratatui_kit/components/
alert_modal.rs

1// AlertModal 组件:带输入互斥的提示弹窗。
2
3use crossterm::event::{Event, KeyCode, KeyEventKind};
4use ratatui::{
5    layout::{Alignment, Constraint},
6    style::Style,
7    text::Line,
8    widgets::Padding,
9};
10use ratatui_kit_macros::{Props, component, element};
11
12use crate::{
13    AnyElement, ComponentTheme, Handler, Hooks, Palette, UseEventHandler, UseInputLayer, UseTheme,
14    components::theme::resolve_style,
15    components::{Border, Modal, Text, TextParagraph},
16    input::{EventPriority, EventResult, EventScope},
17};
18
19/// AlertModal 组件的主题 slot。提示语义 → 边框/标题取 `warning` 色;遮罩委托给 [`Modal`]。
20#[non_exhaustive]
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct AlertModalTheme {
23    /// 边框样式。
24    pub border_style: Style,
25    /// 标题样式。
26    pub title_style: Style,
27    /// 正文样式。
28    pub message_style: Style,
29}
30
31impl ComponentTheme for AlertModalTheme {
32    fn from_palette(palette: &Palette) -> Self {
33        Self {
34            border_style: Style::new().fg(palette.warning),
35            title_style: Style::new().fg(palette.warning),
36            message_style: Style::new().fg(palette.fg),
37        }
38    }
39}
40
41impl Default for AlertModalTheme {
42    fn default() -> Self {
43        Self::from_palette(&Palette::default())
44    }
45}
46
47#[derive(Props)]
48pub struct AlertModalProps {
49    pub open: bool,
50    pub title: Line<'static>,
51    pub message: TextParagraph<'static>,
52    pub close_hint: Option<Line<'static>>,
53    pub close_keys: Vec<KeyCode>,
54    pub on_close: Handler<'static, ()>,
55    pub width: Constraint,
56    pub height: Constraint,
57    // 遮罩样式覆盖(透传给 `Modal`)。`None` 用 `ModalTheme`(默认 DIM)。
58    pub style: Option<Style>,
59    // 以下样式覆盖:`None` 用 `AlertModalTheme`,`Some(s)` 以 `theme.patch(s)` 覆盖。
60    pub border_style: Option<Style>,
61    pub title_style: Option<Style>,
62    pub message_style: Option<Style>,
63    pub padding: Padding,
64}
65
66impl Default for AlertModalProps {
67    fn default() -> Self {
68        Self {
69            open: false,
70            title: Line::from("Alert"),
71            message: TextParagraph::from(""),
72            close_hint: Some(Line::from("Esc / Enter").centered()),
73            close_keys: vec![KeyCode::Esc, KeyCode::Enter],
74            on_close: Handler::default(),
75            width: Constraint::Percentage(50),
76            height: Constraint::Length(6),
77            style: None,
78            border_style: None,
79            title_style: None,
80            message_style: None,
81            padding: Padding::uniform(1),
82        }
83    }
84}
85
86#[component]
87pub fn AlertModal(props: &mut AlertModalProps, mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
88    let layer = hooks.use_input_layer(props.open, true);
89    let close_keys = props.close_keys.clone();
90    let mut on_close = props.on_close.take();
91
92    hooks.use_event_handler(
93        EventScope::Layer(layer),
94        EventPriority::High,
95        move |event| {
96            if let Event::Key(key) = event
97                && key.kind == KeyEventKind::Press
98                && close_keys.contains(&key.code)
99            {
100                on_close(());
101            }
102            EventResult::Consumed
103        },
104    );
105
106    // 主题解析:每个 slot 铺底,对应 props 的 Option<Style> 在上 patch(None → 用主题)。
107    let theme = hooks.use_component_theme::<AlertModalTheme>();
108    let border_style = resolve_style(theme.border_style, props.border_style);
109    let title_style = resolve_style(theme.title_style, props.title_style);
110    let message_style = resolve_style(theme.message_style, props.message_style);
111
112    element!(Modal(
113        open: props.open,
114        layer: Some(layer),
115        width: props.width,
116        height: props.height,
117        style: props.style,
118    ) {
119        Border(
120            border_style: border_style,
121            top_title: props.title.clone().style(title_style).centered(),
122            bottom_title: props.close_hint.clone(),
123            padding: props.padding,
124        ) {
125            Text(
126                text: props.message.clone(),
127                style: message_style,
128                alignment: Alignment::Center,
129                wrap: true,
130            )
131        }
132    })
133}