mod basic;
pub mod form;
use std::borrow::Cow;
use ratatui::{
Frame,
style::{Color, Stylize},
text::Text,
widgets::{*, block::Title},
layout::{Rect, Layout, Constraint, Margin},
};
use crate::prelude::*;
pub use basic::*;
pub use form::form;
pub trait Dialog: Sized {
type Out;
fn format(&self) -> DrawInfo;
fn input(self, key: KeyEvent) -> Signal<Self>;
fn run_over<G>(self, background: &impl State, ctx: &mut Context<G>) -> Self::Out {
Container{ content: self, background }
.run(&mut ctx.chain_without_global())
}
}
impl<T: Dialog> State for T {
type Result<U> = U;
type Out = T::Out;
type Global = ();
fn draw(&self, frame: &mut Frame) {
let draw_info = self.format();
draw_dialog(draw_info, frame)
}
fn input(self, key: KeyEvent, _ctx: &mut Context) -> Signal<Self> {
self.input(key)
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct DrawInfo<'a> {
pub title: Cow<'a, str>,
pub color: Color,
pub body: Text<'a>,
pub hint: Cow<'a, str>,
pub inner_margin: [u16; 2],
pub width_percentage: u8,
pub wrap: Option<Wrap>,
pub create_title: fn(Cow<'a, str>) -> Title<'a>,
pub create_block: fn() -> Block<'a>,
}
impl<'a> Default for DrawInfo<'a> {
fn default() -> DrawInfo<'a> {
DrawInfo {
title: "".into(),
color: Color::Cyan,
body: "".into(),
hint: "".into(),
inner_margin: [3, 1],
width_percentage: 50,
wrap: Some(Wrap{ trim: false }),
create_title: |title| match title.is_empty() {
true => "".into(),
false => format!(" {title} ").to_uppercase().into(),
},
create_block: || Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Thick),
}
}
}
struct Container<'a, T, U> {
content: T,
background: &'a U,
}
impl<T: Dialog, U: State> State for Container<'_, T, U> {
type Result<V> = V;
type Out = T::Out;
type Global = ();
fn draw(&self, frame: &mut Frame) {
self.background.draw(frame);
let draw_info = self.content.format();
draw_dialog(draw_info, frame)
}
fn input(self, key: KeyEvent, _ctx: &mut Context) -> Signal<Self> {
match self.content.input(key) {
Signal::Return(out) => Signal::Return(out),
Signal::Continue(content) => Signal::Continue(Container{ content, ..self }),
}
}
}
#[inline(never)]
fn draw_dialog<'a>(info: DrawInfo<'a>, frame: &mut Frame) {
let DrawInfo {
title,
body,
color,
hint,
inner_margin: [inner_margin_x, inner_margin_y],
width_percentage,
wrap,
create_title,
create_block,
} = info;
let body = match (wrap, Paragraph::new(body)) {
(Some(wrap), body) => body.wrap(wrap),
(None, body) => body,
};
let hint = Paragraph::new(hint)
.wrap(Wrap{ trim: true })
.italic();
let frame_size = frame.size();
let inner_width = (frame_size.width * width_percentage as u16) / 100;
let [hint_height, body_height] = [&hint, &body].map(|x|
x.line_count(inner_width) as u16
);
let inner_height = body_height + 2 + hint_height;
let inner_area = {
let title = create_title(title);
let block = create_block()
.title(title)
.fg(color);
let [outer_width, outer_height] = outer_size(
&block,
inner_width + inner_margin_x * 2,
inner_height + inner_margin_y * 2,
);
let [delta_width, delta_height] = [
frame_size.width.saturating_sub(outer_width),
frame_size.height.saturating_sub(outer_height),
];
let mut outer_area = frame_size.inner(Margin {
horizontal: delta_width / 2,
vertical: delta_height / 2,
});
outer_area.height -= delta_height & 1;
let inner_area = block.inner(outer_area);
frame.render_widget(Clear, outer_area);
frame.render_widget(block, outer_area);
inner_area
};
{
let layout = Layout::default()
.horizontal_margin(inner_margin_x)
.vertical_margin(inner_margin_y)
.constraints([
Constraint::Length(body_height),
Constraint::Min(0),
Constraint::Length(hint_height),
])
.split(inner_area);
frame.render_widget(body, layout[0]);
frame.render_widget(hint, layout[2]);
}
}
fn outer_size(block: &Block, inner_width: u16, inner_height: u16) -> [u16; 2] {
let dummy = Rect::new(0, 0, u16::MAX, u16::MAX);
let Rect{ width, height, .. } = block.inner(dummy);
let dx = dummy.width - width;
let dy = dummy.height - height;
[inner_width + dx, inner_height + dy]
}