use blinc_animation::SharedAnimatedValue;
use blinc_core::Color;
use blinc_layout::prelude::*;
use blinc_theme::{ColorToken, RadiusToken, ThemeState};
#[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,
}
}
}
#[derive(Clone)]
struct ProgressConfig {
value: f32,
size: ProgressSize,
width: f32,
indicator_color: Option<Color>,
track_color: Option<Color>,
corner_radius: Option<f32>,
}
impl ProgressConfig {
fn new(value: f32) -> Self {
Self {
value: value.clamp(0.0, 100.0),
size: ProgressSize::default(),
width: 200.0, indicator_color: None,
track_color: None,
corner_radius: None,
}
}
}
pub struct Progress {
inner: Div,
}
impl Progress {
fn from_config(config: ProgressConfig) -> Self {
let theme = ThemeState::get();
let height = config.size.height();
let radius = config
.corner_radius
.unwrap_or_else(|| theme.radius(RadiusToken::Full));
let indicator_color = config
.indicator_color
.unwrap_or_else(|| theme.color(ColorToken::Primary));
let track_color = config
.track_color
.unwrap_or_else(|| theme.color(ColorToken::Secondary));
let fill_ratio = config.value / 100.0;
let fill_width = config.width * fill_ratio;
let size_class = match config.size {
ProgressSize::Small => "cn-progress--sm",
ProgressSize::Medium => "cn-progress--md",
ProgressSize::Large => "cn-progress--lg",
};
let indicator = div()
.class("cn-progress-bar")
.absolute()
.left(0.0)
.top(0.0)
.w(fill_width)
.h(height)
.rounded(radius)
.bg(indicator_color);
let track = div()
.class("cn-progress")
.class(size_class)
.w(config.width)
.h(height)
.rounded(radius)
.bg(track_color)
.overflow_clip()
.relative()
.child(indicator);
Self { inner: track }
}
pub fn class(mut self, name: impl Into<String>) -> 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_classes(&self) -> &[String] {
self.inner.element_classes()
}
}
pub struct ProgressBuilder {
config: ProgressConfig,
built: std::cell::OnceCell<Progress>,
}
impl ProgressBuilder {
pub fn new(value: f32) -> Self {
Self {
config: ProgressConfig::new(value),
built: std::cell::OnceCell::new(),
}
}
fn get_or_build(&self) -> &Progress {
self.built
.get_or_init(|| 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_classes(&self) -> &[String] {
self.get_or_build().element_classes()
}
}
pub fn progress(value: f32) -> ProgressBuilder {
ProgressBuilder::new(value)
}
#[derive(Clone)]
struct AnimatedProgressConfig {
value: SharedAnimatedValue,
size: ProgressSize,
width: f32,
indicator_color: Option<Color>,
track_color: Option<Color>,
corner_radius: Option<f32>,
}
impl AnimatedProgressConfig {
fn new(value: SharedAnimatedValue) -> Self {
Self {
value,
size: ProgressSize::default(),
width: 200.0,
indicator_color: None,
track_color: None,
corner_radius: None,
}
}
}
pub struct AnimatedProgress {
inner: Div,
}
impl AnimatedProgress {
fn from_config(config: AnimatedProgressConfig) -> Self {
let theme = ThemeState::get();
let height = config.size.height();
let width = config.width;
let radius = config
.corner_radius
.unwrap_or_else(|| theme.radius(RadiusToken::Full));
let indicator_color = config
.indicator_color
.unwrap_or_else(|| theme.color(ColorToken::Primary));
let track_color = config
.track_color
.unwrap_or_else(|| theme.color(ColorToken::Secondary));
let indicator = div().w(width).h(height).rounded(radius).bg(indicator_color);
let animated_indicator = motion().translate_x(config.value.clone()).child(indicator);
let positioned_wrapper = div()
.absolute()
.left(-width)
.top(0.0)
.w(width)
.h(height)
.child(animated_indicator);
let track = div()
.w(width)
.h(height)
.rounded(radius)
.bg(track_color)
.overflow_clip()
.relative()
.child(positioned_wrapper);
Self { inner: track }
}
}
impl ElementBuilder for AnimatedProgress {
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_classes(&self) -> &[String] {
self.inner.element_classes()
}
}
pub struct AnimatedProgressBuilder {
config: AnimatedProgressConfig,
built: std::cell::OnceCell<AnimatedProgress>,
}
impl AnimatedProgressBuilder {
pub fn new(value: SharedAnimatedValue) -> Self {
Self {
config: AnimatedProgressConfig::new(value),
built: std::cell::OnceCell::new(),
}
}
fn get_or_build(&self) -> &AnimatedProgress {
self.built
.get_or_init(|| AnimatedProgress::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
}
}
impl ElementBuilder for AnimatedProgressBuilder {
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_classes(&self) -> &[String] {
self.get_or_build().element_classes()
}
}
pub fn progress_animated(value: SharedAnimatedValue) -> AnimatedProgressBuilder {
AnimatedProgressBuilder::new(value.clone())
}
#[cfg(test)]
mod tests {
use super::*;
fn init_theme() {
let _ = ThemeState::try_get().unwrap_or_else(|| {
ThemeState::init_default();
ThemeState::get()
});
}
#[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 test_progress_value_clamping() {
let config = ProgressConfig::new(150.0);
assert_eq!(config.value, 100.0);
let config = ProgressConfig::new(-10.0);
assert_eq!(config.value, 0.0);
let config = ProgressConfig::new(50.0);
assert_eq!(config.value, 50.0);
}
#[test]
fn test_progress_builder() {
init_theme();
let pb = ProgressBuilder::new(75.0)
.size(ProgressSize::Large)
.w(300.0);
assert_eq!(pb.config.value, 75.0);
assert_eq!(pb.config.size, ProgressSize::Large);
assert_eq!(pb.config.width, 300.0);
}
#[test]
fn test_progress_fill_calculation() {
init_theme();
let config = ProgressConfig::new(50.0);
assert_eq!(config.width * (config.value / 100.0), 100.0);
}
}