use rat_theme4::WidgetStyle;
use rat_theme4::theme::SalsaTheme;
use rat_widget::event::{Dialog, HandleEvent, Outcome};
use rat_widget::msgdialog::{MsgDialog, MsgDialogState, MsgDialogStyle};
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::{Constraint, Rect};
use ratatui_core::style::Style;
use ratatui_core::widgets::StatefulWidget;
use ratatui_crossterm::crossterm::event::Event;
use crate::overlay::Overlay;
use crate::vocabulary::Reply;
const MAX_WIDTH: u16 = 40;
pub(crate) const TEXT_WIDTH: usize = 35;
const CHROME_HEIGHT: u16 = 7;
pub(crate) fn draw(
area: Rect,
buf: &mut Buffer,
dialog: &mut Overlay<MsgDialogState>,
theme: &SalsaTheme,
) {
let Some(state) = dialog.get_mut() else {
return;
};
let rows = state.message.borrow().lines().count().max(1);
let height = u16::try_from(rows)
.unwrap_or(u16::MAX)
.saturating_add(CHROME_HEIGHT);
let pop_up = area.centered(Constraint::Max(MAX_WIDTH), Constraint::Max(height));
MsgDialog::new()
.styles(dialog_style(theme))
.render(pop_up, buf, state);
}
pub(crate) fn handle(event: &Event, dialog: &mut Overlay<MsgDialogState>) -> Reply {
let Some(state) = dialog.get_mut() else {
return Reply::Ignored;
};
let reply = match state.handle(event, Dialog) {
Outcome::Continue => Reply::Ignored,
Outcome::Unchanged => Reply::Consumed,
Outcome::Changed => Reply::Repaint,
};
if !state.active() {
dialog.close();
}
reply
}
fn dialog_style(theme: &SalsaTheme) -> MsgDialogStyle {
let mut style: MsgDialogStyle = theme.style(WidgetStyle::MSG_DIALOG);
if let Some(button) = style.button.as_mut() {
button.focus = Some(Style::new().reversed());
button.armed = Some(Style::new().reversed());
button.hover = None;
}
style
}
#[cfg(test)]
pub(crate) mod testing {
use rat_theme4::theme::SalsaTheme;
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use crate::config::Config;
pub(crate) const REGION: Rect = Rect::new(0, 1, 40, 8);
pub(crate) fn theme() -> SalsaTheme {
Config::default().theme
}
pub(crate) fn picture(buffer: &Buffer) -> String {
(buffer.area.top()..buffer.area.bottom())
.map(|line| {
(buffer.area.left()..buffer.area.right())
.map(|column| buffer[(column, line)].symbol().to_string())
.collect::<String>()
.trim_end()
.to_string()
})
.collect::<Vec<String>>()
.join("\n")
}
}
#[cfg(test)]
mod tests {
use super::testing::{REGION, theme};
use super::*;
use rat_widget::button::{Button, ButtonState};
use ratatui_core::style::Modifier;
use ratatui_crossterm::crossterm::event::{
Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind,
};
fn drawn() -> (Overlay<MsgDialogState>, Buffer) {
let mut dialog = Overlay::up(MsgDialogState::new_active("Title", "A message"));
let mut buffer = Buffer::empty(REGION);
draw(REGION, &mut buffer, &mut dialog, &theme());
(dialog, buffer)
}
fn reversed(buffer: &Buffer, column: u16, line: u16) -> bool {
buffer[(column, line)]
.style()
.add_modifier
.contains(Modifier::REVERSED)
}
fn key(code: KeyCode) -> Event {
Event::Key(KeyEvent::new(code, KeyModifiers::NONE))
}
fn moved(column: u16, row: u16) -> Event {
Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
column,
row,
modifiers: KeyModifiers::NONE,
})
}
#[test]
fn the_three_answers_become_the_routers_reply() {
let mut down = Overlay::Down;
assert_eq!(
handle(&key(KeyCode::Esc), &mut down),
Reply::Ignored,
"a dialog that is down answered the event"
);
let (mut up, _) = drawn();
assert_eq!(
handle(&key(KeyCode::Right), &mut up),
Reply::Consumed,
"a key the dialog kept without a change owed a frame"
);
let (mut closing, _) = drawn();
assert_eq!(
handle(&key(KeyCode::Esc), &mut closing),
Reply::Repaint,
"the escape that closes the dialog owed no frame"
);
}
#[test]
fn tab_moves_from_the_message_to_the_ok_and_the_ok_is_reversed() {
let (mut state, buffer) = drawn();
let (column, line) = crate::frame::cell_of(&buffer, "Ok");
assert!(
!reversed(&buffer, column, line),
"the Ok started focused, so the test proves nothing"
);
let _ = handle(&key(KeyCode::Tab), &mut state);
let mut after = Buffer::empty(REGION);
draw(REGION, &mut after, &mut state, &theme());
assert!(
reversed(&after, column, line),
"Tab did not move the focus to the Ok"
);
}
#[test]
fn a_control_under_the_pointer_is_not_a_state() {
let (mut state, buffer) = drawn();
let (column, line) = crate::frame::cell_of(&buffer, "Ok");
let _ = handle(&moved(column, line), &mut state);
let mut after = Buffer::empty(REGION);
draw(REGION, &mut after, &mut state, &theme());
assert!(
!reversed(&after, column, line),
"the pointer over the Ok was a state of the control"
);
}
#[test]
fn a_pressed_ok_that_is_not_focused_is_reversed() {
let style = dialog_style(&theme())
.button
.expect("the theme gives the dialog a button");
assert!(
style.hover.is_none(),
"the pointer over the Ok is still a style"
);
let area = Rect::new(0, 0, 10, 1);
let mut buffer = Buffer::empty(area);
let mut state = ButtonState {
armed: true,
..ButtonState::default()
};
state.focus.set(false);
Button::new("Ok")
.styles(style)
.render(area, &mut buffer, &mut state);
let (column, line) = crate::frame::cell_of(&buffer, "Ok");
assert!(
reversed(&buffer, column, line),
"a pressed Ok that is not focused is not reversed"
);
}
#[test]
fn a_message_of_several_lines_is_shown_whole() {
let tall = Rect::new(0, 0, 40, 20);
let mut state = Overlay::up(MsgDialogState::new_active(
"Title",
"line one\nline two\nline three",
));
let mut buffer = Buffer::empty(tall);
draw(tall, &mut buffer, &mut state, &theme());
let frame = testing::picture(&buffer);
for line in ["line one", "line two", "line three"] {
assert!(
frame.contains(line),
"the dialog hid {line:?} although the region had room: {frame:?}"
);
}
}
}