Skip to content

Commit

Permalink
Add inline middleware and inline visual test
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielleHuisman committed Apr 7, 2024
1 parent 602fb22 commit 571af01
Show file tree
Hide file tree
Showing 19 changed files with 668 additions and 62 deletions.
6 changes: 3 additions & 3 deletions packages/core/src/compute_position.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::types::{
/// This export does not have any `platform` interface logic. You will need to write one for the platform you are using Floating UI with.
///
/// See [`Platform`][`crate::types::Platform`].
pub fn compute_position<Element, Window>(
pub fn compute_position<Element: Clone, Window: Clone>(
reference: &Element,
floating: &Element,
config: ComputePositionConfig<Element, Window>,
Expand All @@ -24,7 +24,7 @@ pub fn compute_position<Element, Window>(
let rtl = platform.is_rtl(floating);

let mut rects = platform.get_element_rects(GetElementRectsArgs {
reference,
reference: reference.into(),
floating,
strategy,
});
Expand Down Expand Up @@ -79,7 +79,7 @@ pub fn compute_position<Element, Window>(
rects = match reset_rects {
ResetRects::True => {
platform.get_element_rects(GetElementRectsArgs {
reference,
reference: reference.into(),
floating,
strategy,
})
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/detect_overflow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ impl<Element> Default for DetectOverflowOptions<Element> {
/// - `0` = lies flush with the boundary
///
/// See <https://floating-ui.com/docs/detectOverflow> for the original documentation.
pub fn detect_overflow<Element, Window>(
pub fn detect_overflow<Element: Clone, Window: Clone>(
state: MiddlewareState<Element, Window>,
options: DetectOverflowOptions<Element>,
) -> SideObject {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod arrow;
mod auto_placement;
mod flip;
mod hide;
mod inline;
mod offset;
mod shift;
mod size;
Expand All @@ -12,6 +13,7 @@ pub use arrow::*;
pub use auto_placement::*;
pub use flip::*;
pub use hide::*;
pub use inline::*;
pub use offset::*;
pub use shift::*;
pub use size::*;
2 changes: 1 addition & 1 deletion packages/core/src/middleware/auto_placement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ impl<'a, Element: Clone, Window: Clone> Middleware<Element, Window>
})
.collect();

placements_sorted_by_most_space.sort_by_key(|v| v.1 as i64);
placements_sorted_by_most_space.sort_by(|a, b| a.1.total_cmp(&b.1));

let placements_that_fit_on_each_side: Vec<_> = placements_sorted_by_most_space
.clone()
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/middleware/flip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ impl<'a, Element: Clone, Window: Clone> Middleware<Element, Window> for Flip<'a,
.iter()
.filter(|overflow| overflow.overflows[0] <= 0.0)
.collect();
reset_placement.sort_by_key(|overflow| overflow.overflows[1] as i64);
reset_placement.sort_by(|a, b| a.overflows[1].total_cmp(&b.overflows[1]));

let mut reset_placement = reset_placement.first().map(|overflow| overflow.placement);

Expand All @@ -313,7 +313,7 @@ impl<'a, Element: Clone, Window: Clone> Middleware<Element, Window> for Flip<'a,
)
})
.collect();
placement.sort_by_key(|v| v.1 as i64);
placement.sort_by(|a, b| a.1.total_cmp(&b.1));

let placement = placement.first().map(|v| v.0);
if placement.is_some() {
Expand Down
314 changes: 314 additions & 0 deletions packages/core/src/middleware/inline.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
use floating_ui_utils::{
get_padding_object, get_side_axis, rect_to_client_rect, Axis, ClientRectObject, Coords,
DefaultVirtualElement, ElementOrVirtual, Padding, Rect, Side,
};

use crate::{
types::{
Derivable, DerivableFn, Middleware, MiddlewareReturn, MiddlewareState,
MiddlewareWithOptions,
},
GetElementRectsArgs, Reset, ResetRects, ResetValue,
};

fn get_bounding_rect(rects: Vec<ClientRectObject>) -> Rect {
let min_x = rects
.iter()
.map(|rect| rect.left)
.reduce(f64::min)
.unwrap_or(f64::INFINITY);
let min_y = rects
.iter()
.map(|rect| rect.top)
.reduce(f64::min)
.unwrap_or(f64::INFINITY);
let max_x = rects
.iter()
.map(|rect| rect.right)
.reduce(f64::max)
.unwrap_or(f64::NEG_INFINITY);
let max_y = rects
.iter()
.map(|rect| rect.bottom)
.reduce(f64::max)
.unwrap_or(f64::NEG_INFINITY);
Rect {
x: min_x,
y: min_y,
width: max_x - min_y,
height: max_y - min_y,
}
}

fn get_rects_by_line(rects: Vec<ClientRectObject>) -> Vec<ClientRectObject> {
let mut sorted_rects = rects.clone();
sorted_rects.sort_by(|a, b| a.y.total_cmp(&b.y));

let mut groups: Vec<Vec<ClientRectObject>> = vec![];
let mut prev_rect: Option<ClientRectObject> = None;
for rect in sorted_rects {
if prev_rect.is_none()
|| prev_rect.is_some_and(|prev_rect| rect.y - prev_rect.y > prev_rect.height / 2.0)
{
groups.push(vec![rect.clone()]);
} else {
groups
.last_mut()
.expect("Last group should exist.")
.push(rect.clone());
}
prev_rect = Some(rect);
}

groups
.into_iter()
.map(|rects| rect_to_client_rect(get_bounding_rect(rects)))
.collect()
}

/// Name of the [`Inline`] middleware.
pub const INLINE_NAME: &str = "inline";

/// Options for [`Inline`].
#[derive(Clone, Debug, Default)]
pub struct InlineOptions {
/// Viewport-relative `x` coordinate to choose a `ClientRect`.
///
/// Defaults to [`None`].
pub x: Option<f64>,

/// Viewport-relative `y` coordinate to choose a `ClientRect`.
///
/// Defaults to [`None`].
pub y: Option<f64>,

/// Represents the padding around a disjoined rect when choosing it.
///
/// Defaults to `2` on all sides.
pub padding: Option<Padding>,
}

impl InlineOptions {
/// Set `x` option.
pub fn x(mut self, value: f64) -> Self {
self.x = Some(value);
self
}

/// Set `y` option.
pub fn y(mut self, value: f64) -> Self {
self.y = Some(value);
self
}

/// Set `x` and `y` options using [`Coords`].
pub fn coords(mut self, value: Coords) -> Self {
self.x = Some(value.x);
self.y = Some(value.y);
self
}

/// Set `padding` option.
pub fn padding(mut self, value: Padding) -> Self {
self.padding = Some(value);
self
}
}

/// Provides improved positioning for inline reference elements that can span over multiple lines, such as hyperlinks or range selections.
///
/// See <https://floating-ui.com/docs/inline> for the original documentation.
pub struct Inline<'a, Element: Clone, Window: Clone> {
options: Derivable<'a, Element, Window, InlineOptions>,
}

impl<'a, Element: Clone, Window: Clone> Inline<'a, Element, Window> {
/// Constructs a new instance of this middleware.
pub fn new(options: InlineOptions) -> Self {
Inline {
options: options.into(),
}
}

/// Constructs a new instance of this middleware with derivable options.
pub fn new_derivable(options: Derivable<'a, Element, Window, InlineOptions>) -> Self {
Inline { options }
}

/// Constructs a new instance of this middleware with derivable options function.
pub fn new_derivable_fn(options: DerivableFn<'a, Element, Window, InlineOptions>) -> Self {
Inline {
options: options.into(),
}
}
}

impl<'a, Element: Clone, Window: Clone> Clone for Inline<'a, Element, Window> {
fn clone(&self) -> Self {
Self {
options: self.options.clone(),
}
}
}

impl<'a, Element: Clone + 'static, Window: Clone> Middleware<Element, Window>
for Inline<'a, Element, Window>
{
fn name(&self) -> &'static str {
INLINE_NAME
}

fn compute(&self, state: MiddlewareState<Element, Window>) -> MiddlewareReturn {
let options = self.options.evaluate(state.clone());

let MiddlewareState {
placement,
strategy,
elements,
rects,
platform,
..
} = state;

let padding = options.padding.unwrap_or(Padding::All(2.0));

let native_client_rects = platform
.get_client_rects(elements.reference)
.unwrap_or(vec![]);

let client_rects = get_rects_by_line(native_client_rects.clone());
let fallback = rect_to_client_rect(get_bounding_rect(native_client_rects));
let padding_object = get_padding_object(padding);

let get_bounding_client_rect = move || {
// There are two rects and they are disjoined.
if client_rects.len() == 2 && client_rects[0].left > client_rects[1].right {
if let Some(x) = options.x {
if let Some(y) = options.y {
return client_rects
.clone()
.into_iter()
.find(|rect| {
x > rect.left - padding_object.left
&& x < rect.right + padding_object.right
&& y > rect.top - padding_object.top
&& rect.y < rect.bottom + padding_object.bottom
})
.unwrap_or(fallback.clone());
}
}
}

// There are 2 or more connected rects.
if client_rects.len() >= 2 {
if get_side_axis(placement) == Axis::Y {
let first_rect = client_rects.first().expect("Enough elements exist.");
let last_rect = client_rects.last().expect("Enough elements exist.");
let is_top = placement.side() == Side::Top;

let top = first_rect.top;
let bottom = last_rect.bottom;
let left = match is_top {
true => first_rect.left,
false => last_rect.left,
};
let right = match is_top {
true => first_rect.right,
false => last_rect.right,
};
let width = right - left;
let height = bottom - top;

return ClientRectObject {
x: left,
y: top,
width,
height,
top,
right,
bottom,
left,
};
}

let is_left_side = placement.side() == Side::Left;
let max_right = client_rects
.iter()
.map(|rect| rect.right)
.reduce(f64::max)
.expect("Enough elements exist.");
let min_left = client_rects
.iter()
.map(|rect| rect.left)
.reduce(f64::min)
.expect("Enough elements exist.");
let measure_rects: Vec<&ClientRectObject> = client_rects
.iter()
.filter(|rect| match is_left_side {
true => rect.left == min_left,
false => rect.right == max_right,
})
.collect();

let top = measure_rects.first().expect("Enough elements exist.").top;
let bottom = measure_rects.last().expect("Enough elements exist.").bottom;
let left = min_left;
let right = max_right;
let width = right - left;
let height = bottom - top;

return ClientRectObject {
x: left,
y: top,
width,
height,
top,
right,
bottom,
left,
};
}

fallback.clone()
};

let reset_rects = platform.get_element_rects(GetElementRectsArgs {
reference: ElementOrVirtual::VirtualElement(Box::new(DefaultVirtualElement::new(
Box::new(get_bounding_client_rect),
))),
floating: elements.floating,
strategy,
});

if rects.reference.x != reset_rects.reference.x
|| rects.reference.y != reset_rects.reference.y
|| rects.reference.width != reset_rects.reference.width
|| rects.reference.height != reset_rects.reference.height
{
MiddlewareReturn {
x: None,
y: None,
data: None,
reset: Some(Reset::Value(ResetValue {
placement: None,
rects: Some(ResetRects::Value(reset_rects)),
})),
}
} else {
MiddlewareReturn {
x: None,
y: None,
data: None,
reset: None,
}
}
}
}

impl<'a, Element: Clone, Window: Clone> MiddlewareWithOptions<Element, Window, InlineOptions>
for Inline<'a, Element, Window>
{
fn options(&self) -> &Derivable<Element, Window, InlineOptions> {
&self.options
}
}
Loading

0 comments on commit 571af01

Please sign in to comment.