dioxus_floating/scrollable_view.rs
1use std::rc::Rc;
2
3use dioxus::{html::geometry::PixelsVector2D, prelude::*};
4
5use crate::ScrollState;
6
7/// A scrollable container that provides context for floating elements.
8///
9/// `ScrollableView` is the core component of the library. It tracks its own
10/// position, dimensions, and scroll state, providing this data via context
11/// to child hooks like `use_placement`.
12///
13/// # Note on Styles:
14/// Ensure you provide height and overflow styles (e.g., `h-full overflow-auto`)
15/// via the `class` or `style` props, as the component does not apply them by default.
16///
17/// # Example
18///
19/// ```rust,norun
20/// use dioxus::prelude::*;
21/// use dioxus_floating::{use_scroll_context, ScrollableView, ScrollState};
22///
23/// #[component]
24/// fn MyComponent() -> Element {
25/// rsx! {
26/// ScrollableView {
27/// class: "h-screen w-full overflow-y-auto",
28/// on_scroll: move |state: ScrollState| println!("Scrolled to: {}", state.state.y),
29///
30/// div { class: "h-[2000px]", "Very long content..." }
31///
32/// // Any floating elements inside will be positioned correctly
33/// MyDropdown {}
34/// }
35/// }
36/// }
37///
38/// #[component]
39/// fn MyDropdown() -> Element { let ctx = use_scroll_context(); rsx! {} }
40/// ```
41#[component]
42pub fn ScrollableView(
43 #[props(default)] id: Option<String>,
44 #[props(default)] class: String,
45 #[props(default)] style: String,
46 #[props(into)] on_mouse_move: Option<EventHandler<MouseEvent>>,
47 #[props(into)] on_mouse_up: Option<EventHandler<MouseEvent>>,
48 #[props(into)] on_mouse_down: Option<EventHandler<MouseEvent>>,
49 #[props(into)] on_scroll: Option<EventHandler<ScrollState>>,
50 children: Element,
51 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
52) -> Element {
53 let floating = crate::use_floating();
54
55 let mut scrollable_ref = use_signal(|| Option::<Rc<MountedData>>::None);
56 let mut scroll_state = use_signal(|| Option::<ScrollState>::None);
57
58 use_context_provider(move || ScrollableContext {
59 scrollable_ref,
60 scroll_state,
61 });
62
63 rsx! {
64 div { id: id, class: class, style: style,
65 onmounted: move |evt: MountedEvent| {
66 scrollable_ref.set(Some(evt.data.clone()));
67 let mounted_data = evt.data.clone();
68 spawn(async move {
69 let state = floating.generate_scroll_state_from_mounted(mounted_data).await;
70 scroll_state.set(Some(state));
71 });
72 },
73 onresize: move |evt: ResizeEvent| {
74 scroll_state.with_mut(move |sstate| {
75 if let Some(state) = sstate {
76 if let Ok(size) = evt.get_border_box_size() {
77 state.bounds = size;
78 }
79
80 *sstate = Some(state.to_owned());
81 }
82 });
83 if let Some(scrollable) = scrollable_ref() {
84 spawn(async move {
85 if let Ok(size) = scrollable.get_scroll_size().await {
86 scroll_state.with_mut(move |sstate| {
87 if let Some(state) = sstate {
88 state.size = size;
89 *sstate = Some(state.to_owned());
90 }
91 });
92 }
93 });
94 }
95 },
96 onscroll: move |evt: ScrollEvent| {
97 let new_state = floating.generate_scroll_state(evt);
98 scroll_state.set(Some(new_state));
99 if let Some(cb) = on_scroll { cb.call(new_state); }
100 },
101 onmousemove: move |evt: MouseEvent| {
102 if let Some(cb) = on_mouse_move { cb.call(evt); }
103 },
104 onmouseup: move |evt: MouseEvent| {
105 if let Some(cb) = on_mouse_up { cb.call(evt); }
106 },
107 onmousedown: move |evt: MouseEvent| {
108 if let Some(cb) = on_mouse_down { cb.call(evt); }
109 },
110 ..attributes,
111
112 {children}
113 }
114 }
115}
116
117/// Context provided by the [ScrollableView] component.
118///
119/// It contains reactive signals for the scroll state and a reference to the
120/// underlying DOM element, along with methods to programmatically control scrolling.
121#[derive(Debug, Clone, Copy)]
122pub struct ScrollableContext {
123 /// A reactive signal containing the [MountedData] of the scrollable container.
124 pub scrollable_ref: Signal<Option<Rc<MountedData>>>,
125
126 /// A reactive signal containing the current [ScrollState] (dimensions, offset, etc.).
127 pub scroll_state: Signal<Option<ScrollState>>,
128}
129
130impl ScrollableContext {
131 /// Forces a re-calculation of the scroll content size and current offset.
132 /// Useful when the content inside changes but the container's outer bounds remain the same.
133 pub async fn reload(&mut self) {
134 if let Some(data) = self.scrollable_ref.peek().as_ref() {
135 // Мы используем логику из Floating, которую ты уже написал
136 let floating = crate::Floating::default();
137 let new_state = floating
138 .generate_scroll_state_from_mounted(data.clone())
139 .await;
140
141 // Обновляем сигнал
142 self.scroll_state.set(Some(new_state));
143 }
144 }
145
146 /// Programmatically scrolls the container by a given offset.
147 ///
148 /// # Example
149 /// ```rust
150 /// use dioxus::prelude::*;
151 /// use dioxus::html::geometry::PixelsVector2D;
152 /// use dioxus_floating::use_scroll_context;
153 ///
154 /// #[component]
155 /// fn MyComponent() -> Element {
156 /// let ctx = use_scroll_context();
157 /// use_effect(move || {
158 /// spawn(async move {
159 /// ctx.scroll(PixelsVector2D::new(0.0, 100.0), ScrollBehavior::Smooth).await;
160 /// });
161 /// });
162 /// rsx! {}
163 /// }
164 /// ```
165 pub async fn scroll(&self, coordinates: PixelsVector2D, behavior: ScrollBehavior) {
166 if let Some(data) = self.scrollable_ref.peek().as_ref() {
167 let _ = data.scroll(coordinates, behavior).await;
168 }
169 }
170
171 /// Scrolls to a specific position (e.g., top or bottom) based on the behavior.
172 pub async fn scroll_to(&self, behavior: ScrollBehavior) {
173 if let Some(data) = self.scrollable_ref.peek().as_ref() {
174 let _ = data.scroll_to(behavior).await;
175 }
176 }
177
178 /// Scrolls the container using advanced options (like specific element alignment).
179 pub async fn scroll_to_with_options(&self, options: ScrollToOptions) {
180 if let Some(data) = self.scrollable_ref.peek().as_ref() {
181 let _ = data.scroll_to_with_options(options).await;
182 }
183 }
184}