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)]
#[non_exhaustive]
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 })
}
_ => None,
}
}
pub fn click(&self) -> Option<ButtonEvent> {
if self.disabled {
return None;
}
Some(ButtonEvent::Activated { id: self.id })
}
}
impl Default for Button {
fn default() -> Self {
Self::new(WidgetId::new(""), "")
}
}
#[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(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
#[allow(dead_code)]
pub(crate) mod native_surface_paint {
use super::{Button, ButtonLayout, ButtonVariant};
use crate::event::Rect;
use crate::paint_surface::PaintSurface;
use crate::style::Style;
use crate::theme::Theme;
pub(crate) fn paint(
button: &Button,
surface: &mut dyn PaintSurface,
theme: &Theme,
rect: Rect,
style: &Style,
) -> ButtonLayout {
let layout = button.layout(rect);
if rect.width <= 0.0 || rect.height <= 0.0 {
return layout;
}
let bg = if button.disabled {
theme.surface_bg
} else {
match button.variant {
ButtonVariant::Primary => theme.accent_bg,
ButtonVariant::Secondary | ButtonVariant::Icon => theme.surface_bg,
}
};
surface.surface_fill_rounded_rect(rect, style.corner_radius, bg);
if !matches!(button.variant, ButtonVariant::Icon) {
let border = if button.disabled {
theme.muted_fg
} else if button.focused {
theme.accent_fg
} else {
theme.border_fg
};
surface.surface_stroke_rounded_rect(rect, style.corner_radius, border, 1.0);
}
if !button.label.is_empty() {
let fg = if button.disabled {
theme.muted_fg
} else {
theme.foreground
};
surface.surface_draw_text_run(rect, &button.label, fg);
}
layout
}
}
#[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);
}
}