use super::theme::MissionControlTheme;
use ratatui::{
Frame,
layout::Rect,
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, BorderType, Borders, Clear},
};
pub(crate) fn modal_area(area: Rect, width_percent: u16, height_percent: u16) -> Rect {
compact_modal_area(
area,
width_percent.clamp(30, 80),
height_percent.clamp(30, 80),
)
}
pub(crate) fn compact_modal_area(area: Rect, width_percent: u16, height_percent: u16) -> Rect {
let width = (u32::from(area.width) * u32::from(width_percent.min(100)) / 100) as u16;
let height = (u32::from(area.height) * u32::from(height_percent.min(100)) / 100) as u16;
Rect::new(
area.x.saturating_add((area.width - width) / 2),
area.y.saturating_add((area.height - height) / 2),
width,
height,
)
}
pub(crate) fn modal_block<'a>(
title: impl Into<Line<'a>>,
hotkeys: impl Into<Line<'a>>,
theme: MissionControlTheme,
close_hint: Option<&'a str>,
) -> Block<'a> {
let mut block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Plain)
.style(theme.app())
.title_top(title.into().left_aligned())
.title_bottom(hotkeys.into().left_aligned());
if let Some(hint) = close_hint {
block = block.title_top(Line::from(hint).right_aligned());
}
block
}
pub(crate) fn draw_modal_container<'a>(
frame: &mut Frame<'_>,
area: Rect,
title: impl Into<Line<'a>>,
hotkeys: impl Into<Line<'a>>,
theme: MissionControlTheme,
close_hint: Option<&'a str>,
) -> Rect {
let block = modal_block(title, hotkeys, theme, close_hint);
let inner = block.inner(area);
frame.render_widget(Clear, area);
frame.render_widget(block, area);
inner
}
pub(crate) fn modal_tab(label: &str, selected: bool, theme: MissionControlTheme) -> Span<'static> {
if selected {
Span::styled(
format!("[{label}]"),
Style::default()
.fg(theme.text_accent())
.bg(theme.surface_selection())
.add_modifier(Modifier::BOLD),
)
} else {
Span::styled(format!(" {label} "), theme.muted(false))
}
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::{Terminal, backend::TestBackend};
#[test]
fn normal_geometry_clamps_and_centers_in_offset_parent() {
let parent = Rect::new(10, 20, 100, 100);
assert_eq!(modal_area(parent, 60, 60), Rect::new(30, 40, 60, 60));
assert_eq!(modal_area(parent, 0, 100), Rect::new(45, 30, 30, 80));
assert_eq!(
compact_modal_area(parent, 40, 20),
Rect::new(40, 60, 40, 20)
);
for width in 0..12 {
for height in 0..12 {
let parent = Rect::new(3, 5, width, height);
let modal = modal_area(parent, 60, 60);
assert_eq!(modal.intersection(parent), modal);
}
}
}
#[test]
fn shell_places_chrome_on_borders_and_clears_content() {
let theme = MissionControlTheme::default();
let mut terminal = Terminal::new(TestBackend::new(50, 12)).unwrap();
for close_hint in [Some("[Esc] close"), None] {
terminal
.draw(|frame| {
draw_modal_container(
frame,
frame.area(),
"Picker",
"[Enter] select",
theme,
close_hint,
);
})
.unwrap();
let buffer = terminal.backend().buffer();
let row = |y| (0..50).map(|x| buffer[(x, y)].symbol()).collect::<String>();
assert!(row(0).starts_with("┌Picker"));
assert_eq!(row(0).contains("[Esc] close"), close_hint.is_some());
assert!(row(11).contains("[Enter] select"));
assert!(!row(1).contains("select"));
assert_eq!(buffer[(25, 5)].bg, theme.app().bg.unwrap());
}
}
}