slider: Add to support range to Slider. (#1121)
- Add to support Styled for Slider, and `bg`, `text_color`, `radius` can change the slider bar and thumb. ## Break Changes - The `reverse` method is removed from Slider, the Slider with vertical now is always display from bottom to top. <img width="1401" height="1176" alt="image" src="https://github.com/user-attachments/assets/656592d6-ffac-4fd1-b569-848092c081bc" />
This commit is contained in:
parent
900a4aa652
commit
85e2a72d03
3 changed files with 365 additions and 131 deletions
|
|
@ -1,13 +1,13 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
hsla, App, AppContext, Context, Entity, Focusable, Hsla, IntoElement, ParentElement, Render,
|
hsla, px, App, AppContext, Context, Entity, Focusable, Hsla, IntoElement, ParentElement,
|
||||||
SharedString, Styled, Subscription, Window,
|
Render, SharedString, Styled, Subscription, Window,
|
||||||
};
|
};
|
||||||
use gpui_component::{
|
use gpui_component::{
|
||||||
checkbox::Checkbox,
|
checkbox::Checkbox,
|
||||||
clipboard::Clipboard,
|
clipboard::Clipboard,
|
||||||
h_flex,
|
h_flex,
|
||||||
slider::{Slider, SliderEvent, SliderState},
|
slider::{Slider, SliderEvent, SliderState},
|
||||||
v_flex, Colorize as _, ContextModal, StyledExt,
|
v_flex, ActiveTheme, Colorize as _, ContextModal, StyledExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::section;
|
use crate::section;
|
||||||
|
|
@ -18,8 +18,10 @@ pub struct SliderStory {
|
||||||
slider1_value: f32,
|
slider1_value: f32,
|
||||||
slider2: Entity<SliderState>,
|
slider2: Entity<SliderState>,
|
||||||
slider2_value: f32,
|
slider2_value: f32,
|
||||||
|
slider3: Entity<SliderState>,
|
||||||
slider_hsl: [Entity<SliderState>; 4],
|
slider_hsl: [Entity<SliderState>; 4],
|
||||||
slider_hsl_value: Hsla,
|
slider_hsl_value: Hsla,
|
||||||
|
slider4: Entity<SliderState>,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
_subscritions: Vec<Subscription>,
|
_subscritions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
@ -48,11 +50,17 @@ impl SliderStory {
|
||||||
SliderState::new()
|
SliderState::new()
|
||||||
.min(-255.)
|
.min(-255.)
|
||||||
.max(255.)
|
.max(255.)
|
||||||
.default_value(15.)
|
.default_value(75.)
|
||||||
.step(15.)
|
.step(15.)
|
||||||
});
|
});
|
||||||
|
|
||||||
let slider2 = cx.new(|_| SliderState::new().min(0.).max(5.).step(1.0));
|
let slider2 = cx.new(|_| {
|
||||||
|
SliderState::new()
|
||||||
|
.min(0.)
|
||||||
|
.max(5.)
|
||||||
|
.step(1.0)
|
||||||
|
.default_value(2.)
|
||||||
|
});
|
||||||
let slider_hsl = [
|
let slider_hsl = [
|
||||||
cx.new(|_| {
|
cx.new(|_| {
|
||||||
SliderState::new()
|
SliderState::new()
|
||||||
|
|
@ -84,16 +92,32 @@ impl SliderStory {
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let slider3 = cx.new(|_| {
|
||||||
|
SliderState::new()
|
||||||
|
.min(0.)
|
||||||
|
.max(100.)
|
||||||
|
.default_value(12.0..45.0)
|
||||||
|
.step(1.)
|
||||||
|
});
|
||||||
|
|
||||||
|
let slider4 = cx.new(|_| {
|
||||||
|
SliderState::new()
|
||||||
|
.min(0.)
|
||||||
|
.max(360.)
|
||||||
|
.default_value(100.0..300.0)
|
||||||
|
.step(1.)
|
||||||
|
});
|
||||||
|
|
||||||
let mut _subscritions = vec![
|
let mut _subscritions = vec![
|
||||||
cx.subscribe(&slider1, |this, _, event: &SliderEvent, cx| match event {
|
cx.subscribe(&slider1, |this, _, event: &SliderEvent, cx| match event {
|
||||||
SliderEvent::Change(value) => {
|
SliderEvent::Change(value) => {
|
||||||
this.slider1_value = *value;
|
this.slider1_value = value.start();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
cx.subscribe(&slider2, |this, _, event: &SliderEvent, cx| match event {
|
cx.subscribe(&slider2, |this, _, event: &SliderEvent, cx| match event {
|
||||||
SliderEvent::Change(value) => {
|
SliderEvent::Change(value) => {
|
||||||
this.slider2_value = *value;
|
this.slider2_value = value.start();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
@ -106,10 +130,10 @@ impl SliderStory {
|
||||||
cx.subscribe(slider, |this, _, event: &SliderEvent, cx| match event {
|
cx.subscribe(slider, |this, _, event: &SliderEvent, cx| match event {
|
||||||
SliderEvent::Change(_) => {
|
SliderEvent::Change(_) => {
|
||||||
this.slider_hsl_value = hsla(
|
this.slider_hsl_value = hsla(
|
||||||
this.slider_hsl[0].read(cx).value(),
|
this.slider_hsl[0].read(cx).value().start(),
|
||||||
this.slider_hsl[1].read(cx).value(),
|
this.slider_hsl[1].read(cx).value().start(),
|
||||||
this.slider_hsl[2].read(cx).value(),
|
this.slider_hsl[2].read(cx).value().start(),
|
||||||
this.slider_hsl[3].read(cx).value(),
|
this.slider_hsl[3].read(cx).value().start(),
|
||||||
);
|
);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
@ -128,6 +152,8 @@ impl SliderStory {
|
||||||
slider2_value: 0.,
|
slider2_value: 0.,
|
||||||
slider1,
|
slider1,
|
||||||
slider2,
|
slider2,
|
||||||
|
slider3,
|
||||||
|
slider4,
|
||||||
slider_hsl,
|
slider_hsl,
|
||||||
slider_hsl_value: gpui::red(),
|
slider_hsl_value: gpui::red(),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
|
@ -147,10 +173,9 @@ impl Render for SliderStory {
|
||||||
let rgb = SharedString::from(self.slider_hsl_value.to_hex());
|
let rgb = SharedString::from(self.slider_hsl_value.to_hex());
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.items_center()
|
|
||||||
.gap_y_3()
|
.gap_y_3()
|
||||||
.child(
|
.child(
|
||||||
h_flex().justify_between().child(
|
h_flex().child(
|
||||||
Checkbox::new("disabled")
|
Checkbox::new("disabled")
|
||||||
.checked(self.disabled)
|
.checked(self.disabled)
|
||||||
.label("Disabled")
|
.label("Disabled")
|
||||||
|
|
@ -164,24 +189,41 @@ impl Render for SliderStory {
|
||||||
section("Horizontal Slider")
|
section("Horizontal Slider")
|
||||||
.max_w_md()
|
.max_w_md()
|
||||||
.v_flex()
|
.v_flex()
|
||||||
.child(
|
.child(Slider::new(&self.slider1).disabled(self.disabled))
|
||||||
Slider::new(&self.slider1)
|
|
||||||
.horizontal()
|
|
||||||
.disabled(self.disabled),
|
|
||||||
)
|
|
||||||
.child(format!("Value: {}", self.slider1_value)),
|
.child(format!("Value: {}", self.slider1_value)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
section("Slider (0 - 5)")
|
section("Slider (0 - 5) and with color")
|
||||||
.max_w_md()
|
.max_w_md()
|
||||||
.v_flex()
|
.v_flex()
|
||||||
.child(
|
.child(
|
||||||
Slider::new(&self.slider2)
|
Slider::new(&self.slider2)
|
||||||
.horizontal()
|
.disabled(self.disabled)
|
||||||
.disabled(self.disabled),
|
.bg(cx.theme().success)
|
||||||
|
.text_color(cx.theme().success_foreground),
|
||||||
)
|
)
|
||||||
.child(format!("Value: {}", self.slider2_value)),
|
.child(format!("Value: {}", self.slider2_value)),
|
||||||
)
|
)
|
||||||
|
.child(
|
||||||
|
section("Range Mode")
|
||||||
|
.max_w_md()
|
||||||
|
.v_flex()
|
||||||
|
.child(Slider::new(&self.slider3).disabled(self.disabled))
|
||||||
|
.child(format!("Value: {}", self.slider3.read(cx).value())),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
section("Vertical with Range")
|
||||||
|
.max_w_md()
|
||||||
|
.v_flex()
|
||||||
|
.child(
|
||||||
|
Slider::new(&self.slider4)
|
||||||
|
.vertical()
|
||||||
|
.h(px(200.))
|
||||||
|
.rounded(px(2.))
|
||||||
|
.disabled(self.disabled),
|
||||||
|
)
|
||||||
|
.child(format!("Value: {}", self.slider4.read(cx).value())),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
section(
|
section(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
|
@ -215,7 +257,6 @@ impl Render for SliderStory {
|
||||||
.child(
|
.child(
|
||||||
Slider::new(&self.slider_hsl[0])
|
Slider::new(&self.slider_hsl[0])
|
||||||
.vertical()
|
.vertical()
|
||||||
.reverse()
|
|
||||||
.disabled(self.disabled),
|
.disabled(self.disabled),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
|
@ -234,7 +275,6 @@ impl Render for SliderStory {
|
||||||
.child(
|
.child(
|
||||||
Slider::new(&self.slider_hsl[1])
|
Slider::new(&self.slider_hsl[1])
|
||||||
.vertical()
|
.vertical()
|
||||||
.reverse()
|
|
||||||
.disabled(self.disabled),
|
.disabled(self.disabled),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
|
@ -253,7 +293,6 @@ impl Render for SliderStory {
|
||||||
.child(
|
.child(
|
||||||
Slider::new(&self.slider_hsl[2])
|
Slider::new(&self.slider_hsl[2])
|
||||||
.vertical()
|
.vertical()
|
||||||
.reverse()
|
|
||||||
.disabled(self.disabled),
|
.disabled(self.disabled),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
|
@ -272,7 +311,6 @@ impl Render for SliderStory {
|
||||||
.child(
|
.child(
|
||||||
Slider::new(&self.slider_hsl[3])
|
Slider::new(&self.slider_hsl[3])
|
||||||
.vertical()
|
.vertical()
|
||||||
.reverse()
|
|
||||||
.disabled(self.disabled),
|
.disabled(self.disabled),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
use crate::{h_flex, tooltip::Tooltip, ActiveTheme, AxisExt};
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use crate::{h_flex, tooltip::Tooltip, ActiveTheme, AxisExt, StyledExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
canvas, div, prelude::FluentBuilder as _, px, App, AppContext as _, Axis, Bounds, Context,
|
canvas, div, prelude::FluentBuilder as _, px, Along, App, AppContext as _, Axis, Background,
|
||||||
DragMoveEvent, Empty, Entity, EntityId, EventEmitter, InteractiveElement, IntoElement,
|
Bounds, Context, Corners, DragMoveEvent, Empty, Entity, EntityId, EventEmitter, Hsla,
|
||||||
MouseButton, MouseDownEvent, ParentElement as _, Pixels, Point, Render, RenderOnce,
|
InteractiveElement, IntoElement, MouseButton, MouseDownEvent, ParentElement as _, Pixels,
|
||||||
StatefulInteractiveElement as _, Styled, Window,
|
Point, Render, RenderOnce, StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DragThumb(EntityId);
|
pub struct DragThumb((EntityId, bool));
|
||||||
|
|
||||||
impl Render for DragThumb {
|
impl Render for DragThumb {
|
||||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
|
@ -16,7 +18,104 @@ impl Render for DragThumb {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum SliderEvent {
|
pub enum SliderEvent {
|
||||||
Change(f32),
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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`].
|
/// State of the [`Slider`].
|
||||||
|
|
@ -24,9 +123,11 @@ pub struct SliderState {
|
||||||
min: f32,
|
min: f32,
|
||||||
max: f32,
|
max: f32,
|
||||||
step: f32,
|
step: f32,
|
||||||
value: 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>,
|
bounds: Bounds<Pixels>,
|
||||||
percentage: f32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SliderState {
|
impl SliderState {
|
||||||
|
|
@ -35,8 +136,8 @@ impl SliderState {
|
||||||
min: 0.0,
|
min: 0.0,
|
||||||
max: 100.0,
|
max: 100.0,
|
||||||
step: 1.0,
|
step: 1.0,
|
||||||
value: 0.0,
|
value: SliderValue::default(),
|
||||||
percentage: 0.0,
|
percentage: (0.0..0.0),
|
||||||
bounds: Bounds::default(),
|
bounds: Bounds::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -62,72 +163,81 @@ impl SliderState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the default value of the slider, default: 0.0
|
/// Set the default value of the slider, default: 0.0
|
||||||
pub fn default_value(mut self, value: f32) -> Self {
|
pub fn default_value(mut self, value: impl Into<SliderValue>) -> Self {
|
||||||
self.value = value;
|
self.value = value.into();
|
||||||
self.update_thumb_pos();
|
self.update_thumb_pos();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the value of the slider.
|
/// Set the value of the slider.
|
||||||
pub fn set_value(&mut self, value: f32, _: &mut gpui::Window, cx: &mut gpui::Context<Self>) {
|
pub fn set_value(
|
||||||
self.value = value;
|
&mut self,
|
||||||
|
value: impl Into<SliderValue>,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.value = value.into();
|
||||||
self.update_thumb_pos();
|
self.update_thumb_pos();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the value of the slider.
|
/// Get the value of the slider.
|
||||||
pub fn value(&self) -> f32 {
|
pub fn value(&self) -> SliderValue {
|
||||||
self.value
|
self.value
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_thumb_pos(&mut self) {
|
fn update_thumb_pos(&mut self) {
|
||||||
self.percentage = self.value.clamp(self.min, self.max) / self.max;
|
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
|
/// Update value by mouse position
|
||||||
fn update_value_by_position(
|
fn update_value_by_position(
|
||||||
&mut self,
|
&mut self,
|
||||||
axis: Axis,
|
axis: Axis,
|
||||||
reverse: bool,
|
|
||||||
position: Point<Pixels>,
|
position: Point<Pixels>,
|
||||||
_: &mut gpui::Window,
|
is_start: bool,
|
||||||
cx: &mut gpui::Context<Self>,
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let bounds = self.bounds;
|
let bounds = self.bounds;
|
||||||
let min = self.min;
|
let min = self.min;
|
||||||
let max = self.max;
|
let max = self.max;
|
||||||
let step = self.step;
|
let step = self.step;
|
||||||
|
|
||||||
let percentage = match axis {
|
let inner_pos = if axis.is_horizontal() {
|
||||||
Axis::Horizontal => {
|
position.x - bounds.left()
|
||||||
if reverse {
|
} else {
|
||||||
1. - (position.x - bounds.left()).clamp(px(0.), bounds.size.width)
|
bounds.bottom() - position.y
|
||||||
/ bounds.size.width
|
};
|
||||||
} else {
|
let total_size = bounds.size.along(axis);
|
||||||
(position.x - bounds.left()).clamp(px(0.), bounds.size.width)
|
let percentage = inner_pos.clamp(px(0.), total_size) / total_size;
|
||||||
/ bounds.size.width
|
|
||||||
}
|
let percentage = if is_start {
|
||||||
}
|
percentage.clamp(0.0, self.percentage.end)
|
||||||
Axis::Vertical => {
|
} else {
|
||||||
if reverse {
|
percentage.clamp(self.percentage.start, 1.0)
|
||||||
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 = min + (max - min) * percentage;
|
||||||
let value = (value / step).round() * step;
|
let value = (value / step).round() * step;
|
||||||
|
|
||||||
self.percentage = percentage;
|
if is_start {
|
||||||
self.value = value.clamp(self.min, self.max);
|
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.emit(SliderEvent::Change(self.value));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
@ -145,7 +255,7 @@ impl Render for SliderState {
|
||||||
pub struct Slider {
|
pub struct Slider {
|
||||||
state: Entity<SliderState>,
|
state: Entity<SliderState>,
|
||||||
axis: Axis,
|
axis: Axis,
|
||||||
reverse: bool,
|
style: StyleRefinement,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,8 +264,8 @@ impl Slider {
|
||||||
pub fn new(state: &Entity<SliderState>) -> Self {
|
pub fn new(state: &Entity<SliderState>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
axis: Axis::Horizontal,
|
axis: Axis::Horizontal,
|
||||||
reverse: false,
|
|
||||||
state: state.clone(),
|
state: state.clone(),
|
||||||
|
style: StyleRefinement::default(),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -172,37 +282,62 @@ impl Slider {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the reverse direction of the slider, default: false
|
|
||||||
pub fn reverse(mut self) -> Self {
|
|
||||||
self.reverse = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the disabled state of the slider, default: false
|
/// Set the disabled state of the slider, default: false
|
||||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||||
self.disabled = disabled;
|
self.disabled = disabled;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn render_thumb(
|
fn render_thumb(
|
||||||
&self,
|
&self,
|
||||||
thumb_bar_size: Pixels,
|
start_pos: Pixels,
|
||||||
|
is_start: bool,
|
||||||
|
bar_color: Background,
|
||||||
|
thumb_color: Hsla,
|
||||||
|
radius: Corners<Pixels>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> impl gpui::IntoElement {
|
) -> impl gpui::IntoElement {
|
||||||
let state = self.state.read(cx);
|
let state = self.state.read(cx);
|
||||||
let entity_id = self.state.entity_id();
|
let entity_id = self.state.entity_id();
|
||||||
let value = state.value;
|
let value = state.value;
|
||||||
let reverse = self.reverse;
|
|
||||||
let axis = self.axis;
|
let axis = self.axis;
|
||||||
|
let id = ("slider-thumb", is_start as u32);
|
||||||
|
|
||||||
if self.disabled {
|
if self.disabled {
|
||||||
return div().id("slider-thumb");
|
return div().id(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id("slider-thumb")
|
.id(id)
|
||||||
.on_drag(DragThumb(entity_id), |drag, _, _, cx| {
|
.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.stop_propagation();
|
||||||
cx.new(|_| drag.clone())
|
cx.new(|_| drag.clone())
|
||||||
})
|
})
|
||||||
|
|
@ -210,7 +345,7 @@ impl Slider {
|
||||||
&self.state,
|
&self.state,
|
||||||
move |view, e: &DragMoveEvent<DragThumb>, window, cx| {
|
move |view, e: &DragMoveEvent<DragThumb>, window, cx| {
|
||||||
match e.drag(cx) {
|
match e.drag(cx) {
|
||||||
DragThumb(id) => {
|
DragThumb((id, is_start)) => {
|
||||||
if *id != entity_id {
|
if *id != entity_id {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -218,8 +353,8 @@ impl Slider {
|
||||||
// set value by mouse position
|
// set value by mouse position
|
||||||
view.update_value_by_position(
|
view.update_value_by_position(
|
||||||
axis,
|
axis,
|
||||||
reverse,
|
|
||||||
e.event.position,
|
e.event.position,
|
||||||
|
*is_start,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|
@ -227,51 +362,76 @@ impl Slider {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.absolute()
|
.tooltip(move |window, cx| {
|
||||||
.map(|this| match reverse {
|
Tooltip::new(format!(
|
||||||
true => this
|
"{}",
|
||||||
.when(axis.is_horizontal(), |this| {
|
if is_start { value.start() } else { value.end() }
|
||||||
this.bottom(px(-5.)).right(thumb_bar_size).mr(-px(8.))
|
))
|
||||||
})
|
.build(window, cx)
|
||||||
.when(axis.is_vertical(), |this| {
|
|
||||||
this.bottom(thumb_bar_size).right(px(-5.)).mb(-px(8.))
|
|
||||||
}),
|
|
||||||
false => this
|
|
||||||
.when(axis.is_horizontal(), |this| {
|
|
||||||
this.top(px(-5.)).left(thumb_bar_size).ml(-px(8.))
|
|
||||||
})
|
|
||||||
.when(axis.is_vertical(), |this| {
|
|
||||||
this.top(thumb_bar_size).left(px(-5.)).mt(-px(8.))
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
.size_4()
|
}
|
||||||
.rounded_full()
|
}
|
||||||
.border_1()
|
|
||||||
.border_color(cx.theme().slider_bar.opacity(0.9))
|
impl Styled for Slider {
|
||||||
.when(cx.theme().shadow, |this| this.shadow_md())
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
.bg(cx.theme().slider_thumb)
|
&mut self.style
|
||||||
.tooltip(move |window, cx| Tooltip::new(format!("{}", value)).build(window, cx))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for Slider {
|
impl RenderOnce for Slider {
|
||||||
fn render(self, window: &mut Window, cx: &mut gpui::App) -> impl IntoElement {
|
fn render(self, window: &mut Window, cx: &mut gpui::App) -> impl IntoElement {
|
||||||
let state = self.state.read(cx);
|
|
||||||
let axis = self.axis;
|
let axis = self.axis;
|
||||||
let reverse = self.reverse;
|
let state = self.state.read(cx);
|
||||||
let thumb_bar_size = match axis {
|
let is_range = state.value().is_range();
|
||||||
Axis::Horizontal => state.percentage * state.bounds.size.width,
|
let bar_size = state.bounds.size.along(axis);
|
||||||
Axis::Vertical => state.percentage * state.bounds.size.height,
|
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()
|
div()
|
||||||
.id(("slider", self.state.entity_id()))
|
.id(("slider", self.state.entity_id()))
|
||||||
.flex()
|
.flex()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.when(axis.is_vertical(), |this| {
|
.items_center()
|
||||||
this.items_center().justify_center()
|
.justify_center()
|
||||||
})
|
.when(axis.is_vertical(), |this| this.h(px(120.)))
|
||||||
.when(axis.is_horizontal(), |this| this.w_full())
|
.when(axis.is_horizontal(), |this| this.w_full())
|
||||||
|
.refine_style(&self.style)
|
||||||
|
.bg(cx.theme().transparent)
|
||||||
|
.text_color(cx.theme().foreground)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.when(!self.disabled, |this| {
|
.when(!self.disabled, |this| {
|
||||||
|
|
@ -279,9 +439,20 @@ impl RenderOnce for Slider {
|
||||||
MouseButton::Left,
|
MouseButton::Left,
|
||||||
window.listener_for(
|
window.listener_for(
|
||||||
&self.state,
|
&self.state,
|
||||||
move |view, e: &MouseDownEvent, window, cx| {
|
move |state, e: &MouseDownEvent, window, cx| {
|
||||||
view.update_value_by_position(
|
let mut is_start = false;
|
||||||
axis, reverse, e.position, window, cx,
|
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,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -300,24 +471,41 @@ impl RenderOnce for Slider {
|
||||||
.relative()
|
.relative()
|
||||||
.when(axis.is_horizontal(), |this| this.w_full().h_1p5())
|
.when(axis.is_horizontal(), |this| this.w_full().h_1p5())
|
||||||
.when(axis.is_vertical(), |this| this.h_full().w_1p5())
|
.when(axis.is_vertical(), |this| this.h_full().w_1p5())
|
||||||
.bg(cx.theme().slider_bar.opacity(0.2))
|
.bg(bar_color.opacity(0.2))
|
||||||
.active(|this| this.bg(cx.theme().slider_bar.opacity(0.4)))
|
.active(|this| this.bg(bar_color.opacity(0.4)))
|
||||||
.rounded(px(3.))
|
.corner_radii(radius)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.absolute()
|
.absolute()
|
||||||
.when(!reverse, |this| this.top_0().left_0())
|
|
||||||
.when(reverse, |this| this.bottom_0().right_0())
|
|
||||||
.when(axis.is_horizontal(), |this| {
|
.when(axis.is_horizontal(), |this| {
|
||||||
this.h_full().w(thumb_bar_size)
|
this.h_full().left(bar_start).right(bar_size - bar_end)
|
||||||
})
|
})
|
||||||
.when(axis.is_vertical(), |this| {
|
.when(axis.is_vertical(), |this| {
|
||||||
this.w_full().h(thumb_bar_size)
|
this.w_full().bottom(bar_start).top(bar_size - bar_end)
|
||||||
})
|
})
|
||||||
.bg(cx.theme().slider_bar)
|
.bg(bar_color)
|
||||||
.rounded_full(),
|
.rounded_full(),
|
||||||
)
|
)
|
||||||
.child(self.render_thumb(thumb_bar_size, window, cx))
|
.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({
|
.child({
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
canvas(
|
canvas(
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ use crate::{
|
||||||
ActiveTheme,
|
ActiveTheme,
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, point, px, App, Axis, BoxShadow, DefiniteLength, Div, Edges, Element, FocusHandle, Hsla,
|
div, point, px, App, Axis, BoxShadow, Corners, DefiniteLength, Div, Edges, Element,
|
||||||
Pixels, Refineable, StyleRefinement, Styled, Window,
|
FocusHandle, Hsla, Pixels, Refineable, StyleRefinement, Styled, Window,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
@ -193,6 +193,14 @@ pub trait StyledExt: Styled + Sized {
|
||||||
.shadow_lg()
|
.shadow_lg()
|
||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set corner radii for the element.
|
||||||
|
fn corner_radii(self, radius: Corners<Pixels>) -> Self {
|
||||||
|
self.rounded_tl(radius.top_left)
|
||||||
|
.rounded_tr(radius.top_right)
|
||||||
|
.rounded_bl(radius.bottom_left)
|
||||||
|
.rounded_br(radius.bottom_right)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: Styled> StyledExt for E {}
|
impl<E: Styled> StyledExt for E {}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue