slider: Add vertical and reverse support to Slider. (#556)

- Fix to support stop at any position.
- Fix click bounds.
This commit is contained in:
Jason Lee 2025-01-20 15:22:28 +08:00 committed by GitHub
parent 06e9b1a25c
commit 355dfa5556
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 257 additions and 69 deletions

View file

@ -1,16 +1,18 @@
use gpui::{
div, px, IntoElement, ParentElement, Render, Styled, View, ViewContext, VisualContext,
WindowContext,
div, hsla, px, Hsla, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
View, ViewContext, VisualContext, WindowContext,
};
use ui::{
button::Button,
clipboard::Clipboard,
divider::Divider,
h_flex,
indicator::Indicator,
progress::Progress,
skeleton::Skeleton,
slider::{Slider, SliderEvent},
v_flex, IconName, Sizable,
theme::Colorize,
v_flex, ColorExt as _, ContextModal, IconName, Sizable,
};
pub struct ProgressStory {
@ -20,6 +22,9 @@ pub struct ProgressStory {
slider1_value: f32,
slider2: View<Slider>,
slider2_value: f32,
slider_hsl: [View<Slider>; 4],
slider_hsl_value: Hsla,
_subscritions: Vec<Subscription>,
}
impl super::Story for ProgressStory {
@ -45,30 +50,87 @@ impl ProgressStory {
.default_value(15.)
.step(15.)
});
cx.subscribe(&slider1, |this, _, event: &SliderEvent, cx| match event {
SliderEvent::Change(value) => {
this.slider1_value = *value;
cx.notify();
}
})
.detach();
let slider2 = cx.new_view(|_| Slider::horizontal().min(0.).max(5.).step(1.0));
cx.subscribe(&slider2, |this, _, event: &SliderEvent, cx| match event {
SliderEvent::Change(value) => {
this.slider2_value = *value;
cx.notify();
}
})
.detach();
let slider_hsl = [
cx.new_view(|_| {
Slider::vertical()
.reverse()
.min(0.)
.max(1.)
.step(0.01)
.default_value(0.)
}),
cx.new_view(|_| {
Slider::vertical()
.reverse()
.min(0.)
.max(1.)
.step(0.01)
.default_value(0.5)
}),
cx.new_view(|_| {
Slider::vertical()
.reverse()
.min(0.)
.max(1.)
.step(0.01)
.default_value(0.5)
}),
cx.new_view(|_| {
Slider::vertical()
.reverse()
.min(0.)
.max(1.)
.step(0.01)
.default_value(1.)
}),
];
let mut _subscritions = vec![
cx.subscribe(&slider1, |this, _, event: &SliderEvent, cx| match event {
SliderEvent::Change(value) => {
this.slider1_value = *value;
cx.notify();
}
}),
cx.subscribe(&slider2, |this, _, event: &SliderEvent, cx| match event {
SliderEvent::Change(value) => {
this.slider2_value = *value;
cx.notify();
}
}),
];
_subscritions.extend(
slider_hsl
.iter()
.map(|slider| {
cx.subscribe(slider, |this, _, event: &SliderEvent, cx| match event {
SliderEvent::Change(_) => {
this.slider_hsl_value = hsla(
this.slider_hsl[0].read(cx).value(),
this.slider_hsl[1].read(cx).value(),
this.slider_hsl[2].read(cx).value(),
this.slider_hsl[3].read(cx).value(),
);
cx.notify();
}
})
})
.collect::<Vec<_>>(),
);
Self {
focus_handle: cx.focus_handle(),
value: 50.,
slider1_value: 15.,
slider2_value: 1.,
slider1_value: 0.,
slider2_value: 0.,
slider1,
slider2,
slider_hsl,
slider_hsl_value: gpui::red(),
_subscritions,
}
}
@ -85,6 +147,8 @@ impl gpui::FocusableView for ProgressStory {
impl Render for ProgressStory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let rgb = SharedString::from(self.slider_hsl_value.to_hex_string());
v_flex()
.items_center()
.gap_y_3()
@ -160,6 +224,65 @@ impl Render for ProgressStory {
.child(self.slider2.clone())
.child(format!("Slider 2: {}", self.slider2_value)),
)
.child(
h_flex()
.gap_3()
.justify_start()
.child(
v_flex()
.w_32()
.h_40()
.gap_3()
.items_center()
.child(self.slider_hsl[0].clone())
.child(format!("H: {:.0}", self.slider_hsl_value.h * 360.)),
)
.child(
v_flex()
.w_32()
.h_40()
.gap_3()
.items_center()
.child(self.slider_hsl[1].clone())
.child(format!("S: {:.0}", self.slider_hsl_value.s * 100.)),
)
.child(
v_flex()
.w_32()
.h_40()
.gap_3()
.items_center()
.child(self.slider_hsl[2].clone())
.child(format!("L: {:.0}", self.slider_hsl_value.l * 100.)),
)
.child(
v_flex()
.w_32()
.h_40()
.gap_3()
.items_center()
.child(self.slider_hsl[3].clone())
.child(format!("A: {:.0}", self.slider_hsl_value.a * 100.)),
)
.child(
h_flex()
.gap_2()
.items_center()
.child(
h_flex()
.w_32()
.p_1()
.rounded_lg()
.justify_center()
.bg(self.slider_hsl_value)
.child(rgb.clone())
.text_color(self.slider_hsl_value.invert()),
)
.child(Clipboard::new("copy-hsl").value(rgb).on_copied(|_, cx| {
cx.push_notification("Color copied to clipboard.")
})),
),
)
.child(
h_flex()
.mt_5()

View file

@ -6,8 +6,11 @@ use serde::{de::Error, Deserialize, Deserializer};
use crate::theme::hsl;
use anyhow::Result;
pub(crate) trait ColorExt {
/// Extension methods for Hsla.
pub trait ColorExt {
/// Convert the color to a hex string. For example, "#F8FAFC".
fn to_hex_string(&self) -> String;
/// Parse a hex string to a color.
fn parse_hex_string(hex: &str) -> Result<Hsla>;
}

View file

@ -1,6 +1,6 @@
use crate::{theme::ActiveTheme, tooltip::Tooltip};
use crate::{h_flex, theme::ActiveTheme, tooltip::Tooltip, AxisExt};
use gpui::{
canvas, div, prelude::FluentBuilder as _, px, relative, Axis, Bounds, DragMoveEvent, EntityId,
canvas, div, prelude::FluentBuilder as _, px, Axis, Bounds, DragMoveEvent, EntityId,
EventEmitter, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, ParentElement as _,
Pixels, Point, Render, StatefulInteractiveElement as _, Styled, ViewContext,
VisualContext as _,
@ -20,6 +20,8 @@ pub struct Slider {
max: f32,
step: f32,
value: f32,
reverse: bool,
percentage: f32,
bounds: Bounds<Pixels>,
}
@ -31,23 +33,39 @@ impl Slider {
max: 100.0,
step: 1.0,
value: 0.0,
percentage: 0.0,
reverse: false,
bounds: Bounds::default(),
}
}
/// Create a horizontal slider.
pub fn horizontal() -> Self {
Self::new(Axis::Horizontal)
}
/// Create a vertical slider.
pub fn vertical() -> Self {
Self::new(Axis::Vertical)
}
/// Set the reverse direction of the slider, default: false
pub fn reverse(mut self) -> Self {
self.reverse = true;
self
}
/// 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
}
@ -60,27 +78,24 @@ impl Slider {
/// Set the default value of the slider, default: 0.0
pub fn default_value(mut self, value: f32) -> Self {
self.value = value;
self.update_thumb_pos();
self
}
/// Set the value of the slider.
pub fn set_value(&mut self, value: f32, cx: &mut gpui::ViewContext<Self>) {
self.value = value;
self.update_thumb_pos();
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;
fn update_thumb_pos(&mut self) {
self.percentage = self.value.clamp(self.min, self.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)
/// Get the value of the slider.
pub fn value(&self) -> f32 {
self.value
}
/// Update value by mouse position
@ -95,25 +110,45 @@ impl Slider {
let max = self.max;
let step = self.step;
let value = match axis {
let percentage = match axis {
Axis::Horizontal => {
let relative = (position.x - bounds.left()) / bounds.size.width;
min + (max - min) * relative
if self.reverse {
1. - (position.x - bounds.left()).clamp(px(0.), bounds.size.width)
/ bounds.size.width
} else {
(position.x - bounds.left()).clamp(px(0.), bounds.size.width)
/ bounds.size.width
}
}
Axis::Vertical => {
let relative = (position.y - bounds.top()) / bounds.size.height;
max - (max - min) * relative
if self.reverse {
1. - (position.y - bounds.top()).clamp(px(0.), bounds.size.height)
/ bounds.size.height
} else {
(position.y - bounds.top()).clamp(px(0.), bounds.size.height)
/ bounds.size.height
}
}
};
let value = match axis {
Axis::Horizontal => min + (max - min) * percentage,
Axis::Vertical => max - (max - min) * percentage,
};
let value = (value / step).round() * step;
self.percentage = percentage;
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 {
fn render_thumb(
&self,
thumb_bar_size: Pixels,
cx: &mut ViewContext<Self>,
) -> impl gpui::IntoElement {
let value = self.value;
let entity_id = cx.entity_id();
@ -136,9 +171,22 @@ impl Slider {
},
))
.absolute()
.top(px(-5.))
.left(relative(self.relative_value()))
.ml(-px(8.))
.map(|this| match self.reverse {
true => this
.when(self.axis.is_horizontal(), |this| {
this.bottom(px(-5.)).right(thumb_bar_size).mr(-px(8.))
})
.when(self.axis.is_vertical(), |this| {
this.bottom(thumb_bar_size).right(px(-5.)).mb(-px(8.))
}),
false => this
.when(self.axis.is_horizontal(), |this| {
this.top(px(-5.)).left(thumb_bar_size).ml(-px(8.))
})
.when(self.axis.is_vertical(), |this| {
this.top(thumb_bar_size).left(px(-5.)).mt(-px(8.))
}),
})
.size_4()
.rounded_full()
.border_1()
@ -157,31 +205,45 @@ impl EventEmitter<SliderEvent> for Slider {}
impl Render for Slider {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
let thumb_bar_size = match self.axis {
Axis::Horizontal => self.percentage * self.bounds.size.width,
Axis::Vertical => self.percentage * self.bounds.size.height,
};
h_flex()
.id("slider")
.on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
.h_5()
.when(self.axis.is_horizontal(), |this| {
this.items_center().h_6().w_full()
})
.when(self.axis.is_vertical(), |this| {
this.justify_center().w_6().h_full()
})
.flex_shrink_0()
.child(
div()
.id("slider-bar")
.relative()
.w_full()
.my_1p5()
.h_1p5()
.when(self.axis.is_horizontal(), |this| this.w_full().h_1p5())
.when(self.axis.is_vertical(), |this| this.h_full().w_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()))
.when(!self.reverse, |this| this.top_0().left_0())
.when(self.reverse, |this| this.bottom_0().right_0())
.when(self.axis.is_horizontal(), |this| {
this.h_full().w(thumb_bar_size)
})
.when(self.axis.is_vertical(), |this| {
this.w_full().h(thumb_bar_size)
})
.bg(cx.theme().slider_bar)
.rounded_l(px(3.)),
.rounded_full(),
)
.child(self.render_thumb(cx))
.child(self.render_thumb(thumb_bar_size, cx))
.child({
let view = cx.view().clone();
canvas(

View file

@ -70,19 +70,31 @@ pub fn box_shadow(
}
}
pub trait Colorize {
/// Returns a new color with the given opacity.
///
/// The opacity is a value between 0.0 and 1.0, where 0.0 is fully transparent and 1.0 is fully opaque.
fn opacity(&self, opacity: f32) -> Hsla;
/// Returns a new color with each channel divided by the given divisor.
///
/// The divisor in range of 0.0 .. 1.0
fn divide(&self, divisor: f32) -> Hsla;
/// Return inverted color
fn invert(&self) -> Hsla;
/// Return inverted lightness
fn invert_l(&self) -> Hsla;
/// Return a new color with the lightness increased by the given factor.
///
/// factor range: 0.0 .. 1.0
fn lighten(&self, amount: f32) -> Hsla;
/// Return a new color with the darkness increased by the given factor.
///
/// factor range: 0.0 .. 1.0
fn darken(&self, amount: f32) -> Hsla;
/// Return a new color with the same lightness and alpha but different hue and saturation.
fn apply(&self, base_color: Hsla) -> Hsla;
}
impl Colorize for Hsla {
/// Returns a new color with the given opacity.
///
/// The opacity is a value between 0.0 and 1.0, where 0.0 is fully transparent and 1.0 is fully opaque.
fn opacity(&self, factor: f32) -> Hsla {
Hsla {
a: self.a * factor.clamp(0.0, 1.0),
@ -90,9 +102,6 @@ impl Colorize for Hsla {
}
}
/// Returns a new color with each channel divided by the given divisor.
///
/// The divisor in range of 0.0 .. 1.0
fn divide(&self, divisor: f32) -> Hsla {
Hsla {
a: divisor,
@ -100,17 +109,15 @@ impl Colorize for Hsla {
}
}
/// Return inverted color
fn invert(&self) -> Hsla {
Hsla {
h: (self.h + 1.8) % 3.6,
h: 1.0 - self.h,
s: 1.0 - self.s,
l: 1.0 - self.l,
a: self.a,
}
}
/// Return inverted lightness
fn invert_l(&self) -> Hsla {
Hsla {
l: 1.0 - self.l,
@ -118,25 +125,18 @@ impl Colorize for Hsla {
}
}
/// Return a new color with the lightness increased by the given factor.
///
/// factor range: 0.0 .. 1.0
fn lighten(&self, factor: f32) -> Hsla {
let l = self.l * (1.0 + factor.clamp(0.0, 1.0));
Hsla { l, ..*self }
}
/// Return a new color with the darkness increased by the given factor.
///
/// factor range: 0.0 .. 1.0
fn darken(&self, factor: f32) -> Hsla {
let l = self.l * (1.0 - factor.clamp(0.0, 1.0));
Hsla { l, ..*self }
}
/// Return a new color with the same lightness and alpha but different hue and saturation.
fn apply(&self, new_color: Hsla) -> Hsla {
Hsla {
h: new_color.h,