mod input;
mod render;
use ratatui::layout::Rect;
use ratatui::style::Color;
pub use input::ButtonEvent;
pub use render::{render_button, render_button_row};
use super::FocusState;
#[derive(Debug, Clone)]
pub struct ButtonState {
pub label: String,
pub focus: FocusState,
pub pressed: bool,
}
impl ButtonState {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
focus: FocusState::Normal,
pressed: false,
}
}
pub fn with_focus(mut self, focus: FocusState) -> Self {
self.focus = focus;
self
}
pub fn is_enabled(&self) -> bool {
self.focus != FocusState::Disabled
}
pub fn set_pressed(&mut self, pressed: bool) {
self.pressed = pressed;
}
}
#[derive(Debug, Clone, Copy)]
pub struct ButtonColors {
pub text: Color,
pub border: Color,
pub pressed_bg: Color,
pub focused: Color,
pub hovered: Color,
pub disabled: Color,
}
impl Default for ButtonColors {
fn default() -> Self {
Self {
text: Color::White,
border: Color::Gray,
pressed_bg: Color::DarkGray,
focused: Color::Cyan,
hovered: Color::Blue,
disabled: Color::DarkGray,
}
}
}
impl ButtonColors {
pub fn from_theme(theme: &crate::view::theme::Theme) -> Self {
Self {
text: theme.editor_fg,
border: theme.line_number_fg,
pressed_bg: theme.selection_bg,
focused: theme.selection_bg,
hovered: theme.menu_hover_bg,
disabled: theme.line_number_fg,
}
}
pub fn primary() -> Self {
Self {
text: Color::Black,
border: Color::Cyan,
pressed_bg: Color::LightCyan,
focused: Color::Cyan,
hovered: Color::LightCyan,
disabled: Color::DarkGray,
}
}
pub fn danger() -> Self {
Self {
text: Color::White,
border: Color::Red,
pressed_bg: Color::LightRed,
focused: Color::Red,
hovered: Color::LightRed,
disabled: Color::DarkGray,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ButtonLayout {
pub button_area: Rect,
}
impl ButtonLayout {
pub fn contains(&self, x: u16, y: u16) -> bool {
x >= self.button_area.x
&& x < self.button_area.x + self.button_area.width
&& y >= self.button_area.y
&& y < self.button_area.y + self.button_area.height
}
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::backend::TestBackend;
use ratatui::Terminal;
fn test_frame<F>(width: u16, height: u16, f: F)
where
F: FnOnce(&mut ratatui::Frame, Rect),
{
let backend = TestBackend::new(width, height);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|frame| {
let area = Rect::new(0, 0, width, height);
f(frame, area);
})
.unwrap();
}
#[test]
fn test_button_renders() {
test_frame(20, 1, |frame, area| {
let state = ButtonState::new("OK");
let colors = ButtonColors::default();
let layout = render_button(frame, area, &state, &colors);
assert_eq!(layout.button_area.width, 6); });
}
#[test]
fn test_button_hit_detection() {
test_frame(20, 1, |frame, area| {
let state = ButtonState::new("Click");
let colors = ButtonColors::default();
let layout = render_button(frame, area, &state, &colors);
assert!(layout.contains(0, 0));
assert!(layout.contains(5, 0));
assert!(!layout.contains(15, 0));
});
}
#[test]
fn test_button_row() {
test_frame(40, 1, |frame, area| {
let ok = ButtonState::new("OK");
let cancel = ButtonState::new("Cancel");
let colors = ButtonColors::default();
let layouts = render_button_row(frame, area, &[(&ok, &colors), (&cancel, &colors)], 2);
assert_eq!(layouts.len(), 2);
assert!(layouts[0].button_area.x < layouts[1].button_area.x);
});
}
#[test]
fn test_button_disabled() {
let state = ButtonState::new("Save").with_focus(FocusState::Disabled);
assert!(!state.is_enabled());
}
#[test]
fn test_button_pressed_state() {
let mut state = ButtonState::new("Submit");
assert!(!state.pressed);
state.set_pressed(true);
assert!(state.pressed);
}
#[test]
fn test_button_truncation() {
test_frame(8, 1, |frame, area| {
let state = ButtonState::new("Very Long Button Text");
let colors = ButtonColors::default();
let layout = render_button(frame, area, &state, &colors);
assert!(layout.button_area.width <= area.width);
});
}
}