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;