1use std::rc::Rc;
2
3use dioxus::html::geometry::{ClientPoint, PixelsRect, PixelsSize, PixelsVector2D};
4use dioxus::logger::tracing;
5use dioxus::prelude::*;
6
7#[derive(Debug, Clone, Copy, Default)]
13pub struct Floating;
14
15#[derive(Debug, Clone, Copy)]
17pub struct ScrollState {
18 pub size: PixelsSize,
20 pub bounds: PixelsSize,
22 pub state: PixelsVector2D,
24}
25
26#[derive(Debug, Clone, Copy)]
28pub enum Placement {
29 TopStart,
30 TopCenter,
31 TopEnd,
32 BottomStart,
33 BottomCenter,
34 BottomEnd,
35 LeftStart,
36 LeftCenter,
37 LeftEnd,
38 RightStart,
39 RightCenter,
40 RightEnd,
41}
42
43impl Placement {
44 pub fn is_vertical(&self) -> bool {
46 matches!(
47 self,
48 Placement::TopStart
49 | Placement::TopCenter
50 | Placement::TopEnd
51 | Placement::BottomStart
52 | Placement::BottomCenter
53 | Placement::BottomEnd
54 )
55 }
56
57 pub fn is_top(&self) -> bool {
59 matches!(
60 self,
61 Placement::TopEnd | Placement::TopCenter | Placement::TopStart
62 )
63 }
64
65 pub fn is_left(&self) -> bool {
67 matches!(
68 self,
69 Placement::LeftCenter | Placement::LeftEnd | Placement::LeftStart
70 )
71 }
72
73 pub fn get_modifier(&self) -> PlacementModifier {
75 match self {
76 &Placement::BottomCenter => PlacementModifier::Center,
77 &Placement::LeftCenter => PlacementModifier::Center,
78 &Placement::RightCenter => PlacementModifier::Center,
79 &Placement::TopCenter => PlacementModifier::Center,
80 &Placement::BottomEnd => PlacementModifier::End,
81 &Placement::LeftEnd => PlacementModifier::End,
82 &Placement::RightEnd => PlacementModifier::End,
83 &Placement::TopEnd => PlacementModifier::End,
84 &Placement::BottomStart => PlacementModifier::Start,
85 &Placement::LeftStart => PlacementModifier::Start,
86 &Placement::RightStart => PlacementModifier::Start,
87 &Placement::TopStart => PlacementModifier::Start,
88 }
89 }
90}
91
92pub enum PlacementModifier {
94 Center,
95 Start,
96 End,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq)]
101pub enum Middleware {
102 Flip,
104 Shift,
106}
107
108#[derive(Debug, Clone)]
110pub struct FloatingOptions {
111 pub middleware: Vec<Middleware>,
113 pub offset: f64,
115 pub padding: f64,
117 pub placement: Placement,
119}
120
121impl FloatingOptions {
122 pub fn can_flip(&self) -> bool {
124 self.middleware.contains(&Middleware::Flip)
125 }
126
127 pub fn can_shift(&self) -> bool {
129 self.middleware.contains(&Middleware::Shift)
130 }
131}
132
133impl Default for FloatingOptions {
134 fn default() -> Self {
137 FloatingOptions {
138 middleware: vec![Middleware::Flip, Middleware::Shift],
139 offset: 1_f64,
140 padding: 0_f64,
141 placement: Placement::BottomStart,
142 }
143 }
144}
145
146impl Floating {
147 pub async fn generate_scroll_state_from_mounted(&self, data: Rc<MountedData>) -> ScrollState {
155 let rect = data.get_client_rect().await;
156 let scroll = data.get_scroll_size().await;
157 let offset = data.get_scroll_offset().await;
158
159 let size = scroll
160 .map(|s| PixelsSize::new(s.width, s.height))
161 .unwrap_or(PixelsSize::new(0_f64, 0_f64));
162 let bounds = rect
163 .map(|r| PixelsSize::new(r.width(), r.height()))
164 .unwrap_or(PixelsSize::new(0_f64, 0_f64));
165 let state = offset
166 .map(|o| PixelsVector2D::new(o.x, o.y))
167 .unwrap_or(PixelsVector2D::new(0_f64, 0_f64));
168
169 ScrollState {
170 size,
171 bounds,
172 state,
173 }
174 }
175
176 pub fn generate_scroll_state(&self, evt: ScrollEvent) -> ScrollState {
202 ScrollState {
203 size: PixelsSize::new(evt.scroll_width() as f64, evt.scroll_height() as f64),
204 bounds: PixelsSize::new(evt.client_width() as f64, evt.client_height() as f64),
205 state: PixelsVector2D::new(evt.scroll_left(), evt.scroll_top()),
206 }
207 }
208
209 pub async fn placement_on_point(
217 &self,
218 scroll_state: ScrollState,
219 scrollable_ref: Rc<MountedData>,
220 element_ref: Rc<MountedData>,
221 trigger: ClientPoint,
222 options: FloatingOptions,
223 ) -> (f64, f64) {
224 let scrollable_rect = scrollable_ref
225 .get_client_rect()
226 .await
227 .unwrap_or(PixelsRect::new(
228 PixelsVector2D::new(0_f64, 0_f64).to_point(),
229 scroll_state.bounds,
230 ));
231 let trigger_rect = PixelsRect::new(
232 PixelsVector2D::new(trigger.x, trigger.y).to_point(),
233 PixelsSize::new(1_f64, 1_f64),
234 );
235
236 match element_ref.get_client_rect().await {
237 Ok(element_rect) => {
238 self.calculate_placement(scrollable_rect, element_rect, trigger_rect, options)
239 }
240 Err(_) => (trigger_rect.min_x(), trigger_rect.min_y()),
241 }
242 }
243
244 pub async fn placement_on_trigger(
252 &self,
253 scroll_state: ScrollState,
254 scrollable_ref: Rc<MountedData>,
255 element_ref: Rc<MountedData>,
256 trigger_ref: Rc<MountedData>,
257 options: FloatingOptions,
258 ) -> (f64, f64) {
259 let scrollable_rect = scrollable_ref
260 .get_client_rect()
261 .await
262 .unwrap_or(PixelsRect::new(
263 PixelsVector2D::new(0_f64, 0_f64).to_point(),
264 scroll_state.bounds,
265 ));
266 let trigger_rect = trigger_ref
267 .get_client_rect()
268 .await
269 .unwrap_or(PixelsRect::new(
270 PixelsVector2D::new(0_f64, 0_f64).to_point(),
271 PixelsSize::new(1_f64, 1_f64),
272 ));
273
274 match element_ref.get_client_rect().await {
275 Ok(element_rect) => {
276 self.calculate_placement(scrollable_rect, element_rect, trigger_rect, options)
277 }
278 Err(_) => (trigger_rect.min_x(), trigger_rect.min_y()),
279 }
280 }
281
282 fn compute_base_coords(
285 &self,
286 element: PixelsRect,
287 trigger: PixelsRect,
288 options: FloatingOptions,
289 ) -> (f64, f64) {
290 let x: f64;
291 let y: f64;
292
293 (x, y) = if options.placement.is_vertical() {
295 let x = match options.placement.get_modifier() {
296 PlacementModifier::Center => {
297 trigger.min_x() + (trigger.width() / 2_f64) - (element.width() / 2_f64)
298 }
299 PlacementModifier::Start => trigger.min_x(),
300 PlacementModifier::End => trigger.max_x() - element.width(),
301 };
302 let y = if options.placement.is_top() {
303 trigger.min_y() - element.height() - options.offset
304 } else {
305 trigger.max_y() + options.offset
306 };
307 (x, y)
308 } else {
309 let x = if options.placement.is_left() {
310 trigger.min_x() - element.width() - options.offset
311 } else {
312 trigger.max_x() + options.offset
313 };
314 let y = match options.placement.get_modifier() {
315 PlacementModifier::Center => {
316 trigger.min_y() + (trigger.height() / 2_f64) - (element.height() / 2_f64)
317 }
318 PlacementModifier::Start => trigger.min_y(),
319 PlacementModifier::End => trigger.max_y() - element.height(),
320 };
321 (x, y)
322 };
323
324 (x, y)
325 }
326
327 fn apply_middleware(
330 &self,
331 initial_pos: (f64, f64),
332 scrollable: PixelsRect,
333 element: PixelsRect,
334 trigger: PixelsRect,
335 options: FloatingOptions,
336 ) -> (f64, f64) {
337 let (mut x, mut y) = initial_pos;
338
339 if options.can_flip() {
341 if options.placement.is_vertical() {
342 if options.placement.is_top() && y < scrollable.min_y() {
343 y = trigger.max_y() + options.offset;
344 } else if !options.placement.is_top() && y + element.height() > scrollable.max_y() {
345 y = trigger.min_y() - element.height() - options.offset;
346 }
347 } else {
348 if options.placement.is_left() && x < scrollable.min_x() {
349 x = trigger.max_x() + options.offset;
350 } else if !options.placement.is_left() && x + element.width() > scrollable.max_x() {
351 x = trigger.min_x() - element.width() - options.offset;
352 }
353 }
354 }
355 if options.can_shift() {
357 if options.placement.is_vertical() {
358 let min_allowed_x = trigger.min_x() - element.width() + options.padding;
361 let max_allowed_x = trigger.max_x() - options.padding;
362
363 if x < scrollable.min_x() {
365 x = scrollable.min_x();
366 }
367 if x + element.width() > scrollable.max_x() {
368 x = scrollable.max_x() - element.width();
369 }
370
371 x = x.clamp(min_allowed_x, max_allowed_x);
373 } else {
374 let min_allowed_y = trigger.min_y() - element.height() + options.padding;
375 let max_allowed_y = trigger.max_y() - options.padding;
376
377 if y < scrollable.min_y() {
378 y = scrollable.min_y();
379 }
380 if y + element.height() > scrollable.max_y() {
381 y = scrollable.max_y() - element.height();
382 }
383
384 y = y.clamp(min_allowed_y, max_allowed_y);
385 }
386 }
387
388 (x, y)
389 }
390
391 pub fn calculate_placement(
399 &self,
400 scrollable: PixelsRect,
401 element: PixelsRect,
402 trigger: PixelsRect,
403 options: FloatingOptions,
404 ) -> (f64, f64) {
405 let base_pos = self.compute_base_coords(element, trigger, options.clone());
406 let final_pos =
407 self.apply_middleware(base_pos, scrollable, element, trigger, options.clone());
408
409 tracing::debug!(
410 "Calculated for scrollable: {scrollable:?}, element: {element:?}, trigger: {trigger:?}, option: {options:?}"
411 );
412
413 final_pos
414 }
415}