use crate::core::buffer::Buffer;
use crate::core::color::Color;
use crate::core::rect::Rect;
use crate::overlays::{Overlay, Transition};
use crate::widgets::block::{Block, BorderStyle};
use crate::widgets::paragraph::Paragraph;
use crate::widgets::Widget;
use std::time::Duration;
#[derive(Debug, Clone, PartialEq)]
pub enum PopupStyle {
Floating,
Drawer,
Tooltip,
Panel,
}
#[derive(Debug, Clone)]
pub struct Popup {
pub title: String,
pub content: String,
pub style: PopupStyle,
pub visible: bool,
pub position: PopupPosition,
pub width: u16,
pub height: u16,
pub min_width: u16,
pub min_height: u16,
pub max_width: u16,
pub max_height: u16,
pub border_color: Color,
pub bg: Color,
pub opacity: f32,
pub transition: Transition,
pub transition_progress: f32,
pub auto_close_on_resize: bool,
pub close_on_esc: bool,
pub close_on_click_outside: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PopupPosition {
TopLeft,
TopCenter,
TopRight,
CenterLeft,
Center,
CenterRight,
BottomLeft,
BottomCenter,
BottomRight,
FollowCursor(u16, u16),
}
impl Popup {
pub fn new(title: &str, content: &str) -> Self {
Self {
title: title.to_string(),
content: content.to_string(),
style: PopupStyle::Floating,
visible: false,
position: PopupPosition::Center,
width: 40,
height: 12,
min_width: 10,
min_height: 3,
max_width: 80,
max_height: 40,
border_color: Color::rgb(88, 166, 255),
bg: Color::rgb(22, 27, 34),
opacity: 1.0,
transition: Transition::Fade,
transition_progress: 0.0,
auto_close_on_resize: true,
close_on_esc: true,
close_on_click_outside: false,
}
}
pub fn with_style(mut self, style: PopupStyle) -> Self {
self.style = style;
self
}
pub fn set_style(&mut self, style: PopupStyle) {
match style {
PopupStyle::Floating => {
self.border_color = Color::rgb(88, 166, 255);
}
PopupStyle::Drawer => {
self.border_color = Color::rgb(63, 185, 80);
}
PopupStyle::Tooltip => {
self.border_color = Color::rgb(255, 178, 72);
}
PopupStyle::Panel => {
self.border_color = Color::rgb(248, 81, 73);
}
}
self.style = style;
}
pub fn with_size(mut self, w: u16, h: u16) -> Self {
self.width = w;
self.height = h;
self
}
pub fn with_min_size(mut self, w: u16, h: u16) -> Self {
self.min_width = w;
self.min_height = h;
self
}
pub fn with_max_size(mut self, w: u16, h: u16) -> Self {
self.max_width = w;
self.max_height = h;
self
}
pub fn with_position(mut self, pos: PopupPosition) -> Self {
self.position = pos;
self
}
pub fn with_border_color(mut self, c: Color) -> Self {
self.border_color = c;
self
}
pub fn with_auto_close_on_resize(mut self, v: bool) -> Self {
self.auto_close_on_resize = v;
self
}
pub fn adapt_to_terminal(&mut self, cols: u16, rows: u16) {
let max_w = cols.saturating_sub(4);
let max_h = rows.saturating_sub(4);
self.width = self
.width
.min(max_w)
.max(self.min_width)
.min(self.max_width);
self.height = self
.height
.min(max_h)
.max(self.min_height)
.min(self.max_height);
if self.auto_close_on_resize && (cols < 20 || rows < 8) {
self.visible = false;
}
}
fn calc_position(&self, screen: Rect) -> (u16, u16) {
match self.position {
PopupPosition::TopLeft => (screen.x + 1, screen.y + 1),
PopupPosition::TopCenter => (
screen.x + (screen.width.saturating_sub(self.width)) / 2,
screen.y + 1,
),
PopupPosition::TopRight => {
(screen.right().saturating_sub(self.width + 1), screen.y + 1)
}
PopupPosition::CenterLeft => (
screen.x + 1,
screen.y + (screen.height.saturating_sub(self.height)) / 2,
),
PopupPosition::Center => (
screen.x + (screen.width.saturating_sub(self.width)) / 2,
screen.y + (screen.height.saturating_sub(self.height)) / 2,
),
PopupPosition::CenterRight => (
screen.right().saturating_sub(self.width + 1),
screen.y + (screen.height.saturating_sub(self.height)) / 2,
),
PopupPosition::BottomLeft => (
screen.x + 1,
screen.bottom().saturating_sub(self.height + 1),
),
PopupPosition::BottomCenter => (
screen.x + (screen.width.saturating_sub(self.width)) / 2,
screen.bottom().saturating_sub(self.height + 1),
),
PopupPosition::BottomRight => (
screen.right().saturating_sub(self.width + 1),
screen.bottom().saturating_sub(self.height + 1),
),
PopupPosition::FollowCursor(cx, cy) => {
let x = cx.min(screen.right().saturating_sub(self.width));
let y = cy.min(screen.bottom().saturating_sub(self.height));
(x, y)
}
}
}
fn render_floating(&self, buffer: &mut Buffer, area: Rect) {
let (x, y) = self.calc_position(area);
let rect = Rect::new(x, y, self.width, self.height);
let block = Block::new(&self.title)
.with_borders(match self.style {
PopupStyle::Floating => BorderStyle::Rounded,
PopupStyle::Drawer => BorderStyle::Double,
PopupStyle::Tooltip => BorderStyle::Plain,
PopupStyle::Panel => BorderStyle::Thick,
})
.with_border_color(self.border_color)
.with_bg(self.bg);
block.render(buffer, rect);
let inner = block.inner(rect);
let p = Paragraph::new(&self.content);
p.render(buffer, inner);
}
}
impl Overlay for Popup {
fn is_visible(&self) -> bool {
self.visible
}
fn show(&mut self) {
self.visible = true;
self.transition_progress = 0.0;
}
fn hide(&mut self) {
self.visible = false;
}
fn toggle(&mut self) {
if self.visible {
self.hide();
} else {
self.show();
}
}
fn update(&mut self, delta: Duration) {
if self.transition == Transition::Fade && self.transition_progress < 1.0 {
self.transition_progress =
(self.transition_progress + delta.as_secs_f32() * 4.0).min(1.0);
}
}
fn render(&self, buffer: &mut Buffer, area: Rect) {
if self.visible {
self.render_floating(buffer, area);
}
}
fn position(&self, screen: Rect) -> Rect {
let (x, y) = self.calc_position(screen);
Rect::new(x, y, self.width, self.height)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_popup_creation() {
let p = Popup::new("Test", "content");
assert!(!p.visible);
assert_eq!(p.width, 40);
}
#[test]
fn test_popup_toggle() {
let mut p = Popup::new("Test", "content");
p.toggle();
assert!(p.visible);
p.toggle();
assert!(!p.visible);
}
#[test]
fn test_popup_adapt_to_terminal() {
let mut p = Popup::new("T", "c").with_size(60, 30);
p.adapt_to_terminal(30, 15);
assert!(p.width <= 26);
assert!(p.height <= 11);
}
#[test]
fn test_popup_adapt_close_on_small_terminal() {
let mut p = Popup::new("T", "c").with_auto_close_on_resize(true);
p.show();
p.adapt_to_terminal(10, 5);
assert!(!p.visible);
}
#[test]
fn test_popup_calc_position_center() {
let p = Popup::new("T", "c")
.with_size(20, 5)
.with_position(PopupPosition::Center);
let screen = Rect::new(0, 0, 80, 24);
let pos = p.calc_position(screen);
assert_eq!(pos, (30, 9));
}
}