//! 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, /// 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>, _available: taffy::Size, ) -> taffy::Size { 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 { 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;