///|
/// Touch-native adaptation of RUI drawer.mbt at b1291945 (MIT).
priv struct DrawerDrag {
pointer : Int
start : Double
position : Double
started_at : Double
timestamp : Double
size : Double?
ended : Bool
} derive(Eq)
///|
priv struct DrawerState {
token : Int
drag : DrawerDrag?
} derive(Eq)
///|
priv enum DrawerMsg {
Start(@minimoon.TouchDetail)
Move(@minimoon.TouchDetail)
End(@minimoon.TouchDetail)
Cancel
CancelTouch(@minimoon.TouchDetail)
Measured(Int, Result[Array[@minimoon.NodeRect?], @minimoon.HostError])
}
///|
fn drawer_axis(side : Side, point : @minimoon.TouchPoint) -> Double {
match side {
Left | Right => point.client_x
Top | Bottom => point.client_y
}
}
///|
fn drawer_distance(side : Side, drag : DrawerDrag) -> Double {
let sign = if side == Left || side == Top { -1.0 } else { 1.0 }
((drag.position - drag.start) * sign).max(0.0)
}
///|
fn drawer_dismisses(side : Side, drag : DrawerDrag) -> Bool {
guard drag.size is Some(size) else { return false }
let distance = drawer_distance(side, drag)
let elapsed = (drag.timestamp - drag.started_at).max(1.0)
distance >= (size * 0.25).max(48.0).min(160.0) ||
(distance >= 16.0 && distance / elapsed >= 0.5)
}
///|
fn drawer_trigger(
id : String,
label : String,
open : Bool,
on_change : @minimoon.Emit[Bool],
disabled : Bool,
) -> @minimoon.Node {
@minimoon.button(
id=id + "-trigger",
disabled~,
on_tap=if disabled { @minimoon.none } else { on_change(true) },
event_key=id + "/trigger",
semantics=@minimoon.semantics(
expanded=open,
controls=id + "-surface",
has_popup="dialog",
),
label,
)
}
///|
pub fn drawer_controlled(
context : UiContext,
id~ : String,
title~ : String,
trigger_label~ : String,
content~ : @minimoon.Val[@minimoon.Node],
open~ : @minimoon.Val[Bool],
on_change~ : @minimoon.Emit[Bool],
side? : Side = Bottom,
disabled? : Bool = false,
close_on_overlay? : Bool = true,
) -> @minimoon.Val[@minimoon.Node] {
open.switch_by(
(opened, _) => {
if !opened {
return @minimoon.Val::constant(
drawer_trigger(id, trigger_label, false, on_change, disabled),
)
}
let (state, emit) = @minimoon.create_state(
DrawerState::{ token: 0, drag: None, },
update=(state, msg : DrawerMsg, emit) => {
match msg {
CancelTouch(detail) =>
if state.drag is Some(drag) &&
slider_point(detail.changed_touches, drag.pointer) is Some(_) {
@minimoon.no_cmd({ token: state.token + 1, drag: None, })
} else {
@minimoon.no_cmd(state)
}
Cancel => @minimoon.no_cmd({ token: state.token + 1, drag: None, })
Start(detail) => {
guard !disabled &&
state.drag is None &&
detail.changed_touches.length() == 1 else {
return @minimoon.no_cmd(state)
}
let point = detail.changed_touches[0]
let token = state.token + 1
(
{
token,
drag: Some({
pointer: point.identifier,
start: drawer_axis(side, point),
position: drawer_axis(side, point),
started_at: detail.timestamp,
timestamp: detail.timestamp,
size: None,
ended: false,
}),
},
@minimoon.measure_nodes(
[id + "-surface"],
emit.map(result => Measured(token, result)),
),
)
}
Measured(token, result) => {
guard state.token == token && state.drag is Some(drag) else {
return @minimoon.no_cmd(state)
}
guard result is Ok([Some(rect)]) else {
return @minimoon.no_cmd({ ..state, drag: None, })
}
let size = if side == Top || side == Bottom {
rect.height
} else {
rect.width
}
guard size > 0.0 else {
return @minimoon.no_cmd({ ..state, drag: None, })
}
let drag = { ..drag, size: Some(size), }
if drag.ended {
(
{ ..state, drag: None, },
if drawer_dismisses(side, drag) {
on_change(false)
} else {
@minimoon.none
},
)
} else {
@minimoon.no_cmd({ ..state, drag: Some(drag), })
}
}
Move(detail) | End(detail) => {
guard state.drag is Some(drag) else {
return @minimoon.no_cmd(state)
}
let ended = msg is End(_)
let points = if ended {
detail.changed_touches
} else {
detail.touches
}
guard slider_point(points, drag.pointer) is Some(point) else {
return @minimoon.no_cmd(state)
}
let drag = {
..drag,
position: drawer_axis(side, point),
timestamp: detail.timestamp,
ended,
}
if ended && drag.size is Some(_) {
(
{ ..state, drag: None, },
if drawer_dismisses(side, drag) {
on_change(false)
} else {
@minimoon.none
},
)
} else {
@minimoon.no_cmd({ ..state, drag: Some(drag), })
}
}
}
},
subscriptions=(_, emit) => context.page.on_hide(emit(Cancel)),
)
@minimoon.Val::view3(content, state, context.visible, (
content,
state,
visible,
) => {
let offset = match state.drag {
Some(drag) if drag.size is Some(_) => drawer_distance(side, drag)
_ => 0.0
}
let signed = if side == Left || side == Top { -offset } else { offset }
let axis = if side == Left || side == Right { "X" } else { "Y" }
let surface = if visible {
@minimoon.layer(id=id + "-layer", [
@minimoon.tap_view(
class="mmui-overlay",
style="position:fixed;left:0;right:0;top:0;bottom:0;",
on_tap=if close_on_overlay {
on_change(false)
} else {
@minimoon.none
},
event_key=id + "/dismiss",
@minimoon.nothing(),
),
ui_container(
"drawer",
class="mmui-surface",
id=id + "-surface",
style=surface_style(SheetSurface(side), None) +
"transform:translate" +
axis +
"(" +
signed.to_string() +
"px);",
semantics=@minimoon.semantics(
role=@minimoon.DialogRole,
modal=true,
labelled_by=id + "-title",
),
[
@minimoon.touch_view(
id=id + "-handle",
class="mmui-drawer-handle",
style="min-width:44px;min-height:44px;",
catch_move=true,
on_touch_start=emit.map(value => DrawerMsg::Start(value)),
on_touch_move=emit.map(value => Move(value)),
on_touch_end=emit.map(value => DrawerMsg::End(value)),
on_touch_cancel=emit.map(value => CancelTouch(value)),
event_key=id + "/drag",
semantics=@minimoon.semantics(label="Drag to close"),
@minimoon.text("—"),
),
@minimoon.h1(
id=id + "-title",
class="mmui-surface-title",
title,
),
content,
@minimoon.button(
on_tap=on_change(false),
event_key=id + "/close",
"Close",
),
],
),
])
} else {
@minimoon.nothing()
}
ui_container("drawer-root", [
drawer_trigger(id, trigger_label, true, on_change, disabled),
surface,
])
})
},
by=value => value.to_string(),
)
}
///|
pub fn drawer(
context : UiContext,
id~ : String,
title~ : String,
trigger_label~ : String,
content~ : @minimoon.Val[@minimoon.Node],
side? : Side = Bottom,
initially_open? : Bool = false,
disabled? : Bool = false,
) -> @minimoon.Val[@minimoon.Node] {
let (open, change) = @minimoon.create_variable(initially_open)
drawer_controlled(
context,
id~,
title~,
trigger_label~,
content~,
side~,
open~,
on_change=change.map(value => _ => value),
disabled~,
)
}