From a05892f2fafe655d6430879e91b11efd9eb19118 Mon Sep 17 00:00:00 2001 From: wjian23 Date: Fri, 13 Dec 2024 14:24:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(widgets):=20=F0=9F=8E=B8=20Added=20the=20w?= =?UTF-8?q?idget=20of=20Slider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 +- .../slider_widgets_with_default_by_wgpu.png | Bin 0 -> 1912 bytes .../slider_widgets_with_material_by_wgpu.png | Bin 0 -> 4672 bytes themes/material/src/classes.rs | 2 + themes/material/src/classes/slider_cls.rs | 137 ++++++ widgets/src/lib.rs | 3 +- widgets/src/slider.rs | 389 ++++++++++++++++++ 7 files changed, 535 insertions(+), 4 deletions(-) create mode 100644 test_cases/ribir_widgets/slider/tests/slider_widgets_with_default_by_wgpu.png create mode 100644 test_cases/ribir_widgets/slider/tests/slider_widgets_with_material_by_wgpu.png create mode 100644 themes/material/src/classes/slider_cls.rs create mode 100644 widgets/src/slider.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f24dd3e7b..b296c0ede 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,11 +27,13 @@ Please only add new entries below the [Unreleased](#unreleased---releasedate) he ### Features -- **core**: Added `grab_pointer` to grabs all the pointer input. (#pr @wjian23) +- **core**: Added `grab_pointer` to grabs all the pointer input. (#669 @wjian23) +- **widgets**: Added the widget of Slider (#669 @wjian23) ### Fixed -- **core**: fix mismatch of providers. (#pr @wjian23) -- **core**: Added DeclarerWithSubscription to let Widget `Expanded` accept pipe value. (#pr @wjian23) + +- **core**: fix mismatch of providers. (#669 @wjian23) +- **core**: Added DeclarerWithSubscription to let Widget `Expanded` accept pipe value. (#669 @wjian23) ## [0.4.0-alpha.18] - 2024-12-11 diff --git a/test_cases/ribir_widgets/slider/tests/slider_widgets_with_default_by_wgpu.png b/test_cases/ribir_widgets/slider/tests/slider_widgets_with_default_by_wgpu.png new file mode 100644 index 0000000000000000000000000000000000000000..8865e256c7fce41cf0197e7afb20fefdb5e0c9c3 GIT binary patch literal 1912 zcmeAS@N?(olHy`uVBq!ia0y~yVAKP$PjRpTN$E#xlNlJ;?LA!_Ln;{GK46@n;yFph zQ>c5@%-!xTE@^3LVWB`!4g%lL&;pczj1CV6*^-v#=H}9}d-hDO^FC?e<)!82{^vnf z`XtQ_TfFnts@3a$-ETe>8XjAE^}k-ox?Mn#f8`5{t^!5A?&qGm8YJ>Rc127nP~`7> zr>8&-yI$3cYk@R`{@)z{HKKLYh|#bZO&6niWVE~(Ejy`EkNiFVdQ&MBb@0P*X3od5s; literal 0 HcmV?d00001 diff --git a/test_cases/ribir_widgets/slider/tests/slider_widgets_with_material_by_wgpu.png b/test_cases/ribir_widgets/slider/tests/slider_widgets_with_material_by_wgpu.png new file mode 100644 index 0000000000000000000000000000000000000000..b32a281321f0643824db90f187ba4348fdcdd727 GIT binary patch literal 4672 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 F{0B?Y@d5w< literal 0 HcmV?d00001 diff --git a/themes/material/src/classes.rs b/themes/material/src/classes.rs index 3d3f93eaf..f21604b8e 100644 --- a/themes/material/src/classes.rs +++ b/themes/material/src/classes.rs @@ -4,6 +4,7 @@ mod checkbox_cls; mod progress_cls; mod radio_cls; mod scrollbar_cls; +mod slider_cls; mod tooltips_cls; pub fn initd_classes() -> Classes { @@ -14,6 +15,7 @@ pub fn initd_classes() -> Classes { progress_cls::init(&mut classes); checkbox_cls::init(&mut classes); tooltips_cls::init(&mut classes); + slider_cls::init(&mut classes); classes } diff --git a/themes/material/src/classes/slider_cls.rs b/themes/material/src/classes/slider_cls.rs new file mode 100644 index 000000000..095345306 --- /dev/null +++ b/themes/material/src/classes/slider_cls.rs @@ -0,0 +1,137 @@ +use ribir_core::prelude::*; +use ribir_widgets::prelude::*; + +use crate::md; + +const INDICATOR_HEIGHT: f32 = 44.; +const TRACK_HEIGHT: f32 = 16.; +const TRACK_WIDTH: f32 = 4.; + +const SMALL_RADIUS: f32 = 2.; +const LARGE_RADIUS: f32 = 8.; +const STOP_INDICATOR_MARGIN: EdgeInsets = EdgeInsets::horizontal(6.); +const STOP_INDICATOR_SIZE: Size = Size::new(4., 4.); + +class_names! { + BASE_SLIDER_TRACK, +} + +macro_rules! stop_indicator_class { + ($($field: ident: $value: expr),* ) => { + style_class! { + v_align: VAlign::Center, + border_radius: Radius::all(SMALL_RADIUS), + margin: STOP_INDICATOR_MARGIN, + clamp: BoxClamp::fixed_size(STOP_INDICATOR_SIZE), + $($field: $value),* + } + }; +} + +pub(super) fn init(classes: &mut Classes) { + classes.insert(BASE_SLIDER_TRACK, |w| { + fn_widget! { + let flex = Provider::of::>(BuildCtx::get()).unwrap(); + part_writer!(&mut flex.flex).transition( + EasingTransition { + easing: easing::LinearEasing, + duration: md::easing::duration::SHORT2, + }); + let w = FatObj::new(w); + @ $w { + clamp: BoxClamp::fixed_height(TRACK_HEIGHT), + } + } + .into_widget() + }); + + classes.insert( + SLIDER_CONTAINER, + style_class!( + clamp: BoxClamp::fixed_height(INDICATOR_HEIGHT) + ), + ); + classes.insert(SLIDER_ACTIVE_TRACK, |w| { + fn_widget! { + let w = FatObj::new(w); + @ $w { + class: BASE_SLIDER_TRACK, + background: Palette::of(BuildCtx::get()).primary(), + border_radius: Radius::new(LARGE_RADIUS, SMALL_RADIUS, LARGE_RADIUS, SMALL_RADIUS), + } + } + .into_widget() + }); + + classes.insert(SLIDER_INACTIVE_TRACK, |w| { + fn_widget! { + let w = FatObj::new(w); + @ $w { + class: BASE_SLIDER_TRACK, + background: Palette::of(BuildCtx::get()).secondary_container(), + border_radius: Radius::new(SMALL_RADIUS, LARGE_RADIUS, SMALL_RADIUS, LARGE_RADIUS), + } + } + .into_widget() + }); + + classes.insert(SLIDER_INDICATOR, |w| { + fn_widget! { + let w = FatObj::new(w); + @ Cursor { + cursor: CursorIcon::Pointer, + @ $w { + v_align: VAlign::Center, + background: Palette::of(BuildCtx::get()).primary(), + margin: EdgeInsets::horizontal(6.), + clamp: BoxClamp::fixed_size(Size::new(TRACK_WIDTH, INDICATOR_HEIGHT)), + } + } + } + .into_widget() + }); + + classes.insert(RANGE_SLIDER_INACTIVE_TRACK_LEFT, |w| { + fn_widget! { + let w = FatObj::new(w); + @ $w { + class: BASE_SLIDER_TRACK, + border_radius: Radius::new(LARGE_RADIUS, SMALL_RADIUS, LARGE_RADIUS, SMALL_RADIUS), + background: Palette::of(BuildCtx::get()).secondary_container(), + } + } + .into_widget() + }); + + classes.insert(RANGE_SLIDER_INACTIVE_TRACK_RIGHT, |w| { + fn_widget! { + let w = FatObj::new(w); + @ $w { + class: BASE_SLIDER_TRACK, + border_radius: Radius::new(SMALL_RADIUS, LARGE_RADIUS, SMALL_RADIUS, LARGE_RADIUS,), + background: Palette::of(BuildCtx::get()).secondary_container(), + } + } + .into_widget() + }); + + classes.insert(RANGE_SLIDER_ACTIVE_TRACK, |w| { + fn_widget! { + let w = FatObj::new(w); + @ $w { + class: BASE_SLIDER_TRACK, + border_radius: Radius::all(SMALL_RADIUS), + background: Palette::of(BuildCtx::get()).primary(), + } + } + .into_widget() + }); + + classes.insert(STOP_INDICATOR_ACTIVE, stop_indicator_class! { + background: Palette::of(BuildCtx::get()).on_primary() + }); + + classes.insert(STOP_INDICATOR_INACTIVE, stop_indicator_class! { + background: Palette::of(BuildCtx::get()).on_secondary_container() + }); +} diff --git a/widgets/src/lib.rs b/widgets/src/lib.rs index ab5b0f5a7..5db001e7a 100644 --- a/widgets/src/lib.rs +++ b/widgets/src/lib.rs @@ -14,6 +14,7 @@ pub mod path; pub mod progress; pub mod radio; pub mod scrollbar; +pub mod slider; pub mod tabs; pub mod text_field; @@ -22,6 +23,6 @@ pub mod prelude { pub use super::{ avatar::*, buttons::*, checkbox::*, common_widget::*, divider::*, grid_view::*, icon::*, input::*, label::*, layout::*, link::*, lists::*, path::*, progress::*, radio::*, scrollbar::*, - tabs::*, text_field::*, transform_box::*, + slider::*, tabs::*, text_field::*, transform_box::*, }; } diff --git a/widgets/src/slider.rs b/widgets/src/slider.rs new file mode 100644 index 000000000..b7e62114f --- /dev/null +++ b/widgets/src/slider.rs @@ -0,0 +1,389 @@ +use std::{mem::swap, ops::Range}; + +use ribir_core::prelude::*; + +use crate::{ + layout::{Expanded, JustifyContent, Row}, + prelude::Stack, +}; + +class_names! { + #[doc = "Class name for the slider container"] + SLIDER_CONTAINER, + #[doc = "Class name for the slider indicator"] + SLIDER_INDICATOR, + #[doc = "Class name for the slider track"] + SLIDER_ACTIVE_TRACK, + #[doc = "Class name for the slider inactive track"] + SLIDER_INACTIVE_TRACK, + #[doc = "Class name for the left inactive track of range slider"] + RANGE_SLIDER_INACTIVE_TRACK_LEFT, + #[doc = "Class name for the right inactive track of range slider"] + RANGE_SLIDER_INACTIVE_TRACK_RIGHT, + #[doc = "Class name for the active track of range slider"] + RANGE_SLIDER_ACTIVE_TRACK, + #[doc="Class name for the active stop indicator"] + STOP_INDICATOR_ACTIVE, + #[doc="Class name for the inactive stop indicator"] + STOP_INDICATOR_INACTIVE, +} + +#[derive(Declare)] +pub struct Slider { + pub value: f32, + #[declare(default = 100.)] + pub max: f32, + #[declare(default = 0.)] + pub min: f32, + #[declare(default)] + pub divisions: Option, +} + +impl Slider { + fn set_to(&mut self, mut v: f32) { + v = v.clamp(0., 1.); + if let Some(divisions) = self.divisions { + if divisions > 0 { + v = (v * divisions as f32).round() / (divisions as f32); + } + } + + self.value = (self.min + v * (self.max - self.min)).clamp(self.min, self.max); + } + + fn ratio(&self) -> f32 { + if self.max == self.min { + return 1.; + } + let mut v = (self.value - self.min) / (self.max - self.min); + v = v.clamp(0., 1.); + if let Some(divisions) = self.divisions { + if divisions > 0 { + v = (v * divisions as f32).round() / (divisions as f32) + } + } + v + } + + fn validate(mut this: WriteRef) { + if this.max < this.min { + let Self { max, min, .. } = &mut *this; + swap(max, min); + } + + if this.value < this.min { + this.value = this.min; + } + + if this.value > this.max { + this.value = this.max; + } + } + + fn stop_indicator_track(&self) -> Option> { + let divisions = self.divisions?; + if divisions == 0 { + return None; + } + let active = (self.ratio() * divisions as f32) as usize; + Some(stop_indicator_track(divisions + 1, 0..active, vec![active])) + } +} + +fn precision(min: f32, max: f32) -> usize { + ((max - min).log10().floor() - 2.).min(-2.).abs() as usize +} + +impl Compose for Slider { + fn compose(this: impl StateWriter) -> Widget<'static> { + fn_widget! { + let u = this.modifies().subscribe(move |_| Slider::validate($this.write())); + + let mut row = @Row { align_items: Align::Center }; + let track1 = @Expanded { flex: pipe!($this.ratio())}; + let track2 = @Expanded { flex: pipe!($this.ratio()).map(|v| 1. - v)}; + + let drag_handle = Stateful::new(None); + @ Stack { + class: SLIDER_CONTAINER, + @ $row { + v_align: VAlign::Center, + on_tap: move |e| { + let width = $row.layout_size().width; + $this.write().set_to(e.position().x / width); + }, + on_disposed: move |_| u.unsubscribe(), + @ { + Provider::new(Box::new(track1.clone_writer())).with_child( + fn_widget!{ @$track1 { @Void { class: SLIDER_ACTIVE_TRACK } } } + ) + } + @Void { + class: SLIDER_INDICATOR , + on_tap: move |e| e.stop_propagation(), + on_pointer_down: move |e| { + if let Some(handle) = e.window().grab_pointer(e.current_target()) { + *$drag_handle.write() = Some( + (handle.guard(), e.global_pos().x, $this.ratio()) + ); + } + }, + on_pointer_move: move|e| if let Some((_, pos, ratio)) = $drag_handle.as_ref() { + let width = $row.layout_size().width; + let val = ratio + (e.global_pos().x - pos) / width; + $this.write().set_to(val); + }, + on_pointer_up: move |_| { + $drag_handle.write().take(); + }, + tooltips: pipe!($this.value).map(move |v| { + let precision = precision($this.min, $this.max); + format!("{:.1$}", v, precision) + }), + } + @ { + Provider::new(Box::new(track2.clone_writer())).with_child( + fn_widget! { @ $track2 { @Void { class: SLIDER_INACTIVE_TRACK } } } + ) + } + } + + @{ pipe!($this.stop_indicator_track()) } + } + } + .into_widget() + } +} + +#[derive(Declare)] +pub struct RangeSlider { + pub start: f32, + pub end: f32, + #[declare(default = 100.)] + pub max: f32, + #[declare(default = 0.)] + pub min: f32, + #[declare(default)] + pub divisions: Option, +} + +impl RangeSlider { + fn set_ratio(&mut self, mut ratio: f32) { + ratio = ratio.clamp(0., 1.); + let val = self.convert_ratio(ratio); + if (self.start - val).abs() < (self.end - val).abs() { + self.start = val; + } else { + self.end = val; + } + } + + fn set_start_ratio(&mut self, ratio: f32) { + self.start = self + .convert_ratio(ratio) + .min(self.end) + .max(self.min); + } + + fn set_end_ratio(&mut self, ratio: f32) { + self.end = self + .convert_ratio(ratio) + .max(self.start) + .min(self.max); + } + + fn convert_ratio(&self, mut ratio: f32) -> f32 { + if let Some(divisions) = self.divisions { + if divisions > 1 { + ratio = (ratio * divisions as f32).round() / (divisions as f32); + } + } + self.min + ratio * (self.max - self.min) + } + + fn ratio(&self, v: f32) -> f32 { + if self.max == self.min { + return 1.; + } + let mut v = (v - self.min) / (self.max - self.min); + v = v.clamp(0., 1.); + if let Some(divisions) = self.divisions { + if divisions > 0 { + v = (v * divisions as f32).round() / (divisions as f32); + } + } + v + } + + fn start_ratio(&self) -> f32 { self.ratio(self.start) } + + fn end_ratio(&self) -> f32 { self.ratio(self.end) } + + fn validate(mut this: WriteRef) { + if this.max < this.min { + let Self { max, min, .. } = &mut *this; + swap(max, min); + } + + if this.start > this.end { + let Self { start, end, .. } = &mut *this; + swap(start, end); + } + + if this.start < this.min { + this.start = this.min; + } + + if this.end > this.max { + this.end = this.max; + } + } + + fn stop_indicator_track(&self) -> Option> { + let divisions = self.divisions?; + if divisions == 0 { + return None; + } + let start = (self.start_ratio() * divisions as f32) as usize; + let end = (self.end_ratio() * divisions as f32) as usize; + Some(stop_indicator_track(divisions + 1, start..end + 1, vec![start, end])) + } +} + +impl Compose for RangeSlider { + fn compose(this: impl StateWriter) -> Widget<'static> { + fn_widget! { + let u = this.modifies().subscribe(move |_| RangeSlider::validate($this.write())); + + let mut row = @Row { align_items: Align::Center }; + let track1 = @Expanded { flex: pipe!($this.start_ratio()) }; + let track2 = @Expanded { flex: pipe!($this.end_ratio() - $this.start_ratio()) }; + let track3 = @Expanded { flex: pipe!($this.end_ratio()).map(|v| 1. - v) }; + let drag_handle1 = Stateful::new(None); + let drag_handle2 = Stateful::new(None); + @Stack { + class: SLIDER_CONTAINER, + @ $row { + v_align: VAlign::Center, + on_tap: move |e| { + let width = $row.layout_size().width; + $this.write().set_ratio(e.position().x / width); + }, + on_disposed: move |_| u.unsubscribe(), + @ { + Provider::new(Box::new(track1.clone_writer())).with_child( + fn_widget!{ @ $track1 { @Void { class: RANGE_SLIDER_INACTIVE_TRACK_LEFT } } } + ) + } + @Void { + class: SLIDER_INDICATOR, + tooltips: pipe!($this.start).map(move |v| { + let precision = precision($this.min, $this.max); + format!("{:.1$}", v, precision) + }), + on_tap: move |e| e.stop_propagation(), + on_pointer_down: move |e| { + if let Some(handle) = e.window().grab_pointer(e.current_target()) { + *$drag_handle1.write() = Some( + (handle.guard(), e.global_pos().x, $this.start_ratio()) + ); + } + }, + on_pointer_move: move |e| { + if let Some((_, pos, ratio)) = $drag_handle1.as_ref() { + let width = $row.layout_size().width; + let val = ratio + (e.global_pos().x - pos) / width; + $this.write().set_start_ratio(val); + } + }, + on_pointer_up: move |_| { $drag_handle1.write().take(); } + } + @ { + Provider::new(Box::new(track2.clone_writer())).with_child( + fn_widget! { @ $track2 { @Void { class: RANGE_SLIDER_ACTIVE_TRACK } } } + ) + } + @Void { + class: SLIDER_INDICATOR, + tooltips: pipe!($this.end).map(move |v| { + let precision = precision($this.min, $this.max); + format!("{:.1$}", v, precision) + }), + on_tap: move |e| e.stop_propagation(), + on_pointer_down: move |e| { + if let Some(handle) = e.window().grab_pointer(e.current_target()) { + *$drag_handle2.write() = Some( + (handle.guard(), e.global_pos().x, $this.end_ratio()) + ); + } + }, + on_pointer_move: move |e| { + if let Some((_, pos, ratio)) = $drag_handle2.as_ref() { + let width = $row.layout_size().width; + let val = ratio + (e.global_pos().x - pos) / width; + $this.write().set_end_ratio(val); + } + }, + on_pointer_up: move |_| { $drag_handle2.write().take(); } + } + @ { + Provider::new(Box::new(track3.clone_writer())).with_child( + fn_widget! { @ $track3 { @Void { class: RANGE_SLIDER_INACTIVE_TRACK_RIGHT } } } + ) + } + } + @{ pipe!($this.stop_indicator_track()) } + } + } + .into_widget() + } +} + +fn stop_indicator_track(cnt: usize, actives: Range, filter: Vec) -> Widget<'static> { + fn_widget!( + @IgnorePointer { + ignore: true, + @Row { + v_align: VAlign::Center, + align_items: Align::Center, + justify_content: JustifyContent::SpaceBetween, + @ { + (0..cnt).map(move |i| { + @Void { + class: if actives.contains(&i) { + STOP_INDICATOR_ACTIVE + } else { + STOP_INDICATOR_INACTIVE + }, + visible: !filter.contains(&i), + } + }) + } + } + } + ) + .into_widget() +} + +#[cfg(test)] +mod tests { + use ribir_core::test_helper::*; + use ribir_dev_helper::*; + + use super::*; + use crate::prelude::*; + + widget_image_tests!( + slider_widgets, + WidgetTester::new(self::column! { + justify_content: JustifyContent::SpaceAround, + align_items: Align::Center, + @Slider { value: 32. } + @Slider { value: 32., divisions: Some(10) } + @RangeSlider { start: 10., end: 73. } + @RangeSlider { start: 10., end: 73., divisions: Some(10) } + }) + .with_wnd_size(Size::new(300., 200.)) + .with_comparison(0.002) + ); +}