use crossterm::event::{KeyEvent, MouseEvent};
use ratatui::{Frame, layout::Rect};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EventResult {
Consumed,
NotHandled,
Action(ContainerAction),
}
impl EventResult {
pub fn is_consumed(&self) -> bool {
!matches!(self, EventResult::NotHandled)
}
pub fn is_action(&self) -> bool {
matches!(self, EventResult::Action(_))
}
pub fn action(&self) -> Option<&ContainerAction> {
match self {
EventResult::Action(action) => Some(action),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ContainerAction {
Close,
Submit,
Custom(String),
}
impl ContainerAction {
pub fn custom(name: impl Into<String>) -> Self {
Self::Custom(name.into())
}
pub fn is_close(&self) -> bool {
matches!(self, ContainerAction::Close)
}
pub fn is_submit(&self) -> bool {
matches!(self, ContainerAction::Submit)
}
pub fn custom_name(&self) -> Option<&str> {
match self {
ContainerAction::Custom(name) => Some(name),
_ => None,
}
}
}
pub trait Container {
type State;
fn render(&self, frame: &mut Frame, area: Rect, state: &Self::State);
fn handle_key(&self, key: KeyEvent, state: &mut Self::State) -> EventResult;
fn handle_mouse(&self, mouse: MouseEvent, state: &mut Self::State) -> EventResult;
fn preferred_size(&self) -> (u16, u16);
}
pub trait PopupContainer: Container {
fn popup_area(&self, screen: Rect) -> Rect {
let (width, height) = self.preferred_size();
let width = width.min(screen.width.saturating_sub(4));
let height = height.min(screen.height.saturating_sub(4));
let x = (screen.width.saturating_sub(width)) / 2;
let y = (screen.height.saturating_sub(height)) / 2;
Rect::new(x, y, width, height)
}
fn close_on_outside_click(&self) -> bool {
true
}
fn close_on_escape(&self) -> bool {
true
}
fn screen_margin(&self) -> u16 {
2
}
fn popup_area_anchored(
&self,
screen: Rect,
anchor_x: Option<u16>,
anchor_y: Option<u16>,
) -> Rect {
let (width, height) = self.preferred_size();
let margin = self.screen_margin();
let width = width.min(screen.width.saturating_sub(margin * 2));
let height = height.min(screen.height.saturating_sub(margin * 2));
let x = match anchor_x {
Some(ax) => {
let ideal = ax.saturating_sub(width / 2);
ideal.clamp(margin, screen.width.saturating_sub(width + margin))
}
None => (screen.width.saturating_sub(width)) / 2,
};
let y = match anchor_y {
Some(ay) => {
let below = ay + 1;
if below + height <= screen.height.saturating_sub(margin) {
below
} else {
ay.saturating_sub(height + 1).max(margin)
}
}
None => (screen.height.saturating_sub(height)) / 2,
};
Rect::new(x, y, width, height)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_event_result_consumed() {
assert!(EventResult::Consumed.is_consumed());
assert!(EventResult::Action(ContainerAction::Close).is_consumed());
assert!(!EventResult::NotHandled.is_consumed());
}
#[test]
fn test_event_result_action() {
assert!(!EventResult::Consumed.is_action());
assert!(!EventResult::NotHandled.is_action());
assert!(EventResult::Action(ContainerAction::Close).is_action());
let result = EventResult::Action(ContainerAction::Submit);
assert_eq!(result.action(), Some(&ContainerAction::Submit));
assert_eq!(EventResult::Consumed.action(), None);
}
#[test]
fn test_container_action_types() {
assert!(ContainerAction::Close.is_close());
assert!(!ContainerAction::Close.is_submit());
assert!(ContainerAction::Submit.is_submit());
assert!(!ContainerAction::Submit.is_close());
let custom = ContainerAction::custom("my_action");
assert_eq!(custom.custom_name(), Some("my_action"));
assert!(!custom.is_close());
assert!(!custom.is_submit());
assert_eq!(ContainerAction::Close.custom_name(), None);
}
struct TestContainer {
preferred_width: u16,
preferred_height: u16,
}
impl Container for TestContainer {
type State = ();
fn render(&self, _frame: &mut Frame, _area: Rect, _state: &Self::State) {}
fn handle_key(&self, _key: KeyEvent, _state: &mut Self::State) -> EventResult {
EventResult::NotHandled
}
fn handle_mouse(&self, _mouse: MouseEvent, _state: &mut Self::State) -> EventResult {
EventResult::NotHandled
}
fn preferred_size(&self) -> (u16, u16) {
(self.preferred_width, self.preferred_height)
}
}
impl PopupContainer for TestContainer {}
#[test]
fn test_popup_area_centered() {
let container = TestContainer {
preferred_width: 40,
preferred_height: 20,
};
let screen = Rect::new(0, 0, 100, 50);
let area = container.popup_area(screen);
assert_eq!(area.width, 40);
assert_eq!(area.height, 20);
assert_eq!(area.x, 30); assert_eq!(area.y, 15); }
#[test]
fn test_popup_area_constrained() {
let container = TestContainer {
preferred_width: 200, preferred_height: 100,
};
let screen = Rect::new(0, 0, 80, 24);
let area = container.popup_area(screen);
assert_eq!(area.width, 76); assert_eq!(area.height, 20); }
#[test]
fn test_popup_defaults() {
let container = TestContainer {
preferred_width: 40,
preferred_height: 20,
};
assert!(container.close_on_escape());
assert!(container.close_on_outside_click());
assert_eq!(container.screen_margin(), 2);
}
#[test]
fn test_popup_area_anchored() {
let container = TestContainer {
preferred_width: 20,
preferred_height: 10,
};
let screen = Rect::new(0, 0, 80, 24);
let area = container.popup_area_anchored(screen, Some(40), Some(10));
assert_eq!(area.x, 30); assert_eq!(area.y, 11);
let area = container.popup_area_anchored(screen, Some(40), Some(20));
assert_eq!(area.y, 9); }
}