floating_ui_utils/
lib.rs

1//! Rust port of [Floating UI](https://floating-ui.com/).
2//!
3//! Utility functions shared across Floating UI packages. You may use these functions in your own projects, but are subject to breaking changes.
4//!
5//! See [the Rust Floating UI book](https://floating-ui.rustforweb.org/) for more documenation.
6//!
7//! See [@floating-ui/utils](https://www.npmjs.com/package/@floating-ui/utils) for the original package.
8
9#[cfg(feature = "dom")]
10pub mod dom;
11
12use std::rc::Rc;
13
14use dyn_derive::dyn_trait;
15use serde::{Deserialize, Serialize};
16
17#[derive(Copy, Clone, Debug, PartialEq)]
18pub enum Alignment {
19    Start,
20    End,
21}
22
23#[derive(Copy, Clone, Debug, PartialEq)]
24pub enum Side {
25    Top,
26    Right,
27    Bottom,
28    Left,
29}
30
31impl Side {
32    pub fn opposite(&self) -> Side {
33        match self {
34            Side::Top => Side::Bottom,
35            Side::Right => Side::Left,
36            Side::Bottom => Side::Top,
37            Side::Left => Side::Right,
38        }
39    }
40
41    pub fn axis(&self) -> Axis {
42        match self {
43            Side::Top => Axis::Y,
44            Side::Right => Axis::X,
45            Side::Bottom => Axis::Y,
46            Side::Left => Axis::X,
47        }
48    }
49}
50
51#[derive(Copy, Clone, Debug, PartialEq)]
52pub enum AlignedPlacement {
53    TopStart,
54    TopEnd,
55    RightStart,
56    RightEnd,
57    BottomStart,
58    BottomEnd,
59    LeftStart,
60    LeftEnd,
61}
62
63#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
64pub enum Placement {
65    Top,
66    TopStart,
67    TopEnd,
68    Right,
69    RightStart,
70    RightEnd,
71    Bottom,
72    BottomStart,
73    BottomEnd,
74    Left,
75    LeftStart,
76    LeftEnd,
77}
78
79impl Placement {
80    pub fn alignment(&self) -> Option<Alignment> {
81        match self {
82            Placement::Top => None,
83            Placement::TopStart => Some(Alignment::Start),
84            Placement::TopEnd => Some(Alignment::End),
85            Placement::Right => None,
86            Placement::RightStart => Some(Alignment::Start),
87            Placement::RightEnd => Some(Alignment::End),
88            Placement::Bottom => None,
89            Placement::BottomStart => Some(Alignment::Start),
90            Placement::BottomEnd => Some(Alignment::End),
91            Placement::Left => None,
92            Placement::LeftStart => Some(Alignment::Start),
93            Placement::LeftEnd => Some(Alignment::End),
94        }
95    }
96
97    pub fn side(&self) -> Side {
98        match self {
99            Placement::Top => Side::Top,
100            Placement::TopStart => Side::Top,
101            Placement::TopEnd => Side::Top,
102            Placement::Right => Side::Right,
103            Placement::RightStart => Side::Right,
104            Placement::RightEnd => Side::Right,
105            Placement::Bottom => Side::Bottom,
106            Placement::BottomStart => Side::Bottom,
107            Placement::BottomEnd => Side::Bottom,
108            Placement::Left => Side::Left,
109            Placement::LeftStart => Side::Left,
110            Placement::LeftEnd => Side::Left,
111        }
112    }
113
114    pub fn opposite(&self) -> Placement {
115        match self {
116            Placement::Top => Placement::Bottom,
117            Placement::TopStart => Placement::BottomStart,
118            Placement::TopEnd => Placement::BottomEnd,
119            Placement::Right => Placement::Left,
120            Placement::RightStart => Placement::LeftStart,
121            Placement::RightEnd => Placement::LeftEnd,
122            Placement::Bottom => Placement::Top,
123            Placement::BottomStart => Placement::TopStart,
124            Placement::BottomEnd => Placement::TopEnd,
125            Placement::Left => Placement::Right,
126            Placement::LeftStart => Placement::RightStart,
127            Placement::LeftEnd => Placement::RightEnd,
128        }
129    }
130
131    pub fn opposite_alignment(&self) -> Placement {
132        match self {
133            Placement::Top => Placement::Top,
134            Placement::TopStart => Placement::TopEnd,
135            Placement::TopEnd => Placement::TopStart,
136            Placement::Right => Placement::Right,
137            Placement::RightStart => Placement::RightEnd,
138            Placement::RightEnd => Placement::RightStart,
139            Placement::Bottom => Placement::Bottom,
140            Placement::BottomStart => Placement::BottomEnd,
141            Placement::BottomEnd => Placement::BottomStart,
142            Placement::Left => Placement::Left,
143            Placement::LeftStart => Placement::LeftEnd,
144            Placement::LeftEnd => Placement::LeftStart,
145        }
146    }
147}
148
149impl From<(Side, Option<Alignment>)> for Placement {
150    fn from(value: (Side, Option<Alignment>)) -> Self {
151        match value {
152            (Side::Top, None) => Placement::Top,
153            (Side::Top, Some(Alignment::Start)) => Placement::TopStart,
154            (Side::Top, Some(Alignment::End)) => Placement::TopEnd,
155            (Side::Right, None) => Placement::Right,
156            (Side::Right, Some(Alignment::Start)) => Placement::RightStart,
157            (Side::Right, Some(Alignment::End)) => Placement::RightEnd,
158            (Side::Bottom, None) => Placement::Bottom,
159            (Side::Bottom, Some(Alignment::Start)) => Placement::BottomStart,
160            (Side::Bottom, Some(Alignment::End)) => Placement::BottomEnd,
161            (Side::Left, None) => Placement::Left,
162            (Side::Left, Some(Alignment::Start)) => Placement::LeftStart,
163            (Side::Left, Some(Alignment::End)) => Placement::LeftEnd,
164        }
165    }
166}
167
168#[derive(Copy, Clone, Debug, PartialEq)]
169pub enum Strategy {
170    Absolute,
171    Fixed,
172}
173
174#[derive(Copy, Clone, Debug, PartialEq)]
175pub enum Axis {
176    X,
177    Y,
178}
179
180impl Axis {
181    pub fn opposite(&self) -> Axis {
182        match self {
183            Axis::X => Axis::Y,
184            Axis::Y => Axis::X,
185        }
186    }
187
188    pub fn length(&self) -> Length {
189        match self {
190            Axis::X => Length::Width,
191            Axis::Y => Length::Height,
192        }
193    }
194}
195
196#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
197pub struct Coords {
198    pub x: f64,
199    pub y: f64,
200}
201
202impl Coords {
203    pub fn new(value: f64) -> Self {
204        Self { x: value, y: value }
205    }
206
207    pub fn axis(&self, axis: Axis) -> f64 {
208        match axis {
209            Axis::X => self.x,
210            Axis::Y => self.y,
211        }
212    }
213
214    pub fn update_axis<F>(&mut self, axis: Axis, update: F)
215    where
216        F: Fn(f64) -> f64,
217    {
218        match axis {
219            Axis::X => {
220                self.x = update(self.x);
221            }
222            Axis::Y => {
223                self.y = update(self.y);
224            }
225        }
226    }
227}
228
229#[derive(Copy, Clone, Debug, PartialEq)]
230pub enum Length {
231    Width,
232    Height,
233}
234
235#[derive(Clone, Debug)]
236pub struct Dimensions {
237    pub width: f64,
238    pub height: f64,
239}
240
241impl Dimensions {
242    pub fn length(&self, length: Length) -> f64 {
243        match length {
244            Length::Width => self.width,
245            Length::Height => self.height,
246        }
247    }
248}
249
250#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
251pub struct SideObject {
252    pub top: f64,
253    pub right: f64,
254    pub bottom: f64,
255    pub left: f64,
256}
257
258impl SideObject {
259    pub fn side(&self, side: Side) -> f64 {
260        match side {
261            Side::Top => self.top,
262            Side::Right => self.right,
263            Side::Bottom => self.bottom,
264            Side::Left => self.left,
265        }
266    }
267}
268
269#[derive(Clone, Debug, PartialEq)]
270pub struct PartialSideObject {
271    pub top: Option<f64>,
272    pub right: Option<f64>,
273    pub bottom: Option<f64>,
274    pub left: Option<f64>,
275}
276
277#[derive(Clone, Debug, PartialEq)]
278pub struct Rect {
279    pub x: f64,
280    pub y: f64,
281    pub width: f64,
282    pub height: f64,
283}
284
285impl Rect {
286    pub fn axis(&self, axis: Axis) -> f64 {
287        match axis {
288            Axis::X => self.x,
289            Axis::Y => self.y,
290        }
291    }
292
293    pub fn length(&self, length: Length) -> f64 {
294        match length {
295            Length::Width => self.width,
296            Length::Height => self.height,
297        }
298    }
299}
300
301#[derive(Clone, Debug, PartialEq)]
302pub enum Padding {
303    All(f64),
304    PerSide(PartialSideObject),
305}
306
307#[derive(Clone, Debug, PartialEq)]
308pub struct ClientRectObject {
309    pub x: f64,
310    pub y: f64,
311    pub width: f64,
312    pub height: f64,
313    pub top: f64,
314    pub right: f64,
315    pub bottom: f64,
316    pub left: f64,
317}
318
319impl From<Rect> for ClientRectObject {
320    fn from(value: Rect) -> Self {
321        ClientRectObject {
322            x: value.x,
323            y: value.y,
324            width: value.width,
325            height: value.height,
326            top: value.y,
327            right: value.x + value.width,
328            bottom: value.y + value.height,
329            left: value.x,
330        }
331    }
332}
333
334cfg_if::cfg_if! {
335    if #[cfg(feature = "dom")] {
336        impl ClientRectObject {
337            pub fn from_dom_rect_list(value: web_sys::DomRectList) -> Vec<Self> {
338                (0..value.length())
339                    .filter_map(|i| value.item(i).map(ClientRectObject::from))
340                    .collect()
341            }
342        }
343
344        impl From<web_sys::DomRect> for ClientRectObject {
345            fn from(value: web_sys::DomRect) -> Self {
346                Self {
347                    x: value.x(),
348                    y: value.y(),
349                    width: value.width(),
350                    height: value.height(),
351                    top: value.top(),
352                    right: value.right(),
353                    bottom: value.bottom(),
354                    left: value.left(),
355                }
356            }
357        }
358    }
359}
360
361#[derive(Clone, Debug, PartialEq)]
362pub struct ElementRects {
363    pub reference: Rect,
364    pub floating: Rect,
365}
366
367/// Custom positioning reference element.
368///
369/// See [the Rust Floating UI book](https://floating-ui.rustforweb.org/virtual-elements.html) for more documentation.
370#[dyn_trait]
371pub trait VirtualElement<Element: 'static>: Clone + PartialEq {
372    fn get_bounding_client_rect(&self) -> ClientRectObject;
373
374    fn get_client_rects(&self) -> Option<Vec<ClientRectObject>>;
375
376    fn context_element(&self) -> Option<Element>;
377}
378
379#[dyn_trait]
380pub trait GetBoundingClientRectCloneable: Clone {
381    fn call(&self) -> ClientRectObject;
382}
383
384impl<F> GetBoundingClientRectCloneable for F
385where
386    F: Fn() -> ClientRectObject + Clone + 'static,
387{
388    fn call(&self) -> ClientRectObject {
389        self()
390    }
391}
392
393#[dyn_trait]
394pub trait GetClientRectsCloneable: Clone {
395    fn call(&self) -> Vec<ClientRectObject>;
396}
397
398impl<F> GetClientRectsCloneable for F
399where
400    F: Fn() -> Vec<ClientRectObject> + Clone + 'static,
401{
402    fn call(&self) -> Vec<ClientRectObject> {
403        self()
404    }
405}
406
407#[derive(Clone)]
408pub struct DefaultVirtualElement<Element: Clone> {
409    pub get_bounding_client_rect: Rc<dyn GetBoundingClientRectCloneable>,
410    pub get_client_rects: Option<Rc<dyn GetClientRectsCloneable>>,
411    pub context_element: Option<Element>,
412}
413
414impl<Element: Clone> DefaultVirtualElement<Element> {
415    pub fn new(get_bounding_client_rect: Rc<dyn GetBoundingClientRectCloneable>) -> Self {
416        DefaultVirtualElement {
417            get_bounding_client_rect,
418            get_client_rects: None,
419            context_element: None,
420        }
421    }
422
423    pub fn get_bounding_client_rect(
424        mut self,
425        get_bounding_client_rect: Rc<dyn GetBoundingClientRectCloneable>,
426    ) -> Self {
427        self.get_bounding_client_rect = get_bounding_client_rect;
428        self
429    }
430
431    pub fn get_client_rects(mut self, get_client_rects: Rc<dyn GetClientRectsCloneable>) -> Self {
432        self.get_client_rects = Some(get_client_rects);
433        self
434    }
435
436    pub fn context_element(mut self, context_element: Element) -> Self {
437        self.context_element = Some(context_element);
438        self
439    }
440}
441
442impl<Element: Clone + PartialEq + 'static> VirtualElement<Element>
443    for DefaultVirtualElement<Element>
444{
445    fn get_bounding_client_rect(&self) -> ClientRectObject {
446        (self.get_bounding_client_rect).call()
447    }
448
449    fn get_client_rects(&self) -> Option<Vec<ClientRectObject>> {
450        self.get_client_rects
451            .as_ref()
452            .map(|get_client_rects| get_client_rects.call())
453    }
454
455    fn context_element(&self) -> Option<Element> {
456        self.context_element.clone()
457    }
458}
459
460impl<Element: Clone + PartialEq + 'static> PartialEq for DefaultVirtualElement<Element> {
461    fn eq(&self, other: &Self) -> bool {
462        Rc::ptr_eq(
463            &self.get_bounding_client_rect,
464            &other.get_bounding_client_rect,
465        ) && match (
466            self.get_client_rects.as_ref(),
467            other.get_client_rects.as_ref(),
468        ) {
469            (Some(a), Some(b)) => Rc::ptr_eq(a, b),
470            (None, None) => true,
471            _ => false,
472        } && self.context_element == other.context_element
473    }
474}
475
476#[derive(Clone)]
477pub enum ElementOrVirtual<'a, Element: Clone + 'static> {
478    Element(&'a Element),
479    VirtualElement(Box<dyn VirtualElement<Element>>),
480}
481
482impl<Element: Clone + 'static> ElementOrVirtual<'_, Element> {
483    pub fn resolve(self) -> Option<Element> {
484        match self {
485            ElementOrVirtual::Element(element) => Some(element.clone()),
486            ElementOrVirtual::VirtualElement(virtal_element) => virtal_element.context_element(),
487        }
488    }
489}
490
491impl<'a, Element: Clone> From<&'a Element> for ElementOrVirtual<'a, Element> {
492    fn from(value: &'a Element) -> Self {
493        ElementOrVirtual::Element(value)
494    }
495}
496
497impl<Element: Clone> From<Box<dyn VirtualElement<Element>>> for ElementOrVirtual<'_, Element> {
498    fn from(value: Box<dyn VirtualElement<Element>>) -> Self {
499        ElementOrVirtual::VirtualElement(value)
500    }
501}
502
503impl<'a, Element: Clone> From<&'a OwnedElementOrVirtual<Element>>
504    for ElementOrVirtual<'a, Element>
505{
506    fn from(value: &'a OwnedElementOrVirtual<Element>) -> Self {
507        match value {
508            OwnedElementOrVirtual::Element(element) => ElementOrVirtual::Element(element),
509            OwnedElementOrVirtual::VirtualElement(virtual_element) => {
510                ElementOrVirtual::VirtualElement(virtual_element.clone())
511            }
512        }
513    }
514}
515
516#[derive(Clone)]
517pub enum OwnedElementOrVirtual<Element: 'static> {
518    Element(Element),
519    VirtualElement(Box<dyn VirtualElement<Element>>),
520}
521
522impl<Element: 'static> OwnedElementOrVirtual<Element> {
523    pub fn resolve(self) -> Option<Element> {
524        match self {
525            OwnedElementOrVirtual::Element(element) => Some(element),
526            OwnedElementOrVirtual::VirtualElement(virtal_element) => {
527                virtal_element.context_element()
528            }
529        }
530    }
531}
532
533impl<Element> From<Element> for OwnedElementOrVirtual<Element> {
534    fn from(value: Element) -> Self {
535        OwnedElementOrVirtual::Element(value)
536    }
537}
538
539impl<Element> From<Box<dyn VirtualElement<Element>>> for OwnedElementOrVirtual<Element> {
540    fn from(value: Box<dyn VirtualElement<Element>>) -> Self {
541        OwnedElementOrVirtual::VirtualElement(value)
542    }
543}
544
545#[derive(Clone, Debug, PartialEq)]
546pub enum ElementOrWindow<'a, Element, Window> {
547    Element(&'a Element),
548    Window(&'a Window),
549}
550
551impl<'a, Element, Window> From<&'a OwnedElementOrWindow<Element, Window>>
552    for ElementOrWindow<'a, Element, Window>
553{
554    fn from(value: &'a OwnedElementOrWindow<Element, Window>) -> Self {
555        match value {
556            OwnedElementOrWindow::Element(element) => ElementOrWindow::Element(element),
557            OwnedElementOrWindow::Window(window) => ElementOrWindow::Window(window),
558        }
559    }
560}
561
562#[derive(Clone, Debug, PartialEq)]
563pub enum OwnedElementOrWindow<Element, Window> {
564    Element(Element),
565    Window(Window),
566}
567
568pub const ALL_PLACEMENTS: [Placement; 12] = [
569    Placement::Top,
570    Placement::TopStart,
571    Placement::TopEnd,
572    Placement::Right,
573    Placement::RightStart,
574    Placement::RightEnd,
575    Placement::Bottom,
576    Placement::BottomStart,
577    Placement::BottomEnd,
578    Placement::Left,
579    Placement::LeftStart,
580    Placement::LeftEnd,
581];
582
583pub const ALL_SIDES: [Side; 4] = [Side::Top, Side::Right, Side::Bottom, Side::Left];
584
585pub fn clamp(start: f64, value: f64, end: f64) -> f64 {
586    value.min(end).max(start)
587}
588
589pub fn get_side(placement: Placement) -> Side {
590    placement.side()
591}
592
593pub fn get_alignment(placement: Placement) -> Option<Alignment> {
594    placement.alignment()
595}
596
597pub fn get_placement(side: Side, alignment: Option<Alignment>) -> Placement {
598    (side, alignment).into()
599}
600
601pub fn get_opposite_axis(axis: Axis) -> Axis {
602    axis.opposite()
603}
604
605pub fn get_axis_length(axis: Axis) -> Length {
606    axis.length()
607}
608
609pub fn get_side_axis(placement: Placement) -> Axis {
610    placement.side().axis()
611}
612
613pub fn get_alignment_axis(placement: Placement) -> Axis {
614    get_opposite_axis(get_side_axis(placement))
615}
616
617pub fn get_alignment_sides(
618    placement: Placement,
619    rects: &ElementRects,
620    rtl: Option<bool>,
621) -> (Side, Side) {
622    let alignment = get_alignment(placement);
623    let alignment_axis = get_alignment_axis(placement);
624    let length = get_axis_length(alignment_axis);
625
626    let mut main_alignment_side = match (alignment_axis, alignment) {
627        (Axis::X, Some(Alignment::Start)) => match rtl {
628            Some(true) => Side::Left,
629            _ => Side::Right,
630        },
631        (Axis::X, _) => match rtl {
632            Some(true) => Side::Right,
633            _ => Side::Left,
634        },
635        (Axis::Y, Some(Alignment::Start)) => Side::Bottom,
636        (Axis::Y, _) => Side::Top,
637    };
638
639    if rects.reference.length(length) > rects.floating.length(length) {
640        main_alignment_side = get_opposite_side(main_alignment_side);
641    }
642
643    (main_alignment_side, get_opposite_side(main_alignment_side))
644}
645
646pub fn get_expanded_placements(placement: Placement) -> Vec<Placement> {
647    let opposite_placement = get_opposite_placement(placement);
648
649    vec![
650        get_opposite_alignment_placement(placement),
651        opposite_placement,
652        get_opposite_alignment_placement(opposite_placement),
653    ]
654}
655
656pub fn get_opposite_alignment_placement(placement: Placement) -> Placement {
657    placement.opposite_alignment()
658}
659
660pub fn get_side_list(side: Side, is_start: bool, rtl: Option<bool>) -> Vec<Side> {
661    match side {
662        Side::Top | Side::Bottom => match rtl {
663            Some(true) => {
664                if is_start {
665                    vec![Side::Right, Side::Left]
666                } else {
667                    vec![Side::Left, Side::Right]
668                }
669            }
670            _ => {
671                if is_start {
672                    vec![Side::Left, Side::Right]
673                } else {
674                    vec![Side::Right, Side::Left]
675                }
676            }
677        },
678        Side::Right | Side::Left => {
679            if is_start {
680                vec![Side::Top, Side::Bottom]
681            } else {
682                vec![Side::Bottom, Side::Top]
683            }
684        }
685    }
686}
687
688pub fn get_opposite_side(side: Side) -> Side {
689    side.opposite()
690}
691
692pub fn get_opposite_axis_placements(
693    placement: Placement,
694    flip_alignment: bool,
695    direction: Option<Alignment>,
696    rtl: Option<bool>,
697) -> Vec<Placement> {
698    let alignment = get_alignment(placement);
699    let side_list = get_side_list(
700        get_side(placement),
701        direction.is_some_and(|d| d == Alignment::Start),
702        rtl,
703    );
704
705    let mut list: Vec<Placement> = side_list
706        .into_iter()
707        .map(|side| get_placement(side, alignment))
708        .collect();
709
710    if flip_alignment {
711        let mut opposite_list: Vec<Placement> = list
712            .clone()
713            .into_iter()
714            .map(get_opposite_alignment_placement)
715            .collect();
716
717        list.append(&mut opposite_list);
718    }
719
720    list
721}
722
723pub fn get_opposite_placement(placement: Placement) -> Placement {
724    placement.opposite()
725}
726
727pub fn expand_padding_object(padding: PartialSideObject) -> SideObject {
728    SideObject {
729        top: padding.top.unwrap_or(0.0),
730        right: padding.right.unwrap_or(0.0),
731        bottom: padding.bottom.unwrap_or(0.0),
732        left: padding.left.unwrap_or(0.0),
733    }
734}
735
736pub fn get_padding_object(padding: Padding) -> SideObject {
737    match padding {
738        Padding::All(padding) => SideObject {
739            top: padding,
740            right: padding,
741            bottom: padding,
742            left: padding,
743        },
744        Padding::PerSide(padding) => expand_padding_object(padding),
745    }
746}
747
748pub fn rect_to_client_rect(rect: Rect) -> ClientRectObject {
749    ClientRectObject {
750        x: rect.x,
751        y: rect.y,
752        width: rect.width,
753        height: rect.height,
754        top: rect.y,
755        right: rect.x + rect.width,
756        bottom: rect.y + rect.height,
757        left: rect.x,
758    }
759}