Event Handling
Interactive applications respond to user input - clicks, key presses, touches, and more. Freya provides a comprehensive event system for handling all types of user interactions.
[!NOTE] What Are Events? Events are notifications that something happened - the user clicked, pressed a key, scrolled, etc. Your app “listens” for these events and responds accordingly.
Event Basics
Attaching Event Handlers
Events are attached to elements using handler methods:
rect()
.on_pointer_down(|e| {
println!("Clicked at: {:?}", e.element_location());
})
Event Data
All event handlers receive an Event<T> where T is the event data type:
rect()
.on_pointer_down(|e: Event<PointerEventData>| {
// Access data directly via deref
let location = e.element_location();
// Or via data() method
let data = e.data();
})
Controlling Event Flow
rect()
.on_key_down(|e| {
// Stop event from bubbling to parents
e.stop_propagation();
// Prevent default behavior
e.prevent_default();
})
Pointer Events
Pointer events unify mouse and touch input - use them for interactive elements.
Available Handlers
| Handler | Description |
|---|---|
on_pointer_down | Pointer pressed (mouse or touch) |
on_pointer_press | Pointer released (click/tap) |
on_pointer_enter | Pointer entered element |
on_pointer_leave | Pointer left element |
Basic Example
fn interactive_box() -> impl IntoElement {
let is_hovered = use_state(|| false);
rect()
.width(Size::px(200.0))
.height(Size::px(200.0))
.background(if is_hovered() {
Color::BLUE
} else {
Color::GRAY
})
.on_pointer_enter(move |_| is_hovered.set(true))
.on_pointer_leave(move |_| is_hovered.set(false))
.on_pointer_down(|e| {
println!("Clicked at {:?}", e.element_location());
})
}
Getting Location
rect()
.on_pointer_down(|e| {
// Global position (screen coordinates)
let global = e.global_location();
// Position relative to element (0,0 is top-left of element)
let local = e.element_location();
println!("Global: {:?}, Local: {:?}", global, local);
})
Drag Example
fn draggable_box() -> impl IntoElement {
let is_dragging = use_state(|| false);
let position = use_state(|| CursorPoint::new(100.0, 100.0));
let offset = use_state(|| CursorPoint::new(0.0, 0.0));
rect()
.width(Size::px(100.0))
.height(Size::px(100.0))
.background(Color::INDIGO)
.corner_radius(8.0)
.position(Position::Absolute)
.left(Size::px(position().x))
.top(Size::px(position().y))
.on_pointer_down(move |e| {
is_dragging.set(true);
let pos = position();
offset.set(CursorPoint::new(
pos.x - e.global_location().x,
pos.y - e.global_location().y,
));
})
.on_pointer_enter(move |_| is_hovered.set(true))
.on_pointer_leave(move |_| is_hovered.set(false))
.child(
rect()
.expanded()
.on_global_pointer_move(move |e| {
if is_dragging() {
position.set(CursorPoint::new(
e.global_location().x + offset().x,
e.global_location().y + offset().y,
));
}
})
.on_global_pointer_up(move |_| {
is_dragging.set(false);
})
)
}
Mouse Events
For mouse-specific handling (access to which button was pressed).
Available Handlers
| Handler | Description |
|---|---|
on_mouse_down | Mouse button pressed |
on_mouse_up | Mouse button released |
on_mouse_move | Mouse moved |
Mouse Event Data
struct MouseEventData {
global_location: CursorPoint, // Position on screen
element_location: CursorPoint, // Position relative to element
button: Option<MouseButton>, // Which button (if any)
}
Mouse Buttons
enum MouseButton {
Left,
Right,
Middle,
Back,
Forward,
Other(u16),
}
Example: Right-Click Context Menu
fn context_menu_trigger() -> impl IntoElement {
let show_menu = use_state(|| false);
rect()
.on_mouse_down(move |e| {
if e.button == Some(MouseButton::Right) {
show_menu.set(true);
e.stop_propagation();
}
})
.child(label().text("Right-click me"))
.child(if show_menu() {
context_menu()
} else {
rect()
})
}
Keyboard Events
Handle keyboard input for shortcuts, navigation, and text input.
Available Handlers
| Handler | Description |
|---|---|
on_key_down | Key pressed |
on_key_up | Key released |
[!IMPORTANT] Focus Required Keyboard events only fire on focusable elements. Add
.a11y_focusable(true)or use components like Button and Input that are already focusable.
Keyboard Event Data
struct KeyboardEventData {
key: Key, // Logical key (e.g., Key::Character("a"))
code: Code, // Physical key (e.g., Code::KeyA)
modifiers: Modifiers, // Ctrl, Shift, Alt, etc.
}
Named Keys
enum NamedKey {
Enter,
Escape,
Tab,
ArrowUp,
ArrowDown,
ArrowLeft,
ArrowRight,
Backspace,
Delete,
Space,
Home,
End,
PageUp,
PageDown,
F1, F2, F3, /* ... */ F12,
// ...
}
Checking Modifiers
rect()
.a11y_focusable(true)
.on_key_down(|e| {
// Check for Ctrl
if e.modifiers.contains(Modifiers::CTRL) {
println!("Ctrl is held");
}
// Check for Shift
if e.modifiers.contains(Modifiers::SHIFT) {
println!("Shift is held");
}
// Check for Alt
if e.modifiers.contains(Modifiers::ALT) {
println!("Alt is held");
}
})
Example: Keyboard Shortcuts
fn shortcut_demo() -> impl IntoElement {
let count = use_state(|| 0);
rect()
.expanded()
.a11y_focusable(true)
.on_key_down(move |e| {
match &e.key {
// Simple key
Key::Named(NamedKey::Space) => {
println!("Space pressed");
}
// Enter key
Key::Named(NamedKey::Enter) => {
*count.write() += 1;
}
// Escape key
Key::Named(NamedKey::Escape) => {
count.set(0);
}
// Character key
Key::Character(c) => {
println!("Typed: {}", c);
}
_ => {}
}
// Ctrl+S to save
if e.key == Key::Character("s".to_string())
&& e.modifiers.contains(Modifiers::CTRL)
{
println!("Saving...");
e.prevent_default();
}
// Ctrl+Z to undo
if e.key == Key::Character("z".to_string())
&& e.modifiers.contains(Modifiers::CTRL)
{
println!("Undo");
}
})
.child(label().text(format!("Count: {}", count())))
}
Arrow Key Navigation
fn arrow_navigation() -> impl IntoElement {
let position = use_state(|| (0, 0));
rect()
.expanded()
.a11y_focusable(true)
.on_key_down(move |e| {
match &e.key {
Key::Named(NamedKey::ArrowUp) => {
position.with_mut(|(x, y)| *y -= 1);
}
Key::Named(NamedKey::ArrowDown) => {
position.with_mut(|(x, y)| *y += 1);
}
Key::Named(NamedKey::ArrowLeft) => {
position.with_mut(|(x, y)| *x -= 1);
}
Key::Named(NamedKey::ArrowRight) => {
position.with_mut(|(x, y)| *x += 1);
}
_ => {}
}
})
.child(label().text(format!("Position: {:?}", position())))
}
Touch Events
For touch-specific handling on touchscreens.
Available Handlers
| Handler | Description |
|---|---|
on_touch_start | Touch started |
on_touch_move | Touch moved |
on_touch_end | Touch ended |
on_touch_cancel | Touch cancelled |
Touch Event Data
struct TouchEventData {
global_location: CursorPoint,
element_location: CursorPoint,
finger_id: u64, // Unique ID for this finger
phase: TouchPhase, // Started, Moved, Ended, Cancelled
force: Option<Force>, // Pressure (if available)
}
Touch Phases
enum TouchPhase {
Started, // Finger touched down
Moved, // Finger moved
Ended, // Finger lifted
Cancelled, // Touch cancelled (e.g., incoming call)
}
Example: Multi-Touch Tracking
fn touch_tracker() -> impl IntoElement {
let touches = use_state(|| HashMap::<u64, CursorPoint>::new());
rect()
.expanded()
.background(Color::from_rgb(30, 30, 30))
.on_touch_start(move |e| {
touches.with_mut(|t| {
t.insert(e.finger_id, e.element_location());
});
})
.on_touch_move(move |e| {
touches.with_mut(|t| {
t.insert(e.finger_id, e.element_location());
});
})
.on_touch_end(move |e| {
touches.with_mut(|t| {
t.remove(&e.finger_id);
});
})
.child(
rect()
.expanded()
.child(label().text(format!("Active touches: {}", touches().len())))
)
}
Wheel Events
Handle mouse wheel and trackpad scrolling.
Event Data
struct WheelEventData {
delta_x: f64, // Horizontal scroll
delta_y: f64, // Vertical scroll
source: WheelSource,
global_location: CursorPoint,
element_location: CursorPoint,
}
Example: Custom Scroll Zoom
fn zoomable() -> impl IntoElement {
let scale = use_state(|| 1.0);
rect()
.expanded()
.on_wheel(move |e| {
// Zoom in/out with scroll wheel
let delta = e.delta_y;
if delta < 0.0 {
scale.with_mut(|s| *s = (*s * 1.1).min(5.0));
} else {
scale.with_mut(|s| *s = (*s / 1.1).max(0.1));
}
})
.child(
rect()
.scale(Scale::new(scale()))
.child(content())
)
}
Global Events
Global events fire regardless of which element is targeted.
Available Handlers
| Handler | Description |
|---|---|
on_global_mouse_up | Any mouse up |
on_global_mouse_down | Any mouse down |
on_global_mouse_move | Any mouse movement |
on_global_key_down | Any key press |
on_global_key_up | Any key release |
Example: Global Escape Handler
fn modal() -> impl IntoElement {
let is_open = use_state(|| true);
if !is_open() {
return rect();
}
rect()
.position(Position::Absolute)
.expanded()
.background(Color::BLACK.with_a(128))
.main_align_center()
.cross_align_center()
.on_pointer_down(|_| {
// Close when clicking outside
})
.on_global_key_down(move |e| {
if e.key == Key::Named(NamedKey::Escape) {
is_open.set(false);
}
})
.child(
rect()
.width(Size::px(400.0))
.background(Color::WHITE)
.corner_radius(12.0)
.padding(24.0)
.on_pointer_down(|e| e.stop_propagation()) // Don't close when clicking inside
.child(label().text("Modal content"))
)
}
Capture Events
Capture events have higher priority than regular events:
rect()
.on_capture_global_key_down(|e| {
// This fires before other key_down handlers
// Useful for global shortcuts
if e.key == Key::Named(NamedKey::Escape) {
println!("Escape captured globally");
}
})
Event Propagation
Events bubble up through the element tree.
Bubbling Example
┌─────────────────────────────┐
│ Parent │
│ ┌───────────────────────┐ │
│ │ Child │ │
│ │ ┌─────────────────┐ │ │
│ │ │ Grandchild │ │ │
│ │ │ [Clicked!] │ │ │
│ │ └─────────────────┘ │ │
│ └───────────────────────┘ │
└─────────────────────────────┘
When you click the grandchild:
- Grandchild receives event
- Child receives event
- Parent receives event
Stopping Propagation
rect()
.on_pointer_down(|_| {
println!("Parent received event");
})
.child(
rect()
.on_pointer_down(|e| {
println!("Child received event");
e.stop_propagation(); // Parent won't receive this event
})
)
File Events
Handle drag-and-drop file operations.
Example
fn file_drop_zone() -> impl IntoElement {
let is_dragging = use_state(|| false);
let file_path = use_state(|| None::<PathBuf>);
rect()
.width(Size::px(400.0))
.height(Size::px(200.0))
.background(if is_dragging() {
Color::BLUE.with_a(50)
} else {
Color::GRAY.with_a(50)
})
.border(Border::new()
.width(2.0)
.fill(if is_dragging() {
Color::BLUE
} else {
Color::GRAY
})
)
.corner_radius(8.0)
.main_align_center()
.cross_align_center()
.on_file_drag_enter(|_| is_dragging.set(true))
.on_file_drag_leave(|_| is_dragging.set(false))
.on_file_drop(move |e| {
is_dragging.set(false);
if let Some(path) = &e.file_path {
file_path.set(Some(path.clone()));
println!("File dropped: {:?}", path);
}
})
.child(
if let Some(path) = file_path.peek() {
label().text(format!("Dropped: {:?}", path))
} else {
label().text("Drop a file here")
}
)
}
Best Practices
1. Use Pointer Events for Interactivity
// Good: Works with both mouse and touch
rect().on_pointer_down(handler)
// Less ideal: Only works with mouse
rect().on_mouse_down(handler)
2. Make Interactive Elements Focusable
rect()
.a11y_focusable(true) // Required for keyboard events
.on_click(|_| {})
.on_key_down(|e| {
if e.key == Key::Named(NamedKey::Enter) {
// Handle Enter key like a click
}
})
3. Stop Propagation When Appropriate
// Modal content should not close when clicked
modal_content
.on_pointer_down(|e| e.stop_propagation())
4. Use Global Events Sparingly
Global events can impact performance - use them only when necessary.
5. Provide Keyboard Alternatives
Every mouse interaction should have a keyboard equivalent for accessibility.
Summary
In this tutorial, you learned:
- Pointer events for unified mouse/touch handling
- Mouse events for button-specific handling
- Keyboard events for shortcuts and text input
- Touch events for multi-touch handling
- Wheel events for scroll and zoom
- Global events for app-wide handling
- Event propagation and stopping it
- File events for drag-and-drop
Previous: Part 6: State Management ←
Next: Part 8: Built-in Components →
In the next tutorial, we’ll explore Freya’s built-in components - buttons, inputs, switches, sliders, and more.