use std::ops::Range;
use crate::{h_flex, tooltip::Tooltip, ActiveTheme, AxisExt, StyledExt};
use gpui::{
canvas, div, prelude::FluentBuilder as _, px, Along, App, AppContext as _, Axis, Background,
Bounds, Context, Corners, DragMoveEvent, Empty, Entity, EntityId, EventEmitter, Hsla,
InteractiveElement, IntoElement, MouseButton, MouseDownEvent, ParentElement as _, Pixels,
Point, Render, RenderOnce, StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
};
#[derive(Clone)]
pub struct DragThumb((EntityId, bool));
impl Render for DragThumb {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
Empty
}
}
pub enum SliderEvent {
Change(SliderValue),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum SliderValue {
Single(f32),
Range(f32, f32),
}
impl std::fmt::Display for SliderValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SliderValue::Single(value) => write!(f, "{}", value),
SliderValue::Range(start, end) => write!(f, "{}..{}", start, end),
}
}
}
impl From<f32> for SliderValue {
fn from(value: f32) -> Self {
SliderValue::Single(value)
}
}
impl From<(f32, f32)> for SliderValue {
fn from(value: (f32, f32)) -> Self {
SliderValue::Range(value.0, value.1)
}
}
impl From<Range<f32>> for SliderValue {
fn from(value: Range<f32>) -> Self {
SliderValue::Range(value.start, value.end)
}
}
impl Default for SliderValue {
fn default() -> Self {
SliderValue::Single(0.)
}
}
impl SliderValue {
pub fn clamp(self, min: f32, max: f32) -> Self {
match self {
SliderValue::Single(value) => SliderValue::Single(value.clamp(min, max)),
SliderValue::Range(start, end) => {
SliderValue::Range(start.clamp(min, max), end.clamp(min, max))
}
}
}
#[inline]
pub fn is_single(&self) -> bool {
matches!(self, SliderValue::Single(_))
}
#[inline]
pub fn is_range(&self) -> bool {
matches!(self, SliderValue::Range(_, _))
}
pub fn start(&self) -> f32 {
match self {
SliderValue::Single(value) => *value,
SliderValue::Range(start, _) => *start,
}
}
pub fn end(&self) -> f32 {
match self {
SliderValue::Single(value) => *value,
SliderValue::Range(_, end) => *end,
}
}
fn set_start(&mut self, value: f32) {
if let SliderValue::Range(_, end) = self {
*self = SliderValue::Range(value.min(*end), *end);
} else {
*self = SliderValue::Single(value);
}
}
fn set_end(&mut self, value: f32) {
if let SliderValue::Range(start, _) = self {
*self = SliderValue::Range(*start, value.max(*start));
} else {
*self = SliderValue::Single(value);
}
}
}
pub struct SliderState {
min: f32,
max: f32,
step: f32,
value: SliderValue,
percentage: Range<f32>,
bounds: Bounds<Pixels>,
}
impl SliderState {
pub fn new() -> Self {
Self {
min: 0.0,
max: 100.0,
step: 1.0,
value: SliderValue::default(),
percentage: (0.0..0.0),
bounds: Bounds::default(),
}
}
pub fn min(mut self, min: f32) -> Self {
self.min = min;
self.update_thumb_pos();
self
}
pub fn max(mut self, max: f32) -> Self {
self.max = max;
self.update_thumb_pos();
self
}
pub fn step(mut self, step: f32) -> Self {
self.step = step;
self
}
pub fn default_value(mut self, value: impl Into<SliderValue>) -> Self {
self.value = value.into();
self.update_thumb_pos();
self
}
pub fn set_value(
&mut self,
value: impl Into<SliderValue>,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.value = value.into();
self.update_thumb_pos();
cx.notify();
}
pub fn value(&self) -> SliderValue {
self.value
}
fn update_thumb_pos(&mut self) {
match self.value {
SliderValue::Single(value) => {
let percentage = value.clamp(self.min, self.max) / self.max;
self.percentage = 0.0..percentage;
}
SliderValue::Range(start, end) => {
let clamped_start = start.clamp(self.min, self.max);
let clamped_end = end.clamp(self.min, self.max);
self.percentage = (clamped_start / self.max)..(clamped_end / self.max);
}
}
}
fn update_value_by_position(
&mut self,
axis: Axis,
position: Point<Pixels>,
is_start: bool,
_: &mut Window,
cx: &mut Context<Self>,
) {
let bounds = self.bounds;
let min = self.min;
let max = self.max;
let step = self.step;
let inner_pos = if axis.is_horizontal() {
position.x - bounds.left()
} else {
bounds.bottom() - position.y
};
let total_size = bounds.size.along(axis);
let percentage = inner_pos.clamp(px(0.), total_size) / total_size;
let percentage = if is_start {
percentage.clamp(0.0, self.percentage.end)
} else {
percentage.clamp(self.percentage.start, 1.0)
};
let value = min + (max - min) * percentage;
let value = (value / step).round() * step;
if is_start {
self.percentage.start = percentage;
self.value.set_start(value);
} else {
self.percentage.end = percentage;
self.value.set_end(value);
}
cx.emit(SliderEvent::Change(self.value));
cx.notify();
}
}
impl EventEmitter<SliderEvent> for SliderState {}
impl Render for SliderState {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
Empty
}
}
#[derive(IntoElement)]
pub struct Slider {
state: Entity<SliderState>,
axis: Axis,
style: StyleRefinement,
disabled: bool,
}
impl Slider {
pub fn new(state: &Entity<SliderState>) -> Self {
Self {
axis: Axis::Horizontal,
state: state.clone(),
style: StyleRefinement::default(),
disabled: false,
}
}
pub fn horizontal(mut self) -> Self {
self.axis = Axis::Horizontal;
self
}
pub fn vertical(mut self) -> Self {
self.axis = Axis::Vertical;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
#[allow(clippy::too_many_arguments)]
fn render_thumb(
&self,
start_pos: Pixels,
is_start: bool,
bar_color: Background,
thumb_color: Hsla,
radius: Corners<Pixels>,
window: &mut Window,
cx: &mut App,
) -> impl gpui::IntoElement {
let state = self.state.read(cx);
let entity_id = self.state.entity_id();
let value = state.value;
let axis = self.axis;
let id = ("slider-thumb", is_start as u32);
if self.disabled {
return div().id(id);
}
div()
.id(id)
.absolute()
.when(axis.is_horizontal(), |this| {
this.top(px(-5.)).left(start_pos).ml(-px(8.))
})
.when(axis.is_vertical(), |this| {
this.bottom(start_pos).left(px(-5.)).mb(-px(8.))
})
.flex()
.items_center()
.justify_center()
.flex_shrink_0()
.corner_radii(radius)
.bg(bar_color.opacity(0.5))
.when(cx.theme().shadow, |this| this.shadow_md())
.size_4()
.p(px(1.))
.child(
div()
.flex_shrink_0()
.size_full()
.corner_radii(radius)
.bg(thumb_color),
)
.on_mouse_down(MouseButton::Left, |_, _, cx| {
cx.stop_propagation();
})
.on_drag(DragThumb((entity_id, is_start)), |drag, _, _, cx| {
cx.stop_propagation();
cx.new(|_| drag.clone())
})
.on_drag_move(window.listener_for(
&self.state,
move |view, e: &DragMoveEvent<DragThumb>, window, cx| {
match e.drag(cx) {
DragThumb((id, is_start)) => {
if *id != entity_id {
return;
}
view.update_value_by_position(
axis,
e.event.position,
*is_start,
window,
cx,
)
}
}
},
))
.tooltip(move |window, cx| {
Tooltip::new(format!(
"{}",
if is_start { value.start() } else { value.end() }
))
.build(window, cx)
})
}
}
impl Styled for Slider {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl RenderOnce for Slider {
fn render(self, window: &mut Window, cx: &mut gpui::App) -> impl IntoElement {
let axis = self.axis;
let state = self.state.read(cx);
let is_range = state.value().is_range();
let bar_size = state.bounds.size.along(axis);
let bar_start = state.percentage.start * bar_size;
let bar_end = state.percentage.end * bar_size;
let rem_size = window.rem_size();
let bar_color = self
.style
.background
.clone()
.and_then(|bg| bg.color())
.unwrap_or(cx.theme().slider_bar.into());
let thumb_color = self
.style
.text
.clone()
.and_then(|text| text.color)
.unwrap_or_else(|| cx.theme().slider_thumb);
let corner_radii = self.style.corner_radii.clone();
let default_radius = px(999.);
let radius = Corners {
top_left: corner_radii
.top_left
.map(|v| v.to_pixels(rem_size))
.unwrap_or(default_radius),
top_right: corner_radii
.top_right
.map(|v| v.to_pixels(rem_size))
.unwrap_or(default_radius),
bottom_left: corner_radii
.bottom_left
.map(|v| v.to_pixels(rem_size))
.unwrap_or(default_radius),
bottom_right: corner_radii
.bottom_right
.map(|v| v.to_pixels(rem_size))
.unwrap_or(default_radius),
};
div()
.id(("slider", self.state.entity_id()))
.flex()
.flex_1()
.items_center()
.justify_center()
.when(axis.is_vertical(), |this| this.h(px(120.)))
.when(axis.is_horizontal(), |this| this.w_full())
.refine_style(&self.style)
.bg(cx.theme().transparent)
.text_color(cx.theme().foreground)
.child(
h_flex()
.when(!self.disabled, |this| {
this.on_mouse_down(
MouseButton::Left,
window.listener_for(
&self.state,
move |state, e: &MouseDownEvent, window, cx| {
let mut is_start = false;
if is_range {
let inner_pos = if axis.is_horizontal() {
e.position.x - state.bounds.left()
} else {
state.bounds.bottom() - e.position.y
};
let center = (bar_end - bar_start) / 2.0 + bar_start;
is_start = inner_pos < center;
}
state.update_value_by_position(
axis, e.position, is_start, window, cx,
)
},
),
)
})
.when(axis.is_horizontal(), |this| {
this.items_center().h_6().w_full()
})
.when(axis.is_vertical(), |this| {
this.justify_center().w_6().h_full()
})
.flex_shrink_0()
.child(
div()
.id("slider-bar")
.relative()
.when(axis.is_horizontal(), |this| this.w_full().h_1p5())
.when(axis.is_vertical(), |this| this.h_full().w_1p5())
.bg(bar_color.opacity(0.2))
.active(|this| this.bg(bar_color.opacity(0.4)))
.corner_radii(radius)
.child(
div()
.absolute()
.when(axis.is_horizontal(), |this| {
this.h_full().left(bar_start).right(bar_size - bar_end)
})
.when(axis.is_vertical(), |this| {
this.w_full().bottom(bar_start).top(bar_size - bar_end)
})
.bg(bar_color)
.rounded_full(),
)
.when(is_range, |this| {
this.child(self.render_thumb(
bar_start,
true,
bar_color,
thumb_color,
radius,
window,
cx,
))
})
.child(self.render_thumb(
bar_end,
false,
bar_color,
thumb_color,
radius,
window,
cx,
))
.child({
let state = self.state.clone();
canvas(
move |bounds, _, cx| state.update(cx, |r, _| r.bounds = bounds),
|_, _, _, _| {},
)
.absolute()
.size_full()
}),
),
)
}
}