use std::time::Duration;
use crate::{prelude::FluentBuilder as _, *};
#[derive(IntoElement)]
pub struct NumberTicker {
_id: ElementId,
base: Div,
value: i64,
separator: Option<char>,
prefix: Option<SharedString>,
suffix: Option<SharedString>,
duration: Duration,
}
impl NumberTicker {
pub fn new(id: impl Into<ElementId>, value: i64) -> Self {
Self {
_id: id.into(),
base: div(),
value,
separator: None,
prefix: None,
suffix: None,
duration: Duration::from_millis(600),
}
}
pub fn separator(mut self, sep: char) -> Self {
self.separator = Some(sep);
self
}
pub fn prefix(mut self, prefix: impl Into<SharedString>) -> Self {
self.prefix = Some(prefix.into());
self
}
pub fn suffix(mut self, suffix: impl Into<SharedString>) -> Self {
self.suffix = Some(suffix.into());
self
}
pub fn duration(mut self, duration: Duration) -> Self {
self.duration = duration;
self
}
}
impl Styled for NumberTicker {
fn style(&mut self) -> &mut StyleRefinement {
self.base.style()
}
}
#[derive(Clone, Copy)]
enum DigitOrSeparator {
Digit(u8),
Separator(char),
}
fn format_with_separator(value: i64, separator: Option<char>) -> Vec<DigitOrSeparator> {
let is_negative = value < 0;
let abs_str = value.unsigned_abs().to_string();
let mut result = Vec::new();
if is_negative {
result.push(DigitOrSeparator::Separator('-'));
}
let digits: Vec<u8> = abs_str.bytes().map(|b| b - b'0').collect();
let len = digits.len();
for (i, &digit) in digits.iter().enumerate() {
result.push(DigitOrSeparator::Digit(digit));
if let Some(sep) = separator {
let remaining = len - 1 - i;
if remaining > 0 && remaining.is_multiple_of(3) {
result.push(DigitOrSeparator::Separator(sep));
}
}
}
result
}
impl RenderOnce for NumberTicker {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let chars = format_with_separator(self.value, self.separator);
let duration = self.duration;
let digit_height = px(24.0);
let column_height = digit_height * 10.0;
self.base
.flex()
.flex_row()
.items_center()
.text_color(theme.tokens.foreground)
.font_family(theme.mono_font_family.clone())
.when_some(self.prefix.clone(), |el, prefix| {
el.child(div().child(prefix))
})
.children(chars.iter().enumerate().map(move |(pos, item)| {
match *item {
DigitOrSeparator::Separator(ch) => div()
.flex_shrink_0()
.child(SharedString::from(String::from(ch)))
.into_any_element(),
DigitOrSeparator::Digit(digit) => {
let target_offset = -(digit_height * digit as f32);
div()
.flex_shrink_0()
.h(digit_height)
.overflow_hidden()
.child(
div()
.id(("digit-col", pos as u32))
.flex()
.flex_col()
.h(column_height)
.children((0..10u8).map(|d| {
div()
.h(digit_height)
.flex()
.items_center()
.justify_center()
.child(SharedString::from(d.to_string()))
}))
.with_animation(
("digit-roll", pos as u32),
Animation::new(duration).with_easing(ease_out_cubic),
move |el, delta| {
let offset = target_offset * delta;
el.top(offset)
},
),
)
.into_any_element()
}
}
}))
.when_some(self.suffix, |el, suffix| el.child(div().child(suffix)))
.into_any_element()
}
}
#[cfg(test)]
mod tests {
use super::{DigitOrSeparator, format_with_separator};
#[test]
fn test_format_without_separator() {
let chars = format_with_separator(-123, None);
let mut kinds = chars.iter();
assert!(matches!(
kinds.next(),
Some(DigitOrSeparator::Separator('-'))
));
assert!(matches!(kinds.next(), Some(DigitOrSeparator::Digit(1))));
assert!(matches!(kinds.next(), Some(DigitOrSeparator::Digit(2))));
assert!(matches!(kinds.next(), Some(DigitOrSeparator::Digit(3))));
}
#[test]
fn test_format_with_separator() {
let chars = format_with_separator(1234567, Some(','));
let kinds: Vec<u8> = chars
.iter()
.filter_map(|k| match k {
DigitOrSeparator::Digit(d) => Some(*d),
DigitOrSeparator::Separator(_) => None,
})
.collect();
assert_eq!(kinds, vec![1, 2, 3, 4, 5, 6, 7]);
let sep_count = chars
.iter()
.filter(|k| matches!(k, DigitOrSeparator::Separator(',')))
.count();
assert_eq!(sep_count, 2);
}
}