use crate::{theme::ActiveTheme, tooltip::Tooltip}; use gpui::{ canvas, div, prelude::FluentBuilder as _, px, relative, Axis, Bounds, DragMoveEvent, EntityId, EventEmitter, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, ParentElement as _, Pixels, Point, Render, StatefulInteractiveElement as _, Styled, ViewContext, VisualContext as _, }; #[derive(Clone, Render)] pub struct DragThumb(EntityId); pub enum SliderEvent { Change(f32), } /// A Slider element. pub struct Slider { axis: Axis, min: f32, max: f32, step: f32, value: f32, bounds: Bounds, } impl Slider { fn new(axis: Axis) -> Self { Self { axis, min: 0.0, max: 100.0, step: 1.0, value: 0.0, bounds: Bounds::default(), } } pub fn horizontal() -> Self { Self::new(Axis::Horizontal) } /// Set the minimum value of the slider, default: 0.0 pub fn min(mut self, min: f32) -> Self { self.min = min; self } /// Set the maximum value of the slider, default: 100.0 pub fn max(mut self, max: f32) -> Self { self.max = max; self } /// Set the step value of the slider, default: 1.0 pub fn step(mut self, step: f32) -> Self { self.step = step; self } /// Set the default value of the slider, default: 0.0 pub fn default_value(mut self, value: f32) -> Self { self.value = value; self } /// Set the value of the slider. pub fn set_value(&mut self, value: f32, cx: &mut gpui::ViewContext) { self.value = value; cx.notify(); } /// Return percentage value of the slider, range of 0.0..1.0 fn relative_value(&self) -> f32 { let step = self.step; let value = self.value; let min = self.min; let max = self.max; let relative_value = (value - min) / (max - min); let relative_step = step / (max - min); let relative_value = (relative_value / relative_step).round() * relative_step; relative_value.clamp(0.0, 1.0) } /// Update value by mouse position fn update_value_by_position( &mut self, position: Point, cx: &mut gpui::ViewContext, ) { let bounds = self.bounds; let axis = self.axis; let min = self.min; let max = self.max; let step = self.step; let value = match axis { Axis::Horizontal => { let relative = (position.x - bounds.left()) / bounds.size.width; min + (max - min) * relative } Axis::Vertical => { let relative = (position.y - bounds.top()) / bounds.size.height; max - (max - min) * relative } }; let value = (value / step).round() * step; self.value = value.clamp(self.min, self.max); cx.emit(SliderEvent::Change(self.value)); cx.notify(); } fn render_thumb(&self, cx: &mut ViewContext) -> impl gpui::IntoElement { let value = self.value; let entity_id = cx.entity_id(); div() .id("slider-thumb") .on_drag(DragThumb(entity_id), |drag, _, cx| { cx.stop_propagation(); cx.new_view(|_| drag.clone()) }) .on_drag_move(cx.listener( move |view, e: &DragMoveEvent, cx| match e.drag(cx) { DragThumb(id) => { if *id != entity_id { return; } // set value by mouse position view.update_value_by_position(e.event.position, cx) } }, )) .absolute() .top(px(-5.)) .left(relative(self.relative_value())) .ml(-px(8.)) .size_4() .rounded_full() .border_1() .border_color(cx.theme().slider_bar.opacity(0.9)) .when(cx.theme().shadow, |this| this.shadow_md()) .bg(cx.theme().slider_thumb) .tooltip(move |cx| Tooltip::new(format!("{}", value), cx)) } fn on_mouse_down(&mut self, event: &MouseDownEvent, cx: &mut gpui::ViewContext) { self.update_value_by_position(event.position, cx); } } impl EventEmitter for Slider {} impl Render for Slider { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { div() .id("slider") .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down)) .h_5() .child( div() .id("slider-bar") .relative() .w_full() .my_1p5() .h_1p5() .bg(cx.theme().slider_bar.opacity(0.2)) .active(|this| this.bg(cx.theme().slider_bar.opacity(0.4))) .rounded(px(3.)) .child( div() .absolute() .top_0() .left_0() .h_full() .w(relative(self.relative_value())) .bg(cx.theme().slider_bar) .rounded_l(px(3.)), ) .child(self.render_thumb(cx)) .child({ let view = cx.view().clone(); canvas( move |bounds, cx| view.update(cx, |r, _| r.bounds = bounds), |_, _, _| {}, ) .absolute() .size_full() }), ) } }