diff --git a/README.md b/README.md index eae2d27a..a1f7b0c0 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ A UI components for building desktop application by using [GPUI](https://gpui.rs - [x] Progress - [x] ProgressBar - [x] Indicator +- [x] Slider - [ ] Skeleton - [ ] DatePicker - [ ] DateTimePicker diff --git a/crates/story/src/progress_story.rs b/crates/story/src/progress_story.rs index 8436e9d4..d3ce3768 100644 --- a/crates/story/src/progress_story.rs +++ b/crates/story/src/progress_story.rs @@ -3,17 +3,50 @@ use gpui::{ WindowContext, }; use ui::{ - button::Button, h_flex, indicator::Indicator, progress::Progress, v_flex, Clickable, IconName, - Size, + button::Button, divider::Divider, h_flex, indicator::Indicator, progress::Progress, + slider::Slider, v_flex, Clickable, IconName, Size, }; pub struct ProgressStory { value: f32, + slider1: View, + slider1_value: f32, + slider2: View, + slider2_value: f32, } impl ProgressStory { pub fn view(cx: &mut WindowContext) -> View { - cx.new_view(|_| Self { value: 50. }) + cx.new_view(Self::new) + } + + fn new(cx: &mut ViewContext) -> Self { + let slider1 = Slider::horizontal() + .min(-255.) + .max(255.) + .default_value(15.) + .step(15.) + .on_change(cx.listener(|this, value, cx| { + this.slider1_value = *value; + cx.notify(); + })); + + let slider2 = Slider::horizontal() + .min(0.) + .max(5.) + .step(1.0) + .on_change(cx.listener(|this, value, cx| { + this.slider2_value = *value; + cx.notify(); + })); + + Self { + value: 50., + slider1_value: 15., + slider2_value: 1., + slider1: cx.new_view(|_| slider1), + slider2: cx.new_view(|_| slider2), + } } pub fn set_value(&mut self, value: f32) { @@ -91,5 +124,15 @@ impl Render for ProgressStory { ) .child(Indicator::new().size(px(64.))), ) + .child(Divider::horizontal().mt_10().label("Slider")) + .child(self.slider1.clone()) + .child(format!("Slider 1: {}", self.slider1_value)) + .child( + v_flex() + .gap_3() + .w(px(200.)) + .child(self.slider2.clone()) + .child(format!("Slider 2: {}", self.slider2_value)), + ) } } diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 9b3f37b5..fff16537 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -25,6 +25,7 @@ pub mod progress; pub mod radio; pub mod resizable; pub mod scroll; +pub mod slider; pub mod switch; pub mod tab; pub mod table; diff --git a/crates/ui/src/slider.rs b/crates/ui/src/slider.rs new file mode 100644 index 00000000..359066f2 --- /dev/null +++ b/crates/ui/src/slider.rs @@ -0,0 +1,200 @@ +use crate::{ + theme::{ActiveTheme, Colorize}, + tooltip::Tooltip, +}; +use gpui::{ + canvas, div, px, relative, Axis, Bounds, DragMoveEvent, EntityId, InteractiveElement, + IntoElement, MouseButton, MouseDownEvent, ParentElement as _, Pixels, Point, Render, + StatefulInteractiveElement as _, Styled, ViewContext, VisualContext as _, WindowContext, +}; + +#[derive(Clone, Render)] +pub struct DragThumb(EntityId); + +/// A slider component. +pub struct Slider { + axis: Axis, + min: f32, + max: f32, + step: f32, + value: f32, + on_change: Option>, + bounds: Bounds, +} + +impl Slider { + fn new(axis: Axis) -> Self { + Self { + axis, + min: 0.0, + max: 100.0, + step: 1.0, + value: 0.0, + on_change: None, + bounds: Bounds::default(), + } + } + + pub fn horizontal() -> Self { + Self::new(Axis::Horizontal) + } + + /// Set the minimum value of the slider, default: 0.0 + pub fn min(mut self, min: f32) -> Self { + self.min = min; + self + } + + /// Set the maximum value of the slider, default: 100.0 + pub fn max(mut self, max: f32) -> Self { + self.max = max; + 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 default value of the slider, default: 0.0 + pub fn default_value(mut self, value: f32) -> Self { + self.value = value; + self + } + + /// Set the on_change callback of the slider. + pub fn on_change(mut self, on_change: impl Fn(&f32, &mut WindowContext) + 'static) -> Self { + self.on_change = Some(Box::new(on_change)); + self + } + + /// Set the value of the slider. + pub fn set_value(&mut self, value: f32, cx: &mut gpui::ViewContext) { + self.value = value; + 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; + + 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) + } + + /// Update value by mouse position + fn update_value_by_position( + &mut self, + position: Point, + cx: &mut gpui::ViewContext, + ) { + let bounds = self.bounds; + let axis = self.axis; + let min = self.min; + let max = self.max; + let step = self.step; + + let value = match axis { + Axis::Horizontal => { + let relative = (position.x - bounds.left()) / bounds.size.width; + min + (max - min) * relative + } + Axis::Vertical => { + let relative = (position.y - bounds.top()) / bounds.size.height; + max - (max - min) * relative + } + }; + let value = (value / step).round() * step; + + self.value = value.clamp(self.min, self.max); + if let Some(on_change) = &self.on_change { + on_change(&self.value, cx); + } + cx.notify(); + } + + fn render_thumb(&self, cx: &mut ViewContext) -> impl gpui::IntoElement { + let value = self.value; + let entity_id = cx.entity_id(); + + div() + .id("slider-thumb") + .on_drag(DragThumb(entity_id), |drag, cx| { + cx.stop_propagation(); + cx.new_view(|_| drag.clone()) + }) + .on_drag_move(cx.listener( + move |view, e: &DragMoveEvent, cx| match e.drag(cx) { + DragThumb(id) => { + if *id != entity_id { + return; + } + + // set value by mouse position + view.update_value_by_position(e.event.position, cx) + } + }, + )) + .absolute() + .top(px(-4.)) + .left(relative(self.relative_value())) + .ml(-px(8.)) + .size_4() + .rounded_full() + .border_1() + .border_color(cx.theme().slider_bar.opacity(0.9)) + .shadow_md() + .bg(cx.theme().slider_thumb) + .tooltip(move |cx| Tooltip::new(format!("{}", value), cx)) + } + + fn on_mouse_down(&mut self, event: &MouseDownEvent, cx: &mut gpui::ViewContext) { + self.update_value_by_position(event.position, cx); + } +} + +impl Render for Slider { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .id("slider") + .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down)) + .py_1() + .child( + div() + .id("slider-bar") + .relative() + .w_full() + .h_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())) + .bg(cx.theme().slider_bar) + .rounded_l(px(3.)), + ) + .child(self.render_thumb(cx)) + .child({ + let view = cx.view().clone(); + canvas( + move |bounds, cx| view.update(cx, |r, _| r.bounds = bounds), + |_, _, _| {}, + ) + .absolute() + .size_full() + }), + ) + } +} diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index 2d1ce606..c9a27eb0 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -306,6 +306,8 @@ pub struct Theme { pub tab_active_foreground: Hsla, pub indicator: Hsla, pub progress_bar: Hsla, + pub slider_bar: Hsla, + pub slider_thumb: Hsla, } impl Global for Theme {} @@ -369,6 +371,8 @@ impl From for Theme { tab_active_foreground: colors.foreground, indicator: colors.secondary_foreground, progress_bar: colors.primary, + slider_bar: colors.primary, + slider_thumb: colors.background, } } }