indicator.rs raw

//! Controls that show rather than take: a rule between things, and a bar
//! showing how far along something is.
//!
//! Neither is focusable and neither reports anything. They are here together
//! because they are the same shape of widget — a little geometry, a couple of
//! theme tokens, and an accessibility role that says what the shape means.

use guiduck_scene::Fragment;
use guiduck_scene::geom::{Rect, Size};
use guiduck_scene::paint::{Brush, Color};
use taffy::AvailableSpace;

use super::Widget;
use crate::dirty::Dirt;
use crate::style::{ComputedStyle, StyleReader};
use crate::text::TextContext;

/// A rule between things.
///
/// Its thickness is the theme's; its length is the layout's, so it stretches
/// along whichever axis its container runs. A `menu-separator` is the same
/// idea confined to a menu, which is why this one exists separately: that one
/// may only appear inside a menu, and most rules are not in menus.
#[derive(Default)]
pub struct Separator {
    tokens: RuleTokens,
    dirt: Dirt,
}

impl Separator {
    pub fn new() -> Self {
        Self::default()
    }
}

struct RuleTokens {
    color: Brush,
    thickness: f64,
}

impl RuleTokens {
    fn blank() -> Self {
        Self {
            color: Color::TRANSPARENT.into(),
            thickness: 0.0,
        }
    }

    fn read(&mut self, style: &ComputedStyle) -> bool {
        let mut reader = StyleReader::new(style);
        reader.brush(&mut self.color, "color");
        reader.number(&mut self.thickness, "thickness");
        reader.changed()
    }
}

impl Default for RuleTokens {
    fn default() -> Self {
        let mut tokens = Self::blank();
        tokens.read(&super::fallback_style("separator"));
        tokens
    }
}

impl Widget for Separator {
    fn measure(
        &mut self,
        _text: &mut TextContext,
        known: taffy::Size<Option<f32>>,
        _available: taffy::Size<AvailableSpace>,
    ) -> taffy::Size<f32> {
        // Thick in one direction and led by the layout in the other: a rule
        // in a column is a horizontal line the width of the column, and the
        // same widget in a row is a vertical one.
        taffy::Size {
            width: known.width.unwrap_or(self.tokens.thickness as f32),
            height: known.height.unwrap_or(self.tokens.thickness as f32),
        }
    }

    fn paint(&mut self, fragment: &mut Fragment, size: Size) {
        fragment.fill(
            Rect::new(0.0, 0.0, size.width, size.height),
            self.tokens.color.clone(),
        );
    }

    fn role(&self) -> accesskit::Role {
        accesskit::Role::Splitter
    }

    fn take_dirt(&mut self) -> Dirt {
        std::mem::take(&mut self.dirt)
    }

    fn type_name(&self) -> &'static str {
        "separator"
    }

    fn apply_style(&mut self, style: &ComputedStyle) {
        if self.tokens.read(style) {
            self.dirt.mark_layout();
        }
    }
}

/// How far along something is, as a bar.
///
/// `:value` is a fraction from 0 to 1, clamped — a progress bar that has gone
/// past its end is a bug in the arithmetic, and drawing it that way would hide
/// the bug rather than the overflow.
#[derive(Default)]
pub struct Progress {
    value: f64,
    tokens: ProgressTokens,
    dirt: Dirt,
}

impl Progress {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn value(mut self, value: f64) -> Self {
        self.set_value(value);
        self
    }

    /// Equality-gated, like every setter: a binding that re-writes the same
    /// fraction costs no repaint.
    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
    }
}

struct ProgressTokens {
    track: Brush,
    fill: Brush,
    corner_radius: f64,
    thickness: f64,
}

impl ProgressTokens {
    fn blank() -> Self {
        Self {
            track: Color::TRANSPARENT.into(),
            fill: Color::TRANSPARENT.into(),
            corner_radius: 0.0,
            thickness: 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.number(&mut self.corner_radius, "corner-radius");
        reader.number(&mut self.thickness, "thickness");
        reader.changed()
    }
}

impl Default for ProgressTokens {
    fn default() -> Self {
        let mut tokens = Self::blank();
        tokens.read(&super::fallback_style("progress"));
        tokens
    }
}

impl Widget for Progress {
    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.thickness as f32),
        }
    }

    fn paint(&mut self, fragment: &mut Fragment, size: Size) {
        let track = Rect::new(0.0, 0.0, size.width, size.height);
        fragment.fill(
            track.to_rounded_rect(self.tokens.corner_radius),
            self.tokens.track.clone(),
        );
        let filled = size.width * self.value;
        if filled > 0.0 {
            fragment.fill(
                Rect::new(0.0, 0.0, filled, size.height).to_rounded_rect(self.tokens.corner_radius),
                self.tokens.fill.clone(),
            );
        }
    }

    fn role(&self) -> accesskit::Role {
        accesskit::Role::ProgressIndicator
    }

    fn accessibility(&self, node: &mut accesskit::Node) {
        // Announced as a percentage, which is what the fraction means and
        // what a screen reader reads out.
        node.set_numeric_value(self.value * 100.0);
        node.set_min_numeric_value(0.0);
        node.set_max_numeric_value(100.0);
    }

    fn take_dirt(&mut self) -> Dirt {
        std::mem::take(&mut self.dirt)
    }

    fn type_name(&self) -> &'static str {
        "progress"
    }

    fn apply_style(&mut self, style: &ComputedStyle) {
        if self.tokens.read(style) {
            self.dirt.mark_layout();
        }
    }
}

#[cfg(test)]
mod tests;