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                let has_initial_main_axis_overflow = overflows_data
304                    .first()
305                    .and_then(|overflow| overflow.overflows.first())
306                    .is_some_and(|overflow| *overflow > 0.0);
307
308                if !ignore_cross_axis_overflow || has_initial_main_axis_overflow {
309                    // Try next placement and re-run the lifecycle.
310                    return MiddlewareReturn {
311                        x: None,
312                        y: None,
313                        data: Some(
314                            serde_json::to_value(FlipData {
315                                index: next_index,
316                                overflows: overflows_data,
317                            })
318                            .expect("Data should be valid JSON."),
319                        ),
320                        reset: Some(Reset::Value(ResetValue {
321                            placement: Some(*next_placement),
322                            rects: None,
323                        })),
324                    };
325                }
326            }
327
328            // 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.
329            let mut reset_placement: Vec<&FlipDataOverflow> = overflows_data
330                .iter()
331                .filter(|overflow| overflow.overflows[0] <= 0.0)
332                .collect();
333            reset_placement.sort_by(|a, b| a.overflows[1].total_cmp(&b.overflows[1]));
334
335            let mut reset_placement = reset_placement.first().map(|overflow| overflow.placement);
336
337            // Otherwise fallback.
338            if reset_placement.is_none() {
339                match fallback_strategy {
340                    FallbackStrategy::BestFit => {
341                        let mut placement: Vec<(Placement, f64)> = overflows_data
342                            .into_iter()
343                            .filter(|overflow| {
344                                if has_fallback_axis_side_direction {
345                                    let current_side_axis = get_side_axis(overflow.placement);
346
347                                    // Create a bias to the `y` side axis due to horizontal reading directions favoring greater width.
348                                    current_side_axis == initial_side_axis
349                                        || current_side_axis == Axis::Y
350                                } else {
351                                    true
352                                }
353                            })
354                            .map(|overflow| {
355                                (
356                                    overflow.placement,
357                                    overflow
358                                        .overflows
359                                        .into_iter()
360                                        .filter(|overflow| *overflow > 0.0)
361                                        .sum::<f64>(),
362                                )
363                            })
364                            .collect();
365                        placement.sort_by(|a, b| a.1.total_cmp(&b.1));
366
367                        let placement = placement.first().map(|v| v.0);
368                        if placement.is_some() {
369                            reset_placement = placement;
370                        }
371                    }
372                    FallbackStrategy::InitialPlacement => {
373                        reset_placement = Some(initial_placement);
374                    }
375                }
376            }
377
378            if placement != reset_placement.expect("Reset placement is not none.") {
379                return MiddlewareReturn {
380                    x: None,
381                    y: None,
382                    data: None,
383                    reset: Some(Reset::Value(ResetValue {
384                        placement: reset_placement,
385                        rects: None,
386                    })),
387                };
388            }
389        }
390
391        MiddlewareReturn {
392            x: None,
393            y: None,
394            data: None,
395            reset: None,
396        }
397    }
398}
399
400impl<Element: Clone, Window: Clone> MiddlewareWithOptions<Element, Window, FlipOptions<Element>>
401    for Flip<'_, Element, Window>
402{
403    fn options(&self) -> &Derivable<Element, Window, FlipOptions<Element>> {
404        &self.options
405    }
406}