Middleware
Objects that change the positioning of the floating element, executed in order as a queue.
Middleware allow you to customize the behavior of the positioning and be as granular as you want, adding your own custom logic.
compute_position()
starts with initial positioning via placement - then middleware are executed as an in-between "middle" step of the initial placement computation and eventual return of data for rendering.
Each middleware is executed in order:
compute_position(
reference_el,
floating_el,
ComputePositionConfig::new(platform)
.position(Placement::Right)
.middleware(vec![]),
);
Example
use floating_ui_core::{Middleware, MiddlewareReturn, MiddlewareState};
const SHIFT_BY_ONE_PIXEL_NAME: &str = "shiftByOnePixel";
#[derive(Clone, PartialEq)]
struct ShiftByOnePixel {}
impl ShiftByOnePixel {
pub fn new() -> Self {
ShiftByOnePixel {}
}
}
impl<Element: Clone + PartialEq, Window: Clone + PartialEq> Middleware<Element, Window>
for ShiftByOnePixel
{
fn name(&self) -> &'static str {
SHIFT_BY_ONE_PIXEL_NAME
}
fn compute(&self, state: MiddlewareState<Element, Window>) -> MiddlewareReturn {
MiddlewareReturn {
x: Some(state.x + 1.0),
y: Some(state.y + 1.0),
data: None,
reset: None,
}
}
}
This (not particularly useful) middleware adds 1
pixel to the coordinates. To use this middleware, add it to your middleware
vector:
compute_position(
reference_el,
floating_el,
ComputePositionConfig::new(platform)
.position(Placement::Right)
.middleware(vec![
Box::new(ShiftByOnePixel::new())
]),
);
Here, compute_position()
will compute coordinates that will place the floating element to the right center of the reference element, lying flush with it.
Middleware are then executed, resulting in these coordinates getting shifted by one pixel. Then that data is returned for rendering.
Shape
A middleware is a struct that implements the Middleware
trait. It has a name
and a compute
method The compute
method provides the logic of the middleware, which returns new positioning coordinates or useful data.
Data
Any data can be passed via an optional data
field of the struct instance that is returned from compute
. This will be accessible to the consumer via the middleware_data
field:
use floating_ui_core::{Middleware, MiddlewareReturn, MiddlewareState};
use serde::{Deserialize, Serialize};
const SHIFT_BY_ONE_PIXEL_NAME: &str = "shiftByOnePixel";
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct ShiftByOnePixelData {
pub amount: f64,
}
#[derive(Clone, PartialEq)]
struct ShiftByOnePixel {}
impl ShiftByOnePixel {
pub fn new() -> Self {
ShiftByOnePixel {}
}
}
impl<Element: Clone + PartialEq, Window: Clone + PartialEq> Middleware<Element, Window>
for ShiftByOnePixel
{
fn name(&self) -> &'static str {
SHIFT_BY_ONE_PIXEL_NAME
}
fn compute(&self, state: MiddlewareState<Element, Window>) -> MiddlewareReturn {
MiddlewareReturn {
x: Some(state.x + 1.0),
y: Some(state.y + 1.0),
data: Some(
serde_json::to_value(ShiftByOnePixelData {
amount: 1.0,
})
.expect("Data should be valid JSON."),
),
reset: None,
}
}
}
let ComputePositionReturn {
middleware_data,
..
} = compute_position(
reference_el,
floating_el,
ComputePositionConfig::new(platform)
.position(Placement::Right)
.middleware(vec![
Box::new(ShiftByOnePixel::new())
]),
);
if let Some(data) = middleware_data.get_as::<ShiftByOnePixelData>(SHIFT_BY_ONE_PIXEL_NAME) {
log::info!("{:#?}", data);
}
Options
Options can be passed to the middleware and stored in the struct:
use floating_ui_core::{Middleware, MiddlewareReturn, MiddlewareState};
const SHIFT_BY_ONE_PIXEL_NAME: &str = "shiftByOnePixel";
#[derive(Clone, Debug, Default, PartialEq)]
struct ShiftByOnePixelOptions {
amount: f64,
}
impl ShiftByOnePixelOptions {
pub fn amount(mut self, value: f64) -> Self {
self.amount = value;
self
}
}
#[derive(Clone, PartialEq)]
struct ShiftByOnePixel {
options: ShiftByOnePixelOptions,
}
impl ShiftByOnePixel {
pub fn new(options: ShiftByOnePixelOptions) -> Self {
ShiftByOnePixel {
options,
}
}
}
impl<Element: Clone + PartialEq, Window: Clone + PartialEq> Middleware<Element, Window>
for ShiftByOnePixel
{
fn name(&self) -> &'static str {
SHIFT_BY_ONE_PIXEL_NAME
}
fn compute(&self, state: MiddlewareState<Element, Window>) -> MiddlewareReturn {
MiddlewareReturn {
x: Some(state.x + self.options.amount),
y: Some(state.y + self.options.amount),
data: None,
reset: None,
}
}
}
The options can be passed to the middleware to configure the behavior:
let middleware = vec![Box::new(ShiftByOnePixel::new(ShiftByOnePixelOptions::default().amount(10)))];
Middleware State
A struct instance is passed to compute
containing useful data about the middleware lifecycle being executed.
In the previous examples, we used x
and y
out of the compute
parameter struct. These are only two fields that get passed into middleware, but there are many more.
The fields passed are below:
pub struct MiddlewareState<'a, Element: Clone + 'static, Window: Clone> {
pub x: f64,
pub y: f64,
pub initial_placement: Placement,
pub placement: Placement,
pub strategy: Strategy,
pub middleware_data: &'a MiddlewareData,
pub elements: Elements<'a, Element>,
pub rects: &'a ElementRects,
pub platform: &'a dyn Platform<Element, Window>,
}
x
This is the x-axis coordinate to position the floating element to.
y
This is the y-axis coordinate to position the floating element to.
elements
This is a struct instance containing the reference and floating elements.
rects
This is a struct instance containing the Rect
s of the reference and floating elements, a struct of shape {x, y, width, height}
.
middleware_data
This is a struct instance containing all the data of any middleware at the current step in the lifecycle. The lifecycle loops over the middleware
vector, so later middleware have access to data from any middleware run prior.
strategy
The positioning strategy.
initial_placement
The initial (or preferred) placement passed in to compute_position()
.
placement
The stateful resultant placement. Middleware like Flip
change initial_placement
to a new one.
platform
A struct instance containing methods to make Floating UI work on the current platform, e.g. DOM.
Ordering
The order in which middleware are placed in the vector matters, as middleware use the coordinates that were returned from previous ones. This means they perform their work based on the current positioning state.
Three ShiftByOnePixel
in the middleware vector means the coordinates get shifted by 3 pixels in total:
let middleware = vec![
Box::new(ShiftByOnePixel::new()),
Box::new(ShiftByOnePixel::new()),
Box::new(ShiftByOnePixel::new()),
];
If the later ShiftByOnePixel
implementations had a condition based on the current value of x
and y
, the condition can change based on their placement in the vector.
Understanding this can help in knowing which order to place middleware in, as placing a middleware before or after another can produce a different result.
In general, Offset
should always go at the beginning of the middleware vector, while Arrow
and Hide
at the end. The other core middleware can be shifted around depending on the desired behavior.
let middleware = vec![
Box::new(Offset::new(OffsetOptions::default())),
// ...
Box::new(Arrow::new(ArrowOptions::new(arrow_element))),
Box::new(Hide::new(HideOptions::default())),
];
Resetting the Lifecycle
There are use cases for needing to reset the middleware lifecycle so that other middleware perform fresh logic.
- When
Flip
andAutoPlacement
change the placement, they reset the lifecycle so that other middleware that modify the coordinates based on the currentplacement
do not perform stale logic. Size
resets the lifecycle with the newly applied dimensions, as many middleware read the dimensions to perform their logic.Inline
resets the lifecycle when it changes the reference rect to a custom implementation, similar to a Virtual Element.
In order to do this, add a reset
field to the struct instance returned from compute
.
pub enum Reset {
True,
Value(ResetValue),
}
pub struct ResetValue {
pub placement: Option<Placement>,
pub rects: Option<ResetRects>,
}
// `True` will compute the new `rects` if the dimensions were mutated.
// Otherwise, you can return your own new rects.
pub enum ResetRects {
True,
Value(ElementRects),
}
const SOME_NAME: &str = "some";
#[derive(Clone, PartialEq)]
struct SomeMiddleware {}
impl SomeMiddleware {
pub fn new() -> Self {
SomeMiddleware {}
}
}
impl<Element: Clone + PartialEq, Window: Clone + PartialEq> Middleware<Element, Window>
for SomeMiddleware
{
fn name(&self) -> &'static str {
SOME_NAME
}
fn compute(&self, state: MiddlewareState<Element, Window>) -> MiddlewareReturn {
if some_condition {
MiddlewareReturn {
x: None,
y: None,
data: None,
reset: Some(Reset::Value(ResetValue {
placements: Some(next_placement),
reset: None
})),
}
} else {
MiddlewareReturn {
x: None,
y: None,
data: None,
reset: None,
}
}
}
}
Data supplied to middleware_data
is preserved by doing this, so you can read it at any point after you've reset the lifecycle.