use crate::event::{FocusManager, FocusTrap};
use crate::render::Cell;
use crate::style::Color;
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
#[derive(Clone)]
pub struct ModalButton {
pub label: String,
pub style: ModalButtonStyle,
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum ModalButtonStyle {
#[default]
Default,
Primary,
Danger,
}
impl ModalButton {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
style: ModalButtonStyle::Default,
}
}
pub fn primary(label: impl Into<String>) -> Self {
Self {
label: label.into(),
style: ModalButtonStyle::Primary,
}
}
pub fn danger(label: impl Into<String>) -> Self {
Self {
label: label.into(),
style: ModalButtonStyle::Danger,
}
}
pub fn style(mut self, style: ModalButtonStyle) -> Self {
self.style = style;
self
}
}
pub struct Modal {
title: String,
content: Vec<String>,
body: Option<Box<dyn View>>,
buttons: Vec<ModalButton>,
selected_button: usize,
visible: bool,
width: u16,
height: Option<u16>,
title_fg: Option<Color>,
border_fg: Option<Color>,
props: WidgetProps,
focus_trap: Option<FocusTrap>,
}
impl Modal {
pub fn new() -> Self {
Self {
title: String::new(),
content: Vec::new(),
body: None,
buttons: Vec::new(),
selected_button: 0,
visible: false,
width: 40,
height: None,
title_fg: Some(Color::WHITE),
border_fg: Some(Color::WHITE),
props: WidgetProps::new(),
focus_trap: None,
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
pub fn content(mut self, content: impl Into<String>) -> Self {
self.content = content.into().lines().map(|s| s.to_string()).collect();
self
}
pub fn line(mut self, line: impl Into<String>) -> Self {
self.content.push(line.into());
self
}
pub fn buttons(mut self, buttons: Vec<ModalButton>) -> Self {
self.buttons = buttons;
self
}
pub fn ok(mut self) -> Self {
self.buttons.push(ModalButton::primary("OK"));
self
}
pub fn cancel(mut self) -> Self {
self.buttons.push(ModalButton::new("Cancel"));
self
}
pub fn ok_cancel(mut self) -> Self {
self.buttons.push(ModalButton::primary("OK"));
self.buttons.push(ModalButton::new("Cancel"));
self
}
pub fn yes_no(mut self) -> Self {
self.buttons.push(ModalButton::primary("Yes"));
self.buttons.push(ModalButton::new("No"));
self
}
pub fn yes_no_cancel(mut self) -> Self {
self.buttons.push(ModalButton::primary("Yes"));
self.buttons.push(ModalButton::new("No"));
self.buttons.push(ModalButton::new("Cancel"));
self
}
pub fn width(mut self, width: u16) -> Self {
self.width = width;
self
}
pub fn height(mut self, height: u16) -> Self {
self.height = Some(height);
self
}
pub fn body(mut self, widget: impl View + 'static) -> Self {
self.body = Some(Box::new(widget));
self
}
pub fn title_fg(mut self, color: Color) -> Self {
self.title_fg = Some(color);
self
}
pub fn border_fg(mut self, color: Color) -> Self {
self.border_fg = Some(color);
self
}
pub fn show(&mut self) {
self.visible = true;
}
pub fn show_with_focus_trap(
&mut self,
fm: &mut FocusManager,
container_id: u64,
button_ids: &[u64],
) {
self.visible = true;
let mut trap = FocusTrap::new(container_id).with_children(button_ids);
trap.activate(fm);
self.focus_trap = Some(trap);
}
pub fn hide(&mut self) {
self.visible = false;
}
pub fn hide_with_focus_restore(&mut self, fm: &mut FocusManager) {
self.visible = false;
if let Some(mut trap) = self.focus_trap.take() {
trap.deactivate(fm);
}
}
pub fn has_focus_trap(&self) -> bool {
self.focus_trap.as_ref().is_some_and(|t| t.is_active())
}
pub fn toggle(&mut self) {
self.visible = !self.visible;
}
pub fn is_visible(&self) -> bool {
self.visible
}
pub fn selected_button(&self) -> usize {
self.selected_button
}
pub fn next_button(&mut self) {
if !self.buttons.is_empty() {
self.selected_button = (self.selected_button + 1) % self.buttons.len();
}
}
pub fn prev_button(&mut self) {
if !self.buttons.is_empty() {
self.selected_button = self
.selected_button
.checked_sub(1)
.unwrap_or(self.buttons.len() - 1);
}
}
pub fn handle_key(&mut self, key: &crate::event::Key) -> Option<usize> {
use crate::event::Key;
match key {
Key::Enter | Key::Char(' ') => {
if !self.buttons.is_empty() {
Some(self.selected_button)
} else {
None
}
}
Key::Left | Key::Char('h') => {
self.prev_button();
None
}
Key::Right | Key::Char('l') => {
self.next_button();
None
}
Key::Tab => {
self.next_button();
None
}
Key::Escape => {
self.hide();
None
}
_ => None,
}
}
pub fn handle_key_with_focus(
&mut self,
key: &crate::event::Key,
fm: &mut FocusManager,
) -> Option<usize> {
let result = self.handle_key(key);
match key {
crate::event::Key::Escape => {
if let Some(mut trap) = self.focus_trap.take() {
trap.deactivate(fm);
}
}
crate::event::Key::Enter | crate::event::Key::Char(' ') => {
if result.is_some() {
if let Some(mut trap) = self.focus_trap.take() {
trap.deactivate(fm);
}
}
}
_ => {}
}
result
}
pub fn alert(title: impl Into<String>, message: impl Into<String>) -> Self {
Self::new().title(title).content(message).ok()
}
pub fn confirm(title: impl Into<String>, message: impl Into<String>) -> Self {
Self::new().title(title).content(message).yes_no()
}
pub fn error(message: impl Into<String>) -> Self {
Self::new()
.title("Error")
.title_fg(Color::RED)
.border_fg(Color::RED)
.content(message)
.ok()
}
pub fn warning(message: impl Into<String>) -> Self {
Self::new()
.title("Warning")
.title_fg(Color::YELLOW)
.border_fg(Color::YELLOW)
.content(message)
.ok()
}
#[doc(hidden)]
pub fn required_height(&self) -> u16 {
if let Some(h) = self.height {
return h;
}
let content_lines = if self.body.is_some() {
5u16
} else {
self.content.len() as u16
};
let button_line = if self.buttons.is_empty() { 0 } else { 1 };
3 + content_lines + 1 + button_line + 1
}
#[doc(hidden)]
pub fn get_title(&self) -> &str {
&self.title
}
#[doc(hidden)]
pub fn get_content(&self) -> &[String] {
&self.content
}
#[doc(hidden)]
pub fn get_buttons(&self) -> &[ModalButton] {
&self.buttons
}
#[doc(hidden)]
pub fn get_body(&self) -> bool {
self.body.is_some()
}
#[doc(hidden)]
pub fn get_height(&self) -> Option<u16> {
self.height
}
#[doc(hidden)]
pub fn get_title_fg(&self) -> Option<Color> {
self.title_fg
}
#[doc(hidden)]
pub fn get_border_fg(&self) -> Option<Color> {
self.border_fg
}
}
impl Default for Modal {
fn default() -> Self {
Self::new()
}
}
impl View for Modal {
fn render(&self, ctx: &mut RenderContext) {
if !self.visible {
return;
}
let area = ctx.area;
let modal_width = self.width.min(area.width.saturating_sub(4));
let modal_height = self.required_height().min(area.height.saturating_sub(2));
let x = (area.width.saturating_sub(modal_width)) / 2;
let y = (area.height.saturating_sub(modal_height)) / 2;
self.render_border(ctx, x, y, modal_width, modal_height);
if !self.title.is_empty() && modal_width > 4 {
let title_x = x + 2;
let title_width = modal_width.saturating_sub(4);
let title_fg = self.title_fg.unwrap_or(Color::WHITE);
ctx.draw_text_clipped_bold(title_x, y + 1, &self.title, title_fg, title_width);
for dx in 1..modal_width.saturating_sub(1) {
ctx.set(x + dx, y + 2, Cell::new('─'));
}
ctx.set(x, y + 2, Cell::new('├'));
ctx.set(x + modal_width.saturating_sub(1), y + 2, Cell::new('┤'));
}
let has_title = !self.title.is_empty() && modal_width > 4;
let content_y = if has_title { y + 3 } else { y + 1 };
let content_width = modal_width.saturating_sub(4);
let content_height = if has_title {
modal_height.saturating_sub(6)
} else {
modal_height.saturating_sub(4)
};
if let Some(ref body_widget) = self.body {
let content_area = ctx.sub_area(x + 2, content_y, content_width, content_height);
let mut body_ctx = RenderContext::new(ctx.buffer, content_area);
body_widget.render(&mut body_ctx);
} else {
for (i, line) in self.content.iter().enumerate() {
let cy = content_y + i as u16;
if cy >= y + modal_height - 2 {
break;
}
ctx.draw_text_clipped(x + 2, cy, line, Color::rgb(220, 220, 220), content_width);
}
}
if !self.buttons.is_empty() && modal_height > 2 {
let button_y = y + modal_height.saturating_sub(2);
let total_button_width: usize = self
.buttons
.iter()
.map(|b| b.label.len() + 4) .sum::<usize>()
+ (self.buttons.len() - 1) * 2;
if total_button_width as u16 > modal_width {
return;
}
let start_x = x + (modal_width.saturating_sub(total_button_width as u16)) / 2;
let mut bx = start_x;
for (i, button) in self.buttons.iter().enumerate() {
let is_selected = i == self.selected_button;
let button_text = format!("[ {} ]", button.label);
let (fg, bg) = if is_selected {
match button.style {
ModalButtonStyle::Primary => (Some(Color::WHITE), Some(Color::BLUE)),
ModalButtonStyle::Danger => (Some(Color::WHITE), Some(Color::RED)),
ModalButtonStyle::Default => (Some(Color::BLACK), Some(Color::WHITE)),
}
} else {
(None, None)
};
let mut btn_x = bx;
for ch in button_text.chars() {
let cw = crate::utils::char_width(ch) as u16;
let mut cell = Cell::new(ch);
cell.fg = fg;
cell.bg = bg;
if is_selected {
cell.modifier |= crate::render::Modifier::BOLD;
}
ctx.set(btn_x, button_y, cell);
btn_x += cw;
}
bx = btn_x + 2;
}
}
}
crate::impl_view_meta!("Modal");
}
impl Modal {
fn render_border(&self, ctx: &mut RenderContext, x: u16, y: u16, width: u16, height: u16) {
if width < 2 || height < 2 {
return;
}
for dy in 1..height.saturating_sub(1) {
for dx in 1..width.saturating_sub(1) {
ctx.set(x + dx, y + dy, Cell::new(' '));
}
}
let mut corner = Cell::new('┌');
corner.fg = self.border_fg;
ctx.set(x, y, corner);
for dx in 1..width.saturating_sub(1) {
let mut cell = Cell::new('─');
cell.fg = self.border_fg;
ctx.set(x + dx, y, cell);
}
let mut corner = Cell::new('┐');
corner.fg = self.border_fg;
ctx.set(x + width.saturating_sub(1), y, corner);
for dy in 1..height.saturating_sub(1) {
let mut cell = Cell::new('│');
cell.fg = self.border_fg;
ctx.set(x, y + dy, cell);
ctx.set(x + width.saturating_sub(1), y + dy, cell);
}
let mut corner = Cell::new('└');
corner.fg = self.border_fg;
ctx.set(x, y + height.saturating_sub(1), corner);
for dx in 1..width.saturating_sub(1) {
let mut cell = Cell::new('─');
cell.fg = self.border_fg;
ctx.set(x + dx, y + height.saturating_sub(1), cell);
}
let mut corner = Cell::new('┘');
corner.fg = self.border_fg;
ctx.set(
x + width.saturating_sub(1),
y + height.saturating_sub(1),
corner,
);
}
}
pub fn modal() -> Modal {
Modal::new()
}
impl_styled_view!(Modal);
impl_props_builders!(Modal);
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::Rect;
use crate::render::Buffer;
#[test]
fn test_modal_new() {
let m = Modal::new();
assert!(!m.is_visible());
assert!(m.title.is_empty());
assert!(m.content.is_empty());
assert!(m.buttons.is_empty());
}
#[test]
fn test_modal_builder() {
let m = Modal::new()
.title("Test")
.content(
"Hello
World",
)
.ok_cancel();
assert_eq!(m.title, "Test");
assert_eq!(m.content.len(), 2);
assert_eq!(m.buttons.len(), 2);
}
#[test]
fn test_modal_visibility() {
let mut m = Modal::new();
assert!(!m.is_visible());
m.show();
assert!(m.is_visible());
m.hide();
assert!(!m.is_visible());
m.toggle();
assert!(m.is_visible());
}
#[test]
fn test_modal_button_navigation() {
let mut m = Modal::new().ok_cancel();
assert_eq!(m.selected_button(), 0);
m.next_button();
assert_eq!(m.selected_button(), 1);
m.next_button(); assert_eq!(m.selected_button(), 0);
m.prev_button(); assert_eq!(m.selected_button(), 1);
}
#[test]
fn test_modal_handle_key() {
use crate::event::Key;
let mut m = Modal::new().yes_no();
m.show();
m.handle_key(&Key::Right);
assert_eq!(m.selected_button(), 1);
m.handle_key(&Key::Left);
assert_eq!(m.selected_button(), 0);
let result = m.handle_key(&Key::Enter);
assert_eq!(result, Some(0));
m.handle_key(&Key::Escape);
assert!(!m.is_visible());
}
#[test]
fn test_modal_presets() {
let alert = Modal::alert("Title", "Message");
assert_eq!(alert.title, "Title");
assert_eq!(alert.buttons.len(), 1);
let confirm = Modal::confirm("Title", "Question?");
assert_eq!(confirm.buttons.len(), 2);
let error = Modal::error("Something went wrong");
assert_eq!(error.title, "Error");
}
#[test]
fn test_modal_render_hidden() {
let mut buffer = Buffer::new(80, 24);
let area = Rect::new(0, 0, 80, 24);
let mut ctx = RenderContext::new(&mut buffer, area);
let m = Modal::new().title("Test");
m.render(&mut ctx);
assert_eq!(buffer.get(0, 0).unwrap().symbol, ' ');
}
#[test]
fn test_modal_render_visible() {
let mut buffer = Buffer::new(80, 24);
let area = Rect::new(0, 0, 80, 24);
let mut ctx = RenderContext::new(&mut buffer, area);
let mut m = Modal::new().title("Test Dialog").content("Hello").ok();
m.show();
m.render(&mut ctx);
let center_x = (80 - 40) / 2;
let center_y = (24 - m.required_height()) / 2;
assert_eq!(buffer.get(center_x, center_y).unwrap().symbol, '┌');
}
#[test]
fn test_modal_button_styles() {
let btn = ModalButton::new("Test");
assert!(matches!(btn.style, ModalButtonStyle::Default));
let btn = ModalButton::primary("OK");
assert!(matches!(btn.style, ModalButtonStyle::Primary));
let btn = ModalButton::danger("Delete");
assert!(matches!(btn.style, ModalButtonStyle::Danger));
}
#[test]
fn test_modal_helper() {
let m = modal().title("Quick").ok();
assert_eq!(m.title, "Quick");
}
#[test]
fn test_modal_with_body() {
use crate::widget::Text;
let m = Modal::new()
.title("Form")
.body(Text::new("Custom content"))
.height(10)
.ok();
assert!(m.body.is_some());
assert_eq!(m.height, Some(10));
}
#[test]
fn test_modal_body_render() {
use crate::widget::Text;
let mut buffer = Buffer::new(80, 24);
let area = Rect::new(0, 0, 80, 24);
let mut ctx = RenderContext::new(&mut buffer, area);
let mut m = Modal::new()
.title("Body Test")
.body(Text::new("Widget content"))
.width(50)
.height(12)
.ok();
m.show();
m.render(&mut ctx);
let center_x = (80 - 50) / 2;
let center_y = (24 - 12) / 2;
assert_eq!(buffer.get(center_x, center_y).unwrap().symbol, '┌');
}
#[test]
fn test_modal_render_small_area_no_panic() {
let mut buffer = Buffer::new(80, 24);
let area = Rect::new(0, 0, 0, 24);
let mut ctx = RenderContext::new(&mut buffer, area);
let mut m = Modal::new().title("Test").ok();
m.show();
m.render(&mut ctx);
let mut buffer = Buffer::new(80, 24);
let area = Rect::new(0, 0, 1, 24);
let mut ctx = RenderContext::new(&mut buffer, area);
let mut m = Modal::new().title("Test").ok();
m.show();
m.render(&mut ctx);
let mut buffer = Buffer::new(80, 24);
let area = Rect::new(0, 0, 2, 24);
let mut ctx = RenderContext::new(&mut buffer, area);
let mut m = Modal::new().title("Test").ok();
m.show();
m.render(&mut ctx);
let mut buffer = Buffer::new(80, 24);
let area = Rect::new(0, 0, 80, 0);
let mut ctx = RenderContext::new(&mut buffer, area);
let mut m = Modal::new().title("Test").ok();
m.show();
m.render(&mut ctx);
let mut buffer = Buffer::new(80, 24);
let area = Rect::new(0, 0, 80, 1);
let mut ctx = RenderContext::new(&mut buffer, area);
let mut m = Modal::new().title("Test").ok();
m.show();
m.render(&mut ctx);
let mut buffer = Buffer::new(80, 24);
let area = Rect::new(0, 0, 80, 2);
let mut ctx = RenderContext::new(&mut buffer, area);
let mut m = Modal::new().title("Test").ok();
m.show();
m.render(&mut ctx);
let mut buffer = Buffer::new(80, 24);
let area = Rect::new(0, 0, 0, 0);
let mut ctx = RenderContext::new(&mut buffer, area);
let mut m = Modal::new().title("Test").ok();
m.show();
m.render(&mut ctx); }
#[test]
fn test_modal_render_width_2_border() {
let mut buffer = Buffer::new(10, 10);
let area = Rect::new(0, 0, 4, 10); let mut ctx = RenderContext::new(&mut buffer, area);
let mut m = Modal::new().title("X").width(2).height(4);
m.show();
m.render(&mut ctx); }
#[test]
fn test_modal_button_style_default() {
let style = ModalButtonStyle::default();
assert!(matches!(style, ModalButtonStyle::Default));
}
#[test]
fn test_modal_button_style_clone() {
let style1 = ModalButtonStyle::Primary;
let style2 = style1.clone();
assert_eq!(style1, style2);
}
#[test]
fn test_modal_button_style_copy() {
let style1 = ModalButtonStyle::Danger;
let style2 = style1;
assert_eq!(style2, ModalButtonStyle::Danger);
assert_eq!(style1, ModalButtonStyle::Danger);
}
#[test]
fn test_modal_button_style_partial_eq() {
assert_eq!(ModalButtonStyle::Default, ModalButtonStyle::Default);
assert_eq!(ModalButtonStyle::Primary, ModalButtonStyle::Primary);
assert_eq!(ModalButtonStyle::Danger, ModalButtonStyle::Danger);
assert_ne!(ModalButtonStyle::Default, ModalButtonStyle::Primary);
assert_ne!(ModalButtonStyle::Primary, ModalButtonStyle::Danger);
assert_ne!(ModalButtonStyle::Danger, ModalButtonStyle::Default);
}
#[test]
fn test_modal_button_style_all_variants() {
let styles = [
ModalButtonStyle::Default,
ModalButtonStyle::Primary,
ModalButtonStyle::Danger,
];
for (i, style1) in styles.iter().enumerate() {
for (j, style2) in styles.iter().enumerate() {
if i == j {
assert_eq!(style1, style2);
} else {
assert_ne!(style1, style2);
}
}
}
}
#[test]
fn test_modal_button_clone() {
let btn1 = ModalButton::new("Test").style(ModalButtonStyle::Primary);
let btn2 = btn1.clone();
assert_eq!(btn1.label, btn2.label);
assert_eq!(btn1.style, btn2.style);
}
#[test]
fn test_modal_button_new_with_string() {
let label = String::from("Owned Label");
let btn = ModalButton::new(label);
assert_eq!(btn.label, "Owned Label");
assert!(matches!(btn.style, ModalButtonStyle::Default));
}
#[test]
fn test_modal_button_new_with_str() {
let btn = ModalButton::new("Test Label");
assert_eq!(btn.label, "Test Label");
assert!(matches!(btn.style, ModalButtonStyle::Default));
}
#[test]
fn test_modal_button_empty_label() {
let btn = ModalButton::new("");
assert_eq!(btn.label, "");
}
#[test]
fn test_modal_button_primary_with_string() {
let label = String::from("Submit");
let btn = ModalButton::primary(label);
assert_eq!(btn.label, "Submit");
assert!(matches!(btn.style, ModalButtonStyle::Primary));
}
#[test]
fn test_modal_button_primary_with_str() {
let btn = ModalButton::primary("OK");
assert_eq!(btn.label, "OK");
assert!(matches!(btn.style, ModalButtonStyle::Primary));
}
#[test]
fn test_modal_button_danger_with_string() {
let label = String::from("Delete");
let btn = ModalButton::danger(label);
assert_eq!(btn.label, "Delete");
assert!(matches!(btn.style, ModalButtonStyle::Danger));
}
#[test]
fn test_modal_button_danger_with_str() {
let btn = ModalButton::danger("Cancel");
assert_eq!(btn.label, "Cancel");
assert!(matches!(btn.style, ModalButtonStyle::Danger));
}
#[test]
fn test_modal_button_all_distinct() {
let default_btn = ModalButton::new("Default");
let primary_btn = ModalButton::primary("Primary");
let danger_btn = ModalButton::danger("Danger");
assert!(matches!(default_btn.style, ModalButtonStyle::Default));
assert!(matches!(primary_btn.style, ModalButtonStyle::Primary));
assert!(matches!(danger_btn.style, ModalButtonStyle::Danger));
}
#[test]
fn test_modal_empty_title() {
let m = Modal::new().title("");
assert_eq!(m.title, "");
}
#[test]
fn test_modal_empty_content() {
let m = Modal::new().content("");
assert!(m.content.is_empty());
}
#[test]
fn test_modal_content_with_multiline() {
let m = Modal::new().content("Line 1\nLine 2\nLine 3");
assert_eq!(m.content.len(), 3);
assert_eq!(m.content[0], "Line 1");
assert_eq!(m.content[1], "Line 2");
assert_eq!(m.content[2], "Line 3");
}
#[test]
fn test_modal_line_multiple() {
let m = Modal::new().line("Line 1").line("Line 2").line("Line 3");
assert_eq!(m.content.len(), 3);
}
#[test]
fn test_modal_buttons_empty() {
let m = Modal::new().buttons(vec![]);
assert!(m.buttons.is_empty());
}
#[test]
fn test_modal_width_zero() {
let m = Modal::new().width(0);
assert_eq!(m.width, 0);
}
#[test]
fn test_modal_height_zero() {
let m = Modal::new().height(0);
assert_eq!(m.height, Some(0));
}
#[test]
fn test_modal_title_colors() {
let m = Modal::new().title_fg(Color::CYAN);
assert_eq!(m.title_fg, Some(Color::CYAN));
}
#[test]
fn test_modal_border_colors() {
let m = Modal::new().border_fg(Color::MAGENTA);
assert_eq!(m.border_fg, Some(Color::MAGENTA));
}
#[test]
fn test_modal_selected_button_initial() {
let m = Modal::new();
assert_eq!(m.selected_button(), 0);
}
#[test]
fn test_modal_next_button_empty() {
let mut m = Modal::new();
m.next_button(); assert_eq!(m.selected_button(), 0);
}
#[test]
fn test_modal_prev_button_empty() {
let mut m = Modal::new();
m.prev_button(); assert_eq!(m.selected_button(), 0);
}
#[test]
fn test_modal_handle_key_no_buttons() {
use crate::event::Key;
let mut m = Modal::new();
let result = m.handle_key(&Key::Enter);
assert_eq!(result, None);
}
#[test]
fn test_modal_handle_key_unknown() {
use crate::event::Key;
let mut m = Modal::new().ok();
let result = m.handle_key(&Key::Char('x'));
assert_eq!(result, None);
}
#[test]
fn test_modal_builder_chain_full() {
let m = Modal::new()
.title("Chain Title")
.content("Chain content")
.width(60)
.height(10)
.title_fg(Color::YELLOW)
.border_fg(Color::GREEN);
assert_eq!(m.title, "Chain Title");
assert_eq!(m.content.len(), 1);
assert_eq!(m.content[0], "Chain content");
assert_eq!(m.width, 60);
assert_eq!(m.height, Some(10));
assert_eq!(m.title_fg, Some(Color::YELLOW));
assert_eq!(m.border_fg, Some(Color::GREEN));
}
#[test]
fn test_modal_buttons_builder_chain() {
let buttons = vec![
ModalButton::new("One"),
ModalButton::primary("Two"),
ModalButton::danger("Three"),
];
let m = Modal::new().buttons(buttons.clone());
assert_eq!(m.buttons.len(), 3);
assert_eq!(m.buttons[0].label, "One");
assert_eq!(m.buttons[1].label, "Two");
assert_eq!(m.buttons[2].label, "Three");
}
#[test]
fn test_modal_show_with_focus_trap() {
let mut fm = FocusManager::new();
fm.register(1); fm.register(2); fm.register(10); fm.register(11); fm.focus(1);
let mut m = Modal::new().ok_cancel();
m.show_with_focus_trap(&mut fm, 100, &[10, 11]);
assert!(m.is_visible());
assert!(m.has_focus_trap());
assert!(fm.is_trapped());
assert_eq!(fm.current(), Some(10));
}
#[test]
fn test_modal_hide_with_focus_restore() {
let mut fm = FocusManager::new();
fm.register(1);
fm.register(2);
fm.register(10);
fm.register(11);
fm.focus(1);
let mut m = Modal::new().ok_cancel();
m.show_with_focus_trap(&mut fm, 100, &[10, 11]);
assert_eq!(fm.current(), Some(10));
m.hide_with_focus_restore(&mut fm);
assert!(!m.is_visible());
assert!(!m.has_focus_trap());
assert!(!fm.is_trapped());
assert_eq!(fm.current(), Some(1));
}
#[test]
fn test_modal_handle_key_with_focus_escape() {
use crate::event::Key;
let mut fm = FocusManager::new();
fm.register(1);
fm.register(10);
fm.register(11);
fm.focus(1);
let mut m = Modal::new().ok_cancel();
m.show_with_focus_trap(&mut fm, 100, &[10, 11]);
m.handle_key_with_focus(&Key::Escape, &mut fm);
assert!(!m.is_visible());
assert!(!fm.is_trapped());
assert_eq!(fm.current(), Some(1));
}
#[test]
fn test_modal_handle_key_with_focus_confirm() {
use crate::event::Key;
let mut fm = FocusManager::new();
fm.register(1);
fm.register(10);
fm.register(11);
fm.focus(1);
let mut m = Modal::new().ok_cancel();
m.show_with_focus_trap(&mut fm, 100, &[10, 11]);
let result = m.handle_key_with_focus(&Key::Enter, &mut fm);
assert_eq!(result, Some(0));
assert!(!fm.is_trapped());
}
#[test]
fn test_modal_focus_trap_tab_does_not_release() {
use crate::event::Key;
let mut fm = FocusManager::new();
fm.register(1);
fm.register(10);
fm.register(11);
fm.focus(1);
let mut m = Modal::new().ok_cancel();
m.show_with_focus_trap(&mut fm, 100, &[10, 11]);
m.handle_key_with_focus(&Key::Tab, &mut fm);
assert!(m.has_focus_trap());
assert!(fm.is_trapped());
}
#[test]
fn test_modal_no_focus_trap_by_default() {
let m = Modal::new().ok();
assert!(!m.has_focus_trap());
}
}