Custom State Machines
For complex interactions beyond hover/press, define custom state types with the StateTransitions trait.
Defining Custom States
#![allow(unused)]
fn main() {
use blinc_layout::stateful::StateTransitions;
use blinc_core::events::event_types::*;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
enum PlayerState {
#[default]
Stopped,
Playing,
Paused,
}
impl StateTransitions for PlayerState {
fn on_event(&self, event: u32) -> Option<Self> {
match (self, event) {
// Click cycles through states
(PlayerState::Stopped, POINTER_UP) => Some(PlayerState::Playing),
(PlayerState::Playing, POINTER_UP) => Some(PlayerState::Paused),
(PlayerState::Paused, POINTER_UP) => Some(PlayerState::Playing),
_ => None,
}
}
}
}
Using Custom States
#![allow(unused)]
fn main() {
use blinc_layout::prelude::*;
fn player_button() -> impl ElementBuilder {
stateful::<PlayerState>()
.w(60.0)
.h(60.0)
.rounded_full()
.flex_center()
.on_state(|ctx| {
let bg = match ctx.state() {
PlayerState::Stopped => Color::rgba(0.3, 0.3, 0.35, 1.0),
PlayerState::Playing => Color::rgba(0.2, 0.8, 0.4, 1.0),
PlayerState::Paused => Color::rgba(0.9, 0.6, 0.2, 1.0),
};
div().bg(bg).child(text("▶").color(Color::WHITE))
})
}
}
Event Types
Available event types for state transitions:
#![allow(unused)]
fn main() {
use blinc_core::events::event_types::*;
POINTER_ENTER // Mouse enters element
POINTER_LEAVE // Mouse leaves element
POINTER_DOWN // Mouse button pressed
POINTER_UP // Mouse button released (click)
POINTER_MOVE // Mouse moved over element
KEY_DOWN // Keyboard key pressed
KEY_UP // Keyboard key released
TEXT_INPUT // Character typed
FOCUS // Element gained focus
BLUR // Element lost focus
SCROLL // Scroll event
DRAG // Drag motion
DRAG_END // Drag completed
}
Multi-Phase Interactions
Drag State Machine
#![allow(unused)]
fn main() {
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
enum DragPhase {
#[default]
Idle,
Hovering,
Pressing,
Dragging,
}
impl StateTransitions for DragPhase {
fn on_event(&self, event: u32) -> Option<Self> {
match (self, event) {
// Enter hover
(DragPhase::Idle, POINTER_ENTER) => Some(DragPhase::Hovering),
(DragPhase::Hovering, POINTER_LEAVE) => Some(DragPhase::Idle),
// Start press
(DragPhase::Hovering, POINTER_DOWN) => Some(DragPhase::Pressing),
// Transition to drag on move while pressed
(DragPhase::Pressing, DRAG) => Some(DragPhase::Dragging),
// Release
(DragPhase::Pressing, POINTER_UP) => Some(DragPhase::Hovering),
(DragPhase::Dragging, DRAG_END) => Some(DragPhase::Idle),
_ => None,
}
}
}
fn draggable_card() -> impl ElementBuilder {
stateful::<DragPhase>()
.w(120.0)
.h(80.0)
.rounded(8.0)
.on_state(|ctx| {
let (bg, cursor) = match ctx.state() {
DragPhase::Idle => (Color::BLUE, "default"),
DragPhase::Hovering => (Color::CYAN, "grab"),
DragPhase::Pressing => (Color::YELLOW, "grabbing"),
DragPhase::Dragging => (Color::GREEN, "grabbing"),
};
div().bg(bg).cursor(cursor)
})
}
}
Focus State Machine
#![allow(unused)]
fn main() {
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
enum InputFocus {
#[default]
Idle,
Hovered,
Focused,
FocusedHovered,
}
impl StateTransitions for InputFocus {
fn on_event(&self, event: u32) -> Option<Self> {
match (self, event) {
// Hover transitions
(InputFocus::Idle, POINTER_ENTER) => Some(InputFocus::Hovered),
(InputFocus::Hovered, POINTER_LEAVE) => Some(InputFocus::Idle),
(InputFocus::Focused, POINTER_ENTER) => Some(InputFocus::FocusedHovered),
(InputFocus::FocusedHovered, POINTER_LEAVE) => Some(InputFocus::Focused),
// Focus transitions
(InputFocus::Idle, FOCUS) => Some(InputFocus::Focused),
(InputFocus::Hovered, FOCUS) => Some(InputFocus::FocusedHovered),
(InputFocus::Hovered, POINTER_UP) => Some(InputFocus::FocusedHovered),
(InputFocus::Focused, BLUR) => Some(InputFocus::Idle),
(InputFocus::FocusedHovered, BLUR) => Some(InputFocus::Hovered),
_ => None,
}
}
}
fn focusable_input() -> impl ElementBuilder {
stateful::<InputFocus>()
.w(200.0)
.h(40.0)
.rounded(4.0)
.on_state(|ctx| {
let (border_color, border_width) = match ctx.state() {
InputFocus::Idle => (Color::GRAY, 1.0),
InputFocus::Hovered => (Color::LIGHT_GRAY, 1.0),
InputFocus::Focused => (Color::BLUE, 2.0),
InputFocus::FocusedHovered => (Color::BLUE, 2.0),
};
div().border(border_width, border_color)
})
}
}
Combining with External State
Use .deps() to combine state machine transitions with external signals:
#![allow(unused)]
fn main() {
fn smart_button() -> impl ElementBuilder {
let enabled = use_state_keyed("enabled", || true);
stateful::<ButtonState>()
.px(16.0)
.py(8.0)
.rounded(8.0)
.deps([enabled.signal_id()])
.on_state(move |ctx| {
let is_enabled = enabled.get();
let bg = if !is_enabled {
Color::rgba(0.2, 0.2, 0.25, 0.5) // Disabled
} else {
match ctx.state() {
ButtonState::Idle => Color::rgba(0.3, 0.5, 0.9, 1.0),
ButtonState::Hovered => Color::rgba(0.4, 0.6, 1.0, 1.0),
ButtonState::Pressed => Color::rgba(0.2, 0.4, 0.8, 1.0),
_ => Color::rgba(0.3, 0.5, 0.9, 1.0),
}
};
div().bg(bg).child(text("Submit").color(Color::WHITE))
})
}
}
Accessing Dependencies via Context
Use ctx.dep() for cleaner dependency access:
#![allow(unused)]
fn main() {
fn counter_button(count: State<i32>) -> impl ElementBuilder {
stateful::<ButtonState>()
.deps([count.signal_id()])
.on_state(|ctx| {
// Access by index - no need to capture in closure
let value: i32 = ctx.dep(0).unwrap_or_default();
// Or get a State handle for reading/writing
if let Some(count_state) = ctx.dep_as_state::<i32>(0) {
// count_state.set(value + 1);
}
let bg = match ctx.state() {
ButtonState::Hovered => Color::CYAN,
_ => Color::BLUE,
};
div()
.bg(bg)
.child(text(&format!("Count: {}", value)))
})
}
}
Using Scoped State
StateContext provides scoped signals and animated values:
#![allow(unused)]
fn main() {
fn interactive_counter() -> impl ElementBuilder {
stateful::<ButtonState>()
.on_state(|ctx| {
// Scoped signal - persists across rebuilds
let clicks = ctx.use_signal("clicks", || 0);
// Scoped animated value with spring physics
let scale = ctx.use_animated_value("scale", 1.0);
// Animate based on state
match ctx.state() {
ButtonState::Pressed => {
scale.lock().unwrap().set_target(0.95);
}
_ => {
scale.lock().unwrap().set_target(1.0);
}
}
let s = scale.lock().unwrap().get();
div()
.transform(Transform::scale(s, s))
.child(text(&format!("Clicks: {}", clicks.get())))
.on_click(move |_| {
clicks.update(|n| n + 1);
})
})
}
}
State Debugging
Log state transitions for debugging:
#![allow(unused)]
fn main() {
impl StateTransitions for MyState {
fn on_event(&self, event: u32) -> Option<Self> {
let next = match (self, event) {
// ... transitions ...
_ => None,
};
if let Some(ref new_state) = next {
println!("State: {:?} -> {:?} (event: {})", self, new_state, event);
}
next
}
}
}
Setting Initial State
Use .initial() when you need a non-default starting state:
#![allow(unused)]
fn main() {
fn initially_disabled_button(disabled: bool) -> impl ElementBuilder {
stateful::<ButtonState>()
.initial(if disabled { ButtonState::Disabled } else { ButtonState::Idle })
.on_state(|ctx| {
let bg = match ctx.state() {
ButtonState::Disabled => Color::GRAY,
ButtonState::Idle => Color::BLUE,
ButtonState::Hovered => Color::CYAN,
ButtonState::Pressed => Color::DARK_BLUE,
};
div().bg(bg)
})
}
}
NoState for Dependency-Only Containers
When you only need dependency tracking without state transitions:
#![allow(unused)]
fn main() {
fn data_display(data: State<Vec<String>>) -> impl ElementBuilder {
stateful::<NoState>()
.deps([data.signal_id()])
.on_state(|ctx| {
// Access data via context
let items: Vec<String> = ctx.dep(0).unwrap_or_default();
div()
.flex_col()
.gap(4.0)
.children(items.iter().map(|item| {
div().child(text(item))
}))
})
}
}
Best Practices
-
Keep states minimal - Only include states you need to distinguish visually.
-
Handle all paths - Consider every possible event in each state.
-
Use descriptive names - State names should clearly indicate the UI appearance.
-
Return None for no-ops - If an event doesn’t cause a transition, return
None. -
Test transitions - Verify all state paths work as expected.
-
Use
.deps()for external dependencies - When combining with signals. -
Use
ctx.dep()over closures - Cleaner access to dependency values. -
Implement Default - Mark the default state with
#[default]attribute. -
Use scoped signals -
ctx.use_signal()for state local to the stateful. -
Use animated values -
ctx.use_animated_value()for smooth transitions.