use crate::render::{Cell, Modifier};
use crate::style::Color;
use crate::widget::theme::{DARK_GRAY, DISABLED_FG, SEPARATOR_COLOR};
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SliderOrientation {
#[default]
Horizontal,
Vertical,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SliderStyle {
#[default]
Block,
Line,
Thin,
Gradient,
Dots,
}
pub struct Slider {
value: f64,
min: f64,
max: f64,
step: f64,
orientation: SliderOrientation,
style: SliderStyle,
length: u16,
show_value: bool,
value_format: Option<String>,
track_color: Color,
fill_color: Color,
knob_color: Color,
focused: bool,
disabled: bool,
label: Option<String>,
show_ticks: bool,
tick_count: u16,
props: WidgetProps,
}
impl Slider {
pub fn new() -> Self {
Self {
value: 0.0,
min: 0.0,
max: 100.0,
step: 0.0,
orientation: SliderOrientation::Horizontal,
style: SliderStyle::Block,
length: 20,
show_value: true,
value_format: None,
track_color: SEPARATOR_COLOR,
fill_color: Color::CYAN,
knob_color: Color::WHITE,
focused: false,
disabled: false,
label: None,
show_ticks: false,
tick_count: 5,
props: WidgetProps::new(),
}
}
pub fn value(mut self, value: f64) -> Self {
self.value = self.clamp_value(value);
self
}
pub fn range(mut self, min: f64, max: f64) -> Self {
self.min = min;
self.max = max;
self.value = self.clamp_value(self.value);
self
}
pub fn step(mut self, step: f64) -> Self {
self.step = step.abs();
self
}
pub fn orientation(mut self, orientation: SliderOrientation) -> Self {
self.orientation = orientation;
self
}
pub fn horizontal(mut self) -> Self {
self.orientation = SliderOrientation::Horizontal;
self
}
pub fn vertical(mut self) -> Self {
self.orientation = SliderOrientation::Vertical;
self
}
pub fn style(mut self, style: SliderStyle) -> Self {
self.style = style;
self
}
pub fn length(mut self, length: u16) -> Self {
self.length = length.max(3);
self
}
pub fn show_value(mut self, show: bool) -> Self {
self.show_value = show;
self
}
pub fn value_format(mut self, format: impl Into<String>) -> Self {
self.value_format = Some(format.into());
self
}
pub fn track_color(mut self, color: Color) -> Self {
self.track_color = color;
self
}
pub fn fill_color(mut self, color: Color) -> Self {
self.fill_color = color;
self
}
pub fn knob_color(mut self, color: Color) -> Self {
self.knob_color = color;
self
}
pub fn focused(mut self, focused: bool) -> Self {
self.focused = focused;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn ticks(mut self, count: u16) -> Self {
self.show_ticks = true;
self.tick_count = count.max(2);
self
}
fn clamp_value(&self, value: f64) -> f64 {
let clamped = value.clamp(self.min, self.max);
if self.step > 0.0 {
let steps = ((clamped - self.min) / self.step).round();
(self.min + steps * self.step).clamp(self.min, self.max)
} else {
clamped
}
}
fn normalized(&self) -> f64 {
if (self.max - self.min).abs() < f64::EPSILON {
0.0
} else {
(self.value - self.min) / (self.max - self.min)
}
}
pub fn set_value(&mut self, value: f64) {
self.value = self.clamp_value(value);
}
pub fn get_value(&self) -> f64 {
self.value
}
pub fn increment(&mut self) {
let step = if self.step > 0.0 {
self.step
} else {
(self.max - self.min) / 100.0
};
self.value = self.clamp_value(self.value + step);
}
pub fn decrement(&mut self) {
let step = if self.step > 0.0 {
self.step
} else {
(self.max - self.min) / 100.0
};
self.value = self.clamp_value(self.value - step);
}
pub fn set_min(&mut self) {
self.value = self.min;
}
pub fn set_max(&mut self) {
self.value = self.max;
}
pub fn handle_key(&mut self, key: &crate::event::Key) -> bool {
use crate::event::Key;
if self.disabled || !self.focused {
return false;
}
match (&self.orientation, key) {
(SliderOrientation::Horizontal, Key::Right | Key::Char('l'))
| (SliderOrientation::Vertical, Key::Up | Key::Char('k')) => {
self.increment();
true
}
(SliderOrientation::Horizontal, Key::Left | Key::Char('h'))
| (SliderOrientation::Vertical, Key::Down | Key::Char('j')) => {
self.decrement();
true
}
(_, Key::Home) => {
self.set_min();
true
}
(_, Key::End) => {
self.set_max();
true
}
_ => false,
}
}
fn format_value(&self) -> String {
if let Some(ref fmt) = self.value_format {
fmt.replace("{value}", &format!("{:.1}", self.value))
.replace("{pct}", &format!("{:.0}", self.normalized() * 100.0))
.replace("{}", &format!("{:.1}", self.value))
} else if self.step >= 1.0 || (self.step == 0.0 && self.max - self.min >= 10.0) {
format!("{:.0}", self.value)
} else {
format!("{:.1}", self.value)
}
}
fn render_horizontal(&self, ctx: &mut RenderContext) {
let area = ctx.area;
let mut x: u16 = 0;
let y: u16 = 0;
if let Some(ref label) = self.label {
for (i, ch) in label.chars().enumerate() {
if x + i as u16 >= area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(if self.disabled {
DISABLED_FG
} else {
Color::WHITE
});
ctx.set(x + i as u16, y, cell);
}
x += label.len() as u16 + 1;
}
let track_len = self.length.min(area.width.saturating_sub(x));
let filled = (self.normalized() * (track_len - 1) as f64).round() as u16;
match self.style {
SliderStyle::Block => {
for i in 0..track_len {
let ch = if i <= filled { '█' } else { '░' };
let fg = if i <= filled {
self.fill_color
} else {
self.track_color
};
let mut cell = Cell::new(ch);
cell.fg = Some(if self.disabled { DARK_GRAY } else { fg });
ctx.set(x + i, y, cell);
}
}
SliderStyle::Line => {
for i in 0..track_len {
let is_knob = i == filled;
let ch = if is_knob { '●' } else { '━' };
let fg = if is_knob {
self.knob_color
} else if i < filled {
self.fill_color
} else {
self.track_color
};
let mut cell = Cell::new(ch);
cell.fg = Some(if self.disabled { DARK_GRAY } else { fg });
ctx.set(x + i, y, cell);
}
}
SliderStyle::Thin => {
for i in 0..track_len {
let is_knob = i == filled;
let ch = if is_knob { '┃' } else { '─' };
let fg = if is_knob {
self.knob_color
} else {
self.track_color
};
let mut cell = Cell::new(ch);
cell.fg = Some(if self.disabled { DARK_GRAY } else { fg });
ctx.set(x + i, y, cell);
}
}
SliderStyle::Gradient => {
let blocks = ['░', '▒', '▓', '█'];
for i in 0..track_len {
let progress = i as f64 / track_len as f64;
let block_idx = if progress <= self.normalized() {
((progress / self.normalized()) * 3.0).min(3.0) as usize
} else {
0
};
let ch = if i as f64 / track_len as f64 <= self.normalized() {
blocks[block_idx.min(3)]
} else {
'░'
};
let fg = if i as f64 / track_len as f64 <= self.normalized() {
self.fill_color
} else {
self.track_color
};
let mut cell = Cell::new(ch);
cell.fg = Some(if self.disabled { DARK_GRAY } else { fg });
ctx.set(x + i, y, cell);
}
}
SliderStyle::Dots => {
for i in 0..track_len {
let ch = if i <= filled { '●' } else { '○' };
let fg = if i <= filled {
self.fill_color
} else {
self.track_color
};
let mut cell = Cell::new(ch);
cell.fg = Some(if self.disabled { DARK_GRAY } else { fg });
ctx.set(x + i, y, cell);
}
}
}
x += track_len;
if self.show_value {
let value_str = self.format_value();
x += 1;
for (i, ch) in value_str.chars().enumerate() {
if x + i as u16 >= area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(if self.focused {
Color::CYAN
} else {
Color::WHITE
});
if self.focused {
cell.modifier |= Modifier::BOLD;
}
ctx.set(x + i as u16, y, cell);
}
}
if self.show_ticks && area.height > 1 {
let tick_y = y + 1;
for i in 0..self.tick_count {
let tick_x = (self.label.as_ref().map(|l| l.len() + 1).unwrap_or(0) as u16)
+ (i as f64 / (self.tick_count - 1) as f64 * (track_len - 1) as f64) as u16;
if tick_x < area.width {
let mut cell = Cell::new('┴');
cell.fg = Some(self.track_color);
ctx.set(tick_x, tick_y, cell);
}
}
}
}
fn render_vertical(&self, ctx: &mut RenderContext) {
let area = ctx.area;
let x: u16 = 0;
let track_len = self.length.min(area.height);
let filled = (self.normalized() * (track_len - 1) as f64).round() as u16;
for i in 0..track_len {
let from_bottom = track_len - 1 - i;
let y = i;
let (ch, fg) = match self.style {
SliderStyle::Block => {
if from_bottom <= filled {
('█', self.fill_color)
} else {
('░', self.track_color)
}
}
SliderStyle::Line | SliderStyle::Thin => {
if from_bottom == filled {
('●', self.knob_color)
} else {
(
'│',
if from_bottom < filled {
self.fill_color
} else {
self.track_color
},
)
}
}
SliderStyle::Gradient | SliderStyle::Dots => {
if from_bottom <= filled {
('●', self.fill_color)
} else {
('○', self.track_color)
}
}
};
let mut cell = Cell::new(ch);
cell.fg = Some(if self.disabled { DARK_GRAY } else { fg });
ctx.set(x, y, cell);
}
if self.show_value && area.width > 2 {
let value_str = self.format_value();
let value_y = track_len / 2;
for (i, ch) in value_str.chars().enumerate() {
if x + 2 + i as u16 >= area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(if self.focused {
Color::CYAN
} else {
Color::WHITE
});
ctx.set(x + 2 + i as u16, value_y, cell);
}
}
}
}
impl Default for Slider {
fn default() -> Self {
Self::new()
}
}
impl View for Slider {
crate::impl_view_meta!("Slider");
fn render(&self, ctx: &mut RenderContext) {
match self.orientation {
SliderOrientation::Horizontal => self.render_horizontal(ctx),
SliderOrientation::Vertical => self.render_vertical(ctx),
}
}
}
impl_styled_view!(Slider);
impl_props_builders!(Slider);
pub fn slider() -> Slider {
Slider::new()
}
pub fn slider_range(min: f64, max: f64) -> Slider {
Slider::new().range(min, max)
}
pub fn percentage_slider() -> Slider {
Slider::new().range(0.0, 100.0).value_format("{}%")
}
pub fn volume_slider() -> Slider {
Slider::new()
.range(0.0, 100.0)
.label("Vol")
.style(SliderStyle::Block)
}