#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Default)]
pub enum ButtonState {
#[default]
Idle,
Hover,
Active,
Focused,
Disabled,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum ButtonEvent {
MouseEnter,
MouseLeave,
MouseDown,
MouseUp,
Focus,
Blur,
Enable,
Disable,
}
#[derive(Clone, Debug)]
pub struct ButtonTransition {
pub from: ButtonState,
pub event: ButtonEvent,
pub to: ButtonState,
}
pub struct ButtonStateMachine {
transitions: Vec<ButtonTransition>,
current_state: ButtonState,
}
impl Default for ButtonStateMachine {
fn default() -> Self {
Self::new()
}
}
impl ButtonStateMachine {
pub fn new() -> Self {
let transitions = vec![
ButtonTransition {
from: ButtonState::Idle,
event: ButtonEvent::MouseEnter,
to: ButtonState::Hover,
},
ButtonTransition {
from: ButtonState::Idle,
event: ButtonEvent::Focus,
to: ButtonState::Focused,
},
ButtonTransition {
from: ButtonState::Idle,
event: ButtonEvent::Disable,
to: ButtonState::Disabled,
},
ButtonTransition {
from: ButtonState::Hover,
event: ButtonEvent::MouseLeave,
to: ButtonState::Idle,
},
ButtonTransition {
from: ButtonState::Hover,
event: ButtonEvent::MouseDown,
to: ButtonState::Active,
},
ButtonTransition {
from: ButtonState::Hover,
event: ButtonEvent::Blur,
to: ButtonState::Idle,
},
ButtonTransition {
from: ButtonState::Hover,
event: ButtonEvent::Disable,
to: ButtonState::Disabled,
},
ButtonTransition {
from: ButtonState::Active,
event: ButtonEvent::MouseUp,
to: ButtonState::Hover,
},
ButtonTransition {
from: ButtonState::Active,
event: ButtonEvent::MouseLeave,
to: ButtonState::Idle,
},
ButtonTransition {
from: ButtonState::Active,
event: ButtonEvent::Blur,
to: ButtonState::Idle,
},
ButtonTransition {
from: ButtonState::Active,
event: ButtonEvent::Disable,
to: ButtonState::Disabled,
},
ButtonTransition {
from: ButtonState::Focused,
event: ButtonEvent::Blur,
to: ButtonState::Idle,
},
ButtonTransition {
from: ButtonState::Focused,
event: ButtonEvent::MouseEnter,
to: ButtonState::Hover,
},
ButtonTransition {
from: ButtonState::Focused,
event: ButtonEvent::MouseDown,
to: ButtonState::Active,
},
ButtonTransition {
from: ButtonState::Focused,
event: ButtonEvent::Disable,
to: ButtonState::Disabled,
},
ButtonTransition {
from: ButtonState::Disabled,
event: ButtonEvent::Enable,
to: ButtonState::Idle,
},
];
Self {
transitions,
current_state: ButtonState::Idle,
}
}
#[inline]
pub fn current_state(&self) -> ButtonState {
self.current_state
}
pub fn handle_event(&mut self, event: ButtonEvent) -> Option<ButtonState> {
for transition in &self.transitions {
if transition.from == self.current_state && transition.event == event {
self.current_state = transition.to;
return Some(self.current_state);
}
}
match event {
ButtonEvent::Disable if self.current_state != ButtonState::Disabled => {
self.current_state = ButtonState::Disabled;
return Some(ButtonState::Disabled);
}
ButtonEvent::Enable if self.current_state == ButtonState::Disabled => {
self.current_state = ButtonState::Idle;
return Some(ButtonState::Idle);
}
_ => {}
}
None
}
#[inline]
pub fn reset(&mut self) {
self.current_state = ButtonState::Idle;
}
#[inline]
pub fn is_in(&self, state: ButtonState) -> bool {
self.current_state == state
}
#[inline]
pub fn is_interactive(&self) -> bool {
self.current_state != ButtonState::Disabled
}
#[inline]
pub fn css_class_suffix(&self) -> &'static str {
match self.current_state {
ButtonState::Idle => "",
ButtonState::Hover => "hover",
ButtonState::Active => "active",
ButtonState::Focused => "focus",
ButtonState::Disabled => "disabled",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum ButtonAnimationTarget {
None,
Scale,
GlowOpacity,
GlowSpread,
BackgroundColor,
TextColor,
}
pub struct ButtonAnimationConfig {
pub duration_ms: u32,
pub easing: &'static str,
pub targets: Vec<ButtonAnimationTarget>,
}
impl Default for ButtonAnimationConfig {
fn default() -> Self {
Self {
duration_ms: 100,
easing: "ease-out",
targets: vec![
ButtonAnimationTarget::GlowOpacity,
ButtonAnimationTarget::Scale,
],
}
}
}
impl ButtonAnimationConfig {
pub fn press() -> Self {
Self {
duration_ms: 100,
easing: "ease-out",
targets: vec![
ButtonAnimationTarget::GlowOpacity,
ButtonAnimationTarget::Scale,
],
}
}
pub fn release() -> Self {
Self {
duration_ms: 100,
easing: "ease-out",
targets: vec![
ButtonAnimationTarget::GlowOpacity,
ButtonAnimationTarget::Scale,
],
}
}
pub fn hover() -> Self {
Self {
duration_ms: 150,
easing: "ease-out",
targets: vec![ButtonAnimationTarget::GlowOpacity],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_transitions() {
let mut sm = ButtonStateMachine::new();
assert_eq!(
sm.handle_event(ButtonEvent::MouseEnter),
Some(ButtonState::Hover)
);
assert!(sm.is_in(ButtonState::Hover));
assert_eq!(
sm.handle_event(ButtonEvent::MouseDown),
Some(ButtonState::Active)
);
assert!(sm.is_in(ButtonState::Active));
assert_eq!(
sm.handle_event(ButtonEvent::MouseUp),
Some(ButtonState::Hover)
);
assert!(sm.is_in(ButtonState::Hover));
assert_eq!(
sm.handle_event(ButtonEvent::MouseLeave),
Some(ButtonState::Idle)
);
assert!(sm.is_in(ButtonState::Idle));
}
#[test]
fn test_disabled_transition() {
let mut sm = ButtonStateMachine::new();
sm.handle_event(ButtonEvent::MouseEnter);
assert_eq!(
sm.handle_event(ButtonEvent::Disable),
Some(ButtonState::Disabled)
);
assert!(!sm.is_interactive());
assert_eq!(
sm.handle_event(ButtonEvent::Enable),
Some(ButtonState::Idle)
);
assert!(sm.is_interactive());
}
#[test]
fn test_invalid_transition_ignored() {
let mut sm = ButtonStateMachine::new();
assert_eq!(sm.handle_event(ButtonEvent::MouseDown), None);
assert_eq!(sm.current_state(), ButtonState::Idle);
sm.handle_event(ButtonEvent::MouseEnter);
assert_eq!(sm.handle_event(ButtonEvent::Focus), None);
}
}