///|
/// Touch-native pane resizing adapted from RUI b1291945 (MIT).
pub(all) struct ResizablePane {
key : String
content : @minimoon.Node
minimum : Double
maximum : Double
} derive(Eq)
///|
pub fn resizable_pane(
key~ : String,
content~ : @minimoon.Node,
minimum? : Double = 0.0,
maximum? : Double = 100.0,
) -> ResizablePane {
guard key != "" && minimum >= 0.0 && maximum <= 100.0 && minimum <= maximum else {
abort("resizable pane requires a key and 0 <= minimum <= maximum <= 100")
}
{ key, content, minimum, maximum, }
}
///|
fn resize_sizes(
panes : Array[ResizablePane],
sizes : Array[Double],
) -> Array[Double] {
guard panes.length() > 0 else { return [] }
let keys : Set[String] = Set([])
let mut minimum = 0.0
let mut maximum = 0.0
for pane in panes {
guard pane.key != "" &&
!keys.contains(pane.key) &&
pane.minimum >= 0.0 &&
pane.maximum <= 100.0 &&
pane.minimum <= pane.maximum else {
abort("resizable pane keys and constraints are invalid")
}
keys.add(pane.key)
minimum += pane.minimum
maximum += pane.maximum
}
guard minimum <= 100.0 && maximum >= 100.0 else {
abort("resizable constraints cannot fill 100 percent")
}
if sizes.length() == panes.length() {
let mut valid = true
let mut total = 0.0
for index, size in sizes {
if !(size >= panes[index].minimum && size <= panes[index].maximum) {
valid = false
}
total += size
}
if valid && (total - 100.0).abs() < 0.000001 {
return sizes.copy()
}
}
let result = panes.map(pane => pane.minimum)
let mut remaining = 100.0 - minimum
while remaining > 0.000001 {
let mut available = 0
for index, size in result {
if panes[index].maximum - size > 0.000001 {
available += 1
}
}
if available == 0 {
break
}
let share = remaining / available.to_double()
for index, size in result {
let add = share.min(panes[index].maximum - size).max(0.0)
result[index] = size + add
remaining -= add
}
}
result
}
///|
priv struct ResizeDrag {
pointer : Int
index : Int
keys : Array[String]
initial : Array[Double]
start : Double
position : Double
extent : Double?
ended : Bool
} derive(Eq)
///|
priv struct ResizeState {
token : Int
drag : ResizeDrag?
} derive(Eq)
///|
priv enum ResizeMsg {
StartResize(Int, @minimoon.TouchDetail)
MoveResize(@minimoon.TouchDetail)
EndResize(@minimoon.TouchDetail)
CancelResize
CancelResizeTouch(@minimoon.TouchDetail)
MeasuredResize(Int, Result[Array[@minimoon.NodeRect?], @minimoon.HostError])
ResizeStep(Int, Double)
}
///|
fn resized_values(
panes : Array[ResizablePane],
drag : ResizeDrag,
orientation : Orientation,
direction : @theme.TextDirection,
) -> Array[Double] {
let delta = (drag.position - drag.start) /
drag.extent.unwrap_or(1.0) *
100.0 *
(if orientation == Horizontal && direction == @theme.Rtl {
-1.0
} else {
1.0
})
@headless.resize_pair(
drag.initial,
drag.index,
delta,
panes.map(pane => pane.minimum),
panes.map(pane => pane.maximum),
)
}
///|
pub fn resizable_controlled(
context : UiContext,
id~ : String,
panes~ : @minimoon.Val[Array[ResizablePane]],
sizes~ : @minimoon.Val[Array[Double]],
on_change~ : @minimoon.Emit[Array[Double]],
on_commit? : @minimoon.Emit[Array[Double]],
orientation? : Orientation = Horizontal,
direction? : @theme.TextDirection = @theme.Ltr,
disabled? : Bool = false,
style? : String = "height:320px;",
) -> @minimoon.Val[@minimoon.Node] {
let input = @minimoon.Val::map2(panes, sizes, (panes, sizes) => {
(panes, resize_sizes(panes, sizes))
})
let (state, emit) = @minimoon.create_state_with_input(
input~,
init=(_, _) => @minimoon.no_cmd(ResizeState::{ token: 0, drag: None, }),
update=(state, input, msg : ResizeMsg, emit) => {
let (panes, sizes) = input
let keys = panes.map(pane => pane.key)
let state : ResizeState = if state.drag is Some(drag) && drag.keys != keys {
{ token: state.token + 1, drag: None, }
} else {
state
}
match msg {
CancelResizeTouch(detail) =>
if state.drag is Some(drag) &&
slider_point(detail.changed_touches, drag.pointer) is Some(_) {
({ token: state.token + 1, drag: None, }, on_change(drag.initial))
} else {
@minimoon.no_cmd(state)
}
CancelResize =>
(
{ token: state.token + 1, drag: None, },
match state.drag {
Some(drag) if drag.keys == keys => on_change(drag.initial)
_ => @minimoon.none
},
)
ResizeStep(index, amount) => {
if disabled {
return @minimoon.no_cmd(state)
}
let next = @headless.resize_pair(
sizes,
index,
amount,
panes.map(pane => pane.minimum),
panes.map(pane => pane.maximum),
)
(
state,
@minimoon.batch([
on_change(next),
on_commit.map(commit => commit(next)).unwrap_or(@minimoon.none),
]),
)
}
StartResize(index, detail) => {
guard !disabled &&
state.drag is None &&
detail.changed_touches.length() == 1 &&
index >= 0 &&
index + 1 < panes.length() else {
return @minimoon.no_cmd(state)
}
let point = detail.changed_touches[0]
let start = if orientation == Horizontal {
point.client_x
} else {
point.client_y
}
let token = state.token + 1
(
{
token,
drag: Some({
pointer: point.identifier,
index,
keys,
initial: sizes,
start,
position: start,
extent: None,
ended: false,
}),
},
@minimoon.measure_nodes(
[id],
emit.map(result => MeasuredResize(token, result)),
),
)
}
MeasuredResize(token, result) => {
guard state.token == token &&
state.drag is Some(drag) &&
drag.keys == keys else {
return @minimoon.no_cmd(state)
}
guard result is Ok([Some(rect)]) else {
return @minimoon.no_cmd({ ..state, drag: None, })
}
let extent = (if orientation == Horizontal {
rect.width
} else {
rect.height
}) -
(panes.length() - 1).to_double() * 44.0
guard extent > 0.0 else {
return @minimoon.no_cmd({ ..state, drag: None, })
}
let drag = { ..drag, extent: Some(extent), }
let next = resized_values(panes, drag, orientation, direction)
(
{ ..state, drag: if drag.ended { None } else { Some(drag) }, },
@minimoon.batch([
on_change(next),
if drag.ended {
on_commit.map(commit => commit(next)).unwrap_or(@minimoon.none)
} else {
@minimoon.none
},
]),
)
}
MoveResize(detail) | EndResize(detail) => {
guard state.drag is Some(drag) else { return @minimoon.no_cmd(state) }
guard drag.keys == keys else {
return @minimoon.no_cmd({ ..state, drag: None, })
}
let ended = msg is EndResize(_)
guard slider_point(
if ended {
detail.changed_touches
} else {
detail.touches
},
drag.pointer,
)
is Some(point) else {
return @minimoon.no_cmd(state)
}
let drag = {
..drag,
position: if orientation == Horizontal {
point.client_x
} else {
point.client_y
},
ended,
}
if drag.extent is None {
return @minimoon.no_cmd({ ..state, drag: Some(drag), })
}
let next = resized_values(panes, drag, orientation, direction)
(
{ ..state, drag: if ended { None } else { Some(drag) }, },
@minimoon.batch([
on_change(next),
if ended {
on_commit.map(commit => commit(next)).unwrap_or(@minimoon.none)
} else {
@minimoon.none
},
]),
)
}
}
},
subscriptions=(_, _, emit) => context.page.on_hide(emit(CancelResize)),
)
@minimoon.Val::view2(input, state, (input, _state) => {
let (panes, sizes) = input
let nodes : Array[@minimoon.KeyedNode] = []
for index, pane in panes {
nodes.push(
@minimoon.keyed(
"pane:" + pane.key,
ui_container(
"resizable-panel",
id=id + "-pane-" + index.to_string(),
[pane.content],
style="flex:" +
sizes[index].to_string() +
" 1 0%;min-width:0;min-height:0;overflow:hidden;",
),
),
)
if index + 1 < panes.length() {
let handle = id + "-handle-" + index.to_string()
nodes.push(
@minimoon.keyed(
"handle:" + pane.key,
@minimoon.touch_view(
id=handle,
class="mmui-resizable-handle",
style="flex:0 0 44px;min-width:0;min-height:0;display:flex;align-items:center;justify-content:center;flex-direction:" +
(if orientation == Horizontal { "column" } else { "row" }) +
";",
catch_move=true,
on_touch_start=emit.map(value => StartResize(index, value)),
on_touch_move=emit.map(value => MoveResize(value)),
on_touch_end=emit.map(value => EndResize(value)),
on_touch_cancel=emit.map(value => CancelResizeTouch(value)),
event_key=handle,
semantics=@minimoon.semantics(
role=@minimoon.SeparatorRole,
orientation=if orientation == Horizontal {
"vertical"
} else {
"horizontal"
},
value_now=sizes[index].to_int(),
disabled~,
),
[
@minimoon.button(
disabled~,
on_tap=emit(ResizeStep(index, -5.0)),
event_key=handle + "/decrease",
semantics=@minimoon.semantics(label="Decrease previous pane"),
"−",
),
@minimoon.button(
disabled~,
on_tap=emit(ResizeStep(index, 5.0)),
event_key=handle + "/increase",
semantics=@minimoon.semantics(label="Increase previous pane"),
"+",
),
],
),
),
)
}
}
ui_container(
"resizable",
id~,
style="display:flex;min-width:0;min-height:0;flex-direction:" +
(if orientation == Vertical {
"column"
} else if direction == @theme.Rtl {
"row-reverse"
} else {
"row"
}) +
";" +
style,
[@minimoon.keyed_fragment(nodes)],
)
})
}
///|
pub fn resizable(
context : UiContext,
id~ : String,
panes~ : @minimoon.Val[Array[ResizablePane]],
initial_sizes? : Array[Double] = [],
orientation? : Orientation = Horizontal,
direction? : @theme.TextDirection = @theme.Ltr,
disabled? : Bool = false,
style? : String = "height:320px;",
) -> @minimoon.Val[@minimoon.Node] {
let (sizes, change) = @minimoon.create_variable(initial_sizes)
resizable_controlled(
context,
id~,
panes~,
sizes~,
on_change=change.map(value => _ => value),
orientation~,
direction~,
disabled~,
style~,
)
}