use crate::event::{Key, NamedKey, Point, Rect};
use crate::types::{Modifiers, WidgetId};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum ButtonVariant {
#[default]
Primary,
Secondary,
Icon,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Button {
pub id: WidgetId,
pub label: String,
#[serde(default)]
pub variant: ButtonVariant,
#[serde(default)]
pub disabled: bool,
#[serde(default)]
pub focused: bool,
}
impl Button {
pub fn new(id: WidgetId, label: impl Into<String>) -> Self {
Self {
id,
label: label.into(),
variant: ButtonVariant::Primary,
disabled: false,
focused: false,
}
}
pub fn layout(&self, bounds: Rect) -> ButtonLayout {
ButtonLayout { bounds }
}
pub fn handle_key(&self, key: &Key, _modifiers: &Modifiers) -> Option<ButtonEvent> {
if self.disabled || !self.focused {
return None;
}
match key {
Key::Named(NamedKey::Enter) | Key::Char(' ') => Some(ButtonEvent::Activated {
id: self.id.clone(),
}),
_ => None,
}
}
pub fn click(&self) -> Option<ButtonEvent> {
if self.disabled {
return None;
}
Some(ButtonEvent::Activated {
id: self.id.clone(),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ButtonLayout {
pub bounds: Rect,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ButtonHit {
Body,
Outside,
}
impl ButtonLayout {
pub fn hit_test(&self, p: Point) -> ButtonHit {
if self.bounds.contains(p) {
ButtonHit::Body
} else {
ButtonHit::Outside
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ButtonEvent {
Activated { id: WidgetId },
}
#[cfg(test)]
mod tests {
use super::*;
fn button() -> Button {
Button::new(WidgetId::new("dialog:ok"), "OK")
}
#[test]
fn new_defaults_to_primary_enabled_and_unfocused() {
let b = button();
assert_eq!(b.variant, ButtonVariant::Primary);
assert!(!b.disabled);
assert!(!b.focused);
assert_eq!(b.label, "OK");
}
#[test]
fn hit_test_inside_and_outside() {
let b = button();
let layout = b.layout(Rect::new(2.0, 2.0, 6.0, 1.0));
assert_eq!(layout.hit_test(Point::new(3.0, 2.0)), ButtonHit::Body);
assert_eq!(layout.hit_test(Point::new(0.0, 0.0)), ButtonHit::Outside);
}
#[test]
fn handle_key_activates_on_enter_when_focused_and_enabled() {
let mut b = button();
b.focused = true;
let event = b.handle_key(&Key::Named(NamedKey::Enter), &Modifiers::default());
assert_eq!(event, Some(ButtonEvent::Activated { id: b.id.clone() }));
}
#[test]
fn handle_key_activates_on_space() {
let mut b = button();
b.focused = true;
let event = b.handle_key(&Key::Char(' '), &Modifiers::default());
assert_eq!(event, Some(ButtonEvent::Activated { id: b.id.clone() }));
}
#[test]
fn handle_key_ignores_unrelated_keys() {
let mut b = button();
b.focused = true;
assert_eq!(
b.handle_key(&Key::Named(NamedKey::Tab), &Modifiers::default()),
None
);
}
#[test]
fn handle_key_is_inert_when_not_focused() {
let b = button();
assert_eq!(
b.handle_key(&Key::Named(NamedKey::Enter), &Modifiers::default()),
None
);
}
#[test]
fn handle_key_is_inert_when_disabled() {
let mut b = button();
b.focused = true;
b.disabled = true;
assert_eq!(
b.handle_key(&Key::Named(NamedKey::Enter), &Modifiers::default()),
None
);
}
#[test]
fn click_activates_without_requiring_focus() {
let b = button();
assert_eq!(b.click(), Some(ButtonEvent::Activated { id: b.id.clone() }));
}
#[test]
fn click_is_inert_when_disabled() {
let mut b = button();
b.disabled = true;
assert_eq!(b.click(), None);
}
#[test]
fn serde_round_trip() {
let mut b = button();
b.variant = ButtonVariant::Secondary;
b.focused = true;
let json = serde_json::to_string(&b).unwrap();
let back: Button = serde_json::from_str(&json).unwrap();
assert_eq!(b, back);
}
#[test]
fn serde_defaults_variant_primary_and_flags_false_when_omitted() {
let json = r#"{"id":"dialog:ok","label":"OK"}"#;
let b: Button = serde_json::from_str(json).unwrap();
assert_eq!(b.variant, ButtonVariant::Primary);
assert!(!b.disabled);
assert!(!b.focused);
}
}