stepper.rs
raw
//! 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<String>,
layout: Option<Layout<Brush>>,
tokens: Tokens,
dirt: Dirt,
emitted: Vec<EventData>,
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<String>) {
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<Brush> {
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<Brush> = 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<Option<f32>>,
_available: taffy::Size<AvailableSpace>,
) -> taffy::Size<f32> {
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<EventData> {
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;