Jason Lee 2024-07-17 16:01:27 +08:00 committed by GitHub
parent 3aaa348fbb
commit d71b697f41
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 252 additions and 3 deletions

View file

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

View file

@ -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)),
)
}
}

View file

@ -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
View 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()
}),
)
}
}

View file

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