///|
/// Touch-native compound navigation adapted from RUI b1291945 (MIT).
struct NavigationMenuScope {
id : String
value : String?
viewport : Bool
bounds : @headless.Bounds?
change : @minimoon.Emit[String?]
toggle : @minimoon.Emit[String]
contents : Ref[@minimoon.Node?]
chrome : Ref[@minimoon.Node?]
keys : Set[String]
}
///|
struct NavigationMenuItemScope {
owner : NavigationMenuScope
value : String
disabled : Bool
}
///|
priv struct NavigationBounds {
value : String?
bounds : @headless.Bounds?
} derive(Eq)
///|
fn navigation_part_id(id : String, value : String, part : String) -> String {
let pieces : Array[String] = value
.iter()
.map(character => character.to_int().to_string())
.collect()
let encoded = pieces.join("_")
id + "-" + part + "-" + encoded
}
///|
pub fn navigation_menu_value(scope : NavigationMenuScope) -> String? {
scope.value
}
///|
pub fn navigation_menu_activate(
scope : NavigationMenuScope,
value : String,
) -> @minimoon.Cmd {
(scope.change)(Some(value))
}
///|
pub fn navigation_menu_close(scope : NavigationMenuScope) -> @minimoon.Cmd {
(scope.change)(None)
}
///|
pub fn navigation_menu_item_is_open(scope : NavigationMenuItemScope) -> Bool {
scope.owner.value == Some(scope.value) && !scope.disabled
}
///|
pub fn[C : @minimoon.IsChildren] navigation_menu_list(
scope : NavigationMenuScope,
children : C,
class? : String = "",
style? : String = "",
) -> @minimoon.Node {
ui_container(
"navigation-menu-list",
id=scope.id + "-list",
class~,
style="display:flex;flex-wrap:wrap;gap:4px;" + style,
children.to_nodes(),
)
}
///|
pub fn navigation_menu_item(
scope : NavigationMenuScope,
value~ : String,
render~ : (NavigationMenuItemScope) -> @minimoon.Node,
disabled? : Bool = false,
class? : String = "",
style? : String = "",
) -> @minimoon.Node {
guard value != "" && !scope.keys.contains(value) else {
abort("navigation items require unique nonempty values")
}
scope.keys.add(value)
ui_container(
"navigation-menu-item",
class~,
style="position:relative;" + style,
[render({ owner: scope, value, disabled, })],
)
}
///|
pub fn[C : @minimoon.IsChildren] navigation_menu_trigger(
scope : NavigationMenuItemScope,
children : C,
disabled? : Bool = false,
class? : String = "",
on_tap? : @minimoon.Cmd,
) -> @minimoon.Node {
let disabled = disabled || scope.disabled
let open = navigation_menu_item_is_open(scope)
@minimoon.button_children(
id=navigation_part_id(scope.owner.id, scope.value, "trigger"),
class="mmui-navigation-menu-trigger " + class,
disabled~,
event_key=scope.owner.id + "/trigger/" + scope.value,
on_tap=if disabled {
@minimoon.none
} else {
@minimoon.batch([
(scope.owner.toggle)(scope.value),
on_tap.unwrap_or(@minimoon.none),
])
},
semantics=@minimoon.semantics(
expanded=open,
controls=if scope.owner.viewport {
scope.owner.id + "-viewport"
} else {
navigation_part_id(scope.owner.id, scope.value, "content")
},
),
children,
)
}
///|
fn navigation_surface(
scope : NavigationMenuScope,
id : String,
content : @minimoon.Node,
) -> @minimoon.Node {
@minimoon.layer(id=scope.id + "-layer", [
@minimoon.tap_view(
on_tap=navigation_menu_close(scope),
event_key=scope.id + "/dismiss",
style="position:fixed;left:0;right:0;top:0;bottom:0;",
@minimoon.nothing(),
),
ui_container(
"navigation-menu-viewport",
id~,
class="mmui-surface",
style=surface_style(PopoverSurface, scope.bounds) +
"min-width:240px;max-height:70vh;overflow:auto;",
semantics=@minimoon.semantics(role=@minimoon.RegionRole),
[
content,
@minimoon.button(
on_tap=navigation_menu_close(scope),
event_key=scope.id + "/close",
"Close menu",
),
],
),
])
}
///|
pub fn[C : @minimoon.IsChildren] navigation_menu_content(
scope : NavigationMenuItemScope,
children : C,
class? : String = "",
style? : String = "",
) -> @minimoon.Node {
if !navigation_menu_item_is_open(scope) {
return @minimoon.nothing()
}
let content = ui_container(
"navigation-menu-content",
class~,
style~,
semantics=@minimoon.semantics(
role=@minimoon.RegionRole,
labelled_by=navigation_part_id(scope.owner.id, scope.value, "trigger"),
),
children.to_nodes(),
)
if scope.owner.viewport {
scope.owner.contents.val = Some(content)
@minimoon.nothing()
} else {
navigation_surface(
scope.owner,
navigation_part_id(scope.owner.id, scope.value, "content"),
content,
)
}
}
///|
/// Optional shared viewport chrome is collected only within the current render.
pub fn[C : @minimoon.IsChildren] navigation_menu_viewport(
scope : NavigationMenuScope,
children : C,
class? : String = "",
style? : String = "",
) -> @minimoon.Node {
guard scope.chrome.val is None else {
abort("navigation menu has only one shared viewport")
}
scope.chrome.val = Some(
ui_container(
"navigation-menu-viewport-chrome",
class~,
style~,
children.to_nodes(),
),
)
@minimoon.nothing()
}
///|
pub fn navigation_menu_indicator(
scope : NavigationMenuItemScope,
class? : String = "",
) -> @minimoon.Node {
if !navigation_menu_item_is_open(scope) {
return @minimoon.nothing()
}
ui_container(
"navigation-menu-indicator",
class~,
style="position:absolute;bottom:-4px;left:50%;width:8px;height:8px;transform:rotate(45deg);background:var(--mmui-border);",
semantics=@minimoon.semantics(hidden=true),
[],
)
}
///|
pub fn[C : @minimoon.IsChildren] navigation_menu_link(
target~ : @minimoon.Route,
children : C,
active? : Bool = false,
scope? : NavigationMenuScope,
class? : String = "",
style? : String = "",
on_tap? : @minimoon.Cmd,
) -> @minimoon.Node {
@minimoon.tap_view(
on_tap=@minimoon.batch([
scope.map(navigation_menu_close).unwrap_or(@minimoon.none),
on_tap.unwrap_or(@minimoon.none),
]),
@minimoon.navigator(
target~,
class="mmui-navigation-menu-link " +
(if active { "mmui-active " } else { "" }) +
class,
style~,
children,
),
)
}