use blinc_core::Color;
use blinc_core::reactive::{Computed, ReactiveGraph, computed};
use blinc_layout::binding::{IntoReactive, Reactive};
use blinc_layout::prelude::*;
use std::sync::Arc;
const INDETERMINATE_SHARE: f32 = 0.3;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ProgressSize {
Small,
#[default]
Medium,
Large,
}
impl ProgressSize {
fn height(&self) -> f32 {
match self {
ProgressSize::Small => 4.0,
ProgressSize::Medium => 8.0,
ProgressSize::Large => 12.0,
}
}
fn attr(&self) -> &'static str {
match self {
ProgressSize::Small => "sm",
ProgressSize::Medium => "md",
ProgressSize::Large => "lg",
}
}
}
#[derive(Clone)]
struct ProgressConfig {
value: Option<Reactive<f32>>,
size: ProgressSize,
width: f32,
indicator_color: Option<Color>,
track_color: Option<Color>,
corner_radius: Option<f32>,
}
impl ProgressConfig {
fn new(value: Option<Reactive<f32>>) -> Self {
Self {
value,
size: ProgressSize::default(),
width: 200.0,
indicator_color: None,
track_color: None,
corner_radius: None,
}
}
}
fn share(percent: f32) -> f32 {
if percent.is_finite() {
(percent / 100.0).clamp(0.0, 1.0)
} else {
0.0
}
}
fn bound_share(value: Reactive<f32>) -> Reactive<f32> {
match value {
Reactive::Const(v) => Reactive::Const(share(v)),
Reactive::Bound(state) => {
let signal = state.signal();
Reactive::Computed(computed(move |g: &ReactiveGraph| {
share(g.get(signal).unwrap_or(0.0))
}))
}
Reactive::Computed(source) => {
let id = source.derived_id();
let graph = source.graph();
Reactive::Computed(Computed::mapped(
id,
Arc::new(move || source.try_get().map(share)),
graph,
))
}
}
}
pub struct Progress {
inner: Div,
}
impl Progress {
fn from_config(config: ProgressConfig) -> Self {
let height = config.size.height();
let mut bar = div().class("cn-progress-bar").w(0.0).h_full();
let mut track = div()
.class("cn-progress")
.attr("data-size", config.size.attr())
.flex_row()
.w(config.width)
.h(height)
.overflow_clip();
bar = match config.value {
Some(value) => bar.flex_grow_value(bound_share(value)),
None => {
track = track.attr("data-indeterminate", "");
bar.flex_grow_value(INDETERMINATE_SHARE)
}
};
if let Some(color) = config.indicator_color {
bar = bar.bg(color);
}
if let Some(color) = config.track_color {
track = track.bg(color);
}
if let Some(radius) = config.corner_radius {
bar = bar.rounded(radius);
track = track.rounded(radius);
}
Self {
inner: track.child(bar),
}
}
pub fn class(mut self, name: impl AsRef<str>) -> Self {
self.inner = self.inner.class(name);
self
}
pub fn id(mut self, id: &str) -> Self {
self.inner = self.inner.id(id);
self
}
}
impl ElementBuilder for Progress {
fn build(&self, tree: &mut blinc_layout::tree::LayoutTree) -> blinc_layout::tree::LayoutNodeId {
self.inner.build(tree)
}
fn render_props(&self) -> blinc_layout::element::RenderProps {
self.inner.render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.inner.children_builders()
}
fn element_type_id(&self) -> blinc_layout::div::ElementTypeId {
self.inner.element_type_id()
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.inner.layout_style()
}
fn element_id(&self) -> Option<&str> {
ElementBuilder::element_id(&self.inner)
}
fn element_classes(&self) -> &[std::sync::Arc<str>] {
self.inner.element_classes()
}
}
pub struct ProgressBuilder {
config: ProgressConfig,
built: std::cell::OnceCell<Progress>,
}
impl ProgressBuilder {
pub fn new(value: impl IntoReactive<f32>) -> Self {
Self::with_value(Some(value.into_reactive()))
}
fn with_value(value: Option<Reactive<f32>>) -> Self {
Self {
config: ProgressConfig::new(value),
built: std::cell::OnceCell::new(),
}
}
fn get_or_build(&self) -> &Progress {
::blinc_layout::build_once::build_once(&self.built, || {
Progress::from_config(self.config.clone())
})
}
pub fn size(mut self, size: ProgressSize) -> Self {
self.config.size = size;
self
}
pub fn w(mut self, width: f32) -> Self {
self.config.width = width;
self
}
pub fn indicator_color(mut self, color: impl Into<Color>) -> Self {
self.config.indicator_color = Some(color.into());
self
}
pub fn track_color(mut self, color: impl Into<Color>) -> Self {
self.config.track_color = Some(color.into());
self
}
pub fn rounded(mut self, radius: f32) -> Self {
self.config.corner_radius = Some(radius);
self
}
pub fn build_component(self) -> Progress {
Progress::from_config(self.config)
}
}
impl ElementBuilder for ProgressBuilder {
fn build(&self, tree: &mut blinc_layout::tree::LayoutTree) -> blinc_layout::tree::LayoutNodeId {
self.get_or_build().build(tree)
}
fn render_props(&self) -> blinc_layout::element::RenderProps {
self.get_or_build().render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.get_or_build().children_builders()
}
fn element_type_id(&self) -> blinc_layout::div::ElementTypeId {
self.get_or_build().element_type_id()
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.get_or_build().layout_style()
}
fn element_id(&self) -> Option<&str> {
self.get_or_build().element_id()
}
fn element_classes(&self) -> &[std::sync::Arc<str>] {
self.get_or_build().element_classes()
}
}
pub fn progress(value: impl IntoReactive<f32>) -> ProgressBuilder {
ProgressBuilder::new(value)
}
pub fn progress_indeterminate() -> ProgressBuilder {
ProgressBuilder::with_value(None)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_progress_size_heights() {
assert_eq!(ProgressSize::Small.height(), 4.0);
assert_eq!(ProgressSize::Medium.height(), 8.0);
assert_eq!(ProgressSize::Large.height(), 12.0);
}
#[test]
fn a_value_is_its_share_of_the_track_within_the_bounds() {
assert_eq!(share(50.0), 0.5);
assert_eq!(share(150.0), 1.0);
assert_eq!(share(-10.0), 0.0);
assert_eq!(share(f32::NAN), 0.0);
}
}