Add Slider (#39)
https://github.com/user-attachments/assets/a1799cd9-45af-413c-b32b-c60fb1b8d586
This commit is contained in:
parent
3aaa348fbb
commit
d71b697f41
5 changed files with 252 additions and 3 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Slider>,
|
||||
slider1_value: f32,
|
||||
slider2: View<Slider>,
|
||||
slider2_value: f32,
|
||||
}
|
||||
|
||||
impl ProgressStory {
|
||||
pub fn view(cx: &mut WindowContext) -> View<Self> {
|
||||
cx.new_view(|_| Self { value: 50. })
|
||||
cx.new_view(Self::new)
|
||||
}
|
||||
|
||||
fn new(cx: &mut ViewContext<Self>) -> 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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
200
crates/ui/src/slider.rs
Normal file
200
crates/ui/src/slider.rs
Normal file
|
|
@ -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<Box<dyn Fn(&f32, &mut WindowContext) + 'static>>,
|
||||
bounds: Bounds<Pixels>,
|
||||
}
|
||||
|
||||
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>) {
|
||||
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<Pixels>,
|
||||
cx: &mut gpui::ViewContext<Self>,
|
||||
) {
|
||||
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<Self>) -> 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<DragThumb>, 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>) {
|
||||
self.update_value_by_position(event.position, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Slider {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> 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()
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Colors> for Theme {
|
|||
tab_active_foreground: colors.foreground,
|
||||
indicator: colors.secondary_foreground,
|
||||
progress_bar: colors.primary,
|
||||
slider_bar: colors.primary,
|
||||
slider_thumb: colors.background,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue