///|
struct MenuItem {
key : String
label : String
disabled : Bool
kind : MenuItemKind
presentation : MenuPresentation?
stamp : @minimoon.Node
}
///|
priv enum MenuItemKind {
Action(@minimoon.Cmd)
Check(Bool, @minimoon.Emit[Bool])
Radio(Bool, @minimoon.Cmd)
Branch(Array[MenuItem])
Group(Array[MenuItem])
Heading
Divider
}
///|
pub impl Eq for MenuItem with fn equal(a, b) {
a.stamp == b.stamp
}
///|
pub fn menu_item(
key~ : String,
label~ : String,
on_select~ : @minimoon.Cmd,
disabled? : Bool = false,
) -> MenuItem {
{
key,
label,
disabled,
kind: Action(on_select),
presentation: None,
stamp: @minimoon.nothing(),
}
}
///|
pub fn menu_checkbox_item(
key~ : String,
label~ : String,
checked~ : Bool,
on_change~ : @minimoon.Emit[Bool],
disabled? : Bool = false,
) -> MenuItem {
{
key,
label,
disabled,
kind: Check(checked, on_change),
presentation: None,
stamp: @minimoon.nothing(),
}
}
///|
pub fn menu_radio_item(
key~ : String,
label~ : String,
selected~ : Bool,
on_select~ : @minimoon.Cmd,
disabled? : Bool = false,
) -> MenuItem {
{
key,
label,
disabled,
kind: Radio(selected, on_select),
presentation: None,
stamp: @minimoon.nothing(),
}
}
///|
pub fn menu_submenu(
key~ : String,
label~ : String,
items~ : Array[MenuItem],
disabled? : Bool = false,
) -> MenuItem {
{
key,
label,
disabled,
kind: Branch(items),
presentation: None,
stamp: @minimoon.nothing(),
}
}
///|
pub fn menu_label(key~ : String, label~ : String) -> MenuItem {
{
key,
label,
disabled: false,
kind: Heading,
presentation: None,
stamp: @minimoon.nothing(),
}
}
///|
pub fn menu_separator(key~ : String) -> MenuItem {
{
key,
label: "",
disabled: false,
kind: Divider,
presentation: None,
stamp: @minimoon.nothing(),
}
}
///|
fn render_menu(
id : String,
items : Array[MenuItem],
expanded : Array[String],
select : @minimoon.Emit[String],
) -> @minimoon.Node {
let keys : Set[String] = Set([])
for item in items {
guard item.key != "" && !keys.contains(item.key) else {
abort("menu keys must be unique and nonempty")
}
keys.add(item.key)
}
ui_container(
"menu",
semantics=@minimoon.semantics(role=@minimoon.MenuRole),
items.map(item => {
let key = id + "/" + item.key
let presentation = item.presentation.unwrap_or(
default_menu_presentation(item.label),
)
match item.kind {
Heading => ui_container("menu-label", [@minimoon.text(item.label)])
Divider =>
ui_container(
"menu-separator",
semantics=@minimoon.semantics(role=@minimoon.SeparatorRole),
[],
)
Group(children) =>
ui_container("menu-group", [
if item.label == "" {
@minimoon.nothing()
} else {
ui_container("menu-label", [@minimoon.text(item.label)])
},
render_menu(key, children, expanded, select),
])
Branch(children) => {
let open = expanded.contains(key)
ui_container("menu-submenu", [
@minimoon.button_children(
disabled=item.disabled,
event_key=key,
on_tap=if item.disabled { @minimoon.none } else { select(key) },
semantics=@minimoon.semantics(
role=@minimoon.MenuItemRole,
expanded=open,
),
menu_item_content(presentation, submenu=true),
),
if open {
render_menu(key, children, expanded, select)
} else {
@minimoon.nothing()
},
])
}
_ => {
let (role, checked) = match item.kind {
Check(checked, _) => (@minimoon.MenuItemCheckboxRole, Some(checked))
Radio(selected, _) => (@minimoon.MenuItemRadioRole, Some(selected))
_ => (@minimoon.MenuItemRole, None)
}
@minimoon.button_children(
class="mmui-menu-item" +
(if presentation.destructive { " mmui-destructive" } else { "" }),
style=if presentation.inset { "padding-left:32px;" } else { "" },
disabled=item.disabled,
event_key=key,
on_tap=if item.disabled { @minimoon.none } else { select(key) },
semantics=@minimoon.semantics(role~, checked?),
menu_item_content(
presentation,
checked?,
radio=item.kind is Radio(_, _),
),
)
}
}
}),
)
}
///|
pub fn dropdown_menu_controlled(
context : UiContext,
id~ : String,
trigger_label~ : String,
items~ : @minimoon.Val[Array[MenuItem]],
open~ : @minimoon.Val[Bool],
on_change~ : @minimoon.Emit[Bool],
context_menu? : Bool = false,
) -> @minimoon.Val[@minimoon.Node] {
let (expanded, emit) = menu_dispatch(id, items, on_change(false), open~)
let content = @minimoon.Val::view2(items, expanded, (items, expanded) => {
render_menu(id, items, expanded, emit)
})
overlay_controlled(
context,
id~,
title=trigger_label,
trigger_label~,
content~,
open~,
on_change~,
kind=if context_menu { ContextSurface } else { PopoverSurface },
)
}
///|
pub fn dropdown_menu(
context : UiContext,
id~ : String,
trigger_label~ : String,
items~ : @minimoon.Val[Array[MenuItem]],
) -> @minimoon.Val[@minimoon.Node] {
let (open, emit) = @minimoon.create_variable(false)
dropdown_menu_controlled(
context,
id~,
trigger_label~,
items~,
open~,
on_change=emit.map(value => _ => value),
)
}
///|
pub fn context_menu(
context : UiContext,
id~ : String,
trigger_label~ : String,
items~ : @minimoon.Val[Array[MenuItem]],
) -> @minimoon.Val[@minimoon.Node] {
let (open, emit) = @minimoon.create_variable(false)
dropdown_menu_controlled(
context,
id~,
trigger_label~,
items~,
open~,
on_change=emit.map(value => _ => value),
context_menu=true,
)
}
///|
pub(all) struct MenuGroup {
key : String
label : String
items : @minimoon.Val[Array[MenuItem]]
}
///|
pub fn menubar(
context : UiContext,
id~ : String,
groups~ : Array[MenuGroup],
) -> @minimoon.Val[@minimoon.Node] {
let initial : String? = None
let (selected, emit) = @minimoon.create_variable(initial)
stack(
groups.map(group => {
dropdown_menu_controlled(
context,
id=id + "-" + group.key,
trigger_label=group.label,
items=group.items,
open=selected.map(value => value == Some(group.key)),
on_change=emit.map(open => {
_ => if open { Some(group.key) } else { None }
}),
)
}),
).map(content => ui_container("menubar", [content]))
}