//! A stepper: a number you nudge up and down. //! //! Unlike [`Slider`](super::Slider) this one carries a real range, because the //! number it shows is the number the application means — there is nothing to //! scale, and rendering "0.42" when the value is 42 would be a lie. So //! `:value`, `:min`, `:max`, and `:step` are all its own. //! //! Controlled like every value here: a programmatic set is silent, user input //! reports [`EventData::ValueChanged`](crate::event::EventData::ValueChanged). use guiduck_scene::Fragment; use guiduck_scene::geom::{Point, Rect, Size, Stroke}; use guiduck_scene::paint::{Brush, Color}; use parley::{Alignment, AlignmentOptions, Layout, StyleProperty}; 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; pub struct Stepper { value: f64, min: f64, max: f64, step: f64, font_size: f32, family: Option, layout: Option>, tokens: Tokens, dirt: Dirt, emitted: Vec, focused: bool, width: f64, } impl Default for Stepper { fn default() -> Self { Self { value: 0.0, min: 0.0, max: 100.0, step: 1.0, font_size: 14.0, family: None, layout: None, tokens: Tokens::default(), dirt: Dirt::CLEAN, emitted: Vec::new(), focused: false, width: 0.0, } } } impl Stepper { pub fn new() -> Self { Self::default() } pub fn value(mut self, value: f64) -> Self { self.set_value(value); self } pub fn set_value(&mut self, value: f64) { let value = self.clamped(value); if self.value != value { self.value = value; self.layout = None; self.dirt.mark_layout(); } } pub fn get_value(&self) -> f64 { self.value } pub fn set_min(&mut self, min: f64) { if self.min != min { self.min = min; self.set_value(self.value); } } pub fn set_max(&mut self, max: f64) { if self.max != max { self.max = max; self.set_value(self.value); } } pub fn set_step(&mut self, step: f64) { // A step of zero would make the arrows do nothing for ever; the // sensible reading of "no step given" is one. let step = if step > 0.0 { step } else { 1.0 }; if self.step != step { self.step = step; } } pub fn set_font_size(&mut self, size: f32) { if self.font_size != size { self.font_size = size; self.layout = None; self.dirt.mark_layout(); } } pub fn set_family(&mut self, family: impl Into) { let family = Some(family.into()); if self.family != family { self.family = family; self.layout = None; self.dirt.mark_layout(); } } fn clamped(&self, value: f64) -> f64 { if self.max >= self.min { value.clamp(self.min, self.max) } else { value } } /// Nudge under user input: move by whole steps from the minimum, so a /// range of 0..10 stepping by 3 offers 0, 3, 6, 9 rather than drifting off /// the grid after the first clamp. fn nudge(&mut self, steps: f64) { let target = self.clamped(self.value + steps * self.step); if self.value == target { return; } self.value = target; self.layout = None; self.emitted.push(EventData::ValueChanged(target)); self.dirt.mark_layout(); } fn text(&self) -> String { // Whole numbers read as whole numbers; a fractional step keeps enough // places to show what it did. if self.step.fract() == 0.0 && self.value.fract() == 0.0 { format!("{}", self.value as i64) } else { format!("{:.2}", self.value) } } fn layout_at(&mut self, text_cx: &mut TextContext) -> &Layout { if self.layout.is_none() { let text = self.text(); let mut builder = text_cx .layout_cx .ranged_builder(&mut text_cx.font_cx, &text, 1.0, true); if let Some(family) = &self.family { builder.push_default(StyleProperty::FontFamily(crate::text::font_family(family))); } builder.push_default(StyleProperty::FontSize(self.font_size)); builder.push_default(StyleProperty::Brush(self.tokens.color.clone())); let mut layout: Layout = builder.build(&text); layout.break_all_lines(None); layout.align(Alignment::Start, AlignmentOptions::default()); self.layout = Some(layout); } self.layout.as_ref().expect("just built") } } struct Tokens { background: Brush, color: Brush, border_color: Brush, border_width: f64, corner_radius: f64, button_color: Brush, focus_ring_color: Brush, focus_ring_width: f64, } impl Tokens { fn blank() -> Self { Self { background: Color::TRANSPARENT.into(), color: Color::TRANSPARENT.into(), border_color: Color::TRANSPARENT.into(), border_width: 0.0, corner_radius: 0.0, button_color: Color::TRANSPARENT.into(), focus_ring_color: 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.background, "background"); reader.brush(&mut self.color, "color"); reader.brush(&mut self.border_color, "border-color"); reader.number(&mut self.border_width, "border-width"); reader.number(&mut self.corner_radius, "corner-radius"); reader.brush(&mut self.button_color, "button-color"); reader.brush(&mut self.focus_ring_color, "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("stepper")); tokens } } /// Width of the up/down button column. const BUTTON_W: f64 = 18.0; const PAD: f64 = 6.0; impl Widget for Stepper { fn measure( &mut self, text: &mut TextContext, known: taffy::Size>, _available: taffy::Size, ) -> taffy::Size { let layout = self.layout_at(text); let (w, h) = (layout.width() as f64, layout.height() as f64); taffy::Size { width: known.width.unwrap_or((w + PAD * 2.0 + BUTTON_W) as f32), height: known.height.unwrap_or((h + PAD) 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 bounds = Rect::new(0.0, 0.0, size.width, size.height); if self.focused && !is_transparent(&self.tokens.focus_ring_color) { let ring = bounds.inflate(self.tokens.focus_ring_width, self.tokens.focus_ring_width); fragment.fill( ring.to_rounded_rect(self.tokens.corner_radius + self.tokens.focus_ring_width), self.tokens.focus_ring_color.clone(), ); } fragment.fill( bounds.to_rounded_rect(self.tokens.corner_radius), self.tokens.background.clone(), ); if self.tokens.border_width > 0.0 { fragment.stroke( bounds.to_rounded_rect(self.tokens.corner_radius), self.tokens.border_color.clone(), Stroke::new(self.tokens.border_width), ); } // Two little triangles standing for up and down. let x = size.width - BUTTON_W / 2.0; for (dir, cy) in [(-1.0, size.height * 0.3), (1.0, size.height * 0.7)] { let mut arrow = guiduck_scene::geom::BezPath::new(); arrow.move_to((x - 4.0, cy - 2.0 * dir)); arrow.line_to((x + 4.0, cy - 2.0 * dir)); arrow.line_to((x, cy + 3.0 * dir)); arrow.close_path(); fragment.fill(arrow, self.tokens.button_color.clone()); } if let Some(layout) = &self.layout { let y = (size.height - layout.height() as f64) / 2.0; crate::text::append_layout(fragment, layout, Point::new(PAD, y)); } } fn role(&self) -> accesskit::Role { accesskit::Role::SpinButton } fn accessibility(&self, node: &mut accesskit::Node) { node.set_numeric_value(self.value); node.set_min_numeric_value(self.min); node.set_max_numeric_value(self.max); node.set_value(self.text()); } 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 steps = match key.key { Key::Up => 1.0, Key::Down => -1.0, Key::PageUp => 10.0, Key::PageDown => -10.0, _ => return false, }; let before = self.value; self.nudge(steps); self.value != before } fn on_pointer( &mut self, kind: EventKind, event: &PointerEvent, _text: &mut TextContext, _clipboard: &mut dyn crate::clipboard::Clipboard, ) -> bool { if kind != EventKind::PointerDown || event.local.x < self.width - BUTTON_W { return false; } // Top half of the button column steps up, bottom half steps down. let midpoint = self .layout .as_ref() .map_or(0.0, |l| l.height() as f64) .max(4.0); let up = event.local.y < midpoint; self.nudge(if up { 1.0 } else { -1.0 }); true } 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 { "stepper" } fn apply_style(&mut self, style: &ComputedStyle) { if self.tokens.read(style) { self.layout = None; self.dirt.mark_layout(); } } } #[cfg(test)] mod tests;