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) -> impl IntoElement { Empty } } pub enum SliderEvent { Change(SliderValue), } /// The value of the slider, can be a single value or a range of values. /// /// - Can from a f32 value, which will be treated as a single value. /// - Or from a (f32, f32) tuple, which will be treated as a range of values. /// /// The default value is `SliderValue::Single(0.0)`. #[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 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> for SliderValue { fn from(value: Range) -> Self { SliderValue::Range(value.start, value.end) } } impl Default for SliderValue { fn default() -> Self { SliderValue::Single(0.) } } impl SliderValue { /// Clamp the value to the given range. 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); } } } /// State of the [`Slider`]. pub struct SliderState { min: f32, max: f32, step: f32, value: SliderValue, /// When is single value mode, only `end` is used, the start is always 0.0. percentage: Range, /// The bounds of the slider after rendered. bounds: Bounds, } 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(), } } /// Set the minimum value of the slider, default: 0.0 pub fn min(mut self, min: f32) -> Self { self.min = min; self.update_thumb_pos(); self } /// Set the maximum value of the slider, default: 100.0 pub fn max(mut self, max: f32) -> Self { self.max = max; self.update_thumb_pos(); 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: impl Into) -> Self { self.value = value.into(); self.update_thumb_pos(); self } /// Set the value of the slider. pub fn set_value( &mut self, value: impl Into, _: &mut Window, cx: &mut Context, ) { self.value = value.into(); self.update_thumb_pos(); cx.notify(); } /// Get the value of the slider. 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); } } } /// Update value by mouse position fn update_value_by_position( &mut self, axis: Axis, position: Point, is_start: bool, _: &mut Window, cx: &mut Context, ) { 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 for SliderState {} impl Render for SliderState { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { Empty } } /// A Slider element. #[derive(IntoElement)] pub struct Slider { state: Entity, axis: Axis, style: StyleRefinement, disabled: bool, } impl Slider { /// Create a new [`Slider`] element bind to the [`SliderState`]. pub fn new(state: &Entity) -> Self { Self { axis: Axis::Horizontal, state: state.clone(), style: StyleRefinement::default(), disabled: false, } } /// As a horizontal slider. pub fn horizontal(mut self) -> Self { self.axis = Axis::Horizontal; self } /// As a vertical slider. pub fn vertical(mut self) -> Self { self.axis = Axis::Vertical; self } /// Set the disabled state of the slider, default: false 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, 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, window, cx| { match e.drag(cx) { DragThumb((id, is_start)) => { if *id != entity_id { return; } // set value by mouse position 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() }), ), ) } }