use crate::{
Component,
Event,
Focusable,
InputResult,
RenderError,
Rendered,
layout::{
Border,
Rect,
},
theme::{
ColorMode,
Palette,
Style,
Theme,
stylize,
},
};
pub struct Modal {
content: Box<dyn Component>,
title: Option<String>,
border: Border,
width: u16,
focused: bool,
}
impl Modal {
pub fn new(content: Box<dyn Component>) -> Self {
Self {
content,
title: None,
border: Border::ROUNDED,
width: 40,
focused: false,
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn border(mut self, border: Border) -> Self {
self.border = border;
self
}
pub fn width(mut self, width: u16) -> Self {
self.width = width;
self
}
}
impl Focusable for Modal {
fn focused(&self) -> bool {
self.focused
}
fn set_focused(&mut self, focused: bool) {
self.focused = focused;
if let Some(f) = self.content.as_focusable_mut() {
f.set_focused(focused);
}
}
}
impl Component for Modal {
fn render(&self, width: u16) -> Result<Rendered, RenderError> {
let w = width.min(self.width);
let rect = Rect::new(0, 0, w, 24); self.render_rect(rect)
}
fn render_rect(&self, rect: Rect) -> Result<Rendered, RenderError> {
let theme = Theme::current();
let mode = ColorMode::detect();
let border_style = Style::new().fg(theme.border_default());
let border_prefix = border_style.prefix(mode);
let suffix = Style::suffix();
let inner_w = rect.width.saturating_sub(2);
let inner_h = rect.height.saturating_sub(2);
let content_rect = Rect::new(1, 1, inner_w, inner_h);
let content_rendered = self.content.render_rect(content_rect)?;
let content_h = content_rendered.lines.len().min(inner_h as usize) as u16;
let _total_h = content_h + 2;
let mut screen = Rendered::empty();
let fill_w = inner_w as usize;
{
let mut top = String::new();
top.push_str(&border_prefix);
top.push(self.border.top_left);
top.push_str(suffix);
if let Some(ref title) = self.title {
let indicator = if self.focused { "▼ " } else { "▶ " };
let max_title = fill_w.saturating_sub(2);
let t = if title.len() > max_title {
&title[..max_title]
} else {
title
};
let label = format!(" {}{} ", indicator, t);
let label_styled = stylize(&label, &Style::new().fg(theme.text_primary()).bold());
let t_visible = crate::utils::visible_width(&label_styled);
let fill_count = fill_w.saturating_sub(t_visible);
top.push_str(&label_styled);
if fill_count > 0 {
top.push_str(&border_prefix);
top.push_str(&self.border.top.to_string().repeat(fill_count));
top.push_str(suffix);
}
} else {
top.push_str(&border_prefix);
top.push_str(&self.border.top.to_string().repeat(fill_w));
top.push_str(suffix);
}
top.push_str(&border_prefix);
top.push(self.border.top_right);
top.push_str(suffix);
screen.lines.push(top);
}
for i in 0..content_h {
let mut line = String::new();
line.push_str(&border_prefix);
line.push(self.border.left);
line.push_str(suffix);
let content_line = content_rendered
.lines
.get(i as usize)
.map(|s| s.as_str())
.unwrap_or("");
let pad = inner_w as usize - crate::utils::visible_width(content_line);
line.push_str(content_line);
if pad > 0 {
line.push_str(&" ".repeat(pad));
}
line.push_str(&border_prefix);
line.push(self.border.right);
line.push_str(suffix);
screen.lines.push(line);
}
{
let mut bottom = String::new();
bottom.push_str(&border_prefix);
bottom.push(self.border.bottom_left);
bottom.push_str(suffix);
bottom.push_str(&border_prefix);
bottom.push_str(&self.border.bottom.to_string().repeat(fill_w));
bottom.push_str(suffix);
bottom.push_str(&border_prefix);
bottom.push(self.border.bottom_right);
bottom.push_str(suffix);
screen.lines.push(bottom);
}
if let Some((r, c)) = content_rendered.cursor {
if ((r + 1) as usize) < screen.lines.len() {
screen.cursor = Some((r + 1, c + 1));
}
}
Ok(screen)
}
fn handle_input(&mut self, event: &Event) -> InputResult {
self.content.handle_input(event)
}
fn as_focusable(&self) -> Option<&dyn Focusable> {
Some(self)
}
fn as_focusable_mut(&mut self) -> Option<&mut dyn Focusable> {
Some(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
components::Text,
theme::Theme,
};
#[test]
fn modal_renders_with_border() {
Theme::with(Theme::Light, || {
let modal = Modal::new(Box::new(Text::new("hi", 0, 0)));
let rendered = modal.render_rect(Rect::new(0, 0, 10, 5)).unwrap();
assert!(rendered.lines[0].contains("╭"));
assert!(rendered.lines[0].contains("╮"));
assert!(rendered.lines[2].contains("╰"));
assert!(rendered.lines[2].contains("╯"));
});
}
#[test]
fn modal_renders_title() {
Theme::with(Theme::Light, || {
let modal = Modal::new(Box::new(Text::new("hi", 0, 0))).title("Alert");
let rendered = modal.render_rect(Rect::new(0, 0, 20, 5)).unwrap();
assert!(rendered.lines[0].contains("Alert"));
});
}
#[test]
fn modal_forwards_focus() {
Theme::with(Theme::Light, || {
let mut modal = Modal::new(Box::new(Text::new("hi", 0, 0)));
modal.set_focused(true);
assert!(modal.focused());
});
}
}