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, PartialEq)]
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, PartialEq)]
110pub struct OffsetOptions {
111 pub main_axis: f64,
113 pub cross_axis: f64,
115}
116
117impl Default for OffsetOptions {
118 fn default() -> Self {
119 Self {
120 main_axis: 1_f64,
121 cross_axis: 1_f64,
122 }
123 }
124}
125
126impl OffsetOptions {
127 pub fn new(main_axis: f64, cross_axis: f64) -> Self {
129 Self {
130 main_axis,
131 cross_axis,
132 }
133 }
134
135 pub fn rect(offset: f64) -> Self {
137 Self {
138 main_axis: offset,
139 cross_axis: offset,
140 }
141 }
142
143 pub fn zero() -> Self {
145 Self::rect(0_f64)
146 }
147}
148
149#[derive(Debug, Clone)]
151pub struct FloatingOptions {
152 pub middleware: Vec<Middleware>,
154 pub offset: OffsetOptions,
156 pub padding: f64,
158 pub placement: Placement,
160}
161
162impl FloatingOptions {
163 pub fn can_flip(&self) -> bool {
165 self.middleware.contains(&Middleware::Flip)
166 }
167
168 pub fn can_shift(&self) -> bool {
170 self.middleware.contains(&Middleware::Shift)
171 }
172}
173
174impl Default for FloatingOptions {
175 fn default() -> Self {
178 FloatingOptions {
179 middleware: vec![Middleware::Flip, Middleware::Shift],
180 offset: OffsetOptions::default(),
181 padding: 0_f64,
182 placement: Placement::BottomStart,
183 }
184 }
185}
186
187impl Floating {
188 pub async fn generate_scroll_state_from_mounted(&self, data: Rc<MountedData>) -> ScrollState {
196 let rect = data.get_client_rect().await;
197 let scroll = data.get_scroll_size().await;
198 let offset = data.get_scroll_offset().await;
199
200 let size = scroll
201 .map(|s| PixelsSize::new(s.width, s.height))
202 .unwrap_or(PixelsSize::new(0_f64, 0_f64));
203 let bounds = rect
204 .map(|r| PixelsSize::new(r.width(), r.height()))
205 .unwrap_or(PixelsSize::new(0_f64, 0_f64));
206 let state = offset
207 .map(|o| PixelsVector2D::new(o.x, o.y))
208 .unwrap_or(PixelsVector2D::new(0_f64, 0_f64));
209
210 ScrollState {
211 size,
212 bounds,
213 state,
214 }
215 }
216
217 pub fn generate_scroll_state(&self, evt: ScrollEvent) -> ScrollState {
243 ScrollState {
244 size: PixelsSize::new(evt.scroll_width() as f64, evt.scroll_height() as f64),
245 bounds: PixelsSize::new(evt.client_width() as f64, evt.client_height() as f64),
246 state: PixelsVector2D::new(evt.scroll_left(), evt.scroll_top()),
247 }
248 }
249
250 pub async fn placement_on_point(
258 &self,
259 scroll_state: ScrollState,
260 scrollable_ref: Rc<MountedData>,
261 element_ref: Rc<MountedData>,
262 trigger: ClientPoint,
263 options: FloatingOptions,
264 ) -> (f64, f64) {
265 let scrollable_rect = scrollable_ref
266 .get_client_rect()
267 .await
268 .unwrap_or(PixelsRect::new(
269 PixelsVector2D::new(0_f64, 0_f64).to_point(),
270 scroll_state.bounds,
271 ));
272 let trigger_rect = PixelsRect::new(
273 PixelsVector2D::new(trigger.x, trigger.y).to_point(),
274 PixelsSize::new(1_f64, 1_f64),
275 );
276
277 match element_ref.get_client_rect().await {
278 Ok(element_rect) => {
279 self.calculate_placement(scrollable_rect, element_rect, trigger_rect, options)
280 }
281 Err(_) => (trigger_rect.min_x(), trigger_rect.min_y()),
282 }
283 }
284
285 pub async fn placement_on_trigger(
293 &self,
294 scroll_state: ScrollState,
295 scrollable_ref: Rc<MountedData>,
296 element_ref: Rc<MountedData>,
297 trigger_ref: Rc<MountedData>,
298 options: FloatingOptions,
299 ) -> (f64, f64) {
300 let scrollable_rect = scrollable_ref
301 .get_client_rect()
302 .await
303 .unwrap_or(PixelsRect::new(
304 PixelsVector2D::new(0_f64, 0_f64).to_point(),
305 scroll_state.bounds,
306 ));
307 let trigger_rect = trigger_ref
308 .get_client_rect()
309 .await
310 .unwrap_or(PixelsRect::new(
311 PixelsVector2D::new(0_f64, 0_f64).to_point(),
312 PixelsSize::new(1_f64, 1_f64),
313 ));
314
315 match element_ref.get_client_rect().await {
316 Ok(element_rect) => {
317 self.calculate_placement(scrollable_rect, element_rect, trigger_rect, options)
318 }
319 Err(_) => (trigger_rect.min_x(), trigger_rect.min_y()),
320 }
321 }
322
323 fn compute_base_coords(
326 &self,
327 element: PixelsRect,
328 trigger: PixelsRect,
329 options: FloatingOptions,
330 ) -> (f64, f64) {
331 let x: f64;
332 let y: f64;
333
334 (x, y) = if options.placement.is_vertical() {
336 let x = match options.placement.get_modifier() {
337 PlacementModifier::Center => {
338 trigger.min_x() + (trigger.width() / 2_f64) - (element.width() / 2_f64)
339 }
340 PlacementModifier::Start => trigger.min_x(),
341 PlacementModifier::End => trigger.max_x() - element.width(),
342 };
343 let y = if options.placement.is_top() {
344 trigger.min_y() - element.height() - options.offset.cross_axis
345 } else {
346 trigger.max_y() + options.offset.cross_axis
347 };
348 (x + options.offset.main_axis, y)
349 } else {
350 let x = if options.placement.is_left() {
351 trigger.min_x() - element.width() - options.offset.main_axis
352 } else {
353 trigger.max_x() + options.offset.main_axis
354 };
355 let y = match options.placement.get_modifier() {
356 PlacementModifier::Center => {
357 trigger.min_y() + (trigger.height() / 2_f64) - (element.height() / 2_f64)
358 }
359 PlacementModifier::Start => trigger.min_y(),
360 PlacementModifier::End => trigger.max_y() - element.height(),
361 };
362 (x, y + options.offset.cross_axis)
363 };
364
365 (x, y)
366 }
367
368 fn apply_middleware(
371 &self,
372 initial_pos: (f64, f64),
373 scrollable: PixelsRect,
374 element: PixelsRect,
375 trigger: PixelsRect,
376 options: FloatingOptions,
377 ) -> (f64, f64) {
378 let (mut x, mut y) = initial_pos;
379
380 if options.can_flip() {
382 if options.placement.is_vertical() {
383 if options.placement.is_top() && y < scrollable.min_y() {
384 y = trigger.max_y() + options.offset.cross_axis;
385 } else if !options.placement.is_top() && y + element.height() > scrollable.max_y() {
386 y = trigger.min_y() - element.height() - options.offset.cross_axis;
387 }
388 } else if options.placement.is_left() && x < scrollable.min_x() {
389 x = trigger.max_x() + options.offset.main_axis;
390 } else if !options.placement.is_left() && x + element.width() > scrollable.max_x() {
391 x = trigger.min_x() - element.width() - options.offset.main_axis;
392 }
393 }
394 if options.can_shift() {
396 if options.placement.is_vertical() {
397 let min_allowed_x = trigger.min_x() - element.width() + options.padding;
400 let max_allowed_x = trigger.max_x() - options.padding;
401
402 if x < scrollable.min_x() {
404 x = scrollable.min_x();
405 }
406 if x + element.width() > scrollable.max_x() {
407 x = scrollable.max_x() - element.width();
408 }
409
410 x = x.clamp(min_allowed_x, max_allowed_x);
412 } else {
413 let min_allowed_y = trigger.min_y() - element.height() + options.padding;
414 let max_allowed_y = trigger.max_y() - options.padding;
415
416 if y < scrollable.min_y() {
417 y = scrollable.min_y();
418 }
419 if y + element.height() > scrollable.max_y() {
420 y = scrollable.max_y() - element.height();
421 }
422
423 y = y.clamp(min_allowed_y, max_allowed_y);
424 }
425 }
426
427 (x, y)
428 }
429
430 pub fn calculate_placement(
438 &self,
439 scrollable: PixelsRect,
440 element: PixelsRect,
441 trigger: PixelsRect,
442 options: FloatingOptions,
443 ) -> (f64, f64) {
444 let base_pos = self.compute_base_coords(element, trigger, options.clone());
445 let final_pos =
446 self.apply_middleware(base_pos, scrollable, element, trigger, options.clone());
447
448 tracing::debug!(
449 "Calculated for scrollable: {scrollable:?}, element: {element:?}, trigger: {trigger:?}, option: {options:?}"
450 );
451
452 final_pos
453 }
454}