slider: Add vertical and reverse support to Slider. (#556)
- Fix to support stop at any position. - Fix click bounds.
This commit is contained in:
parent
06e9b1a25c
commit
355dfa5556
4 changed files with 257 additions and 69 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue