use crossterm::event::{Event, KeyCode, KeyEventKind, MouseButton, MouseEventKind};
use ratatui::{
layout::{Alignment, Constraint},
style::Style,
text::Line,
widgets::Padding,
};
use ratatui_kit_macros::{Props, component, element};
use std::sync::{Arc, Mutex};
use crate::{
AnyElement, ComponentTheme, Handler, Hooks, Palette, UseEventHandler, UseInputLayer, UseTheme,
components::theme::resolve_style,
components::{Border, Modal, Text, TextParagraph},
input::{EventOptions, EventPriority, EventResult, EventScope},
};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AlertModalTheme {
pub border_style: Style,
pub title_style: Style,
pub message_style: Style,
}
impl ComponentTheme for AlertModalTheme {
fn from_palette(palette: &Palette) -> Self {
Self {
border_style: Style::new().fg(palette.warning),
title_style: Style::new().fg(palette.warning),
message_style: Style::new().fg(palette.fg),
}
}
}
impl Default for AlertModalTheme {
fn default() -> Self {
Self::from_palette(&Palette::default())
}
}
#[derive(Props)]
pub struct AlertModalProps {
pub open: bool,
pub title: Line<'static>,
pub message: TextParagraph<'static>,
pub close_hint: Option<Line<'static>>,
pub close_keys: Vec<KeyCode>,
pub on_close: Handler<'static, ()>,
pub width: Constraint,
pub height: Constraint,
pub style: Option<Style>,
pub border_style: Option<Style>,
pub title_style: Option<Style>,
pub message_style: Option<Style>,
pub padding: Padding,
}
impl Default for AlertModalProps {
fn default() -> Self {
Self {
open: false,
title: Line::from("Alert"),
message: TextParagraph::from(""),
close_hint: Some(Line::from("Esc / Enter").centered()),
close_keys: vec![KeyCode::Esc, KeyCode::Enter],
on_close: Handler::default(),
width: Constraint::Percentage(50),
height: Constraint::Length(6),
style: None,
border_style: None,
title_style: None,
message_style: None,
padding: Padding::uniform(1),
}
}
}
#[component]
pub fn AlertModal(props: &mut AlertModalProps, mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
let layer = hooks.use_input_layer(props.open, true);
let close_keys = props.close_keys.clone();
let on_close = Arc::new(Mutex::new(props.on_close.take()));
hooks.use_event_handler(EventScope::Layer(layer), EventPriority::High, {
let on_close = Arc::clone(&on_close);
move |event| {
let Event::Key(key) = event else {
return EventResult::Ignored;
};
if key.kind == KeyEventKind::Press && close_keys.contains(&key.code) {
(on_close.lock().unwrap())(());
}
EventResult::Consumed
}
});
hooks.use_event_handler_with_options(
EventScope::Layer(layer),
EventPriority::Low,
EventOptions { hit_test: false },
{
let on_close = Arc::clone(&on_close);
move |event| {
if let Event::Mouse(m) = event
&& m.kind == MouseEventKind::Down(MouseButton::Left)
{
(on_close.lock().unwrap())(());
return EventResult::Consumed;
}
EventResult::Ignored
}
},
);
let theme = hooks.use_component_theme::<AlertModalTheme>();
let border_style = resolve_style(theme.border_style, props.border_style);
let title_style = resolve_style(theme.title_style, props.title_style);
let message_style = resolve_style(theme.message_style, props.message_style);
element!(Modal(
open: props.open,
layer: Some(layer),
width: props.width,
height: props.height,
style: props.style,
) {
Border(
border_style: border_style,
top_title: props.title.clone().style(title_style).centered(),
bottom_title: props.close_hint.clone(),
padding: props.padding,
) {
Text(
text: props.message.clone(),
style: message_style,
alignment: Alignment::Center,
wrap: true,
)
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mouse_test::Mounted;
use crossterm::event::{KeyCode, MouseButton, MouseEventKind};
use std::sync::{Arc, Mutex};
type SyncLog = Arc<Mutex<Vec<String>>>;
#[derive(Clone, Default, Props)]
struct HarnessProps {
log: SyncLog,
}
#[component]
fn Harness(props: &HarnessProps, _hooks: Hooks) -> impl Into<AnyElement<'static>> {
let log = props.log.clone();
element!(AlertModal(
open: true,
on_close: move |_| log.lock().unwrap().push("close".into()),
))
}
#[test]
fn scrim_click_and_esc_close() {
let log: SyncLog = Default::default();
let mut m = Mounted::mount(element!(Harness(log: log.clone())), true);
m.dispatch(MouseEventKind::Down(MouseButton::Left), 0, 0);
assert_eq!(*log.lock().unwrap(), vec!["close".to_string()]);
m.dispatch_key(KeyCode::Esc);
assert_eq!(
*log.lock().unwrap(),
vec!["close".to_string(), "close".to_string()]
);
}
#[test]
fn wheel_over_modal_does_not_close() {
let log: SyncLog = Default::default();
let mut m = Mounted::mount(element!(Harness(log: log.clone())), true);
m.dispatch(MouseEventKind::ScrollDown, 20, 4);
assert!(log.lock().unwrap().is_empty());
}
}