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