use std::{cell::Cell, rc::Rc};
use crate::{
buffer::Buffer,
enums::Color,
prelude::{MouseButton, MouseEvent, Rect, Vec2},
style::{Style, Styleable},
term::backend::MouseEventKind,
text::Text,
widgets::{Element, EventResult, LayoutNode, Widget},
};
type ProgressBarHandler<M> = Box<dyn Fn(f64) -> M>;
pub struct ProgressBar<M> {
state: Rc<Cell<f64>>,
thumb_chars: Vec<char>,
style: Style,
track_char: char,
track_style: Style,
label: Option<ProgressLabel>,
handlers: Vec<(MouseButton, ProgressBarHandler<M>)>,
}
pub enum ProgressLabel {
Static(Box<dyn Text>),
Dynamic(Box<dyn Fn(f64) -> Box<dyn Text>>),
}
impl<M> ProgressBar<M> {
#[must_use]
pub fn new(state: Rc<Cell<f64>>) -> Self {
Self {
state,
style: Default::default(),
thumb_chars: vec!['▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'],
track_char: ' ',
track_style: Default::default(),
label: None,
handlers: vec![],
}
}
#[must_use]
pub fn thumb_chars<C>(mut self, chars: C) -> Self
where
C: IntoIterator<Item = char>,
{
self.thumb_chars = chars.into_iter().collect();
self
}
#[must_use]
pub fn style<S>(mut self, style: S) -> Self
where
S: Into<Style>,
{
self.style = style.into();
self
}
#[must_use]
pub fn track_char(mut self, track: char) -> Self {
self.track_char = track;
self
}
#[must_use]
pub fn track_style<S>(mut self, style: S) -> Self
where
S: Into<Style>,
{
self.track_style = style.into();
self
}
pub fn label<T>(mut self, text: T) -> Self
where
T: Into<Box<dyn Text>>,
{
self.label = Some(ProgressLabel::Static(text.into()));
self
}
pub fn dyn_label<F>(mut self, builder: F) -> Self
where
F: Fn(f64) -> Box<dyn Text> + 'static,
{
self.label = Some(ProgressLabel::Dynamic(Box::new(builder)));
self
}
#[must_use]
pub fn on_click<F>(self, response: F) -> Self
where
F: Fn(f64) -> M + 'static,
{
self.on_press(MouseButton::Left, response)
}
#[must_use]
pub fn on_press<F>(mut self, button: MouseButton, response: F) -> Self
where
F: Fn(f64) -> M + 'static,
{
self.handlers.retain(|(b, _)| *b != button);
self.handlers.push((button, Box::new(response)));
self
}
}
impl<M: Clone + 'static> Widget<M> for ProgressBar<M> {
fn render(&self, buffer: &mut Buffer, layout: &LayoutNode) {
let rect = layout.area;
if rect.is_empty() || self.thumb_chars.is_empty() {
return;
}
let (full_cells, head_id) = self.calc_size(&rect);
let mut rest_len = rect.width().saturating_sub(full_cells);
let mut track_pos = Vec2::new(rect.x() + full_cells, rect.y());
if head_id > 0 {
rest_len = rest_len.saturating_sub(1);
buffer[track_pos]
.char(self.thumb_chars[head_id])
.style(self.style);
track_pos.x += 1;
}
let thumb = self.thumb_chars[self.thumb_chars.len() - 1];
buffer.set_str_styled(
thumb.to_string().repeat(full_cells),
rect.pos(),
self.style,
);
buffer.set_str_styled(
self.track_char.to_string().repeat(rest_len),
&track_pos,
self.track_style,
);
self.render_label(buffer, rect, full_cells);
}
fn height(&self, _size: &Vec2) -> usize {
1
}
fn width(&self, size: &Vec2) -> usize {
size.x
}
fn on_event(&self, node: &LayoutNode, e: &MouseEvent) -> EventResult<M> {
let area = node.area;
if !area.contains_pos(&e.pos) {
return EventResult::None;
}
match &e.kind {
MouseEventKind::Down(button) => {
let rx = e.pos.x.saturating_sub(area.x());
let progress = (rx as f64 / area.width() as f64).clamp(0., 1.);
self.handlers
.iter()
.find(|(b, _)| b == button)
.map(|(_, m)| EventResult::Response(m(progress)))
.unwrap_or(EventResult::None)
}
_ => EventResult::None,
}
}
}
impl<M> ProgressBar<M> {
fn calc_size(&self, rect: &Rect) -> (usize, usize) {
let progress = self.state.get().clamp(0.0, 1.0);
let len = rect.width() as f64 * progress;
let full_cells = len.floor() as usize;
let frac = len - full_cells as f64;
let head_id = (frac * (self.thumb_chars.len() - 1) as f64).round();
(full_cells, head_id as usize)
}
fn render_label(&self, buffer: &mut Buffer, rect: Rect, full: usize) {
let mut render_text = |text: &Box<dyn Text>| {
let mut lines = vec![];
text.append_lines(&mut lines, rect.size(), None);
let Some(line) = lines.first() else {
return;
};
let align = text.get_align();
line.render(buffer, rect, align);
let offset = line.align_offset(&rect, align);
self.recolor_label(buffer, &rect, line.width, offset, full);
};
match &self.label {
Some(ProgressLabel::Static(text)) => {
render_text(text);
}
Some(ProgressLabel::Dynamic(builder)) => {
let progress = self.state.get().clamp(0.0, 1.0);
render_text(&builder(progress));
}
_ => {}
}
}
fn recolor_label(
&self,
buffer: &mut Buffer,
rect: &Rect,
width: usize,
offset: usize,
full: usize,
) {
let (thumb_color, track_color) = self.label_colors();
let mut set_cols = |x, bg, fg| {
let pos = Vec2::new(x, rect.y());
if let Some(c) = bg {
buffer[pos].bg = c;
}
if let Some(c) = fg {
buffer[pos].fg = c;
}
};
let base_x = rect.x() + offset;
for x in base_x..base_x + width {
if x < rect.x() + full {
let fg = track_color.unwrap_or(Color::Black);
set_cols(x, thumb_color, Some(fg));
} else {
set_cols(x, track_color, thumb_color);
}
}
}
fn label_colors(&self) -> (Option<Color>, Option<Color>) {
let full_thumb_char = self.thumb_chars.last().copied().unwrap_or(' ');
let thumb_color = if full_thumb_char.is_whitespace() {
self.style.bg.or(self.style.fg)
} else {
self.style.fg.or(self.style.bg)
};
let track_color = if self.track_char.is_whitespace() {
self.track_style.bg.or(self.track_style.fg)
} else {
self.track_style.fg.or(self.track_style.bg)
};
(thumb_color, track_color)
}
}
impl<M> Styleable for ProgressBar<M> {
fn style_mut(&mut self) -> &mut Style {
&mut self.style
}
}
impl<M: Clone + 'static> From<ProgressBar<M>> for Element<M> {
fn from(value: ProgressBar<M>) -> Self {
Element::new(value)
}
}
impl<M: Clone + 'static> From<ProgressBar<M>> for Box<dyn Widget<M>> {
fn from(value: ProgressBar<M>) -> Self {
Box::new(value)
}
}