Files
coop/crates/ui/src/slider.rs
2024-12-10 09:40:27 +07:00

197 lines
5.9 KiB
Rust

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<Pixels>,
}
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>) {
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<Pixels>,
cx: &mut gpui::ViewContext<Self>,
) {
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<Self>) -> 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<DragThumb>, 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>) {
self.update_value_by_position(event.position, cx);
}
}
impl EventEmitter<SliderEvent> for Slider {}
impl Render for Slider {
fn render(&mut self, cx: &mut ViewContext<Self>) -> 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()
}),
)
}
}