::new();
@@ -87,24 +91,19 @@ pub fn GridItem(
col_span,
current_col_span: Arc::new(move || grid_item_data.get_untracked().span.col_span),
on_drag_move: Arc::new(move |drag_px_pos| {
+ let layout = layout.get_untracked();
+ let item = grid_item_data.get_untracked();
+ drop_preview.set(Some(DropPreview::from_drag(&item, drag_px_pos, &layout)));
+
grid_item_data.update(|item| {
item.px_pos = drag_px_pos;
log!("Update item with new px_pos: {drag_px_pos:?}");
})
}),
- on_drag_end: Arc::new(move |col_start, row_start, snapped_px_pos| {
- grid_item_data.update(|item| {
- item.px_pos = snapped_px_pos;
- item.grid_pos = GridPosition {
- col_start,
- row_start,
- };
- });
-
- // Move item with collision detection and push other items down
+ on_drag_end: Arc::new(move |col_start, row_start, _snapped_px_pos, drag_px_pos| {
+ drop_preview.set(None);
layout.update(|layout| {
- // TODO: drag & detect collisions
- // layout.move_item_with_collision(grid_item_data, new_row, new_col);
+ layout.move_item_with_collision(grid_item_data, row_start, col_start, drag_px_pos);
});
}),
..Default::default()
@@ -118,16 +117,10 @@ pub fn GridItem(
// TODO: see to remove this from the UseDraggableGridItemReturn? Or keep for API if open sourced?
// position: drag_position,
transition: drag_transition,
+ is_dragging,
..
} = use_draggable_grid_item(grid_item_ref, draggable_options);
- // Absolute element width/height
- let UseElementBoundingReturn {
- width: item_width,
- height: item_height,
- ..
- } = use_element_bounding(grid_item_ref);
-
// Grid item resize
let resize_options = UseResizableGridItemOptions {
handle: Some(resize_button_ref),
@@ -137,18 +130,22 @@ pub fn GridItem(
current_col_span: Arc::new(move || grid_item_data.get_untracked().span.col_span),
current_row_span: Arc::new(move || grid_item_data.get_untracked().span.row_span),
on_resize_move: Arc::new(move |size| {
+ let layout = layout.get_untracked();
+ let item = grid_item_data.get_untracked();
+ resize_preview.set(Some(ResizePreview::from_resize(&item, size, &layout)));
+
grid_item_data.update(|item| {
item.size = size;
});
}),
on_resize_end: Arc::new(move |size| {
- let cell_size = layout.get_untracked().cell_size;
- let col_span = ((size.width / cell_size.width).round() as usize).max(1);
- let row_span = ((size.height / cell_size.height).round() as usize).max(1);
+ resize_preview.set(None);
+ layout.update(|layout| {
+ let cell_size = layout.cell_size;
+ let col_span = (size.width / cell_size.width).round() as usize;
+ let row_span = (size.height / cell_size.height).round() as usize;
- grid_item_data.update(|item| {
- item.size = size;
- item.span = Span { row_span, col_span };
+ layout.resize_item_with_collision(grid_item_data, col_span, row_span);
});
}),
..Default::default()
@@ -159,39 +156,6 @@ pub fn GridItem(
transition: resize_transition,
} = use_resizable_grid_item(grid_item_ref, resize_options);
- // TODO: Handle collisions
-
- // TODO: clamp dragging event.
- // Avoid issues where min > max.
- // let left = move || {
- // // let x = metadata.get().position.col_start * layout.get().cell_size.width as u32;
- // match drag_state.get() {
- // DragState::Dragging(p) | DragState::DragEnded(p) => p.x,
- // }
- // // let grid_w = layout.get().size.width;
- // // let max = if grid_w <= 0. {
- // // 0.
- // // } else {
- // // grid_w - item_width.get()
- // // };
-
- // // x.clamp(0., max.round())
- // };
- // let top = move || {
- // // let y = metadata.get().position.row_start * layout.get().cell_size.height as u32;
- // match drag_state.get() {
- // DragState::Dragging(p) | DragState::DragEnded(p) => p.y,
- // }
- // // let grid_h = layout.get().size.height;
- // // let max = if grid_h <= 0. {
- // // 0.
- // // } else {
- // // grid_h - item_height.get()
- // // };
-
- // // y.clamp(0.0, max.round())
- // };
-
let style = move || {
// let Size { width, height } = metadata.get().size;
let drag_transition = drag_transition.get();
@@ -203,6 +167,7 @@ pub fn GridItem(
let visual_height = (height - GRID_ITEM_GAP_PX).max(0.0);
let visual_left = left + GRID_ITEM_INSET_PX;
let visual_top = top + GRID_ITEM_INSET_PX;
+ let z_index = if is_dragging.get() { 1000 } else { 1 };
log!("resize: {width};{height}");
@@ -212,7 +177,8 @@ pub fn GridItem(
transition: {resize_transition}, {drag_transition};
touch-action: none;
left: {visual_left}px;
- top: {visual_top}px;"#
+ top: {visual_top}px;
+ z-index: {z_index};"#
)
};
diff --git a/crates/ui/src/components/grid/grid_layout.rs b/crates/ui/src/components/grid/grid_layout.rs
index 1f55503..edbcd27 100644
--- a/crates/ui/src/components/grid/grid_layout.rs
+++ b/crates/ui/src/components/grid/grid_layout.rs
@@ -1,10 +1,13 @@
+use crate::components::grid::core::drop_preview::DropPreview;
use crate::components::grid::core::layout::LayoutBuilder;
+use crate::components::grid::core::resize_preview::ResizePreview;
use crate::components::grid::core::size::Size;
-use crate::components::grid::grid_item::GridItem;
+use crate::components::grid::grid_item::{GridItem, GRID_ITEM_GAP_PX, GRID_ITEM_INSET_PX};
use crate::components::page_layout::PageLayout;
use leptos::{html::Div, logging::log, prelude::*};
use leptos_use::{
- use_element_bounding_with_options, UseElementBoundingOptions, UseElementBoundingReturn,
+ core::Position, use_element_bounding_with_options, UseElementBoundingOptions,
+ UseElementBoundingReturn,
};
#[component]
@@ -21,11 +24,15 @@ pub fn GridLayout(children: Children, columns: usize, display_grid: bool) -> imp
.cell_size(100., 100.)
.build(),
);
+ let drop_preview = RwSignal::new(None::
);
+ let resize_preview = RwSignal::new(None::);
provide_context(layout);
+ provide_context(drop_preview);
+ provide_context(resize_preview);
// Track dynamically added items
- let next_id = RwSignal::new(1u32);
+ let next_id = RwSignal::new(10_000u32);
let grid_items = RwSignal::new(Vec::::new());
// Handler to add new items
@@ -114,7 +121,53 @@ pub fn GridLayout(children: Children, columns: usize, display_grid: bool) -> imp
}
}
}
- // {children()}
+ {children()}
+ {
+ move || {
+ drop_preview.get().map(|preview| {
+ let layout = layout.get();
+ let (Position { x: left, y: top }, Size { width, height }) =
+ preview.pixel_rect(layout.cell_size);
+ let visual_left = left + GRID_ITEM_INSET_PX;
+ let visual_top = top + GRID_ITEM_INSET_PX;
+ let visual_width = (width - GRID_ITEM_GAP_PX).max(0.0);
+ let visual_height = (height - GRID_ITEM_GAP_PX).max(0.0);
+
+ view! {
+
+ }
+ })
+ }
+ }
+ {
+ move || {
+ resize_preview.get().map(|preview| {
+ let layout = layout.get();
+ let (Position { x: left, y: top }, Size { width, height }) =
+ preview.pixel_rect(layout.cell_size);
+ let visual_left = left + GRID_ITEM_INSET_PX;
+ let visual_top = top + GRID_ITEM_INSET_PX;
+ let visual_width = (width - GRID_ITEM_GAP_PX).max(0.0);
+ let visual_height = (height - GRID_ITEM_GAP_PX).max(0.0);
+
+ view! {
+
+ }
+ })
+ }
+ }
imp
id=id
col_span=3
row_span=3
- // FIXME: this is just for debugging
col_start=0
row_start=0
label=format!("Item {}", id)
diff --git a/crates/ui/src/components/grid/utils/draggable_item.rs b/crates/ui/src/components/grid/utils/draggable_item.rs
index d57fb15..470aca4 100644
--- a/crates/ui/src/components/grid/utils/draggable_item.rs
+++ b/crates/ui/src/components/grid/utils/draggable_item.rs
@@ -2,10 +2,22 @@ use leptos::html::Div;
use leptos::prelude::*;
use leptos_use::{core::Position, use_draggable_with_options, UseDraggableOptions};
use std::sync::Arc;
+use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast;
use crate::components::grid::core::{layout::Layout, size::Size};
+// Keep this slightly above the drag transition duration so the released item
+// stays above its neighbors until the snap-back animation has fully finished.
+const DRAG_RELEASE_ELEVATION_MS: i32 = 280;
+
+// Clicks on the drag handle can still emit a draggable end event. Requiring a
+// small pointer delta keeps simple clicks from snapping the item with a stale
+// internal draggable position.
+const DRAG_ACTIVATION_THRESHOLD_PX: f64 = 4.0;
+const DRAG_ACTIVATION_THRESHOLD_SQUARED: f64 =
+ DRAG_ACTIVATION_THRESHOLD_PX * DRAG_ACTIVATION_THRESHOLD_PX;
+
#[derive(Clone, Copy, Debug)]
pub enum DragState {
Dragging(Position),
@@ -28,7 +40,7 @@ pub struct UseDraggableGridItemOptions {
/// Callback during dragging
pub on_drag_move: Arc,
/// Callback when drag ends with final grid position
- pub on_drag_end: Arc,
+ pub on_drag_end: Arc,
}
impl Default for UseDraggableGridItemOptions {
@@ -41,7 +53,7 @@ impl Default for UseDraggableGridItemOptions {
current_col_span: Arc::new(|| 1),
on_drag_start: Arc::new(|_| {}),
on_drag_move: Arc::new(|_| {}),
- on_drag_end: Arc::new(|_, _, _| {}),
+ on_drag_end: Arc::new(|_, _, _, _| {}),
}
}
}
@@ -52,6 +64,8 @@ pub struct UseDraggableGridItemReturn {
pub position: Signal,
/// CSS transition string for drag animations
pub transition: Signal<&'static str>,
+ /// Whether the item is actively being dragged
+ pub is_dragging: Signal,
}
pub fn use_draggable_grid_item(
@@ -60,6 +74,10 @@ pub fn use_draggable_grid_item(
) -> UseDraggableGridItemReturn {
let layout = use_context::>().expect("Layout context must be provided");
let drag_state = RwSignal::new(DragState::Dragging(Position::default()));
+ let is_dragging = RwSignal::new(false);
+ let drag_elevation_epoch = RwSignal::new(0_u32);
+ let drag_pointer_start = RwSignal::new(None::<(i32, i32)>);
+ let drag_threshold_reached = RwSignal::new(false);
let UseDraggableGridItemOptions {
handle,
col_start,
@@ -102,7 +120,31 @@ pub fn use_draggable_grid_item(
};
Position { x: pos.x, y: pos.y }
})
+ .on_start(move |drag_event| {
+ drag_pointer_start.set(Some((
+ drag_event.event.client_x(),
+ drag_event.event.client_y(),
+ )));
+ drag_threshold_reached.set(false);
+ true
+ })
.on_move(move |drag_event| {
+ if !drag_threshold_reached.get_untracked() {
+ let Some((start_x, start_y)) = drag_pointer_start.get_untracked() else {
+ return;
+ };
+ let delta_x = (drag_event.event.client_x() - start_x) as f64;
+ let delta_y = (drag_event.event.client_y() - start_y) as f64;
+
+ if delta_x * delta_x + delta_y * delta_y < DRAG_ACTIVATION_THRESHOLD_SQUARED {
+ return;
+ }
+
+ drag_threshold_reached.set(true);
+ }
+
+ drag_elevation_epoch.update(|epoch| *epoch = epoch.wrapping_add(1));
+
let layout = layout.get_untracked();
let max_col_start = layout.columns.saturating_sub(current_col_span_for_move());
let max_x = max_col_start as f64 * layout.cell_size.width;
@@ -111,10 +153,23 @@ pub fn use_draggable_grid_item(
y: drag_event.position.y.max(0.0),
};
+ is_dragging.set(true);
drag_state.set(DragState::Dragging(clamped_position));
on_drag_move(clamped_position);
})
.on_end(move |drag_event| {
+ let drag_was_activated = drag_threshold_reached.get_untracked();
+ drag_pointer_start.set(None);
+ drag_threshold_reached.set(false);
+
+ if !drag_was_activated {
+ is_dragging.set(false);
+ return;
+ }
+
+ drag_elevation_epoch.update(|epoch| *epoch = epoch.wrapping_add(1));
+ let drag_end_epoch = drag_elevation_epoch.get_untracked();
+
let layout = layout.get_untracked();
let cell_size = layout.cell_size;
let max_col_start = layout.columns.saturating_sub(current_col_span_for_end());
@@ -138,7 +193,22 @@ pub fn use_draggable_grid_item(
drag_state.set(DragState::DragEnded(final_position));
- on_drag_end(col_start, row_start, final_position);
+ on_drag_end(col_start, row_start, final_position, drag_position);
+
+ // Release drag elevation after the CSS transition, not on pointerup.
+ // Otherwise a snapping item can briefly pass behind another panel
+ // during the return animation and make the interaction look broken.
+ let release_drag_elevation = Closure::wrap(Box::new(move || {
+ if drag_elevation_epoch.get_untracked() == drag_end_epoch {
+ is_dragging.set(false);
+ }
+ }) as Box);
+
+ let _ = window().set_timeout_with_callback_and_timeout_and_arguments_0(
+ release_drag_elevation.as_ref().unchecked_ref(),
+ DRAG_RELEASE_ELEVATION_MS,
+ );
+ release_drag_elevation.forget();
})
.target_offset(move |event_target: web_sys::EventTarget| {
let target: web_sys::HtmlElement = event_target.unchecked_into();
@@ -161,5 +231,6 @@ pub fn use_draggable_grid_item(
UseDraggableGridItemReturn {
transition,
position,
+ is_dragging: is_dragging.into(),
}
}
diff --git a/crates/ui/src/components/grid/utils/resizable_item.rs b/crates/ui/src/components/grid/utils/resizable_item.rs
index 6abbafe..54195f0 100644
--- a/crates/ui/src/components/grid/utils/resizable_item.rs
+++ b/crates/ui/src/components/grid/utils/resizable_item.rs
@@ -5,6 +5,7 @@ use std::default::Default;
use std::sync::Arc;
use crate::components::grid::core::layout::Layout;
+use crate::components::grid::core::resize_preview::directional_snap_span;
use crate::components::grid::core::size::Size;
fn clamp_size_to_grid(layout: &Layout, col_start: usize, size: Size) -> Size {
@@ -128,6 +129,8 @@ pub fn use_resizable_grid_item(
let current_col_start_for_size = Arc::clone(¤t_col_start);
let current_col_span_for_layout = Arc::clone(¤t_col_span);
let current_row_span_for_layout = Arc::clone(¤t_row_span);
+ let current_col_span_for_resize = Arc::clone(¤t_col_span);
+ let current_row_span_for_resize = Arc::clone(¤t_row_span);
let _resize_starts = use_event_listener(handle, leptos::ev::pointerdown, move |evt| {
evt.prevent_default();
@@ -146,6 +149,8 @@ pub fn use_resizable_grid_item(
let on_resize_end = Arc::clone(&on_resize_end);
let current_col_start_for_move = Arc::clone(¤t_col_start_for_resize);
let current_col_start_for_end = Arc::clone(¤t_col_start_for_resize);
+ let current_col_span_for_end = Arc::clone(¤t_col_span_for_resize);
+ let current_row_span_for_end = Arc::clone(¤t_row_span_for_resize);
let _resize_in_progress =
use_event_listener(window(), leptos::ev::pointermove, move |evt| {
@@ -197,15 +202,9 @@ pub fn use_resizable_grid_item(
let total_offset_x = last_client_pos.0 - start_pos.0;
let total_offset_y = last_client_pos.1 - start_pos.1;
- // Grid-snapping when resizing ends.
- //
- // If the last mouse position x is 253, and the resize started at 100px, then we get a movement
- // of 153px. To stick the movement to the grid we need to know if we reached the middle of the
- // last cell in which case we fill it, otherwise, we go back to the previous cell.
- //
- // Here the calcul for a grid cell width of 100px is: (153 / 100).round() -> 1.53.round() -> 2
-
- // Calculate the raw new size (before snapping)
+ // Calculate the raw new size first, then snap in the resize
+ // direction so the final size matches the preview shown during
+ // pointer movement.
let raw_size = clamp_size_to_grid(
&layout,
current_col_start_for_end(),
@@ -215,16 +214,23 @@ pub fn use_resizable_grid_item(
},
);
- let snapped_width = (raw_size.width / cell_size.width).round() * cell_size.width;
- let snapped_height =
- (raw_size.height / cell_size.height).round() * cell_size.height;
+ let snapped_col_span = directional_snap_span(
+ raw_size.width,
+ current_col_span_for_end(),
+ cell_size.width,
+ );
+ let snapped_row_span = directional_snap_span(
+ raw_size.height,
+ current_row_span_for_end(),
+ cell_size.height,
+ );
let snapped_size = clamp_size_to_grid(
&layout,
current_col_start_for_end(),
Size {
- width: snapped_width,
- height: snapped_height,
+ width: snapped_col_span as f64 * cell_size.width,
+ height: snapped_row_span as f64 * cell_size.height,
},
);