use std::rc::Rc;
use gpui::{App, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div};
use gpui_kit_semantics::{NodeSpec, Role, Semantic};
use gpui_kit_theme::{ActiveTheme, TypeScale};
use crate::foundation::{Ident, StyledExt};
use crate::motion::{Easing, MotionSpec, Transition, keyed};
type Format = Rc<dyn Fn(f64) -> String>;
#[derive(Default)]
struct Counter(Option<Transition<f32>>);
#[derive(IntoElement)]
pub struct AnimatedNumber {
ident: Ident,
value: f64,
format: Option<Format>,
spec: Option<MotionSpec>,
scale: TypeScale,
}
impl std::fmt::Debug for AnimatedNumber {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("AnimatedNumber")
.field("ident", &self.ident)
.field("value", &self.value)
.field("formatted", &self.formatted(self.value))
.finish()
}
}
impl AnimatedNumber {
pub fn new(ident: impl Into<Ident>, value: f64) -> Self {
Self {
ident: ident.into(),
value,
format: None,
spec: None,
scale: TypeScale::Title,
}
}
pub fn format(mut self, format: impl Fn(f64) -> String + 'static) -> Self {
self.format = Some(Rc::new(format));
self
}
pub fn spec(mut self, spec: MotionSpec) -> Self {
self.spec = Some(spec);
self
}
pub fn type_scale(mut self, scale: TypeScale) -> Self {
self.scale = scale;
self
}
fn formatted(&self, value: f64) -> String {
match &self.format {
Some(format) => format(value),
None => format!("{value}"),
}
}
}
impl RenderOnce for AnimatedNumber {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme().clone();
let spec = self.spec.unwrap_or_else(|| {
MotionSpec::new(theme.motion.resize_ms, Easing::Standard.curve(&theme))
});
let target = self.value as f32;
let counter = keyed::slot::<Counter>(&self.ident.semantic_id(), cx);
let shown = {
let mut counter = counter.borrow_mut();
let mut transition = counter
.0
.unwrap_or_else(|| Transition::new(target, spec))
.spec(spec);
transition.set(target);
let shown = transition.animate(window, cx);
counter.0 = Some(transition);
shown
};
let announced = self.formatted(self.value);
let painted = self.formatted(shown as f64);
div()
.child(
div()
.type_scale(&theme, self.scale)
.text_color(theme.colors.text)
.child(SharedString::from(painted)),
)
.semantic_in(
cx,
NodeSpec::new(self.ident.semantic_id(), Role::Status).value(announced),
)
}
}
pub fn grouped(value: f64) -> String {
let rounded = value.round() as i64;
let negative = rounded < 0;
let digits = rounded.abs().to_string();
let mut grouped = String::with_capacity(digits.len() + digits.len() / 3 + 1);
for (index, digit) in digits.chars().enumerate() {
if index > 0 && (digits.len() - index).is_multiple_of(3) {
grouped.push(',');
}
grouped.push(digit);
}
if negative {
format!("-{grouped}")
} else {
grouped
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grouping_inserts_a_separator_every_three_digits() {
assert_eq!(grouped(0.0), "0");
assert_eq!(grouped(999.0), "999");
assert_eq!(grouped(1204.0), "1,204");
assert_eq!(grouped(1_204_000.0), "1,204,000");
assert_eq!(grouped(-4200.0), "-4,200");
}
#[test]
fn the_debug_view_reports_the_target_not_a_frame_of_it() {
let number = AnimatedNumber::new("total", 1204.0).format(grouped);
assert!(format!("{number:?}").contains("1,204"));
}
}