Skip to main content

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, OffsetOptions, 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<E, T>(
137    // Signal containing the reference to the floating element.
138    element_ref: E,
139    // Signal containing the reference to the trigger (anchor) element.
140    trigger_ref: T,
141    // Positioning options including [Placement], [Middleware], and offsets.
142    options: FloatingOptions,
143) -> ReadSignal<FloatingResult>
144where
145    E: Into<ReadSignal<Option<Rc<MountedData>>>>,
146    T: Into<ReadSignal<Option<Rc<MountedData>>>>,
147{
148    let element_ref = element_ref.into();
149    let trigger_ref = trigger_ref.into();
150
151    let floating = use_floating();
152    let mut result = use_signal(FloatingResult::default);
153
154    // context without panic
155    let context = match try_use_context::<ScrollableContext>() {
156        Some(ctx) => ctx,
157        None => {
158            tracing::warn!(
159                "use_placement hook used outside of ScrollableView. \
160                Ensure your component is wrapped in a ScrollableView or provide a ScrollableContext."
161            );
162            return result.into();
163        }
164    };
165
166    use_effect(move || {
167        let zip = (context.scroll_state)()
168            .zip((context.scrollable_ref)())
169            .zip(element_ref())
170            .zip(trigger_ref());
171
172        if let Some((((scroll_state, scrollable), element), trigger)) = zip {
173            let options = options.clone();
174            spawn(async move {
175                // wait render virtual dom elements
176                gloo_timers::future::TimeoutFuture::new(1).await;
177
178                let pos = floating
179                    .placement_on_trigger(scroll_state, scrollable, element, trigger, options)
180                    .await;
181
182                result.set(FloatingResult {
183                    x: pos.0,
184                    y: pos.1,
185                    is_ready: true,
186                });
187
188                tracing::debug!(
189                    "Floating placement updated: x={}, y={}, ready=true",
190                    pos.0,
191                    pos.1
192                );
193            });
194        } else {
195            // drop ready flag
196            if result.peek().is_ready {
197                result.set(FloatingResult::default());
198                tracing::debug!("Floating placement reset: ready=false");
199            }
200        }
201    });
202
203    result.into()
204}
205
206/// Reactive hook for positioning a floating element relative to a specific point (e.g., mouse click).
207///
208/// This is specifically designed for context menus or custom popups that appear at
209/// a given [ClientPoint]. It automatically subscribes to the nearest [ScrollableView]
210/// to handle positioning within a scrollable area.
211///
212/// # Note on Usage:
213/// Unlike `use_placement`, this hook expects a point in viewport coordinates.
214/// If you are using this for a context menu, ensure you capture the coordinates
215/// from the `MouseEvent`.
216///
217/// # Example
218///
219/// ```rust
220/// use dioxus::prelude::*;
221/// use dioxus_floating::{use_placement_on_point, FloatingOptions};
222///
223/// #[component]
224/// fn MyComponent() -> Element {
225///     let mut click_point = use_signal(|| None);
226///     let mut element_ref = use_signal(|| None);
227///
228///     let placement = use_placement_on_point(
229///         element_ref,
230///         click_point,
231///         FloatingOptions::default(),
232///     );
233///
234///     rsx! {
235///         div {
236///             oncontextmenu: move |e| {
237///                 e.prevent_default();
238///                 click_point.set(Some(e.client_coordinates()));
239///             },
240///             "Right click here to open menu"
241///         }
242///     
243///         // Render the element as soon as we have a target point
244///         if click_point().is_some() {
245///             div {
246///                 onmounted: move |e| element_ref.set(Some(e.data.clone())),
247///                 // Keep it invisible until positioning is calculated
248///                 class: if placement().is_ready { "opacity-100" } else { "opacity-0" },
249///                 style: "position: fixed; transform: translate3d({placement().x}px, {placement().y}px, 0);",
250///                 "Context Menu Content"
251///             }
252///         }
253///     }
254/// }
255/// ```
256///
257/// # Example: Custom Style Generation
258///
259/// ```rust
260/// use dioxus::prelude::*;
261/// use dioxus_floating::{use_placement_on_point, FloatingOptions};
262///
263/// #[component]
264/// fn MyComponent() -> Element {
265///     let el = use_signal(|| None);
266///     let mut click = use_signal(|| None);
267///     let pos = use_placement_on_point(el, click, FloatingOptions::default());
268///     let style = use_memo(move || {
269///         pos.with(|p| format!(
270///             "position: fixed; transform: translate3d({}px, {}px, 0); opacity: {};",
271///             p.x, p.y, if p.is_ready { 1 } else { 0 }
272///         ))
273///     });
274///     rsx! {
275///         button {
276///             onclick: move |evt: MouseEvent| { click.set(Some(evt.client_coordinates())) }
277///         }
278///     }
279/// }
280/// ```
281pub fn use_placement_on_point<E, T>(
282    element_ref: E,
283    trigger_point: T,
284    options: FloatingOptions,
285) -> ReadSignal<FloatingResult>
286where
287    E: Into<ReadSignal<Option<Rc<MountedData>>>>,
288    T: Into<ReadSignal<Option<ClientPoint>>>,
289{
290    let element_ref = element_ref.into();
291    let trigger_point = trigger_point.into();
292    let floating = use_floating();
293    let mut result = use_signal(FloatingResult::default);
294    // context without panic
295    let context = match try_use_context::<ScrollableContext>() {
296        Some(ctx) => ctx,
297        None => {
298            tracing::warn!(
299                "use_placement hook used outside of ScrollableView. \
300                Ensure your component is wrapped in a ScrollableView or provide a ScrollableContext."
301            );
302            return result.into();
303        }
304    };
305
306    use_effect(move || {
307        let zip = (context.scroll_state)()
308            .zip((context.scrollable_ref)())
309            .zip(element_ref())
310            .zip(trigger_point());
311
312        if let Some((((scroll_state, scrollable), element), trigger)) = zip {
313            let options = options.clone();
314            spawn(async move {
315                // wait render virtual dom elements
316                gloo_timers::future::TimeoutFuture::new(1).await;
317
318                let pos = floating
319                    .placement_on_point(scroll_state, scrollable, element, trigger, options)
320                    .await;
321
322                result.set(FloatingResult {
323                    x: pos.0,
324                    y: pos.1,
325                    is_ready: true,
326                });
327
328                tracing::debug!(
329                    "Floating placement updated: x={}, y={}, ready=true",
330                    pos.0,
331                    pos.1
332                );
333            });
334        } else {
335            // drop ready flag
336            if result.peek().is_ready {
337                result.set(FloatingResult::default());
338                tracing::debug!("Floating placement reset: ready=false");
339            }
340        }
341    });
342
343    result.into()
344}