///|
priv enum SliderMsg {
SetSliderValue(Int, Int)
SetSliderKeyboardValue(Int, Int)
CommitSliderValue(Int, Int)
}
///|
const SliderThumbStyle : String = "position:absolute;z-index:2;display:block;width:1rem;height:1rem;border:1px solid var(--rui-primary,oklch(0.205 0 0));border-radius:9999px;background:var(--rui-slider-thumb,var(--rui-background,oklch(1 0 0)));--rui-slider-thumb-base-shadow:0 1px 2px rgb(0 0 0 / 0.1);box-shadow:var(--rui-slider-thumb-shadow,var(--rui-slider-thumb-base-shadow,0 1px 2px rgb(0 0 0 / 0.1)));pointer-events:none"
///|
fn slider_parse(value : String, fallback : Int) -> Int {
@string.parse_int(value) catch {
_ => fallback
}
}
///|
fn slider_clamp(value : Int, min : Int, max : Int) -> Int {
if value < min {
min
} else if value > max {
max
} else {
value
}
}
///|
fn slider_step_amount(step : Int) -> Int {
if step > 0 {
step
} else {
1
}
}
///|
fn slider_align_to_step(value : Int, min : Int, max : Int, step : Int) -> Int {
let value = slider_clamp(value, min, max)
let amount = slider_step_amount(step)
let offset = value - min
let aligned = min + (offset + amount / 2) / amount * amount
slider_clamp(aligned, min, max)
}
///|
fn slider_percent(value : Int, min : Int, max : Int) -> Int {
if max <= min {
0
} else {
(slider_clamp(value, min, max) - min) * 100 / (max - min)
}
}
///|
fn slider_minimum_gap(
count : Int,
min : Int,
max : Int,
step : Int,
min_steps_between_values : Int,
) -> Int {
if count <= 1 || min_steps_between_values <= 0 || max <= min {
0
} else {
let amount = slider_step_amount(step)
let available_steps = (max - min) / amount
let possible_steps = available_steps / (count - 1)
slider_clamp(min_steps_between_values, 0, possible_steps) * amount
}
}
///|
fn slider_normalize_values(
values : Array[Int],
min : Int,
max : Int,
step : Int,
min_steps_between_values : Int,
) -> Array[Int] {
let requested = if values.length() == 0 { [min] } else { values }
let normalized = requested.map(value => {
slider_align_to_step(value, min, max, step)
})
normalized.sort()
let count = normalized.length()
if count > 1 {
let gap = slider_minimum_gap(
count, min, max, step, min_steps_between_values,
)
for index in 0.. Array[Int] {
slider_constrain_thumb_value(
values,
index,
slider_align_to_step(value, min, max, step),
min,
max,
step,
min_steps_between_values,
)
}
///|
fn slider_constrain_thumb_value(
values : Array[Int],
index : Int,
value : Int,
min : Int,
max : Int,
step : Int,
min_steps_between_values : Int,
) -> Array[Int] {
let next = values.copy()
if index < 0 || index >= next.length() {
return next
}
let gap = slider_minimum_gap(
next.length(),
min,
max,
step,
min_steps_between_values,
)
let lower = if index == 0 { min } else { next[index - 1] + gap }
let upper = if index + 1 >= next.length() {
max
} else {
next[index + 1] - gap
}
next[index] = slider_clamp(value, lower, upper)
next
}
///|
fn slider_round_to_step(value : Int, min : Int, step : Int) -> Int {
let amount = slider_step_amount(step)
let offset = value - min
min + (offset + amount / 2) / amount * amount
}
///|
fn slider_key_value(
value : Int,
min : Int,
max : Int,
step : Int,
large_step : Int,
shift : Bool,
rtl : Bool,
key : String,
) -> Int? {
let amount = if shift { large_step } else { slider_step_amount(step) }
let rounded = slider_round_to_step(value, min, step)
let requested = match key {
"Home" => Some(min)
"End" => Some(max)
"ArrowUp" => Some(rounded + amount)
"ArrowRight" => Some(rounded + (if rtl { -amount } else { amount }))
"ArrowDown" => Some(rounded - amount)
"ArrowLeft" => Some(rounded + (if rtl { amount } else { -amount }))
"PageUp" => Some(rounded + large_step)
"PageDown" => Some(rounded - large_step)
_ => None
}
requested.map(next => slider_clamp(next, min, max))
}
///|
fn slider_thumb_key_value(
values : Array[Int],
index : Int,
min : Int,
max : Int,
step : Int,
min_steps_between_values : Int,
large_step : Int,
shift : Bool,
rtl : Bool,
key : String,
) -> Int? {
if index < 0 || index >= values.length() {
return None
}
let current = values[index]
let gap = slider_minimum_gap(
values.length(),
min,
max,
step,
min_steps_between_values,
)
let lower = if index == 0 { min } else { values[index - 1] }
let upper = if index + 1 >= values.length() { max } else { values[index + 1] }
let requested = match key {
"Home" => Some(if index == 0 { min } else { lower + gap })
"End" => Some(if index + 1 >= values.length() { max } else { upper - gap })
_ => slider_key_value(current, min, max, step, large_step, shift, rtl, key)
}
match requested {
Some(requested) => {
let next = slider_clamp(requested, lower, upper)
if (index > 0 && next - lower < gap) ||
(index + 1 < values.length() && upper - next < gap) {
Some(current)
} else {
Some(next)
}
}
None => None
}
}
///|
#cfg(target="js")
priv struct SliderPointerConfig {
min : Int
max : Int
step : Int
gap : Int
}
///|
#cfg(target="js")
priv struct SliderPointerBinding {
root : @dom.Element
mut active_index : Int
mut active_pointer_id : Int?
mut last_used_index : Int
}
///|
#cfg(target="js")
let slider_pointer_bindings : Array[SliderPointerBinding] = []
///|
#cfg(target="js")
fn slider_prune_pointer_bindings() -> Unit {
let mut index = 0
while index < slider_pointer_bindings.length() {
if slider_pointer_bindings[index].root.get_is_connected() {
index += 1
} else {
ignore(slider_pointer_bindings.remove(index))
}
}
}
///|
#cfg(target="js")
fn slider_pointer_binding(root : @dom.Element) -> SliderPointerBinding? {
for binding in slider_pointer_bindings {
if binding.root.is_same_node(root.as_node()) {
return Some(binding)
}
}
None
}
///|
#cfg(target="js")
fn slider_pointer_controls(
binding : SliderPointerBinding,
) -> Array[@dom.HTMLInputElement] {
let controls : Array[@dom.HTMLInputElement] = []
for
element in binding.root.query_selector_all("[data-slot=\"slider-control\"]") {
if ui_element_is_enabled(element) &&
element.to_html_input_element() is Some(input) {
controls.push(input)
}
}
controls
}
///|
#cfg(target="js")
fn slider_pointer_config(
binding : SliderPointerBinding,
count : Int,
) -> SliderPointerConfig {
let min = ui_parse_int_or(
binding.root.get_attribute("data-min").unwrap_or(""),
0,
)
let max = ui_parse_int_or(
binding.root.get_attribute("data-max").unwrap_or(""),
100,
)
let step = ui_parse_int_or(
binding.root.get_attribute("data-step").unwrap_or(""),
1,
).max(1)
let min_steps = ui_parse_int_or(
binding.root.get_attribute("data-min-steps-between-values").unwrap_or(""),
0,
).max(0)
let available_steps = (max - min).max(0) / step
let possible_steps = if count > 1 { available_steps / (count - 1) } else { 0 }
let effective_steps = if count > 1 {
min_steps.min(possible_steps.max(0))
} else {
0
}
{ min, max, step, gap: step * effective_steps }
}
///|
#cfg(target="js")
fn slider_pointer_value_at(
binding : SliderPointerBinding,
pointer : @dom.PointerEvent,
) -> Int? {
let rect = binding.root.get_bounding_client_rect()
let vertical = binding.root.get_attribute("data-orientation").unwrap_or("") ==
"vertical"
let size = if vertical { rect.get_height() } else { rect.get_width() }
if size <= 0.0 {
return None
}
let rtl = !vertical &&
(
binding.root.get_attribute("data-direction").unwrap_or("") == "rtl" ||
binding.root.get_attribute("dir").unwrap_or("") == "rtl" ||
ui_element_is_rtl(binding.root)
)
let offset = if vertical {
rect.get_bottom() - pointer.get_client_y().to_double()
} else if rtl {
rect.get_right() - pointer.get_client_x().to_double()
} else {
pointer.get_client_x().to_double() - rect.get_left()
}
let percent = (offset / size).clamp(min=0.0, max=1.0)
let config = slider_pointer_config(binding, 1)
let raw = config.min.to_double() +
percent * (config.max - config.min).to_double()
let aligned = config.min +
((raw - config.min.to_double()) / config.step.to_double()).round().to_int() *
config.step
Some(slider_clamp(aligned, config.min, config.max))
}
///|
#cfg(target="js")
fn slider_pointer_closest_index(
binding : SliderPointerBinding,
requested : Int,
controls : Array[@dom.HTMLInputElement],
) -> Int {
let mut best = -1
let mut distance = 0
for index, input in controls {
let next_distance = (ui_parse_int_or(input.value(), requested) - requested).abs()
if best < 0 ||
next_distance < distance ||
(
next_distance == distance &&
(
index == binding.last_used_index ||
(best != binding.last_used_index && index < best)
)
) {
best = index
distance = next_distance
}
}
best
}
///|
#cfg(target="js")
fn slider_pointer_activate(
binding : SliderPointerBinding,
index : Int,
controls : Array[@dom.HTMLInputElement],
) -> Bool {
if index < 0 || index >= controls.length() {
return false
}
binding.active_index = index
binding.last_used_index = index
for current, input in controls {
input
.get_style()
.set_property(
"z-index",
(if current == index { controls.length() + 4 } else { current + 3 }).to_string(),
)
}
controls[index].focus_with_options(prevent_scroll=true)
true
}
///|
#cfg(target="js")
fn slider_pointer_update(
binding : SliderPointerBinding,
pointer : @dom.PointerEvent,
commit : Bool,
) -> Unit {
let controls = slider_pointer_controls(binding)
let index = binding.active_index
if index < 0 || index >= controls.length() {
return
}
guard slider_pointer_value_at(binding, pointer) is Some(requested) else {
return
}
let config = slider_pointer_config(binding, controls.length())
let lower = if index == 0 {
config.min
} else {
ui_parse_int_or(controls[index - 1].value(), config.min) + config.gap
}
let upper = if index + 1 >= controls.length() {
config.max
} else {
ui_parse_int_or(controls[index + 1].value(), config.max) - config.gap
}
let next = requested.clamp(min=lower, max=upper)
let input = controls[index]
if ui_parse_int_or(input.value(), next) != next {
input.set_value(next.to_string())
input.dispatch_event(@dom.Event::new("input", bubbles=true))
}
if commit {
input.dispatch_event(@dom.Event::new("change", bubbles=true))
}
}
///|
#cfg(target="js")
fn slider_pointer_finish(
binding : SliderPointerBinding,
pointer : @dom.PointerEvent,
commit : Bool,
) -> Unit {
guard binding.active_pointer_id is Some(pointer_id) else { return }
guard pointer_id == pointer.get_pointer_id() else { return }
if commit {
slider_pointer_update(binding, pointer, true)
}
if binding.root.has_pointer_capture(pointer_id) {
binding.root.release_pointer_capture(pointer_id)
}
binding.active_index = -1
binding.active_pointer_id = None
}
///|
#cfg(target="js")
fn slider_bind_pointer_listeners(binding : SliderPointerBinding) -> Unit {
binding.root.add_event_listener("pointerdown", event => {
guard event.to_pointer_event() is Some(pointer) else { return }
if binding.root.get_attribute("data-disabled").unwrap_or("") == "true" ||
pointer.get_button() != 0 ||
binding.active_pointer_id is Some(_) {
return
}
guard slider_pointer_value_at(binding, pointer) is Some(requested) else {
return
}
let controls = slider_pointer_controls(binding)
if controls.length() == 0 {
return
}
let index = slider_pointer_closest_index(binding, requested, controls)
if !slider_pointer_activate(binding, index, controls) {
return
}
binding.active_pointer_id = Some(pointer.get_pointer_id())
pointer.prevent_default()
binding.root.set_pointer_capture(pointer.get_pointer_id())
slider_pointer_update(binding, pointer, false)
})
binding.root.add_event_listener("pointermove", event => {
guard event.to_pointer_event() is Some(pointer) else { return }
guard binding.active_pointer_id is Some(pointer_id) else { return }
if pointer_id != pointer.get_pointer_id() {
return
}
pointer.prevent_default()
slider_pointer_update(binding, pointer, false)
})
binding.root.add_event_listener("pointerup", event => {
if event.to_pointer_event() is Some(pointer) {
slider_pointer_finish(binding, pointer, true)
}
})
binding.root.add_event_listener("pointercancel", event => {
if event.to_pointer_event() is Some(pointer) {
slider_pointer_finish(binding, pointer, false)
}
})
binding.root.add_event_listener("lostpointercapture", event => {
guard event.to_pointer_event() is Some(pointer) else { return }
guard binding.active_pointer_id is Some(pointer_id) else { return }
if pointer_id == pointer.get_pointer_id() {
binding.active_index = -1
binding.active_pointer_id = None
}
})
}
///|
#cfg(target="js")
fn bind_slider_pointer(id : String) -> Unit {
guard ui_element_by_id(id + "-root") is Some(root) else { return }
slider_prune_pointer_bindings()
if slider_pointer_binding(root) is Some(_) {
return
}
let binding : SliderPointerBinding = {
root,
active_index: -1,
active_pointer_id: None,
last_used_index: -1,
}
slider_pointer_bindings.push(binding)
slider_bind_pointer_listeners(binding)
}
///|
#cfg(target="js")
fn slider_bind_pointer_cmd(id : String) -> @cmd.Cmd {
@cmd.custom_cmd(kind=@cmd.AfterRender, _ => {
ui_after_mount(
id + "-root",
() => bind_slider_pointer(id),
purpose="slider-pointer-bind",
)
})
}
///|
#cfg(target="js")
fn slider_input_pointer_style() -> String {
"pointer-events:none"
}
///|
#cfg(not(target="js"))
fn slider_input_pointer_style() -> String {
""
}
///|
#cfg(target="js")
fn slider_event_is_rtl(event : @dom.KeyboardEvent) -> Bool {
let target = match event.current_target().to_option() {
Some(target) => target
None => event.target()
}
guard target.to_element() is Some(element) else { return false }
let root = element.closest("[data-slot=\"slider\"]").unwrap_or(element)
root.get_attribute("data-direction").unwrap_or("") == "rtl" ||
root.get_attribute("dir").unwrap_or("") == "rtl" ||
ui_element_is_rtl(root)
}
///|
fn slider_key_stops_propagation(key : String) -> Bool {
key == "ArrowUp" ||
key == "ArrowRight" ||
key == "ArrowDown" ||
key == "ArrowLeft" ||
key == "Home" ||
key == "End"
}
///|
#cfg(target="js")
fn slider_control_attrs(
input_attrs : @html.Attrs?,
values : Array[Int],
index : Int,
emit : @cmd.Emit[SliderMsg],
min : Int,
max : Int,
step : Int,
min_steps_between_values : Int,
large_step : Int,
orientation : SeparatorOrientation,
disabled : Bool,
) -> @html.Attrs {
let value = values[index]
let attrs = ui_attrs(input_attrs)
.data_set("slot", "slider-control")
.data_set("index", index.to_string())
.aria_orientation(separator_orientation_value(orientation))
if disabled {
ignore(attrs.disabled(true))
} else {
ignore(
attrs.on_keydown(event => {
if event.get_default_prevented() {
return @cmd.none
}
let key = event.key()
match
slider_thumb_key_value(
values,
index,
min,
max,
step,
min_steps_between_values,
large_step,
event.shift_key(),
slider_event_is_rtl(event),
key,
) {
Some(next) => {
if slider_key_stops_propagation(key) {
event.stop_propagation()
}
event.prevent_default()
if next == value {
@cmd.none
} else {
emit(SetSliderKeyboardValue(index, next))
}
}
None => @cmd.none
}
}),
)
}
attrs
}
///|
#cfg(not(target="js"))
fn slider_control_attrs(
input_attrs : @html.Attrs?,
values : Array[Int],
index : Int,
emit : @cmd.Emit[SliderMsg],
min : Int,
max : Int,
step : Int,
min_steps_between_values : Int,
large_step : Int,
orientation : SeparatorOrientation,
disabled : Bool,
) -> @html.Attrs {
ignore((values, emit, min, max, step, min_steps_between_values, large_step))
let attrs = ui_attrs(input_attrs)
.data_set("slot", "slider-control")
.data_set("index", index.to_string())
.aria_orientation(separator_orientation_value(orientation))
if disabled {
ignore(attrs.disabled(true))
}
attrs
}
///|
fn slider_serialized_values(values : Array[Int]) -> String {
values.map(value => value.to_string()).join(",")
}
///|
fn slider_thumb_label(aria_label : String, index : Int, count : Int) -> String {
if count <= 1 {
aria_label
} else {
"\{aria_label} \{index + 1}"
}
}
///|
fn render_slider_values(
values : Array[Int],
emit : @cmd.Emit[SliderMsg],
min : Int,
max : Int,
step : Int,
min_steps_between_values : Int,
large_step : Int,
orientation : SeparatorOrientation,
disabled : Bool,
aria_label : String,
id : String,
legacy_single_id : Bool,
class : String?,
title : String?,
attrs : @html.Attrs?,
input_attrs : @html.Attrs?,
style : Array[String],
) -> @html.Html {
let count = values.length()
let orientation_value = separator_orientation_value(orientation)
let serialized_values = slider_serialized_values(values)
let first_value = if count == 0 { min } else { values[0] }
let last_value = if count == 0 { min } else { values[count - 1] }
let range_start = if count <= 1 { min } else { first_value }
let start_percent = slider_percent(range_start, min, max)
let end_percent = slider_percent(last_value, min, max)
let range_percent = end_percent - start_percent
let track = @html.div(
attrs=@html.Attrs::build()
.data_set("slot", "slider-track")
.aria_hidden("true"),
style=[
"position:absolute;overflow:hidden;border-radius:9999px;background:var(--rui-muted,oklch(0.97 0 0))",
if orientation is Vertical {
"left:50%;top:0;width:0.375rem;height:100%;transform:translateX(-50%)"
} else {
"inset-inline-start:0;top:50%;width:100%;height:0.375rem;transform:translateY(-50%)"
},
],
@html.div(
attrs=@html.Attrs::build()
.data_set("slot", "slider-range")
.data_set("start", first_value.to_string())
.data_set("end", last_value.to_string()),
style=[
"position:absolute;background:var(--rui-primary,oklch(0.205 0 0))",
if orientation is Vertical {
"left:0;right:0;bottom:\{start_percent}%;height:\{range_percent}%"
} else {
"inset-inline-start:\{start_percent}%;top:0;bottom:0;width:\{range_percent}%"
},
],
"",
),
)
let children : Array[@html.Html] = [track]
for index in 0.. {
SetSliderValue(index, slider_parse(text, value))
}),
on_change=emit.map(text => {
CommitSliderValue(index, slider_parse(text, value))
}),
attrs=control_attrs.aria_label(
slider_thumb_label(aria_label, index, count),
),
style=[
"position:absolute;inset:0;z-index:\{3 + index};width:100%;height:100%;margin:0;background:transparent;cursor:pointer;appearance:none;-webkit-appearance:none",
slider_input_pointer_style(),
if orientation is Vertical {
"writing-mode:vertical-lr;direction:rtl"
} else {
""
},
],
),
)
}
@html.div(
id="\{id}-root",
class?,
title?,
attrs=ui_attrs(attrs)
.data_set("slot", "slider")
.data_set("orientation", orientation_value)
.data_set("disabled", ui_bool(disabled))
.data_set("min", min.to_string())
.data_set("max", max.to_string())
.data_set("step", step.to_string())
.data_set("large-step", large_step.to_string())
.data_set("value", serialized_values)
.data_set("values", serialized_values)
.data_set("thumb-count", count.to_string())
.data_set(
"min-steps-between-values",
min_steps_between_values.to_string(),
),
style=ui_styles(
[
UiBoxSizing,
UiFontSans,
"position:relative;display:flex;touch-action:none;align-items:center;user-select:none",
if orientation is Vertical {
"width:1rem;height:11rem;min-height:11rem;flex-direction:column"
} else {
"width:100%;height:1rem"
},
ui_disabled_visual_style(disabled),
],
style,
),
children,
)
}
///|
fn slider_values_notify(
notify : @cmd.Emit[Array[Int]]?,
values : Array[Int],
) -> @cmd.Cmd {
if notify is Some(notify) {
notify(values.copy())
} else {
@cmd.none
}
}
///|
#cfg(target="js")
fn slider_values_impl(
default_values : Array[Int],
min : Int,
max : Int,
step : Int,
large_step : Int,
min_steps_between_values : Int,
orientation : SeparatorOrientation,
disabled : Bool,
aria_label : String,
on_value_change : @cmd.Emit[Array[Int]]?,
on_value_commit : @cmd.Emit[Array[Int]]?,
id : String,
legacy_single_id : Bool,
class : String?,
title : String?,
attrs : @html.Attrs?,
input_attrs : @html.Attrs?,
style : Array[String],
) -> @rabbita.Val[@html.Html] {
let max = if max < min { min } else { max }
let step = slider_step_amount(step)
let large_step = if large_step > 0 { large_step } else { 10 }
let min_steps_between_values = if min_steps_between_values > 0 {
min_steps_between_values
} else {
0
}
let initial = slider_normalize_values(
default_values, min, max, step, min_steps_between_values,
)
let (values, emit) = @rabbita.create_state_with_init(
init=_ => (initial, slider_bind_pointer_cmd(id)),
update=fn(_, msg, current) {
match msg {
SetSliderValue(index, value) => {
let next = slider_set_thumb_value(
current, index, value, min, max, step, min_steps_between_values,
)
let command = if next == current {
@cmd.none
} else {
slider_values_notify(on_value_change, next)
}
(next, command)
}
SetSliderKeyboardValue(index, value) => {
let next = slider_constrain_thumb_value(
current, index, value, min, max, step, min_steps_between_values,
)
let command = if next == current {
@cmd.none
} else {
@cmd.batch([
slider_values_notify(on_value_change, next),
slider_values_notify(on_value_commit, next),
])
}
(next, command)
}
CommitSliderValue(index, value) => {
let next = slider_set_thumb_value(
current, index, value, min, max, step, min_steps_between_values,
)
(next, slider_values_notify(on_value_commit, next))
}
}
},
)
values.view(values => {
render_slider_values(
values, emit, min, max, step, min_steps_between_values, large_step, orientation,
disabled, aria_label, id, legacy_single_id, class, title, attrs, input_attrs,
style,
)
})
}
///|
#cfg(not(target="js"))
fn slider_values_impl(
default_values : Array[Int],
min : Int,
max : Int,
step : Int,
large_step : Int,
min_steps_between_values : Int,
orientation : SeparatorOrientation,
disabled : Bool,
aria_label : String,
on_value_change : @cmd.Emit[Array[Int]]?,
on_value_commit : @cmd.Emit[Array[Int]]?,
id : String,
legacy_single_id : Bool,
class : String?,
title : String?,
attrs : @html.Attrs?,
input_attrs : @html.Attrs?,
style : Array[String],
) -> @rabbita.Val[@html.Html] {
ignore((on_value_change, on_value_commit))
let max = if max < min { min } else { max }
let step = slider_step_amount(step)
let large_step = if large_step > 0 { large_step } else { 10 }
let min_steps_between_values = if min_steps_between_values > 0 {
min_steps_between_values
} else {
0
}
let values = slider_normalize_values(
default_values, min, max, step, min_steps_between_values,
)
let emit : @cmd.Emit[SliderMsg] = @cmd.Emit(_ => @cmd.none)
@rabbita.Val::constant(
render_slider_values(
values, emit, min, max, step, min_steps_between_values, large_step, orientation,
disabled, aria_label, id, legacy_single_id, class, title, attrs, input_attrs,
style,
),
)
}
///|
/// Stateful multi-thumb slider with sorted, non-crossing values.
///
/// Every thumb is backed by its own native range input. Initial and pointer
/// values align to `step`; keyboard page/shift movement uses the exact
/// `large_step`. Values stay sorted and at least
/// `min_steps_between_values` steps apart.
pub fn slider_values(
id~ : String,
default_values~ : Array[Int],
min? : Int = 0,
max? : Int = 100,
step? : Int = 1,
large_step? : Int = 10,
min_steps_between_values? : Int = 0,
orientation? : SeparatorOrientation = Horizontal,
disabled? : Bool = false,
aria_label? : String = "Value",
on_value_change? : @cmd.Emit[Array[Int]],
on_value_commit? : @cmd.Emit[Array[Int]],
class? : String,
title? : String,
attrs? : @html.Attrs,
input_attrs? : @html.Attrs,
style? : Array[String] = [],
) -> @rabbita.Val[@html.Html] {
slider_values_impl(
default_values, min, max, step, large_step, min_steps_between_values, orientation,
disabled, aria_label, on_value_change, on_value_commit, id, false, class, title,
attrs, input_attrs, style,
)
}
///|
fn slider_single_notify(
notify : @cmd.Emit[Int]?,
fallback : Int,
) -> @cmd.Emit[Array[Int]]? {
match notify {
Some(notify) =>
Some(
@cmd.Emit(values => {
notify(if values.length() == 0 { fallback } else { values[0] })
}),
)
None => None
}
}
///|
pub fn slider(
id~ : String,
default_value? : Int = 0,
min? : Int = 0,
max? : Int = 100,
step? : Int = 1,
large_step? : Int = 10,
orientation? : SeparatorOrientation = Horizontal,
disabled? : Bool = false,
aria_label? : String = "Value",
on_value_change? : @cmd.Emit[Int],
on_value_commit? : @cmd.Emit[Int],
class? : String,
title? : String,
attrs? : @html.Attrs,
input_attrs? : @html.Attrs,
style? : Array[String] = [],
) -> @rabbita.Val[@html.Html] {
let fallback = slider_clamp(default_value, min, max)
slider_values_impl(
[default_value],
min,
max,
step,
large_step,
0,
orientation,
disabled,
aria_label,
slider_single_notify(on_value_change, fallback),
slider_single_notify(on_value_commit, fallback),
id,
true,
class,
title,
attrs,
input_attrs,
style,
)
}