Skip to main content

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;
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}