External Events

External events are a way to append event logic to already existing views. You don't need to implement a new View just to handle one click, one mouse move, one shortcut. Create view then chain call on_* methods, hand closure to Flor.

These on_* methods come from EventBuilder, import it before use:

use flor::view::builder::EventBuilder;

Complete signature see Handler API. This page first learns by usage process: start from one click, then gradually use mouse position, keyboard, focus, lifecycle and drag-drop.

First Click Event

Most common usage is directly chaining .on_click(...) after view:

use flor::signal::{Read, Write, create_signal};
use flor::view::builder::EventBuilder;
use flor_lys::button::button;
use flor_lys::label::label;

let count = create_signal(0);

let click_count = count;
let add = button("Add One").on_click(move || {
    click_count.update(|value| *value += 1);
});

let text = label(move || format!("count: {}", count.get()));

The complete on_click closure has three parameters:

|view_id, key_state, mouse_position| { ... }

At first you can write a no-argument closure like above and focus only on the business logic. When you need the event source, mouse position, or modifier key state, add the parameters back. Mouse events also support receiving only ViewId, or omitting ViewId and receiving only KeyState and MousePosition.

If different examples show different parameter counts, that does not mean the event has multiple dispatch rules. It is the same handler conversion mechanism. Event Builder converts the closure, function, or method item you pass into the target handler. Handlers with extra event parameters usually support four writing forms:

FormExampleUse Case
Full arguments|view_id, key_state, mouse_position| { ... }You need the full event context
No arguments|| { ... }You only need to trigger business logic
ViewId only|view_id| { ... }You only care which view triggered the event
Without ViewId|key_state, mouse_position| { ... }You only care about event data

Events whose complete parameter list is only ViewId, such as on_mouse_enter and on_create, only support the "full arguments" and "no arguments" forms. For the underlying mechanism, see Mechanism Explanation. For complete signatures, see Handler API.

Event closures usually write signal, send command, open window, request redraw, or post business message to own task system. Just remember one thing: event closures will execute in GUI event processing flow, don't do long blocking work inside.

Any View Can Attach Events

EventBuilder is implemented for all types implementing ViewIdentity, so events are not button exclusive. A label, a container, or a custom view can attach external events as long as it can provide a ViewId. A function returning impl IntoView can also continue chaining event bindings:

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

let title = label("Click Title").on_click(|view_id, _, _| {
    println!("clicked {view_id}");
});

The first complete argument is the ViewId of the view that triggered the event. It can be used to access view state, request redraw, set focus, capture mouse, and similar operations. Normal business code may not need it and can omit it directly; when you need to associate an event with a specific view, it is the most direct entry point.

Mouse Position and Button State

Mouse-type events mostly use same set of parameters:

|view_id, key_state, mouse_position|

key_state records mouse button and modifier key state when event occurs; mouse_position saves coordinates. For on_mouse_move, on_button_down, on_button_up, on_click, on_double_click — these kinds of mouse hit events, coordinates will be converted to target view's local coordinates, (0, 0) means view top-left corner.

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

let area = label("Drag Here").on_mouse_move(|view_id, key_state, pos| {
    if key_state.lbutton_is_down {
        println!("drag {view_id}: {}, {}", pos.x, pos.y);
    }
});

Click event is synthetic event: only when left button down and up both land on same hit view, Flor will trigger on_click. If just want to know when button down or up, use on_button_down and on_button_up.

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

let area = label("press area")
    .on_button_down(|_, _, pos| {
        println!("down at {}, {}", pos.x, pos.y);
    })
    .on_button_up(|_, _, pos| {
        println!("up at {}, {}", pos.x, pos.y);
    });

Right button also has corresponding events:

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

let item = label("Right Button Operation").on_right_button_click(|_, _, pos| {
    println!("right click: {}, {}", pos.x, pos.y);
});

Middle button currently exposed on_middle_button_down, on_middle_button_up and on_middle_button_double_click. Source code has internal middle click synthesis logic, but external event API currently doesn't have on_middle_button_click.

Keyboard Events Need to Get Focus First

Keyboard events are dispatched to currently focused view. Normal views if want to participate in Tab focus order, need to pair with focus_index. More complete focus explanation see Focus Mechanism.

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

let shortcut_area = label("Press Ctrl+S")
    .focus_index(1)
    .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
        }
    });

on_key_down and on_key_up both need to return HandleResult. Keyboard events can use the full arguments |view_id, code, is_alt, is_ctrl, is_shift|, or omit ViewId as above:

Return ValueMeaning
HandleResult::HandledThis key is already handled by your code
HandleResult::DefaultHand over to default flow to continue processing

Flor internal view logic and external handler both may return Handled. As long as either side returns Handled, final result is Handled.

Focus Change

on_focus and on_blur are used to listen to focus enter and leave. Closure receives ViewId and a u16 type virtual focus number.

Most views only have one focus point, this number is usually 0. Few composite views can expose multiple virtual focus points through View::on_focus_count, then same view internally can distinguish different focus positions.

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

let focusable = label("Focusable Area")
    .focus_index(1)
    .on_focus(|virtual_index| {
        println!("focus virtual index: {virtual_index}");
    })
    .on_blur(|| {
        println!("blur");
    });

Create Event

on_create will trigger after view tree mounts to window. It's suitable for doing initialization work depending on ViewId already entering view tree.

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

let view = label("created").on_create(|view_id| {
    println!("created {view_id}");
});

If just initializing normal Rust data, usually can directly complete before creating view. Only when logic depends on view already registered, already has ViewId and window relationship, then consider on_create.

Wheel Event

Current wheel external event API name is on_wheel_settings_changed. It triggers in mouse wheel message path, closure receives scroll direction, scroll amount, button state and mouse position.

use flor::base::platform::ScrollAxis;
use flor::view::builder::EventBuilder;
use flor_lys::label::label;

let wheel_area = label("Scroll Here").on_wheel_settings_changed(
    |axis, delta, key_state, pos| {
        match axis {
            ScrollAxis::Vertical => println!("vertical wheel: {delta}"),
            ScrollAxis::Horizontal => println!("horizontal wheel: {delta}"),
        }

        if key_state.control_is_down {
            println!("ctrl wheel: {}, {}", pos.x, pos.y);
        }
    },
);

Wheel dispatch will from hit view upward look for scrollable ancestor; if not found, then post to window root view. Currently this path passes in window client area coordinates, not target view local coordinates.

Drag-Drop Events

Drag-drop events need to enable drag-drop feature. Common flow is:

  1. on_drag_enter judges dragged in data format, sets DropEffect;
  2. on_drag_over continuously updates effect during drag move;
  3. on_drop reads real data and completes business processing;
  4. on_drag_leave cleans up hover state.
use flor::base::platform::{DragData, DragFormat, DropEffect};
use flor::view::builder::EventBuilder;
use flor_lys::label::label;

let drop_area = label("Drag Files Here")
    .on_drag_enter(|_, _, formats, effect| {
        let accepts_files = formats
            .iter()
            .any(|format| matches!(format, DragFormat::Files(_)));

        if accepts_files {
            *effect = DropEffect::Copy;
        }
    })
    .on_drop(|_, _, data, effect| {
        if let DragData::Files(files) = data {
            for file in files {
                println!("drop file: {}", file.display());
            }
            *effect = DropEffect::Copy;
        }
    });

on_drag_enter and on_drag_over receive available format list, haven't really extracted data; on_drop will receive DragData. Same as wheel, current drag-drop path passes in window client area coordinates.

Usage Suggestions

External events are suitable for application layer logic: click button modify signal, press shortcut trigger command, drag-drop files to panel, update preview state when mouse move.

If you are writing view library, and event logic is view itself inseparable part, prefer to implement View's internal on_* methods. For example how input field handles IME, cursor, selection and text editing, should belong to view internal logic; application side only needs to bind higher-level external events or the view's own exposed business callbacks.

Event closures need to satisfy Send + Sync + 'static. Common practice is capture signal handle, Arc, channel sender, or other data that can be safely stored long-term. Don't capture temporary references.

Mechanism Explanation

This section is for understanding external events' position in Flor. Normal use doesn't need to read here first.

When each ViewId is created, Flor will put a default ViewHandler in VIEW_STORAGE.handlers. ViewHandler is a set of optional handler slots, for example on_click_handler, on_key_down_handler, on_focus_handler.

When you call:

view.on_click(|view_id, key_state, pos| {
    // ...
})

EventBuilder will take out this view's handler storage, convert the closure through IntoEventHandler into the corresponding handler wrapper type, and write to ViewHandler's on_click_handler slot. Method returns original self, so it can naturally continue chain calling.

This conversion is trait-driven: the full-argument signature first goes through the handler wrapper type's From<F> implementation, while simplified signatures are handled by other IntoEventHandler<T, Args> implementations that fill in the omitted arguments. Args is a marker type used for Rust trait inference. You usually do not write it when calling event methods; you only need to carry this generic parameter out when you write your own function that accepts an event handler. See Framework DSL: Method Composing Views for the forwarding pattern.

After event enters Flor from platform layer, approximate path is:

  1. Platform window procedure converts system message into Message;
  2. WindowsProcHandler hands message to windows::event;
  3. Event bus processes by event type: mouse events first hit test, keyboard events find current focus, drag-drop events maintain current drag-drop target;
  4. After finding target ViewId call corresponding call_* method;
  5. call_* first executes view internal View::on_*, then executes external handler.

Most events are "internal logic + external logic" both will execute. Keyboard events will merge both sides' HandleResult: either side returns Handled, final is Handled. Drag-drop events will first use internal on_drag_* return value to initialize DropEffect, then hand &mut DropEffect to external handler to modify.

Tooltip is an internal special case: its call_tooltip_show and call_tooltip_hide use override mode, if external handler exists, only execute external handler; otherwise execute view internal method. However current EventBuilder doesn't expose the tooltip corresponding chain method, normal users temporarily don't need to depend on this detail.

There are also some handler slots already existing in API, but current event bus hasn't completely dispatched to external handler, for example on_context_menu, on_destroy, on_resize, on_close_requested, on_work_area_changed, on_dpi_change and on_theme_changed. When querying specific status, use Handler API's "Current Dispatch Status" as standard.