Helpers and validation for Guppy's Elixir-owned UI IR.
Elixir processes build full-tree IR values, Guppy validates the supported schema, and the native runtime renders those trees through GPUI while routing events back to the owning process.
Style tokens are represented as ordered lists. That order is preserved across the bridge so later tokens can override earlier ones.
Summary
Functions
Builds a button with a text label; wire :events (e.g. %{click: callback}).
Builds a data-only canvas that paints ordered rect/rounded-rect/pattern commands.
Builds a checkbox; checked is Elixir-owned and change events report toggles.
Builds a semantic virtualized table from column specs and row data.
Builds a container node with flex/grid layout, styling, and event options.
Builds an icon node; source matches image/2 sources.
Builds an image node. source is a URL string or a {:path, path},
{:uri, uri}, or {:embedded, key} tuple.
Builds a virtualized list of variable-height rows with optional row controls.
Builds a popover trigger labeled label whose overlay children render while
open is true. Anchor/fit/close behavior is controlled through opts.
Builds a radio button carrying value; group state lives in Elixir.
Builds a text node from a list of styled runs.
Builds a scrollable container; :axis may be :x, :y, or :both.
Builds a select control over options; :value and :open are Elixir-owned.
Builds a flexible spacer node.
Builds a text leaf node; opts may carry :id, :style, and :events.
Builds a single-line text input; value is Elixir-owned, edits arrive as change events.
Builds a multi-line text input with the same contract as text_input/2.
Builds a semantic tree with Elixir-owned selection and expansion state.
Builds a virtualized list of uniform-height items.
Returns the raw IR tree from a Guppy.IR.Validated wrapper (or the term unchanged).
Validates a full IR tree, returning :ok or {:error, reason}.
Validates an IR tree and wraps it in Guppy.IR.Validated so later render
calls can skip the full-tree walk.
Like validated/1 but raises ArgumentError on invalid IR.
Types
@type action_bindings() :: %{optional(action_name()) => callback_id()}
@type action_name() :: String.t()
@type animation() :: %{ :id => String.t(), optional(:duration_ms) => pos_integer(), optional(:repeat) => boolean(), optional(:from) => number(), optional(:to) => number() }
@type background_pattern_options() :: [ color: gradient_color(), width: number(), interval: number() ]
@type border_radius_axis() ::
:all
| :top
| :right
| :bottom
| :left
| :top_left
| :top_right
| :bottom_left
| :bottom_right
@type box_shadow_options() :: [ color: gradient_color(), x: number(), y: number(), blur: number(), spread: number() ]
@type button_events() :: %{ optional(:click) => String.t(), optional(:hover) => String.t(), optional(:focus) => String.t(), optional(:blur) => String.t(), optional(:key_down) => String.t(), optional(:key_up) => String.t(), optional(:context_menu) => String.t(), optional(:mouse_down) => String.t(), optional(:mouse_up) => String.t(), optional(:mouse_move) => String.t() }
@type button_node() :: %{ :kind => :button, :label => String.t(), optional(:id) => node_id(), optional(:style) => style(), optional(:hover_style) => style(), optional(:focus_style) => style(), optional(:focus_visible_style) => style(), optional(:in_focus_style) => style(), optional(:active_style) => style(), optional(:disabled_style) => style(), optional(:animation) => animation(), optional(:disabled) => boolean(), optional(:tab_index) => integer(), optional(:actions) => action_bindings(), optional(:shortcuts) => [shortcut_binding()], optional(:events) => button_events() }
@type callback_id() :: String.t()
@type canvas_color() :: gradient_color()
@type canvas_command() :: canvas_rect_command() | canvas_rounded_rect_command() | canvas_pattern_rect_command()
@type canvas_node() :: %{ :kind => :canvas, :commands => [canvas_command()], optional(:id) => node_id(), optional(:style) => style(), optional(:events) => canvas_events() }
@type checkbox_node() :: %{ :kind => :checkbox, :label => String.t(), :checked => boolean(), optional(:id) => node_id(), optional(:style) => style(), optional(:hover_style) => style(), optional(:focus_style) => style(), optional(:focus_visible_style) => style(), optional(:in_focus_style) => style(), optional(:active_style) => style(), optional(:disabled_style) => style(), optional(:disabled) => boolean(), optional(:tab_index) => integer(), optional(:events) => checkbox_events() }
@type color_token() :: :red | :green | :blue | :yellow | :black | :white | :gray
@type data_table_cell() :: %{ :column_id => node_id(), :children => [data_table_cell_node()], optional(:style) => style() }
@type data_table_cell_div_node() :: %{ :kind => :div, :children => [data_table_cell_node()], optional(:id) => node_id(), optional(:style) => style(), optional(:disabled) => boolean(), optional(:events) => list_row_click_events() }
@type data_table_cell_node() :: text_node() | spacer_node() | data_table_cell_div_node()
@type data_table_column_width() :: :auto | {:px, number()} | {:fr, pos_integer()}
@type data_table_events() :: %{ optional(:row_click) => String.t(), optional(:cell_click) => String.t(), optional(:sort) => String.t(), optional(:column_reorder) => String.t(), optional(:column_resize) => String.t(), optional(:row_context_menu) => String.t(), optional(:cell_context_menu) => String.t() }
@type data_table_node() :: %{ :kind => :data_table, :columns => [data_table_column()], :rows => [data_table_row()], optional(:id) => node_id(), optional(:style) => style(), optional(:header_style) => style(), optional(:row_style) => style(), optional(:cell_style) => style(), optional(:selected_row_id) => node_id(), optional(:selected_cell) => {node_id(), node_id()}, optional(:sort) => data_table_sort(), optional(:events) => data_table_events() }
@type data_table_row() :: %{ :id => node_id(), :cells => [data_table_cell()], optional(:style) => style() }
@type data_table_sort() :: %{column_id: node_id(), direction: :asc | :desc}
@type div_events() :: %{ optional(:click) => String.t(), optional(:hover) => String.t(), optional(:focus) => String.t(), optional(:blur) => String.t(), optional(:key_down) => String.t(), optional(:key_up) => String.t(), optional(:context_menu) => String.t(), optional(:drag_start) => String.t(), optional(:drag_move) => String.t(), optional(:drop) => String.t(), optional(:mouse_down) => String.t(), optional(:mouse_up) => String.t(), optional(:mouse_move) => String.t(), optional(:scroll_wheel) => String.t() }
@type div_node() :: %{ :kind => :div, :children => [ir_node()], optional(:id) => node_id(), optional(:style) => style(), optional(:hover_style) => style(), optional(:focus_style) => style(), optional(:focus_visible_style) => style(), optional(:in_focus_style) => style(), optional(:active_style) => style(), optional(:disabled_style) => style(), optional(:animation) => animation(), optional(:disabled) => boolean(), optional(:stack_priority) => non_neg_integer(), optional(:occlude) => boolean(), optional(:focusable) => boolean(), optional(:tab_stop) => boolean(), optional(:tab_index) => integer(), optional(:track_scroll) => boolean(), optional(:anchor_scroll) => boolean(), optional(:scroll_to) => boolean(), optional(:tooltip) => String.t(), optional(:actions) => action_bindings(), optional(:shortcuts) => [shortcut_binding()], optional(:events) => div_events() }
@type gradient_color() :: color_token() | String.t()
@type icon_node() :: %{ :kind => :icon, :source => image_source(), optional(:id) => node_id(), optional(:style) => style() }
@type image_node() :: %{ :kind => :image, :source => image_source(), optional(:id) => node_id(), optional(:style) => style(), optional(:object_fit) => image_object_fit(), optional(:grayscale) => boolean() }
@type image_object_fit() :: :fill | :contain | :cover | :scale_down | :none
@type ir_node() :: text_node() | div_node() | scroll_node() | image_node() | icon_node() | button_node() | checkbox_node() | radio_node() | select_node() | uniform_list_node() | list_node() | data_table_node() | tree_node() | canvas_node() | popover_node() | spacer_node() | text_input_node() | textarea_node()
@type linear_gradient_stop() :: {gradient_color(), number()}
@type list_item() :: %{id: node_id(), children: [list_row_node()]}
@type list_node() :: %{ :kind => :list, :items => [list_item()], optional(:id) => node_id(), optional(:style) => style(), optional(:item_style) => style(), optional(:events) => list_events() }
@type list_row_button_node() :: %{ :kind => :button, :label => String.t(), :id => node_id(), optional(:style) => style(), optional(:hover_style) => style(), optional(:focus_style) => style(), optional(:focus_visible_style) => style(), optional(:in_focus_style) => style(), optional(:active_style) => style(), optional(:disabled_style) => style(), optional(:disabled) => boolean(), optional(:tab_index) => integer(), optional(:events) => list_row_click_events() }
@type list_row_change_events() :: %{optional(:change) => String.t()}
@type list_row_checkbox_node() :: %{ :kind => :checkbox, :label => String.t(), :checked => boolean(), :id => node_id(), optional(:style) => style(), optional(:hover_style) => style(), optional(:focus_style) => style(), optional(:focus_visible_style) => style(), optional(:in_focus_style) => style(), optional(:active_style) => style(), optional(:disabled_style) => style(), optional(:disabled) => boolean(), optional(:tab_index) => integer(), optional(:events) => list_row_change_events() }
@type list_row_click_events() :: %{optional(:click) => String.t()}
@type list_row_div_node() :: %{ :kind => :div, :children => [list_row_node()], optional(:id) => node_id(), optional(:style) => style(), optional(:disabled) => boolean(), optional(:events) => list_row_click_events() }
@type list_row_node() :: text_node() | spacer_node() | list_row_div_node() | list_row_button_node() | list_row_checkbox_node() | list_row_radio_node()
@type list_row_radio_node() :: %{ :kind => :radio, :label => String.t(), :value => String.t(), :checked => boolean(), :id => node_id(), optional(:style) => style(), optional(:hover_style) => style(), optional(:focus_style) => style(), optional(:focus_visible_style) => style(), optional(:in_focus_style) => style(), optional(:active_style) => style(), optional(:disabled_style) => style(), optional(:disabled) => boolean(), optional(:tab_index) => integer(), optional(:events) => list_row_change_events() }
@type node_id() :: String.t()
@type popover_anchor() :: :top_left | :top_right | :bottom_left | :bottom_right
@type popover_anchor_fit() ::
:switch_anchor | :snap_to_window | :snap_to_window_with_margin
@type popover_anchor_position_mode() :: :window | :local
@type popover_node() :: %{ :kind => :popover, :label => String.t(), :open => boolean(), :children => [ir_node()], optional(:id) => node_id(), optional(:style) => style(), optional(:popover_style) => style(), optional(:anchor) => popover_anchor(), optional(:anchor_position) => point(), optional(:anchor_offset) => point(), optional(:anchor_position_mode) => popover_anchor_position_mode(), optional(:anchor_fit) => popover_anchor_fit(), optional(:snap_margin) => number(), optional(:close_on_click_outside) => boolean(), optional(:stack_priority) => non_neg_integer(), optional(:disabled) => boolean(), optional(:events) => popover_events() }
@type radio_node() :: %{ :kind => :radio, :label => String.t(), :value => String.t(), :checked => boolean(), optional(:id) => node_id(), optional(:style) => style(), optional(:hover_style) => style(), optional(:focus_style) => style(), optional(:focus_visible_style) => style(), optional(:in_focus_style) => style(), optional(:active_style) => style(), optional(:disabled_style) => style(), optional(:disabled) => boolean(), optional(:tab_index) => integer(), optional(:events) => checkbox_events() }
@type scroll_axis() :: :x | :y | :both
@type scroll_node() :: %{ :kind => :scroll, :children => [ir_node()], optional(:id) => node_id(), optional(:axis) => scroll_axis(), optional(:style) => style() }
@type select_node() :: %{ :kind => :select, :options => [select_option()], optional(:id) => node_id(), optional(:value) => String.t(), optional(:open) => boolean(), optional(:placeholder) => String.t(), optional(:style) => style(), optional(:list_style) => style(), optional(:option_style) => style(), optional(:anchor) => popover_anchor(), optional(:anchor_offset) => point(), optional(:anchor_fit) => popover_anchor_fit(), optional(:snap_margin) => number(), optional(:disabled) => boolean(), optional(:tab_index) => integer(), optional(:events) => select_events() }
@type shortcut_binding() :: {String.t(), action_name()}
@type style() :: [style_op()]
@type style_axis() :: :all | :x | :y | :top | :right | :bottom | :left
@type style_flag() ::
:grid
| :flex
| :flex_col
| :flex_row
| :flex_wrap
| :flex_nowrap
| :flex_none
| :flex_auto
| :flex_grow
| :flex_shrink
| :flex_shrink_0
| :flex_1
| :col_span_full
| :row_span_full
| :size_full
| :w_full
| :h_full
| :w_32
| :w_64
| :w_96
| :h_32
| :min_w_32
| :min_h_0
| :min_h_full
| :max_w_64
| :max_w_96
| :max_w_full
| :max_h_32
| :max_h_96
| :max_h_full
| :gap_1
| :gap_2
| :gap_4
| :p_1
| :p_2
| :p_4
| :p_6
| :p_8
| :px_2
| :py_2
| :pt_2
| :pr_2
| :pb_2
| :pl_2
| :m_2
| :mx_2
| :my_2
| :mt_2
| :mr_2
| :mb_2
| :ml_2
| :relative
| :absolute
| :top_0
| :right_0
| :bottom_0
| :left_0
| :inset_0
| :top_1
| :right_1
| :top_2
| :right_2
| :bottom_2
| :left_2
| :text_left
| :text_center
| :text_right
| :whitespace_normal
| :whitespace_nowrap
| :truncate
| :text_ellipsis
| :line_clamp_2
| :line_clamp_3
| :text_xs
| :text_sm
| :text_base
| :text_lg
| :text_xl
| :text_2xl
| :text_3xl
| :leading_none
| :leading_tight
| :leading_snug
| :leading_normal
| :leading_relaxed
| :leading_loose
| :font_thin
| :font_extralight
| :font_light
| :font_normal
| :font_medium
| :font_semibold
| :font_bold
| :font_extrabold
| :font_black
| :italic
| :not_italic
| :underline
| :line_through
| :items_start
| :items_center
| :items_end
| :justify_start
| :justify_center
| :justify_end
| :justify_between
| :justify_around
| :cursor_pointer
| :rounded_sm
| :rounded_md
| :rounded_lg
| :rounded_xl
| :rounded_2xl
| :rounded_full
| :border_1
| :border_2
| :border_dashed
| :border_t_1
| :border_r_1
| :border_b_1
| :border_l_1
| :shadow_sm
| :shadow_md
| :shadow_lg
| :overflow_scroll
| :overflow_x_scroll
| :overflow_y_scroll
| :overflow_hidden
| :overflow_x_hidden
| :overflow_y_hidden
@type style_op() :: style_flag() | style_value()
@type style_value() :: {:padding, style_axis(), style_length()} | {:margin, style_axis(), style_length() | :auto} | {:gap, :all | :x | :y, style_length()} | {:width, style_length() | :auto} | {:height, style_length() | :auto} | {:size, style_length() | :auto} | {:min_width, style_length() | :auto} | {:min_height, style_length() | :auto} | {:max_width, style_length() | :auto} | {:max_height, style_length() | :auto} | {:aspect_ratio, number()} | {:position, :relative | :absolute} | {:inset, :all | :top | :right | :bottom | :left, style_length() | :auto} | {:display, :block | :flex | :grid | :none} | {:visibility, :visible | :hidden} | {:overflow, :all | :x | :y, :visible | :clip | :hidden | :scroll} | {:allow_concurrent_scroll, boolean()} | {:restrict_scroll_to_axis, boolean()} | {:debug, boolean()} | {:debug_below, boolean()} | {:cursor, atom()} | {:border_width, style_axis(), {:px | :rem, number()}} | {:border_radius, border_radius_axis(), {:px | :rem, number()}} | {:border_style, :solid | :dashed} | {:shadow, :none | :"2xs" | :xs | :sm | :md | :lg | :xl | :"2xl"} | {:flex_direction, :column | :column_reverse | :row | :row_reverse} | {:flex_wrap, :wrap | :wrap_reverse | :nowrap} | {:flex_item, :one | :auto | :initial | :none | :grow | :shrink | :shrink_0} | {:flex_basis, style_length() | :auto} | {:flex_grow, number()} | {:flex_shrink, number()} | {:align_items, :start | :end | :center | :baseline | :stretch} | {:align_self, :start | :end | :center | :baseline | :stretch} | {:justify_content, :start | :end | :center | :between | :around | :evenly | :stretch} | {:align_content, :normal | :start | :end | :center | :between | :around | :evenly | :stretch} | {:bg, color_token()} | {:text_color, color_token()} | {:text_bg, color_token()} | {:font_size, :xs | :sm | :base | :lg | :xl | :"2xl" | :"3xl"} | {:text_size, {:px | :rem, number()}} | {:line_height, :none | :tight | :snug | :normal | :relaxed | :loose} | {:line_height_length, {:px | :rem | :fraction, number()}} | {:font_weight_value, number()} | {:font_family, String.t()} | {:font_fallbacks, [String.t()]} | {:font_features, [{String.t(), non_neg_integer()}]} | {:border_color, color_token()} | {:bg_hex, String.t()} | {:text_color_hex, String.t()} | {:text_bg_hex, String.t()} | {:border_color_hex, String.t()} | {:text_decoration_color, color_token()} | {:text_decoration_color_hex, String.t()} | {:text_decoration_style, :solid | :wavy} | {:text_decoration_thickness, number()} | {:strikethrough_color, color_token()} | {:strikethrough_color_hex, String.t()} | {:strikethrough_thickness, number()} | {:bg_linear_gradient, angle: number(), from: linear_gradient_stop(), to: linear_gradient_stop()} | {:bg_pattern_slash, background_pattern_options()} | {:box_shadow, [box_shadow_options()]} | {:opacity, number()} | {:line_clamp, pos_integer()} | {:grid_cols, pos_integer()} | {:grid_rows, pos_integer()} | {:col_start, integer() | :auto} | {:col_end, integer() | :auto} | {:row_start, integer() | :auto} | {:row_end, integer() | :auto} | {:col_span, pos_integer() | :full} | {:row_span, pos_integer() | :full} | {:w_px, number()} | {:w_rem, number()} | {:w_frac, number()} | {:h_px, number()} | {:h_rem, number()} | {:h_frac, number()} | {:scrollbar_width_px, number()} | {:scrollbar_width_rem, number()}
@type text_events() :: %{optional(:click) => String.t()}
@type text_input_node() :: %{ :kind => :text_input, :value => String.t(), optional(:id) => node_id(), optional(:placeholder) => String.t(), optional(:style) => style(), optional(:disabled) => boolean(), optional(:tab_index) => integer(), optional(:actions) => action_bindings(), optional(:shortcuts) => [shortcut_binding()], optional(:events) => text_input_events() }
@type text_node() :: %{ :kind => :text, :content => String.t(), optional(:id) => node_id(), optional(:style) => style(), optional(:runs) => [text_run()], optional(:events) => text_events() }
@type textarea_node() :: %{ :kind => :textarea, :value => String.t(), optional(:id) => node_id(), optional(:placeholder) => String.t(), optional(:style) => style(), optional(:disabled) => boolean(), optional(:tab_index) => integer(), optional(:actions) => action_bindings(), optional(:shortcuts) => [shortcut_binding()], optional(:events) => text_input_events() }
@type uniform_list_node() :: %{ :kind => :uniform_list, :items => [uniform_list_item()], optional(:id) => node_id(), optional(:style) => style(), optional(:item_style) => style(), optional(:events) => list_events() }
Functions
@spec button( String.t(), keyword() ) :: button_node()
Builds a button with a text label; wire :events (e.g. %{click: callback}).
@spec canvas( [canvas_command()], keyword() ) :: canvas_node()
Builds a data-only canvas that paints ordered rect/rounded-rect/pattern commands.
@spec checkbox(String.t(), boolean(), keyword()) :: checkbox_node()
Builds a checkbox; checked is Elixir-owned and change events report toggles.
@spec data_table([data_table_column()], [data_table_row()], keyword()) :: data_table_node()
Builds a semantic virtualized table from column specs and row data.
Selection (:selected_row_id, :selected_cell) and :sort are
Elixir-owned state echoed back through table events.
Builds a container node with flex/grid layout, styling, and event options.
See the module type docs for the supported opts keys (:id, :style,
state styles, :events, focus/scroll behavior flags, :actions, and
:shortcuts).
@spec icon( image_source(), keyword() ) :: icon_node()
Builds an icon node; source matches image/2 sources.
@spec image( image_source(), keyword() ) :: image_node()
Builds an image node. source is a URL string or a {:path, path},
{:uri, uri}, or {:embedded, key} tuple.
Builds a virtualized list of variable-height rows with optional row controls.
@spec popover(String.t(), boolean(), [ir_node()], keyword()) :: popover_node()
Builds a popover trigger labeled label whose overlay children render while
open is true. Anchor/fit/close behavior is controlled through opts.
@spec radio(String.t(), String.t(), boolean(), keyword()) :: radio_node()
Builds a radio button carrying value; group state lives in Elixir.
Builds a text node from a list of styled runs.
Runs may be plain strings, {text, style} tuples, or %{text: ..., style: ...}
maps; the node's content is the concatenation of all run text.
@spec scroll( [ir_node()], keyword() ) :: scroll_node()
Builds a scrollable container; :axis may be :x, :y, or :both.
@spec select( [select_option()], keyword() ) :: select_node()
Builds a select control over options; :value and :open are Elixir-owned.
@spec spacer(keyword()) :: spacer_node()
Builds a flexible spacer node.
Builds a text leaf node; opts may carry :id, :style, and :events.
@spec text_input( String.t(), keyword() ) :: text_input_node()
Builds a single-line text input; value is Elixir-owned, edits arrive as change events.
@spec textarea( String.t(), keyword() ) :: textarea_node()
Builds a multi-line text input with the same contract as text_input/2.
Builds a semantic tree with Elixir-owned selection and expansion state.
@spec uniform_list( [uniform_list_item()], keyword() ) :: uniform_list_node()
Builds a virtualized list of uniform-height items.
Returns the raw IR tree from a Guppy.IR.Validated wrapper (or the term unchanged).
@spec validate(ir_node() | Guppy.IR.Validated.t()) :: :ok | {:error, term()}
Validates a full IR tree, returning :ok or {:error, reason}.
@spec validated(ir_node()) :: {:ok, Guppy.IR.Validated.t()} | {:error, term()}
Validates an IR tree and wraps it in Guppy.IR.Validated so later render
calls can skip the full-tree walk.
@spec validated!(ir_node()) :: Guppy.IR.Validated.t()
Like validated/1 but raises ArgumentError on invalid IR.