use crate::widgets::{Widget, WidgetArea};
use crate::{
Color, ColorPair, Event, InteractionCache, InteractionId, MouseButton, Result, Window,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SliderOrientation {
Vertical,
Horizontal,
}
#[derive(Debug, Clone, Copy)]
pub struct SliderOptions {
pub orientation: SliderOrientation,
pub min: f32,
pub max: f32,
pub value: f32,
pub viewport_size: f32,
pub track_color: ColorPair,
pub thumb_color: ColorPair,
pub min_thumb_virtual: u16,
}
impl Default for SliderOptions {
fn default() -> Self {
Self {
orientation: SliderOrientation::Vertical,
min: 0.0,
max: 100.0,
value: 0.0,
viewport_size: 1.0,
track_color: ColorPair::new(Color::DarkGray, Color::Transparent),
thumb_color: ColorPair::new(Color::White, Color::Transparent),
min_thumb_virtual: 1,
}
}
}
pub struct Slider {
width: u16,
height: u16,
opts: SliderOptions,
is_dragging: bool,
drag_offset_virtual: u16,
}
impl Slider {
pub fn new(width: u16, height: u16, opts: SliderOptions) -> Self {
Self {
width,
height,
opts,
is_dragging: false,
drag_offset_virtual: 0,
}
}
pub fn register_with_id(&self, ui: &mut InteractionCache, area: WidgetArea, id: InteractionId) {
ui.register_draggable(id, area);
}
pub fn vertical(height: u16) -> Self {
Self::new(
1,
height,
SliderOptions {
orientation: SliderOrientation::Vertical,
..SliderOptions::default()
},
)
}
pub fn horizontal(width: u16) -> Self {
Self::new(
width,
1,
SliderOptions {
orientation: SliderOrientation::Horizontal,
..SliderOptions::default()
},
)
}
pub fn with_size(mut self, width: u16, height: u16) -> Self {
self.width = width;
self.height = height;
self
}
pub fn set_size(&mut self, width: u16, height: u16) {
self.width = width;
self.height = height;
}
pub fn with_colors(mut self, track: ColorPair, thumb: ColorPair) -> Self {
self.opts.track_color = track;
self.opts.thumb_color = thumb;
self
}
pub fn set_colors(&mut self, track: ColorPair, thumb: ColorPair) {
self.opts.track_color = track;
self.opts.thumb_color = thumb;
}
pub fn with_range(mut self, min: f32, max: f32) -> Self {
self.opts.min = min;
self.opts.max = max;
self.set_value(self.opts.value);
self
}
pub fn set_range(&mut self, min: f32, max: f32) {
self.opts.min = min;
self.opts.max = max;
self.set_value(self.opts.value);
}
pub fn with_viewport_size(mut self, viewport_size: f32) -> Self {
self.opts.viewport_size = viewport_size;
self
}
pub fn value(&self) -> f32 {
self.opts.value
}
pub fn set_value(&mut self, value: f32) {
let (min, max) = self.normalized_range();
let v = value.clamp(min, max);
self.opts.value = v;
}
pub fn min(&self) -> f32 {
self.opts.min
}
pub fn max(&self) -> f32 {
self.opts.max
}
pub fn set_viewport_size(&mut self, viewport_size: f32) {
self.opts.viewport_size = viewport_size.max(0.0);
}
pub fn is_dragging(&self) -> bool {
self.is_dragging
}
pub fn cancel_drag(&mut self) {
self.is_dragging = false;
self.drag_offset_virtual = 0;
}
pub fn handle_event(&mut self, event: &Event, area: crate::widgets::WidgetArea) -> bool {
match *event {
Event::MouseClick { x, y, button } => {
if button != MouseButton::Left {
return false;
}
if !area.contains_point(x, y) {
return false;
}
let was_dragging = self.is_dragging;
let old_drag_offset = self.drag_offset_virtual;
let changed = self.update_value_from_mouse_direct(x, y, area);
self.is_dragging = true;
self.drag_offset_virtual = self.calculate_drag_offset_virtual(x, y, area);
changed || !was_dragging || self.drag_offset_virtual != old_drag_offset
}
Event::MouseDrag { x, y, button } => {
if button != MouseButton::Left {
return false;
}
if !self.is_dragging {
return false;
}
self.update_value_from_mouse_with_offset(x, y, area, self.drag_offset_virtual)
}
Event::MouseRelease { button, .. } => {
if button != MouseButton::Left {
return false;
}
if self.is_dragging {
self.is_dragging = false;
self.drag_offset_virtual = 0;
return true;
}
false
}
_ => false,
}
}
fn normalized_range(&self) -> (f32, f32) {
if self.opts.max >= self.opts.min {
(self.opts.min, self.opts.max)
} else {
(self.opts.max, self.opts.min)
}
}
fn axis_len_cells(&self) -> u16 {
match self.opts.orientation {
SliderOrientation::Vertical => self.height,
SliderOrientation::Horizontal => self.width,
}
}
fn axis_len_virtual(&self) -> u16 {
self.axis_len_cells().saturating_mul(2)
}
fn range(&self) -> f32 {
let (min, max) = self.normalized_range();
(max - min).max(0.0)
}
fn thumb_size_virtual(&self) -> u16 {
let track = self.axis_len_virtual();
if track == 0 {
return 0;
}
let range = self.range();
if range <= 0.0 {
return track;
}
let viewport = self.opts.viewport_size.max(1.0);
let content = range + viewport;
if content <= viewport {
return track;
}
let ratio = viewport / content;
let size = (track as f32 * ratio).floor() as i32;
let size = size.max(self.opts.min_thumb_virtual as i32);
let size = size.min(track as i32);
size as u16
}
fn thumb_start_virtual(&self) -> u16 {
let track = self.axis_len_virtual();
if track == 0 {
return 0;
}
let range = self.range();
if range <= 0.0 {
return 0;
}
let (min, _max) = self.normalized_range();
let value_ratio = ((self.opts.value - min) / range).clamp(0.0, 1.0);
let thumb = self.thumb_size_virtual();
let max_start = track.saturating_sub(thumb);
(value_ratio * max_start as f32).round() as u16
}
#[allow(dead_code)]
fn thumb_rect_local(&self) -> (u16, u16, u16, u16) {
let thumb_size_v = self.thumb_size_virtual();
let thumb_start_v = self.thumb_start_virtual();
let thumb_end_v = thumb_start_v.saturating_add(thumb_size_v);
let real_start = thumb_start_v / 2;
let real_end_exclusive = (thumb_end_v + 1) / 2; let real_size = real_end_exclusive.saturating_sub(real_start).max(1);
match self.opts.orientation {
SliderOrientation::Vertical => (0, real_start, self.width.max(1), real_size),
SliderOrientation::Horizontal => (real_start, 0, real_size, self.height.max(1)),
}
}
fn draw_vertical(&self, window: &mut dyn Window) -> Result<()> {
let track = " ".repeat(self.width.max(1) as usize);
for y in 0..self.height {
window.write_str_colored(y, 0, &track, self.opts.track_color)?;
}
let track_v = self.axis_len_virtual();
if track_v == 0 {
return Ok(());
}
let thumb_v = self.thumb_size_virtual();
let start_v = self.thumb_start_virtual();
let end_v = start_v.saturating_add(thumb_v);
let real_start = start_v / 2;
let real_end = ((end_v + 1) / 2).saturating_sub(1); if self.height == 0 {
return Ok(());
}
let start_y = real_start.min(self.height.saturating_sub(1));
let end_y = real_end.min(self.height.saturating_sub(1));
for real_y in start_y..=end_y {
let cell_v_start = real_y.saturating_mul(2);
let cell_v_end = cell_v_start.saturating_add(2);
let cov_start = start_v.max(cell_v_start);
let cov_end = end_v.min(cell_v_end);
let coverage = cov_end.saturating_sub(cov_start);
let ch = if coverage >= 2 {
'█'
} else if coverage == 1 {
if cov_start == cell_v_start {
'▀'
} else {
'▄'
}
} else {
' '
};
if ch != ' ' {
let mut buf = String::new();
buf.extend(std::iter::repeat(ch).take(self.width.max(1) as usize));
window.write_str_colored(real_y, 0, &buf, self.opts.thumb_color)?;
}
}
Ok(())
}
fn draw_horizontal(&self, window: &mut dyn Window) -> Result<()> {
let track = " ".repeat(self.width as usize);
for y in 0..self.height.max(1) {
window.write_str_colored(y, 0, &track, self.opts.track_color)?;
}
let track_v = self.axis_len_virtual();
if track_v == 0 {
return Ok(());
}
let thumb_v = self.thumb_size_virtual();
let start_v = self.thumb_start_virtual();
let end_v = start_v.saturating_add(thumb_v);
let real_start = start_v / 2;
let real_end = ((end_v + 1) / 2).saturating_sub(1); if self.width == 0 {
return Ok(());
}
let start_x = real_start.min(self.width.saturating_sub(1));
let end_x = real_end.min(self.width.saturating_sub(1));
for real_x in start_x..=end_x {
let cell_v_start = real_x.saturating_mul(2);
let cell_v_end = cell_v_start.saturating_add(2);
let cov_start = start_v.max(cell_v_start);
let cov_end = end_v.min(cell_v_end);
let coverage = cov_end.saturating_sub(cov_start);
let ch = if coverage >= 2 {
'█'
} else if coverage == 1 {
if cov_start == cell_v_start {
'▌'
} else {
'▐'
}
} else {
' '
};
if ch != ' ' {
for y in 0..self.height.max(1) {
window.write_str_colored(y, real_x, &ch.to_string(), self.opts.thumb_color)?;
}
}
}
Ok(())
}
fn clamp_mouse_to_area(&self, x: u16, y: u16, area: crate::widgets::WidgetArea) -> (u16, u16) {
let cx = x.clamp(area.x, area.right().saturating_sub(1));
let cy = y.clamp(area.y, area.bottom().saturating_sub(1));
(cx, cy)
}
fn calculate_drag_offset_virtual(
&self,
x: u16,
y: u16,
area: crate::widgets::WidgetArea,
) -> u16 {
let (cx, cy) = self.clamp_mouse_to_area(x, y, area);
let mouse_axis_cells = match self.opts.orientation {
SliderOrientation::Vertical => cy.saturating_sub(area.y),
SliderOrientation::Horizontal => cx.saturating_sub(area.x),
};
let mouse_axis_v = mouse_axis_cells.saturating_mul(2);
let thumb_start_v = self.thumb_start_virtual();
let thumb_size_v = self.thumb_size_virtual();
let offset = mouse_axis_v.saturating_sub(thumb_start_v);
offset.min(thumb_size_v)
}
fn update_value_from_mouse_direct(
&mut self,
x: u16,
y: u16,
area: crate::widgets::WidgetArea,
) -> bool {
let old = self.opts.value;
let (cx, cy) = self.clamp_mouse_to_area(x, y, area);
let track_cells = self.axis_len_cells().max(1);
let mouse_cells = match self.opts.orientation {
SliderOrientation::Vertical => cy.saturating_sub(area.y),
SliderOrientation::Horizontal => cx.saturating_sub(area.x),
};
let ratio = (mouse_cells as f32 / track_cells as f32).clamp(0.0, 1.0);
let (min, _max) = self.normalized_range();
let value = min + ratio * self.range();
self.set_value(value);
(self.opts.value - old).abs() > f32::EPSILON
}
fn update_value_from_mouse_with_offset(
&mut self,
x: u16,
y: u16,
area: crate::widgets::WidgetArea,
offset_virtual: u16,
) -> bool {
let old = self.opts.value;
let (cx, cy) = self.clamp_mouse_to_area(x, y, area);
let track_cells = self.axis_len_cells().max(1);
let track_v = track_cells.saturating_mul(2);
let mouse_cells = match self.opts.orientation {
SliderOrientation::Vertical => cy.saturating_sub(area.y),
SliderOrientation::Horizontal => cx.saturating_sub(area.x),
};
let mouse_v = mouse_cells.saturating_mul(2);
let thumb_v = self.thumb_size_virtual().max(1);
let max_start = track_v.saturating_sub(thumb_v);
let mut desired_start = mouse_v.saturating_sub(offset_virtual);
desired_start = desired_start.min(max_start);
let ratio = if max_start == 0 {
0.0
} else {
(desired_start as f32 / max_start as f32).clamp(0.0, 1.0)
};
let (min, _max) = self.normalized_range();
let value = min + ratio * self.range();
self.set_value(value);
(self.opts.value - old).abs() > f32::EPSILON
}
}
impl Widget for Slider {
fn draw(&self, window: &mut dyn Window) -> Result<()> {
match self.opts.orientation {
SliderOrientation::Vertical => self.draw_vertical(window),
SliderOrientation::Horizontal => self.draw_horizontal(window),
}
}
fn get_size(&self) -> (u16, u16) {
(self.width, self.height)
}
fn get_position(&self) -> (u16, u16) {
(0, 0)
}
}