floating_ui_core/middleware/
inline.rs

1use std::rc::Rc;
2
3use floating_ui_utils::{
4    Axis, ClientRectObject, Coords, DefaultVirtualElement, ElementOrVirtual, Padding, Rect, Side,
5    get_padding_object, get_side_axis, rect_to_client_rect,
6};
7
8use crate::types::{
9    Derivable, DerivableFn, GetElementRectsArgs, Middleware, MiddlewareReturn, MiddlewareState,
10    MiddlewareWithOptions, Reset, ResetRects, ResetValue,
11};
12
13fn get_bounding_rect(rects: Vec<ClientRectObject>) -> Rect {
14    let min_x = rects
15        .iter()
16        .map(|rect| rect.left)
17        .reduce(f64::min)
18        .unwrap_or(f64::INFINITY);
19    let min_y = rects
20        .iter()
21        .map(|rect| rect.top)
22        .reduce(f64::min)
23        .unwrap_or(f64::INFINITY);
24    let max_x = rects
25        .iter()
26        .map(|rect| rect.right)
27        .reduce(f64::max)
28        .unwrap_or(f64::NEG_INFINITY);
29    let max_y = rects
30        .iter()
31        .map(|rect| rect.bottom)
32        .reduce(f64::max)
33        .unwrap_or(f64::NEG_INFINITY);
34    Rect {
35        x: min_x,
36        y: min_y,
37        width: max_x - min_x,
38        height: max_y - min_y,
39    }
40}
41
42fn get_rects_by_line(rects: Vec<ClientRectObject>) -> Vec<ClientRectObject> {
43    let mut sorted_rects = rects.clone();
44    sorted_rects.sort_by(|a, b| a.y.total_cmp(&b.y));
45
46    let mut groups: Vec<Vec<ClientRectObject>> = vec![];
47    let mut prev_rect: Option<ClientRectObject> = None;
48    for rect in sorted_rects {
49        if prev_rect.is_none()
50            || prev_rect.is_some_and(|prev_rect| rect.y - prev_rect.y > prev_rect.height / 2.0)
51        {
52            groups.push(vec![rect.clone()]);
53        } else {
54            groups
55                .last_mut()
56                .expect("Last group should exist.")
57                .push(rect.clone());
58        }
59        prev_rect = Some(rect);
60    }
61
62    groups
63        .into_iter()
64        .map(|rects| rect_to_client_rect(get_bounding_rect(rects)))
65        .collect()
66}
67
68/// Name of the [`Inline`] middleware.
69pub const INLINE_NAME: &str = "inline";
70
71/// Options for [`Inline`].
72#[derive(Clone, Debug, Default, PartialEq)]
73pub struct InlineOptions {
74    /// Viewport-relative `x` coordinate to choose a `ClientRect`.
75    ///
76    /// Defaults to [`None`].
77    pub x: Option<f64>,
78
79    /// Viewport-relative `y` coordinate to choose a `ClientRect`.
80    ///
81    /// Defaults to [`None`].
82    pub y: Option<f64>,
83
84    /// Represents the padding around a disjoined rect when choosing it.
85    ///
86    /// Defaults to `2` on all sides.
87    pub padding: Option<Padding>,
88}
89
90impl InlineOptions {
91    /// Set `x` option.
92    pub fn x(mut self, value: f64) -> Self {
93        self.x = Some(value);
94        self
95    }
96
97    /// Set `y` option.
98    pub fn y(mut self, value: f64) -> Self {
99        self.y = Some(value);
100        self
101    }
102
103    /// Set `x` and `y` options using [`Coords`].
104    pub fn coords(mut self, value: Coords) -> Self {
105        self.x = Some(value.x);
106        self.y = Some(value.y);
107        self
108    }
109
110    /// Set `padding` option.
111    pub fn padding(mut self, value: Padding) -> Self {
112        self.padding = Some(value);
113        self
114    }
115}
116
117/// Inline middleware.
118///
119/// Provides improved positioning for inline reference elements that can span over multiple lines, such as hyperlinks or range selections.
120///
121/// See [the Rust Floating UI book](https://floating-ui.rustforweb.org/middleware/inline.html) for more documentation.
122#[derive(PartialEq)]
123pub struct Inline<'a, Element: Clone + 'static, Window: Clone> {
124    options: Derivable<'a, Element, Window, InlineOptions>,
125}
126
127impl<'a, Element: Clone + 'static, Window: Clone> Inline<'a, Element, Window> {
128    /// Constructs a new instance of this middleware.
129    pub fn new(options: InlineOptions) -> Self {
130        Inline {
131            options: options.into(),
132        }
133    }
134
135    /// Constructs a new instance of this middleware with derivable options.
136    pub fn new_derivable(options: Derivable<'a, Element, Window, InlineOptions>) -> Self {
137        Inline { options }
138    }
139
140    /// Constructs a new instance of this middleware with derivable options function.
141    pub fn new_derivable_fn(options: DerivableFn<'a, Element, Window, InlineOptions>) -> Self {
142        Inline {
143            options: options.into(),
144        }
145    }
146}
147
148impl<Element: Clone, Window: Clone> Clone for Inline<'_, Element, Window> {
149    fn clone(&self) -> Self {
150        Self {
151            options: self.options.clone(),
152        }
153    }
154}
155
156impl<Element: Clone + PartialEq + 'static, Window: Clone + PartialEq + 'static>
157    Middleware<Element, Window> for Inline<'static, Element, Window>
158{
159    fn name(&self) -> &'static str {
160        INLINE_NAME
161    }
162
163    fn compute(&self, state: MiddlewareState<Element, Window>) -> MiddlewareReturn {
164        let options = self.options.evaluate(state.clone());
165
166        let MiddlewareState {
167            placement,
168            strategy,
169            elements,
170            rects,
171            platform,
172            ..
173        } = state;
174
175        // A MouseEvent's client{X,Y} coords can be up to 2 pixels off a ClientRect's bounds,
176        // despite the event listener being triggered. A padding of 2 seems to handle this issue.
177        let padding = options.padding.unwrap_or(Padding::All(2.0));
178
179        let native_client_rects = platform
180            .get_client_rects(elements.reference)
181            .unwrap_or(vec![]);
182
183        let client_rects = get_rects_by_line(native_client_rects.clone());
184        let fallback = rect_to_client_rect(get_bounding_rect(native_client_rects));
185        let padding_object = get_padding_object(padding);
186
187        let get_bounding_client_rect = move || {
188            // There are two rects and they are disjoined.
189            if client_rects.len() == 2
190                && client_rects[0].left > client_rects[1].right
191                && let Some(x) = options.x
192                && let Some(y) = options.y
193            {
194                return client_rects
195                    .clone()
196                    .into_iter()
197                    .find(|rect| {
198                        x > rect.left - padding_object.left
199                            && x < rect.right + padding_object.right
200                            && y > rect.top - padding_object.top
201                            && rect.y < rect.bottom + padding_object.bottom
202                    })
203                    .unwrap_or(fallback.clone());
204            }
205
206            // There are 2 or more connected rects.
207            if client_rects.len() >= 2 {
208                if get_side_axis(placement) == Axis::Y {
209                    let first_rect = client_rects.first().expect("Enough elements exist.");
210                    let last_rect = client_rects.last().expect("Enough elements exist.");
211                    let is_top = placement.side() == Side::Top;
212
213                    let top = first_rect.top;
214                    let bottom = last_rect.bottom;
215                    let left = if is_top {
216                        first_rect.left
217                    } else {
218                        last_rect.left
219                    };
220                    let right = if is_top {
221                        first_rect.right
222                    } else {
223                        last_rect.right
224                    };
225                    let width = right - left;
226                    let height = bottom - top;
227
228                    return ClientRectObject {
229                        x: left,
230                        y: top,
231                        width,
232                        height,
233                        top,
234                        right,
235                        bottom,
236                        left,
237                    };
238                }
239
240                let is_left_side = placement.side() == Side::Left;
241                let max_right = client_rects
242                    .iter()
243                    .map(|rect| rect.right)
244                    .reduce(f64::max)
245                    .expect("Enough elements exist.");
246                let min_left = client_rects
247                    .iter()
248                    .map(|rect| rect.left)
249                    .reduce(f64::min)
250                    .expect("Enough elements exist.");
251                let measure_rects: Vec<&ClientRectObject> = client_rects
252                    .iter()
253                    .filter(|rect| {
254                        if is_left_side {
255                            rect.left == min_left
256                        } else {
257                            rect.right == max_right
258                        }
259                    })
260                    .collect();
261
262                let top = measure_rects.first().expect("Enough elements exist.").top;
263                let bottom = measure_rects.last().expect("Enough elements exist.").bottom;
264                let left = min_left;
265                let right = max_right;
266                let width = right - left;
267                let height = bottom - top;
268
269                return ClientRectObject {
270                    x: left,
271                    y: top,
272                    width,
273                    height,
274                    top,
275                    right,
276                    bottom,
277                    left,
278                };
279            }
280
281            fallback.clone()
282        };
283
284        let reset_rects = platform.get_element_rects(GetElementRectsArgs {
285            reference: ElementOrVirtual::VirtualElement(Box::new(DefaultVirtualElement::new(
286                Rc::new(get_bounding_client_rect),
287            ))),
288            floating: elements.floating,
289            strategy,
290        });
291
292        if rects.reference.x != reset_rects.reference.x
293            || rects.reference.y != reset_rects.reference.y
294            || rects.reference.width != reset_rects.reference.width
295            || rects.reference.height != reset_rects.reference.height
296        {
297            MiddlewareReturn {
298                x: None,
299                y: None,
300                data: None,
301                reset: Some(Reset::Value(ResetValue {
302                    placement: None,
303                    rects: Some(ResetRects::Value(reset_rects)),
304                })),
305            }
306        } else {
307            MiddlewareReturn {
308                x: None,
309                y: None,
310                data: None,
311                reset: None,
312            }
313        }
314    }
315}
316
317impl<Element: Clone, Window: Clone> MiddlewareWithOptions<Element, Window, InlineOptions>
318    for Inline<'_, Element, Window>
319{
320    fn options(&self) -> &Derivable<'_, Element, Window, InlineOptions> {
321        &self.options
322    }
323}