Drafter.Widget.Trait behaviour (drafter v0.3.1)

Copy Markdown View Source

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.

Optional callbacks and their defaults

A trait that omits an optional callback is treated as follows:

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

event_result()

@type event_result() ::
  {:ok, trait_state()} | {:pass, trait_state()} | {:consume, trait_state()}

trait_name()

@type trait_name() :: atom()

trait_state()

@type trait_state() :: map()

Callbacks

default_state()

@callback default_state() :: trait_state()

dependencies()

(optional)
@callback dependencies() :: [trait_name()]

handle_event(term, trait_state, widget_state)

(optional)
@callback handle_event(term(), trait_state(), widget_state :: map()) :: event_result()

handles()

(optional)
@callback handles() :: [atom()]

layout_static?()

(optional)
@callback layout_static?() :: boolean()

name()

@callback name() :: trait_name()

post_render(list, trait_state, widget_state, rect)

(optional)
@callback post_render(
  [Drafter.Draw.Strip.t()],
  trait_state(),
  widget_state :: map(),
  rect :: map()
) :: [
  Drafter.Draw.Strip.t()
]

pre_render(trait_state, widget_state, rect)

(optional)
@callback pre_render(trait_state(), widget_state :: map(), rect :: map()) ::
  {trait_state(), map()}

render_affecting_fields()

(optional)
@callback render_affecting_fields() :: [atom()]

Functions

all_layout_static?(trait_modules)

@spec all_layout_static?([module()]) :: boolean()

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

any_focusable?(trait_modules)

@spec any_focusable?([module()]) :: boolean()

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

build_bitmap(trait_modules)

@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}

collect_handles(trait_modules)

@spec collect_handles([module()]) :: [atom()]

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([])
[]

collect_render_affecting_fields(trait_modules)

@spec collect_render_affecting_fields([module()]) :: [atom()]

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]

handles_event?(bitmap, event_type)

@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

merge_default_states(trait_modules)

@spec merge_default_states([module()]) :: map()

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([])
%{}

resolve_all(trait_specs, opts)

@spec resolve_all(
  [atom() | {atom(), keyword()} | module()],
  keyword()
) :: [module()]

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]

resolve_module(name)

@spec resolve_module(atom() | module()) :: module()

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

scroll_config(trait_modules, opts)

@spec scroll_config(
  [module()],
  keyword()
) :: map() | nil

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/0 rows per wheel notch. Default 1.
  • :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