#![warn(missing_docs)]
use std::panic::Location;
use crate::cursor::Cursor;
use crate::event::{NamedKey, UiEvent, UiEventKind};
use crate::layout::LayoutCtx;
use crate::metrics::MetricsRole;
use crate::tokens;
use crate::tree::*;
pub const TRACK_HEIGHT: f32 = 10.0;
pub const THUMB_SIZE: f32 = 14.0;
pub const DEFAULT_HEIGHT: f32 = 18.0;
#[track_caller]
pub fn slider(key: impl Into<String>, value: f32) -> El {
slider_with_color(key, value, tokens::PRIMARY)
}
#[track_caller]
pub fn slider_with_color(key: impl Into<String>, value: f32, fill_color: Color) -> El {
let value = value.clamp(0.0, 1.0);
let layout = move |ctx: LayoutCtx| {
let rect = ctx.container;
let usable = (rect.w - THUMB_SIZE).max(1.0);
let track_x = rect.x + THUMB_SIZE * 0.5;
let track_y = rect.y + (rect.h - TRACK_HEIGHT) * 0.5;
let thumb_x = rect.x + value * usable;
let thumb_y = rect.y + (rect.h - THUMB_SIZE) * 0.5;
vec![
Rect::new(track_x, track_y, usable, TRACK_HEIGHT),
Rect::new(track_x, track_y, value * usable, TRACK_HEIGHT),
Rect::new(thumb_x, thumb_y, THUMB_SIZE, THUMB_SIZE),
]
};
El::new(Kind::Custom("slider"))
.axis(Axis::Overlay)
.children([
El::new(Kind::Custom("slider-track"))
.height(Size::Fixed(TRACK_HEIGHT))
.width(Size::Fill(1.0))
.fill(tokens::MUTED)
.radius(tokens::RADIUS_PILL),
El::new(Kind::Custom("slider-fill"))
.height(Size::Fixed(TRACK_HEIGHT))
.width(Size::Fill(1.0))
.fill(fill_color)
.radius(tokens::RADIUS_PILL),
El::new(Kind::Custom("slider-thumb"))
.width(Size::Fixed(THUMB_SIZE))
.height(Size::Fixed(THUMB_SIZE))
.fill(tokens::FOREGROUND)
.stroke(tokens::BORDER)
.radius(tokens::RADIUS_PILL)
.state_follows_interactive_ancestor(),
])
.at_loc(Location::caller())
.key(key)
.metrics_role(MetricsRole::Slider)
.focusable()
.cursor(Cursor::Grab)
.cursor_pressed(Cursor::Grabbing)
.consumes_touch_drag()
.layout(layout)
.default_height(Size::Fixed(DEFAULT_HEIGHT))
.paint_overflow(Sides::all(tokens::RING_WIDTH))
.hit_overflow(Sides::all(tokens::HIT_OVERFLOW))
.width(Size::Fill(1.0))
}
pub fn normalized_from_event(rect: Rect, x: f32) -> f32 {
let usable = (rect.w - THUMB_SIZE).max(1.0);
let local = x - rect.x - THUMB_SIZE * 0.5;
(local / usable).clamp(0.0, 1.0)
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum SliderAction {
Step(f32),
Set(f32),
}
pub fn classify_event(
event: &UiEvent,
key: &str,
step: f32,
page_step: f32,
) -> Option<SliderAction> {
if event.kind != UiEventKind::KeyDown || event.route() != Some(key) {
return None;
}
let press = event.key_press.as_ref()?;
Some(match press.logical.named() {
Some(NamedKey::ArrowUp | NamedKey::ArrowRight) => SliderAction::Step(step),
Some(NamedKey::ArrowDown | NamedKey::ArrowLeft) => SliderAction::Step(-step),
Some(NamedKey::PageUp) => SliderAction::Step(page_step),
Some(NamedKey::PageDown) => SliderAction::Step(-page_step),
Some(NamedKey::Home) => SliderAction::Set(0.0),
Some(NamedKey::End) => SliderAction::Set(1.0),
_ => return None,
})
}
pub fn apply_event(value: &mut f32, event: &UiEvent, key: &str, step: f32, page_step: f32) -> bool {
let pointer_kind = matches!(
event.kind,
UiEventKind::Click | UiEventKind::PointerDown | UiEventKind::Drag,
);
if pointer_kind
&& event.route() == Some(key)
&& let (Some(rect), Some(x)) = (event.target_rect(), event.pointer_x())
{
let prev = *value;
*value = normalized_from_event(rect, x);
return *value != prev;
}
let Some(action) = classify_event(event, key, step, page_step) else {
return false;
};
let prev = *value;
let next = match action {
SliderAction::Step(d) => *value + d,
SliderAction::Set(v) => v,
};
*value = next.clamp(0.0, 1.0);
*value != prev
}
#[deprecated(
since = "0.4.4",
note = "renamed to `slider::apply_event`, which now folds pointer and key events"
)]
pub fn apply_input(value: &mut f32, event: &UiEvent, key: &str, step: f32, page_step: f32) -> bool {
apply_event(value, event, key, step, page_step)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{KeyModifiers, KeyPress, LogicalKey, PhysicalKey, UiTarget};
fn key_event(key: &str, ui_key: LogicalKey) -> UiEvent {
UiEvent {
path: None,
key: Some(key.to_string()),
target: Some(UiTarget {
key: key.to_string(),
node_id: format!("/{key}").into(),
rect: Rect::new(0.0, 0.0, 100.0, 20.0),
tooltip: None,
scroll_offset_y: 0.0,
}),
pointer: None,
key_press: Some(KeyPress {
logical: ui_key,
physical: PhysicalKey::Unidentified,
modifiers: KeyModifiers::default(),
repeat: false,
}),
text: None,
selection: None,
modifiers: KeyModifiers::default(),
click_count: 0,
pointer_kind: None,
wheel_delta: None,
kind: UiEventKind::KeyDown,
}
}
#[test]
fn apply_event_steps_and_clamps() {
let mut value = 0.5;
assert!(apply_event(
&mut value,
&key_event("vol", LogicalKey::Named(NamedKey::ArrowUp)),
"vol",
0.1,
0.25
));
assert!((value - 0.6).abs() < 1e-6);
assert!(apply_event(
&mut value,
&key_event("vol", LogicalKey::Named(NamedKey::ArrowDown)),
"vol",
0.1,
0.25
));
assert!((value - 0.5).abs() < 1e-6);
assert!(apply_event(
&mut value,
&key_event("vol", LogicalKey::Named(NamedKey::PageUp)),
"vol",
0.1,
0.25
));
assert!((value - 0.75).abs() < 1e-6);
assert!(apply_event(
&mut value,
&key_event("vol", LogicalKey::Named(NamedKey::Home)),
"vol",
0.1,
0.25
));
assert_eq!(value, 0.0);
assert!(apply_event(
&mut value,
&key_event("vol", LogicalKey::Named(NamedKey::End)),
"vol",
0.1,
0.25
));
assert_eq!(value, 1.0);
assert!(!apply_event(
&mut value,
&key_event("vol", LogicalKey::Named(NamedKey::ArrowUp)),
"vol",
0.1,
0.25
));
assert_eq!(value, 1.0);
}
#[test]
fn apply_event_ignores_unrouted_or_unrelated_keys() {
let mut value = 0.5;
assert!(!apply_event(
&mut value,
&key_event("other", LogicalKey::Named(NamedKey::ArrowUp)),
"vol",
0.1,
0.25
));
assert_eq!(value, 0.5);
assert!(!apply_event(
&mut value,
&key_event("vol", LogicalKey::Named(NamedKey::Tab)),
"vol",
0.1,
0.25
));
assert_eq!(value, 0.5);
}
#[test]
fn classify_left_right_mirrors_up_down() {
assert_eq!(
classify_event(
&key_event("k", LogicalKey::Named(NamedKey::ArrowRight)),
"k",
0.1,
0.25
),
Some(SliderAction::Step(0.1)),
);
assert_eq!(
classify_event(
&key_event("k", LogicalKey::Named(NamedKey::ArrowLeft)),
"k",
0.1,
0.25
),
Some(SliderAction::Step(-0.1)),
);
}
#[test]
fn thumb_borrows_state_envelopes_from_focusable_container() {
let s = slider("demo-slider-thumb", 0.5);
assert!(s.focusable, "container is the focusable / hit target");
let thumb = s
.children
.iter()
.find(|c| matches!(&c.kind, Kind::Custom(name) if *name == "slider-thumb"))
.expect("thumb child");
assert!(
thumb.state_follows_interactive_ancestor,
"thumb borrows hover / press from the slider container",
);
for c in &s.children {
if let Kind::Custom(name) = &c.kind
&& (*name == "slider-track" || *name == "slider-fill")
{
assert!(!c.state_follows_interactive_ancestor);
}
}
}
#[test]
fn slider_declares_grab_at_rest_and_grabbing_while_pressed() {
let s = slider("demo-slider-cursor", 0.5);
assert_eq!(s.cursor, Some(Cursor::Grab));
assert_eq!(s.cursor_pressed, Some(Cursor::Grabbing));
}
#[test]
fn normalized_tracks_thumb_center() {
let rect = Rect::new(10.0, 20.0, 220.0, DEFAULT_HEIGHT);
let left = rect.x + THUMB_SIZE * 0.5;
let usable = rect.w - THUMB_SIZE;
assert_eq!(normalized_from_event(rect, left), 0.0);
assert!((normalized_from_event(rect, left + usable * 0.5) - 0.5).abs() < 1e-6);
assert_eq!(normalized_from_event(rect, left + usable), 1.0);
assert_eq!(normalized_from_event(rect, rect.x - 30.0), 0.0);
assert_eq!(normalized_from_event(rect, rect.x + rect.w + 30.0), 1.0);
}
fn pointer_event(key: &str, kind: UiEventKind, rect: Rect, x: f32) -> UiEvent {
UiEvent {
path: None,
key: Some(key.to_string()),
target: Some(UiTarget {
key: key.to_string(),
node_id: format!("/{key}").into(),
rect,
tooltip: None,
scroll_offset_y: 0.0,
}),
pointer: Some((x, rect.y + rect.h * 0.5)),
key_press: None,
text: None,
selection: None,
modifiers: KeyModifiers::default(),
click_count: 0,
pointer_kind: None,
wheel_delta: None,
kind,
}
}
#[test]
fn apply_event_handles_pointer_drag() {
let rect = Rect::new(10.0, 20.0, 220.0, DEFAULT_HEIGHT);
let usable = rect.w - THUMB_SIZE;
let mid_x = rect.x + THUMB_SIZE * 0.5 + usable * 0.5;
let mut value = 0.0;
assert!(apply_event(
&mut value,
&pointer_event("vol", UiEventKind::Drag, rect, mid_x),
"vol",
0.1,
0.25
));
assert!((value - 0.5).abs() < 1e-6);
let right = rect.x + THUMB_SIZE * 0.5 + usable;
assert!(apply_event(
&mut value,
&pointer_event("vol", UiEventKind::Click, rect, right),
"vol",
0.1,
0.25
));
assert_eq!(value, 1.0);
assert!(!apply_event(
&mut value,
&pointer_event("vol", UiEventKind::Click, rect, right),
"vol",
0.1,
0.25
));
}
#[test]
fn apply_event_handles_keyboard_after_pointer() {
let mut value = 0.5;
assert!(apply_event(
&mut value,
&key_event("vol", LogicalKey::Named(NamedKey::ArrowUp)),
"vol",
0.1,
0.25
));
assert!((value - 0.6).abs() < 1e-6);
}
#[test]
fn apply_event_ignores_pointer_events_for_other_keys() {
let rect = Rect::new(0.0, 0.0, 200.0, DEFAULT_HEIGHT);
let mut value = 0.5;
assert!(!apply_event(
&mut value,
&pointer_event("other", UiEventKind::Drag, rect, 100.0),
"vol",
0.1,
0.25
));
assert_eq!(value, 0.5);
}
#[test]
#[allow(deprecated)]
fn deprecated_apply_input_still_forwards() {
let rect = Rect::new(10.0, 20.0, 220.0, DEFAULT_HEIGHT);
let usable = rect.w - THUMB_SIZE;
let mid_x = rect.x + THUMB_SIZE * 0.5 + usable * 0.5;
let mut value = 0.0;
assert!(apply_input(
&mut value,
&pointer_event("vol", UiEventKind::Drag, rect, mid_x),
"vol",
0.1,
0.25
));
assert!((value - 0.5).abs() < 1e-6);
}
}