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:
Jason Lee 2025-08-08 16:59:23 +08:00 committed by GitHub
parent 900a4aa652
commit 85e2a72d03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 365 additions and 131 deletions

View file

@ -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(

View file

@ -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(

View file

@ -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 {}