Event Builder

Event Builder is used to attach external event handlers to views. It's suitable for application-side code: you don't need to write a new view type, nor implement View, just chain bind events when creating views.

Complete method list, handler types and dispatch status see Handler API. This page only explains how to use.

Basic Writing

After importing EventBuilder, all values implementing ViewIdentity can chain bind events. Normal views, ViewBox, and values returned as impl IntoView all satisfy this condition.

use flor::view::builder::EventBuilder;
use flor_lys::label::label;

let item = label("Save")
    .on_click(|view_id, key_state, mouse_position| {
        println!("{view_id} clicked at {mouse_position:?}");
    })
    .on_mouse_enter(|view_id| {
        println!("{view_id} entered");
    })
    .on_mouse_leave(|view_id| {
        println!("{view_id} left");
    });

Complete event callback parameters are not all the same. For example, the complete mouse click parameters are ViewId, KeyState, and MousePosition; mouse enter/leave use ViewId; keyboard events use ViewId, KeyCode, and modifier key state. Most handlers now also support omitting unused parameters. Specific signatures are listed in EventBuilder API and handler wrapper types.

When examples show different parameter counts, read them as different conversion forms for the same handler. Event Builder converts the closure, function, or method item you pass into the corresponding handler. Handlers with extra event parameters usually support four forms: full arguments, no arguments, ViewId only, and without ViewId. Mechanism details are in External Events: Mechanism Explanation and IntoEventHandler API.

Not Only Can Pass Closure

Event Builder's parameter is not "only can pass closure". Source code's method parameter is impl IntoEventHandler<On...Handler, Args>. Handler types keep the full-signature From<F> conversion, and additionally support common forms such as no arguments, ViewId only, or without ViewId. So you can pass a closure, a normal function, an associated function, or a method item.

use flor::base::platform::{KeyState, MousePosition};
use flor::view::builder::EventBuilder;
use flor::view::ViewId;
use flor_lys::label::label;

fn save_clicked(view_id: ViewId, _key_state: KeyState, _pos: MousePosition) {
    println!("save from {view_id}");
}

let save = label("Save")
    .on_click(save_clicked);

Associated function can also be directly passed:

use flor::base::platform::{KeyState, MousePosition};
use flor::view::builder::EventBuilder;
use flor::view::ViewId;
use flor_lys::label::label;

struct Actions;

impl Actions {
    fn open(view_id: ViewId, _key_state: KeyState, _pos: MousePosition) {
        println!("open from {view_id}");
    }
}

let open = label("Open")
    .on_click(Actions::open);

Generic functions or generic associated functions can also be passed, but Rust must see a function item whose type has already been determined:

use flor::view::builder::EventBuilder;
use flor::view::ViewId;
use flor_lys::label::label;

fn trace_click<const SLOT: usize>(view_id: ViewId) {
    println!("slot {SLOT}: {view_id}");
}

let debug = label("Debug")
    .on_click(trace_click::<1>);

Methods with instance state are usually called through a closure that captures the instance; the value passed to the builder is still an Fn matching the target signature.

Closures are suitable for writing a small amount of logic in place. Functions, method items, and generic functions are useful for reusing logic or splitting page code more clearly. As long as the final signature matches, all of these forms are valid.

Understanding the Args Generic

An event method looks like this:

fn on_click<Args>(
    self,
    handler: impl IntoEventHandler<OnClickHandler, Args>,
) -> Self;

Args is not a runtime event parameter, and users do not need to write it when calling the method. It is only a marker for Rust trait inference: full arguments, no arguments, ViewId only, and without ViewId match different IntoEventHandler implementations.

You only need to write this generic parameter when you accept an event handler in your own function and forward it:

use flor::view::builder::EventBuilder;
use flor::view::handler::{IntoEventHandler, OnClickHandler};
use flor::view::View;
use flor_lys::button::button;

fn action_button<Args>(
    text: &'static str,
    on_click: impl IntoEventHandler<OnClickHandler, Args>,
) -> impl View {
    button(text).on_click(on_click)
}

let save = action_button("Save", || {
    println!("save");
});

Omitting Unused Parameters

If you only do not use some parameters, you can still use _ or _name to ignore them:

use flor::view::builder::EventBuilder;
use flor_lys::label::label;

let item = label("Delete")
    .on_click(|view_id, _, _| {
        println!("delete {view_id}");
    });

Now you can also reduce the parameter count directly. Common supported forms include:

use flor::view::builder::EventBuilder;
use flor_lys::label::label;

let save = label("Save").on_click(|| {
    println!("save");
});

let source = label("Source").on_click(|view_id| {
    println!("save from {view_id}");
});

let inspect = label("Inspect").on_click(|key_state, _pos| {
    println!("ctrl: {}", key_state.control_is_down);
});

The full-argument form is still available. Reduced parameters are useful when business logic does not care about the view ID, mouse position, or button state.

Mouse Events

Mouse events are usually used for click, press, release, move, enter and leave.

use flor::view::builder::EventBuilder;
use flor_lys::label::label;

let item = label("Clickable")
    .on_button_down(|view_id, _, pos| {
        println!("down {view_id}: {pos:?}");
    })
    .on_button_up(|view_id, _, pos| {
        println!("up {view_id}: {pos:?}");
    })
    .on_click(|view_id, _, _| {
        println!("click {view_id}");
    });

Regular mouse hit events' MousePosition is target view's local coordinates. Complete mouse event list see Handler API's current dispatch status.

Keyboard Events

Keyboard events are dispatched to current focus view. To let view receive view-level on_key_down / on_key_up, first let it enter focus table; focus mechanism see Focus Mechanism.

use flor::base::platform::{HandleResult, KeyCode};
use flor::view::builder::{EventBuilder, FocusIndexBuilder};
use flor_lys::label::label;

let editor = label("Press Ctrl+S to save")
    .focus_index(0)
    .on_key_down(|code, is_alt, is_ctrl, is_shift| {
        if !is_alt && is_ctrl && !is_shift && code == KeyCode::S {
            println!("save");
            HandleResult::Handled
        } else {
            HandleResult::Default
        }
    });

Keyboard handler returns HandleResult. Returning Handled means this key is already handled; returning Default means using default handling. Keyboard handlers can use the full arguments or omit ViewId; the complete signature is in Keyboard Events API.

Focus and Lifecycle Events

on_focus / on_blur are triggered by focus manager, second parameter is virtual focus number.

use flor::view::builder::{EventBuilder, FocusIndexBuilder};
use flor_lys::label::label;

let input = label("Name")
    .focus_index(0)
    .on_focus(|virtual_index| {
        println!("focus index: {virtual_index}");
    })
    .on_blur(|| {
        println!("blur");
    });

on_create is suitable for doing one-time runtime initialization after view tree creation, for example pushing focus scope for popup layer:

use flor::view::builder::EventBuilder;
use flor_lys::label::label;

let dialog = label("Popup Layer")
    .on_create(|view_id| {
        view_id.push_focus_scope();
    });

Lifecycle-type handler's aliases and parameters see View and Lifecycle Events API.

Feature-gated Events

Drag-drop and theme change — these kinds of events are controlled by feature. After enabling the corresponding feature, related builder methods are available. Complete explanation see Handler API.

use flor::view::builder::EventBuilder;
use flor_lys::label::label;

#[cfg(feature = "drag-drop")]
let drop_area = label("Drop Here")
    .on_drop(|_key_state, _mouse_position, data, _effect| {
        println!("drop: {data:?}");
    });

If you're unsure whether an event is already connected to dispatch path, first see Current Dispatch Status. Tutorial page won't repeatedly maintain each method's complete status table.