use std::cell::Cell;
use std::rc::Rc;
use gpui::prelude::*;
use gpui::*;
use crate::theme::{get_theme_or, Theme};
use crate::utils::format_display_value;
use super::focus_navigation::{handle_tab_navigation, with_focus_actions, EnabledCursorExt};
#[derive(Clone, Debug)]
pub enum SliderEvent {
Change(f64),
ChangeComplete,
}
#[doc(hidden)]
#[derive(Clone)]
struct SliderDragState;
#[doc(hidden)]
struct EmptyDragView;
impl Render for EmptyDragView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<'_, Self>) -> impl IntoElement {
div().size_0()
}
}
pub struct Slider {
value: f64,
min: f64,
max: f64,
step: Option<f64>,
focus_handle: FocusHandle,
custom_theme: Option<Theme>,
show_value: bool,
display_precision: Option<usize>,
enabled: bool,
track_origin: Rc<Cell<f32>>,
track_width: Rc<Cell<f32>>,
dragging: bool,
}
impl EventEmitter<SliderEvent> for Slider {}
impl Focusable for Slider {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Slider {
pub fn new(cx: &mut Context<Self>) -> Self {
Self {
value: 0.0,
min: 0.0,
max: 100.0,
step: None,
focus_handle: cx.focus_handle().tab_stop(true),
custom_theme: None,
show_value: false,
display_precision: None,
enabled: true,
track_origin: Rc::new(Cell::new(0.0)),
track_width: Rc::new(Cell::new(0.0)),
dragging: false,
}
}
#[must_use]
pub fn with_value(mut self, value: f64) -> Self {
self.value = value.clamp(self.min, self.max);
self
}
#[must_use]
pub fn min(mut self, min: f64) -> Self {
self.min = min;
self.value = self.value.clamp(self.min, self.max);
self
}
#[must_use]
pub fn max(mut self, max: f64) -> Self {
self.max = max;
self.value = self.value.clamp(self.min, self.max);
self
}
#[must_use]
pub fn step(mut self, step: f64) -> Self {
self.step = Some(step);
self
}
#[must_use]
pub fn show_value(mut self, show: bool) -> Self {
self.show_value = show;
self
}
#[must_use]
pub fn display_precision(mut self, precision: usize) -> Self {
self.display_precision = Some(precision);
self
}
#[must_use]
pub fn theme(mut self, theme: Theme) -> Self {
self.custom_theme = Some(theme);
self
}
#[must_use]
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn value(&self) -> f64 {
self.value
}
pub fn get_min(&self) -> f64 {
self.min
}
pub fn get_max(&self) -> f64 {
self.max
}
pub fn get_step(&self) -> Option<f64> {
self.step
}
pub fn get_display_precision(&self) -> Option<usize> {
self.display_precision
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn set_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
if self.enabled != enabled {
self.enabled = enabled;
cx.notify();
}
}
pub fn set_value(&mut self, value: f64, cx: &mut Context<Self>) {
let normalized = self.normalize_value(value);
if (self.value - normalized).abs() > f64::EPSILON {
self.value = normalized;
cx.emit(SliderEvent::Change(self.value));
cx.notify();
}
}
pub fn focus_handle(&self) -> &FocusHandle {
&self.focus_handle
}
fn percentage(&self) -> f64 {
if (self.max - self.min).abs() < f64::EPSILON {
0.0
} else {
(self.value - self.min) / (self.max - self.min)
}
}
fn normalize_value(&self, value: f64) -> f64 {
let snapped = if let Some(step) = self.step {
if step > 0.0 {
let offset = value - self.min;
let n = (offset / step).round();
self.min + n * step
} else {
value
}
} else {
value
};
snapped.clamp(self.min, self.max)
}
fn format_value(&self) -> String {
format_display_value(self.value, self.display_precision)
}
fn set_value_from_position(&mut self, x: f32, cx: &mut Context<Self>) {
let track_origin = self.track_origin.get();
let track_width = self.track_width.get();
if track_width > 0.0 {
let relative_x = (x - track_origin).clamp(0.0, track_width);
let percentage = (relative_x / track_width) as f64;
let raw_value = self.min + percentage * (self.max - self.min);
let normalized = self.normalize_value(raw_value);
if (self.value - normalized).abs() > f64::EPSILON {
self.value = normalized;
cx.emit(SliderEvent::Change(self.value));
cx.notify();
}
}
}
fn adjust_value(&mut self, direction: f64, multiplier: f64, cx: &mut Context<Self>) {
let step = self.step.unwrap_or(1.0) * multiplier * direction;
let new_value = self.normalize_value(self.value + step);
if (self.value - new_value).abs() > f64::EPSILON {
self.value = new_value;
cx.emit(SliderEvent::Change(self.value));
cx.notify();
}
}
fn increment(&mut self, multiplier: f64, cx: &mut Context<Self>) {
self.adjust_value(1.0, multiplier, cx);
}
fn decrement(&mut self, multiplier: f64, cx: &mut Context<Self>) {
self.adjust_value(-1.0, multiplier, cx);
}
fn go_to_min(&mut self, cx: &mut Context<Self>) {
self.set_value(self.min, cx);
}
fn go_to_max(&mut self, cx: &mut Context<Self>) {
self.set_value(self.max, cx);
}
fn start_drag(&mut self) {
self.dragging = true;
}
fn end_drag(&mut self, cx: &mut Context<Self>) {
if self.dragging {
self.dragging = false;
cx.emit(SliderEvent::ChangeComplete);
}
}
}
impl Render for Slider {
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
let theme = get_theme_or(cx, self.custom_theme.as_ref());
let focus_handle = self.focus_handle.clone();
let is_focused = self.focus_handle.is_focused(window);
let percentage = self.percentage();
let show_value = self.show_value;
let display_value = self.format_value();
let enabled = self.enabled;
let track_height = 6.0;
let thumb_size = 16.0;
let track_origin = self.track_origin.clone();
let track_width = self.track_width.clone();
let track_bg = if enabled { theme.bg_input } else { theme.disabled_bg };
let filled_bg = if enabled { theme.primary } else { theme.disabled_text };
let thumb_border = if enabled { theme.primary } else { theme.disabled_text };
let value_color = if enabled { theme.text_value } else { theme.disabled_text };
let mut track_element = div()
.id("ccf_slider_track")
.relative()
.flex_1()
.h(px(thumb_size)) .cursor_for_enabled(enabled)
.child(
canvas(
{
let origin = track_origin.clone();
let width = track_width.clone();
move |bounds, _window, _cx| {
origin.set(bounds.origin.x.into());
width.set(bounds.size.width.into());
bounds
}
},
|_, _, _, _| {},
)
.size_full()
.absolute()
)
.child(
div()
.absolute()
.top(px((thumb_size - track_height) / 2.0))
.left_0()
.right_0()
.h(px(track_height))
.rounded_full()
.bg(rgb(track_bg))
)
.child(
div()
.absolute()
.top(px((thumb_size - track_height) / 2.0))
.left_0()
.w(relative(percentage as f32))
.h(px(track_height))
.rounded_full()
.bg(rgb(filled_bg))
)
.child(
div()
.absolute()
.top_0()
.left(relative(percentage as f32))
.ml(px(-(thumb_size / 2.0)))
.w(px(thumb_size))
.h(px(thumb_size))
.rounded_full()
.bg(rgb(theme.bg_white))
.border_2()
.border_color(rgb(thumb_border))
.when(enabled, |d| d.shadow_sm())
);
if enabled {
track_element = track_element
.on_mouse_down(MouseButton::Left, cx.listener(|slider, event: &MouseDownEvent, window, cx| {
if !slider.enabled {
return;
}
slider.focus_handle.focus(window);
slider.start_drag();
let x: f32 = event.position.x.into();
slider.set_value_from_position(x, cx);
}))
.on_drag(SliderDragState, |_state, _position, _window, cx| {
cx.new(|_| EmptyDragView)
})
.on_drag_move(cx.listener(|slider, event: &DragMoveEvent<SliderDragState>, _window, cx| {
if !slider.enabled {
return;
}
if slider.dragging {
let x: f32 = event.event.position.x.into();
slider.set_value_from_position(x, cx);
}
}))
.on_mouse_up(MouseButton::Left, cx.listener(|slider, _event: &MouseUpEvent, _window, cx| {
slider.end_drag(cx);
}))
.on_mouse_up_out(MouseButton::Left, cx.listener(|slider, _event: &MouseUpEvent, _window, cx| {
slider.end_drag(cx);
}));
}
with_focus_actions(
div()
.id("ccf_slider")
.track_focus(&focus_handle)
.tab_stop(enabled),
cx,
)
.on_key_down(cx.listener(|slider, event: &KeyDownEvent, window, cx| {
if !slider.enabled {
return;
}
if handle_tab_navigation(event, window) {
return;
}
let multiplier = if event.keystroke.modifiers.shift { 10.0 } else { 1.0 };
match event.keystroke.key.as_str() {
"left" => slider.decrement(multiplier, cx),
"right" => slider.increment(multiplier, cx),
"home" => slider.go_to_min(cx),
"end" => slider.go_to_max(cx),
_ => {}
}
}))
.flex()
.flex_row()
.gap_3()
.items_center()
.w_full()
.py_1()
.px_1()
.rounded_sm()
.border_2()
.border_color(if is_focused && enabled { rgb(theme.border_focus) } else { rgba(0x00000000) })
.child(track_element)
.when(show_value, |d| {
d.child(
div()
.min_w(px(40.0))
.text_sm()
.text_color(rgb(value_color))
.text_right()
.child(display_value)
)
})
}
}