Skip to content

Event snapshots

A snapshot records a component's state at the moment an event fired, before any handler or default behavior ran. A handler can compare it with the live component to see what the event changed: the text before a key press, whether a box was checked before a click, where a component was before a drag.

Getting a snapshot

There is nothing to ask for. Whenever an event's path has a handler for its type, Strata takes the snapshots before the first handler runs, and any handler on the path can read them:

// Every edit asks first, so the snapshot holds the text, caret and selection from before it
final Deque<TextBox.State> undo = new ArrayDeque<>();
name.onEvent(TextInput.EDIT, event ->
        event.snapshot().state(TextBox.State.class).ifPresent(undo::push));

Inside the handler:

Method Returns
event.snapshot() The target's snapshot
event.currentSnapshot() The snapshot of the component whose handler is running
event.snapshotOf(component) Any component's snapshot, as an Optional

These methods belong to SnapshotEvent, the event interface that input events and a control's requests extend: PointerEvent and its subtypes (drags and transfers included), KeyEvent, CharEvent, FocusEvent, ModifiersEvent and RequestEvent. Other events have no snapshot methods at all, so a handler for one cannot ask. A Post event, such as TextInput.EDITED, fires after its change, so it carries the value from before the change as previous() instead:

// A Post event fires after its change, so it carries the value from before instead of a snapshot
name.onEvent(TextInput.EDITED, event ->
        System.out.println("\"" + event.previous() + "\" became \"" + event.value() + "\""));

What is captured, and when

Before an event is delivered, Strata walks its path: the target and, if the type bubbles, the ancestors it will reach. For a control's request, the path ends at the first composite above the control, as the request itself does (see How far events travel). If no component on that path has a handler for this event type, nothing is captured and nothing is allocated. Otherwise Strata snapshots:

  • the target, always; and
  • every component on the path that has a handler for this type.

Nothing else is captured. All snapshots are taken before the first handler runs, so even a handler at the top of the tree sees the target as it was before the target's own handlers and default behavior changed it. Paint and measure events never take snapshots, PAINT included, although it is a Pre event: PaintEvent.Pre is Cancelable but not a SnapshotEvent.

What "when the event fired" means

Snapshots are taken after the host has updated its own bookkeeping for the input and before the event reaches any handler. A few events change host state first, and their snapshots show the new state:

Event Already updated in the snapshot
POINTER_PRESS Focus has moved to the target or its nearest focusable ancestor, and the target is pressed
POINTER_RELEASE, CLICK, DRAG_END The target is no longer pressed, unless another button still holds it
POINTER_ENTER, POINTER_EXIT The target is hovered, or no longer hovered
FOCUS_GAINED, FOCUS_LOST, FOCUS_ENTER, FOCUS_EXIT Focus has already changed

Everything a handler or a default behavior changes, such as text, caret, checked state or enabled state, is still in its old state.

Shallow

Snapshots are shallow. A snapshot of a panel holds the panel's own state and nothing about its children. A child that was captured has its own snapshot.

// A handler on a container: the snapshots cover the target and this container, not the other children
palette.onEvent(InputEvents.POINTER_PRESS, event -> {
    final ComponentSnapshot target = event.snapshot();
    final ComponentSnapshot self = event.currentSnapshot();
    System.out.println(target.component() + " pressed while " + self.component() + " was at " + self.bounds());
});

What a snapshot holds

ComponentSnapshot is a record. It holds what the Component getters returned when it was taken:

Group Fields
Identity component(), the live component
Flags visible, enabled, hovered, pressed, focused
Layout inputs margin, minWidth, maxWidth, minHeight, maxHeight, horizontalAlignment, verticalAlignment
Layout results bounds, desiredWidth, desiredHeight
Kind-specific state(), or state(Class) for a typed Optional
save.onEvent(InputEvents.CLICK, event -> {
    final ComponentSnapshot before = event.snapshot();
    // Every snapshot has what every component has: flags, layout inputs and bounds
    System.out.println("Clicked at " + before.bounds() + ", focused: " + before.focused());
});

The state is what the component's kind adds. The built-ins record a nested State record:

Component State record Fields
TextBox TextBox.State text, caret, anchor, maxLength, plus selectionStart(), selectionEnd() and hasSelection()
PasswordTextBox PasswordTextBox.State length, caret, anchor, maxLength, revealed, plus selectionStart(), selectionEnd() and hasSelection(). Never the text.
Button Button.State text
TextArea TextArea.State text, caret, anchor, maxLength
DropDownButton DropDownButton.State text, open
CheckBox CheckBox.State state (a CheckState), text
RadioButton, ToggleSwitch RadioButton.State, ToggleSwitch.State selected, text
Slider Slider.State value, min, max, step
Spinner Spinner.State value, min, max, step, decimals
ScrollBar ScrollBar.State value, min, max, visibleAmount
ColorPicker ColorPicker.State color, hue, saturation, brightness, alphaShown
CalendarView CalendarView.State date, shownMonth, zoom
DatePicker, TimePicker, DateTimePicker DatePicker.State, TimePicker.State, DateTimePicker.State date, time or dateTime, and open
Image Image.State sprite, visual, tint, imageWidth, imageHeight, scaling
Avatar Avatar.State value, name, initials, group, badge
InfoBar InfoBar.State severity, title, text, open
Label Label.State text, style, wraps, textAlignment
Flyout Flyout.State open
Expander Expander.State expanded, title
NavigationView NavigationView.State selectedIndex, shownMode, expanded
StackPanel StackPanel.State orientation, spacing, spread
GridPanel GridPanel.State columns, rows, columnSpacing, rowSpacing. Not the children or their cells.
ScrollPanel ScrollPanel.State scrollX, scrollY, maxScrollX, maxScrollY
AnchorPanel, AbsolutePanel and the controls not listed none state() is null

state(Class) returns empty when there is no state or it is of another type, so it is safe to call on any snapshot.

Snapshots outlive the event

Unlike the event, a snapshot stays valid after delivery and never changes. Keep it for undo, logging or animation:

// Snapshots stay valid after the event, so they can be kept for undo or logging
final List<CheckBox.State> history = new ArrayList<>();
box.onEvent(Toggle.TOGGLE, event ->
        event.snapshot().state(CheckBox.State.class).ifPresent(history::add));

A snapshot holds a reference to its live component, so keeping one keeps the component reachable.

Your own components

A CustomComponent adds its state by overriding snapshotState():

@Override
protected Object snapshotState() {
    return new State(this.checked, this.text);
}

Strata calls it only when the component is captured, before any handler runs. Return an immutable value, such as a record, and copy any mutable data: a live list would change along with the component. Record only the component's own state; a container must not include its children, which are captured on their own. Do not change any component from it. The default returns null. The switch example shows a complete component with a snapshot state.

When there is no snapshot

Situation snapshot() currentSnapshot() snapshotOf(c)
A report, or a paint or measure event does not exist: these events are not SnapshotEvents
Any SnapshotEvent read inside a handler works, since the target is always captured works, since a handler is running on the current component present for the target and every component on the path with a handler for the type
A SnapshotEvent read after delivery, or for a component that joined the path throws IllegalStateException for the target or current component when none was taken throws the same empty

A component that joins the path while the event is being delivered, for example because a handler moved the target into another container, has no snapshot.

Cost

An event whose path has no handler for its type costs nothing extra. An event that does have one allocates one snapshot per captured component, plus whatever snapshotState() builds. Prefer narrow event types: a handler for POINTER_MOVE on a root panel snapshots the hovered component on every mouse move.

Why snapshot whenever something listens?

Letting each handler ask for snapshots would need a second registration method, and a handler that forgot to ask would get an exception at the worst moment. A snapshot is a small record taken only on the paths that have a listener, never while painting, so taking it for every listener keeps the API to one method at a cost that only input with handlers pays.