slider.rs
raw
//! A slider: a value you drag or arrow along a track.
//!
//! `:value` is a fraction from 0 to 1, and the application scales it to
//! whatever it means. That is a decision rather than an omission: a slider
//! carrying its own minimum, maximum, and step would be doing arithmetic the
//! application is already doing, in units the framework cannot check — and the
//! fraction is what the accessibility layer wants anyway, as a percentage.
//!
//! Controlled, like every value in this framework: dragging sets the widget's
//! own position optimistically and reports
//! [`EventData::ValueChanged`](crate::event::EventData::ValueChanged); the
//! binding is what confirms it, and a programmatic set is silent so a binding
//! cannot loop.
use guiduck_scene::Fragment;
use guiduck_scene::geom::{Point, Rect, Size};
use guiduck_scene::paint::{Brush, Color};
use taffy::AvailableSpace;
use super::{Widget, is_transparent};
use crate::dirty::Dirt;
use crate::event::{EventData, EventKind, Key, KeyInput, PointerEvent};
use crate::style::{ComputedStyle, StyleReader};
use crate::text::TextContext;
use crate::widget::CursorShape;
/// How far an arrow key moves the value, as a fraction of the whole.
const ARROW_STEP: f64 = 0.05;
pub struct Slider {
value: f64,
tokens: Tokens,
dirt: Dirt,
emitted: Vec<EventData>,
/// The width of the last layout, so a drag can turn a position into a
/// fraction without asking the tree where it is.
width: f64,
focused: bool,
}
impl Default for Slider {
fn default() -> Self {
Self {
value: 0.0,
tokens: Tokens::default(),
dirt: Dirt::CLEAN,
emitted: Vec::new(),
width: 0.0,
focused: false,
}
}
}
impl Slider {
pub fn new() -> Self {
Self::default()
}
pub fn value(mut self, value: f64) -> Self {
self.set_value(value);
self
}
/// Set the position without reporting it — the controlled contract, so a
/// `:value` binding writing back what the user just did cannot loop.
pub fn set_value(&mut self, value: f64) {
let value = value.clamp(0.0, 1.0);
if self.value != value {
self.value = value;
self.dirt.mark_paint();
}
}
pub fn get_value(&self) -> f64 {
self.value
}
/// Move under user input: set the position and say so.
fn move_to(&mut self, value: f64) {
let value = value.clamp(0.0, 1.0);
if self.value == value {
return;
}
self.value = value;
self.emitted.push(EventData::ValueChanged(value));
self.dirt.mark_paint();
}
/// The fraction a point along the track stands for. The thumb has width,
/// so the travel is shorter than the track: without this the value would
/// never quite reach either end.
fn fraction_at(&self, x: f64) -> f64 {
let travel = (self.width - self.tokens.thumb_size).max(f64::EPSILON);
((x - self.tokens.thumb_size / 2.0) / travel).clamp(0.0, 1.0)
}
}
struct Tokens {
track: Brush,
fill: Brush,
thumb: Brush,
thumb_size: f64,
thickness: f64,
corner_radius: f64,
focus_ring: Brush,
focus_ring_width: f64,
}
impl Tokens {
fn blank() -> Self {
Self {
track: Color::TRANSPARENT.into(),
fill: Color::TRANSPARENT.into(),
thumb: Color::TRANSPARENT.into(),
thumb_size: 0.0,
thickness: 0.0,
corner_radius: 0.0,
focus_ring: Color::TRANSPARENT.into(),
focus_ring_width: 0.0,
}
}
fn read(&mut self, style: &ComputedStyle) -> bool {
let mut reader = StyleReader::new(style);
reader.brush(&mut self.track, "track-color");
reader.brush(&mut self.fill, "fill-color");
reader.brush(&mut self.thumb, "thumb-color");
reader.number(&mut self.thumb_size, "thumb-size");
reader.number(&mut self.thickness, "thickness");
reader.number(&mut self.corner_radius, "corner-radius");
reader.brush(&mut self.focus_ring, "focus-ring-color");
reader.number(&mut self.focus_ring_width, "focus-ring-width");
reader.changed()
}
}
impl Default for Tokens {
fn default() -> Self {
let mut tokens = Self::blank();
tokens.read(&super::fallback_style("slider"));
tokens
}
}
impl Widget for Slider {
fn measure(
&mut self,
_text: &mut TextContext,
known: taffy::Size<Option<f32>>,
_available: taffy::Size<AvailableSpace>,
) -> taffy::Size<f32> {
taffy::Size {
width: known.width.unwrap_or(0.0),
height: known.height.unwrap_or(self.tokens.thumb_size as f32),
}
}
fn finalize_layout(&mut self, _text: &mut TextContext, size: Size, _content: Size) {
self.width = size.width;
}
fn paint(&mut self, fragment: &mut Fragment, size: Size) {
let thumb = self.tokens.thumb_size;
let mid = size.height / 2.0;
let half = self.tokens.thickness / 2.0;
let track = Rect::new(0.0, mid - half, size.width, mid + half);
fragment.fill(
track.to_rounded_rect(self.tokens.corner_radius),
self.tokens.track.clone(),
);
// The thumb's centre travels between the two half-widths, so the
// filled part is measured to the centre rather than to the edge.
let centre = thumb / 2.0 + (size.width - thumb).max(0.0) * self.value;
if centre > 0.0 {
fragment.fill(
Rect::new(0.0, mid - half, centre, mid + half)
.to_rounded_rect(self.tokens.corner_radius),
self.tokens.fill.clone(),
);
}
let knob = Rect::new(
centre - thumb / 2.0,
mid - thumb / 2.0,
centre + thumb / 2.0,
mid + thumb / 2.0,
);
if self.focused && !is_transparent(&self.tokens.focus_ring) {
let ring = knob.inflate(self.tokens.focus_ring_width, self.tokens.focus_ring_width);
fragment.fill(
ring.to_rounded_rect(ring.height() / 2.0),
self.tokens.focus_ring.clone(),
);
}
fragment.fill(knob.to_rounded_rect(thumb / 2.0), self.tokens.thumb.clone());
}
fn role(&self) -> accesskit::Role {
accesskit::Role::Slider
}
fn accessibility(&self, node: &mut accesskit::Node) {
node.set_numeric_value(self.value * 100.0);
node.set_min_numeric_value(0.0);
node.set_max_numeric_value(100.0);
}
fn focusable(&self) -> bool {
true
}
fn on_focus_changed(&mut self, focused: bool) {
if self.focused != focused {
self.focused = focused;
self.dirt.mark_paint();
}
}
fn cursor(&self, _local: Point) -> CursorShape {
CursorShape::Pointer
}
fn on_key(
&mut self,
key: &KeyInput,
_text: &mut TextContext,
_clipboard: &mut dyn crate::clipboard::Clipboard,
) -> bool {
let target = match key.key {
Key::Left | Key::Down => self.value - ARROW_STEP,
Key::Right | Key::Up => self.value + ARROW_STEP,
Key::Home => 0.0,
Key::End => 1.0,
_ => return false,
};
// A key that cannot move it further is *not* consumed: at the end of
// the range the keystroke should still reach Tab or a shortcut, the
// same rule a scroll area follows.
let before = self.value;
self.move_to(target);
self.value != before
}
fn on_pointer(
&mut self,
kind: EventKind,
event: &PointerEvent,
_text: &mut TextContext,
_clipboard: &mut dyn crate::clipboard::Clipboard,
) -> bool {
match kind {
// A press jumps to where it landed and captures, so the drag that
// follows keeps moving the value.
EventKind::PointerDown | EventKind::PointerMove => {
self.move_to(self.fraction_at(event.local.x));
true
}
_ => false,
}
}
fn consumes_click(&self) -> bool {
true
}
fn take_dirt(&mut self) -> Dirt {
std::mem::take(&mut self.dirt)
}
fn take_emitted(&mut self) -> Vec<EventData> {
std::mem::take(&mut self.emitted)
}
fn type_name(&self) -> &'static str {
"slider"
}
fn apply_style(&mut self, style: &ComputedStyle) {
if self.tokens.read(style) {
self.dirt.mark_layout();
}
}
}
#[cfg(test)]
mod tests;