use std::fmt;
use derive_setters::Setters;
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::{Constraint, Rect};
use ratatui_core::style::Style;
use ratatui_core::symbols::border::Set;
use ratatui_core::text::Line;
use ratatui_core::widgets::{StatefulWidget, Widget};
use ratatui_widgets::block::Block;
use ratatui_widgets::borders::Borders;
use ratatui_widgets::clear::Clear;
use crate::{KnownSize, PopupState};
#[derive(Setters)]
#[setters(into)]
#[non_exhaustive]
pub struct Popup<'content, W> {
#[setters(skip)]
pub body: W,
pub title: Line<'content>,
pub style: Style,
pub borders: Borders,
pub border_set: Set<'content>,
pub border_style: Style,
}
impl<W> fmt::Debug for Popup<'_, W> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Popup")
.field("body", &"...")
.field("title", &self.title)
.field("style", &self.style)
.field("borders", &self.borders)
.field("border_set", &self.border_set)
.field("border_style", &self.border_style)
.finish()
}
}
impl<W: PartialEq> PartialEq for Popup<'_, W> {
fn eq(&self, other: &Self) -> bool {
self.body == other.body
&& self.title == other.title
&& self.style == other.style
&& self.borders == other.borders
&& self.border_set == other.border_set
&& self.border_style == other.border_style
}
}
impl<W> Popup<'_, W> {
pub fn new(body: W) -> Self {
Self {
body,
borders: Borders::ALL,
border_set: Set::default(),
border_style: Style::default(),
title: Line::default(),
style: Style::default(),
}
}
}
impl<W: KnownSize + Widget> Widget for Popup<'_, W> {
fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = PopupState::default();
StatefulWidget::render(self, area, buf, &mut state);
}
}
impl<W> Widget for &Popup<'_, W>
where
W: KnownSize,
for<'a> &'a W: Widget,
{
fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = PopupState::default();
StatefulWidget::render(self, area, buf, &mut state);
}
}
impl<W: KnownSize + Widget> StatefulWidget for Popup<'_, W> {
type State = PopupState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let area = area.clamp(buf.area);
let popup_area = self.popup_area(state, area);
state.area.replace(popup_area);
Clear.render(popup_area, buf);
let block = Block::default()
.borders(self.borders)
.border_set(self.border_set)
.border_style(self.border_style)
.title(self.title)
.style(self.style);
let inner_area = block.inner(popup_area);
block.render(popup_area, buf);
self.body.render(inner_area, buf);
}
}
impl<W> StatefulWidget for &Popup<'_, W>
where
W: KnownSize,
for<'a> &'a W: Widget,
{
type State = PopupState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let area = area.clamp(buf.area);
let popup_area = self.popup_area(state, area);
state.area.replace(popup_area);
Clear.render(popup_area, buf);
let block = Block::default()
.borders(self.borders)
.border_set(self.border_set)
.border_style(self.border_style)
.title(self.title.clone())
.style(self.style);
let inner_area = block.inner(popup_area);
block.render(popup_area, buf);
self.body.render(inner_area, buf);
}
}
impl<W: KnownSize> Popup<'_, W> {
fn popup_area(&self, state: &mut PopupState, area: Rect) -> Rect {
if let Some(current) = state.area.take() {
return current.clamp(area);
}
let has_top = self.borders.intersects(Borders::TOP);
let has_bottom = self.borders.intersects(Borders::BOTTOM);
let has_left = self.borders.intersects(Borders::LEFT);
let has_right = self.borders.intersects(Borders::RIGHT);
let border_height = usize::from(has_top) + usize::from(has_bottom);
let border_width = usize::from(has_left) + usize::from(has_right);
let height = self.body.height().saturating_add(border_height);
let width = self.body.width().saturating_add(border_width);
let height = u16::try_from(height).unwrap_or(area.height);
let width = u16::try_from(width).unwrap_or(area.width);
area.centered(Constraint::Length(width), Constraint::Length(height))
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use ratatui_core::text::Text;
use super::*;
struct RefBody;
impl KnownSize for RefBody {
fn width(&self) -> usize {
11
}
fn height(&self) -> usize {
1
}
}
impl Widget for &RefBody {
fn render(self, area: Rect, buf: &mut Buffer) {
"Hello World".render(area, buf);
}
}
#[test]
fn new() {
let popup = Popup::new("Test Body");
assert_eq!(
popup,
Popup {
body: "Test Body", borders: Borders::ALL,
border_set: Set::default(),
border_style: Style::default(),
title: Line::default(),
style: Style::default(),
}
);
}
#[test]
fn render() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 20, 5));
let mut state = PopupState::default();
let expected = Buffer::with_lines([
" ",
" ┌Title──────┐ ",
" │Hello World│ ",
" └───────────┘ ",
" ",
]);
let popup = Popup::new(RefBody).title("Title");
StatefulWidget::render(&popup, buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, expected);
let popup = Popup::new("Hello World").title("Title");
StatefulWidget::render(popup, buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, expected);
let popup = Popup::new("Hello World".to_string()).title("Title");
StatefulWidget::render(popup, buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, expected);
let popup = Popup::new(Text::from("Hello World")).title("Title");
Widget::render(&popup, buffer.area, &mut buffer);
assert_eq!(buffer, expected);
let popup = Popup::new("Hello World").title("Title");
Widget::render(popup, buffer.area, &mut buffer);
assert_eq!(buffer, expected);
let popup = Popup::new("Hello World".to_string()).title("Title");
Widget::render(popup, buffer.area, &mut buffer);
assert_eq!(buffer, expected);
}
}