floating_ui_core/middleware/
flip.rs

1use floating_ui_utils::{
2    Alignment, Axis, Placement, get_alignment, get_alignment_sides, get_expanded_placements,
3    get_opposite_axis_placements, get_opposite_placement, get_side, get_side_axis,
4};
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    detect_overflow::{DetectOverflowOptions, detect_overflow},
9    middleware::arrow::{ARROW_NAME, ArrowData},
10    types::{
11        Derivable, DerivableFn, Middleware, MiddlewareReturn, MiddlewareState,
12        MiddlewareWithOptions, Reset, ResetValue,
13    },
14};
15
16/// Name of the [`Flip`] middleware.
17pub const FLIP_NAME: &str = "flip";
18
19/// Cross axis option used by [`Flip`] middleware.
20#[derive(Copy, Clone, Debug, PartialEq)]
21pub enum CrossAxis {
22    /// Whether to check cross axis overflow for both side and alignment flipping.
23    True,
24    /// Whether to disable all cross axis overflow checking.
25    False,
26    /// Whether to check cross axis overflow for alignment flipping only.
27    Alignment,
28}
29
30/// Fallback strategy used by [`Flip`] middleware.
31#[derive(Copy, Clone, Debug, Default, PartialEq)]
32pub enum FallbackStrategy {
33    #[default]
34    BestFit,
35    InitialPlacement,
36}
37
38/// Options for [`Flip`] middleware.
39#[derive(Clone, Debug, PartialEq)]
40pub struct FlipOptions<Element: Clone> {
41    /// Options for [`detect_overflow`].
42    ///
43    /// Defaults to [`DetectOverflowOptions::default`].
44    pub detect_overflow: Option<DetectOverflowOptions<Element>>,
45
46    /// The axis that runs along the side of the floating element. Determines whether overflow along this axis is checked to perform a flip.
47    ///
48    /// Defaults to `true`.
49    pub main_axis: Option<bool>,
50
51    /// The axis that runs along the alignment of the floating element. Determines whether overflow along this axis is checked to perform a flip.
52    /// - [`CrossAxis::True`]: Whether to check cross axis overflow for both side and alignment flipping.
53    /// - [`CrossAxis::False`]: Whether to disable all cross axis overflow checking.
54    /// - [`CrossAxis::Alignment`]: Whether to check cross axis overflow for alignment flipping only.
55    ///
56    /// Defaults to `true`.
57    pub cross_axis: Option<CrossAxis>,
58
59    /// Placements to try sequentially if the preferred `placement` does not fit.
60    ///
61    /// Defaults to the opposite placement.
62    pub fallback_placements: Option<Vec<Placement>>,
63
64    /// What strategy to use when no placements fit.
65    ///
66    /// Defaults to [`FallbackStrategy::BestFit`].
67    pub fallback_strategy: Option<FallbackStrategy>,
68
69    /// Whether to allow fallback to the perpendicular axis of the preferred placement, and if so, which side direction along the axis to prefer.
70    ///
71    /// Defaults to [`Option::None`] (disallow fallback).
72    pub fallback_axis_side_direction: Option<Alignment>,
73
74    /// Whether to flip to placements with the opposite alignment if they fit better.
75    ///
76    /// Defaults to `true`.
77    pub flip_alignment: Option<bool>,
78}
79
80impl<Element: Clone> FlipOptions<Element> {
81    /// Set `detect_overflow` option.
82    pub fn detect_overflow(mut self, value: DetectOverflowOptions<Element>) -> Self {
83        self.detect_overflow = Some(value);
84        self
85    }
86
87    /// Set `main_axis` option.
88    pub fn main_axis(mut self, value: bool) -> Self {
89        self.main_axis = Some(value);
90        self
91    }
92
93    /// Set `cross_axis` option.
94    pub fn cross_axis(mut self, value: CrossAxis) -> Self {
95        self.cross_axis = Some(value);
96        self
97    }
98
99    /// Set `fallback_placements` option.
100    pub fn fallback_placements(mut self, value: Vec<Placement>) -> Self {
101        self.fallback_placements = Some(value);
102        self
103    }
104
105    /// Set `fallback_strategy` option.
106    pub fn fallback_strategy(mut self, value: FallbackStrategy) -> Self {
107        self.fallback_strategy = Some(value);
108        self
109    }
110
111    /// Set `fallback_axis_side_direction` option.
112    pub fn fallback_axis_side_direction(mut self, value: Alignment) -> Self {
113        self.fallback_axis_side_direction = Some(value);
114        self
115    }
116
117    /// Set `flip_alignment` option.
118    pub fn flip_alignment(mut self, value: bool) -> Self {
119        self.flip_alignment = Some(value);
120        self
121    }
122}
123
124impl<Element: Clone> Default for FlipOptions<Element> {
125    fn default() -> Self {
126        Self {
127            detect_overflow: Default::default(),
128            main_axis: Default::default(),
129            cross_axis: Default::default(),
130            fallback_placements: Default::default(),
131            fallback_strategy: Default::default(),
132            fallback_axis_side_direction: Default::default(),
133            flip_alignment: Default::default(),
134        }
135    }
136}
137
138/// An overflow stored in [`FlipData`].
139#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
140pub struct FlipDataOverflow {
141    pub placement: Placement,
142    pub overflows: Vec<f64>,
143}
144
145/// Data stored by [`Flip`] middleware.
146#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
147pub struct FlipData {
148    pub index: usize,
149    pub overflows: Vec<FlipDataOverflow>,
150}
151
152/// Flip middleware.
153///
154/// Optimizes the visibility of the floating element by flipping the `placement` in order to keep it in view when the preferred placement(s) will overflow the clipping boundary.
155/// Alternative to [`AutoPlacement`][`crate::middleware::AutoPlacement`].
156///
157/// See [the Rust Floating UI book](https://floating-ui.rustforweb.org/middleware/flip.html) for more documentation.
158#[derive(PartialEq)]
159pub struct Flip<'a, Element: Clone + 'static, Window: Clone> {
160    options: Derivable<'a, Element, Window, FlipOptions<Element>>,
161}
162
163impl<'a, Element: Clone + 'static, Window: Clone> Flip<'a, Element, Window> {
164    /// Constructs a new instance of this middleware.
165    pub fn new(options: FlipOptions<Element>) -> Self {
166        Flip {
167            options: options.into(),
168        }
169    }
170
171    /// Constructs a new instance of this middleware with derivable options.
172    pub fn new_derivable(options: Derivable<'a, Element, Window, FlipOptions<Element>>) -> Self {
173        Flip { options }
174    }
175
176    /// Constructs a new instance of this middleware with derivable options function.
177    pub fn new_derivable_fn(
178        options: DerivableFn<'a, Element, Window, FlipOptions<Element>>,
179    ) -> Self {
180        Flip {
181            options: options.into(),
182        }
183    }
184}
185
186impl<Element: Clone + 'static, Window: Clone> Clone for Flip<'_, Element, Window> {
187    fn clone(&self) -> Self {
188        Self {
189            options: self.options.clone(),
190        }
191    }
192}
193
194impl<Element: Clone + PartialEq, Window: Clone + PartialEq> Middleware<Element, Window>
195    for Flip<'static, Element, Window>
196{
197    fn name(&self) -> &'static str {
198        FLIP_NAME
199    }
200
201    fn compute(&self, state: MiddlewareState<Element, Window>) -> MiddlewareReturn {
202        let options = self.options.evaluate(state.clone());
203
204        let MiddlewareState {
205            placement,
206            initial_placement,
207            middleware_data,
208            elements,
209            rects,
210            platform,
211            ..
212        } = state;
213
214        let data: FlipData = middleware_data.get_as(self.name()).unwrap_or(FlipData {
215            index: 0,
216            overflows: vec![],
217        });
218
219        let check_main_axis = options.main_axis.unwrap_or(true);
220        let check_cross_axis = options.cross_axis.unwrap_or(CrossAxis::True);
221        let specified_fallback_placements = options.fallback_placements.clone();
222        let fallback_strategy = options.fallback_strategy.unwrap_or_default();
223        let fallback_axis_side_direction = options.fallback_axis_side_direction;
224        let flip_alignment = options.flip_alignment.unwrap_or(true);
225
226        // If a reset by the arrow was caused due to an alignment offset being added,
227        // we should skip any logic now since `flip()` has already done its work.
228        let arrow_data: Option<ArrowData> = middleware_data.get_as(ARROW_NAME);
229        if arrow_data
230            .and_then(|arrow_data| arrow_data.alignment_offset)
231            .is_some()
232        {
233            return MiddlewareReturn {
234                x: None,
235                y: None,
236                data: None,
237                reset: None,
238            };
239        }
240
241        let side = get_side(placement);
242        let initial_side_axis = get_side_axis(initial_placement);
243        let is_base_placement = get_alignment(initial_placement).is_none();
244        let rtl = platform.is_rtl(elements.floating);
245
246        let has_specified_fallback_placements = specified_fallback_placements.is_some();
247        let mut placements =
248            specified_fallback_placements.unwrap_or(if is_base_placement || !flip_alignment {
249                vec![get_opposite_placement(initial_placement)]
250            } else {
251                get_expanded_placements(initial_placement)
252            });
253
254        let has_fallback_axis_side_direction = fallback_axis_side_direction.is_some();
255
256        if !has_specified_fallback_placements && has_fallback_axis_side_direction {
257            placements.append(&mut get_opposite_axis_placements(
258                initial_placement,
259                flip_alignment,
260                fallback_axis_side_direction,
261                rtl,
262            ));
263        }
264
265        placements.insert(0, initial_placement);
266
267        let overflow = detect_overflow(
268            MiddlewareState {
269                elements: elements.clone(),
270                ..state
271            },
272            options.detect_overflow.unwrap_or_default(),
273        );
274
275        let mut overflows: Vec<f64> = Vec::new();
276        let mut overflows_data = data.overflows;
277
278        if check_main_axis {
279            overflows.push(overflow.side(side));
280        }
281        if check_cross_axis == CrossAxis::True || check_cross_axis == CrossAxis::Alignment {
282            let sides = get_alignment_sides(placement, rects, rtl);
283            overflows.push(overflow.side(sides.0));
284            overflows.push(overflow.side(sides.1));
285        }
286
287        overflows_data.push(FlipDataOverflow {
288            placement,
289            overflows: overflows.clone(),
290        });
291
292        // One or more sides is overflowing.
293        if !overflows.into_iter().all(|side| side <= 0.0) {
294            let next_index = data.index + 1;
295            let next_placement = placements.get(next_index);
296
297            if let Some(next_placement) = next_placement {
298                let ignore_cross_axis_overflow = if check_cross_axis == CrossAxis::Alignment {
299                    initial_side_axis != get_side_axis(*next_placement)
300                } else {
301                    false
302                };
303
304                if !ignore_cross_axis_overflow ||
305                    // We leave the current main axis only if every placement on that axis overflows the main axis.
306                    overflows_data.iter().all(|d| {
307                        if get_side_axis(d.placement) == initial_side_axis {
308                            d.overflows.first().is_some_and(|overflow| *overflow > 0.0)
309                        } else {
310                            true
311                        }
312                    })
313                {
314                    // Try next placement and re-run the lifecycle.
315                    return MiddlewareReturn {
316                        x: None,
317                        y: None,
318                        data: Some(
319                            serde_json::to_value(FlipData {
320                                index: next_index,
321                                overflows: overflows_data,
322                            })
323                            .expect("Data should be valid JSON."),
324                        ),
325                        reset: Some(Reset::Value(ResetValue {
326                            placement: Some(*next_placement),
327                            rects: None,
328                        })),
329                    };
330                }
331            }
332
333            // First, find the candidates that fit on the main axis side of overflow, then find the placement that fits the best on the main cross axis side.
334            let mut reset_placement: Vec<&FlipDataOverflow> = overflows_data
335                .iter()
336                .filter(|overflow| overflow.overflows[0] <= 0.0)
337                .collect();
338            reset_placement.sort_by(|a, b| a.overflows[1].total_cmp(&b.overflows[1]));
339
340            let mut reset_placement = reset_placement.first().map(|overflow| overflow.placement);
341
342            // Otherwise fallback.
343            if reset_placement.is_none() {
344                match fallback_strategy {
345                    FallbackStrategy::BestFit => {
346                        let mut placement: Vec<(Placement, f64)> = overflows_data
347                            .into_iter()
348                            .filter(|overflow| {
349                                if has_fallback_axis_side_direction {
350                                    let current_side_axis = get_side_axis(overflow.placement);
351
352                                    // Create a bias to the `y` side axis due to horizontal reading directions favoring greater width.
353                                    current_side_axis == initial_side_axis
354                                        || current_side_axis == Axis::Y
355                                } else {
356                                    true
357                                }
358                            })
359                            .map(|overflow| {
360                                (
361                                    overflow.placement,
362                                    overflow
363                                        .overflows
364                                        .into_iter()
365                                        .filter(|overflow| *overflow > 0.0)
366                                        .sum::<f64>(),
367                                )
368                            })
369                            .collect();
370                        placement.sort_by(|a, b| a.1.total_cmp(&b.1));
371
372                        let placement = placement.first().map(|v| v.0);
373                        if placement.is_some() {
374                            reset_placement = placement;
375                        }
376                    }
377                    FallbackStrategy::InitialPlacement => {
378                        reset_placement = Some(initial_placement);
379                    }
380                }
381            }
382
383            if placement != reset_placement.expect("Reset placement is not none.") {
384                return MiddlewareReturn {
385                    x: None,
386                    y: None,
387                    data: None,
388                    reset: Some(Reset::Value(ResetValue {
389                        placement: reset_placement,
390                        rects: None,
391                    })),
392                };
393            }
394        }
395
396        MiddlewareReturn {
397            x: None,
398            y: None,
399            data: None,
400            reset: None,
401        }
402    }
403}
404
405impl<Element: Clone, Window: Clone> MiddlewareWithOptions<Element, Window, FlipOptions<Element>>
406    for Flip<'_, Element, Window>
407{
408    fn options(&self) -> &Derivable<'_, Element, Window, FlipOptions<Element>> {
409        &self.options
410    }
411}