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}