use std::rc::Rc;
use dioxus::html::geometry::{ClientPoint, PixelsRect, PixelsSize, PixelsVector2D};
use dioxus::logger::tracing;
use dioxus::prelude::*;
#[derive(Debug, Clone, Copy, Default)]
pub struct Floating;
#[derive(Debug, Clone, Copy)]
pub struct ScrollState {
pub size: PixelsSize,
pub bounds: PixelsSize,
pub state: PixelsVector2D,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Placement {
TopStart,
TopCenter,
TopEnd,
BottomStart,
BottomCenter,
BottomEnd,
LeftStart,
LeftCenter,
LeftEnd,
RightStart,
RightCenter,
RightEnd,
}
impl Placement {
pub fn is_vertical(&self) -> bool {
matches!(
self,
Placement::TopStart
| Placement::TopCenter
| Placement::TopEnd
| Placement::BottomStart
| Placement::BottomCenter
| Placement::BottomEnd
)
}
pub fn is_top(&self) -> bool {
matches!(
self,
Placement::TopEnd | Placement::TopCenter | Placement::TopStart
)
}
pub fn is_left(&self) -> bool {
matches!(
self,
Placement::LeftCenter | Placement::LeftEnd | Placement::LeftStart
)
}
pub fn get_modifier(&self) -> PlacementModifier {
match self {
&Placement::BottomCenter => PlacementModifier::Center,
&Placement::LeftCenter => PlacementModifier::Center,
&Placement::RightCenter => PlacementModifier::Center,
&Placement::TopCenter => PlacementModifier::Center,
&Placement::BottomEnd => PlacementModifier::End,
&Placement::LeftEnd => PlacementModifier::End,
&Placement::RightEnd => PlacementModifier::End,
&Placement::TopEnd => PlacementModifier::End,
&Placement::BottomStart => PlacementModifier::Start,
&Placement::LeftStart => PlacementModifier::Start,
&Placement::RightStart => PlacementModifier::Start,
&Placement::TopStart => PlacementModifier::Start,
}
}
}
pub enum PlacementModifier {
Center,
Start,
End,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Middleware {
Flip,
Shift,
}
#[derive(Debug, Clone)]
pub struct FloatingOptions {
pub middleware: Vec<Middleware>,
pub offset: f64,
pub padding: f64,
pub placement: Placement,
}
impl FloatingOptions {
pub fn can_flip(&self) -> bool {
self.middleware.contains(&Middleware::Flip)
}
pub fn can_shift(&self) -> bool {
self.middleware.contains(&Middleware::Shift)
}
}
impl Default for FloatingOptions {
fn default() -> Self {
FloatingOptions {
middleware: vec![Middleware::Flip, Middleware::Shift],
offset: 1_f64,
padding: 0_f64,
placement: Placement::BottomStart,
}
}
}
impl Floating {
pub async fn generate_scroll_state_from_mounted(&self, data: Rc<MountedData>) -> ScrollState {
let rect = data.get_client_rect().await;
let scroll = data.get_scroll_size().await;
let offset = data.get_scroll_offset().await;
let size = scroll
.map(|s| PixelsSize::new(s.width, s.height))
.unwrap_or(PixelsSize::new(0_f64, 0_f64));
let bounds = rect
.map(|r| PixelsSize::new(r.width(), r.height()))
.unwrap_or(PixelsSize::new(0_f64, 0_f64));
let state = offset
.map(|o| PixelsVector2D::new(o.x, o.y))
.unwrap_or(PixelsVector2D::new(0_f64, 0_f64));
ScrollState {
size,
bounds,
state,
}
}
pub fn generate_scroll_state(&self, evt: ScrollEvent) -> ScrollState {
ScrollState {
size: PixelsSize::new(evt.scroll_width() as f64, evt.scroll_height() as f64),
bounds: PixelsSize::new(evt.client_width() as f64, evt.client_height() as f64),
state: PixelsVector2D::new(evt.scroll_left(), evt.scroll_top()),
}
}
pub async fn placement_on_point(
&self,
scroll_state: ScrollState,
scrollable_ref: Rc<MountedData>,
element_ref: Rc<MountedData>,
trigger: ClientPoint,
options: FloatingOptions,
) -> (f64, f64) {
let scrollable_rect = scrollable_ref
.get_client_rect()
.await
.unwrap_or(PixelsRect::new(
PixelsVector2D::new(0_f64, 0_f64).to_point(),
scroll_state.bounds,
));
let trigger_rect = PixelsRect::new(
PixelsVector2D::new(trigger.x, trigger.y).to_point(),
PixelsSize::new(1_f64, 1_f64),
);
match element_ref.get_client_rect().await {
Ok(element_rect) => {
self.calculate_placement(scrollable_rect, element_rect, trigger_rect, options)
}
Err(_) => (trigger_rect.min_x(), trigger_rect.min_y()),
}
}
pub async fn placement_on_trigger(
&self,
scroll_state: ScrollState,
scrollable_ref: Rc<MountedData>,
element_ref: Rc<MountedData>,
trigger_ref: Rc<MountedData>,
options: FloatingOptions,
) -> (f64, f64) {
let scrollable_rect = scrollable_ref
.get_client_rect()
.await
.unwrap_or(PixelsRect::new(
PixelsVector2D::new(0_f64, 0_f64).to_point(),
scroll_state.bounds,
));
let trigger_rect = trigger_ref
.get_client_rect()
.await
.unwrap_or(PixelsRect::new(
PixelsVector2D::new(0_f64, 0_f64).to_point(),
PixelsSize::new(1_f64, 1_f64),
));
match element_ref.get_client_rect().await {
Ok(element_rect) => {
self.calculate_placement(scrollable_rect, element_rect, trigger_rect, options)
}
Err(_) => (trigger_rect.min_x(), trigger_rect.min_y()),
}
}
fn compute_base_coords(
&self,
element: PixelsRect,
trigger: PixelsRect,
options: FloatingOptions,
) -> (f64, f64) {
let x: f64;
let y: f64;
(x, y) = if options.placement.is_vertical() {
let x = match options.placement.get_modifier() {
PlacementModifier::Center => {
trigger.min_x() + (trigger.width() / 2_f64) - (element.width() / 2_f64)
}
PlacementModifier::Start => trigger.min_x(),
PlacementModifier::End => trigger.max_x() - element.width(),
};
let y = if options.placement.is_top() {
trigger.min_y() - element.height() - options.offset
} else {
trigger.max_y() + options.offset
};
(x, y)
} else {
let x = if options.placement.is_left() {
trigger.min_x() - element.width() - options.offset
} else {
trigger.max_x() + options.offset
};
let y = match options.placement.get_modifier() {
PlacementModifier::Center => {
trigger.min_y() + (trigger.height() / 2_f64) - (element.height() / 2_f64)
}
PlacementModifier::Start => trigger.min_y(),
PlacementModifier::End => trigger.max_y() - element.height(),
};
(x, y)
};
(x, y)
}
fn apply_middleware(
&self,
initial_pos: (f64, f64),
scrollable: PixelsRect,
element: PixelsRect,
trigger: PixelsRect,
options: FloatingOptions,
) -> (f64, f64) {
let (mut x, mut y) = initial_pos;
if options.can_flip() {
if options.placement.is_vertical() {
if options.placement.is_top() && y < scrollable.min_y() {
y = trigger.max_y() + options.offset;
} else if !options.placement.is_top() && y + element.height() > scrollable.max_y() {
y = trigger.min_y() - element.height() - options.offset;
}
} else {
if options.placement.is_left() && x < scrollable.min_x() {
x = trigger.max_x() + options.offset;
} else if !options.placement.is_left() && x + element.width() > scrollable.max_x() {
x = trigger.min_x() - element.width() - options.offset;
}
}
}
if options.can_shift() {
if options.placement.is_vertical() {
let min_allowed_x = trigger.min_x() - element.width() + options.padding;
let max_allowed_x = trigger.max_x() - options.padding;
if x < scrollable.min_x() {
x = scrollable.min_x();
}
if x + element.width() > scrollable.max_x() {
x = scrollable.max_x() - element.width();
}
x = x.clamp(min_allowed_x, max_allowed_x);
} else {
let min_allowed_y = trigger.min_y() - element.height() + options.padding;
let max_allowed_y = trigger.max_y() - options.padding;
if y < scrollable.min_y() {
y = scrollable.min_y();
}
if y + element.height() > scrollable.max_y() {
y = scrollable.max_y() - element.height();
}
y = y.clamp(min_allowed_y, max_allowed_y);
}
}
(x, y)
}
pub fn calculate_placement(
&self,
scrollable: PixelsRect,
element: PixelsRect,
trigger: PixelsRect,
options: FloatingOptions,
) -> (f64, f64) {
let base_pos = self.compute_base_coords(element, trigger, options.clone());
let final_pos =
self.apply_middleware(base_pos, scrollable, element, trigger, options.clone());
tracing::debug!(
"Calculated for scrollable: {scrollable:?}, element: {element:?}, trigger: {trigger:?}, option: {options:?}"
);
final_pos
}
}