use crate::app::AppState;
use ratatui::widgets::BorderType;
use ratatui::{
Frame,
layout::{Alignment, Rect},
style::{Color, Style},
text::{Span, Text},
widgets::{Block, Borders, Clear, Paragraph},
};
use serde::de::Error;
use serde::{Deserialize, Deserializer};
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum DialogPosition {
Center,
Top,
Bottom,
Left,
Right,
TopLeft,
TopRight,
BottomLeft,
BottomRight,
Custom(u16, u16),
}
impl<'de> Deserialize<'de> for DialogPosition {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Helper {
Str(String),
Arr([u16; 2]),
XY { x: u16, y: u16 },
}
match Helper::deserialize(deserializer)? {
Helper::Str(ref s) if s.eq_ignore_ascii_case("center") => Ok(DialogPosition::Center),
Helper::Str(ref s) if s.eq_ignore_ascii_case("top") => Ok(DialogPosition::Top),
Helper::Str(ref s) if s.eq_ignore_ascii_case("bottom") => Ok(DialogPosition::Bottom),
Helper::Str(ref s) if s.eq_ignore_ascii_case("left") => Ok(DialogPosition::Left),
Helper::Str(ref s) if s.eq_ignore_ascii_case("right") => Ok(DialogPosition::Right),
Helper::Str(ref s)
if s.eq_ignore_ascii_case("top_left") || s.eq_ignore_ascii_case("topleft") =>
{
Ok(DialogPosition::TopLeft)
}
Helper::Str(ref s)
if s.eq_ignore_ascii_case("top_right") || s.eq_ignore_ascii_case("topright") =>
{
Ok(DialogPosition::TopRight)
}
Helper::Str(ref s)
if s.eq_ignore_ascii_case("bottom_left")
|| s.eq_ignore_ascii_case("bottomleft") =>
{
Ok(DialogPosition::BottomLeft)
}
Helper::Str(ref s)
if s.eq_ignore_ascii_case("bottom_right")
|| s.eq_ignore_ascii_case("bottomright") =>
{
Ok(DialogPosition::BottomRight)
}
Helper::Str(s) => Err(D::Error::custom(format!("invalid DialogPosition: '{}'", s))),
Helper::Arr([x, y]) => Ok(DialogPosition::Custom(x, y)),
Helper::XY { x, y } => Ok(DialogPosition::Custom(x, y)),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum DialogSize {
Small,
Medium,
Large,
Custom(u16, u16),
}
impl<'de> Deserialize<'de> for DialogSize {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Helper {
Str(String),
Arr([u16; 2]),
Obj { w: u16, h: u16 },
}
match Helper::deserialize(deserializer)? {
Helper::Str(ref s) if s.eq_ignore_ascii_case("small") => Ok(DialogSize::Small),
Helper::Str(ref s) if s.eq_ignore_ascii_case("medium") => Ok(DialogSize::Medium),
Helper::Str(ref s) if s.eq_ignore_ascii_case("large") => Ok(DialogSize::Large),
Helper::Str(s) => Err(D::Error::custom(format!("invalid DialogSize: '{}'", s))),
Helper::Arr([w, h]) => Ok(DialogSize::Custom(w, h)),
Helper::Obj { w, h } => Ok(DialogSize::Custom(w, h)),
}
}
}
pub(crate) struct DialogStyle {
pub(crate) border: Borders,
pub(crate) border_style: Style,
pub(crate) bg: Style,
pub(crate) fg: Style,
pub(crate) title: Option<Span<'static>>,
pub(crate) title_alignment: Option<Alignment>,
}
impl Default for DialogStyle {
fn default() -> Self {
Self {
border: Borders::ALL,
border_style: Style::default().fg(Color::White),
bg: Style::default().bg(Color::Black),
fg: Style::default().fg(Color::Reset),
title: None,
title_alignment: Some(Alignment::Left),
}
}
}
pub(crate) struct DialogLayout {
pub(crate) area: Rect,
pub(crate) position: DialogPosition,
pub(crate) size: DialogSize,
}
pub(crate) fn dialog_area(area: Rect, size: DialogSize, pos: DialogPosition) -> Rect {
let min_w = 7;
let min_h = 3;
let (w, h) = match size {
DialogSize::Small => (
(area.width * 24 / 100).max(min_w).min(area.width),
(area.height * 7 / 100).max(min_h).min(area.height),
),
DialogSize::Medium => (
(area.width * 26 / 100).max(min_w).min(area.width),
(area.height * 14 / 100).max(min_h).min(area.height),
),
DialogSize::Large => (
(area.width * 32 / 100).max(min_w).min(area.width),
(area.height * 40 / 100).max(min_h).min(area.height),
),
DialogSize::Custom(w_cells, h_cells) => (
w_cells.max(min_w).min(area.width),
h_cells.max(min_h).min(area.height),
),
};
match pos {
DialogPosition::Center => Rect {
x: area.x + (area.width - w) / 2,
y: area.y + (area.height - h) / 2,
width: w,
height: h,
},
DialogPosition::Top => Rect {
x: area.x + (area.width - w) / 2,
y: area.y,
width: w,
height: h,
},
DialogPosition::Bottom => Rect {
x: area.x + (area.width - w) / 2,
y: area.y + area.height - h,
width: w,
height: h,
},
DialogPosition::Left => Rect {
x: area.x,
y: area.y + (area.height - h) / 2,
width: w,
height: h,
},
DialogPosition::Right => Rect {
x: area.x + area.width - w,
y: area.y + (area.height - h) / 2,
width: w,
height: h,
},
DialogPosition::TopLeft => Rect {
x: area.x,
y: area.y,
width: w,
height: h,
},
DialogPosition::TopRight => Rect {
x: area.x + area.width - w,
y: area.y,
width: w,
height: h,
},
DialogPosition::BottomLeft => Rect {
x: area.x,
y: area.y + area.height - h,
width: w,
height: h,
},
DialogPosition::BottomRight => Rect {
x: area.x + area.width - w,
y: area.y + area.height - h,
width: w,
height: h,
},
DialogPosition::Custom(xp, yp) => {
let x = area.x + ((area.width - w) * xp / 100).min(area.width - w);
let y = area.y + ((area.height - h) * yp / 100).min(area.height - h);
Rect {
x,
y,
width: w,
height: h,
}
}
}
}
pub(crate) fn draw_dialog<'a, T>(
frame: &mut Frame,
layout: DialogLayout,
border: BorderType,
style: &DialogStyle,
content: T,
alignment: Option<Alignment>,
) where
T: Into<Text<'a>>,
{
let dialog = dialog_area(layout.area, layout.size, layout.position);
frame.render_widget(Clear, dialog);
let mut block = Block::default()
.borders(style.border)
.border_style(style.border_style)
.border_type(border)
.style(style.bg);
if let Some(title) = &style.title {
block = block.title(title.clone());
if let Some(align) = style.title_alignment {
block = block.title_alignment(align);
}
}
let para = Paragraph::new(content.into())
.block(block)
.alignment(alignment.unwrap_or(Alignment::Left))
.style(style.fg);
frame.render_widget(para, dialog);
}
pub(crate) fn get_pane_block(title: &str, app: &AppState) -> Block<'static> {
let mut block = Block::default();
if app.config().display().is_split() {
block = block
.borders(Borders::ALL)
.border_style(app.config().theme().accent_style());
if app.config().display().titles() {
block = block.title(title.to_string());
}
}
block
}