use crate::{
etf_decode,
ir_allowed::{allowed_node_event_fields, allowed_node_fields},
};
use eetf::{Atom, Term, Tuple};
use gpui::{KeybindingKeystroke, Keystroke};
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, OnceLock};
pub type DivStyle = Arc<[StyleOp]>;
static IR_FIELD_KEYS: OnceLock<IrFieldKeys> = OnceLock::new();
static EMPTY_IR_NODES: OnceLock<Arc<[IrNode]>> = OnceLock::new();
static EMPTY_SHORTCUTS: OnceLock<Arc<[ShortcutBinding]>> = OnceLock::new();
static EMPTY_STYLE: OnceLock<DivStyle> = OnceLock::new();
static EMPTY_TEXT_RUNS: OnceLock<Arc<[TextRunSegment]>> = OnceLock::new();
static EMPTY_TREE_ITEMS: OnceLock<Arc<[TreeItem]>> = OnceLock::new();
static DEFAULT_BUTTON_STYLE: OnceLock<DivStyle> = OnceLock::new();
static DEFAULT_BUTTON_FOCUS_STYLE: OnceLock<DivStyle> = OnceLock::new();
static DEFAULT_BUTTON_ACTIVE_STYLE: OnceLock<DivStyle> = OnceLock::new();
static DEFAULT_BUTTON_DISABLED_STYLE: OnceLock<DivStyle> = OnceLock::new();
const MAX_IR_NODE_DEPTH: usize = 256;
struct IrFieldKeys {
kind: Term,
id: Term,
content: Term,
runs: Term,
text: Term,
value: Term,
placeholder: Term,
children: Term,
items: Term,
columns: Term,
rows: Term,
cells: Term,
column_id: Term,
width: Term,
sortable: Term,
pinned: Term,
header_style: Term,
row_style: Term,
cell_style: Term,
selected_row_id: Term,
selected_cell: Term,
sort: Term,
row_click: Term,
cell_click: Term,
column_reorder: Term,
column_resize: Term,
row_context_menu: Term,
cell_context_menu: Term,
direction: Term,
nodes: Term,
selected_id: Term,
expanded: Term,
select: Term,
toggle: Term,
commands: Term,
op: Term,
x: Term,
y: Term,
height: Term,
fill: Term,
color: Term,
line_width: Term,
interval: Term,
radius: Term,
options: Term,
axis: Term,
style: Term,
item_style: Term,
list_style: Term,
option_style: Term,
hover_style: Term,
focus_style: Term,
focus_visible_style: Term,
in_focus_style: Term,
active_style: Term,
disabled_style: Term,
animation: Term,
duration_ms: Term,
repeat: Term,
from: Term,
to: Term,
disabled: Term,
tab_index: Term,
actions: Term,
shortcuts: Term,
label: Term,
open: Term,
events: Term,
click: Term,
close: Term,
hover: Term,
focus: Term,
blur: Term,
change: Term,
key_down: Term,
key_up: Term,
context_menu: Term,
drag_start: Term,
drag_move: Term,
drop: Term,
mouse_down: Term,
mouse_up: Term,
mouse_move: Term,
scroll_wheel: Term,
stack_priority: Term,
occlude: Term,
focusable: Term,
tab_stop: Term,
track_scroll: Term,
anchor_scroll: Term,
scroll_to: Term,
tooltip: Term,
source: Term,
popover_style: Term,
anchor: Term,
anchor_position: Term,
anchor_offset: Term,
anchor_position_mode: Term,
anchor_fit: Term,
snap_margin: Term,
close_on_click_outside: Term,
object_fit: Term,
grayscale: Term,
checked: Term,
}
impl IrFieldKeys {
fn new() -> Self {
Self {
kind: atom_term("kind"),
id: atom_term("id"),
content: atom_term("content"),
runs: atom_term("runs"),
text: atom_term("text"),
value: atom_term("value"),
placeholder: atom_term("placeholder"),
children: atom_term("children"),
items: atom_term("items"),
columns: atom_term("columns"),
rows: atom_term("rows"),
cells: atom_term("cells"),
column_id: atom_term("column_id"),
width: atom_term("width"),
sortable: atom_term("sortable"),
pinned: atom_term("pinned"),
header_style: atom_term("header_style"),
row_style: atom_term("row_style"),
cell_style: atom_term("cell_style"),
selected_row_id: atom_term("selected_row_id"),
selected_cell: atom_term("selected_cell"),
sort: atom_term("sort"),
row_click: atom_term("row_click"),
cell_click: atom_term("cell_click"),
column_reorder: atom_term("column_reorder"),
column_resize: atom_term("column_resize"),
row_context_menu: atom_term("row_context_menu"),
cell_context_menu: atom_term("cell_context_menu"),
direction: atom_term("direction"),
nodes: atom_term("nodes"),
selected_id: atom_term("selected_id"),
expanded: atom_term("expanded"),
select: atom_term("select"),
toggle: atom_term("toggle"),
commands: atom_term("commands"),
op: atom_term("op"),
x: atom_term("x"),
y: atom_term("y"),
height: atom_term("height"),
fill: atom_term("fill"),
color: atom_term("color"),
line_width: atom_term("line_width"),
interval: atom_term("interval"),
radius: atom_term("radius"),
options: atom_term("options"),
axis: atom_term("axis"),
style: atom_term("style"),
item_style: atom_term("item_style"),
list_style: atom_term("list_style"),
option_style: atom_term("option_style"),
hover_style: atom_term("hover_style"),
focus_style: atom_term("focus_style"),
focus_visible_style: atom_term("focus_visible_style"),
in_focus_style: atom_term("in_focus_style"),
active_style: atom_term("active_style"),
disabled_style: atom_term("disabled_style"),
animation: atom_term("animation"),
duration_ms: atom_term("duration_ms"),
repeat: atom_term("repeat"),
from: atom_term("from"),
to: atom_term("to"),
disabled: atom_term("disabled"),
tab_index: atom_term("tab_index"),
actions: atom_term("actions"),
shortcuts: atom_term("shortcuts"),
label: atom_term("label"),
open: atom_term("open"),
events: atom_term("events"),
click: atom_term("click"),
close: atom_term("close"),
hover: atom_term("hover"),
focus: atom_term("focus"),
blur: atom_term("blur"),
change: atom_term("change"),
key_down: atom_term("key_down"),
key_up: atom_term("key_up"),
context_menu: atom_term("context_menu"),
drag_start: atom_term("drag_start"),
drag_move: atom_term("drag_move"),
drop: atom_term("drop"),
mouse_down: atom_term("mouse_down"),
mouse_up: atom_term("mouse_up"),
mouse_move: atom_term("mouse_move"),
scroll_wheel: atom_term("scroll_wheel"),
stack_priority: atom_term("stack_priority"),
occlude: atom_term("occlude"),
focusable: atom_term("focusable"),
tab_stop: atom_term("tab_stop"),
track_scroll: atom_term("track_scroll"),
anchor_scroll: atom_term("anchor_scroll"),
scroll_to: atom_term("scroll_to"),
tooltip: atom_term("tooltip"),
source: atom_term("source"),
popover_style: atom_term("popover_style"),
anchor: atom_term("anchor"),
anchor_position: atom_term("anchor_position"),
anchor_offset: atom_term("anchor_offset"),
anchor_position_mode: atom_term("anchor_position_mode"),
anchor_fit: atom_term("anchor_fit"),
snap_margin: atom_term("snap_margin"),
close_on_click_outside: atom_term("close_on_click_outside"),
object_fit: atom_term("object_fit"),
grayscale: atom_term("grayscale"),
checked: atom_term("checked"),
}
}
}
fn atom_term(name: &str) -> Term {
Term::Atom(Atom::from(name))
}
fn field_keys() -> &'static IrFieldKeys {
IR_FIELD_KEYS.get_or_init(IrFieldKeys::new)
}
#[derive(Clone, Debug, PartialEq)]
pub struct ShortcutBinding {
pub shortcut: String,
pub action: String,
pub callback: String,
pub parsed: KeybindingKeystroke,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StyleAxis {
All,
X,
Y,
Top,
Right,
Bottom,
Left,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum StyleLength {
Px(f32),
Rem(f32),
Fraction(f32),
Auto,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PositionStyle {
Relative,
Absolute,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DisplayStyle {
Block,
Flex,
Grid,
None,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VisibilityStyle {
Visible,
Hidden,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OverflowStyle {
Visible,
Clip,
Hidden,
Scroll,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MouseCursorStyle {
Default,
Pointer,
Text,
Move,
NotAllowed,
ContextMenu,
Crosshair,
VerticalText,
Alias,
Copy,
NoDrop,
Grab,
Grabbing,
EwResize,
NsResize,
NeswResize,
NwseResize,
ColResize,
RowResize,
NResize,
EResize,
SResize,
WResize,
None,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BorderLineStyle {
Solid,
Dashed,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BorderRadiusAxis {
All,
Top,
Right,
Bottom,
Left,
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ShadowStyle {
None,
TwoXs,
Xs,
Sm,
Md,
Lg,
Xl,
TwoXl,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FlexDirectionStyle {
Column,
ColumnReverse,
Row,
RowReverse,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FlexWrapStyle {
Wrap,
WrapReverse,
NoWrap,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FlexItemStyle {
One,
Auto,
Initial,
None,
Grow,
Shrink,
Shrink0,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AlignItemsStyle {
Start,
End,
Center,
Baseline,
Stretch,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum JustifyContentStyle {
Start,
End,
Center,
Between,
Around,
Evenly,
Stretch,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AlignContentStyle {
Normal,
Start,
End,
Center,
Between,
Around,
Evenly,
Stretch,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TextAlignStyle {
Left,
Center,
Right,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WhiteSpaceStyle {
Normal,
NoWrap,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TextOverflowStyle {
Ellipsis,
Truncate,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FontSizeStyle {
Xs,
Sm,
Base,
Lg,
Xl,
TwoXl,
ThreeXl,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LineHeightStyle {
None,
Tight,
Snug,
Normal,
Relaxed,
Loose,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FontWeightStyle {
Thin,
Extralight,
Light,
Normal,
Medium,
Semibold,
Bold,
Extrabold,
Black,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FontStyleValue {
Italic,
Normal,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TextDecorationStyle {
Underline,
LineThrough,
None,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TextDecorationLineStyle {
Solid,
Wavy,
}
#[derive(Clone, Debug, PartialEq)]
pub enum StyleOp {
Grid,
Flex,
FlexCol,
FlexRow,
FlexWrap,
FlexNowrap,
FlexNone,
FlexAuto,
FlexGrow,
FlexShrink,
FlexShrink0,
Flex1,
ColSpanFull,
RowSpanFull,
SizeFull,
WFull,
HFull,
W32,
W64,
W96,
H32,
MinW32,
MinH0,
MinHFull,
MaxW64,
MaxW96,
MaxWFull,
MaxH32,
MaxH96,
MaxHFull,
Gap1,
Gap2,
Gap4,
P1,
P2,
P4,
P6,
P8,
Px2,
Py2,
Pt2,
Pr2,
Pb2,
Pl2,
M2,
Mx2,
My2,
Mt2,
Mr2,
Mb2,
Ml2,
Relative,
Absolute,
Top0,
Right0,
Bottom0,
Left0,
Inset0,
Top1,
Right1,
Top2,
Right2,
Bottom2,
Left2,
TextLeft,
TextCenter,
TextRight,
WhitespaceNormal,
WhitespaceNowrap,
Truncate,
TextEllipsis,
LineClamp2,
LineClamp3,
TextXs,
TextSm,
TextBase,
TextLg,
TextXl,
Text2xl,
Text3xl,
LeadingNone,
LeadingTight,
LeadingSnug,
LeadingNormal,
LeadingRelaxed,
LeadingLoose,
FontThin,
FontExtralight,
FontLight,
FontNormal,
FontMedium,
FontSemibold,
FontBold,
FontExtrabold,
FontBlack,
Italic,
NotItalic,
Underline,
LineThrough,
ItemsStart,
ItemsCenter,
ItemsEnd,
JustifyStart,
JustifyCenter,
JustifyEnd,
JustifyBetween,
JustifyAround,
CursorPointer,
RoundedSm,
RoundedMd,
RoundedLg,
RoundedXl,
Rounded2xl,
RoundedFull,
Border1,
Border2,
BorderDashed,
BorderT1,
BorderR1,
BorderB1,
BorderL1,
ShadowSm,
ShadowMd,
ShadowLg,
OverflowScroll,
OverflowXScroll,
OverflowYScroll,
OverflowHidden,
OverflowXHidden,
OverflowYHidden,
Padding {
axis: StyleAxis,
length: StyleLength,
},
Margin {
axis: StyleAxis,
length: StyleLength,
},
Gap {
axis: StyleAxis,
length: StyleLength,
},
Width(StyleLength),
Height(StyleLength),
Size(StyleLength),
MinWidth(StyleLength),
MinHeight(StyleLength),
MaxWidth(StyleLength),
MaxHeight(StyleLength),
AspectRatio(f32),
Position(PositionStyle),
Inset {
axis: StyleAxis,
length: StyleLength,
},
Display(DisplayStyle),
Visibility(VisibilityStyle),
Overflow {
axis: StyleAxis,
behavior: OverflowStyle,
},
AllowConcurrentScroll(bool),
RestrictScrollToAxis(bool),
Debug(bool),
DebugBelow(bool),
Cursor(MouseCursorStyle),
BorderWidth {
axis: StyleAxis,
length: StyleLength,
},
BorderRadius {
axis: BorderRadiusAxis,
length: StyleLength,
},
BorderStyle(BorderLineStyle),
Shadow(ShadowStyle),
FlexDirection(FlexDirectionStyle),
FlexWrapValue(FlexWrapStyle),
FlexItem(FlexItemStyle),
FlexBasis(StyleLength),
FlexGrowValue(f32),
FlexShrinkValue(f32),
AlignItems(AlignItemsStyle),
AlignSelf(AlignItemsStyle),
JustifyContent(JustifyContentStyle),
AlignContent(AlignContentStyle),
TextAlign(TextAlignStyle),
WhiteSpace(WhiteSpaceStyle),
TextOverflow(TextOverflowStyle),
FontSize(FontSizeStyle),
TextSize(StyleLength),
LineHeight(LineHeightStyle),
LineHeightLength(StyleLength),
FontWeight(FontWeightStyle),
FontWeightValue(f32),
FontStyle(FontStyleValue),
FontFamily(String),
FontFallbacks(Vec<String>),
FontFeatures(Vec<(String, u32)>),
TextDecoration(TextDecorationStyle),
TextDecorationColor(ColorToken),
TextDecorationColorHex(u32),
TextDecorationLineStyle(TextDecorationLineStyle),
TextDecorationThickness(f32),
StrikethroughColor(ColorToken),
StrikethroughColorHex(u32),
StrikethroughThickness(f32),
Bg(ColorToken),
TextColor(ColorToken),
TextBg(ColorToken),
BorderColor(ColorToken),
BgHex(u32),
TextColorHex(u32),
TextBgHex(u32),
BorderColorHex(u32),
BgLinearGradient {
angle: f32,
from: LinearGradientStop,
to: LinearGradientStop,
},
BgPatternSlash(BackgroundPatternSlash),
BoxShadow(Vec<BoxShadowSpec>),
Opacity(f32),
LineClamp(u16),
GridCols(u16),
GridRows(u16),
ColStart(i16),
ColStartAuto,
ColEnd(i16),
ColEndAuto,
RowStart(i16),
RowStartAuto,
RowEnd(i16),
RowEndAuto,
ColSpan(u16),
RowSpan(u16),
WPx(f32),
WRem(f32),
WFrac(f32),
HPx(f32),
HRem(f32),
HFrac(f32),
ScrollbarWidthPx(f32),
ScrollbarWidthRem(f32),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ScrollAxis {
X,
Y,
Both,
}
#[derive(Clone, Debug, PartialEq)]
pub enum StyleColor {
Token(ColorToken),
Hex(u32),
}
#[derive(Clone, Debug, PartialEq)]
pub struct LinearGradientStop {
pub color: StyleColor,
pub percentage: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct BackgroundPatternSlash {
pub color: StyleColor,
pub width: f32,
pub interval: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct BoxShadowSpec {
pub color: StyleColor,
pub x: f32,
pub y: f32,
pub blur: f32,
pub spread: f32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ColorToken {
Red,
Green,
Blue,
Yellow,
Black,
White,
Gray,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ImageSource {
Auto(String),
Uri(String),
Path(String),
Embedded(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ImageObjectFit {
Fill,
Contain,
Cover,
ScaleDown,
None,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PopoverAnchor {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PopoverAnchorPositionMode {
Window,
Local,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PopoverAnchorFit {
SwitchAnchor,
SnapToWindow,
SnapToWindowWithMargin,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CheckboxNode {
pub id: Option<String>,
pub label: String,
pub checked: bool,
pub style: DivStyle,
pub hover_style: DivStyle,
pub focus_style: DivStyle,
pub focus_visible_style: DivStyle,
pub in_focus_style: DivStyle,
pub active_style: DivStyle,
pub disabled_style: DivStyle,
pub disabled: bool,
pub tab_index: Option<isize>,
pub change: Option<String>,
pub focus: Option<String>,
pub blur: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct TextRunSegment {
pub text: String,
pub style: DivStyle,
}
#[derive(Clone, Debug, PartialEq)]
pub struct RadioNode {
pub id: Option<String>,
pub label: String,
pub value: String,
pub checked: bool,
pub style: DivStyle,
pub hover_style: DivStyle,
pub focus_style: DivStyle,
pub focus_visible_style: DivStyle,
pub in_focus_style: DivStyle,
pub active_style: DivStyle,
pub disabled_style: DivStyle,
pub disabled: bool,
pub tab_index: Option<isize>,
pub change: Option<String>,
pub focus: Option<String>,
pub blur: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct UniformListItem {
pub id: String,
pub label: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ListItem {
pub id: String,
pub children: Arc<[IrNode]>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum DataTableColumnWidth {
Auto,
Px(f32),
Fr(u32),
}
#[derive(Clone, Debug, PartialEq)]
pub struct DataTableColumn {
pub id: String,
pub label: String,
pub width: DataTableColumnWidth,
pub sortable: bool,
pub pinned: bool,
pub style: DivStyle,
}
#[derive(Clone, Debug, PartialEq)]
pub struct DataTableCell {
pub column_id: String,
pub children: Arc<[IrNode]>,
pub style: DivStyle,
}
#[derive(Clone, Debug, PartialEq)]
pub struct DataTableRow {
pub id: String,
pub cells: Arc<[DataTableCell]>,
pub style: DivStyle,
}
#[derive(Clone, Debug, PartialEq)]
pub enum DataTableSortDirection {
Asc,
Desc,
}
#[derive(Clone, Debug, PartialEq)]
pub struct DataTableSort {
pub column_id: String,
pub direction: DataTableSortDirection,
}
#[derive(Clone, Debug, PartialEq)]
pub struct DataTableNode {
pub id: Option<String>,
pub columns: Arc<[DataTableColumn]>,
pub rows: Arc<[DataTableRow]>,
pub style: DivStyle,
pub header_style: DivStyle,
pub row_style: DivStyle,
pub cell_style: DivStyle,
pub selected_row_id: Option<String>,
pub selected_cell: Option<(String, String)>,
pub sort: Option<DataTableSort>,
pub row_click: Option<String>,
pub cell_click: Option<String>,
pub sort_callback: Option<String>,
pub column_reorder: Option<String>,
pub column_resize: Option<String>,
pub row_context_menu: Option<String>,
pub cell_context_menu: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct TreeItem {
pub id: String,
pub label: String,
pub expanded: bool,
pub children: Arc<[TreeItem]>,
pub style: DivStyle,
}
#[derive(Clone, Debug, PartialEq)]
pub struct TreeNode {
pub id: Option<String>,
pub nodes: Arc<[TreeItem]>,
pub style: DivStyle,
pub row_style: DivStyle,
pub selected_id: Option<String>,
pub select: Option<String>,
pub toggle: Option<String>,
pub context_menu: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum CanvasCommand {
Rect {
x: f32,
y: f32,
width: f32,
height: f32,
fill: StyleColor,
radius: f32,
},
PatternRect {
x: f32,
y: f32,
width: f32,
height: f32,
color: StyleColor,
line_width: f32,
interval: f32,
radius: f32,
},
}
#[derive(Clone, Debug, PartialEq)]
pub struct CanvasNode {
pub id: Option<String>,
pub commands: Arc<[CanvasCommand]>,
pub style: DivStyle,
pub click: Option<String>,
pub context_menu: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SelectOption {
pub value: String,
pub label: String,
pub disabled: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SelectNode {
pub id: Option<String>,
pub value: Option<String>,
pub open: bool,
pub placeholder: String,
pub options: Arc<[SelectOption]>,
pub style: DivStyle,
pub list_style: DivStyle,
pub option_style: DivStyle,
pub anchor: PopoverAnchor,
pub anchor_offset: Option<(f32, f32)>,
pub anchor_fit: PopoverAnchorFit,
pub snap_margin: f32,
pub disabled: bool,
pub tab_index: Option<isize>,
pub click: Option<String>,
pub change: Option<String>,
pub close: Option<String>,
pub focus: Option<String>,
pub blur: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AnimationSpec {
pub id: String,
pub duration_ms: u64,
pub repeat: bool,
pub from: f32,
pub to: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct DivNode {
pub id: Option<String>,
pub style: DivStyle,
pub hover_style: DivStyle,
pub focus_style: DivStyle,
pub focus_visible_style: DivStyle,
pub in_focus_style: DivStyle,
pub active_style: DivStyle,
pub disabled_style: DivStyle,
pub animation: Option<AnimationSpec>,
pub disabled: bool,
pub stack_priority: Option<usize>,
pub occlude: bool,
pub focusable: bool,
pub tab_stop: Option<bool>,
pub tab_index: Option<isize>,
pub track_scroll: bool,
pub anchor_scroll: bool,
pub scroll_to: bool,
pub tooltip: Option<String>,
pub shortcuts: Arc<[ShortcutBinding]>,
pub children: Arc<[IrNode]>,
pub click: Option<String>,
pub hover: Option<String>,
pub focus: Option<String>,
pub blur: Option<String>,
pub key_down: Option<String>,
pub key_up: Option<String>,
pub context_menu: Option<String>,
pub drag_start: Option<String>,
pub drag_move: Option<String>,
pub drop: Option<String>,
pub mouse_down: Option<String>,
pub mouse_up: Option<String>,
pub mouse_move: Option<String>,
pub scroll_wheel: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum IrNode {
Text {
id: Option<String>,
content: String,
runs: Arc<[TextRunSegment]>,
style: DivStyle,
click: Option<String>,
},
TextInput {
id: Option<String>,
value: String,
placeholder: String,
style: DivStyle,
disabled: bool,
tab_index: Option<isize>,
shortcuts: Arc<[ShortcutBinding]>,
change: Option<String>,
focus: Option<String>,
blur: Option<String>,
context_menu: Option<String>,
},
Textarea {
id: Option<String>,
value: String,
placeholder: String,
style: DivStyle,
disabled: bool,
tab_index: Option<isize>,
shortcuts: Arc<[ShortcutBinding]>,
change: Option<String>,
focus: Option<String>,
blur: Option<String>,
context_menu: Option<String>,
},
Scroll {
id: Option<String>,
axis: ScrollAxis,
style: DivStyle,
children: Arc<[IrNode]>,
},
Image {
id: Option<String>,
source: ImageSource,
style: DivStyle,
object_fit: ImageObjectFit,
grayscale: bool,
},
Icon {
id: Option<String>,
source: ImageSource,
style: DivStyle,
},
Button(Box<DivNode>),
Checkbox(Box<CheckboxNode>),
Radio(Box<RadioNode>),
UniformList {
id: Option<String>,
items: Arc<[UniformListItem]>,
style: DivStyle,
item_style: DivStyle,
click: Option<String>,
context_menu: Option<String>,
},
List {
id: Option<String>,
items: Arc<[ListItem]>,
style: DivStyle,
item_style: DivStyle,
click: Option<String>,
context_menu: Option<String>,
},
DataTable(Box<DataTableNode>),
Tree(Box<TreeNode>),
Canvas(Box<CanvasNode>),
Select(Box<SelectNode>),
Popover {
id: Option<String>,
label: String,
open: bool,
style: DivStyle,
popover_style: DivStyle,
anchor: PopoverAnchor,
anchor_position: Option<(f32, f32)>,
anchor_offset: Option<(f32, f32)>,
anchor_position_mode: PopoverAnchorPositionMode,
anchor_fit: PopoverAnchorFit,
snap_margin: f32,
close_on_click_outside: bool,
stack_priority: Option<usize>,
disabled: bool,
click: Option<String>,
close: Option<String>,
children: Arc<[IrNode]>,
},
Spacer {
id: Option<String>,
style: DivStyle,
},
Div(Box<DivNode>),
}
impl IrNode {
pub fn text(content: impl Into<String>) -> Self {
Self::Text {
id: None,
content: content.into(),
runs: empty_text_runs(),
style: empty_style(),
click: None,
}
}
pub fn decode_etf(bytes: &[u8]) -> Result<Self, String> {
let term = etf_decode::decode_term(bytes)?;
Self::from_term(&term)
}
fn from_term(term: &Term) -> Result<Self, String> {
Self::from_term_at_depth(term, 0)
}
#[inline(never)]
fn from_term_at_depth(term: &Term, depth: usize) -> Result<Self, String> {
ensure_ir_node_depth(depth)?;
let map = expect_map(term)?;
let kind = get_atom_field(map, "kind")?;
let Some(allowed_fields) = allowed_node_fields(kind) else {
return Err(format!("unsupported ir kind: {kind}"));
};
ensure_allowed_fields(map, allowed_fields, kind)?;
ensure_allowed_node_events(kind, map)?;
let id = get_optional_string_field(map, "id")?;
// Explicit ids key retained native state; an empty id would collide
// across nodes. The Elixir validator rejects these too.
if matches!(id.as_deref(), Some("")) {
return Err(format!("{kind} id must not be empty"));
}
match kind {
"text" => decode_text_ir_node(map, id),
"text_input" => decode_text_input_ir_node(map, id),
"textarea" => decode_textarea_ir_node(map, id),
"scroll" => decode_scroll_ir_node(map, id, depth),
"uniform_list" => decode_uniform_list_ir_node(map, id),
"list" => decode_list_ir_node(map, id, depth),
"data_table" => decode_data_table_ir_node(map, id, depth),
"tree" => decode_tree_ir_node(map, id),
"canvas" => decode_canvas_ir_node(map, id),
"select" => decode_select_ir_node(map, id),
"popover" => decode_popover_ir_node(map, id, depth),
"image" => decode_image_ir_node(map, id),
"icon" => decode_icon_ir_node(map, id),
"checkbox" => Ok(Self::Checkbox(Box::new(decode_checkbox_node(map, id)?))),
"radio" => Ok(Self::Radio(Box::new(decode_radio_node(map, id)?))),
"spacer" => decode_spacer_ir_node(map, id),
"button" => Ok(Self::Button(Box::new(decode_button_node(map, id)?))),
"div" => decode_div_ir_node(map, id, depth),
other => Err(format!("unsupported ir kind: {other}")),
}
}
}
fn ensure_ir_node_depth(depth: usize) -> Result<(), String> {
if depth > MAX_IR_NODE_DEPTH {
Err(format!(
"ir node tree exceeds maximum depth of {MAX_IR_NODE_DEPTH}"
))
} else {
Ok(())
}
}
fn next_ir_node_depth(depth: usize) -> usize {
depth.saturating_add(1)
}
#[inline(never)]
fn decode_text_ir_node(map: &HashMap<Term, Term>, id: Option<String>) -> Result<IrNode, String> {
let content = get_string_field(map, "content")?;
let runs = get_text_runs_field(map, &content)?;
Ok(IrNode::Text {
id,
content,
runs,
style: get_div_style(map)?,
click: get_click_event(map)?,
})
}
#[inline(never)]
fn decode_text_input_ir_node(
map: &HashMap<Term, Term>,
id: Option<String>,
) -> Result<IrNode, String> {
let actions = get_div_actions(map)?;
Ok(IrNode::TextInput {
id,
value: get_string_field(map, "value")?,
placeholder: get_optional_string_field(map, "placeholder")?.unwrap_or_default(),
style: get_div_style(map)?,
disabled: get_boolean_field(map, "disabled")?,
tab_index: get_optional_integer_field(map, "tab_index")?,
shortcuts: get_div_shortcuts(map, &actions)?,
change: get_change_event(map)?,
focus: get_focus_event(map)?,
blur: get_blur_event(map)?,
context_menu: get_optional_event(map, "context_menu")?,
})
}
#[inline(never)]
fn decode_textarea_ir_node(
map: &HashMap<Term, Term>,
id: Option<String>,
) -> Result<IrNode, String> {
let actions = get_div_actions(map)?;
Ok(IrNode::Textarea {
id,
value: get_string_field(map, "value")?,
placeholder: get_optional_string_field(map, "placeholder")?.unwrap_or_default(),
style: get_div_style(map)?,
disabled: get_boolean_field(map, "disabled")?,
tab_index: get_optional_integer_field(map, "tab_index")?,
shortcuts: get_div_shortcuts(map, &actions)?,
change: get_change_event(map)?,
focus: get_focus_event(map)?,
blur: get_blur_event(map)?,
context_menu: get_optional_event(map, "context_menu")?,
})
}
#[inline(never)]
fn decode_scroll_ir_node(
map: &HashMap<Term, Term>,
id: Option<String>,
depth: usize,
) -> Result<IrNode, String> {
Ok(IrNode::Scroll {
id,
axis: get_scroll_axis_field(map)?,
style: get_div_style(map)?,
children: get_child_nodes_field(map, depth)?,
})
}
#[inline(never)]
fn decode_uniform_list_ir_node(
map: &HashMap<Term, Term>,
id: Option<String>,
) -> Result<IrNode, String> {
Ok(IrNode::UniformList {
id,
items: get_uniform_list_items_field(map)?,
style: get_div_style(map)?,
item_style: get_style_list_field(map, "item_style")?,
click: get_click_event(map)?,
context_menu: get_optional_event(map, "context_menu")?,
})
}
#[inline(never)]
fn decode_list_ir_node(
map: &HashMap<Term, Term>,
id: Option<String>,
depth: usize,
) -> Result<IrNode, String> {
Ok(IrNode::List {
id,
items: get_list_items_field(map, depth)?,
style: get_div_style(map)?,
item_style: get_style_list_field(map, "item_style")?,
click: get_click_event(map)?,
context_menu: get_optional_event(map, "context_menu")?,
})
}
#[inline(never)]
fn decode_data_table_ir_node(
map: &HashMap<Term, Term>,
id: Option<String>,
depth: usize,
) -> Result<IrNode, String> {
let columns = get_data_table_columns_field(map)?;
let column_ids = columns
.iter()
.map(|column| column.id.as_str())
.collect::<HashSet<_>>();
let rows = get_data_table_rows_field(map, &column_ids, depth)?;
let row_ids = rows
.iter()
.map(|row| row.id.as_str())
.collect::<HashSet<_>>();
let selected_row_id = get_optional_string_field(map, "selected_row_id")?;
ensure_optional_id_known(
selected_row_id.as_deref(),
&row_ids,
"unknown data_table selected row",
)?;
let selected_cell = get_optional_string_pair_field(map, "selected_cell")?;
if let Some((row_id, column_id)) = selected_cell.as_ref() {
ensure_id_known(row_id, &row_ids, "unknown data_table selected row")?;
ensure_id_known(column_id, &column_ids, "unknown data_table selected column")?;
}
let sort = get_data_table_sort_field(map, &column_ids)?;
Ok(IrNode::DataTable(Box::new(DataTableNode {
id,
columns,
rows,
style: get_div_style(map)?,
header_style: get_style_list_field(map, "header_style")?,
row_style: get_style_list_field(map, "row_style")?,
cell_style: get_style_list_field(map, "cell_style")?,
selected_row_id,
selected_cell,
sort,
row_click: get_optional_event(map, "row_click")?,
cell_click: get_optional_event(map, "cell_click")?,
sort_callback: get_optional_event(map, "sort")?,
column_reorder: get_optional_event(map, "column_reorder")?,
column_resize: get_optional_event(map, "column_resize")?,
row_context_menu: get_optional_event(map, "row_context_menu")?,
cell_context_menu: get_optional_event(map, "cell_context_menu")?,
})))
}
#[inline(never)]
fn decode_tree_ir_node(map: &HashMap<Term, Term>, id: Option<String>) -> Result<IrNode, String> {
let (nodes, tree_ids) = get_tree_nodes_field(map)?;
let tree_id_refs = tree_ids
.iter()
.map(|id| id.as_str())
.collect::<HashSet<_>>();
let selected_id = get_optional_string_field(map, "selected_id")?;
ensure_optional_id_known(
selected_id.as_deref(),
&tree_id_refs,
"unknown tree selected item",
)?;
Ok(IrNode::Tree(Box::new(TreeNode {
id,
nodes,
style: get_div_style(map)?,
row_style: get_style_list_field(map, "row_style")?,
selected_id,
select: get_optional_event(map, "select")?,
toggle: get_optional_event(map, "toggle")?,
context_menu: get_context_menu_event(map)?,
})))
}
#[inline(never)]
fn decode_canvas_ir_node(map: &HashMap<Term, Term>, id: Option<String>) -> Result<IrNode, String> {
Ok(IrNode::Canvas(Box::new(CanvasNode {
id,
commands: get_canvas_commands_field(map)?,
style: get_div_style(map)?,
click: get_click_event(map)?,
context_menu: get_context_menu_event(map)?,
})))
}
#[inline(never)]
fn decode_select_ir_node(map: &HashMap<Term, Term>, id: Option<String>) -> Result<IrNode, String> {
Ok(IrNode::Select(Box::new(SelectNode {
id,
value: get_optional_string_field(map, "value")?,
open: get_boolean_field(map, "open")?,
placeholder: get_optional_string_field(map, "placeholder")?
.unwrap_or_else(|| "Select…".into()),
options: get_select_options_field(map)?,
style: get_div_style(map)?,
list_style: get_style_list_field(map, "list_style")?,
option_style: get_style_list_field(map, "option_style")?,
anchor: get_popover_anchor_field(map)?,
anchor_offset: get_optional_point_field(map, "anchor_offset")?,
anchor_fit: get_popover_anchor_fit_field(map)?,
snap_margin: get_non_neg_f32_field(map, "snap_margin", 8.0)?,
disabled: get_boolean_field(map, "disabled")?,
tab_index: get_optional_integer_field(map, "tab_index")?,
click: get_click_event(map)?,
change: get_change_event(map)?,
close: get_close_event(map)?,
focus: get_focus_event(map)?,
blur: get_blur_event(map)?,
})))
}
#[inline(never)]
fn decode_popover_ir_node(
map: &HashMap<Term, Term>,
id: Option<String>,
depth: usize,
) -> Result<IrNode, String> {
let children = get_child_nodes_field(map, depth)?;
ensure_no_nested_overlay_nodes(&children)?;
Ok(IrNode::Popover {
id,
label: get_string_field(map, "label")?,
open: get_required_boolean_field(map, "open")?,
style: get_div_style(map)?,
popover_style: get_style_list_field(map, "popover_style")?,
anchor: get_popover_anchor_field(map)?,
anchor_position: get_optional_point_field(map, "anchor_position")?,
anchor_offset: get_optional_point_field(map, "anchor_offset")?,
anchor_position_mode: get_popover_anchor_position_mode_field(map)?,
anchor_fit: get_popover_anchor_fit_field(map)?,
snap_margin: get_non_neg_f32_field(map, "snap_margin", 8.0)?,
close_on_click_outside: get_boolean_field(map, "close_on_click_outside")?
|| get_field(map, "close_on_click_outside").is_none(),
stack_priority: get_optional_usize_field(map, "stack_priority")?.or(Some(1)),
disabled: get_boolean_field(map, "disabled")?,
click: get_click_event(map)?,
close: get_close_event(map)?,
children,
})
}
#[inline(never)]
fn decode_image_ir_node(map: &HashMap<Term, Term>, id: Option<String>) -> Result<IrNode, String> {
Ok(IrNode::Image {
id,
source: get_image_source_field(map)?,
style: get_div_style(map)?,
object_fit: get_image_object_fit_field(map)?,
grayscale: get_boolean_field(map, "grayscale")?,
})
}
#[inline(never)]
fn decode_icon_ir_node(map: &HashMap<Term, Term>, id: Option<String>) -> Result<IrNode, String> {
Ok(IrNode::Icon {
id,
source: get_image_source_field(map)?,
style: get_div_style(map)?,
})
}
#[inline(never)]
fn decode_spacer_ir_node(map: &HashMap<Term, Term>, id: Option<String>) -> Result<IrNode, String> {
Ok(IrNode::Spacer {
id,
style: get_div_style(map)?,
})
}
#[inline(never)]
fn decode_div_ir_node(
map: &HashMap<Term, Term>,
id: Option<String>,
depth: usize,
) -> Result<IrNode, String> {
let children = get_child_nodes_field(map, depth)?;
let actions = get_div_actions(map)?;
Ok(IrNode::Div(Box::new(DivNode {
id,
style: get_div_style(map)?,
hover_style: get_div_hover_style(map)?,
focus_style: get_div_focus_style(map)?,
focus_visible_style: get_div_focus_visible_style(map)?,
in_focus_style: get_div_in_focus_style(map)?,
active_style: get_div_active_style(map)?,
disabled_style: get_div_disabled_style(map)?,
animation: get_animation_field(map)?,
disabled: get_boolean_field(map, "disabled")?,
stack_priority: get_optional_usize_field(map, "stack_priority")?,
occlude: get_boolean_field(map, "occlude")?,
focusable: get_boolean_field(map, "focusable")?,
tab_stop: get_optional_boolean_field(map, "tab_stop")?,
tab_index: get_optional_integer_field(map, "tab_index")?,
track_scroll: get_boolean_field(map, "track_scroll")?,
anchor_scroll: get_boolean_field(map, "anchor_scroll")?,
scroll_to: get_boolean_field(map, "scroll_to")?,
tooltip: get_optional_string_field(map, "tooltip")?,
shortcuts: get_div_shortcuts(map, &actions)?,
children,
click: get_click_event(map)?,
hover: get_hover_event(map)?,
focus: get_focus_event(map)?,
blur: get_blur_event(map)?,
key_down: get_key_down_event(map)?,
key_up: get_key_up_event(map)?,
context_menu: get_context_menu_event(map)?,
drag_start: get_drag_start_event(map)?,
drag_move: get_drag_move_event(map)?,
drop: get_drop_event(map)?,
mouse_down: get_mouse_down_event(map)?,
mouse_up: get_mouse_up_event(map)?,
mouse_move: get_mouse_move_event(map)?,
scroll_wheel: get_scroll_wheel_event(map)?,
})))
}
fn get_child_nodes_field(
map: &HashMap<Term, Term>,
parent_depth: usize,
) -> Result<Arc<[IrNode]>, String> {
let Some(children_term) = get_field(map, "children") else {
return Ok(empty_ir_nodes());
};
let children = get_list(children_term)?;
if children.is_empty() {
return Ok(empty_ir_nodes());
}
let child_depth = next_ir_node_depth(parent_depth);
let mut decoded = Vec::with_capacity(children.len());
for child in children {
decoded.push(IrNode::from_term_at_depth(child, child_depth)?);
}
Ok(decoded.into())
}
fn ensure_no_nested_overlay_nodes(children: &[IrNode]) -> Result<(), String> {
for child in children {
match child {
IrNode::Select(_) => return Err("nested overlay not supported: select".into()),
IrNode::Popover { .. } => return Err("nested overlay not supported: popover".into()),
IrNode::Div(div) => ensure_no_nested_overlay_nodes(&div.children)?,
IrNode::Scroll { children, .. } => ensure_no_nested_overlay_nodes(children)?,
_ => {}
}
}
Ok(())
}
fn collect_arc<T, E>(items: impl Iterator<Item = Result<T, E>>) -> Result<Arc<[T]>, E> {
items.collect::<Result<Vec<_>, _>>().map(Into::into)
}
fn decode_button_node(map: &HashMap<Term, Term>, id: Option<String>) -> Result<DivNode, String> {
let actions = get_div_actions(map)?;
let label = get_string_field(map, "label")?;
let style = prepend_style(default_button_style(), get_div_style(map)?);
let hover_style = get_div_hover_style(map)?;
let focus_style = prepend_style(default_button_focus_style(), get_div_focus_style(map)?);
let focus_visible_style = get_div_focus_visible_style(map)?;
let in_focus_style = get_div_in_focus_style(map)?;
let active_style = prepend_style(default_button_active_style(), get_div_active_style(map)?);
let disabled_style = prepend_style(
default_button_disabled_style(),
get_div_disabled_style(map)?,
);
Ok(DivNode {
id,
style,
hover_style,
focus_style,
focus_visible_style,
in_focus_style,
active_style,
disabled_style,
animation: get_animation_field(map)?,
disabled: get_boolean_field(map, "disabled")?,
stack_priority: None,
occlude: false,
focusable: true,
tab_stop: Some(true),
tab_index: get_optional_integer_field(map, "tab_index")?,
track_scroll: false,
anchor_scroll: false,
scroll_to: false,
tooltip: None,
shortcuts: get_div_shortcuts(map, &actions)?,
children: vec![IrNode::text(label)].into(),
click: get_click_event(map)?,
hover: get_hover_event(map)?,
focus: get_focus_event(map)?,
blur: get_blur_event(map)?,
key_down: get_key_down_event(map)?,
key_up: get_key_up_event(map)?,
context_menu: get_context_menu_event(map)?,
drag_start: None,
drag_move: None,
drop: None,
mouse_down: get_mouse_down_event(map)?,
mouse_up: get_mouse_up_event(map)?,
mouse_move: get_mouse_move_event(map)?,
scroll_wheel: None,
})
}
fn decode_checkbox_node(
map: &HashMap<Term, Term>,
id: Option<String>,
) -> Result<CheckboxNode, String> {
Ok(CheckboxNode {
id,
label: get_string_field(map, "label")?,
checked: get_required_boolean_field(map, "checked")?,
style: get_div_style(map)?,
hover_style: get_div_hover_style(map)?,
focus_style: get_div_focus_style(map)?,
focus_visible_style: get_div_focus_visible_style(map)?,
in_focus_style: get_div_in_focus_style(map)?,
active_style: get_div_active_style(map)?,
disabled_style: get_div_disabled_style(map)?,
disabled: get_boolean_field(map, "disabled")?,
tab_index: get_optional_integer_field(map, "tab_index")?,
change: get_change_event(map)?,
focus: get_focus_event(map)?,
blur: get_blur_event(map)?,
})
}
fn decode_radio_node(map: &HashMap<Term, Term>, id: Option<String>) -> Result<RadioNode, String> {
Ok(RadioNode {
id,
label: get_string_field(map, "label")?,
value: get_string_field(map, "value")?,
checked: get_required_boolean_field(map, "checked")?,
style: get_div_style(map)?,
hover_style: get_div_hover_style(map)?,
focus_style: get_div_focus_style(map)?,
focus_visible_style: get_div_focus_visible_style(map)?,
in_focus_style: get_div_in_focus_style(map)?,
active_style: get_div_active_style(map)?,
disabled_style: get_div_disabled_style(map)?,
disabled: get_boolean_field(map, "disabled")?,
tab_index: get_optional_integer_field(map, "tab_index")?,
change: get_change_event(map)?,
focus: get_focus_event(map)?,
blur: get_blur_event(map)?,
})
}
fn expect_map(term: &Term) -> Result<&HashMap<Term, Term>, String> {
etf_decode::expect_hash_map(term, "map ir node")
}
fn get_field<'a>(map: &'a HashMap<Term, Term>, key: &str) -> Option<&'a Term> {
map.get(field_key(key)?)
}
fn field_key(key: &str) -> Option<&'static Term> {
let keys = field_keys();
Some(match key {
"kind" => &keys.kind,
"id" => &keys.id,
"content" => &keys.content,
"runs" => &keys.runs,
"text" => &keys.text,
"value" => &keys.value,
"placeholder" => &keys.placeholder,
"children" => &keys.children,
"items" => &keys.items,
"columns" => &keys.columns,
"rows" => &keys.rows,
"cells" => &keys.cells,
"column_id" => &keys.column_id,
"width" => &keys.width,
"sortable" => &keys.sortable,
"pinned" => &keys.pinned,
"header_style" => &keys.header_style,
"row_style" => &keys.row_style,
"cell_style" => &keys.cell_style,
"selected_row_id" => &keys.selected_row_id,
"selected_cell" => &keys.selected_cell,
"sort" => &keys.sort,
"row_click" => &keys.row_click,
"cell_click" => &keys.cell_click,
"column_reorder" => &keys.column_reorder,
"column_resize" => &keys.column_resize,
"row_context_menu" => &keys.row_context_menu,
"cell_context_menu" => &keys.cell_context_menu,
"direction" => &keys.direction,
"nodes" => &keys.nodes,
"selected_id" => &keys.selected_id,
"expanded" => &keys.expanded,
"select" => &keys.select,
"toggle" => &keys.toggle,
"commands" => &keys.commands,
"op" => &keys.op,
"x" => &keys.x,
"y" => &keys.y,
"height" => &keys.height,
"fill" => &keys.fill,
"color" => &keys.color,
"line_width" => &keys.line_width,
"interval" => &keys.interval,
"radius" => &keys.radius,
"options" => &keys.options,
"axis" => &keys.axis,
"style" => &keys.style,
"item_style" => &keys.item_style,
"list_style" => &keys.list_style,
"option_style" => &keys.option_style,
"hover_style" => &keys.hover_style,
"focus_style" => &keys.focus_style,
"focus_visible_style" => &keys.focus_visible_style,
"in_focus_style" => &keys.in_focus_style,
"active_style" => &keys.active_style,
"disabled_style" => &keys.disabled_style,
"animation" => &keys.animation,
"duration_ms" => &keys.duration_ms,
"repeat" => &keys.repeat,
"from" => &keys.from,
"to" => &keys.to,
"disabled" => &keys.disabled,
"tab_index" => &keys.tab_index,
"actions" => &keys.actions,
"shortcuts" => &keys.shortcuts,
"label" => &keys.label,
"open" => &keys.open,
"events" => &keys.events,
"click" => &keys.click,
"close" => &keys.close,
"hover" => &keys.hover,
"focus" => &keys.focus,
"blur" => &keys.blur,
"change" => &keys.change,
"key_down" => &keys.key_down,
"key_up" => &keys.key_up,
"context_menu" => &keys.context_menu,
"drag_start" => &keys.drag_start,
"drag_move" => &keys.drag_move,
"drop" => &keys.drop,
"mouse_down" => &keys.mouse_down,
"mouse_up" => &keys.mouse_up,
"mouse_move" => &keys.mouse_move,
"scroll_wheel" => &keys.scroll_wheel,
"stack_priority" => &keys.stack_priority,
"occlude" => &keys.occlude,
"focusable" => &keys.focusable,
"tab_stop" => &keys.tab_stop,
"track_scroll" => &keys.track_scroll,
"anchor_scroll" => &keys.anchor_scroll,
"scroll_to" => &keys.scroll_to,
"tooltip" => &keys.tooltip,
"source" => &keys.source,
"popover_style" => &keys.popover_style,
"anchor" => &keys.anchor,
"anchor_position" => &keys.anchor_position,
"anchor_offset" => &keys.anchor_offset,
"anchor_position_mode" => &keys.anchor_position_mode,
"anchor_fit" => &keys.anchor_fit,
"snap_margin" => &keys.snap_margin,
"close_on_click_outside" => &keys.close_on_click_outside,
"object_fit" => &keys.object_fit,
"grayscale" => &keys.grayscale,
"checked" => &keys.checked,
_ => return None,
})
}
fn get_atom_field<'a>(map: &'a HashMap<Term, Term>, key: &str) -> Result<&'a str, String> {
match get_field(map, key) {
Some(Term::Atom(atom)) => Ok(atom.name.as_str()),
Some(other) => Err(format!("expected atom field {key}, got {other}")),
None => Err(format!("missing required field: {key}")),
}
}
fn get_scroll_axis_field(map: &HashMap<Term, Term>) -> Result<ScrollAxis, String> {
match get_field(map, "axis") {
Some(Term::Atom(atom)) if atom.name == "x" => Ok(ScrollAxis::X),
Some(Term::Atom(atom)) if atom.name == "y" => Ok(ScrollAxis::Y),
Some(Term::Atom(atom)) if atom.name == "both" => Ok(ScrollAxis::Both),
Some(other) => Err(format!("expected scroll axis atom, got {other}")),
None => Ok(ScrollAxis::Y),
}
}
fn get_image_source_field(map: &HashMap<Term, Term>) -> Result<ImageSource, String> {
match get_field(map, "source") {
Some(source @ (Term::Binary(_) | Term::ByteList(_))) => {
term_to_string(source).map(ImageSource::Auto)
}
Some(Term::Tuple(Tuple { elements })) if elements.len() == 2 => {
let kind = match &elements[0] {
Term::Atom(atom) => atom.name.as_str(),
other => return Err(format!("expected image source kind atom, got {other}")),
};
let value = term_to_string(&elements[1])?;
match kind {
"uri" => Ok(ImageSource::Uri(value)),
"path" => Ok(ImageSource::Path(value)),
"embedded" => Ok(ImageSource::Embedded(value)),
other => Err(format!("unsupported image source kind: {other}")),
}
}
Some(other) => Err(format!(
"expected image source string or tuple, got {other}"
)),
None => Err("missing required field: source".into()),
}
}
fn get_image_object_fit_field(map: &HashMap<Term, Term>) -> Result<ImageObjectFit, String> {
match get_field(map, "object_fit") {
Some(Term::Atom(atom)) if atom.name == "fill" => Ok(ImageObjectFit::Fill),
Some(Term::Atom(atom)) if atom.name == "contain" => Ok(ImageObjectFit::Contain),
Some(Term::Atom(atom)) if atom.name == "cover" => Ok(ImageObjectFit::Cover),
Some(Term::Atom(atom)) if atom.name == "scale_down" => Ok(ImageObjectFit::ScaleDown),
Some(Term::Atom(atom)) if atom.name == "none" => Ok(ImageObjectFit::None),
Some(other) => Err(format!("expected image object_fit atom, got {other}")),
None => Ok(ImageObjectFit::Contain),
}
}
fn get_popover_anchor_field(map: &HashMap<Term, Term>) -> Result<PopoverAnchor, String> {
match get_field(map, "anchor") {
Some(Term::Atom(atom)) if atom.name == "top_left" => Ok(PopoverAnchor::TopLeft),
Some(Term::Atom(atom)) if atom.name == "top_right" => Ok(PopoverAnchor::TopRight),
Some(Term::Atom(atom)) if atom.name == "bottom_left" => Ok(PopoverAnchor::BottomLeft),
Some(Term::Atom(atom)) if atom.name == "bottom_right" => Ok(PopoverAnchor::BottomRight),
Some(other) => Err(format!("expected popover anchor atom, got {other}")),
None => Ok(PopoverAnchor::TopLeft),
}
}
fn get_popover_anchor_position_mode_field(
map: &HashMap<Term, Term>,
) -> Result<PopoverAnchorPositionMode, String> {
match get_field(map, "anchor_position_mode") {
Some(Term::Atom(atom)) if atom.name == "window" => Ok(PopoverAnchorPositionMode::Window),
Some(Term::Atom(atom)) if atom.name == "local" => Ok(PopoverAnchorPositionMode::Local),
Some(other) => Err(format!(
"expected popover anchor_position_mode atom, got {other}"
)),
None => Ok(PopoverAnchorPositionMode::Window),
}
}
fn get_popover_anchor_fit_field(map: &HashMap<Term, Term>) -> Result<PopoverAnchorFit, String> {
match get_field(map, "anchor_fit") {
Some(Term::Atom(atom)) if atom.name == "switch_anchor" => {
Ok(PopoverAnchorFit::SwitchAnchor)
}
Some(Term::Atom(atom)) if atom.name == "snap_to_window" => {
Ok(PopoverAnchorFit::SnapToWindow)
}
Some(Term::Atom(atom)) if atom.name == "snap_to_window_with_margin" => {
Ok(PopoverAnchorFit::SnapToWindowWithMargin)
}
Some(other) => Err(format!("expected popover anchor_fit atom, got {other}")),
None => Ok(PopoverAnchorFit::SnapToWindowWithMargin),
}
}
fn get_optional_point_field(
map: &HashMap<Term, Term>,
key: &str,
) -> Result<Option<(f32, f32)>, String> {
let Some(term) = get_field(map, key) else {
return Ok(None);
};
let Term::Tuple(Tuple { elements }) = term else {
return Err(format!(
"expected optional point tuple field {key}, got {term}"
));
};
if elements.len() != 2 {
return Err(format!(
"expected optional point tuple field {key} with 2 elements, got {term}"
));
}
Ok(Some((parse_f32(&elements[0])?, parse_f32(&elements[1])?)))
}
fn get_f32_field(map: &HashMap<Term, Term>, key: &str) -> Result<f32, String> {
match get_field(map, key) {
Some(term) => parse_f32(term),
None => Err(format!("missing required field: {key}")),
}
}
fn get_positive_f32_field(map: &HashMap<Term, Term>, key: &str) -> Result<f32, String> {
let value = get_f32_field(map, key)?;
if value > 0.0 {
Ok(value)
} else {
Err(format!(
"expected positive numeric field {key}, got {value}"
))
}
}
fn get_positive_unit_f32_field(map: &HashMap<Term, Term>, key: &str) -> Result<f32, String> {
let value = get_f32_field(map, key)?;
if value > 0.0 && value <= 1.0 {
Ok(value)
} else {
Err(format!("expected unit numeric field {key}, got {value}"))
}
}
fn get_non_neg_f32_field(
map: &HashMap<Term, Term>,
key: &str,
default: f32,
) -> Result<f32, String> {
let Some(term) = get_field(map, key) else {
return Ok(default);
};
let value = parse_f32(term)?;
if value >= 0.0 {
Ok(value)
} else {
Err(format!(
"expected non-negative numeric field {key}, got {term}"
))
}
}
fn get_string_field(map: &HashMap<Term, Term>, key: &str) -> Result<String, String> {
match get_field(map, key) {
Some(term) => term_to_string(term),
None => Err(format!("missing required field: {key}")),
}
}
fn get_animation_field(map: &HashMap<Term, Term>) -> Result<Option<AnimationSpec>, String> {
let Some(term) = get_field(map, "animation") else {
return Ok(None);
};
let animation = expect_map(term)?;
ensure_allowed_fields(
animation,
&["id", "duration_ms", "repeat", "from", "to"],
"animation",
)?;
let duration_ms = get_optional_u64_field(animation, "duration_ms")?.unwrap_or(1_000);
if duration_ms == 0 {
return Err("expected positive animation duration_ms".into());
}
Ok(Some(AnimationSpec {
id: get_string_field(animation, "id")?,
duration_ms,
repeat: get_boolean_field(animation, "repeat")?,
from: get_unit_f32_field(animation, "from", 0.0)?,
to: get_unit_f32_field(animation, "to", 1.0)?,
}))
}
fn get_text_runs_field(
map: &HashMap<Term, Term>,
content: &str,
) -> Result<Arc<[TextRunSegment]>, String> {
let Some(runs_term) = get_field(map, "runs") else {
return Ok(empty_text_runs());
};
let runs = get_list(runs_term)?
.iter()
.map(|term| {
let run = expect_map(term)?;
ensure_allowed_fields(run, &["text", "style"], "text run")?;
Ok(TextRunSegment {
text: get_string_field(run, "text")?,
style: get_style_list_field(run, "style")?,
})
})
.collect::<Result<Vec<_>, String>>()?;
if text_runs_match_content(&runs, content) {
Ok(runs.into())
} else {
let joined = runs.iter().map(|run| run.text.as_str()).collect::<String>();
Err(format!(
"text runs content mismatch: expected {content:?}, got {joined:?}"
))
}
}
fn text_runs_match_content(runs: &[TextRunSegment], content: &str) -> bool {
let mut remaining = content;
for run in runs {
let Some(next_remaining) = remaining.strip_prefix(run.text.as_str()) else {
return false;
};
remaining = next_remaining;
}
remaining.is_empty()
}
fn get_uniform_list_items_field(
map: &HashMap<Term, Term>,
) -> Result<Arc<[UniformListItem]>, String> {
let Some(items_term) = get_field(map, "items") else {
return Err("missing required field: items".into());
};
collect_arc(get_list(items_term)?.iter().map(|term| {
let item = expect_map(term)?;
ensure_allowed_fields(item, &["id", "label"], "uniform_list item")?;
Ok(UniformListItem {
id: get_string_field(item, "id")?,
label: get_string_field(item, "label")?,
})
}))
}
fn get_list_items_field(
map: &HashMap<Term, Term>,
parent_depth: usize,
) -> Result<Arc<[ListItem]>, String> {
let Some(items_term) = get_field(map, "items") else {
return Err("missing required field: items".into());
};
let child_depth = next_ir_node_depth(parent_depth);
collect_arc(get_list(items_term)?.iter().map(|term| {
let item = expect_map(term)?;
ensure_allowed_fields(item, &["id", "children"], "list item")?;
let Some(children_term) = get_field(item, "children") else {
return Err("missing required field: list item children".into());
};
let children = collect_arc(
get_list(children_term)?
.iter()
.map(|child| decode_list_row_child_term(child, child_depth)),
)?;
ensure_unique_list_row_control_ids(&children)?;
Ok(ListItem {
id: get_string_field(item, "id")?,
children,
})
}))
}
fn get_data_table_columns_field(
map: &HashMap<Term, Term>,
) -> Result<Arc<[DataTableColumn]>, String> {
let Some(columns_term) = get_field(map, "columns") else {
return Err("missing required field: columns".into());
};
let mut seen = HashSet::new();
collect_arc(get_list(columns_term)?.iter().map(|term| {
let column = expect_map(term)?;
ensure_allowed_fields(
column,
&["id", "label", "width", "sortable", "pinned", "style"],
"data_table column",
)?;
let id = get_string_field(column, "id")?;
if !seen.insert(id.clone()) {
return Err(format!("duplicate data_table column id: {id}"));
}
Ok(DataTableColumn {
id,
label: get_string_field(column, "label")?,
width: get_data_table_column_width(column)?,
sortable: get_boolean_field(column, "sortable")?,
pinned: get_boolean_field(column, "pinned")?,
style: get_div_style(column)?,
})
}))
}
fn get_data_table_column_width(map: &HashMap<Term, Term>) -> Result<DataTableColumnWidth, String> {
let Some(width) = get_field(map, "width") else {
return Ok(DataTableColumnWidth::Auto);
};
match width {
Term::Atom(atom) if atom.name == "auto" => Ok(DataTableColumnWidth::Auto),
Term::Tuple(Tuple { elements }) if elements.len() == 2 => {
let kind = match &elements[0] {
Term::Atom(atom) => atom.name.as_str(),
other => {
return Err(format!(
"expected data_table column width kind atom, got {other}"
));
}
};
match kind {
"px" => {
let px = parse_f32(&elements[1])?;
if px > 0.0 {
Ok(DataTableColumnWidth::Px(px))
} else {
Err(format!(
"expected positive data_table px width, got {width}"
))
}
}
"fr" => parse_positive_u32(&elements[1]).map(DataTableColumnWidth::Fr),
other => Err(format!("unsupported data_table column width kind: {other}")),
}
}
other => Err(format!("expected data_table column width, got {other}")),
}
}
fn get_data_table_rows_field(
map: &HashMap<Term, Term>,
column_ids: &HashSet<&str>,
parent_depth: usize,
) -> Result<Arc<[DataTableRow]>, String> {
let Some(rows_term) = get_field(map, "rows") else {
return Err("missing required field: rows".into());
};
let mut seen = HashSet::new();
collect_arc(get_list(rows_term)?.iter().map(|term| {
let row = expect_map(term)?;
ensure_allowed_fields(row, &["id", "cells", "style"], "data_table row")?;
let id = get_string_field(row, "id")?;
if !seen.insert(id.clone()) {
return Err(format!("duplicate data_table row id: {id}"));
}
Ok(DataTableRow {
id,
cells: get_data_table_cells(row, column_ids, parent_depth)?,
style: get_div_style(row)?,
})
}))
}
fn get_data_table_cells(
row: &HashMap<Term, Term>,
column_ids: &HashSet<&str>,
parent_depth: usize,
) -> Result<Arc<[DataTableCell]>, String> {
let Some(cells_term) = get_field(row, "cells") else {
return Err("missing required field: data_table row cells".into());
};
let mut seen = HashSet::new();
collect_arc(get_list(cells_term)?.iter().map(|term| {
let cell = expect_map(term)?;
ensure_allowed_fields(cell, &["column_id", "children", "style"], "data_table cell")?;
let column_id = get_string_field(cell, "column_id")?;
ensure_id_known(&column_id, column_ids, "unknown data_table cell column")?;
if !seen.insert(column_id.clone()) {
return Err(format!("duplicate data_table cell column: {column_id}"));
}
Ok(DataTableCell {
column_id,
children: get_data_table_cell_children(cell, parent_depth)?,
style: get_div_style(cell)?,
})
}))
}
fn get_data_table_cell_children(
map: &HashMap<Term, Term>,
parent_depth: usize,
) -> Result<Arc<[IrNode]>, String> {
let Some(children_term) = get_field(map, "children") else {
return Err("missing required field: data_table cell children".into());
};
let child_depth = next_ir_node_depth(parent_depth);
collect_arc(
get_list(children_term)?
.iter()
.map(|child| decode_data_table_cell_child_term(child, child_depth)),
)
}
fn decode_data_table_cell_child_term(term: &Term, depth: usize) -> Result<IrNode, String> {
let map = expect_map(term)?;
let kind = get_atom_field(map, "kind")?;
match kind {
"text" => {
ensure_allowed_fields(
map,
&["kind", "content", "id", "style", "runs", "events"],
"data_table cell text",
)?;
ensure_allowed_event_fields(map, &["click"], "data_table cell text events")?;
IrNode::from_term_at_depth(term, depth)
}
"spacer" => {
ensure_allowed_fields(map, &["kind", "id", "style"], "data_table cell spacer")?;
IrNode::from_term_at_depth(term, depth)
}
"div" => decode_static_data_table_cell_div(map, depth),
_ => Err(format!("unsupported data_table cell child kind: {kind}")),
}
}
fn get_data_table_sort_field(
map: &HashMap<Term, Term>,
column_ids: &HashSet<&str>,
) -> Result<Option<DataTableSort>, String> {
let Some(sort_term) = get_field(map, "sort") else {
return Ok(None);
};
let sort = expect_map(sort_term)?;
ensure_allowed_fields(sort, &["column_id", "direction"], "data_table sort")?;
let column_id = get_string_field(sort, "column_id")?;
ensure_id_known(&column_id, column_ids, "unknown data_table sort column")?;
let direction = match get_field(sort, "direction") {
Some(Term::Atom(atom)) if atom.name == "asc" => DataTableSortDirection::Asc,
Some(Term::Atom(atom)) if atom.name == "desc" => DataTableSortDirection::Desc,
Some(other) => {
return Err(format!(
"expected data_table sort direction atom, got {other}"
));
}
None => return Err("missing required field: direction".into()),
};
Ok(Some(DataTableSort {
column_id,
direction,
}))
}
fn get_tree_nodes_field(
map: &HashMap<Term, Term>,
) -> Result<(Arc<[TreeItem]>, HashSet<String>), String> {
let Some(nodes_term) = get_field(map, "nodes") else {
return Err("missing required field: nodes".into());
};
let mut seen = HashSet::new();
let nodes = get_tree_nodes(nodes_term, &mut seen)?;
Ok((nodes, seen))
}
fn get_tree_nodes(term: &Term, seen: &mut HashSet<String>) -> Result<Arc<[TreeItem]>, String> {
collect_arc(get_list(term)?.iter().map(|term| get_tree_item(term, seen)))
}
fn get_tree_item(term: &Term, seen: &mut HashSet<String>) -> Result<TreeItem, String> {
let item = expect_map(term)?;
ensure_allowed_fields(
item,
&["id", "label", "expanded", "children", "style"],
"tree item",
)?;
let id = get_string_field(item, "id")?;
if !seen.insert(id.clone()) {
return Err(format!("duplicate tree item id: {id}"));
}
let children = match get_field(item, "children") {
Some(children) => get_tree_nodes(children, seen)?,
None => empty_tree_items(),
};
Ok(TreeItem {
id,
label: get_string_field(item, "label")?,
expanded: get_boolean_field(item, "expanded")?,
children,
style: get_div_style(item)?,
})
}
fn ensure_optional_id_known(
id: Option<&str>,
known: &HashSet<&str>,
context: &str,
) -> Result<(), String> {
if let Some(id) = id {
ensure_id_known(id, known, context)?;
}
Ok(())
}
fn ensure_id_known(id: &str, known: &HashSet<&str>, context: &str) -> Result<(), String> {
if known.contains(id) {
Ok(())
} else {
Err(format!("{context}: {id}"))
}
}
fn get_canvas_commands_field(map: &HashMap<Term, Term>) -> Result<Arc<[CanvasCommand]>, String> {
let Some(commands_term) = get_field(map, "commands") else {
return Err("missing required field: commands".into());
};
collect_arc(get_list(commands_term)?.iter().map(decode_canvas_command))
}
fn decode_canvas_command(term: &Term) -> Result<CanvasCommand, String> {
let command = expect_map(term)?;
let op = get_atom_field(command, "op")?;
match op {
"rect" => {
ensure_allowed_fields(
command,
&["op", "x", "y", "width", "height", "fill", "radius"],
"canvas rect command",
)?;
let (x, y, width, height) = get_canvas_bounds(command)?;
Ok(CanvasCommand::Rect {
x,
y,
width,
height,
fill: get_canvas_color_field(command, "fill")?,
radius: get_non_neg_f32_field(command, "radius", 0.0)?,
})
}
"rounded_rect" => {
ensure_allowed_fields(
command,
&["op", "x", "y", "width", "height", "fill", "radius"],
"canvas rounded_rect command",
)?;
if get_field(command, "radius").is_none() {
return Err("missing required field: radius".into());
}
let (x, y, width, height) = get_canvas_bounds(command)?;
Ok(CanvasCommand::Rect {
x,
y,
width,
height,
fill: get_canvas_color_field(command, "fill")?,
radius: get_non_neg_f32_field(command, "radius", 0.0)?,
})
}
"pattern_rect" => {
ensure_allowed_fields(
command,
&[
"op",
"x",
"y",
"width",
"height",
"color",
"line_width",
"interval",
"radius",
],
"canvas pattern_rect command",
)?;
let (x, y, width, height) = get_canvas_bounds(command)?;
Ok(CanvasCommand::PatternRect {
x,
y,
width,
height,
color: get_canvas_color_field(command, "color")?,
line_width: get_positive_unit_f32_field(command, "line_width")?,
interval: get_positive_unit_f32_field(command, "interval")?,
radius: get_non_neg_f32_field(command, "radius", 0.0)?,
})
}
other => Err(format!("unsupported canvas command op: {other}")),
}
}
fn get_canvas_bounds(map: &HashMap<Term, Term>) -> Result<(f32, f32, f32, f32), String> {
Ok((
get_f32_field(map, "x")?,
get_f32_field(map, "y")?,
get_positive_f32_field(map, "width")?,
get_positive_f32_field(map, "height")?,
))
}
fn get_canvas_color_field(map: &HashMap<Term, Term>, key: &str) -> Result<StyleColor, String> {
match get_field(map, key) {
Some(term) => parse_style_color(term),
None => Err(format!("missing required field: {key}")),
}
}
fn get_select_options_field(map: &HashMap<Term, Term>) -> Result<Arc<[SelectOption]>, String> {
let Some(options_term) = get_field(map, "options") else {
return Err("missing required field: options".into());
};
collect_arc(get_list(options_term)?.iter().map(|term| {
let option = expect_map(term)?;
ensure_allowed_fields(option, &["value", "label", "disabled"], "select option")?;
Ok(SelectOption {
value: get_string_field(option, "value")?,
label: get_string_field(option, "label")?,
disabled: get_boolean_field(option, "disabled")?,
})
}))
}
fn decode_list_row_child_term(term: &Term, depth: usize) -> Result<IrNode, String> {
let map = expect_map(term)?;
let kind = get_atom_field(map, "kind")?;
match kind {
"text" => {
ensure_allowed_fields(
map,
&["kind", "content", "id", "style", "runs", "events"],
"list row text",
)?;
ensure_allowed_event_fields(map, &["click"], "list row text events")?;
IrNode::from_term_at_depth(term, depth)
}
"spacer" => {
ensure_allowed_fields(map, &["kind", "id", "style"], "list row spacer")?;
IrNode::from_term_at_depth(term, depth)
}
"div" => decode_static_list_row_div(map, depth),
"button" => decode_list_row_button(map),
"checkbox" => decode_list_row_checkbox(map),
"radio" => decode_list_row_radio(map),
_ => Err(format!("unsupported list row child kind: {kind}")),
}
}
fn decode_list_row_button(map: &HashMap<Term, Term>) -> Result<IrNode, String> {
ensure_allowed_fields(
map,
&[
"kind",
"label",
"id",
"style",
"hover_style",
"focus_style",
"focus_visible_style",
"in_focus_style",
"active_style",
"disabled_style",
"disabled",
"tab_index",
"events",
],
"list row button",
)?;
ensure_allowed_event_fields(map, &["click"], "list row button events")?;
let id = get_list_row_control_id(map, "button")?;
Ok(IrNode::Button(Box::new(decode_button_node(map, Some(id))?)))
}
fn decode_list_row_checkbox(map: &HashMap<Term, Term>) -> Result<IrNode, String> {
ensure_allowed_fields(
map,
&[
"kind",
"label",
"checked",
"id",
"style",
"hover_style",
"focus_style",
"focus_visible_style",
"in_focus_style",
"active_style",
"disabled_style",
"disabled",
"tab_index",
"events",
],
"list row checkbox",
)?;
ensure_allowed_event_fields(map, &["change"], "list row checkbox events")?;
let id = get_list_row_control_id(map, "checkbox")?;
Ok(IrNode::Checkbox(Box::new(decode_checkbox_node(
map,
Some(id),
)?)))
}
fn decode_list_row_radio(map: &HashMap<Term, Term>) -> Result<IrNode, String> {
ensure_allowed_fields(
map,
&[
"kind",
"label",
"value",
"checked",
"id",
"style",
"hover_style",
"focus_style",
"focus_visible_style",
"in_focus_style",
"active_style",
"disabled_style",
"disabled",
"tab_index",
"events",
],
"list row radio",
)?;
ensure_allowed_event_fields(map, &["change"], "list row radio events")?;
let id = get_list_row_control_id(map, "radio")?;
Ok(IrNode::Radio(Box::new(decode_radio_node(map, Some(id))?)))
}
fn get_list_row_control_id(map: &HashMap<Term, Term>, kind: &str) -> Result<String, String> {
get_optional_string_field(map, "id")?
.ok_or_else(|| format!("missing list row control id: {kind}"))
}
fn decode_static_list_row_div(map: &HashMap<Term, Term>, depth: usize) -> Result<IrNode, String> {
decode_static_div(
map,
depth,
"list row div",
"list row div events",
"missing required field: list row div children",
decode_list_row_child_term,
)
}
fn decode_static_data_table_cell_div(
map: &HashMap<Term, Term>,
depth: usize,
) -> Result<IrNode, String> {
decode_static_div(
map,
depth,
"data_table cell div",
"data_table cell div events",
"missing required field: data_table cell div children",
decode_data_table_cell_child_term,
)
}
fn decode_static_div(
map: &HashMap<Term, Term>,
depth: usize,
context: &str,
events_context: &str,
missing_children_error: &str,
decode_child: fn(&Term, usize) -> Result<IrNode, String>,
) -> Result<IrNode, String> {
ensure_allowed_fields(
map,
&["kind", "children", "id", "style", "disabled", "events"],
context,
)?;
ensure_allowed_event_fields(map, &["click"], events_context)?;
let Some(children_term) = get_field(map, "children") else {
return Err(missing_children_error.into());
};
let child_depth = next_ir_node_depth(depth);
let children = collect_arc(
get_list(children_term)?
.iter()
.map(|child| decode_child(child, child_depth)),
)?;
let empty_style = empty_style();
Ok(IrNode::Div(Box::new(DivNode {
id: get_optional_string_field(map, "id")?,
style: get_div_style(map)?,
hover_style: empty_style.clone(),
focus_style: empty_style.clone(),
focus_visible_style: empty_style.clone(),
in_focus_style: empty_style.clone(),
active_style: empty_style.clone(),
disabled_style: empty_style,
animation: None,
disabled: get_boolean_field(map, "disabled")?,
stack_priority: None,
occlude: false,
focusable: false,
tab_stop: None,
tab_index: None,
track_scroll: false,
anchor_scroll: false,
scroll_to: false,
tooltip: None,
shortcuts: empty_shortcuts(),
children,
click: get_click_event(map)?,
hover: None,
focus: None,
blur: None,
key_down: None,
key_up: None,
context_menu: None,
drag_start: None,
drag_move: None,
drop: None,
mouse_down: None,
mouse_up: None,
mouse_move: None,
scroll_wheel: None,
})))
}
fn ensure_unique_list_row_control_ids(children: &[IrNode]) -> Result<(), String> {
let mut seen = HashSet::new();
collect_list_row_control_ids(children, &mut seen)
}
fn collect_list_row_control_ids(
children: &[IrNode],
seen: &mut HashSet<String>,
) -> Result<(), String> {
for child in children {
match child {
IrNode::Button(node) => track_list_row_control_id(node.id.as_deref(), seen)?,
IrNode::Checkbox(node) => track_list_row_control_id(node.id.as_deref(), seen)?,
IrNode::Radio(node) => track_list_row_control_id(node.id.as_deref(), seen)?,
IrNode::Div(node) => collect_list_row_control_ids(&node.children, seen)?,
_ => {}
}
}
Ok(())
}
fn track_list_row_control_id(id: Option<&str>, seen: &mut HashSet<String>) -> Result<(), String> {
let Some(id) = id else {
return Err("missing list row control id".into());
};
if seen.insert(id.to_owned()) {
Ok(())
} else {
Err(format!("duplicate list row control id: {id}"))
}
}
fn empty_ir_nodes() -> Arc<[IrNode]> {
EMPTY_IR_NODES.get_or_init(|| Arc::new([])).clone()
}
pub(crate) fn empty_shortcuts() -> Arc<[ShortcutBinding]> {
EMPTY_SHORTCUTS.get_or_init(|| Arc::new([])).clone()
}
fn empty_style() -> DivStyle {
EMPTY_STYLE.get_or_init(|| Arc::new([])).clone()
}
fn empty_text_runs() -> Arc<[TextRunSegment]> {
EMPTY_TEXT_RUNS.get_or_init(|| Arc::new([])).clone()
}
fn empty_tree_items() -> Arc<[TreeItem]> {
EMPTY_TREE_ITEMS.get_or_init(|| Arc::new([])).clone()
}
fn ensure_allowed_node_events(kind: &str, map: &HashMap<Term, Term>) -> Result<(), String> {
let Some((allowed, context)) = allowed_node_event_fields(kind) else {
return Ok(());
};
ensure_allowed_event_fields(map, allowed, context)
}
fn ensure_allowed_fields(
map: &HashMap<Term, Term>,
allowed: &[&str],
context: &str,
) -> Result<(), String> {
for key in map.keys() {
let known = allowed
.iter()
.filter_map(|allowed_key| field_key(allowed_key))
.any(|allowed_key| key == allowed_key);
if !known {
return Err(format!("unsupported {context} field: {key}"));
}
}
Ok(())
}
fn ensure_allowed_event_fields(
map: &HashMap<Term, Term>,
allowed: &[&str],
context: &str,
) -> Result<(), String> {
let Some(events_term) = get_field(map, "events") else {
return Ok(());
};
ensure_allowed_fields(expect_map(events_term)?, allowed, context)
}
fn get_optional_string_field(
map: &HashMap<Term, Term>,
key: &str,
) -> Result<Option<String>, String> {
match get_field(map, key) {
Some(term) => term_to_string(term).map(Some),
None => Ok(None),
}
}
fn get_optional_string_pair_field(
map: &HashMap<Term, Term>,
key: &str,
) -> Result<Option<(String, String)>, String> {
let Some(term) = get_field(map, key) else {
return Ok(None);
};
let Term::Tuple(Tuple { elements }) = term else {
return Err(format!(
"expected string pair tuple field {key}, got {term}"
));
};
if elements.len() != 2 {
return Err(format!(
"expected string pair tuple field {key} with 2 elements, got {term}"
));
}
Ok(Some((
term_to_string(&elements[0])?,
term_to_string(&elements[1])?,
)))
}
fn get_boolean_field(map: &HashMap<Term, Term>, key: &str) -> Result<bool, String> {
match get_field(map, key) {
Some(Term::Atom(atom)) if atom.name == "true" => Ok(true),
Some(Term::Atom(atom)) if atom.name == "false" => Ok(false),
Some(other) => Err(format!("expected boolean field {key}, got {other}")),
None => Ok(false),
}
}
fn get_required_boolean_field(map: &HashMap<Term, Term>, key: &str) -> Result<bool, String> {
match get_field(map, key) {
Some(Term::Atom(atom)) if atom.name == "true" => Ok(true),
Some(Term::Atom(atom)) if atom.name == "false" => Ok(false),
Some(other) => Err(format!(
"expected required boolean field {key}, got {other}"
)),
None => Err(format!("missing required field: {key}")),
}
}
fn get_optional_boolean_field(
map: &HashMap<Term, Term>,
key: &str,
) -> Result<Option<bool>, String> {
match get_field(map, key) {
Some(Term::Atom(atom)) if atom.name == "true" => Ok(Some(true)),
Some(Term::Atom(atom)) if atom.name == "false" => Ok(Some(false)),
Some(other) => Err(format!(
"expected optional boolean field {key}, got {other}"
)),
None => Ok(None),
}
}
fn get_optional_integer_field(
map: &HashMap<Term, Term>,
key: &str,
) -> Result<Option<isize>, String> {
match get_field(map, key) {
Some(Term::FixInteger(value)) => Ok(Some(value.value as isize)),
Some(Term::BigInteger(value)) => value
.value
.clone()
.try_into()
.map(Some)
.map_err(|_| format!("invalid integer field {key}: {value}")),
Some(other) => Err(format!(
"expected optional integer field {key}, got {other}"
)),
None => Ok(None),
}
}
fn get_unit_f32_field(map: &HashMap<Term, Term>, key: &str, default: f32) -> Result<f32, String> {
let Some(term) = get_field(map, key) else {
return Ok(default);
};
let value = parse_f32(term)?;
if (0.0..=1.0).contains(&value) {
Ok(value)
} else {
Err(format!("expected unit numeric field {key}, got {term}"))
}
}
fn get_optional_u64_field(map: &HashMap<Term, Term>, key: &str) -> Result<Option<u64>, String> {
match get_field(map, key) {
Some(Term::FixInteger(value)) => u64::try_from(value.value)
.map(Some)
.map_err(|_| format!("expected non-negative integer field {key}, got {value:?}")),
Some(Term::BigInteger(value)) => value
.value
.clone()
.try_into()
.map(Some)
.map_err(|_| format!("expected non-negative integer field {key}, got {value:?}")),
Some(other) => Err(format!("expected integer field {key}, got {other}")),
None => Ok(None),
}
}
fn parse_positive_u32(term: &Term) -> Result<u32, String> {
match term {
Term::FixInteger(value) => u32::try_from(value.value)
.ok()
.filter(|value| *value > 0)
.ok_or_else(|| format!("expected positive u32, got {term}")),
Term::BigInteger(value) => value
.value
.clone()
.try_into()
.ok()
.filter(|value: &u32| *value > 0)
.ok_or_else(|| format!("expected positive u32, got {term}")),
other => Err(format!("expected positive u32, got {other}")),
}
}
fn get_optional_usize_field(map: &HashMap<Term, Term>, key: &str) -> Result<Option<usize>, String> {
match get_field(map, key) {
Some(Term::FixInteger(value)) => usize::try_from(value.value)
.map(Some)
.map_err(|error| format!("invalid usize field {key}: {error}")),
Some(Term::BigInteger(value)) => value
.value
.clone()
.try_into()
.map(Some)
.map_err(|_| format!("invalid usize field {key}: {value}")),
Some(other) => Err(format!("expected optional usize field {key}, got {other}")),
None => Ok(None),
}
}
fn default_button_style() -> DivStyle {
DEFAULT_BUTTON_STYLE
.get_or_init(|| {
vec![
StyleOp::Flex,
StyleOp::JustifyCenter,
StyleOp::ItemsCenter,
StyleOp::TextCenter,
StyleOp::P2,
StyleOp::RoundedMd,
StyleOp::Border1,
StyleOp::BorderColor(ColorToken::White),
StyleOp::Bg(ColorToken::Gray),
StyleOp::TextColor(ColorToken::White),
StyleOp::CursorPointer,
]
.into()
})
.clone()
}
fn default_button_focus_style() -> DivStyle {
DEFAULT_BUTTON_FOCUS_STYLE
.get_or_init(|| vec![StyleOp::BorderColor(ColorToken::Yellow)].into())
.clone()
}
fn default_button_active_style() -> DivStyle {
DEFAULT_BUTTON_ACTIVE_STYLE
.get_or_init(|| vec![StyleOp::Opacity(0.85)].into())
.clone()
}
fn default_button_disabled_style() -> DivStyle {
DEFAULT_BUTTON_DISABLED_STYLE
.get_or_init(|| vec![StyleOp::Opacity(0.45)].into())
.clone()
}
fn prepend_style(defaults: DivStyle, style: DivStyle) -> DivStyle {
if defaults.is_empty() {
return style;
}
if style.is_empty() {
return defaults;
}
defaults
.iter()
.cloned()
.chain(style.iter().cloned())
.collect::<Vec<_>>()
.into()
}
fn get_div_style(map: &HashMap<Term, Term>) -> Result<DivStyle, String> {
get_style_list_field(map, "style")
}
fn get_div_hover_style(map: &HashMap<Term, Term>) -> Result<DivStyle, String> {
get_style_list_field(map, "hover_style")
}
fn get_div_focus_style(map: &HashMap<Term, Term>) -> Result<DivStyle, String> {
get_style_list_field(map, "focus_style")
}
fn get_div_focus_visible_style(map: &HashMap<Term, Term>) -> Result<DivStyle, String> {
get_style_list_field(map, "focus_visible_style")
}
fn get_div_in_focus_style(map: &HashMap<Term, Term>) -> Result<DivStyle, String> {
get_style_list_field(map, "in_focus_style")
}
fn get_div_active_style(map: &HashMap<Term, Term>) -> Result<DivStyle, String> {
get_style_list_field(map, "active_style")
}
fn get_div_disabled_style(map: &HashMap<Term, Term>) -> Result<DivStyle, String> {
get_style_list_field(map, "disabled_style")
}
fn get_div_actions(map: &HashMap<Term, Term>) -> Result<HashMap<String, String>, String> {
let Some(actions_term) = get_field(map, "actions") else {
return Ok(HashMap::new());
};
let actions_map = expect_map(actions_term)?;
let mut actions = HashMap::new();
for (action_term, callback_term) in actions_map {
let action_name = term_to_string(action_term)?;
let callback_id = term_to_string(callback_term)?;
actions.insert(action_name, callback_id);
}
Ok(actions)
}
fn get_div_shortcuts(
map: &HashMap<Term, Term>,
actions: &HashMap<String, String>,
) -> Result<Arc<[ShortcutBinding]>, String> {
let Some(shortcuts_term) = get_field(map, "shortcuts") else {
return Ok(empty_shortcuts());
};
let shortcuts = get_list(shortcuts_term)?;
collect_arc(
shortcuts
.iter()
.map(|shortcut| parse_shortcut_binding(shortcut, actions)),
)
}
fn parse_shortcut_binding(
term: &Term,
actions: &HashMap<String, String>,
) -> Result<ShortcutBinding, String> {
let Term::Tuple(Tuple { elements }) = term else {
return Err(format!("expected shortcut tuple, got {term}"));
};
if elements.len() != 2 {
return Err(format!(
"expected shortcut tuple with 2 elements, got {term}"
));
}
let shortcut = term_to_string(&elements[0])?;
let action = term_to_string(&elements[1])?;
let callback = actions
.get(&action)
.cloned()
.ok_or_else(|| format!("shortcut references unknown action: {action}"))?;
Keystroke::parse(&shortcut)
.map(KeybindingKeystroke::from_keystroke)
.map_err(|error| error.to_string())
.map(|parsed| ShortcutBinding {
shortcut,
action,
callback,
parsed,
})
}
fn get_style_list_field(map: &HashMap<Term, Term>, key: &str) -> Result<DivStyle, String> {
let Some(style_term) = get_field(map, key) else {
return Ok(empty_style());
};
let style_list = get_list(style_term)?;
if style_list.is_empty() {
return Ok(empty_style());
}
collect_arc(style_list.iter().map(parse_style_op))
}
fn parse_style_op(term: &Term) -> Result<StyleOp, String> {
match term {
Term::Atom(atom) => parse_style_flag(&atom.name),
Term::Tuple(Tuple { elements }) if elements.len() == 3 => {
let key = match &elements[0] {
Term::Atom(atom) => atom.name.as_str(),
other => return Err(format!("expected style tuple key atom, got {other}")),
};
match key {
"padding" => Ok(StyleOp::Padding {
axis: parse_style_axis(&elements[1], "padding")?,
length: parse_style_length(&elements[2], "padding", false, false)?,
}),
"inset" => Ok(StyleOp::Inset {
axis: parse_inset_axis(&elements[1])?,
length: parse_style_length(&elements[2], "inset", true, true)?,
}),
"margin" => Ok(StyleOp::Margin {
axis: parse_style_axis(&elements[1], "margin")?,
length: parse_style_length(&elements[2], "margin", true, true)?,
}),
"gap" => Ok(StyleOp::Gap {
axis: parse_gap_axis(&elements[1])?,
length: parse_style_length(&elements[2], "gap", false, true)?,
}),
"overflow" => Ok(StyleOp::Overflow {
axis: parse_gap_axis(&elements[1])?,
behavior: parse_overflow_style(&elements[2])?,
}),
"border_width" => Ok(StyleOp::BorderWidth {
axis: parse_style_axis(&elements[1], "border_width")?,
length: parse_absolute_style_length(&elements[2], "border_width")?,
}),
"border_radius" => Ok(StyleOp::BorderRadius {
axis: parse_border_radius_axis(&elements[1])?,
length: parse_absolute_style_length(&elements[2], "border_radius")?,
}),
other => Err(format!("unsupported style tuple key: {other}")),
}
}
Term::Tuple(Tuple { elements }) if elements.len() == 2 => {
let key = match &elements[0] {
Term::Atom(atom) => atom.name.as_str(),
other => return Err(format!("expected style tuple key atom, got {other}")),
};
match key {
"width" => Ok(StyleOp::Width(parse_style_length(
&elements[1],
"width",
true,
true,
)?)),
"height" => Ok(StyleOp::Height(parse_style_length(
&elements[1],
"height",
true,
true,
)?)),
"size" => Ok(StyleOp::Size(parse_style_length(
&elements[1],
"size",
true,
true,
)?)),
"min_width" => Ok(StyleOp::MinWidth(parse_style_length(
&elements[1],
"min_width",
true,
true,
)?)),
"min_height" => Ok(StyleOp::MinHeight(parse_style_length(
&elements[1],
"min_height",
true,
true,
)?)),
"max_width" => Ok(StyleOp::MaxWidth(parse_style_length(
&elements[1],
"max_width",
true,
true,
)?)),
"max_height" => Ok(StyleOp::MaxHeight(parse_style_length(
&elements[1],
"max_height",
true,
true,
)?)),
"aspect_ratio" => Ok(StyleOp::AspectRatio(parse_positive_style_f32(
&elements[1],
"aspect_ratio",
)?)),
"position" => Ok(StyleOp::Position(parse_position_style(&elements[1])?)),
"display" => Ok(StyleOp::Display(parse_display_style(&elements[1])?)),
"visibility" => Ok(StyleOp::Visibility(parse_visibility_style(&elements[1])?)),
"allow_concurrent_scroll" => Ok(StyleOp::AllowConcurrentScroll(parse_style_bool(
&elements[1],
"allow_concurrent_scroll",
)?)),
"restrict_scroll_to_axis" => Ok(StyleOp::RestrictScrollToAxis(parse_style_bool(
&elements[1],
"restrict_scroll_to_axis",
)?)),
"debug" => Ok(StyleOp::Debug(parse_style_bool(&elements[1], "debug")?)),
"debug_below" => Ok(StyleOp::DebugBelow(parse_style_bool(
&elements[1],
"debug_below",
)?)),
"cursor" => Ok(StyleOp::Cursor(parse_mouse_cursor_style(&elements[1])?)),
"border_style" => Ok(StyleOp::BorderStyle(parse_border_line_style(&elements[1])?)),
"shadow" => Ok(StyleOp::Shadow(parse_shadow_style(&elements[1])?)),
"flex_direction" => Ok(StyleOp::FlexDirection(parse_flex_direction_style(
&elements[1],
)?)),
"flex_wrap" => Ok(StyleOp::FlexWrapValue(parse_flex_wrap_style(&elements[1])?)),
"flex_item" => Ok(StyleOp::FlexItem(parse_flex_item_style(&elements[1])?)),
"flex_basis" => Ok(StyleOp::FlexBasis(parse_style_length(
&elements[1],
"flex_basis",
true,
false,
)?)),
"flex_grow" => Ok(StyleOp::FlexGrowValue(parse_non_negative_style_f32(
&elements[1],
"flex_grow",
)?)),
"flex_shrink" => Ok(StyleOp::FlexShrinkValue(parse_non_negative_style_f32(
&elements[1],
"flex_shrink",
)?)),
"align_items" => Ok(StyleOp::AlignItems(parse_align_items_style(&elements[1])?)),
"align_self" => Ok(StyleOp::AlignSelf(parse_align_items_style(&elements[1])?)),
"justify_content" => Ok(StyleOp::JustifyContent(parse_justify_content_style(
&elements[1],
)?)),
"align_content" => Ok(StyleOp::AlignContent(parse_align_content_style(
&elements[1],
)?)),
"text_align" => Ok(StyleOp::TextAlign(parse_text_align_style(&elements[1])?)),
"white_space" => Ok(StyleOp::WhiteSpace(parse_white_space_style(&elements[1])?)),
"text_overflow" => Ok(StyleOp::TextOverflow(parse_text_overflow_style(
&elements[1],
)?)),
"font_size" => Ok(StyleOp::FontSize(parse_font_size_style(&elements[1])?)),
"text_size" => Ok(StyleOp::TextSize(parse_absolute_style_length(
&elements[1],
"text_size",
)?)),
"line_height" => Ok(StyleOp::LineHeight(parse_line_height_style(&elements[1])?)),
"line_height_length" => Ok(StyleOp::LineHeightLength(parse_style_length(
&elements[1],
"line_height_length",
false,
false,
)?)),
"font_weight" => Ok(StyleOp::FontWeight(parse_font_weight_style(&elements[1])?)),
"font_weight_value" => Ok(StyleOp::FontWeightValue(parse_font_weight_value(
&elements[1],
)?)),
"font_style" => Ok(StyleOp::FontStyle(parse_font_style_value(&elements[1])?)),
"font_family" => Ok(StyleOp::FontFamily(parse_non_empty_style_string(
&elements[1],
"font_family",
)?)),
"font_fallbacks" => Ok(StyleOp::FontFallbacks(parse_non_empty_style_string_list(
&elements[1],
"font_fallbacks",
)?)),
"font_features" => Ok(StyleOp::FontFeatures(parse_font_features(&elements[1])?)),
"text_decoration" => Ok(StyleOp::TextDecoration(parse_text_decoration_style(
&elements[1],
)?)),
"text_decoration_color" => Ok(StyleOp::TextDecorationColor(parse_atom_color(
&elements[1],
)?)),
"text_decoration_color_hex" => Ok(StyleOp::TextDecorationColorHex(
parse_hex_style_color(&elements[1])?,
)),
"text_decoration_style" => Ok(StyleOp::TextDecorationLineStyle(
parse_text_decoration_line_style(&elements[1])?,
)),
"text_decoration_thickness" => Ok(StyleOp::TextDecorationThickness(
parse_non_negative_style_f32(&elements[1], "text_decoration_thickness")?,
)),
"strikethrough_color" => {
Ok(StyleOp::StrikethroughColor(parse_atom_color(&elements[1])?))
}
"strikethrough_color_hex" => Ok(StyleOp::StrikethroughColorHex(
parse_hex_style_color(&elements[1])?,
)),
"strikethrough_thickness" => Ok(StyleOp::StrikethroughThickness(
parse_non_negative_style_f32(&elements[1], "strikethrough_thickness")?,
)),
"bg" => Ok(StyleOp::Bg(parse_atom_color(&elements[1])?)),
"text_color" => Ok(StyleOp::TextColor(parse_atom_color(&elements[1])?)),
"text_bg" => Ok(StyleOp::TextBg(parse_atom_color(&elements[1])?)),
"border_color" => Ok(StyleOp::BorderColor(parse_atom_color(&elements[1])?)),
"bg_hex" => Ok(StyleOp::BgHex(parse_hex_style_color(&elements[1])?)),
"text_color_hex" => Ok(StyleOp::TextColorHex(parse_hex_style_color(&elements[1])?)),
"text_bg_hex" => Ok(StyleOp::TextBgHex(parse_hex_style_color(&elements[1])?)),
"border_color_hex" => Ok(StyleOp::BorderColorHex(parse_hex_style_color(
&elements[1],
)?)),
"bg_linear_gradient" => {
let (angle, from, to) = parse_linear_gradient_options(&elements[1])?;
Ok(StyleOp::BgLinearGradient { angle, from, to })
}
"bg_pattern_slash" => Ok(StyleOp::BgPatternSlash(parse_background_pattern_slash(
&elements[1],
)?)),
"box_shadow" => Ok(StyleOp::BoxShadow(parse_box_shadows(&elements[1])?)),
"opacity" => Ok(StyleOp::Opacity(parse_unit_style_f32(
&elements[1],
"opacity",
)?)),
"line_clamp" => Ok(StyleOp::LineClamp(parse_positive_u16(
&elements[1],
"line_clamp",
)?)),
"grid_cols" => Ok(StyleOp::GridCols(parse_grid_u16(&elements[1])?)),
"grid_rows" => Ok(StyleOp::GridRows(parse_grid_u16(&elements[1])?)),
"col_start" => parse_grid_line_style(
&elements[1],
"col_start",
StyleOp::ColStart,
StyleOp::ColStartAuto,
),
"col_end" => parse_grid_line_style(
&elements[1],
"col_end",
StyleOp::ColEnd,
StyleOp::ColEndAuto,
),
"row_start" => parse_grid_line_style(
&elements[1],
"row_start",
StyleOp::RowStart,
StyleOp::RowStartAuto,
),
"row_end" => parse_grid_line_style(
&elements[1],
"row_end",
StyleOp::RowEnd,
StyleOp::RowEndAuto,
),
"col_span" => parse_grid_span_style(
&elements[1],
"col_span",
StyleOp::ColSpan,
StyleOp::ColSpanFull,
),
"row_span" => parse_grid_span_style(
&elements[1],
"row_span",
StyleOp::RowSpan,
StyleOp::RowSpanFull,
),
"w_px" => Ok(StyleOp::WPx(parse_non_negative_style_f32(
&elements[1],
"w_px",
)?)),
"w_rem" => Ok(StyleOp::WRem(parse_non_negative_style_f32(
&elements[1],
"w_rem",
)?)),
"w_frac" => Ok(StyleOp::WFrac(parse_unit_style_f32(
&elements[1],
"w_frac",
)?)),
"h_px" => Ok(StyleOp::HPx(parse_non_negative_style_f32(
&elements[1],
"h_px",
)?)),
"h_rem" => Ok(StyleOp::HRem(parse_non_negative_style_f32(
&elements[1],
"h_rem",
)?)),
"h_frac" => Ok(StyleOp::HFrac(parse_unit_style_f32(
&elements[1],
"h_frac",
)?)),
"scrollbar_width_px" => Ok(StyleOp::ScrollbarWidthPx(
parse_non_negative_style_f32(&elements[1], "scrollbar_width_px")?,
)),
"scrollbar_width_rem" => Ok(StyleOp::ScrollbarWidthRem(
parse_non_negative_style_f32(&elements[1], "scrollbar_width_rem")?,
)),
other => Err(format!("unsupported style tuple key: {other}")),
}
}
other => Err(format!("unsupported style op: {other}")),
}
}
fn parse_style_flag(token: &str) -> Result<StyleOp, String> {
match token {
"grid" => Ok(StyleOp::Grid),
"flex" => Ok(StyleOp::Flex),
"flex_col" => Ok(StyleOp::FlexCol),
"flex_row" => Ok(StyleOp::FlexRow),
"flex_wrap" => Ok(StyleOp::FlexWrap),
"flex_nowrap" => Ok(StyleOp::FlexNowrap),
"flex_none" => Ok(StyleOp::FlexNone),
"flex_auto" => Ok(StyleOp::FlexAuto),
"flex_grow" => Ok(StyleOp::FlexGrow),
"flex_shrink" => Ok(StyleOp::FlexShrink),
"flex_shrink_0" => Ok(StyleOp::FlexShrink0),
"flex_1" => Ok(StyleOp::Flex1),
"col_span_full" => Ok(StyleOp::ColSpanFull),
"row_span_full" => Ok(StyleOp::RowSpanFull),
"size_full" => Ok(StyleOp::SizeFull),
"w_full" => Ok(StyleOp::WFull),
"h_full" => Ok(StyleOp::HFull),
"w_32" => Ok(StyleOp::W32),
"w_64" => Ok(StyleOp::W64),
"w_96" => Ok(StyleOp::W96),
"h_32" => Ok(StyleOp::H32),
"min_w_32" => Ok(StyleOp::MinW32),
"min_h_0" => Ok(StyleOp::MinH0),
"min_h_full" => Ok(StyleOp::MinHFull),
"max_w_64" => Ok(StyleOp::MaxW64),
"max_w_96" => Ok(StyleOp::MaxW96),
"max_w_full" => Ok(StyleOp::MaxWFull),
"max_h_32" => Ok(StyleOp::MaxH32),
"max_h_96" => Ok(StyleOp::MaxH96),
"max_h_full" => Ok(StyleOp::MaxHFull),
"gap_1" => Ok(StyleOp::Gap1),
"gap_2" => Ok(StyleOp::Gap2),
"gap_4" => Ok(StyleOp::Gap4),
"p_1" => Ok(StyleOp::P1),
"p_2" => Ok(StyleOp::P2),
"p_4" => Ok(StyleOp::P4),
"p_6" => Ok(StyleOp::P6),
"p_8" => Ok(StyleOp::P8),
"px_2" => Ok(StyleOp::Px2),
"py_2" => Ok(StyleOp::Py2),
"pt_2" => Ok(StyleOp::Pt2),
"pr_2" => Ok(StyleOp::Pr2),
"pb_2" => Ok(StyleOp::Pb2),
"pl_2" => Ok(StyleOp::Pl2),
"m_2" => Ok(StyleOp::M2),
"mx_2" => Ok(StyleOp::Mx2),
"my_2" => Ok(StyleOp::My2),
"mt_2" => Ok(StyleOp::Mt2),
"mr_2" => Ok(StyleOp::Mr2),
"mb_2" => Ok(StyleOp::Mb2),
"ml_2" => Ok(StyleOp::Ml2),
"relative" => Ok(StyleOp::Relative),
"absolute" => Ok(StyleOp::Absolute),
"top_0" => Ok(StyleOp::Top0),
"right_0" => Ok(StyleOp::Right0),
"bottom_0" => Ok(StyleOp::Bottom0),
"left_0" => Ok(StyleOp::Left0),
"inset_0" => Ok(StyleOp::Inset0),
"top_1" => Ok(StyleOp::Top1),
"right_1" => Ok(StyleOp::Right1),
"top_2" => Ok(StyleOp::Top2),
"right_2" => Ok(StyleOp::Right2),
"bottom_2" => Ok(StyleOp::Bottom2),
"left_2" => Ok(StyleOp::Left2),
"text_left" => Ok(StyleOp::TextLeft),
"text_center" => Ok(StyleOp::TextCenter),
"text_right" => Ok(StyleOp::TextRight),
"whitespace_normal" => Ok(StyleOp::WhitespaceNormal),
"whitespace_nowrap" => Ok(StyleOp::WhitespaceNowrap),
"truncate" => Ok(StyleOp::Truncate),
"text_ellipsis" => Ok(StyleOp::TextEllipsis),
"line_clamp_2" => Ok(StyleOp::LineClamp2),
"line_clamp_3" => Ok(StyleOp::LineClamp3),
"text_xs" => Ok(StyleOp::TextXs),
"text_sm" => Ok(StyleOp::TextSm),
"text_base" => Ok(StyleOp::TextBase),
"text_lg" => Ok(StyleOp::TextLg),
"text_xl" => Ok(StyleOp::TextXl),
"text_2xl" => Ok(StyleOp::Text2xl),
"text_3xl" => Ok(StyleOp::Text3xl),
"leading_none" => Ok(StyleOp::LeadingNone),
"leading_tight" => Ok(StyleOp::LeadingTight),
"leading_snug" => Ok(StyleOp::LeadingSnug),
"leading_normal" => Ok(StyleOp::LeadingNormal),
"leading_relaxed" => Ok(StyleOp::LeadingRelaxed),
"leading_loose" => Ok(StyleOp::LeadingLoose),
"font_thin" => Ok(StyleOp::FontThin),
"font_extralight" => Ok(StyleOp::FontExtralight),
"font_light" => Ok(StyleOp::FontLight),
"font_normal" => Ok(StyleOp::FontNormal),
"font_medium" => Ok(StyleOp::FontMedium),
"font_semibold" => Ok(StyleOp::FontSemibold),
"font_bold" => Ok(StyleOp::FontBold),
"font_extrabold" => Ok(StyleOp::FontExtrabold),
"font_black" => Ok(StyleOp::FontBlack),
"italic" => Ok(StyleOp::Italic),
"not_italic" => Ok(StyleOp::NotItalic),
"underline" => Ok(StyleOp::Underline),
"line_through" => Ok(StyleOp::LineThrough),
"items_start" => Ok(StyleOp::ItemsStart),
"items_center" => Ok(StyleOp::ItemsCenter),
"items_end" => Ok(StyleOp::ItemsEnd),
"justify_start" => Ok(StyleOp::JustifyStart),
"justify_center" => Ok(StyleOp::JustifyCenter),
"justify_end" => Ok(StyleOp::JustifyEnd),
"justify_between" => Ok(StyleOp::JustifyBetween),
"justify_around" => Ok(StyleOp::JustifyAround),
"cursor_pointer" => Ok(StyleOp::CursorPointer),
"rounded_sm" => Ok(StyleOp::RoundedSm),
"rounded_md" => Ok(StyleOp::RoundedMd),
"rounded_lg" => Ok(StyleOp::RoundedLg),
"rounded_xl" => Ok(StyleOp::RoundedXl),
"rounded_2xl" => Ok(StyleOp::Rounded2xl),
"rounded_full" => Ok(StyleOp::RoundedFull),
"border_1" => Ok(StyleOp::Border1),
"border_2" => Ok(StyleOp::Border2),
"border_dashed" => Ok(StyleOp::BorderDashed),
"border_t_1" => Ok(StyleOp::BorderT1),
"border_r_1" => Ok(StyleOp::BorderR1),
"border_b_1" => Ok(StyleOp::BorderB1),
"border_l_1" => Ok(StyleOp::BorderL1),
"shadow_sm" => Ok(StyleOp::ShadowSm),
"shadow_md" => Ok(StyleOp::ShadowMd),
"shadow_lg" => Ok(StyleOp::ShadowLg),
"overflow_scroll" => Ok(StyleOp::OverflowScroll),
"overflow_x_scroll" => Ok(StyleOp::OverflowXScroll),
"overflow_y_scroll" => Ok(StyleOp::OverflowYScroll),
"overflow_hidden" => Ok(StyleOp::OverflowHidden),
"overflow_x_hidden" => Ok(StyleOp::OverflowXHidden),
"overflow_y_hidden" => Ok(StyleOp::OverflowYHidden),
other => Err(format!("unsupported style token: {other}")),
}
}
fn parse_atom_color(term: &Term) -> Result<ColorToken, String> {
match term {
Term::Atom(atom) => parse_color_token(&atom.name),
other => Err(format!("expected style tuple value atom, got {other}")),
}
}
fn for_each_keyword_option<F>(term: &Term, context: &str, mut parse: F) -> Result<(), String>
where
F: FnMut(&str, &Term) -> Result<(), String>,
{
for option in get_list(term)? {
let Term::Tuple(Tuple { elements }) = option else {
return Err(format!("expected {context} option tuple, got {option}"));
};
if elements.len() != 2 {
return Err(format!(
"expected {context} option tuple with 2 elements, got {option}"
));
}
let Term::Atom(key) = &elements[0] else {
return Err(format!(
"expected {context} option key atom, got {}",
elements[0]
));
};
parse(&key.name, &elements[1])?;
}
Ok(())
}
fn parse_linear_gradient_options(
term: &Term,
) -> Result<(f32, LinearGradientStop, LinearGradientStop), String> {
let mut angle = None;
let mut from = None;
let mut to = None;
for_each_keyword_option(term, "gradient", |key, value| match key {
"angle" => set_once(
&mut angle,
parse_gradient_angle(value)?,
"duplicate gradient angle option",
),
"from" => set_once(
&mut from,
parse_linear_gradient_stop(value)?,
"duplicate gradient from option",
),
"to" => set_once(
&mut to,
parse_linear_gradient_stop(value)?,
"duplicate gradient to option",
),
other => Err(format!("unsupported gradient option: {other}")),
})?;
Ok((
angle.ok_or_else(|| "missing gradient angle option".to_string())?,
from.ok_or_else(|| "missing gradient from option".to_string())?,
to.ok_or_else(|| "missing gradient to option".to_string())?,
))
}
fn set_once<T>(slot: &mut Option<T>, value: T, duplicate_error: &str) -> Result<(), String> {
if slot.is_some() {
Err(duplicate_error.into())
} else {
*slot = Some(value);
Ok(())
}
}
fn parse_gradient_angle(term: &Term) -> Result<f32, String> {
let value = parse_f32(term)?;
if value.is_finite() && (0.0..=360.0).contains(&value) {
Ok(value)
} else {
Err(format!("invalid gradient angle: {value}"))
}
}
fn parse_background_pattern_slash(term: &Term) -> Result<BackgroundPatternSlash, String> {
let mut color = None;
let mut width = None;
let mut interval = None;
for_each_keyword_option(term, "background pattern", |key, value| match key {
"color" => set_once(
&mut color,
parse_style_color(value)
.map_err(|error| format!("invalid background pattern color: {error}"))?,
"duplicate background pattern color option",
),
"width" => set_once(
&mut width,
parse_non_negative_style_f32(value, "background pattern width")?,
"duplicate background pattern width option",
),
"interval" => set_once(
&mut interval,
parse_non_negative_style_f32(value, "background pattern interval")?,
"duplicate background pattern interval option",
),
other => Err(format!("unsupported background pattern option: {other}")),
})?;
Ok(BackgroundPatternSlash {
color: color.ok_or_else(|| "missing background pattern color option".to_string())?,
width: width.ok_or_else(|| "missing background pattern width option".to_string())?,
interval: interval
.ok_or_else(|| "missing background pattern interval option".to_string())?,
})
}
fn parse_box_shadows(term: &Term) -> Result<Vec<BoxShadowSpec>, String> {
let shadows = get_list(term)?;
shadows
.iter()
.map(parse_box_shadow_spec)
.collect::<Result<Vec<_>, _>>()
}
fn parse_box_shadow_spec(term: &Term) -> Result<BoxShadowSpec, String> {
let mut color = None;
let mut x = None;
let mut y = None;
let mut blur = None;
let mut spread = None;
for_each_keyword_option(term, "box shadow", |key, value| match key {
"color" => set_once(
&mut color,
parse_style_color(value)
.map_err(|error| format!("invalid box shadow color: {error}"))?,
"duplicate box shadow color option",
),
"x" => set_once(
&mut x,
parse_finite_style_f32(value, "box shadow x")?,
"duplicate box shadow x option",
),
"y" => set_once(
&mut y,
parse_finite_style_f32(value, "box shadow y")?,
"duplicate box shadow y option",
),
"blur" => set_once(
&mut blur,
parse_non_negative_style_f32(value, "box shadow blur")?,
"duplicate box shadow blur option",
),
"spread" => set_once(
&mut spread,
parse_finite_style_f32(value, "box shadow spread")?,
"duplicate box shadow spread option",
),
other => Err(format!("unsupported box shadow option: {other}")),
})?;
Ok(BoxShadowSpec {
color: color.ok_or_else(|| "missing box shadow color option".to_string())?,
x: x.ok_or_else(|| "missing box shadow x option".to_string())?,
y: y.ok_or_else(|| "missing box shadow y option".to_string())?,
blur: blur.ok_or_else(|| "missing box shadow blur option".to_string())?,
spread: spread.ok_or_else(|| "missing box shadow spread option".to_string())?,
})
}
fn parse_linear_gradient_stop(term: &Term) -> Result<LinearGradientStop, String> {
let Term::Tuple(Tuple { elements }) = term else {
return Err(format!("expected gradient stop tuple, got {term}"));
};
if elements.len() != 2 {
return Err(format!(
"expected gradient stop tuple with 2 elements, got {term}"
));
}
Ok(LinearGradientStop {
color: parse_style_color(&elements[0])?,
percentage: parse_gradient_percentage(&elements[1])?,
})
}
fn parse_gradient_percentage(term: &Term) -> Result<f32, String> {
let value = parse_f32(term)?;
if value.is_finite() && (0.0..=1.0).contains(&value) {
Ok(value)
} else {
Err(format!("invalid gradient percentage: {value}"))
}
}
fn parse_style_color(term: &Term) -> Result<StyleColor, String> {
match term {
Term::Atom(atom) => parse_color_token(&atom.name).map(StyleColor::Token),
Term::Binary(_) | Term::ByteList(_) => parse_strict_hex_color(term)
.map(StyleColor::Hex)
.map_err(|error| format!("invalid gradient color: {error}")),
other => Err(format!(
"expected gradient color atom or #RRGGBB string, got {other}"
)),
}
}
fn parse_hex_style_color(term: &Term) -> Result<u32, String> {
parse_hex_color(term, false, "style hex color")
}
fn parse_strict_hex_color(term: &Term) -> Result<u32, String> {
parse_hex_color(term, true, "strict hex color")
}
fn parse_hex_color(term: &Term, require_hash: bool, context: &str) -> Result<u32, String> {
let value = term_to_str(term)?;
if is_hex_color(value, require_hash) {
Ok(hex_color_u24(value))
} else {
Err(format!("invalid {context}: {value}"))
}
}
fn hex_color_u24(value: &str) -> u32 {
let normalized = value.strip_prefix('#').unwrap_or(value);
u32::from_str_radix(normalized, 16).unwrap_or(0xff00ff)
}
fn is_hex_color(value: &str, require_hash: bool) -> bool {
let normalized = value.strip_prefix('#').unwrap_or(value);
(!require_hash || value.starts_with('#'))
&& normalized.len() == 6
&& normalized.bytes().all(|byte| byte.is_ascii_hexdigit())
}
fn parse_style_axis(term: &Term, key: &str) -> Result<StyleAxis, String> {
match term {
Term::Atom(atom) if atom.name == "all" => Ok(StyleAxis::All),
Term::Atom(atom) if atom.name == "x" => Ok(StyleAxis::X),
Term::Atom(atom) if atom.name == "y" => Ok(StyleAxis::Y),
Term::Atom(atom) if atom.name == "top" => Ok(StyleAxis::Top),
Term::Atom(atom) if atom.name == "right" => Ok(StyleAxis::Right),
Term::Atom(atom) if atom.name == "bottom" => Ok(StyleAxis::Bottom),
Term::Atom(atom) if atom.name == "left" => Ok(StyleAxis::Left),
other => Err(format!("invalid {key} axis: {other}")),
}
}
fn parse_gap_axis(term: &Term) -> Result<StyleAxis, String> {
let axis = parse_style_axis(term, "gap")?;
match axis {
StyleAxis::All | StyleAxis::X | StyleAxis::Y => Ok(axis),
_ => Err(format!("invalid gap axis: {term}")),
}
}
fn parse_inset_axis(term: &Term) -> Result<StyleAxis, String> {
let axis = parse_style_axis(term, "inset")?;
match axis {
StyleAxis::All
| StyleAxis::Top
| StyleAxis::Right
| StyleAxis::Bottom
| StyleAxis::Left => Ok(axis),
_ => Err(format!("invalid inset axis: {term}")),
}
}
fn parse_position_style(term: &Term) -> Result<PositionStyle, String> {
match term {
Term::Atom(atom) if atom.name == "relative" => Ok(PositionStyle::Relative),
Term::Atom(atom) if atom.name == "absolute" => Ok(PositionStyle::Absolute),
other => Err(format!("invalid position style: {other}")),
}
}
fn parse_display_style(term: &Term) -> Result<DisplayStyle, String> {
match term {
Term::Atom(atom) if atom.name == "block" => Ok(DisplayStyle::Block),
Term::Atom(atom) if atom.name == "flex" => Ok(DisplayStyle::Flex),
Term::Atom(atom) if atom.name == "grid" => Ok(DisplayStyle::Grid),
Term::Atom(atom) if atom.name == "none" => Ok(DisplayStyle::None),
other => Err(format!("invalid display style: {other}")),
}
}
fn parse_visibility_style(term: &Term) -> Result<VisibilityStyle, String> {
match term {
Term::Atom(atom) if atom.name == "visible" => Ok(VisibilityStyle::Visible),
Term::Atom(atom) if atom.name == "hidden" => Ok(VisibilityStyle::Hidden),
other => Err(format!("invalid visibility style: {other}")),
}
}
fn parse_style_bool(term: &Term, key: &str) -> Result<bool, String> {
match term {
Term::Atom(atom) if atom.name == "true" => Ok(true),
Term::Atom(atom) if atom.name == "false" => Ok(false),
other => Err(format!("invalid boolean style {key}: {other}")),
}
}
fn parse_overflow_style(term: &Term) -> Result<OverflowStyle, String> {
match term {
Term::Atom(atom) if atom.name == "visible" => Ok(OverflowStyle::Visible),
Term::Atom(atom) if atom.name == "clip" => Ok(OverflowStyle::Clip),
Term::Atom(atom) if atom.name == "hidden" => Ok(OverflowStyle::Hidden),
Term::Atom(atom) if atom.name == "scroll" => Ok(OverflowStyle::Scroll),
other => Err(format!("invalid overflow style: {other}")),
}
}
fn parse_border_radius_axis(term: &Term) -> Result<BorderRadiusAxis, String> {
let Term::Atom(atom) = term else {
return Err(format!("invalid border_radius axis: {term}"));
};
match atom.name.as_str() {
"all" => Ok(BorderRadiusAxis::All),
"top" => Ok(BorderRadiusAxis::Top),
"right" => Ok(BorderRadiusAxis::Right),
"bottom" => Ok(BorderRadiusAxis::Bottom),
"left" => Ok(BorderRadiusAxis::Left),
"top_left" => Ok(BorderRadiusAxis::TopLeft),
"top_right" => Ok(BorderRadiusAxis::TopRight),
"bottom_left" => Ok(BorderRadiusAxis::BottomLeft),
"bottom_right" => Ok(BorderRadiusAxis::BottomRight),
other => Err(format!("invalid border_radius axis: {other}")),
}
}
fn parse_border_line_style(term: &Term) -> Result<BorderLineStyle, String> {
match term {
Term::Atom(atom) if atom.name == "solid" => Ok(BorderLineStyle::Solid),
Term::Atom(atom) if atom.name == "dashed" => Ok(BorderLineStyle::Dashed),
other => Err(format!("invalid border_style: {other}")),
}
}
fn parse_shadow_style(term: &Term) -> Result<ShadowStyle, String> {
let Term::Atom(atom) = term else {
return Err(format!("invalid shadow style: {term}"));
};
match atom.name.as_str() {
"none" => Ok(ShadowStyle::None),
"2xs" => Ok(ShadowStyle::TwoXs),
"xs" => Ok(ShadowStyle::Xs),
"sm" => Ok(ShadowStyle::Sm),
"md" => Ok(ShadowStyle::Md),
"lg" => Ok(ShadowStyle::Lg),
"xl" => Ok(ShadowStyle::Xl),
"2xl" => Ok(ShadowStyle::TwoXl),
other => Err(format!("invalid shadow style: {other}")),
}
}
fn parse_flex_direction_style(term: &Term) -> Result<FlexDirectionStyle, String> {
let Term::Atom(atom) = term else {
return Err(format!("invalid flex_direction style: {term}"));
};
match atom.name.as_str() {
"column" => Ok(FlexDirectionStyle::Column),
"column_reverse" => Ok(FlexDirectionStyle::ColumnReverse),
"row" => Ok(FlexDirectionStyle::Row),
"row_reverse" => Ok(FlexDirectionStyle::RowReverse),
other => Err(format!("invalid flex_direction style: {other}")),
}
}
fn parse_flex_wrap_style(term: &Term) -> Result<FlexWrapStyle, String> {
let Term::Atom(atom) = term else {
return Err(format!("invalid flex_wrap style: {term}"));
};
match atom.name.as_str() {
"wrap" => Ok(FlexWrapStyle::Wrap),
"wrap_reverse" => Ok(FlexWrapStyle::WrapReverse),
"nowrap" => Ok(FlexWrapStyle::NoWrap),
other => Err(format!("invalid flex_wrap style: {other}")),
}
}
fn parse_flex_item_style(term: &Term) -> Result<FlexItemStyle, String> {
let Term::Atom(atom) = term else {
return Err(format!("invalid flex_item style: {term}"));
};
match atom.name.as_str() {
"one" => Ok(FlexItemStyle::One),
"auto" => Ok(FlexItemStyle::Auto),
"initial" => Ok(FlexItemStyle::Initial),
"none" => Ok(FlexItemStyle::None),
"grow" => Ok(FlexItemStyle::Grow),
"shrink" => Ok(FlexItemStyle::Shrink),
"shrink_0" => Ok(FlexItemStyle::Shrink0),
other => Err(format!("invalid flex_item style: {other}")),
}
}
fn parse_align_items_style(term: &Term) -> Result<AlignItemsStyle, String> {
let Term::Atom(atom) = term else {
return Err(format!("invalid align_items style: {term}"));
};
match atom.name.as_str() {
"start" => Ok(AlignItemsStyle::Start),
"end" => Ok(AlignItemsStyle::End),
"center" => Ok(AlignItemsStyle::Center),
"baseline" => Ok(AlignItemsStyle::Baseline),
"stretch" => Ok(AlignItemsStyle::Stretch),
other => Err(format!("invalid align_items style: {other}")),
}
}
fn parse_justify_content_style(term: &Term) -> Result<JustifyContentStyle, String> {
let Term::Atom(atom) = term else {
return Err(format!("invalid justify_content style: {term}"));
};
match atom.name.as_str() {
"start" => Ok(JustifyContentStyle::Start),
"end" => Ok(JustifyContentStyle::End),
"center" => Ok(JustifyContentStyle::Center),
"between" => Ok(JustifyContentStyle::Between),
"around" => Ok(JustifyContentStyle::Around),
"evenly" => Ok(JustifyContentStyle::Evenly),
"stretch" => Ok(JustifyContentStyle::Stretch),
other => Err(format!("invalid justify_content style: {other}")),
}
}
fn parse_align_content_style(term: &Term) -> Result<AlignContentStyle, String> {
let Term::Atom(atom) = term else {
return Err(format!("invalid align_content style: {term}"));
};
match atom.name.as_str() {
"normal" => Ok(AlignContentStyle::Normal),
"start" => Ok(AlignContentStyle::Start),
"end" => Ok(AlignContentStyle::End),
"center" => Ok(AlignContentStyle::Center),
"between" => Ok(AlignContentStyle::Between),
"around" => Ok(AlignContentStyle::Around),
"evenly" => Ok(AlignContentStyle::Evenly),
"stretch" => Ok(AlignContentStyle::Stretch),
other => Err(format!("invalid align_content style: {other}")),
}
}
fn parse_text_align_style(term: &Term) -> Result<TextAlignStyle, String> {
let Term::Atom(atom) = term else {
return Err(format!("invalid text_align style: {term}"));
};
match atom.name.as_str() {
"left" => Ok(TextAlignStyle::Left),
"center" => Ok(TextAlignStyle::Center),
"right" => Ok(TextAlignStyle::Right),
other => Err(format!("invalid text_align style: {other}")),
}
}
fn parse_white_space_style(term: &Term) -> Result<WhiteSpaceStyle, String> {
let Term::Atom(atom) = term else {
return Err(format!("invalid white_space style: {term}"));
};
match atom.name.as_str() {
"normal" => Ok(WhiteSpaceStyle::Normal),
"nowrap" => Ok(WhiteSpaceStyle::NoWrap),
other => Err(format!("invalid white_space style: {other}")),
}
}
fn parse_text_overflow_style(term: &Term) -> Result<TextOverflowStyle, String> {
let Term::Atom(atom) = term else {
return Err(format!("invalid text_overflow style: {term}"));
};
match atom.name.as_str() {
"ellipsis" => Ok(TextOverflowStyle::Ellipsis),
"truncate" => Ok(TextOverflowStyle::Truncate),
other => Err(format!("invalid text_overflow style: {other}")),
}
}
fn parse_font_size_style(term: &Term) -> Result<FontSizeStyle, String> {
let Term::Atom(atom) = term else {
return Err(format!("invalid font_size style: {term}"));
};
match atom.name.as_str() {
"xs" => Ok(FontSizeStyle::Xs),
"sm" => Ok(FontSizeStyle::Sm),
"base" => Ok(FontSizeStyle::Base),
"lg" => Ok(FontSizeStyle::Lg),
"xl" => Ok(FontSizeStyle::Xl),
"2xl" => Ok(FontSizeStyle::TwoXl),
"3xl" => Ok(FontSizeStyle::ThreeXl),
other => Err(format!("invalid font_size style: {other}")),
}
}
fn parse_line_height_style(term: &Term) -> Result<LineHeightStyle, String> {
let Term::Atom(atom) = term else {
return Err(format!("invalid line_height style: {term}"));
};
match atom.name.as_str() {
"none" => Ok(LineHeightStyle::None),
"tight" => Ok(LineHeightStyle::Tight),
"snug" => Ok(LineHeightStyle::Snug),
"normal" => Ok(LineHeightStyle::Normal),
"relaxed" => Ok(LineHeightStyle::Relaxed),
"loose" => Ok(LineHeightStyle::Loose),
other => Err(format!("invalid line_height style: {other}")),
}
}
fn parse_font_weight_style(term: &Term) -> Result<FontWeightStyle, String> {
let Term::Atom(atom) = term else {
return Err(format!("invalid font_weight style: {term}"));
};
match atom.name.as_str() {
"thin" => Ok(FontWeightStyle::Thin),
"extralight" => Ok(FontWeightStyle::Extralight),
"light" => Ok(FontWeightStyle::Light),
"normal" => Ok(FontWeightStyle::Normal),
"medium" => Ok(FontWeightStyle::Medium),
"semibold" => Ok(FontWeightStyle::Semibold),
"bold" => Ok(FontWeightStyle::Bold),
"extrabold" => Ok(FontWeightStyle::Extrabold),
"black" => Ok(FontWeightStyle::Black),
other => Err(format!("invalid font_weight style: {other}")),
}
}
fn parse_font_weight_value(term: &Term) -> Result<f32, String> {
let value = parse_f32(term)?;
if value.is_finite() && (100.0..=900.0).contains(&value) {
Ok(value)
} else {
Err(format!("invalid font_weight_value: {value}"))
}
}
fn parse_font_style_value(term: &Term) -> Result<FontStyleValue, String> {
let Term::Atom(atom) = term else {
return Err(format!("invalid font_style: {term}"));
};
match atom.name.as_str() {
"italic" => Ok(FontStyleValue::Italic),
"normal" => Ok(FontStyleValue::Normal),
other => Err(format!("invalid font_style: {other}")),
}
}
fn parse_non_empty_style_string(term: &Term, key: &str) -> Result<String, String> {
let value = term_to_string(term)?;
if value.is_empty() {
Err(format!("invalid {key} string: expected non-empty string"))
} else {
Ok(value)
}
}
fn parse_non_empty_style_string_list(term: &Term, key: &str) -> Result<Vec<String>, String> {
let values = get_list(term)?
.iter()
.map(|term| parse_non_empty_style_string(term, key))
.collect::<Result<Vec<_>, _>>()?;
if values.is_empty() {
Err(format!(
"invalid {key} string list: expected at least one string"
))
} else {
Ok(values)
}
}
fn parse_font_features(term: &Term) -> Result<Vec<(String, u32)>, String> {
let values = get_list(term)?
.iter()
.map(parse_font_feature)
.collect::<Result<Vec<_>, _>>()?;
if values.is_empty() {
Err("invalid font_features: expected at least one feature".into())
} else {
Ok(values)
}
}
fn parse_font_feature(term: &Term) -> Result<(String, u32), String> {
let Term::Tuple(Tuple { elements }) = term else {
return Err(format!("invalid font feature entry: {term}"));
};
let [tag_term, value_term] = elements.as_slice() else {
return Err(format!("invalid font feature entry arity: {term}"));
};
let tag = term_to_string(tag_term)?;
if !valid_font_feature_tag(&tag) {
return Err(format!("invalid font feature tag: {tag}"));
}
Ok((tag, parse_non_negative_u32(value_term, "font_features")?))
}
fn valid_font_feature_tag(tag: &str) -> bool {
tag.len() == 4
&& tag
.chars()
.all(|character| character.is_ascii_alphanumeric())
}
fn parse_non_negative_u32(term: &Term, key: &str) -> Result<u32, String> {
match term {
Term::FixInteger(value) => u32::try_from(value.value)
.map_err(|_| format!("expected non-negative u32 style {key}, got {term}")),
Term::BigInteger(value) => value
.value
.clone()
.try_into()
.map_err(|_| format!("expected non-negative u32 style {key}, got {term}")),
other => Err(format!(
"expected non-negative u32 style {key}, got {other}"
)),
}
}
fn parse_text_decoration_style(term: &Term) -> Result<TextDecorationStyle, String> {
let Term::Atom(atom) = term else {
return Err(format!("invalid text_decoration style: {term}"));
};
match atom.name.as_str() {
"underline" => Ok(TextDecorationStyle::Underline),
"line_through" => Ok(TextDecorationStyle::LineThrough),
"none" => Ok(TextDecorationStyle::None),
other => Err(format!("invalid text_decoration style: {other}")),
}
}
fn parse_text_decoration_line_style(term: &Term) -> Result<TextDecorationLineStyle, String> {
let Term::Atom(atom) = term else {
return Err(format!("invalid text_decoration_style: {term}"));
};
match atom.name.as_str() {
"solid" => Ok(TextDecorationLineStyle::Solid),
"wavy" => Ok(TextDecorationLineStyle::Wavy),
other => Err(format!("invalid text_decoration_style: {other}")),
}
}
fn parse_mouse_cursor_style(term: &Term) -> Result<MouseCursorStyle, String> {
let Term::Atom(atom) = term else {
return Err(format!("invalid cursor style: {term}"));
};
match atom.name.as_str() {
"default" => Ok(MouseCursorStyle::Default),
"pointer" => Ok(MouseCursorStyle::Pointer),
"text" => Ok(MouseCursorStyle::Text),
"move" => Ok(MouseCursorStyle::Move),
"not_allowed" => Ok(MouseCursorStyle::NotAllowed),
"context_menu" => Ok(MouseCursorStyle::ContextMenu),
"crosshair" => Ok(MouseCursorStyle::Crosshair),
"vertical_text" => Ok(MouseCursorStyle::VerticalText),
"alias" => Ok(MouseCursorStyle::Alias),
"copy" => Ok(MouseCursorStyle::Copy),
"no_drop" => Ok(MouseCursorStyle::NoDrop),
"grab" => Ok(MouseCursorStyle::Grab),
"grabbing" => Ok(MouseCursorStyle::Grabbing),
"ew_resize" => Ok(MouseCursorStyle::EwResize),
"ns_resize" => Ok(MouseCursorStyle::NsResize),
"nesw_resize" => Ok(MouseCursorStyle::NeswResize),
"nwse_resize" => Ok(MouseCursorStyle::NwseResize),
"col_resize" => Ok(MouseCursorStyle::ColResize),
"row_resize" => Ok(MouseCursorStyle::RowResize),
"n_resize" => Ok(MouseCursorStyle::NResize),
"e_resize" => Ok(MouseCursorStyle::EResize),
"s_resize" => Ok(MouseCursorStyle::SResize),
"w_resize" => Ok(MouseCursorStyle::WResize),
"none" => Ok(MouseCursorStyle::None),
other => Err(format!("invalid cursor style: {other}")),
}
}
fn parse_style_length(
term: &Term,
key: &str,
allow_auto: bool,
allow_negative: bool,
) -> Result<StyleLength, String> {
match term {
Term::Atom(atom) if atom.name == "auto" && allow_auto => Ok(StyleLength::Auto),
Term::Atom(atom) if atom.name == "auto" => Err(format!("{key} length does not allow auto")),
Term::Tuple(Tuple { elements }) if elements.len() == 2 => {
let unit = match &elements[0] {
Term::Atom(atom) => atom.name.as_str(),
other => return Err(format!("expected {key} length unit atom, got {other}")),
};
let value = parse_bounded_style_f32(&elements[1], key, allow_negative)?;
match unit {
"px" => Ok(StyleLength::Px(value)),
"rem" => Ok(StyleLength::Rem(value)),
"fraction" => Ok(StyleLength::Fraction(value)),
other => Err(format!("invalid {key} length unit: {other}")),
}
}
other => Err(format!("expected {key} length tuple, got {other}")),
}
}
fn parse_absolute_style_length(term: &Term, key: &str) -> Result<StyleLength, String> {
let Term::Tuple(Tuple { elements }) = term else {
return Err(format!("expected {key} length tuple, got {term}"));
};
if elements.len() != 2 {
return Err(format!(
"expected {key} length tuple with 2 elements, got {term}"
));
}
let unit = match &elements[0] {
Term::Atom(atom) => atom.name.as_str(),
other => return Err(format!("expected {key} length unit atom, got {other}")),
};
let value = parse_non_negative_style_f32(&elements[1], key)?;
match unit {
"px" => Ok(StyleLength::Px(value)),
"rem" => Ok(StyleLength::Rem(value)),
other => Err(format!("invalid {key} length unit: {other}")),
}
}
fn parse_unit_style_f32(term: &Term, key: &str) -> Result<f32, String> {
let value = parse_f32(term)?;
if value.is_finite() && (0.0..=1.0).contains(&value) {
Ok(value)
} else {
Err(format!("invalid unit numeric style {key}: {value}"))
}
}
fn parse_non_negative_style_f32(term: &Term, key: &str) -> Result<f32, String> {
parse_bounded_style_f32(term, key, false)
}
fn parse_finite_style_f32(term: &Term, key: &str) -> Result<f32, String> {
parse_bounded_style_f32(term, key, true)
}
fn parse_positive_style_f32(term: &Term, key: &str) -> Result<f32, String> {
let value = parse_non_negative_style_f32(term, key)?;
if value > 0.0 {
Ok(value)
} else {
Err(format!("invalid positive numeric style {key}: {value}"))
}
}
fn parse_bounded_style_f32(term: &Term, key: &str, allow_negative: bool) -> Result<f32, String> {
let value = parse_f32(term)?;
if value.is_finite() && (allow_negative || value >= 0.0) {
Ok(value)
} else if allow_negative {
Err(format!("invalid numeric style {key}: {value}"))
} else {
Err(format!("invalid non-negative numeric style {key}: {value}"))
}
}
fn parse_grid_u16(term: &Term) -> Result<u16, String> {
parse_positive_u16(term, "grid")
}
fn parse_grid_span_style(
term: &Term,
key: &str,
span: fn(u16) -> StyleOp,
full: StyleOp,
) -> Result<StyleOp, String> {
match term {
Term::Atom(atom) if atom.name == "full" => Ok(full),
term => parse_positive_u16(term, key).map(span),
}
}
fn parse_grid_line_style(
term: &Term,
key: &str,
line: fn(i16) -> StyleOp,
auto: StyleOp,
) -> Result<StyleOp, String> {
match term {
Term::Atom(atom) if atom.name == "auto" => Ok(auto),
Term::FixInteger(value) => i16::try_from(value.value)
.map(line)
.map_err(|_| format!("invalid {key} grid line; expected -32768..32767, got {term}")),
Term::BigInteger(value) => value
.value
.clone()
.try_into()
.ok()
.and_then(|value: i64| i16::try_from(value).ok())
.map(line)
.ok_or_else(|| format!("invalid {key} grid line; expected -32768..32767, got {term}")),
other => Err(format!(
"invalid {key} grid line; expected integer or auto, got {other}"
)),
}
}
fn parse_positive_u16(term: &Term, key: &str) -> Result<u16, String> {
let value = match term {
Term::FixInteger(value) => value.value,
Term::BigInteger(value) => value
.value
.clone()
.try_into()
.map_err(|_| format!("invalid {key} integer; expected 1..65535, got {term}"))?,
other => {
return Err(format!(
"invalid {key} integer; expected integer, got {other}"
));
}
};
u16::try_from(value)
.ok()
.filter(|value| *value >= 1)
.ok_or_else(|| format!("invalid {key} integer; expected 1..65535, got {term}"))
}
fn parse_f32(term: &Term) -> Result<f32, String> {
let value = match term {
Term::FixInteger(value) => value.value as f32,
Term::BigInteger(value) => {
let value: i64 = value
.value
.clone()
.try_into()
.map_err(|_| format!("expected bounded integer numeric value, got {term}"))?;
value as f32
}
Term::Float(value) => value.value as f32,
other => return Err(format!("expected numeric value, got {other}")),
};
if value.is_finite() {
Ok(value)
} else {
Err(format!("expected finite numeric value, got {term}"))
}
}
fn parse_color_token(token: &str) -> Result<ColorToken, String> {
match token {
"red" => Ok(ColorToken::Red),
"green" => Ok(ColorToken::Green),
"blue" => Ok(ColorToken::Blue),
"yellow" => Ok(ColorToken::Yellow),
"black" => Ok(ColorToken::Black),
"white" => Ok(ColorToken::White),
"gray" => Ok(ColorToken::Gray),
other => Err(format!("unsupported color token: {other}")),
}
}
fn get_click_event(map: &HashMap<Term, Term>) -> Result<Option<String>, String> {
get_optional_event(map, "click")
}
fn get_close_event(map: &HashMap<Term, Term>) -> Result<Option<String>, String> {
get_optional_event(map, "close")
}
fn get_hover_event(map: &HashMap<Term, Term>) -> Result<Option<String>, String> {
get_optional_event(map, "hover")
}
fn get_focus_event(map: &HashMap<Term, Term>) -> Result<Option<String>, String> {
get_optional_event(map, "focus")
}
fn get_blur_event(map: &HashMap<Term, Term>) -> Result<Option<String>, String> {
get_optional_event(map, "blur")
}
fn get_change_event(map: &HashMap<Term, Term>) -> Result<Option<String>, String> {
get_optional_event(map, "change")
}
fn get_key_down_event(map: &HashMap<Term, Term>) -> Result<Option<String>, String> {
get_optional_event(map, "key_down")
}
fn get_key_up_event(map: &HashMap<Term, Term>) -> Result<Option<String>, String> {
get_optional_event(map, "key_up")
}
fn get_context_menu_event(map: &HashMap<Term, Term>) -> Result<Option<String>, String> {
get_optional_event(map, "context_menu")
}
fn get_drag_start_event(map: &HashMap<Term, Term>) -> Result<Option<String>, String> {
get_optional_event(map, "drag_start")
}
fn get_drag_move_event(map: &HashMap<Term, Term>) -> Result<Option<String>, String> {
get_optional_event(map, "drag_move")
}
fn get_drop_event(map: &HashMap<Term, Term>) -> Result<Option<String>, String> {
get_optional_event(map, "drop")
}
fn get_mouse_down_event(map: &HashMap<Term, Term>) -> Result<Option<String>, String> {
get_optional_event(map, "mouse_down")
}
fn get_mouse_up_event(map: &HashMap<Term, Term>) -> Result<Option<String>, String> {
get_optional_event(map, "mouse_up")
}
fn get_mouse_move_event(map: &HashMap<Term, Term>) -> Result<Option<String>, String> {
get_optional_event(map, "mouse_move")
}
fn get_scroll_wheel_event(map: &HashMap<Term, Term>) -> Result<Option<String>, String> {
get_optional_event(map, "scroll_wheel")
}
fn get_optional_event(map: &HashMap<Term, Term>, key: &str) -> Result<Option<String>, String> {
let Some(events_term) = get_field(map, "events") else {
return Ok(None);
};
let events = expect_map(events_term)?;
match get_field(events, key) {
Some(term) => term_to_string(term).map(Some),
None => Ok(None),
}
}
fn get_list(term: &Term) -> Result<&Vec<Term>, String> {
etf_decode::expect_list(term)
}
fn term_to_string(term: &Term) -> Result<String, String> {
etf_decode::term_to_binary_string(term)
}
fn term_to_str(term: &Term) -> Result<&str, String> {
etf_decode::term_to_binary_str(term)
}
#[cfg(test)]
#[path = "ir_tests.rs"]
mod tests;