diff --git a/demo/src/app.rs b/demo/src/app.rs index eae129d..4deb83b 100644 --- a/demo/src/app.rs +++ b/demo/src/app.rs @@ -73,6 +73,7 @@ fn TheRouter(is_routing: RwSignal) -> impl IntoView { + diff --git a/demo/src/pages/components.rs b/demo/src/pages/components.rs index 76d7bd8..3223c65 100644 --- a/demo/src/pages/components.rs +++ b/demo/src/pages/components.rs @@ -284,6 +284,13 @@ pub(crate) fn gen_menu_data() -> Vec { }, ], }, + MenuGroupOption { + label: "Utility Components".into(), + children: vec![MenuItemOption { + value: "scrollbar".into(), + label: "Scrollbar".into(), + }], + }, MenuGroupOption { label: "Config Components".into(), children: vec![MenuItemOption { diff --git a/demo_markdown/docs/scrollbar/mod.md b/demo_markdown/docs/scrollbar/mod.md new file mode 100644 index 0000000..e1e99e1 --- /dev/null +++ b/demo_markdown/docs/scrollbar/mod.md @@ -0,0 +1,32 @@ +# Scrollbar + +```rust demo +view! { + +

+ r#"This being said, the world is moving in the direction opposite to Clarke's predictions. In 2001: A Space Odyssey, in the year of 2001, which has already passed, human beings have built magnificent cities in space, and established permanent colonies on the moon, and huge nuclear-powered spacecraft have sailed to Saturn. However, today, in 2018, the walk on the moon has become a distant memory.And the farthest reach of our manned space flights is just as long as the two-hour mileage of a high-speed train passing through my city. At the same time, information technology is developing at an unimaginable speed. With the entire world covered by the Internet, people have gradually lost their interest in space, as they find themselves increasingly comfortable in the space created by IT. Instead of an exploration of the real space, which is full of real difficulties, people now just prefer to experience virtual space through VR. Just like someone said, "You promised me an ocean of stars, but you actually gave me Facebook.""# +

+
+} +``` + +### Scroll horizontally + +```rust demo +view! { + +

+ r#"This being said, the world is moving in the direction opposite to Clarke's predictions. In 2001: A Space Odyssey, in the year of 2001, which has already passed, human beings have built magnificent cities in space, and established permanent colonies on the moon, and huge nuclear-powered spacecraft have sailed to Saturn. However, today, in 2018, the walk on the moon has become a distant memory.And the farthest reach of our manned space flights is just as long as the two-hour mileage of a high-speed train passing through my city. At the same time, information technology is developing at an unimaginable speed. With the entire world covered by the Internet, people have gradually lost their interest in space, as they find themselves increasingly comfortable in the space created by IT. Instead of an exploration of the real space, which is full of real difficulties, people now just prefer to experience virtual space through VR. Just like someone said, "You promised me an ocean of stars, but you actually gave me Facebook.""# +

+
+} +``` + +### Scrollbar Props + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| class | `OptionalProp>` | `Default::default()` | Additional classes for the scrollbar element. | +| style | `Option>` | `Default::default()` | Scrollbar's style. | +| size | `u8` | `8` | Size of scrollbar. | +| children | `Children` | | Scrollbar's content. | diff --git a/demo_markdown/src/lib.rs b/demo_markdown/src/lib.rs index 30166d1..78f378c 100644 --- a/demo_markdown/src/lib.rs +++ b/demo_markdown/src/lib.rs @@ -55,6 +55,7 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt "PopoverMdPage" => "../docs/popover/mod.md", "ProgressMdPage" => "../docs/progress/mod.md", "RadioMdPage" => "../docs/radio/mod.md", + "ScrollbarMdPage" => "../docs/scrollbar/mod.md", "SelectMdPage" => "../docs/select/mod.md", "SkeletonMdPage" => "../docs/skeleton/mod.md", "SliderMdPage" => "../docs/slider/mod.md", diff --git a/thaw/src/lib.rs b/thaw/src/lib.rs index 5f44cca..733c9ff 100644 --- a/thaw/src/lib.rs +++ b/thaw/src/lib.rs @@ -28,6 +28,7 @@ mod modal; mod popover; mod progress; mod radio; +mod scrollbar; mod select; mod skeleton; mod slider; @@ -71,6 +72,7 @@ pub use modal::*; pub use popover::*; pub use progress::*; pub use radio::*; +pub use scrollbar::*; pub use select::*; pub use skeleton::*; pub use slider::*; diff --git a/thaw/src/scrollbar/mod.rs b/thaw/src/scrollbar/mod.rs new file mode 100644 index 0000000..e5f806a --- /dev/null +++ b/thaw/src/scrollbar/mod.rs @@ -0,0 +1,302 @@ +mod theme; + +pub use theme::ScrollbarTheme; + +use crate::{use_theme, Theme}; +use leptos::{leptos_dom::helpers::WindowListenerHandle, *}; +use thaw_utils::{class_list, mount_style, OptionalProp}; + +#[component] +pub fn Scrollbar( + #[prop(optional, into)] class: OptionalProp>, + #[prop(optional, into)] style: Option>, + #[prop(default = 8)] size: u8, + children: Children, +) -> impl IntoView { + mount_style("scrollbar", include_str!("./scrollbar.css")); + let theme = use_theme(Theme::light); + let css_vars = Memo::new(move |_| { + let mut css_vars = String::new(); + theme.with(|theme| { + css_vars.push_str(&format!( + "--thaw-scrollbar-background-color: {};", + theme.scrollbar.background_color + )); + css_vars.push_str(&format!( + "--thaw-scrollbar-background-color-hover: {};", + theme.scrollbar.background_color_hover + )); + css_vars.push_str(&format!("--thaw-scrollbar-size: {}px;", size)); + }); + css_vars + }); + + let container_ref = NodeRef::::new(); + let content_ref = NodeRef::::new(); + let x_track_ref = NodeRef::::new(); + let y_track_ref = NodeRef::::new(); + let is_show_x_thumb = RwSignal::new(false); + let is_show_y_thumb = RwSignal::new(false); + let x_track_width = RwSignal::new(0); + let y_track_height = RwSignal::new(0); + let container_width = RwSignal::new(0); + let container_height = RwSignal::new(0); + let container_scroll_top = RwSignal::new(0); + let container_scroll_left = RwSignal::new(0); + let content_width = RwSignal::new(0); + let content_height = RwSignal::new(0); + let thumb_status = StoredValue::new(None::); + + let x_thumb_width = Memo::new(move |_| { + let content_width = f64::from(content_width.get()); + let x_track_width = f64::from(x_track_width.get()); + let container_width = f64::from(container_width.get()); + if content_width <= 0.0 { + return 0.0; + } + x_track_width * container_width / content_width + }); + let x_thumb_left = Memo::new(move |_| { + let x_track_width = f64::from(x_track_width.get()); + let x_thumb_width = f64::from(x_thumb_width.get()); + if x_track_width == x_thumb_width { + is_show_x_thumb.set(false); + return 0.0; + } + + let container_width = f64::from(container_width.get()); + let container_scroll_left = f64::from(container_scroll_left.get()); + let content_width = f64::from(content_width.get()); + + let diff = content_width - container_width; + if diff <= 0.0 { + 0.0 + } else { + (container_scroll_left / diff) * (x_track_width - x_thumb_width) + } + }); + + let y_thumb_height = Memo::new(move |_| { + let content_height = f64::from(content_height.get()); + let y_track_height = f64::from(y_track_height.get()); + let container_height = f64::from(container_height.get()); + if content_height <= 0.0 { + return 0.0; + } + y_track_height * container_height / content_height + }); + let y_thumb_top = Memo::new(move |_| { + let y_track_height = f64::from(y_track_height.get()); + let y_thumb_height = f64::from(y_thumb_height.get()); + if y_track_height == y_thumb_height { + is_show_y_thumb.set(false); + return 0.0; + } + + let container_height = f64::from(container_height.get()); + let container_scroll_top = f64::from(container_scroll_top.get()); + let content_height = f64::from(content_height.get()); + + let diff = content_height - container_height; + if diff <= 0.0 { + 0.0 + } else { + (container_scroll_top / diff) * (y_track_height - y_thumb_height) + } + }); + + let sync_scroll_state = move || { + if let Some(el) = container_ref.get_untracked() { + container_scroll_top.set(el.scroll_top()); + container_scroll_left.set(el.scroll_left()); + } + }; + let sync_position_state = move || { + if let Some(el) = container_ref.get_untracked() { + container_width.set(el.offset_width()); + container_height.set(el.offset_height()); + } + if let Some(el) = content_ref.get_untracked() { + content_width.set(el.offset_width()); + content_height.set(el.offset_height()); + } + if let Some(el) = x_track_ref.get() { + x_track_width.set(el.offset_width()); + } + if let Some(el) = y_track_ref.get() { + y_track_height.set(el.offset_height()); + } + }; + let on_mouseenter = move |_| { + is_show_x_thumb.set(true); + is_show_y_thumb.set(true); + thumb_status.update_value(|thumb_status| { + if thumb_status.is_some() { + *thumb_status = Some(ThumbStatus::Enter); + } + }); + sync_position_state(); + sync_scroll_state(); + }; + let on_mouseleave = move |_| { + thumb_status.update_value(|thumb_status| { + if thumb_status.is_some() { + *thumb_status = Some(ThumbStatus::DelayLeave); + } else { + is_show_y_thumb.set(false); + is_show_x_thumb.set(false); + } + }); + }; + + let on_scroll = move |_| { + sync_scroll_state(); + }; + + let x_trumb_mousemove_handle = StoredValue::new(None::); + let x_trumb_mouseup_handle = StoredValue::new(None::); + let memo_x_left = StoredValue::new(0); + let memo_mouse_x = StoredValue::new(0); + let on_x_thumb_mousedown = move |e: ev::MouseEvent| { + e.prevent_default(); + e.stop_propagation(); + let handle = window_event_listener(ev::mousemove, move |e| { + let container_width = container_width.get(); + let content_width = content_width.get(); + let x_track_width = x_track_width.get(); + let x_thumb_width = x_thumb_width.get() as i32; + + let x_diff = e.client_x() - memo_mouse_x.get_value(); + let to_scroll_left_upper_bound = content_width - container_width; + let scroll_left = + (x_diff * to_scroll_left_upper_bound) / (x_track_width - x_thumb_width); + + let mut to_scroll_left = memo_x_left.get_value() + scroll_left; + to_scroll_left = to_scroll_left.min(to_scroll_left_upper_bound); + to_scroll_left = to_scroll_left.max(0); + + if let Some(el) = container_ref.get_untracked() { + el.set_scroll_left(to_scroll_left); + } + }); + x_trumb_mousemove_handle.set_value(Some(handle)); + let handle = window_event_listener(ev::mouseup, move |_| { + x_trumb_mousemove_handle.update_value(|handle| { + if let Some(handle) = handle.take() { + handle.remove(); + } + }); + x_trumb_mouseup_handle.update_value(|handle| { + if let Some(handle) = handle.take() { + handle.remove(); + } + }); + thumb_status.update_value(|thumb_status| { + if let Some(status) = thumb_status.take() { + if status == ThumbStatus::DelayLeave { + is_show_x_thumb.set(false); + is_show_y_thumb.set(false); + } + } + }); + }); + x_trumb_mouseup_handle.set_value(Some(handle)); + memo_x_left.set_value(container_scroll_left.get()); + memo_mouse_x.set_value(e.client_x()); + thumb_status.set_value(Some(ThumbStatus::Enter)); + }; + + let y_trumb_mousemove_handle = StoredValue::new(None::); + let y_trumb_mouseup_handle = StoredValue::new(None::); + let memo_y_top = StoredValue::new(0); + let memo_mouse_y = StoredValue::new(0); + let on_y_thumb_mousedown = move |e: ev::MouseEvent| { + e.prevent_default(); + e.stop_propagation(); + let handle = window_event_listener(ev::mousemove, move |e| { + let container_height = container_height.get(); + let content_height = content_height.get(); + let y_track_height = y_track_height.get(); + let y_thumb_height = y_thumb_height.get() as i32; + + let y_diff = e.client_y() - memo_mouse_y.get_value(); + let to_scroll_top_upper_bound = content_height - container_height; + let scroll_top = + (y_diff * to_scroll_top_upper_bound) / (y_track_height - y_thumb_height); + + let mut to_scroll_top = memo_y_top.get_value() + scroll_top; + to_scroll_top = to_scroll_top.min(to_scroll_top_upper_bound); + to_scroll_top = to_scroll_top.max(0); + + if let Some(el) = container_ref.get_untracked() { + el.set_scroll_top(to_scroll_top); + } + }); + y_trumb_mousemove_handle.set_value(Some(handle)); + let handle = window_event_listener(ev::mouseup, move |_| { + y_trumb_mousemove_handle.update_value(|handle| { + if let Some(handle) = handle.take() { + handle.remove(); + } + }); + y_trumb_mouseup_handle.update_value(|handle| { + if let Some(handle) = handle.take() { + handle.remove(); + } + }); + thumb_status.update_value(|thumb_status| { + if let Some(status) = thumb_status.take() { + if status == ThumbStatus::DelayLeave { + is_show_x_thumb.set(false); + is_show_y_thumb.set(false); + } + } + }); + }); + y_trumb_mouseup_handle.set_value(Some(handle)); + memo_y_top.set_value(container_scroll_top.get()); + memo_mouse_y.set_value(e.client_y()); + thumb_status.set_value(Some(ThumbStatus::Enter)); + }; + + view! { +
+ +
+
{children()}
+
+
+
+
+
+
+
+
+ } +} + +#[derive(Clone, PartialEq)] +enum ThumbStatus { + Enter, + DelayLeave, +} diff --git a/thaw/src/scrollbar/scrollbar.css b/thaw/src/scrollbar/scrollbar.css new file mode 100644 index 0000000..760d5af --- /dev/null +++ b/thaw/src/scrollbar/scrollbar.css @@ -0,0 +1,67 @@ +.thaw-scrollbar { + overflow: hidden; + position: relative; + z-index: auto; + height: 100%; + width: 100%; +} + +.thaw-scrollbar__container { + width: 100%; + overflow: scroll; + height: 100%; + min-height: inherit; + max-height: inherit; + scrollbar-width: none; +} + +.thaw-scrollbar__content { + display: flow-root; + box-sizing: border-box; + min-width: 100%; +} + +.thaw-scrollbar__container::-webkit-scrollbar, +.thaw-scrollbar__container::-webkit-scrollbar-track-piece, +.thaw-scrollbar__container::-webkit-scrollbar-thumb { + width: 0; + height: 0; + display: none; +} + +.thaw-scrollbar__track--vertical { + position: absolute; + pointer-events: none; + user-select: none; + + right: 2px; + top: 2px; + bottom: 2px; + width: var(--thaw-scrollbar-size); +} + +.thaw-scrollbar__track--horizontal { + position: absolute; + pointer-events: none; + user-select: none; + + bottom: 2px; + left: 2px; + right: 2px; + height: var(--thaw-scrollbar-size); +} + +.thaw-scrollabr__thumb { + position: absolute; + pointer-events: all; + + width: 100%; + height: 100%; + border-radius: var(--thaw-scrollbar-size); + background-color: var(--thaw-scrollbar-background-color); + transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +.thaw-scrollabr__thumb:hover { + background-color: var(--thaw-scrollbar-background-color-hover); +} \ No newline at end of file diff --git a/thaw/src/scrollbar/theme.rs b/thaw/src/scrollbar/theme.rs new file mode 100644 index 0000000..f95ca1e --- /dev/null +++ b/thaw/src/scrollbar/theme.rs @@ -0,0 +1,23 @@ +use crate::theme::ThemeMethod; + +#[derive(Clone)] +pub struct ScrollbarTheme { + pub background_color: String, + pub background_color_hover: String, +} + +impl ThemeMethod for ScrollbarTheme { + fn light() -> Self { + Self { + background_color: "#00000040".into(), + background_color_hover: "#00000066".into(), + } + } + + fn dark() -> Self { + Self { + background_color: "#ffffff33".into(), + background_color_hover: "#ffffff4d".into(), + } + } +} diff --git a/thaw/src/theme/mod.rs b/thaw/src/theme/mod.rs index 9b07ff4..9e42b82 100644 --- a/thaw/src/theme/mod.rs +++ b/thaw/src/theme/mod.rs @@ -5,8 +5,8 @@ use crate::{ mobile::{NavBarTheme, TabbarTheme}, AlertTheme, AutoCompleteTheme, AvatarTheme, BreadcrumbTheme, ButtonTheme, CalendarTheme, CollapseTheme, ColorPickerTheme, DatePickerTheme, InputTheme, MenuTheme, MessageTheme, - PopoverTheme, ProgressTheme, SelectTheme, SkeletionTheme, SliderTheme, SpinnerTheme, - SwitchTheme, TableTheme, TagTheme, TimePickerTheme, TypographyTheme, UploadTheme, + PopoverTheme, ProgressTheme, ScrollbarTheme, SelectTheme, SkeletionTheme, SliderTheme, + SpinnerTheme, SwitchTheme, TableTheme, TagTheme, TimePickerTheme, TypographyTheme, UploadTheme, }; use leptos::*; @@ -45,6 +45,7 @@ pub struct Theme { pub date_picker: DatePickerTheme, pub popover: PopoverTheme, pub collapse: CollapseTheme, + pub scrollbar: ScrollbarTheme, } impl Theme { @@ -78,6 +79,7 @@ impl Theme { date_picker: DatePickerTheme::light(), popover: PopoverTheme::light(), collapse: CollapseTheme::light(), + scrollbar: ScrollbarTheme::light(), } } pub fn dark() -> Self { @@ -110,6 +112,7 @@ impl Theme { date_picker: DatePickerTheme::dark(), popover: PopoverTheme::dark(), collapse: CollapseTheme::dark(), + scrollbar: ScrollbarTheme::dark(), } } }