floating_ui_core/middleware/
size.rs

1use std::ptr;
2
3use floating_ui_utils::{Alignment, Axis, Rect, Side, get_side_axis};
4
5use crate::{
6    detect_overflow::{DetectOverflowOptions, detect_overflow},
7    middleware::shift::{SHIFT_NAME, ShiftData},
8    types::{
9        Derivable, DerivableFn, Middleware, MiddlewareReturn, MiddlewareState,
10        MiddlewareWithOptions, ResetRects, ResetValue,
11    },
12};
13
14/// Name of the [`Size`] middleware.
15pub const SIZE_NAME: &str = "size";
16
17/// State passed to [`SizeOptions::apply`].
18#[derive(Clone)]
19pub struct ApplyState<'a, Element: Clone + 'static, Window: Clone> {
20    pub state: MiddlewareState<'a, Element, Window>,
21    pub available_width: f64,
22    pub available_height: f64,
23}
24
25pub type ApplyFn<Element, Window> = dyn Fn(ApplyState<Element, Window>);
26
27/// Options for [`Size`] middleware.
28#[derive(Clone)]
29pub struct SizeOptions<'a, Element: Clone + 'static, Window: Clone> {
30    /// Options for [`detect_overflow`].
31    ///
32    /// Defaults to [`DetectOverflowOptions::default`].
33    pub detect_overflow: Option<DetectOverflowOptions<Element>>,
34
35    /// Function that is called to perform style mutations to the floating element to change its size.
36    pub apply: Option<&'a ApplyFn<Element, Window>>,
37}
38
39impl<'a, Element: Clone, Window: Clone> SizeOptions<'a, Element, Window> {
40    pub fn new() -> Self {
41        SizeOptions {
42            detect_overflow: None,
43            apply: None,
44        }
45    }
46
47    /// Set `detect_overflow` option.
48    pub fn detect_overflow(mut self, value: DetectOverflowOptions<Element>) -> Self {
49        self.detect_overflow = Some(value);
50        self
51    }
52
53    /// Set `apply` option.
54    pub fn apply(mut self, value: &'a ApplyFn<Element, Window>) -> Self {
55        self.apply = Some(value);
56        self
57    }
58}
59
60impl<Element: Clone, Window: Clone> Default for SizeOptions<'_, Element, Window> {
61    fn default() -> Self {
62        Self {
63            detect_overflow: Default::default(),
64            apply: Default::default(),
65        }
66    }
67}
68
69impl<Element: Clone + PartialEq, Window: Clone + PartialEq> PartialEq
70    for SizeOptions<'_, Element, Window>
71{
72    fn eq(&self, other: &Self) -> bool {
73        self.detect_overflow == other.detect_overflow
74            && match (self.apply, other.apply) {
75                (Some(a), Some(b)) => ptr::eq(a, b),
76                (None, None) => true,
77                _ => false,
78            }
79    }
80}
81
82/// Size middleware.
83///
84/// Provides data that allows you to change the size of the floating element -
85/// for instance, prevent it from overflowing the clipping boundary or match the width of the reference element.
86///
87/// See [the Rust Floating UI book](https://floating-ui.rustforweb.org/middleware/size.html) for more documentation.
88#[derive(PartialEq)]
89pub struct Size<'a, Element: Clone + 'static, Window: Clone> {
90    options: Derivable<'a, Element, Window, SizeOptions<'a, Element, Window>>,
91}
92
93impl<'a, Element: Clone + 'static, Window: Clone> Size<'a, Element, Window> {
94    /// Constructs a new instance of this middleware.
95    pub fn new(options: SizeOptions<'a, Element, Window>) -> Self {
96        Size {
97            options: options.into(),
98        }
99    }
100
101    /// Constructs a new instance of this middleware with derivable options.
102    pub fn new_derivable(
103        options: Derivable<'a, Element, Window, SizeOptions<'a, Element, Window>>,
104    ) -> Self {
105        Size { options }
106    }
107
108    /// Constructs a new instance of this middleware with derivable options function.
109    pub fn new_derivable_fn(
110        options: DerivableFn<'a, Element, Window, SizeOptions<'a, Element, Window>>,
111    ) -> Self {
112        Size {
113            options: options.into(),
114        }
115    }
116}
117
118impl<Element: Clone, Window: Clone> Clone for Size<'_, Element, Window> {
119    fn clone(&self) -> Self {
120        Self {
121            options: self.options.clone(),
122        }
123    }
124}
125
126impl<Element: Clone + PartialEq, Window: Clone + PartialEq> Middleware<Element, Window>
127    for Size<'static, Element, Window>
128{
129    fn name(&self) -> &'static str {
130        SIZE_NAME
131    }
132
133    fn compute(&self, state: MiddlewareState<Element, Window>) -> MiddlewareReturn {
134        let options = self.options.evaluate(state.clone());
135
136        let MiddlewareState {
137            placement,
138            elements,
139            rects,
140            middleware_data,
141            platform,
142            ..
143        } = state;
144
145        let overflow = detect_overflow(
146            MiddlewareState {
147                elements: elements.clone(),
148                ..state
149            },
150            options.detect_overflow.unwrap_or_default(),
151        );
152        let side = placement.side();
153        let alignment = placement.alignment();
154        let is_y_axis = get_side_axis(placement) == Axis::Y;
155        let Rect { width, height, .. } = rects.floating;
156
157        let height_side;
158        let width_side;
159
160        match side {
161            Side::Top | Side::Bottom => {
162                height_side = side;
163                width_side = match alignment {
164                    Some(alignment) => {
165                        if alignment
166                            == match platform.is_rtl(elements.floating) {
167                                Some(true) => Alignment::Start,
168                                _ => Alignment::End,
169                            }
170                        {
171                            Side::Left
172                        } else {
173                            Side::Right
174                        }
175                    }
176                    None => Side::Right,
177                };
178            }
179            Side::Right | Side::Left => {
180                width_side = side;
181                height_side = match alignment {
182                    Some(Alignment::End) => Side::Top,
183                    _ => Side::Bottom,
184                };
185            }
186        }
187
188        let maximum_clipping_height = height - overflow.top - overflow.bottom;
189        let maximum_clipping_width = width - overflow.left - overflow.right;
190
191        let overflow_available_height =
192            maximum_clipping_height.min(height - overflow.side(height_side));
193        let overflow_available_width =
194            maximum_clipping_width.min(width - overflow.side(width_side));
195
196        let no_shift = middleware_data.get(SHIFT_NAME).is_none();
197
198        let mut available_height = overflow_available_height;
199        let mut available_width = overflow_available_width;
200
201        let data: Option<ShiftData> = middleware_data.get_as(SHIFT_NAME);
202        if data.as_ref().is_some_and(|data| data.enabled.x) {
203            available_width = maximum_clipping_width;
204        }
205        if data.as_ref().is_some_and(|data| data.enabled.y) {
206            available_height = maximum_clipping_height;
207        }
208
209        if no_shift && alignment.is_none() {
210            let x_min = overflow.left.max(0.0);
211            let x_max = overflow.right.max(0.0);
212            let y_min = overflow.top.max(0.0);
213            let y_max = overflow.bottom.max(0.0);
214
215            if is_y_axis {
216                available_width = width
217                    - 2.0
218                        * (if x_min != 0.0 || x_max != 0.0 {
219                            x_min + x_max
220                        } else {
221                            overflow.left.max(overflow.right)
222                        });
223            } else {
224                available_height = height
225                    - 2.0
226                        * (if y_min != 0.0 || y_max != 0.0 {
227                            y_min + y_max
228                        } else {
229                            overflow.top.max(overflow.bottom)
230                        });
231            }
232        }
233
234        if let Some(apply) = options.apply {
235            apply(ApplyState {
236                state: MiddlewareState {
237                    elements: elements.clone(),
238                    ..state
239                },
240                available_width,
241                available_height,
242            });
243        }
244
245        let next_dimensions = platform.get_dimensions(elements.floating);
246
247        if width != next_dimensions.width || height != next_dimensions.height {
248            MiddlewareReturn {
249                x: None,
250                y: None,
251                data: None,
252                reset: Some(crate::Reset::Value(ResetValue {
253                    placement: None,
254                    rects: Some(ResetRects::True),
255                })),
256            }
257        } else {
258            MiddlewareReturn {
259                x: None,
260                y: None,
261                data: None,
262                reset: None,
263            }
264        }
265    }
266}
267
268impl<'a, Element: Clone, Window: Clone>
269    MiddlewareWithOptions<Element, Window, SizeOptions<'a, Element, Window>>
270    for Size<'a, Element, Window>
271{
272    fn options(&self) -> &Derivable<Element, Window, SizeOptions<'a, Element, Window>> {
273        &self.options
274    }
275}