Skip to content

Events

Everything a component does reaches your code as an event: input such as clicks and keys, a control's changes such as an edit or a close, and even drawing and sizing. There is one way to listen to all of them, onEvent. This page covers registering handlers, the two kinds of event, the event interfaces and what they carry, the order things run in, cancellation and bubbling, and how input becomes events. For comparing state before and after an event, see Event snapshots.

Registering a handler

component.onEvent(InputEvents.CLICK, event -> { ... });                         // priority 0
component.onEvent(InputEvents.CLICK, -100, event -> { ... });                   // runs before priority 0
Subscription drag = component.subscribe(InputEvents.DRAG, 0, event -> { ... }); // removable later

onEvent and subscribe come from EventTarget, which every component implements. Two things that are not components are event targets too. A RadioGroup reports SingleSelection.SELECTED when its selected button changes, and an Overlay reports Closable.CLOSED when it closes. Their events reach only the handlers registered on the group or the overlay.

Event types are typed keys. InputEvents.CLICK is an EventType<PointerButtonEvent>, so the handler receives a PointerButtonEvent without a cast. Input types are the constants in InputEvents, and the focus events are on Focusable. A control's events are constants on the attributes it has, such as TextEntry.MODIFIED for every field the user types into and Selection.SELECTED for every list, table, tree and other control that selects, or on the control itself for what only it does, such as Button.PRESS. Every component has Component.PAINT, Component.PAINTED and Component.MEASURED. Always use the constants, because types are compared by identity. The event type reference lists every type.

A handler is an EventHandler, a functional interface with one method, handle(event). A lambda or a method reference fits, and one handler kept in a variable can be registered on several components:

// A handler is an EventHandler of the event interface the type carries, so one can serve several fields
final EventHandler<ValueRequestEvent<TextInput<?>, String>> noTabs =
        event -> event.setValue(event.value().replace(" ", "    "));
name.onEvent(TextInput.EDIT, noTabs);
notes.onEvent(TextInput.EDIT, noTabs);

onEvent returns the component, so registration chains with the setters:

final Button saveButton = factory.button("Save")
        .setEnabled(false)
        .onEvent(Button.PRESSED, event -> save.run())
        .onEvent(InputEvents.POINTER_ENTER, event -> System.out.println("Hovering Save"));

Handlers run on the UI thread, as everything that touches components does. Code on another thread hands its work over first; see The UI thread.

Priority

On one component, handlers for a type run in ascending priority, and handlers with the same priority run in the order they were registered. A handler registered without a priority has priority 0, and the highest number runs last. Use a negative number to see an event before other handlers, for example to veto first, and a positive one to see what everyone before you decided, for example the final value of a request. A handler may register or remove handlers while it runs; the change applies from the next event.

Pre and Post

Events come in two kinds.

Kind What it says Can cancel Fires for Examples
Pre "About to do this" Yes, and requests with a value can be rewritten with setValue Input, and the user's actions on a control KEY, CLICK, Button.PRESS, TextInput.EDIT, Closable.CLOSE, Component.PAINT
Post "This happened" No Every change, whether the user or code made it Button.PRESSED, TextInput.EDITED, Closable.CLOSED, Component.PAINTED

A control's Pre events are its requests and its Post events are its reports. To stop or change something, handle its request. To react to it, handle its report. Not every report has a request: moving the caret (TextEntry.CARET_MOVED, with a TextEntry.Caret of anchor and caret) and a change of a field's validation (Validatable.VALIDATED) are reports only, because there is nothing for a handler to refuse. They fire for the user and for code alike, CARET_MOVED after EDITED and MODIFIED when an edit moved the caret.

// A Pre event: canceling it means the button never presses
final Button delete = factory.button("Delete world")
        .onEvent(Button.PRESS, event -> {
            if (!confirmation.text().equals("DELETE")) {
                event.cancel();
            }
        })
        .onEvent(Button.PRESSED, event -> System.out.println("Deleted"));
// A Pre event with a value: set another one to change what the user did
volume.onEvent(Ranged.CHANGE, event -> event.setValue(Math.min(event.value(), cap[0])));
// A Post event: the text has already changed, by typing or by setText
name.onEvent(TextInput.EDITED, event -> preview.setText(event.value().toUpperCase()));

Changes made from code, through setters such as setText and methods such as select or close, fire no request: code does not need permission. Neither do changes a binding writes or that another person made through the server. They all still fire reports, so a label or a binding hears about every change, and each report says who caused it.

A rewritten value goes through the same checks as one set from code, so a slider still clamps a value a handler set out of range, and a text box still applies its length limit. Each event type's Javadoc says what it accepts and whether null is allowed.

A control fires a request or a report only when something on its path has a handler for that type. Otherwise it makes the change without creating an event, so the events you do not listen to cost nothing.

Who caused it

Every event has an origin, which says who caused it:

Origin Who Put by
Origin.User The person at this screen, through input. Its identity() is the platform's object for them, such as a user profile, or null where the platform names nobody. The host, for every input it dispatches
Origin.Remote Another person, whose change reached this screen through a server or another channel Your code, around the change it applies
Origin.Binding A data binding, with the source it reads The binding, while it writes
Origin.CODE A plain call from code Nobody: it is what an event reports when no frame put an origin

event.origin() reads it. Requests fire only when the origin is the user, so a handler on a request can rely on that. Reports fire for every origin, which is where telling them apart pays off:

notify.onEvent(Toggle.TOGGLED, event -> {
    switch (event.origin()) {
        case Origin.User _ -> status.setText("You turned notifications " + (event.value() ? "on" : "off"));
        case Origin.Remote(final Object who) -> status.setText(who + " changed your notifications");
        case Origin.Binding _, Origin.Code _ -> { }     // the model or code set it; nothing to say
    }
});

The origin is one value of the event's cause. event.cause() gives the rest: a chain of the objects the change passed through, most recent first, and a context of values under CauseKeys. For input the host puts the device (DEVICE, MOUSE or KEYBOARD), the modifiers held, the screen, and for pointer presses and releases the button and the click count, so every event one click causes shares them. A control that acts on another pushes itself on the chain: when a radio group deselects a button, the button's TOGGLED still reports the user's click as its origin, and the group is on its chain.

options.onEvent(Toggle.TOGGLED, event -> {
    final boolean byGroup = event.cause().first(RadioGroup.class).isPresent();
    final boolean byKeyboard = event.cause().context(CauseKeys.DEVICE).orElse(null) == Device.KEYBOARD;
    System.out.println(event.target() + " toggled" + (byGroup ? " by its group" : "")
            + (byKeyboard ? " from the keyboard" : ""));
});

Strata makes the cause only when a handler calls cause(), and origin() reads the origin without making it, so neither costs anything when nobody asks.

Motion reports through the same events. Each step of an animation runs in a frame with the Animation pushed on the chain and Origin.CODE, so a slider's CHANGED that an animation moved names the animation among its causes, and requests stay quiet.

Applying a change for someone else

Code that applies a change on another person's behalf, such as a market update the server sent, opens a frame with that person as the origin. Every event fired inside it reports them, and the controls' requests stay quiet:

// The update arrives on a network thread, so the change is handed to the UI thread, and the frame is
// opened there, around the change
Strata.uiThread().execute(() -> {
    try (CauseFrame _ = factory.causes().pushFrame().put(CauseKeys.ORIGIN, new Origin.Remote(buyer))) {
        price.setText(newPrice);                // its EDITED report says the buyer did it
    }
});

A frame belongs to the UI thread. Work from another thread, such as a network handler, hands the change to the UI thread with Strata.uiThread().execute and opens the frame inside that task, as the example above does. Tasks run with no frame open, so what a task changes reports Origin.CODE unless it opens one. Close frames in a try-with-resources block, so they close however the block ends; closing a frame also closes any frame opened inside it that is still open. Objects you push go on the chain, and you can put values of your own under keys made with CauseKey.of.

On an engine with a cause stack of its own, the adapter can make Strata's stack the engine's: frames you open are then the engine's frames, objects you push are on the cause of the engine's own events fired inside them, and a Strata event's chain is the engine's current cause.

Event interfaces

Each event type names the interface its handlers receive. They all extend Event, and every Pre event is also Cancelable:

Interface Kind Adds Used by
Event Both target(), current(), origin(), cause(), stopPropagation(), isPropagationStopped() Every event
Cancelable Pre cancel(), isCancelled(), consume(), isHandled() Input, requests and Component.PAINT
SnapshotEvent Pre snapshot(), currentSnapshot() and snapshotOf(component); see Event snapshots Input and requests. Reports, paint and measure events have none.
PointerEvent and its subtypes, KeyEvent, CharEvent, FocusEvent, ModifiersEvent Pre What the user did; see Clicks and below. A pointer event also has pointerId() and pressure(). InputEvents, Focusable.FOCUS_GAINED, Focusable.FOCUS_LOST
RequestEvent<C> Pre control() Requests with no value, such as Button.PRESS and Closable.CLOSE
ValueRequestEvent<C, V> Pre control(), value(), setValue(value) Requests with a value, such as TextInput.EDIT and Toggle.TOGGLE
ControlEvent<C> Post control() Reports with no value, such as Button.PRESSED and Closable.CLOSED
ValueEvent<C, V> Post control(), value(), previous() Reports with a value, such as TextInput.EDITED and Toggle.TOGGLED
PaintEvent Post painter(), bounds() Component.PAINTED
PaintEvent.Pre Pre The same, and it can be canceled Component.PAINT
MeasureEvent Post context(), availableWidth(), availableHeight(), width(), height(), setWidth, setHeight Component.MEASURED
LifecycleEvent Post Nothing beyond Event Component.ATTACHED, Component.DETACHED

In a control's events, C is the type that declares the event. For a control's own event it is the control, as in RequestEvent<Button>. For an attribute's event it is the attribute, as in ValueEvent<Toggle<?>, Boolean>, because the same type serves check boxes, radio buttons and switches. control() returns the control that fired the event, typed as C. To reach the concrete control, test target() with instanceof.

// A report with a value: control() is typed as the attribute, value() and previous() as its value
settings.onEvent(Toggle.TOGGLED, event -> {
    final Toggle<?> toggle = event.control();
    System.out.println(toggle + " went from " + event.previous() + " to " + event.value());
    save.setEnabled(true);
});

value() of a report is the value after the change, and previous() the value before it. A report of an action rather than a change, such as TextBox.SUBMITTED or Menu.CHOSEN, has previous() equal to value().

What runs, in what order

For an input event, at every component the event reaches:

  1. The component's handlers, in priority order.
  2. The component's default behavior, such as a button pressing or a text box inserting a character, unless the event has been canceled.

A bubbling event then moves to the parent and repeats, unless something stopped it. A direct event stays at its target.

A control's request is different. The control fires it through its whole path first, so any handler on the way can refuse it, and acts afterwards with the final value, unless the request was canceled. If it acts, it then fires the matching report. Requests and reports have no default behavior at the components they pass; only the control that fired them acts.

How far events travel

An input event climbs from its target to the top of the tree it is in: the screen's content, or the content of the overlay it happened in. An overlay's content has no parent, so input inside a popup never reaches the screen below it.

A control's requests and reports climb the same way, except that they stop at the first composite above the control. A composite is a control built from other controls, such as a table, whose inner list and rows are its private parts. The composite's handlers still run, and the event goes no further, so a handler on your form for Selection.SELECTED hears the table and never the list inside it.

Content that a composite holds for you does not count as a private part. Events from a component in a table cell, an expander's content, a split pane's panels or a navigation view's page pass the composite and every composite above it, so a handler around the table hears a button in one of its cells. A CustomComposite you write behaves the same way: events from parts added with addChild stop at it, and events from content added with addContent pass through. See Extending Strata.

Handling a whole form

Control events bubble, so one handler on a panel hears the controls inside it:

// Every text box in the form, with one handler that runs before the form's handlers at priority 0
form.onEvent(TextInput.EDIT, -100, event -> {
    if (event.value().length() > 32) {
        event.cancel();
    }
});

Where a handler only copies a value between a control and your data, a binding says the same in one call. Each attribute declares keys beside its events, such as Toggle.SELECTED beside Toggle.TOGGLED, and a two-way binding still goes through the control's request, so the handlers on this page keep working:

music.bind(Toggle.SELECTED, model.music);       // instead of a TOGGLED handler and a setSelected call

Canceling and stopping

Call On Effect
cancel() Pre events For input, skips the default behavior of the current component and of every ancestor the event still reaches. For a request, the control does not make the change. For PAINT, the component skips its own drawing. Handlers keep running, and the event keeps bubbling.
stopPropagation() All events The event goes no further up. The current component's remaining handlers and its default behavior still run.
consume() Pre events Both: "handled here".

isCancelled(), isPropagationStopped() and isHandled() (either one) report the state. Pre events implement Cancelable; Post events have no cancel(), because what they report already happened.

When an input event finishes, the host reports it to the adapter as consumed if it was canceled or stopped, and the adapter keeps that input from the engine. A handler that acts on input should call consume(), so ancestors and the engine do not act on it too.

Built-in controls consume the input they act on

A button consumes the clicks and keys that press it, and a text box consumes the keys and characters it edits with. Consuming stops propagation, so that input never reaches the control's ancestors, and a handler on a panel does not see clicks on the buttons inside it. To hear about the buttons, handle Button.PRESSED on the panel; control events still bubble.

Target and current

target() is the component the event was fired at. It is fixed for the whole delivery. current() is the component whose handlers or default behavior are running now. It starts equal to the target and moves to each ancestor as the event bubbles. A control's events also have control(), the control that fired them. It is the target, except for the two event targets that are not components: a radio group's SELECTED has the newly selected button as its target, or the one that lost the selection when it was cleared, and an overlay's CLOSED has the overlay's content.

PointerEvent.localX() and localY() are relative to the current component, so they change as the event bubbles. x() and y() are in host coordinates and do not change.

Handling input on a container

One handler on a container can serve all of its children, because input from children bubbles up to it. This works for children that do not consume the event, such as labels:

final Label selected = factory.label("Pick a color");
final StackPanel palette = factory.stackPanel().setSpacing(2);
for (final String color : new String[] {"Red", "Green", "Blue"}) {
    palette.add(factory.label(color));
}
// Labels have no click behavior, so their clicks bubble up to the panel.
// target() is the label that was clicked; current() is the panel running this handler.
palette.onEvent(InputEvents.CLICK, event -> {
    if (event.target() instanceof final Label label) {
        selected.setText("Picked " + label.text());
        event.consume();
    }
});

The instanceof Label test skips clicks on the panel's own padding and spacing, where the panel itself is the target.

Event objects are short-lived

An event object is valid only while it is being delivered. Do not keep it to read later: Strata reuses paint and measure events every frame, and current() changes as an event bubbles. To keep the state a component had when the event fired, take a snapshot, which stays valid.

Removing handlers

onEvent handlers stay as long as their component. For one that must go earlier, such as a handler that follows a drag, register it with subscribe, which returns a Subscription:

// subscribe registers like onEvent and returns a way to remove the handler
final Subscription hover = button.subscribe(InputEvents.POINTER_ENTER, 0, event -> System.out.println("Hi"));
hover.close();

A Subscription is AutoCloseable, so it also works in try-with-resources. close() and unsubscribe() do the same thing, and calling either twice does nothing.

Removing a component from its tree does not remove its handlers. That rarely matters, because a closed screen's tree is dropped along with them. It matters if you keep a component and reuse it.

Exceptions

A handler that throws is not caught by Strata. The handlers after it for the same event do not run, and the exception propagates out of the input call that caused the event, into the adapter and the engine. Catch your own errors in handlers.

Clicks

A press, a release and a click are three events:

  1. POINTER_PRESS fires at the deepest component under the pointer. Just before it fires, focus moves to that component or its nearest focusable ancestor, or is cleared if there is none.
  2. POINTER_RELEASE fires at the component the press started on, wherever the pointer is now.
  3. CLICK fires after the release, at the same component, if the pointer is still over it, the component is enabled, and no drag happened in between.

Each mouse button is tracked separately, so a right-button press during a left-button drag is its own press.

Open overlays come first. A press outside a popup closes the popup and goes no further: focus stays where it was, and no POINTER_PRESS fires. A flyout that turned off closing on a press outside lets the press through. While a dialog is open, nothing below it can be hovered or pressed. See Overlays.

button.onEvent(InputEvents.CLICK, event -> {
    if (event.button() == MouseButton.RIGHT) {
        System.out.println("Context menu at " + event.x() + ", " + event.y());
        event.consume();
    } else if (event.clicks() == 2) {
        System.out.println("Double click");
    }
});

Click counting

clicks() says where a press falls in a run of quick presses: 1 for a single click, 2 for a double, 3 for a triple, and then it starts again at 1. A press continues the run when it hits the same component with the same button within 500 ms and within 4 units on each axis of the previous press. A release and its click carry the count of their press.

Strata delivers every press immediately with its count. It never holds a single click back to wait and see whether a double follows, so a double click produces a click with clicks() == 1 and then one with clicks() == 2. React to the count you care about.

Drags

A press becomes a drag once the pointer moves more than 4 units from where the button went down, measured on either axis.

Event When Positions
DRAG_START The pointer first passes the threshold Those of the original press. Deltas are zero.
DRAG Every move after that, wherever the pointer is The current position, with the movement since the previous drag event in deltaX() and deltaY()
DRAG_END The button comes up The release position. It replaces the CLICK that release would otherwise cause.

Drag events go to the component the press started on, even when the pointer leaves it. startX() and startY() give the press position, and clicks() the press's click count, so a drag after a double click can select by word as text boxes do.

A label that can be dragged

return factory -> {
    final AbsolutePanel board = factory.absolutePanel();
    board.setAnchor(Anchor.FILL);

    // Only containers have a background, so the handle is a panel with a label in it
    final AnchorPanel handle = factory.anchorPanel()
            .setBackground(Paint.solid(0xFF3A3D43))
            .add(factory.label("Drag me").setAnchor(Anchor.CENTER));
    handle.setMinSize(60, 20);
    final float[] position = {20f, 20f};
    board.add(handle, position[0], position[1]);

    // Drag events keep going to the component the press started on, wherever the pointer goes
    handle.onEvent(InputEvents.DRAG, event -> {
        position[0] += event.deltaX();
        position[1] += event.deltaY();
        board.move(handle, position[0], position[1]);
        event.consume();
    });
    return board;
};

If the pointer leaves the host during a drag, the press stays tracked, and the drag continues when the pointer comes back.

Drags that carry data

A DRAG_START handler can attach data to the drag with startTransfer. The components under the pointer then hear five more events, which carry a TransferEvent:

Event Bubbles When
DRAG_ENTER no A drag with data moved onto the component
DRAG_OVER yes The drag moved over the component. A handler calls accept or reject.
DRAG_EXIT no The drag left the component, or ended
DROP yes The button came up over the component that accepted last
TRANSFER_DONE no The drag ended, on the component that started it. Its effect says what the target took.

Drag and drop covers the data, the effects, the drag image and a complete example.

Hover

Only the deepest component under the pointer is hovered, for the mouse and a pen; a touch hovers nothing and fires none of these events (see Devices). Hit testing respects clipping, visibility and enabled state: the pointer cannot hit a hidden or disabled component, or the part of a child cut off by a clipping parent.

Event Bubbles When
POINTER_ENTER no The component became the deepest one under the pointer
POINTER_EXIT no It stopped being the deepest one, or the pointer left the host
POINTER_MOVE yes The pointer moved over it. Fired after any enter or exit for the same movement.

Enter and exit do not bubble, so moving from a panel onto a button inside it gives the panel an exit and the button an enter. When layout moves a component under a still pointer, the enter and exit fire on the next pointer input, not at the moment of the layout. A scroll that something consumed is the exception: the host lays out at once and works out hover again, since the content moved under the pointer.

Scrolling

SCROLL fires at the component under the pointer and bubbles, so an outer container receives scrolls its children do not consume.

Value Meaning
scrollY() Positive when the wheel turns away from the user, which scrolls toward the top of the content
scrollX() Positive toward the left, the start of a row
kind() A ScrollKind: WHEEL for a notched wheel (1 per notch), PRECISE for trackpads and smooth wheels, which report fractions

Amounts are in wheel notches. A component that scrolls by whole steps, such as a list moving by rows, can use kind() to tell a wheel's whole notches from a trackpad's fractions. Engines differ in how horizontal scrolling arrives; see Platform differences.

Keys and characters

KEY carries a physical key (key()), an action (PRESS, REPEAT or RELEASE) and the held modifiers. It fires at the focused component and bubbles. When nothing has focus, it fires at the content of the top dialog, or at the screen's content when no dialog is open. If nothing consumes it, the host goes on: it runs the first enabled command whose shortcut matches, acts on a mnemonic, opens a context menu for the Menu key, presses a default or cancel button for Enter or Escape, closes the top overlay that is not a toast for Escape, and moves focus for Tab. Input and focus gives the order.

CHAR carries a typed character as a code point, after the keyboard layout and any input method. It fires only at the focused component and is dropped when nothing has focus. It bubbles.

A key handler on the screen's content sees every key that nothing focused consumed, which makes it the place for screen-wide shortcuts:

// Key events go to the focused component first and bubble to the root,
// so a handler on the root sees every key nothing else consumed.
root.onEvent(InputEvents.KEY, event -> {
    if (event.is(Key.S, Modifiers.of(Modifier.CONTROL), ModifierPolicy.EXACT)) {
        save.run();
        event.consume();
    }
});

Input and focus covers keys, modifiers, shortcut matching and key repeat in detail.

Focus events

Focusable.FOCUS_LOST and Focusable.FOCUS_GAINED are direct, and fire only at components that are Focusable. When focus moves from one component to another, the old one gets FOCUS_LOST first, then the new one gets FOCUS_GAINED, and both fire after the focus has already changed. other() is the component on the other side of the move, or empty when focus came from or went to nothing. Canceling a focus event cannot keep focus where it was; it only skips the component's default behavior, such as a text box moving its caret to the end.

InputEvents.FOCUS_EXIT and InputEvents.FOCUS_ENTER are the same news for the containers around: they fire on any component, panels included, when focus leaves or enters its subtree, and other() is again the component on the other side. Moving from a section's first field to its second fires neither on the section, since focus stays inside it. A control that keeps focus on a part inside it, such as a table on its list, gets FOCUS_GAINED and FOCUS_LOST itself as well. Input and focus gives the order of all four and an example.

Attaching and detaching

Component.ATTACHED and Component.DETACHED fire when a component joins and leaves a tree that a host is showing. Their LifecycleEvent adds nothing to Event. They are direct: a handler on a panel does not hear its children, because every component of the subtree that joins or leaves hears its own.

for (final Component<?> component : components) {
    final String name = component.id().orElse("?");
    component.onEvent(Component.ATTACHED, event -> log.add("attached " + name));
    component.onEvent(Component.DETACHED, event -> log.add("detached " + name));
}

For a panel that holds a label, adding the panel to an attached container logs attached panel and then attached label, and removing it logs detached label and then detached panel.

  • On attach, parents hear it before their children, and only after the subtree's bindings have set their properties, so a handler sees the bound values.
  • On detach, children hear it before their parent, while the subtree is still attached and its bindings still follow their states. Then the tree is cleared of hover, focus and press state and detached.
  • Each attach a component hears is followed by exactly one detach. A component that is kept and added again, such as the page a navigation view shows again, hears both again.
  • They fire only for a component with a handler for them, build a new event each time and take no snapshots.

Every way a tree changes goes through the same two steps: adding to or removing from an attached container or composite, an expander showing and hiding its content, a navigation view swapping pages, an overlay opening and closing, and a screen opening, closing or being rebuilt (the screen's closed() runs before its tree detaches). A handler that throws does not stop the walk. Strata finishes it, keeps the tree consistent, and throws the first failure at the end with the later ones suppressed.

whileAttached starts something when the component attaches and stops it when it detaches, however often that happens. The supplier returns a Subscription, and it is called at once if the component is already attached. A label that follows a feed only while it shows:

final Label price = ui.label("-");
// Starts when the label joins a shown tree, stops when it leaves, and starts again if it comes back.
// The feed calls the listener on its own thread, so the label changes on the UI thread.
price.whileAttached(() -> feed.latest.subscribe(
        value -> ui.uiThread().execute(() -> price.setText("%.2f".formatted(value)))));

The feed here calls the listener on its own thread, so the label changes inside a task handed to the UI thread. A supplier that returns null throws NullPointerException. A component you write can override onAttached() and onDetached() instead; see Extending Strata.

Drawing and sizing

Drawing and sizing are events too, so you can change how any component looks without writing a new one.

Type Interface When
Component.PAINT PaintEvent.Pre Before the component draws its background. Canceling it skips the component's background, border and content; its children still draw.
Component.PAINTED PaintEvent After the component and its children have drawn, so what a handler draws lands on top
Component.MEASURED MeasureEvent After the component measured its content. setWidth and setHeight change the size it asks for.

A paint event gives the painter() to draw with, valid only for this frame, and the component's bounds() in host coordinates:

// A dot over the button's corner while there is something new
inbox.onEvent(Component.PAINTED, event -> {
    if (unread[0]) {
        final Rect bounds = event.bounds();
        event.painter().fill(new Rect(bounds.right() - 6f, bounds.y() + 2f, 4f, 4f), 0xFFE8A33D);
    }
});

A measure event gives the space offered for the content in availableWidth() and availableHeight(), after the margin and capped by any fixed or maximum size, and possibly infinite. width() and height() start as the size the component asked for. A fixed size set on the component, and its minimum and maximum, still apply to what a handler sets, and negative values count as 0. context() measures text and children for the pass in progress. Do not change layout from a measure handler.

// Never narrower than 80 units, whatever the text
score.onEvent(Component.MEASURED, event -> event.setWidth(Math.max(80f, event.width())));

None of the three bubble, and none take snapshots. They reach only the component's own handlers. A component fires them only when it has a handler for them, and reuses one event object every frame, so a component you do not decorate pays nothing and one you do allocates nothing for the event. Keep paint handlers free of allocation where you can, as in a custom component's paint.

Your own events

A control you write declares its events the way the built-ins do, as constants made with EventType.request, EventType.valueRequest, EventType.report and EventType.valueReport, and fires them with CustomComponent's request and report methods. An event type compares by identity: each call to a factory makes a type equal only to itself, even when another has the same name and event class, so two libraries that both declare EventType.valueReport("changed") get two types and a handler for one never hears the other. The name is only for debugging. Keep each type in a constant, and register handlers and fire events with that constant. Extending Strata shows a complete example.

Input event types

Type Event interface Bubbles Fired at
POINTER_ENTER PointerEvent no The newly hovered component
POINTER_EXIT PointerEvent no The previously hovered component
POINTER_MOVE PointerEvent yes The hovered component
POINTER_PRESS PointerButtonEvent yes The component under the pointer
POINTER_RELEASE PointerButtonEvent yes The component the press started on
CLICK PointerButtonEvent yes The same, if still under the pointer and enabled, with no drag
DRAG_START, DRAG, DRAG_END DragEvent yes The component the press started on
SCROLL ScrollEvent yes The component under the pointer
KEY KeyEvent yes The focused component, or else the top dialog's content, or else the screen's content
CHAR CharEvent yes The focused component only
DRAG_ENTER, DRAG_EXIT TransferEvent no The component a drag with data moved onto or off
DRAG_OVER TransferEvent yes The component under a drag with data, until a handler accepts or rejects
DROP TransferEvent yes The component that accepted the drag last
TRANSFER_DONE TransferEvent no The component the drag started on
FOCUS_GAINED, FOCUS_LOST FocusEvent no The component gaining or losing focus
FOCUS_ENTER, FOCUS_EXIT FocusEvent no A component whose subtree focus entered or left, panels included
MODIFIERS_CHANGED ModifiersEvent no The screen's content and each open overlay's content

The event type reference adds each type's default behavior in the built-in controls, and lists every control event type with its value. Each control's events are also described with the control in Controls.