use std::{cell::Cell, rc::Rc};
use crate::{
buffer::Buffer,
prelude::{MouseButton, MouseEvent, Rect, Vec2},
style::Style,
term::backend::MouseEventKind,
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>,
thumb_style: Style,
track_char: char,
style: Style,
handlers: Vec<(MouseButton, ProgressBarHandler<M>)>,
}
impl<M> ProgressBar<M> {
#[must_use]
pub fn new(state: Rc<Cell<f64>>) -> Self {
Self {
state,
thumb_style: Default::default(),
thumb_chars: vec!['▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'],
track_char: ' ',
style: Default::default(),
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 thumb_style<S>(mut self, style: S) -> Self
where
S: Into<Style>,
{
self.thumb_style = style.into();
self
}
#[must_use]
pub fn track_char(mut self, track: char) -> Self {
self.track_char = track;
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 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.thumb_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.thumb_style,
);
buffer.set_str_styled(
self.track_char.to_string().repeat(rest_len),
&track_pos,
self.style,
);
}
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)
}
}
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)
}
}