use rusty_bubbletea::commands;
use rusty_bubbletea::model::{Cmd, Msg};
use rusty_lipgloss::{self, Color, Style};
use rusty_x_ansi;
use std::fmt;
use std::sync::atomic::{AtomicI64, Ordering};
use std::time::Duration;
pub type ColorFunc = Box<dyn Fn(f64, f64) -> Color + Send + Sync>;
static LAST_ID: AtomicI64 = AtomicI64::new(0);
fn next_id() -> i32 {
(LAST_ID.fetch_add(1, Ordering::SeqCst)) as i32
}
pub const DEFAULT_FULL_CHAR_HALF_BLOCK: char = '▌';
pub const DEFAULT_FULL_CHAR_FULL_BLOCK: char = '█';
pub const DEFAULT_EMPTY_CHAR_BLOCK: char = '░';
const FPS: u64 = 60;
const DEFAULT_WIDTH: usize = 40;
const DEFAULT_FREQUENCY: f64 = 18.0;
const DEFAULT_DAMPING: f64 = 1.0;
pub fn default_blend_start() -> Color {
Color::parse("#5A56E0")
}
pub fn default_blend_end() -> Color {
Color::parse("#EE6FF8")
}
pub fn default_full_color() -> Color {
Color::parse("#7571F9")
}
pub fn default_empty_color() -> Color {
Color::parse("#606060")
}
pub type Option = Box<dyn FnOnce(&mut Model)>;
pub fn with_default_blend() -> Option {
with_colors(&[default_blend_start(), default_blend_end()])
}
pub fn with_colors(colors: &[Color]) -> Option {
let colors = colors.to_vec();
if colors.is_empty() {
return Box::new(|m: &mut Model| {
m.full_color = default_full_color();
m.blend = None;
m.color_func = None;
});
}
if colors.len() == 1 {
return Box::new(move |m: &mut Model| {
m.full_color = colors[0].clone();
m.color_func = None;
m.blend = None;
});
}
Box::new(move |m: &mut Model| {
m.blend = Some(colors.clone());
})
}
pub fn with_color_func(fn_: ColorFunc) -> Option {
Box::new(move |m: &mut Model| {
m.color_func = Some(fn_);
m.blend = None;
})
}
pub fn with_fill_characters(full: char, empty: char) -> Option {
Box::new(move |m: &mut Model| {
m.full = full;
m.empty = empty;
})
}
pub fn without_percentage() -> Option {
Box::new(|m: &mut Model| {
m.show_percentage = false;
})
}
pub fn with_width(w: usize) -> Option {
Box::new(move |m: &mut Model| {
m.set_width(w);
})
}
pub fn with_spring_options(frequency: f64, damping: f64) -> Option {
Box::new(move |m: &mut Model| {
m.set_spring_options(frequency, damping);
m.spring_customized = true;
})
}
pub fn with_scaled(enabled: bool) -> Option {
Box::new(move |m: &mut Model| {
m.scale_blend = enabled;
})
}
#[derive(Debug, Clone)]
pub struct FrameMsg {
id: i32,
tag: i32,
}
pub struct Model {
id: i32,
tag: i32,
width: usize,
pub full: char,
pub full_color: Color,
pub empty: char,
pub empty_color: Color,
pub show_percentage: bool,
pub percent_format: String,
pub percentage_style: Style,
spring: Spring,
spring_customized: bool,
percent_shown: f64,
target_percent: f64,
velocity: f64,
blend: std::option::Option<Vec<Color>>,
scale_blend: bool,
color_func: std::option::Option<ColorFunc>,
}
impl fmt::Debug for Model {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("progress::Model")
.field("id", &self.id)
.field("width", &self.width)
.field("target_percent", &self.target_percent)
.finish()
}
}
pub fn new(opts: Vec<Option>) -> Model {
let mut m = Model {
id: next_id(),
tag: 0,
width: DEFAULT_WIDTH,
full: DEFAULT_FULL_CHAR_HALF_BLOCK,
full_color: default_full_color(),
empty: DEFAULT_EMPTY_CHAR_BLOCK,
empty_color: default_empty_color(),
show_percentage: true,
percent_format: " %3.0f%%".to_string(),
percentage_style: Style::new(),
spring: Spring::identity(),
spring_customized: false,
percent_shown: 0.0,
target_percent: 0.0,
velocity: 0.0,
blend: None,
scale_blend: false,
color_func: None,
};
for opt in opts {
opt(&mut m);
}
if !m.spring_customized {
m.set_spring_options(DEFAULT_FREQUENCY, DEFAULT_DAMPING);
}
m
}
impl Model {
pub fn update(&mut self, msg: &dyn Msg) -> Cmd {
if let Some(m) = msg.as_any().downcast_ref::<FrameMsg>() {
if m.id != self.id || m.tag != self.tag {
return None;
}
if !self.is_animating() {
return None;
}
let (pos, vel) =
self.spring
.update(self.percent_shown, self.velocity, self.target_percent);
self.percent_shown = pos;
self.velocity = vel;
return self.next_frame();
}
None
}
pub fn set_spring_options(&mut self, frequency: f64, damping: f64) {
self.spring = Spring::new(
Duration::from_secs(1).as_secs_f64() / FPS as f64,
frequency,
damping,
);
}
pub fn percent(&self) -> f64 {
self.target_percent
}
pub fn set_percent(&mut self, p: f64) -> Cmd {
self.target_percent = p.clamp(0.0, 1.0);
self.tag += 1;
self.next_frame()
}
pub fn incr_percent(&mut self, v: f64) -> Cmd {
self.set_percent(self.percent() + v)
}
pub fn decr_percent(&mut self, v: f64) -> Cmd {
self.set_percent(self.percent() - v)
}
pub fn view(&self) -> String {
self.view_as(self.percent_shown)
}
pub fn view_as(&self, percent: f64) -> String {
let mut b = String::new();
let percent_view = self.percentage_view(percent);
self.bar_view(&mut b, percent, rusty_x_ansi::string_width(&percent_view));
b.push_str(&percent_view);
b
}
pub fn set_width(&mut self, w: usize) {
self.width = w;
}
pub fn width(&self) -> usize {
self.width
}
pub fn is_animating(&self) -> bool {
let dist = (self.percent_shown - self.target_percent).abs();
!(dist < 0.001 && self.velocity < 0.01)
}
fn next_frame(&self) -> Cmd {
let id = self.id;
let tag = self.tag;
commands::tick(Duration::from_secs(1) / (FPS as u32), move |_| {
Some(Box::new(FrameMsg { id, tag }))
})
}
fn bar_view(&self, b: &mut String, percent: f64, text_width: usize) {
let tw = self.width.saturating_sub(text_width); let mut fw = ((tw as f64) * percent).round() as usize;
fw = fw.min(tw);
let is_half_block = self.full == DEFAULT_FULL_CHAR_HALF_BLOCK;
if let Some(color_func) = &self.color_func {
let mut style = Style::new();
let mut current: f64;
let half_block_perc = 0.5 / (tw as f64);
for i in 0..fw {
current = (i as f64) / (tw as f64);
style = style.foreground_color(color_func(percent, current));
if is_half_block {
let bg = color_func(percent, (current + half_block_perc).min(1.0));
style = style.background_color(bg);
}
b.push_str(&style.render(&self.full.to_string()));
}
} else if let Some(blend) = &self.blend {
let mut multiplier = 1;
if is_half_block {
multiplier = 2;
}
let blend_colors = if self.scale_blend {
rusty_lipgloss::blending::blend_1d(fw * multiplier, blend)
} else {
rusty_lipgloss::blending::blend_1d(tw * multiplier, blend)
};
let mut blend_index = 0;
for i in 0..fw {
if !is_half_block {
b.push_str(
&Style::new()
.foreground_color(blend_colors[i].clone())
.render(&self.full.to_string()),
);
continue;
}
b.push_str(
&Style::new()
.foreground_color(blend_colors[blend_index].clone())
.background_color(blend_colors[blend_index + 1].clone())
.render(&self.full.to_string()),
);
blend_index += 2;
}
} else {
let repeat = self.full.to_string().repeat(fw);
b.push_str(
&Style::new()
.foreground_color(self.full_color.clone())
.render(&repeat),
);
}
let n = tw - fw;
let repeat = self.empty.to_string().repeat(n);
b.push_str(
&Style::new()
.foreground_color(self.empty_color.clone())
.render(&repeat),
);
}
fn percentage_view(&self, percent: f64) -> String {
if !self.show_percentage {
return String::new();
}
let percent = percent.clamp(0.0, 1.0);
let percentage = format!(" {:3.0}%", percent * 100.0);
self.percentage_style
.clone()
.inline(true)
.render(&percentage)
}
}
#[derive(Debug, Clone, Copy)]
struct Spring {
pos_pos_coef: f64,
pos_vel_coef: f64,
vel_pos_coef: f64,
vel_vel_coef: f64,
}
impl Spring {
fn identity() -> Spring {
Spring {
pos_pos_coef: 1.0,
pos_vel_coef: 0.0,
vel_pos_coef: 0.0,
vel_vel_coef: 1.0,
}
}
fn new(delta_time: f64, angular_frequency: f64, damping_ratio: f64) -> Spring {
const EPSILON: f64 = f64::EPSILON;
let angular_frequency = angular_frequency.max(0.0);
let damping_ratio = damping_ratio.max(0.0);
if angular_frequency < EPSILON {
return Spring::identity();
}
if damping_ratio > 1.0 + EPSILON {
let za = -angular_frequency * damping_ratio;
let zb = angular_frequency * (damping_ratio * damping_ratio - 1.0).sqrt();
let z1 = za - zb;
let z2 = za + zb;
let e1 = (z1 * delta_time).exp();
let e2 = (z2 * delta_time).exp();
let inv_two_zb = 1.0 / (2.0 * zb);
let e1_over_two_zb = e1 * inv_two_zb;
let e2_over_two_zb = e2 * inv_two_zb;
let z1e1_over_two_zb = z1 * e1_over_two_zb;
let z2e2_over_two_zb = z2 * e2_over_two_zb;
Spring {
pos_pos_coef: e1_over_two_zb * z2 - z2e2_over_two_zb + e2,
pos_vel_coef: -e1_over_two_zb + e2_over_two_zb,
vel_pos_coef: (z1e1_over_two_zb - z2e2_over_two_zb + e2) * z2,
vel_vel_coef: -z1e1_over_two_zb + z2e2_over_two_zb,
}
} else if damping_ratio < 1.0 - EPSILON {
let omega_zeta = angular_frequency * damping_ratio;
let alpha = angular_frequency * (1.0 - damping_ratio * damping_ratio).sqrt();
let exp_term = (-omega_zeta * delta_time).exp();
let cos_term = (alpha * delta_time).cos();
let sin_term = (alpha * delta_time).sin();
let inv_alpha = 1.0 / alpha;
let exp_sin = exp_term * sin_term;
let exp_cos = exp_term * cos_term;
let exp_omega_zeta_sin_over_alpha = exp_term * omega_zeta * sin_term * inv_alpha;
Spring {
pos_pos_coef: exp_cos + exp_omega_zeta_sin_over_alpha,
pos_vel_coef: exp_sin * inv_alpha,
vel_pos_coef: -exp_sin * alpha - omega_zeta * exp_omega_zeta_sin_over_alpha,
vel_vel_coef: exp_cos - exp_omega_zeta_sin_over_alpha,
}
} else {
let exp_term = (-angular_frequency * delta_time).exp();
let time_exp = delta_time * exp_term;
let time_exp_freq = time_exp * angular_frequency;
Spring {
pos_pos_coef: time_exp_freq + exp_term,
pos_vel_coef: time_exp,
vel_pos_coef: -angular_frequency * time_exp_freq,
vel_vel_coef: -time_exp_freq + exp_term,
}
}
}
fn update(&self, pos: f64, vel: f64, equilibrium_pos: f64) -> (f64, f64) {
let old_pos = pos - equilibrium_pos; let old_vel = vel;
let new_pos = old_pos * self.pos_pos_coef + old_vel * self.pos_vel_coef + equilibrium_pos;
let new_vel = old_pos * self.vel_pos_coef + old_vel * self.vel_vel_coef;
(new_pos, new_vel)
}
}