668 lines
22 KiB
Rust
668 lines
22 KiB
Rust
use std::ops::Range;
|
|
|
|
use crate::{h_flex, 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)]
|
|
struct DragThumb((EntityId, bool));
|
|
|
|
impl Render for DragThumb {
|
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
|
Empty
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct DragSlider(EntityId);
|
|
|
|
impl Render for DragSlider {
|
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
|
Empty
|
|
}
|
|
}
|
|
|
|
/// Events emitted by the [`SliderState`].
|
|
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<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 {
|
|
/// 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))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Check if the value is a single value.
|
|
#[inline]
|
|
pub fn is_single(&self) -> bool {
|
|
matches!(self, SliderValue::Single(_))
|
|
}
|
|
|
|
/// Check if the value is a range of values.
|
|
#[inline]
|
|
pub fn is_range(&self) -> bool {
|
|
matches!(self, SliderValue::Range(_, _))
|
|
}
|
|
|
|
/// Get the start value.
|
|
pub fn start(&self) -> f32 {
|
|
match self {
|
|
SliderValue::Single(value) => *value,
|
|
SliderValue::Range(start, _) => *start,
|
|
}
|
|
}
|
|
|
|
/// Get the end value.
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The scale mode of the slider.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
pub enum SliderScale {
|
|
/// Linear scale where values change uniformly across the slider range.
|
|
/// This is the default mode.
|
|
#[default]
|
|
Linear,
|
|
/// Logarithmic scale where the distance between values increases exponentially.
|
|
///
|
|
/// This is useful for parameters that have a large range of values where smaller
|
|
/// changes are more significant at lower values. Common examples include:
|
|
///
|
|
/// - Volume controls (human hearing perception is logarithmic)
|
|
/// - Frequency controls (musical notes follow a logarithmic scale)
|
|
/// - Zoom levels
|
|
/// - Any parameter where you want finer control at lower values
|
|
///
|
|
/// # For example
|
|
///
|
|
/// ```
|
|
/// use gpui_component::slider::{SliderState, SliderScale};
|
|
///
|
|
/// let slider = SliderState::new()
|
|
/// .min(1.0) // Must be > 0 for logarithmic scale
|
|
/// .max(1000.0)
|
|
/// .scale(SliderScale::Logarithmic);
|
|
/// ```
|
|
///
|
|
/// - Moving the slider 1/3 of the way will yield ~10
|
|
/// - Moving it 2/3 of the way will yield ~100
|
|
/// - The full range covers 3 orders of magnitude evenly
|
|
Logarithmic,
|
|
}
|
|
|
|
impl SliderScale {
|
|
#[inline]
|
|
pub fn is_linear(&self) -> bool {
|
|
matches!(self, SliderScale::Linear)
|
|
}
|
|
|
|
#[inline]
|
|
pub fn is_logarithmic(&self) -> bool {
|
|
matches!(self, SliderScale::Logarithmic)
|
|
}
|
|
}
|
|
|
|
/// 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<f32>,
|
|
/// The bounds of the slider after rendered.
|
|
bounds: Bounds<Pixels>,
|
|
scale: SliderScale,
|
|
}
|
|
|
|
impl SliderState {
|
|
/// Create a new [`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(),
|
|
scale: SliderScale::default(),
|
|
}
|
|
}
|
|
|
|
/// Set the minimum value of the slider, default: 0.0
|
|
pub fn min(mut self, min: f32) -> Self {
|
|
if self.scale.is_logarithmic() {
|
|
assert!(
|
|
min > 0.0,
|
|
"`min` must be greater than 0 for SliderScale::Logarithmic"
|
|
);
|
|
assert!(
|
|
min < self.max,
|
|
"`min` must be less than `max` for Logarithmic scale"
|
|
);
|
|
}
|
|
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 {
|
|
if self.scale.is_logarithmic() {
|
|
assert!(
|
|
max > self.min,
|
|
"`max` must be greater than `min` for Logarithmic scale"
|
|
);
|
|
}
|
|
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 scale of the slider, default: [`SliderScale::Linear`].
|
|
pub fn scale(mut self, scale: SliderScale) -> Self {
|
|
if scale.is_logarithmic() {
|
|
assert!(
|
|
self.min > 0.0,
|
|
"`min` must be greater than 0 for Logarithmic scale"
|
|
);
|
|
assert!(
|
|
self.max > self.min,
|
|
"`max` must be greater than `min` for Logarithmic scale"
|
|
);
|
|
}
|
|
self.scale = scale;
|
|
self.update_thumb_pos();
|
|
self
|
|
}
|
|
|
|
/// Set the default value of the slider, default: 0.0
|
|
pub fn default_value(mut self, value: impl Into<SliderValue>) -> 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<SliderValue>,
|
|
_: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.value = value.into();
|
|
self.update_thumb_pos();
|
|
cx.notify();
|
|
}
|
|
|
|
/// Get the value of the slider.
|
|
pub fn value(&self) -> SliderValue {
|
|
self.value
|
|
}
|
|
|
|
/// Converts a value between 0.0 and 1.0 to a value between the minimum and maximum value,
|
|
/// depending on the chosen scale.
|
|
fn percentage_to_value(&self, percentage: f32) -> f32 {
|
|
match self.scale {
|
|
SliderScale::Linear => self.min + (self.max - self.min) * percentage,
|
|
SliderScale::Logarithmic => {
|
|
// when percentage is 0, this simplifies to (max/min)^0 * min = 1 * min = min
|
|
// when percentage is 1, this simplifies to (max/min)^1 * min = (max*min)/min = max
|
|
// we clamp just to make sure we don't have issue with floating point precision
|
|
let base = self.max / self.min;
|
|
(base.powf(percentage) * self.min).clamp(self.min, self.max)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Converts a value between the minimum and maximum value to a value between 0.0 and 1.0,
|
|
/// depending on the chosen scale.
|
|
fn value_to_percentage(&self, value: f32) -> f32 {
|
|
match self.scale {
|
|
SliderScale::Linear => {
|
|
let range = self.max - self.min;
|
|
if range <= 0.0 {
|
|
0.0
|
|
} else {
|
|
(value - self.min) / range
|
|
}
|
|
}
|
|
SliderScale::Logarithmic => {
|
|
let base = self.max / self.min;
|
|
(value / self.min).log(base).clamp(0.0, 1.0)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn update_thumb_pos(&mut self) {
|
|
match self.value {
|
|
SliderValue::Single(value) => {
|
|
let percentage = self.value_to_percentage(value.clamp(self.min, 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 =
|
|
self.value_to_percentage(clamped_start)..self.value_to_percentage(clamped_end);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Update value by mouse position
|
|
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 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 = self.percentage_to_value(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
|
|
}
|
|
}
|
|
|
|
/// A Slider element.
|
|
#[derive(IntoElement)]
|
|
pub struct Slider {
|
|
state: Entity<SliderState>,
|
|
axis: Axis,
|
|
style: StyleRefinement,
|
|
disabled: bool,
|
|
}
|
|
|
|
impl Slider {
|
|
/// Create a new [`Slider`] element bind to the [`SliderState`].
|
|
pub fn new(state: &Entity<SliderState>) -> 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<Pixels>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> impl gpui::IntoElement {
|
|
let entity_id = self.state.entity_id();
|
|
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;
|
|
}
|
|
|
|
// set value by mouse position
|
|
view.update_value_by_position(
|
|
axis,
|
|
e.event.position,
|
|
*is_start,
|
|
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 entity_id = self.state.entity_id();
|
|
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()
|
|
.id("slider-bar-container")
|
|
.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(!self.disabled && !is_range, |this| {
|
|
this.on_drag(DragSlider(entity_id), |drag, _, _, cx| {
|
|
cx.stop_propagation();
|
|
cx.new(|_| drag.clone())
|
|
})
|
|
.on_drag_move(window.listener_for(
|
|
&self.state,
|
|
move |view, e: &DragMoveEvent<DragSlider>, window, cx| match e.drag(cx)
|
|
{
|
|
DragSlider(id) => {
|
|
if *id != entity_id {
|
|
return;
|
|
}
|
|
|
|
view.update_value_by_position(
|
|
axis,
|
|
e.event.position,
|
|
false,
|
|
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()
|
|
}),
|
|
),
|
|
)
|
|
}
|
|
}
|