NAME

Clay::UI::Interaction - hover, press and focus state of a Clay::UI

SYNOPSIS

use v5.22;
use warnings;
use feature 'signatures';
no warnings 'experimental::signatures';

use Object::Pad;
use Clay::UI;
use Clay::UI::Box;
use Clay::UI::Role::Interaction::Pressable;
use Clay::UI::Role::Interaction::Focusable;

class My::Panel  :strict(params) :does(Clay::UI::Box) {}
class My::Button :strict(params)
	:does(Clay::UI::Box)
	:does(Clay::UI::Role::Interaction::Pressable)
	:does(Clay::UI::Role::Interaction::Focusable)
{}

my $save   = My::Button->new(id => 'save');
my $cancel = My::Button->new(id => 'cancel');
my $root   = My::Panel->new(id => 'root');
$root->add_child($save, $cancel);
my $ui = Clay::UI->new(width => 400, height => 300, root => $root);

$save->on('OnRelease', sub ($event) { say 'save clicked'; return });

my $interaction = $ui->interaction;

# Synthetic input, for example in a test or for keyboard activation:
$interaction->update(over => [ $root, $save ], down => 1, x => 40, y => 12);
$interaction->update(over => [ $root, $save ], down => 0, x => 40, y => 12);

$interaction->is_hovered($save);    # 1, also $save->is_hovered
$interaction->is_pressed($save);    # 0: the button is up again
my $widgets = $interaction->under_pointer;   # [ $root, $save ]

# Focus, for example from a Tab key handler:
$interaction->focus_next;                    # focuses $save
$interaction->focus_next;                    # focuses $cancel
$interaction->set_focused_widget($save);
my $focused = $interaction->get_focused_widget;   # $save
$interaction->has_focus_within($root);       # 1: $save is below it
my @tab_stops = $interaction->focusables;    # ($save, $cancel)

DESCRIPTION

Every Clay::UI owns one interaction tracker, returned by $ui->interaction. The tracker holds:

It turns pointer input into changes of these sets and fires the events that go with them (OnHoverStart, OnHoverStopped, OnPress, OnRelease, OnScroll, OnFocus, OnBlur; see "EVENTS" in Clay::UI). $ui->render passes it the real pointer every frame through "update"; you may call update with synthetic input between frames. Synthetic state lasts until the next render reports where the real pointer is.

The widget readers is_hovered, is_pressed and is_focused, and the derived hovered, pressed and focused states of Clay::UI::Role::Style::HasStates, ask this object. Widgets hold no interaction state of their own.

Whenever the hovered, armed, pressed or focused widgets change, the tracker bumps the revision (Clay::UI::Revision), because widgets may look different in those states.

CONSTRUCTOR

new

my $interaction = Clay::UI::Interaction->new(ui => $ui, tree_order => $sorter);

Clay::UI creates its tracker itself (read it with "interaction" in Clay::UI); you never need to. Both parameters are required; unknown parameters die.

ui

The Clay::UI the tracker belongs to. The tracker holds it weakly; the methods that need it die once it is freed, with Clay::UI::Interaction::update: its Clay::UI no longer exists (the method's own name in place of update).

tree_order

A coderef called as $sorter->(@widgets) that returns the widgets in the tree order of the last layout (depth-first pre-order). Events of one kind fire in this order. Clay::UI passes a coderef that asks its last completed frame. new dies with Clay::UI::Interaction: 'tree_order' must be a coderef for anything else.

POINTER METHODS

update

$interaction->update(
	over     => [ $panel, $button ],   # widgets under the pointer, topmost root first
	down     => 1,                     # button state
	x        => 40,                    # pointer position for OnPress / OnRelease
	y        => 12,
	scrolled => [ [ $list, 0, -30 ] ], # scroll containers that moved
);

Processes one pointer frame: changes the hovered, armed and pressed widgets, then fires the events. "render" in Clay::UI calls it every frame; call it yourself to feed synthetic input. Named arguments:

over

Required. Arrayref of the widgets under the pointer, in Clay's pointer-over order: the topmost floating element first, and each floating element (or the main tree) in depth-first pre-order. Within one such root, a later widget is therefore a descendant of an earlier one or drawn over it. Every widget must belong to this Clay::UI. Only Hoverable widgets become hovered and only Pressable widgets can be armed or pressed; "under_pointer" reports all of them.

down

Required. Whether the pointer button is down, a plain boolean. A change from up to down is a press, from down to up a release.

x
y

Optional. The pointer position that OnPress and OnRelease carry; finite numbers, default 0.

scrolled

Optional. Arrayref of [ $widget, $delta_x, $delta_y ] entries, one per scroll container (a widget composing Clay::UI::Role::Layout::HasScroll) that moved; each gets an OnScroll with those deltas. A widget may appear only once.

Order of work:

  1. All state changes happen first: hover, arming, pressing (see "PRESS AND RELEASE"), and the list behind "under_pointer".

  2. Then the events fire in this order: every OnHoverStopped, every OnHoverStart, OnPress, OnRelease, every OnScroll. Events of one kind fire in tree order; widgets the last layout did not reach come after the others, in the depth-first pre-order of the tree they are in now.

  3. Every event fires even if a listener dies; update then rethrows the first error.

An event that an earlier listener of the same call made stale is dropped: OnPress, OnRelease and OnScroll for a widget that has left the tree, OnHoverStart for a widget that is no longer hovered, OnHoverStopped for one that is hovered again. A widget gets OnHoverStopped only after its OnHoverStart, and OnBlur only after its OnFocus.

Listeners may change the tree and the focus, but may not call update again, and that includes listeners of the events $ui->render fires.

Returns nothing. Dies (all messages start with Clay::UI::Interaction::update:) for an unknown argument, a missing over or down, a reference as down, a non-finite x or y, a widget in over or scrolled that does not belong to this Clay::UI ('over' must hold widgets of this Clay::UI), a malformed scrolled entry, a scrolled widget that is no scroll container, a widget listed twice in scrolled, and a call from one of its own listeners (called from one of its own listeners). Also dies with Clay::UI::Interaction::update: its Clay::UI no longer exists.

under_pointer

my $widgets = $interaction->under_pointer;

Returns a new arrayref of the widgets of the last update's over (normally: under the pointer at the last render), in that order, Hoverable or not. Widgets that have been freed or removed from the tree since are left out.

is_hovered

my $bool = $interaction->is_hovered($widget);

Returns 1 while $widget is hovered, 0 otherwise. "is_hovered" in Clay::UI::Role::Interaction::Hoverable asks this.

is_armed

my $bool = $interaction->is_armed($widget);

Returns 1 while $widget is armed (see "PRESS AND RELEASE"), 0 otherwise.

is_pressed

my $bool = $interaction->is_pressed($widget);

Returns 1 while $widget is pressed, 0 otherwise. "is_pressed" in Clay::UI::Role::Interaction::Pressable asks this.

PRESS AND RELEASE

Only enabled Pressables take part: a widget composing Clay::UI::Role::Interaction::Disableable that is disabled is never armed or pressed, although it is still hovered. Below, candidates are the enabled Pressables in over, in over's order, unless the Pressable the pointer lands on (the origin, chosen by the rule below among all Pressables in over, enabled or not) is disabled: then there are no candidates. A disabled widget absorbs the click, as a disabled button does in HTML. Pressing a disabled button inside a pressable card arms and presses nothing, and the card gets neither OnPress nor OnRelease; dragging a pressed card onto its disabled button unpresses the card. A disabled card around an enabled button does not stop the button.

Press

On a press (down changes from false to true), every candidate becomes armed, and one of them gets OnPress: the origin.

Origin

Start with the first candidate. Walk through the remaining candidates in order; a candidate replaces the current pick unless it is an ancestor of the current pick, or it lies in a different root than the first candidate. A widget's root is its nearest ancestor (or itself) that composes Clay::UI::Role::Layout::HasFloating with a floating setting whose attach_to is not CLAY_ATTACH_TO_NONE; widgets without such an ancestor share the main root. The last pick is the origin.

In effect: of nested Pressables the innermost one wins (a button inside a pressable card); of overlapping siblings, such as the children of a CLAY_BACK_TO_FRONT container, the later one wins; a Pressable inside a floating element beats every Pressable below that element, because Clay lists the topmost floating element first.

Pressed

After every update, a Pressable is pressed when it is armed, enabled and in over, and down is true. Dragging off an armed widget unpresses it; dragging back on (button still down) presses it again. Dragging onto a widget that was not armed does not press it.

Release

On a release (down changes from true to false), the origin is chosen again by the same rule, but only among the candidates that are armed. It gets OnRelease: a completed click. Then every widget is disarmed. A press on a button inside a pressable card that is dragged off the button and released over the card gives the card its OnRelease, because the press armed both. A release over no armed candidate fires nothing.

OnPress and OnRelease bubble with IF_CONTINUE (Clay::UI::Enum::Bubble): the card in the example above sees the button's OnPress only as a bubbled event, when the button has no OnPress listener or all its listeners return Clay::UI::Enum::Result->CONTINUE.

FOCUS

The focus is held by at most one widget, a Clay::UI::Role::Interaction::Focusable of this UI. It changes only through:

In the last two cases the focus does not simply vanish: it goes to the widget's heir (see "Focus heirs"), which gets OnFocus right after the OnBlur.

render never moves the focus. Event listeners, pointer listeners included, may move it.

The focus follows the widget tree as it is now, while pointer events follow the tree of the last layout: the focus must work before the first render and right after the tree changed, whereas the pointer was tested against the last layout.

"set_focused_widget", "focus_next", "focus_previous", "focusables", "default_next_focus" and "default_previous_focus" die with Clay::UI::Interaction::<method>: its Clay::UI no longer exists once the Clay::UI that owns this tracker has been freed.

Focus scopes

"focusables", "default_next_focus" and "default_previous_focus" take an optional within => $widget: the focus scope, a subtree of this UI ($widget and every widget below it in layout pre-order, see "descendants" in Clay::UI::Role::Core::Element). Without it they cover the whole tree. A modal dialog keeps Tab inside itself with a HasFocusOrder that steps within its own subtree (see "default_next_focus" in Clay::UI::Role::Interaction::HasFocusOrder). within must be a widget of this UI; anything else dies with Clay::UI::Interaction::<method>: 'within' must be a widget of this Clay::UI, and another argument dies with Clay::UI::Interaction::<method>: unknown argument(s): ....

Focus heirs

When the focused widget leaves the tree or can no longer take the focus, the tracker hands the focus to an heir, so that keys keep reaching the part of the screen the user was working in. It walks up from the widget that loses the focus (itself first, then its ancestors) and stops at the first node where one of these holds:

  1. The node stays in the tree and can take the focus ("can_take_focus"): it is the heir.

  2. The node has a "focus_heir" method that returns a widget that stays in the tree and can take the focus: that widget is the heir. Nodes that are being removed are asked too, so a removed popup can name the widget that comes after it.

When no node qualifies, nothing has the focus afterwards. Nodes that are being removed never become the heir themselves, nor may a hook name one. The old widget's OnBlur fires first, then the heir's OnFocus, in the same batch of events; while the OnBlur listeners run, "get_focused_widget" already returns the heir.

focus_heir

method focus_heir () { return $self->active_tab_button }

A hook any widget class may implement (no role declares it): who takes the focus when the focus inside this widget is lost, and the widget itself cannot take it. Called during the walk described in "Focus heirs", without arguments; returns a widget, or undef for "none, ask my ancestors". A widget that is being removed or cannot take the focus right now counts as undef. A tab view names its active tab, a collapsible section its header, a popup the widget that had the focus before it opened.

Anything but a widget or undef is a bug in the class: the walk stops, nothing inherits the focus, and the removal or setter that lost it dies afterwards with Clay::UI::Interaction: <class> returned ... from focus_heir, which is not a widget. A hook that dies is treated the same way, with its own error.

get_focused_widget

my $widget = $interaction->get_focused_widget;

Returns the widget that has the focus, or undef when none has it (or the focused widget has been freed; the tracker holds it weakly).

is_focused

my $bool = $interaction->is_focused($widget);

Returns 1 when $widget has the focus, 0 otherwise. "is_focused" in Clay::UI::Role::Interaction::Focusable asks this.

has_focus_within

my $bool = $interaction->has_focus_within($panel);

Returns 1 when the focused widget is $panel or below it (also below an internal child), 0 otherwise, also when nothing is focused. Use it to draw a panel differently while the keyboard is in it, or to close a popup once the focus left it. Dies with Clay::UI::Interaction::has_focus_within: takes a widget, got ... for anything but a widget.

focusables

my @widgets = $interaction->focusables;
my @inside  = $interaction->focusables(within => $dialog);

Returns the widgets of the scope (see "Focus scopes"; the scope's own widget included) that can take the focus now ("can_take_focus"), in depth-first pre-order of the current tree. Disabled widgets and widgets whose can_focus is false are left out.

can_take_focus

my $bool = $interaction->can_take_focus($widget);

Returns 1 when "set_focused_widget" would accept $widget now: it composes Clay::UI::Role::Interaction::Focusable, belongs to this UI (and is not being removed), and its can_focus is true. Returns 0 otherwise, also for a non-widget. Use it to find a widget to focus, for example the nearest ancestor of a clicked widget that can take the focus:

my $target = $clicked->closest(sub ($node) { $interaction->can_take_focus($node) });
$interaction->set_focused_widget($target) if defined $target;

set_focused_widget

$interaction->set_focused_widget($widget);
$interaction->set_focused_widget(undef);     # clear the focus

Gives the focus to $widget, or clears it for undef. Focusing the widget that already has the focus, or clearing an empty focus, does nothing (no events).

The target is checked before anything changes. Dies with Clay::UI::Interaction::set_focused_widget: target followed by must be a blessed widget, must consume Clay::UI::Role::Interaction::Focusable, does not belong to this Clay::UI or is not currently focusable (can_focus returned false).

Then the focus moves (the derived focused state moves with it), the revision is bumped, and Clay::UI::Events::OnBlur fires at the widget that had the focus (if any), then Clay::UI::Events::OnFocus at the new one (if any). Both fire even if the first listener dies; the first error is rethrown afterwards, with the focus already moved. An OnBlur listener may move the focus again; the OnFocus of this call is then dropped, since its widget no longer has the focus.

focus_next

$interaction->focus_next;

Moves the focus to the next widget, for example on Tab. Returns nothing.

Default order

The depth-first pre-order of the current tree (children before the next sibling), skipping widgets whose can_focus is false and wrapping around at the end. With nothing focused, the first widget that can take the focus gets it. Internal children of widgets count like children.

Entering a subtree with its own order

When the next widget of the default order lies at or below a HasFocusOrder widget that does not hold the focus and has an entry hook ("get_entry_focus" in Clay::UI::Role::Interaction::HasFocusOrder), that widget decides where the focus enters: its hook is called with 'next' and its result is validated like a custom order's. undef lets the default order continue into the subtree. With such widgets nested in each other, the outermost one that does not hold the focus decides. A widget that is the scope of the walk, or above it, is not asked.

Custom order

If the focused widget or one of its ancestors composes Clay::UI::Role::Interaction::HasFocusOrder, the nearest such widget decides: its get_next_focus is called. With nothing focused, the root widget decides if it composes HasFocusOrder. Its result must be undef (the focus stays), or a Focusable widget of this UI, which gets the focus (or the focus stays, when its can_focus is false right now). Anything else dies with Clay::UI::Interaction: <class> returned ..., naming the HasFocusOrder class and what was wrong.

focus_previous

$interaction->focus_previous;

Moves the focus to the previous widget, for example on Shift+Tab: the mirror of "focus_next". With nothing focused, the last widget that can take the focus gets it; a HasFocusOrder decides through its get_previous_focus.

default_next_focus

my $widget = $interaction->default_next_focus;
my $inside = $interaction->default_next_focus(within => $dialog);

Returns the widget the default order (see "focus_next") would focus next, ignoring the orders of HasFocusOrder widgets but honouring their entry hooks, or undef when no widget can take the focus. Changes nothing. A HasFocusOrder widget calls it to fall back to the default order (see "default_next_focus" in Clay::UI::Role::Interaction::HasFocusOrder).

With within (see "Focus scopes") only the scope's widgets count: the next one after the focused widget, wrapping around inside the scope, or the first one when the focus is outside the scope or nothing is focused. The scope itself is never asked for an entry, so a HasFocusOrder that steps within its own subtree does not re-enter its own hook.

default_previous_focus

my $widget = $interaction->default_previous_focus;
my $inside = $interaction->default_previous_focus(within => $dialog);

The mirror of "default_next_focus": with the focus outside the scope, the last widget of the scope.

focus_after

my $next     = $interaction->focus_after($widget, 'next');
my $previous = $interaction->focus_after($widget, 'previous');

Returns the widget the default order would focus after the whole subtree of $widget ('next'), or before it ('previous'), or undef when no widget can take the focus. Changes nothing. This is how a HasFocusOrder widget hands the focus on once it has stepped through its own subtree: $self->ui->interaction->focus_after($self, 'next').

The step stays inside the scope of the nearest HasFocusOrder above $widget: that widget's focus_scope when it has one, else the widget itself. Without such an ancestor, the whole tree is the scope. The order wraps around inside the scope, so a subtree at the end of a modal dialog hands the focus to the first widget of the dialog, not to the window behind it. Entry hooks apply as in "default_next_focus".

$widget must be a widget of this UI, and the direction one of the two words; anything else dies with Clay::UI::Interaction::focus_after: ....

release_ineligible

$interaction->release_ineligible($widget);

Drops what $widget may no longer have, at once. A disabled widget stops being armed and pressed. A focused widget whose can_focus is now false loses the focus to its heir (see "Focus heirs") as through "set_focused_widget": it gets OnBlur and the heir OnFocus before this returns. A widget that may keep everything is left alone. Bumps the revision when a state changed.

The disabled writer of Clay::UI::Role::Interaction::Disableable, the can_focus writer of Clay::UI::Role::Interaction::Focusable and its focus_eligibility_changed call it; a class whose accepts_focus answer changes calls focus_eligibility_changed rather than this.

REMOVED WIDGETS

When a subtree leaves the tree, the tracker releases it at once, during the removal:

  1. Its hovered widgets stop being hovered, its armed and pressed widgets are dropped, its widgets leave "under_pointer", and if a widget inside it has the focus, the focus moves to that widget's heir outside the subtree (see "Focus heirs"), or nowhere. The revision is bumped when a state changed.

  2. The hovered widgets get OnHoverStopped (in tree order), the focused widget gets OnBlur and its heir OnFocus. The removed widgets are still attached to their parents while these events fire, so OnBlur bubbles through the old ancestors and $event->target->parent still works.

Armed or pressed widgets get no event: a pending click is simply dropped, and the next release fires nothing for them.

While these events fire, the subtree already counts as gone: a listener cannot hover, press or focus a widget inside it (update and set_focused_widget die as for a widget of another UI). Every event fires even if a listener dies; the first error is rethrown after the children are detached.

release_subtrees

$interaction->release_subtrees(@top_widgets);

Does the above for every widget in @top_widgets and everything below them. The child methods of Clay::UI::Role::Core::Element (and so of Clay::UI::Role::Core::Container and Clay::UI::Grid) call it once per change, with all removed children, while the children are still attached. A widget class that detaches children some other way must call it too; otherwise you do not call it yourself.

SEE ALSO

Clay::UI ("HOW A FRAME WORKS" in Clay::UI, "EVENTS" in Clay::UI), Clay::UI::Role::Interaction::Hoverable, Clay::UI::Role::Interaction::Pressable, Clay::UI::Role::Interaction::Focusable, Clay::UI::Role::Interaction::Disableable, Clay::UI::Role::Interaction::HasFocusOrder, Clay::Manual.