dioxus_floating/
lib.rs

1use std::rc::Rc;
2
3use dioxus::prelude::*;
4use dioxus::{html::geometry::ClientPoint, logger::tracing};
5
6mod floating;
7mod scrollable_view;
8
9pub use floating::{Floating, FloatingOptions, Middleware, Placement, ScrollState};
10pub use scrollable_view::{ScrollableContext, ScrollableView};
11
12/// Returns the global [Floating] engine instance.
13///
14/// This hook initializes the positioning engine (with default settings)
15/// and ensures it persists across component re-renders.
16pub fn use_floating() -> Floating {
17    use_hook(Floating::default)
18}
19
20/// Accesses the nearest [ScrollableContext] provided by a [ScrollableView].
21///
22/// # Panics
23/// This hook will panic if used outside of a [ScrollableView] component.
24/// Use `try_use_context::<ScrollableContext>()` if you need a non-panicking version.
25pub fn use_scroll_context() -> ScrollableContext {
26    use_context::<ScrollableContext>()
27}
28
29/// A shorthand hook to access the current [ScrollState] from the context.
30///
31/// Returns a [Signal] containing the dimensions and scroll offsets of
32/// the nearest [ScrollableView].
33pub fn use_scroll_state() -> Signal<Option<ScrollState>> {
34    let ctx = use_scroll_context();
35
36    ctx.scroll_state
37}
38
39/// A shorthand hook to access the [MountedData] of the parent [ScrollableView].
40///
41/// Useful when you need to programmatically control the scroll container
42/// (e.g., calling `scroll_to`) from a child component.
43pub fn use_scrollable_ref() -> Signal<Option<Rc<MountedData>>> {
44    let ctx = use_scroll_context();
45
46    ctx.scrollable_ref
47}
48
49/// The result of a floating position calculation.
50///
51/// This structure is returned by positioning hooks and contains raw coordinates
52/// and a readiness flag. It is designed to be used with `use_memo` to generate
53/// custom CSS styles.
54#[derive(Debug, Clone, Copy, Default)]
55pub struct FloatingResult {
56    // Calculated X coordinate (viewport-relative pixels).
57    pub x: f64,
58    // Calculated Y coordinate (viewport-relative pixels).
59    pub y: f64,
60    // Use this to toggle visibility (e.g., opacity) to prevent flickering.
61    pub is_ready: bool,
62}
63
64/// Reactive hook for positioning a floating element relative to a trigger element (anchor).
65///
66/// This hook automatically finds the nearest [ScrollableView] context to handle
67/// scrolling and overflow boundary detection.
68///
69/// # Behavior
70/// - It recalculates the position whenever the trigger, the element itself,
71///   or the parent's scroll state changes.
72/// - It uses a 1ms delay to ensure the browser has performed a Layout pass
73///   before measuring dimensions.
74///
75/// # Warning
76/// This hook must be used within a [ScrollableView] component. If no context
77/// is found, it will log a warning and return default (zero) coordinates.
78///
79/// # Example
80///
81/// ```rust
82/// use dioxus::prelude::*;
83/// use dioxus::html::geometry::PixelsVector2D;
84/// use dioxus_floating::{use_placement, FloatingOptions};
85///
86/// fn MyElement() -> Element {
87///     let mut element_ref = use_signal(|| None);
88///     let mut trigger_ref = use_signal(|| None);
89///     let mut is_opened = use_signal(|| false);
90///
91///     let placement = use_placement(element_ref, trigger_ref, FloatingOptions::default());
92///
93///     rsx! {
94///         // The trigger element
95///         button {
96///             onmounted: move |e| trigger_ref.set(Some(e.data.clone())),
97///             onclick: move |_| is_opened.toggle(),
98///             "Toggle Dropdown"
99///         }
100///     
101///         // The floating element
102///         if is_opened() {
103///             div {
104///                 onmounted: move |e| element_ref.set(Some(e.data.clone())),
105///                 // Use is_ready to prevent the element from "jumping" into position
106///                 class: if placement().is_ready { "opacity-100" } else { "opacity-0" },
107///                 style: "position: fixed; transform: translate3d({placement().x}px, {placement().y}px, 0);",
108///                 "I am a dropdown content"
109///             }
110///         }
111///     }
112/// }
113/// ```
114///
115/// # Example: Custom Style Generation
116///
117/// ```rust
118/// use dioxus::prelude::*;
119/// use dioxus_floating::{use_placement, FloatingOptions};
120///
121/// #[component]
122/// fn MyComponent() -> Element {
123///     let el = use_signal(|| None);
124///     let tr = use_signal(|| None);
125///     let pos = use_placement(el, tr, FloatingOptions::default());
126///
127///     let style = use_memo(move || {
128///         pos.with(|p| format!(
129///             "position: fixed; transform: translate3d({}px, {}px, 0); opacity: {};",
130///             p.x, p.y, if p.is_ready { 1 } else { 0 }
131///         ))
132///     });
133///     rsx!{}
134/// }
135/// ```
136pub fn use_placement(
137    // Signal containing the reference to the floating element.
138    element_ref: Signal<Option<Rc<MountedData>>>,
139    // Signal containing the reference to the trigger (anchor) element.
140    trigger_ref: Signal<Option<Rc<MountedData>>>,
141    // Positioning options including [Placement], [Middleware], and offsets.
142    options: FloatingOptions,
143) -> Signal<FloatingResult> {
144    let floating = use_floating();
145    let mut result = use_signal(FloatingResult::default);
146
147    // context without panic
148    let context = match try_use_context::<ScrollableContext>() {
149        Some(ctx) => ctx,
150        None => {
151            tracing::warn!(
152                "use_placement hook used outside of ScrollableView. \
153                Ensure your component is wrapped in a ScrollableView or provide a ScrollableContext."
154            );
155            return result;
156        }
157    };
158
159    use_effect(move || {
160        let zip = (context.scroll_state)()
161            .zip((context.scrollable_ref)())
162            .zip(element_ref())
163            .zip(trigger_ref());
164
165        if let Some((((scroll_state, scrollable), element), trigger)) = zip {
166            let options = options.clone();
167            spawn(async move {
168                // wait render virtual dom elements
169                gloo_timers::future::TimeoutFuture::new(1).await;
170
171                let pos = floating
172                    .placement_on_trigger(scroll_state, scrollable, element, trigger, options)
173                    .await;
174
175                result.set(FloatingResult {
176                    x: pos.0,
177                    y: pos.1,
178                    is_ready: true,
179                });
180
181                tracing::debug!(
182                    "Floating placement updated: x={}, y={}, ready=true",
183                    pos.0,
184                    pos.1
185                );
186            });
187        } else {
188            // drop ready flag
189            if result.peek().is_ready {
190                result.set(FloatingResult::default());
191                tracing::debug!("Floating placement reset: ready=false");
192            }
193        }
194    });
195
196    result
197}
198
199/// Reactive hook for positioning a floating element relative to a specific point (e.g., mouse click).
200///
201/// This is specifically designed for context menus or custom popups that appear at
202/// a given [ClientPoint]. It automatically subscribes to the nearest [ScrollableView]
203/// to handle positioning within a scrollable area.
204///
205/// # Note on Usage:
206/// Unlike `use_placement`, this hook expects a point in viewport coordinates.
207/// If you are using this for a context menu, ensure you capture the coordinates
208/// from the `MouseEvent`.
209///
210/// # Example
211///
212/// ```rust
213/// use dioxus::prelude::*;
214/// use dioxus_floating::{use_placement_on_point, FloatingOptions};
215///
216/// #[component]
217/// fn MyComponent() -> Element {
218///     let mut click_point = use_signal(|| None);
219///     let mut element_ref = use_signal(|| None);
220///
221///     let placement = use_placement_on_point(element_ref, click_point, FloatingOptions::default());
222///
223///     rsx! {
224///         div {
225///             oncontextmenu: move |e| {
226///                 e.prevent_default();
227///                 click_point.set(Some(e.client_coordinates()));
228///             },
229///             "Right click here to open menu"
230///         }
231///     
232///         // Render the element as soon as we have a target point
233///         if click_point().is_some() {
234///             div {
235///                 onmounted: move |e| element_ref.set(Some(e.data.clone())),
236///                 // Keep it invisible until positioning is calculated
237///                 class: if placement().is_ready { "opacity-100" } else { "opacity-0" },
238///                 style: "position: fixed; transform: translate3d({placement().x}px, {placement().y}px, 0);",
239///                 "Context Menu Content"
240///             }
241///         }
242///     }
243/// }
244/// ```
245///
246/// # Example: Custom Style Generation
247///
248/// ```rust
249/// use dioxus::prelude::*;
250/// use dioxus_floating::{use_placement_on_point, FloatingOptions};
251///
252/// #[component]
253/// fn MyComponent() -> Element {
254///     let el = use_signal(|| None);
255///     let mut click = use_signal(|| None);
256///     let pos = use_placement_on_point(el, click, FloatingOptions::default());
257///     let style = use_memo(move || {
258///         pos.with(|p| format!(
259///             "position: fixed; transform: translate3d({}px, {}px, 0); opacity: {};",
260///             p.x, p.y, if p.is_ready { 1 } else { 0 }
261///         ))
262///     });
263///     rsx! {
264///         button {
265///             onclick: move |evt: MouseEvent| { click.set(Some(evt.client_coordinates())) }
266///         }
267///     }
268/// }
269/// ```
270pub fn use_placement_on_point(
271    element_ref: Signal<Option<Rc<MountedData>>>,
272    trigger_point: Signal<Option<ClientPoint>>,
273    options: FloatingOptions,
274) -> Signal<FloatingResult> {
275    let floating = use_floating();
276    let mut result = use_signal(FloatingResult::default);
277    // context without panic
278    let context = match try_use_context::<ScrollableContext>() {
279        Some(ctx) => ctx,
280        None => {
281            tracing::warn!(
282                "use_placement hook used outside of ScrollableView. \
283                Ensure your component is wrapped in a ScrollableView or provide a ScrollableContext."
284            );
285            return result;
286        }
287    };
288
289    use_effect(move || {
290        let zip = (context.scroll_state)()
291            .zip((context.scrollable_ref)())
292            .zip(element_ref())
293            .zip(trigger_point());
294
295        if let Some((((scroll_state, scrollable), element), trigger)) = zip {
296            let options = options.clone();
297            spawn(async move {
298                // wait render virtual dom elements
299                gloo_timers::future::TimeoutFuture::new(1).await;
300
301                let pos = floating
302                    .placement_on_point(scroll_state, scrollable, element, trigger, options)
303                    .await;
304
305                result.set(FloatingResult {
306                    x: pos.0,
307                    y: pos.1,
308                    is_ready: true,
309                });
310
311                tracing::debug!(
312                    "Floating placement updated: x={}, y={}, ready=true",
313                    pos.0,
314                    pos.1
315                );
316            });
317        } else {
318            // drop ready flag
319            if result.peek().is_ready {
320                result.set(FloatingResult::default());
321                tracing::debug!("Floating placement reset: ready=false");
322            }
323        }
324    });
325
326    result
327}