NAME
Clay::UI::Role::Core::Element - base role of every Clay::UI element widget
SYNOPSIS
use v5.22;
use Object::Pad;
use Clay::XS qw(sizing_fixed);
use Clay::UI;
use Clay::UI::Role::Core::Container;
use Clay::UI::Role::Layout::HasLayout;
use Clay::UI::Role::Style::HasBackground;
class My::Panel :strict(params)
:does(Clay::UI::Role::Core::Container)
:does(Clay::UI::Role::Layout::HasLayout)
:does(Clay::UI::Role::Style::HasBackground)
{}
my $root = My::Panel->new(
id => 'root',
layout => {
sizing => { width => sizing_fixed(200), height => sizing_fixed(100) },
},
background_color => [255, 0, 0, 255],
);
$root->add_child(My::Panel->new(background_color => [0, 0, 255, 255]));
# The declaration the layout pass sends to Clay for this widget:
my $config = $root->to_config;
# { layout => { sizing => ... }, background_color => [255, 0, 0, 255] }
my $ui = Clay::UI->new(width => 800, height => 600, root => $root);
my $commands = $ui->render;
DESCRIPTION
Clay::UI::Role::Core::Element is the Object::Pad role every element widget composes, directly or through another role. An element widget becomes one Clay element (a box) in each frame. Text widgets are the other kind of widget; they compose Clay::UI::Role::Core::TextNode instead.
The role provides:
an optional
id("id") and the id Clay::UI derives for widgets without one ("resolve_id");the list of children ("children") and of internal children ("internal_children"), with read access only;
to_config("to_config"), which builds the widget's declaration from thecontribute_*methods of its roles (see "EXTENDING THE DECLARATION");mark_changed("mark_changed") for widget classes that keep state of their own.
It also composes Clay::UI::Role::Layout::HasSizingGroup (the width_group and height_group attributes), Clay::UI::Role::Layout::HasParent (parent, root, ui, contains, tree_changed) and Clay::UI::Role::Events::Listener (on).
Element has no public method that changes the children. Widgets that hold any children their user gives them compose Clay::UI::Role::Core::Container, which adds add_child and the removal methods. Widgets that decide their children themselves (such as Clay::UI::Grid) compose Element alone and use the internal child-list methods. The constructor never takes children: a widget starts empty.
Two terms used below:
- declaration
-
The hash of settings Clay receives for one element:
layout,background_color,border,floatingand so on (see Clay::XS::Structs). Clay::UI widgets build it with snake_case keys; the layout pass converts the keys to Clay's camelCase. - layout pass
-
The part of "render" in Clay::UI that declares the widget tree to Clay: for every widget it calls
to_config(ortext_configfor a text widget), opens the Clay element, configures it and walks the children.
CONSTRUCTOR PARAMETERS
id
my $panel = My::Panel->new(id => 'sidebar');
my $name = $panel->id; # 'sidebar', or undef
The element id of the widget: a non-empty string, used as the Clay element id (Clay_GetElementId($id) gives the same numeric id the layout pass uses). Optional; without it the layout pass derives an id from the widget's position (see "resolve_id").
The id reader is read-only: an id is set at construction and never changes.
An undef id means "no id". The constructor dies with Clay::UI: 'id' must be a non-empty string for a reference or an empty string, and with Clay::UI: 'id' must not start with 'anon:' for an id starting with anon:, which is reserved for derived ids.
Two widgets with the same id in one frame are a Clay error; with the default error_handler of Clay::UI the render dies.
width_group
The width sizing group; see "width_group" in Clay::UI::Role::Layout::HasSizingGroup.
height_group
The height sizing group; see "height_group" in Clay::UI::Role::Layout::HasSizingGroup.
METHODS
children
my $kids = $widget->children; # arrayref, a new one on every call
Returns a new arrayref holding the direct children, in order. Changing that array does not change the widget. Internal children (see "add_internal_children") are not included.
child_count
my $count = $widget->child_count;
Returns the number of direct children, without copying them. Internal children are not counted.
child_at
my $first = $widget->child_at(0);
Returns the direct child at $index (0 is the first), without copying the children, or undef for an index past the last child. Internal children are not included. Dies with Clay::UI: child_at takes an index, got ... for anything but a non-negative integer in plain decimal digits (-1, 1.5, '01', undef and references die).
get_children_with
my @foos = $root->get_children_with(sub { ($_->id // '') =~ /^foo_/ });
my @bars = $root->get_children_with(sub ($child) { $child->isa('My::Bar') });
Returns the direct children for which $predicate->($child) is true, as a list (in scalar context: how many). $_ is set to the child as well, so both calling styles work. Does not look at grandchildren or internal children. Text widgets are passed too; their id is undef. Dies with Clay::UI: get_children_with takes a code reference, got ... for anything else.
has_child
$panel->add_child($footer) unless $panel->has_child($footer);
Returns 1 when the widget is one of the direct children (the very object, compared by identity), 0 otherwise: for a widget attached elsewhere, for a grandchild and for an internal child (see "add_internal_children"). Dies with Clay::UI: has_child takes a widget, got ... for anything but a widget, an id included.
internal_children
my $helpers = $widget->internal_children;
Returns a new arrayref holding the internal children, in order.
layout_children
my $laid_out = $widget->layout_children;
Returns a new arrayref holding the children followed by the internal children: everything a frame lays out directly below this widget. The layout pass, the focus order of Clay::UI::Interaction and the registry that maps render commands back to widgets read this list. Widget users want "children".
descendants
my @below = $widget->descendants;
Returns every widget below this one, not the widget itself, as a list in layout pre-order: each entry of "layout_children" followed by its own subtree. Internal children and the widgets below them are included, so this is the order in which a frame declares the subtree and the default focus order walks it. Element widgets and text widgets are listed alike. To ask whether one widget is below another, use "contains" in Clay::UI::Role::Layout::HasParent, which walks up instead.
add_internal_children
$self->add_internal_children($scrollbar);
For widget classes: attaches widgets that the class needs in the laid-out tree but that are not its user's content, for example a floating scrollbar over a scroll container or a popup. Internal children are:
validated and given this widget as
parent, like children (see "ATTACHING CHILDREN"; the same errors apply);laid out after the children, and part of the UI (
ui, events, focus order) like any child;invisible to the widget's user:
children,get_children_withand the mutators of Clay::UI::Role::Core::Container never show or remove them.
Bumps the revision (Clay::UI::Revision) and returns the widget.
remove_internal_children
$self->remove_internal_children($scrollbar);
Detaches the given internal children the way removed children are detached (their parent becomes undef, hover, press and focus inside them are released, see "ATTACHING CHILDREN"). Widgets that are not internal children of this widget are ignored. Bumps the revision when something was removed. Returns the widget. Dies, changing nothing, with Clay::UI: remove_internal_children takes widgets, got ... for anything but widgets.
to_config
my $config = $widget->to_config;
Builds and returns the widget's declaration: a new hashref with snake_case keys. It calls every contribute_* method of the widget's class once, as $self->$name(\%config), so each one adds its part (see "EXTENDING THE DECLARATION"). A widget with nothing to declare returns {}.
The hashref is new on every call, and so are the parts the roles of this distribution write (layout, border, ...), but the values inside them may be the widget's own copies. Treat the result as read-only; copy what you want to change. The layout pass works on a camelCase copy of it.
Do not override to_config. Object::Pad roles have no SUPER, so an override silently drops every contribute_* method; add a contribute_* method instead.
resolve_id
my $id = $widget->resolve_id($base, \@indices);
Returns the string the layout pass hashes with Clay_GetElementId for this widget: the user's id when it has one, otherwise an anonymous id derived from the widget's position:
'anon:' . length($base) . ":$base/" . join('/', @indices)
$base is the id of the nearest ancestor with an id ('' when there is none) and @indices the positions in layout_children on the way down from that ancestor. For example:
root without an id anon:0:/
its third child anon:0:/2
child 1 of the widget 'list' anon:4:list/1
child 0 of that child anon:4:list/1/0
The length prefix keeps anonymous ids apart from each other (a/b as an id cannot be confused with an index path), and the reserved anon: prefix keeps them apart from user ids.
An anonymous id changes when the widget moves: inserting a sibling before it, or before one of its anonymous ancestors, gives it a new id. Give an id to widgets that need the same Clay element id from frame to frame: scroll containers (Clay::UI::Role::Layout::HasScroll requires one), elements with Clay transitions, and elements you look up with Clay::XS functions such as Clay_GetElementData.
The layout pass supplies the arguments; calling resolve_id yourself is only useful in tests.
mark_changed
$widget->mark_changed;
Bumps the revision (Clay::UI::Revision) and returns the widget. The accessors of this distribution's roles bump it themselves, and so does every change to the children. A widget class that keeps state of its own, which its contribute_* methods turn into the declaration, calls mark_changed from its setters, so that a renderer that skips unchanged frames draws the next one.
EXTENDING THE DECLARATION
to_config knows nothing about layout, colours or borders. It finds every method of the widget's class whose name starts with contribute_ - from the class, its superclasses and every composed role - and calls each one with the declaration hash under construction. Each method writes one part of the declaration. This is how Clay::UI::Role::Layout::HasLayout adds layout, Clay::UI::Role::Style::HasBackground adds background_color, and so on.
A widget class adds a declaration part Clay::UI has no role for by defining a contribute_<name> method. This class adds Clay's aspectRatio (see "aspectRatio" in Clay::XS::Structs):
use v5.22;
use Object::Pad;
use Clay::XS qw(sizing_fixed);
use Clay::UI;
use Clay::UI::Box;
use Scalar::Util ();
class My::Picture :strict(params) :does(Clay::UI::Box) {
field $aspect_ratio :param = 1;
# The constructor and the setter check the value the same way.
sub checked_ratio {
my ($ratio) = @_;
die "My::Picture: aspect_ratio must be a positive number\n"
unless Scalar::Util::looks_like_number($ratio) && $ratio > 0;
return $ratio;
}
ADJUST {
checked_ratio($aspect_ratio);
}
method aspect_ratio (@new) {
return $aspect_ratio unless @new;
$aspect_ratio = checked_ratio($new[0]);
$self->mark_changed; # the declaration changes
return $aspect_ratio;
}
# Adds the aspectRatio part to the declaration.
method contribute_aspect_ratio ($config) {
$config->{aspect_ratio} = { aspect_ratio => $aspect_ratio };
return;
}
}
my $picture = My::Picture->new(
layout => { sizing => { width => sizing_fixed(200) } },
background_color => [90, 90, 90, 255],
aspect_ratio => 2,
);
my $ui = Clay::UI->new(width => 800, height => 600, root => $picture);
my $commands = $ui->render; # the rectangle is 200 x 100
$picture->aspect_ratio(4);
$commands = $ui->render; # now 200 x 50
The rules for contribute_* methods:
Each is called once per
to_configwith the declaration hashref; the return value is ignored. The methods run in alphabetical order of their names (contribute_backgroundbeforecontribute_layout).Write keys in snake_case, the spelling every stored slice and reader uses (an attribute set with camelCase keys reads back in snake_case). camelCase works too, since the layout pass converts every key, but do not use both spellings of one key: merged slices then collide.
A part that another method may also write must be merged, not replaced. Clay::UI::Role::Layout::HasLayout merges the user's
layoutover what is already there; defaults go under what is already there, as Clay::UI::Grid does for itslayout:method contribute_my_defaults ($config) { $config->{layout} = { child_gap => 4, %{ $config->{layout} // {} } }; return; }Do not set a value that another
contribute_*method of the class also sets and that cannot be merged, for example acontribute_state_colourthat setsbackground_colorin a class that also has thebackground_colorattribute (Clay::UI::Role::Style::HasBackground): the result then depends on the order of the method names. To set the background by state (Clay::UI::Role::Style::HasStates), either leave thebackground_colorattribute unset, or compose the roles without HasBackground instead of Clay::UI::Box (as theSettings::Toggleclass in examples/12-ui-interaction.pl does).Write fresh hashes and arrays into the declaration; do not hand out a container the widget keeps changing.
Do not set
user_data: the layout pass sets it to find the widget again, andrenderdies withClay::UI: widget ... set user_data in its config.A setter that changes what a
contribute_*method writes calls "mark_changed".Clay::XS checks the declaration only when the element is configured, and ignores unknown keys there: a misspelled key in a
contribute_*method is silently dropped. Values of the wrong shape makerenderdie. Validate in the constructor and the setters (check_structof Clay::XS checks a value against a Clay struct, see "CHECKING STRUCTS" in Clay::XS).The methods run for every widget in every frame; keep them cheap.
Method names must be unique: Object::Pad dies when two composed roles provide the same contribute_* method, or when a class defines one that a role it composes already provides (Method 'contribute_layout' clashes with the one provided by role ...). The roles of this distribution use these names:
contribute_background(Clay::UI::Role::Style::HasBackground)contribute_border(Clay::UI::Role::Style::HasBorder)contribute_clip(Clay::UI::Role::Layout::HasScroll)contribute_corner_radius(Clay::UI::Role::Style::HasCornerRadius)contribute_floating(Clay::UI::Role::Layout::HasFloating)contribute_grid_defaults(Clay::UI::Grid)contribute_layout(Clay::UI::Role::Layout::HasLayout)contribute_sizing_group(Clay::UI::Role::Layout::HasSizingGroup, part of every element widget)
A subclass may override a contribute_* method of its superclass; only the override runs.
The list of contribute_* methods is looked up once per class and kept, because Object::Pad classes do not change once compiled.
ATTACHING CHILDREN
Every change to the children - through Clay::UI::Role::Core::Container, Clay::UI::Grid or "add_internal_children" - checks all new children before anything changes, so a call that dies leaves the widget as it was. It dies when a new child:
is not a widget:
Clay::UI: child is not a widget (got ...). A widget is a blessed object composingClay::UI::Role::Core::Elementor Clay::UI::Role::Core::TextNode;appears twice in the call:
Clay::UI: the same widget (...) is attached twice in one call;is the widget itself or one of its ancestors:
Clay::UI: cannot attach a widget to itself or to one of its descendants;is the root of a Clay::UI:
Clay::UI: widget ... is the root of a Clay::UI and cannot become a child;still has a parent, even if it is this widget:
Clay::UI: widget ... is still attached to a parent; remove it first(see "ATTACHING AND REMOVING" in Clay::UI::Role::Layout::HasParent).
Every change bumps the revision (Clay::UI::Revision). Widgets that decide their children themselves may also reorder them ("reorder_rows" in Clay::UI::Grid); reordering detaches nothing.
Once a change is complete, every widget of each added or removed subtree gets a call of its tree_changed hook, the subtree's own widget first, then the others in "descendants" order. Every hook runs; the change method then dies with the first error.
A removed child is detached: its parent becomes undef and its subtree is no longer part of the UI until it is attached again. Hover, press and focus inside the subtree are released first: a hovered widget gets OnHoverStopped, the focused widget gets OnBlur (see Clay::UI::Interaction). A listener of those events that dies does not stop the removal: the call completes (the tree_changed hooks included) and then dies with the listener's error.
SEE ALSO
Clay::UI, Clay::UI::Role::Core::Container, Clay::UI::Role::Core::TextNode, Clay::UI::Box, Clay::UI::Revision, Clay::XS::Structs, Clay::Manual.