use crate::components::{Box, Text};
use crate::core::{Color, Element, FlexDirection};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PopoverPosition {
Top,
#[default]
Bottom,
Left,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PopoverArrow {
#[default]
None,
Simple,
Unicode,
}
impl PopoverArrow {
pub fn char_for_position(&self, position: PopoverPosition) -> &'static str {
match self {
PopoverArrow::None => "",
PopoverArrow::Simple => match position {
PopoverPosition::Top => "v",
PopoverPosition::Bottom => "^",
PopoverPosition::Left => ">",
PopoverPosition::Right => "<",
},
PopoverArrow::Unicode => match position {
PopoverPosition::Top => "▼",
PopoverPosition::Bottom => "▲",
PopoverPosition::Left => "▶",
PopoverPosition::Right => "◀",
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PopoverBorder {
None,
#[default]
Single,
Double,
Rounded,
}
impl PopoverBorder {
pub fn chars(
&self,
) -> Option<(
&'static str,
&'static str,
&'static str,
&'static str,
&'static str,
&'static str,
)> {
match self {
PopoverBorder::None => None,
PopoverBorder::Single => Some(("┌", "┐", "└", "┘", "─", "│")),
PopoverBorder::Double => Some(("╔", "╗", "╚", "╝", "═", "║")),
PopoverBorder::Rounded => Some(("╭", "╮", "╰", "╯", "─", "│")),
}
}
}
#[derive(Debug, Clone)]
pub struct PopoverStyle {
pub border: PopoverBorder,
pub arrow: PopoverArrow,
pub background: Option<Color>,
pub foreground: Option<Color>,
pub border_color: Option<Color>,
pub padding: usize,
pub max_width: Option<usize>,
}
impl Default for PopoverStyle {
fn default() -> Self {
Self {
border: PopoverBorder::Single,
arrow: PopoverArrow::None,
background: None,
foreground: None,
border_color: None,
padding: 1,
max_width: Some(40),
}
}
}
impl PopoverStyle {
pub fn new() -> Self {
Self::default()
}
pub fn border(mut self, border: PopoverBorder) -> Self {
self.border = border;
self
}
pub fn arrow(mut self, arrow: PopoverArrow) -> Self {
self.arrow = arrow;
self
}
pub fn background(mut self, color: Color) -> Self {
self.background = Some(color);
self
}
pub fn foreground(mut self, color: Color) -> Self {
self.foreground = Some(color);
self
}
pub fn border_color(mut self, color: Color) -> Self {
self.border_color = Some(color);
self
}
pub fn padding(mut self, padding: usize) -> Self {
self.padding = padding;
self
}
pub fn max_width(mut self, width: usize) -> Self {
self.max_width = Some(width);
self
}
pub fn minimal() -> Self {
Self::new().border(PopoverBorder::None).padding(0)
}
pub fn tooltip() -> Self {
Self::new()
.border(PopoverBorder::Rounded)
.arrow(PopoverArrow::Unicode)
}
pub fn menu() -> Self {
Self::new()
.border(PopoverBorder::Single)
.arrow(PopoverArrow::None)
}
}
#[derive(Debug, Clone)]
pub struct Popover {
trigger: String,
content: String,
position: PopoverPosition,
open: bool,
style: PopoverStyle,
}
impl Popover {
pub fn new(trigger: impl Into<String>) -> Self {
Self {
trigger: trigger.into(),
content: String::new(),
position: PopoverPosition::Bottom,
open: false,
style: PopoverStyle::default(),
}
}
pub fn content(mut self, content: impl Into<String>) -> Self {
self.content = content.into();
self
}
pub fn position(mut self, position: PopoverPosition) -> Self {
self.position = position;
self
}
pub fn open(mut self, open: bool) -> Self {
self.open = open;
self
}
pub fn style(mut self, style: PopoverStyle) -> Self {
self.style = style;
self
}
fn render_content(&self) -> String {
let mut result = String::new();
let content = if let Some(max_width) = self.style.max_width {
if self.content.len() > max_width {
let mut truncated = self.content.chars().take(max_width - 3).collect::<String>();
truncated.push_str("...");
truncated
} else {
self.content.clone()
}
} else {
self.content.clone()
};
if self.position == PopoverPosition::Top {
let arrow = self.style.arrow.char_for_position(self.position);
if !arrow.is_empty() {
result.push_str(arrow);
result.push('\n');
}
}
if let Some((tl, tr, bl, br, h, v)) = self.style.border.chars() {
let padding = " ".repeat(self.style.padding);
let inner_width = content.len() + self.style.padding * 2;
result.push_str(tl);
result.push_str(&h.repeat(inner_width));
result.push_str(tr);
result.push('\n');
result.push_str(v);
result.push_str(&padding);
result.push_str(&content);
result.push_str(&padding);
result.push_str(v);
result.push('\n');
result.push_str(bl);
result.push_str(&h.repeat(inner_width));
result.push_str(br);
} else {
let padding = " ".repeat(self.style.padding);
result.push_str(&padding);
result.push_str(&content);
result.push_str(&padding);
}
if self.position == PopoverPosition::Bottom {
let arrow = self.style.arrow.char_for_position(self.position);
if !arrow.is_empty() {
result.push('\n');
result.push_str(arrow);
}
}
result
}
pub fn into_element(self) -> Element {
let direction = match self.position {
PopoverPosition::Top => FlexDirection::ColumnReverse,
PopoverPosition::Bottom => FlexDirection::Column,
PopoverPosition::Left => FlexDirection::RowReverse,
PopoverPosition::Right => FlexDirection::Row,
};
let mut container = Box::new().flex_direction(direction);
container = container.child(Text::new(&self.trigger).into_element());
if self.open {
let content_text = self.render_content();
let mut content_element = Text::new(content_text);
if let Some(fg) = self.style.foreground {
content_element = content_element.color(fg);
}
if let Some(bg) = self.style.background {
content_element = content_element.background(bg);
}
container = container.child(content_element.into_element());
}
container.into_element()
}
}
impl Default for Popover {
fn default() -> Self {
Self::new("")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_popover_creation() {
let p = Popover::new("Click me");
assert_eq!(p.trigger, "Click me");
assert!(!p.open);
}
#[test]
fn test_popover_content() {
let p = Popover::new("Trigger").content("Content");
assert_eq!(p.content, "Content");
}
#[test]
fn test_popover_position() {
let p = Popover::new("T").position(PopoverPosition::Top);
assert_eq!(p.position, PopoverPosition::Top);
}
#[test]
fn test_popover_open() {
let p = Popover::new("T").open(true);
assert!(p.open);
}
#[test]
fn test_popover_style() {
let style = PopoverStyle::tooltip();
let p = Popover::new("T").style(style);
assert_eq!(p.style.border, PopoverBorder::Rounded);
}
#[test]
fn test_popover_render_content() {
let p = Popover::new("T")
.content("Hello")
.style(PopoverStyle::minimal());
let rendered = p.render_content();
assert!(rendered.contains("Hello"));
}
#[test]
fn test_popover_render_with_border() {
let p = Popover::new("T")
.content("Test")
.style(PopoverStyle::new().border(PopoverBorder::Single));
let rendered = p.render_content();
assert!(rendered.contains("┌"));
assert!(rendered.contains("┘"));
}
#[test]
fn test_popover_into_element() {
let p = Popover::new("Trigger").content("Content").open(true);
let _ = p.into_element();
}
#[test]
fn test_popover_arrow() {
assert_eq!(
PopoverArrow::Unicode.char_for_position(PopoverPosition::Top),
"▼"
);
assert_eq!(
PopoverArrow::Unicode.char_for_position(PopoverPosition::Bottom),
"▲"
);
assert_eq!(
PopoverArrow::Simple.char_for_position(PopoverPosition::Left),
">"
);
assert_eq!(
PopoverArrow::None.char_for_position(PopoverPosition::Right),
""
);
}
#[test]
fn test_popover_border_chars() {
assert!(PopoverBorder::None.chars().is_none());
assert!(PopoverBorder::Single.chars().is_some());
assert!(PopoverBorder::Double.chars().is_some());
assert!(PopoverBorder::Rounded.chars().is_some());
}
#[test]
fn test_popover_style_builder() {
let style = PopoverStyle::new()
.border(PopoverBorder::Rounded)
.arrow(PopoverArrow::Unicode)
.padding(2)
.max_width(50);
assert_eq!(style.border, PopoverBorder::Rounded);
assert_eq!(style.arrow, PopoverArrow::Unicode);
assert_eq!(style.padding, 2);
assert_eq!(style.max_width, Some(50));
}
}