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