///|
let next_node_id : Ref[Int] = Ref(1)
///|
let next_binding_id : Ref[Int] = Ref(1)
///|
struct Node {
id : Int
raw : @ui.Node
bindings : Array[NodeBinding]
}
///|
priv enum NodeBinding {
DirectBinding(@renderer.PageEvent[Cmd])
NestedBindings(Array[NodeBinding])
}
///|
pub impl Eq for Node with fn equal(left, right) {
left.id == right.id
}
///|
struct KeyedNode {
key : String
node : Node
}
///|
pub trait IsChildren {
fn to_nodes(Self) -> Array[Node]
}
///|
pub impl IsChildren for Array[Node] with fn to_nodes(self) {
self
}
///|
pub impl IsChildren for Node with fn to_nodes(self) {
[self]
}
///|
pub impl IsChildren for String with fn to_nodes(self) {
[text(self)]
}
///|
pub(all) enum SemanticRole {
GenericRole
ButtonRole
GroupRole
HeadingRole
RegionRole
NavigationRole
TabRole
TabListRole
TabPanelRole
DialogRole
MenuRole
MenuItemRole
MenuItemCheckboxRole
MenuItemRadioRole
SeparatorRole
AlertRole
AlertDialogRole
CheckboxRole
RadioRole
RadioGroupRole
SwitchRole
TextboxRole
ComboboxRole
ListboxRole
OptionRole
SliderRole
ProgressbarRole
StatusRole
TooltipRole
TableRole
RowRole
CellRole
ColumnHeaderRole
} derive(Eq, Debug)
///|
struct Semantics {
role : String
label : String
expanded : Bool?
selected : Bool?
disabled : Bool?
hidden : Bool?
checked : Bool?
modal : Bool?
controls : String
labelled_by : String
described_by : String
orientation : String
has_popup : String
required : Bool?
invalid : Bool?
busy : Bool?
live : String
value_min : Int?
value_max : Int?
value_now : Int?
value_text : String
}
///|
fn SemanticRole::host_name(self : SemanticRole) -> String {
match self {
GenericRole => "generic"
ButtonRole => "button"
GroupRole => "group"
HeadingRole => "heading"
RegionRole => "region"
NavigationRole => "navigation"
TabRole => "tab"
TabListRole => "tablist"
TabPanelRole => "tabpanel"
DialogRole => "dialog"
MenuRole => "menu"
MenuItemRole => "menuitem"
MenuItemCheckboxRole => "menuitemcheckbox"
MenuItemRadioRole => "menuitemradio"
SeparatorRole => "separator"
AlertRole => "alert"
AlertDialogRole => "alertdialog"
CheckboxRole => "checkbox"
RadioRole => "radio"
RadioGroupRole => "radiogroup"
SwitchRole => "switch"
TextboxRole => "textbox"
ComboboxRole => "combobox"
ListboxRole => "listbox"
OptionRole => "option"
SliderRole => "slider"
ProgressbarRole => "progressbar"
StatusRole => "status"
TooltipRole => "tooltip"
TableRole => "table"
RowRole => "row"
CellRole => "cell"
ColumnHeaderRole => "columnheader"
}
}
///|
pub fn semantics(
role? : SemanticRole,
label? : String = "",
expanded? : Bool,
selected? : Bool,
disabled? : Bool,
hidden? : Bool,
checked? : Bool,
modal? : Bool,
controls? : String = "",
labelled_by? : String = "",
described_by? : String = "",
orientation? : String = "",
has_popup? : String = "",
required? : Bool,
invalid? : Bool,
busy? : Bool,
live? : String = "",
value_min? : Int,
value_max? : Int,
value_now? : Int,
value_text? : String = "",
) -> Semantics {
{
role: role.map(value => value.host_name()).unwrap_or(""),
label,
expanded,
selected,
disabled,
hidden,
checked,
modal,
controls,
labelled_by,
described_by,
orientation,
has_popup,
required,
invalid,
busy,
live,
value_min,
value_max,
value_now,
value_text,
}
}
///|
fn raw_optional_bool_attr(
attrs : Array[@ui.Attr],
name : String,
value : Bool?,
) -> Unit {
match value {
Some(value) => attrs.push(@ui.attr(name, value.to_string()))
None => ()
}
}
///|
fn raw_semantics_attrs(attrs : Array[@ui.Attr], value : Semantics?) -> Unit {
match value {
Some(value) => {
raw_string_attr(attrs, "aria-role", value.role)
raw_string_attr(attrs, "aria-label", value.label)
raw_optional_bool_attr(attrs, "aria-expanded", value.expanded)
raw_optional_bool_attr(attrs, "aria-selected", value.selected)
raw_optional_bool_attr(attrs, "aria-disabled", value.disabled)
raw_optional_bool_attr(attrs, "aria-hidden", value.hidden)
raw_optional_bool_attr(attrs, "aria-checked", value.checked)
raw_optional_bool_attr(attrs, "aria-modal", value.modal)
raw_string_attr(attrs, "aria-controls", value.controls)
raw_string_attr(attrs, "aria-labelledby", value.labelled_by)
raw_string_attr(attrs, "aria-describedby", value.described_by)
raw_string_attr(attrs, "aria-orientation", value.orientation)
raw_string_attr(attrs, "aria-haspopup", value.has_popup)
raw_optional_bool_attr(attrs, "aria-required", value.required)
raw_optional_bool_attr(attrs, "aria-invalid", value.invalid)
raw_optional_bool_attr(attrs, "aria-busy", value.busy)
raw_string_attr(attrs, "aria-live", value.live)
raw_string_attr(attrs, "aria-valuetext", value.value_text)
for
pair in [
("aria-valuemin", value.value_min),
("aria-valuemax", value.value_max),
("aria-valuenow", value.value_now),
] {
if pair.1 is Some(number) {
raw_int_attr(attrs, pair.0, number)
}
}
}
None => ()
}
}
///|
fn raw_element_attrs(
style : String,
data_section : String,
semantics? : Semantics,
) -> Array[@ui.Attr] {
let attrs : Array[@ui.Attr] = []
if style != "" {
attrs.push(@ui.style_attr(style))
}
if data_section != "" {
attrs.push(@ui.attr("data-minimoon-section", data_section))
}
raw_semantics_attrs(attrs, semantics)
attrs
}
///|
fn raw_bool_attr(attrs : Array[@ui.Attr], name : String, value : Bool) -> Unit {
attrs.push(@ui.attr(name, value.to_string()))
}
///|
fn raw_int_attr(attrs : Array[@ui.Attr], name : String, value : Int) -> Unit {
attrs.push(@ui.attr(name, value.to_string()))
}
///|
fn raw_string_attr(
attrs : Array[@ui.Attr],
name : String,
value : String,
) -> Unit {
if value != "" {
attrs.push(@ui.attr(name, value))
}
}
///|
fn raw_event_attr(
attrs : Array[@ui.Attr],
name : String,
expected : String,
event : @renderer.PageEvent[Cmd]?,
) -> Unit {
match event {
Some(event) => {
guard event.kind().host_name() == expected else {
abort(
"MiniApp " +
name +
" requires a " +
expected +
" event: " +
event.key(),
)
}
attrs.push(@ui.attr(name, event.key()))
}
None => ()
}
}
///|
fn wrap_node(raw : @ui.Node, bindings : Array[NodeBinding]) -> Node {
let id = next_node_id.val
next_node_id.val = id + 1
{ id, raw: @ui.retained(id, raw), bindings, }
}
///|
fn child_raw(children : Array[Node]) -> Array[@ui.Node] {
children.map(child => child.raw)
}
///|
fn child_bindings(children : Array[Node]) -> Array[NodeBinding] {
let output : Array[NodeBinding] = []
for child in children {
if child.bindings.length() > 0 {
output.push(NestedBindings(child.bindings))
}
}
output
}
///|
fn append_direct_binding(
bindings : Array[NodeBinding],
binding : @renderer.PageEvent[Cmd],
) -> Unit {
bindings.push(DirectBinding(binding))
}
///|
fn event_binding(
kind : @renderer.PageEventKind,
decode : (Json) -> Result[Cmd, DecodeError],
explicit_key? : String = "",
) -> @renderer.PageEvent[Cmd] {
let id = next_binding_id.val
next_binding_id.val = id + 1
let marker = "$minimoon:" + id.to_string()
let key = if explicit_key == "" {
marker
} else {
marker + "|" + explicit_key
}
@renderer.page_event(key~, kind~, decode=payload => {
match decode(payload) {
Ok(command) => Ok(command)
Err(error) => Err(@renderer.decode_error(error.message()))
}
})
}
///|
fn event_detail(payload : Json) -> Result[Map[String, Json], DecodeError] {
match payload {
Object(root) =>
match root.get("detail") {
Some(Object(detail)) => Ok(detail)
Some(_) => Err(decode_error("event detail must be an object"))
None => Err(decode_error("event detail is missing"))
}
_ => Err(decode_error("event payload must be an object"))
}
}
///|
fn detail_string(payload : Json) -> Result[String, DecodeError] {
match event_detail(payload) {
Ok(detail) =>
match detail.get("value") {
Some(String(value)) => Ok(value)
_ => Err(decode_error("event detail.value must be a string"))
}
Err(error) => Err(error)
}
}
///|
fn detail_bool(payload : Json) -> Result[Bool, DecodeError] {
match event_detail(payload) {
Ok(detail) =>
match detail.get("value") {
Some(True) => Ok(true)
Some(False) => Ok(false)
_ => Err(decode_error("event detail.value must be a boolean"))
}
Err(error) => Err(error)
}
}
///|
fn detail_strings(payload : Json) -> Result[Array[String], DecodeError] {
match event_detail(payload) {
Ok(detail) =>
match detail.get("value") {
Some(Array(values)) => {
let output : Array[String] = []
for value in values {
match value {
String(item) => output.push(item)
_ =>
return Err(
decode_error("event detail.value must contain strings"),
)
}
}
Ok(output)
}
_ => Err(decode_error("event detail.value must be an array"))
}
Err(error) => Err(error)
}
}
///|
fn event_int32(value : Double) -> Int? {
if value < 0.0 || value > 2147483647.0 {
return None
}
let integer = value.to_int()
if integer.to_double() == value {
Some(integer)
} else {
None
}
}
///|
fn decimal_index(value : String) -> Int? {
if value == "" {
return None
}
let result = Ref(0)
for character in value.iter() {
if character < '0' || character > '9' {
return None
}
let digit = character.to_int() - '0'.to_int()
if result.val > (2147483647 - digit) / 10 {
return None
}
result.val = result.val * 10 + digit
}
Some(result.val)
}
///|
fn detail_index(payload : Json) -> Result[Int, DecodeError] {
match event_detail(payload) {
Ok(detail) =>
match detail.get("value") {
Some(String(value)) =>
match decimal_index(value) {
Some(index) => Ok(index)
None => Err(decode_error("event detail.value must be an index"))
}
Some(Number(value, ..)) =>
match event_int32(value) {
Some(index) => Ok(index)
None => Err(decode_error("event detail.value must be an index"))
}
_ => Err(decode_error("event detail.value must be an index"))
}
Err(error) => Err(error)
}
}
///|
fn detail_current(payload : Json) -> Result[Int, DecodeError] {
match event_detail(payload) {
Ok(detail) =>
match detail.get("current") {
Some(Number(value, ..)) =>
match event_int32(value) {
Some(index) => Ok(index)
None => Err(decode_error("event detail.current must be an index"))
}
_ => Err(decode_error("event detail.current must be an index"))
}
Err(error) => Err(error)
}
}