floating_ui_core/middleware/
offset.rs

1use floating_ui_utils::{
2    Alignment, Axis, Coords, Placement, Side, get_alignment, get_side, get_side_axis,
3};
4use serde::{Deserialize, Serialize};
5
6use crate::{
7    middleware::{ARROW_NAME, ArrowData},
8    types::{
9        Derivable, DerivableFn, Middleware, MiddlewareReturn, MiddlewareState,
10        MiddlewareWithOptions,
11    },
12};
13
14fn convert_value_to_coords<Element: Clone, Window: Clone>(
15    state: MiddlewareState<Element, Window>,
16    options: &OffsetOptions,
17) -> Coords {
18    let MiddlewareState {
19        placement,
20        platform,
21        elements,
22        ..
23    } = state;
24
25    let rtl = platform.is_rtl(elements.floating).unwrap_or(false);
26    let side = get_side(placement);
27    let alignment = get_alignment(placement);
28    let is_vertical = get_side_axis(placement) == Axis::Y;
29    let main_axis_multi = match side {
30        Side::Left | Side::Top => -1.0,
31        Side::Right | Side::Bottom => 1.0,
32    };
33    let cross_axis_multi = if rtl && is_vertical { -1.0 } else { 1.0 };
34
35    let (main_axis, mut cross_axis, alignment_axis): (f64, f64, Option<f64>) = match options {
36        OffsetOptions::Value(value) => (*value, 0.0, None),
37        OffsetOptions::Values(values) => (
38            values.main_axis.unwrap_or(0.0),
39            values.cross_axis.unwrap_or(0.0),
40            values.alignment_axis,
41        ),
42    };
43
44    if let Some(alignment) = alignment
45        && let Some(alignment_axis) = alignment_axis
46    {
47        cross_axis = match alignment {
48            Alignment::Start => alignment_axis,
49            Alignment::End => -alignment_axis,
50        };
51    }
52
53    if is_vertical {
54        Coords {
55            x: cross_axis * cross_axis_multi,
56            y: main_axis * main_axis_multi,
57        }
58    } else {
59        Coords {
60            x: main_axis * main_axis_multi,
61            y: cross_axis * cross_axis_multi,
62        }
63    }
64}
65
66/// Name of the [`Offset`] middleware.
67pub const OFFSET_NAME: &str = "offset";
68
69/// Axes configuration for [`OffsetOptions`].
70#[derive(Clone, Default, Debug, PartialEq)]
71pub struct OffsetOptionsValues {
72    /// The axis that runs along the side of the floating element. Represents the distance (gutter or margin) between the reference and floating element.
73    ///
74    /// Defaults to `0`.
75    pub main_axis: Option<f64>,
76
77    /// The axis that runs along the alignment of the floating element. Represents the skidding between the reference and floating element.
78    ///
79    /// Defaults to `0`.
80    pub cross_axis: Option<f64>,
81
82    /// The same axis as [`cross_axis`][`Self::cross_axis`] but applies only to aligned placements and inverts the [`End`][`floating_ui_utils::Alignment::End`] alignment.
83    /// When set to a number, it overrides the [`cross_axis`][`Self::cross_axis`] value.
84    ///
85    /// A positive number will move the floating element in the direction of the opposite edge to the one that is aligned, while a negative number the reverse.
86    ///
87    /// Defaults to [`Option::None`].
88    pub alignment_axis: Option<f64>,
89}
90
91impl OffsetOptionsValues {
92    /// Set `main_axis` option.
93    pub fn main_axis(mut self, value: f64) -> Self {
94        self.main_axis = Some(value);
95        self
96    }
97
98    /// Set `cross_axis` option.
99    pub fn cross_axis(mut self, value: f64) -> Self {
100        self.cross_axis = Some(value);
101        self
102    }
103
104    /// Set `alignment_axis` option.
105    pub fn alignment_axis(mut self, value: f64) -> Self {
106        self.alignment_axis = Some(value);
107        self
108    }
109}
110
111/// Options for [`Offset`] middleware.
112///
113/// A number (shorthand for [`main_axis`][`OffsetOptionsValues::main_axis`] or distance) or an axes configuration ([`OffsetOptionsValues`]).
114#[derive(Clone, Debug, PartialEq)]
115pub enum OffsetOptions {
116    Value(f64),
117    Values(OffsetOptionsValues),
118}
119
120impl Default for OffsetOptions {
121    fn default() -> Self {
122        OffsetOptions::Value(0.0)
123    }
124}
125
126/// Data stored by [`Offset`] middleware.
127#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
128pub struct OffsetData {
129    pub diff_coords: Coords,
130    pub placement: Placement,
131}
132
133/// Offset middleware.
134///
135/// Modifies the placement by translating the floating element along the specified axes.
136///
137/// See [the Rust Floating UI book](https://floating-ui.rustforweb.org/middleware/offset.html) for more documentation.
138#[derive(PartialEq)]
139pub struct Offset<'a, Element: Clone + 'static, Window: Clone> {
140    options: Derivable<'a, Element, Window, OffsetOptions>,
141}
142
143impl<'a, Element: Clone, Window: Clone> Offset<'a, Element, Window> {
144    /// Constructs a new instance of this middleware.
145    pub fn new(options: OffsetOptions) -> Self {
146        Offset {
147            options: options.into(),
148        }
149    }
150
151    /// Constructs a new instance of this middleware with derivable options.
152    pub fn new_derivable(options: Derivable<'a, Element, Window, OffsetOptions>) -> Self {
153        Offset { options }
154    }
155
156    /// Constructs a new instance of this middleware with derivable options function.
157    pub fn new_derivable_fn(options: DerivableFn<'a, Element, Window, OffsetOptions>) -> Self {
158        Offset {
159            options: options.into(),
160        }
161    }
162}
163
164impl<Element: Clone + 'static, Window: Clone> Clone for Offset<'_, Element, Window> {
165    fn clone(&self) -> Self {
166        Self {
167            options: self.options.clone(),
168        }
169    }
170}
171
172impl<Element: Clone + PartialEq, Window: Clone + PartialEq> Middleware<Element, Window>
173    for Offset<'static, Element, Window>
174{
175    fn name(&self) -> &'static str {
176        OFFSET_NAME
177    }
178
179    fn compute(&self, state: MiddlewareState<Element, Window>) -> MiddlewareReturn {
180        let options = self.options.evaluate(state.clone());
181
182        let MiddlewareState {
183            x,
184            y,
185            placement,
186            middleware_data,
187            ..
188        } = state;
189
190        let data: Option<OffsetData> = middleware_data.get_as(self.name());
191
192        let diff_coords = convert_value_to_coords(state, &options);
193
194        // If the placement is the same and the arrow caused an alignment offset then we don't need to change the positioning coordinates.
195        if let Some(data_placement) = data.map(|data| data.placement)
196            && placement == data_placement
197        {
198            let arrow_data: Option<ArrowData> = middleware_data.get_as(ARROW_NAME);
199            if arrow_data
200                .and_then(|arrow_data| arrow_data.alignment_offset)
201                .is_some()
202            {
203                return MiddlewareReturn {
204                    x: None,
205                    y: None,
206                    data: None,
207                    reset: None,
208                };
209            }
210        }
211
212        MiddlewareReturn {
213            x: Some(x + diff_coords.x),
214            y: Some(y + diff_coords.y),
215            data: Some(
216                serde_json::to_value(OffsetData {
217                    diff_coords,
218                    placement,
219                })
220                .expect("Data should be valid JSON."),
221            ),
222            reset: None,
223        }
224    }
225}
226
227impl<Element: Clone, Window: Clone> MiddlewareWithOptions<Element, Window, OffsetOptions>
228    for Offset<'_, Element, Window>
229{
230    fn options(&self) -> &Derivable<'_, Element, Window, OffsetOptions> {
231        &self.options
232    }
233}