Part 7 of 14 21 Jan 2026 By Raj Patil 35 min read

Part 7: Event Handling - Responding to User Input

Part 7 of the Freya Rust GUI series. Learn to handle mouse, keyboard, touch, pointer, wheel, and global events to create interactive applications.

Intermediate #rust #freya #gui #events #input #keyboard #mouse #tutorial
Building Native GUIs with Rust & Freya 7 / 14

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

HandlerDescription
on_pointer_downPointer pressed (mouse or touch)
on_pointer_pressPointer released (click/tap)
on_pointer_enterPointer entered element
on_pointer_leavePointer 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

HandlerDescription
on_mouse_downMouse button pressed
on_mouse_upMouse button released
on_mouse_moveMouse 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

HandlerDescription
on_key_downKey pressed
on_key_upKey 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

HandlerDescription
on_touch_startTouch started
on_touch_moveTouch moved
on_touch_endTouch ended
on_touch_cancelTouch 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

HandlerDescription
on_global_mouse_upAny mouse up
on_global_mouse_downAny mouse down
on_global_mouse_moveAny mouse movement
on_global_key_downAny key press
on_global_key_upAny 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:

  1. Grandchild receives event
  2. Child receives event
  3. 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:


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.