///|
fn[Input : Eq] sidebar_build(
context : UiContext,
id : String,
input : @minimoon.Val[Input],
value : @minimoon.Val[SidebarState],
change : @minimoon.Emit[SidebarState],
render : (SidebarScope, Input) -> @minimoon.Node,
style : String,
) -> @minimoon.Val[@minimoon.Node] {
let initial = SidebarTransient::{
token: 0,
drag: None,
suppress_tap: false,
tooltip: None,
}
let (transient, send) = @minimoon.create_state_with_input(
input=value,
init=(_, _) => @minimoon.no_cmd(initial),
update=(transient_state, current, message : SidebarMsg, emit) => {
match message {
ToggleCollapsed =>
(
{ ..initial, token: transient_state.token + 1, },
change({ ..current, collapsed: !current.collapsed, }),
)
ToggleMobile =>
(
{ ..initial, token: transient_state.token + 1, },
change({ ..current, mobile_open: !current.mobile_open, }),
)
SetCollapsed(collapsed) =>
(
{ ..initial, token: transient_state.token + 1, },
if current.collapsed == collapsed {
@minimoon.none
} else {
change({ ..current, collapsed, })
},
)
SetMobile(mobile_open) =>
(
{ ..initial, token: transient_state.token + 1, },
if current.mobile_open == mobile_open {
@minimoon.none
} else {
change({ ..current, mobile_open, })
},
)
SetWidth(width) =>
(
transient_state,
if width == current.width {
@minimoon.none
} else {
change({ ..current, width, })
},
)
RailTap =>
if transient_state.suppress_tap {
@minimoon.no_cmd({ ..transient_state, suppress_tap: false, })
} else {
(
{ ..initial, token: transient_state.token + 1, },
change({ ..current, collapsed: !current.collapsed, }),
)
}
ClearTransient =>
@minimoon.no_cmd({ ..initial, token: transient_state.token + 1, })
Hide =>
(
{ ..initial, token: transient_state.token + 1, },
transient_state.drag
.map(drag => change({ ..current, width: drag.initial, }))
.unwrap_or(@minimoon.none),
)
RailStart(side, minimum, maximum, detail) => {
guard !current.collapsed &&
!current.mobile_open &&
transient_state.drag is None &&
detail.changed_touches.length() == 1 else {
return @minimoon.no_cmd(transient_state)
}
let point = detail.changed_touches[0]
@minimoon.no_cmd({
..transient_state,
suppress_tap: false,
drag: Some({
pointer: point.identifier,
start: point.client_x,
initial: current.width,
minimum,
maximum,
side,
moved: false,
}),
})
}
RailMove(detail) | RailEnd(detail) => {
guard transient_state.drag is Some(drag) else {
return @minimoon.no_cmd(transient_state)
}
let ended = message is RailEnd(_)
guard slider_point(
if ended {
detail.changed_touches
} else {
detail.touches
},
drag.pointer,
)
is Some(point) else {
return @minimoon.no_cmd(transient_state)
}
let delta = (point.client_x - drag.start) *
(if drag.side == SidebarLeft { 1.0 } else { -1.0 })
let width = (drag.initial + delta).clamp(
min=drag.minimum,
max=drag.maximum,
)
let moved = drag.moved || delta.abs() >= 4.0
(
{
..transient_state,
drag: if ended {
None
} else {
Some({ ..drag, moved, })
},
suppress_tap: ended && moved,
},
if moved && width != current.width {
change({ ..current, width, })
} else {
@minimoon.none
},
)
}
RailCancel(detail) => {
guard transient_state.drag is Some(drag) &&
slider_point(detail.changed_touches, drag.pointer) is Some(_) else {
return @minimoon.no_cmd(transient_state)
}
(
{ ..transient_state, drag: None, suppress_tap: false, },
change({ ..current, width: drag.initial, }),
)
}
CloseTooltip =>
@minimoon.no_cmd({
..transient_state,
token: transient_state.token + 1,
tooltip: None,
})
ShowTooltip(tooltip) => {
let token = transient_state.token + 1
(
{ ..transient_state, token, tooltip: Some(tooltip), },
@minimoon.measure_nodes(
[tooltip.id, tooltip.id + "-tooltip", "mmui-viewport"],
emit.map(result => TooltipMeasured(token, result)),
),
)
}
TooltipMeasured(token, result) => {
guard transient_state.token == token &&
transient_state.tooltip is Some(tip) else {
return @minimoon.no_cmd(transient_state)
}
match result {
Ok([Some(anchor), Some(surface), Some(window)]) =>
@minimoon.no_cmd({
..transient_state,
tooltip: Some({
..tip,
bounds: Some(
sidebar_tooltip_bounds(tip, anchor, surface, window),
),
}),
})
_ => @minimoon.no_cmd({ ..transient_state, tooltip: None, })
}
}
}
},
subscriptions=(_, _, emit) => context.page.on_hide(emit(Hide)),
)
let watcher = value
.map(value => (value.collapsed, value.mobile_open))
.switch_by(
(_, _) => {
let (_, _) = @minimoon.create_state_with_init(
init=_ => ((), send(ClearTransient)),
update=(_, _ : Unit, _) => @minimoon.no_cmd(()),
)
@minimoon.Val::constant(@minimoon.nothing())
},
by=flags => flags.0.to_string() + flags.1.to_string(),
)
let view = @minimoon.Val::view4(value, transient, input, context.visible, (
state,
transient,
input,
visible,
) => {
guard state.width > 0.0 && state.width - state.width == 0.0 else {
abort("sidebar width must be finite and positive")
}
let scope = SidebarScope::{ id, state, transient, visible, send, }
ui_container(
"sidebar-provider",
id~,
style="display:flex;position:relative;min-width:0;min-height:0;" + style,
[render(scope, input)],
)
})
@minimoon.Val::view2(view, watcher, (view, watcher) => {
@minimoon.fragment([view, watcher])
})
}
///|
pub fn[Input : Eq] sidebar_provider_controlled_with_input(
context : UiContext,
id~ : String,
state~ : @minimoon.Val[SidebarState],
on_change~ : @minimoon.Emit[SidebarState],
input~ : @minimoon.Val[Input],
render~ : (SidebarScope, Input) -> @minimoon.Node,
style? : String = "",
) -> @minimoon.Val[@minimoon.Node] {
sidebar_build(context, id, input, state, on_change, render, style)
}
///|
pub fn sidebar_provider_controlled(
context : UiContext,
id~ : String,
state~ : @minimoon.Val[SidebarState],
on_change~ : @minimoon.Emit[SidebarState],
render~ : (SidebarScope) -> @minimoon.Node,
style? : String = "",
) -> @minimoon.Val[@minimoon.Node] {
sidebar_build(
context,
id,
@minimoon.Val::constant(()),
state,
on_change,
(scope, _) => render(scope),
style,
)
}
///|
pub fn[Input : Eq] sidebar_provider_with_input(
context : UiContext,
id~ : String,
input~ : @minimoon.Val[Input],
render~ : (SidebarScope, Input) -> @minimoon.Node,
default_collapsed? : Bool = false,
default_mobile_open? : Bool = false,
default_width? : Double = 256.0,
on_collapsed_change? : @minimoon.Emit[Bool],
on_mobile_open_change? : @minimoon.Emit[Bool],
on_width_change? : @minimoon.Emit[Double],
style? : String = "",
) -> @minimoon.Val[@minimoon.Node] {
let (state, emit) = @minimoon.create_state(
sidebar_state(
collapsed=default_collapsed,
mobile_open=default_mobile_open,
width=default_width,
),
update=(old, next : SidebarState, _) => {
let commands : Array[@minimoon.Cmd] = []
if old.collapsed != next.collapsed {
if on_collapsed_change is Some(send) {
commands.push(send(next.collapsed))
}
}
if old.mobile_open != next.mobile_open {
if on_mobile_open_change is Some(send) {
commands.push(send(next.mobile_open))
}
}
if old.width != next.width {
if on_width_change is Some(send) {
commands.push(send(next.width))
}
}
(next, @minimoon.batch(commands))
},
)
sidebar_build(context, id, input, state, emit, render, style)
}
///|
pub fn sidebar_provider(
context : UiContext,
id~ : String,
render~ : (SidebarScope) -> @minimoon.Node,
default_collapsed? : Bool = false,
default_mobile_open? : Bool = false,
default_width? : Double = 256.0,
on_collapsed_change? : @minimoon.Emit[Bool],
on_mobile_open_change? : @minimoon.Emit[Bool],
on_width_change? : @minimoon.Emit[Double],
style? : String = "",
) -> @minimoon.Val[@minimoon.Node] {
sidebar_provider_with_input(
context,
id~,
input=@minimoon.Val::constant(()),
render=(scope, _) => render(scope),
default_collapsed~,
default_mobile_open~,
default_width~,
on_collapsed_change?,
on_mobile_open_change?,
on_width_change?,
style~,
)
}