Behaviour for composable widget traits.
Traits are reusable capabilities that can be composed onto widgets. Each trait manages its own slice of state, handles specific events, and can decorate rendering via pre/post render hooks.
A widget opts into traits with use Drafter.Widget, traits: [...]; the functions
here are what that macro calls to turn the trait list into handles, default state,
a capability bitmap, and the scroll configuration.
Built-in trait names
resolve_module/1 maps these atoms to modules; any other atom is taken to be a
module name already.
:focusable-Drafter.Widget.Trait.Focusable:scrollable-Drafter.Widget.Trait.Scrollable:selectable-Drafter.Widget.Trait.Selectable:editable-Drafter.Widget.Trait.Editable:collapsible-Drafter.Widget.Trait.Collapsible:resizable-Drafter.Widget.Trait.Resizable:draggable-Drafter.Widget.Trait.Draggable:animatable-Drafter.Widget.Trait.Animatable
Optional callbacks and their defaults
A trait that omits an optional callback is treated as follows:
dependencies/0- no dependencieshandles/0- handles nothingrender_affecting_fields/0- every key ofdefault_state/0layout_static?/0-true
Summary
Functions
Whether every trait reports layout_static?/0 as true, treating a trait that
does not export it as true. An empty list is static.
Whether any trait reports the name :focusable, which is what makes the widget
take part in tab order.
The capability bitmap for a trait list, for use with handles_event?/2.
The deduplicated union of every trait's handles/0, in trait order.
The deduplicated union of every trait's render_affecting_fields/0, in trait
order.
Whether a bitmap built by build_bitmap/1 carries the bit for event_type.
Every trait's default_state/0 merged left to right, so a later trait wins a
key clash.
Expands trait specs into the full deduplicated list of trait modules.
The module behind a trait name.
The scroll configuration for a trait-mode widget, or nil when
Drafter.Widget.Trait.Scrollable is not among its traits.
Types
@type event_result() :: {:ok, trait_state()} | {:pass, trait_state()} | {:consume, trait_state()}
@type trait_name() :: atom()
@type trait_state() :: map()
Callbacks
@callback default_state() :: trait_state()
@callback dependencies() :: [trait_name()]
@callback handle_event(term(), trait_state(), widget_state :: map()) :: event_result()
@callback handles() :: [atom()]
@callback layout_static?() :: boolean()
@callback name() :: trait_name()
@callback post_render( [Drafter.Draw.Strip.t()], trait_state(), widget_state :: map(), rect :: map() ) :: [ Drafter.Draw.Strip.t() ]
@callback pre_render(trait_state(), widget_state :: map(), rect :: map()) :: {trait_state(), map()}
@callback render_affecting_fields() :: [atom()]
Functions
Whether every trait reports layout_static?/0 as true, treating a trait that
does not export it as true. An empty list is static.
iex> Drafter.Widget.Trait.all_layout_static?([Drafter.Widget.Trait.Focusable])
true
iex> Drafter.Widget.Trait.all_layout_static?([])
true
Whether any trait reports the name :focusable, which is what makes the widget
take part in tab order.
iex> Drafter.Widget.Trait.any_focusable?([Drafter.Widget.Trait.Focusable])
true
iex> Drafter.Widget.Trait.any_focusable?([Drafter.Widget.Trait.Scrollable])
false
@spec build_bitmap([module()]) :: non_neg_integer()
The capability bitmap for a trait list, for use with handles_event?/2.
Ors together the bit of every handle the traits collect, plus the focus bit when any trait is focusable. A handle with no bit assigned contributes nothing.
iex> Drafter.Widget.Trait.build_bitmap([])
0
iex> bitmap = Drafter.Widget.Trait.build_bitmap([Drafter.Widget.Trait.Scrollable])
iex> {Drafter.Widget.Trait.handles_event?(bitmap, :scroll), Drafter.Widget.Trait.handles_event?(bitmap, :drag)}
{true, false}
The deduplicated union of every trait's handles/0, in trait order.
A trait that does not export handles/0 contributes nothing.
iex> Drafter.Widget.Trait.collect_handles([Drafter.Widget.Trait.Focusable])
[:focus, :blur]
iex> Drafter.Widget.Trait.collect_handles([])
[]
The deduplicated union of every trait's render_affecting_fields/0, in trait
order.
A trait that does not export it contributes the keys of its
default_state/0 instead.
iex> Drafter.Widget.Trait.collect_render_affecting_fields([Drafter.Widget.Trait.Focusable])
[:focused]
@spec handles_event?(non_neg_integer(), atom()) :: boolean()
Whether a bitmap built by build_bitmap/1 carries the bit for event_type.
Recognised event types are :scroll, :keyboard, :char, :click, :drag,
:hover, :press, :mouse_up, :focus and :blur. :focus and :blur share
one bit, so a bitmap that answers true for either answers true for both. Any
other atom is always false.
iex> bitmap = Drafter.Widget.Trait.build_bitmap([Drafter.Widget.Trait.Focusable])
iex> {Drafter.Widget.Trait.handles_event?(bitmap, :focus), Drafter.Widget.Trait.handles_event?(bitmap, :blur)}
{true, true}
iex> Drafter.Widget.Trait.handles_event?(0, :keyboard)
false
iex> Drafter.Widget.Trait.handles_event?(0xFFFF, :unknown_event)
false
Every trait's default_state/0 merged left to right, so a later trait wins a
key clash.
iex> Drafter.Widget.Trait.merge_default_states([Drafter.Widget.Trait.Focusable])
%{focused: false}
iex> Drafter.Widget.Trait.merge_default_states([])
%{}
Expands trait specs into the full deduplicated list of trait modules.
Each spec is a trait name, a module, or a {name, opts} pair whose options are
discarded. Every trait's dependencies/0 are pulled in transitively and appear
before the trait that asked for them. The opts argument is ignored.
iex> Drafter.Widget.Trait.resolve_all([:focusable, :scrollable], [])
[Drafter.Widget.Trait.Focusable, Drafter.Widget.Trait.Scrollable]
iex> Drafter.Widget.Trait.resolve_all([{:focusable, step: 2}, :focusable], [])
[Drafter.Widget.Trait.Focusable]
The module behind a trait name.
Returns the built-in module for one of the names listed in the module doc, and
name itself for anything else — no check is made that the result is a module.
iex> Drafter.Widget.Trait.resolve_module(:focusable)
Drafter.Widget.Trait.Focusable
iex> Drafter.Widget.Trait.resolve_module(:not_a_trait)
:not_a_trait
The scroll configuration for a trait-mode widget, or nil when
Drafter.Widget.Trait.Scrollable is not among its traits.
Reads opts[:scroll], itself defaulting to []:
:direction-:vertical | :horizontal. Default:vertical.:step-pos_integer/0rows per wheel notch. Default1.:show_scrollbar-:auto | true | false. Default:auto.
These differ from the handles-mode :scroll defaults documented on
Drafter.Widget, which are :horizontal and a step of 5.
iex> Drafter.Widget.Trait.scroll_config([Drafter.Widget.Trait.Scrollable], [])
%{direction: :vertical, step: 1, show_scrollbar: :auto}
iex> Drafter.Widget.Trait.scroll_config([Drafter.Widget.Trait.Scrollable], scroll: [step: 4])
%{direction: :vertical, step: 4, show_scrollbar: :auto}
iex> Drafter.Widget.Trait.scroll_config([Drafter.Widget.Trait.Focusable], [])
nil