tiles: Add to support panel auto-snap while drag movement (#1552)
Before: https://github.com/user-attachments/assets/b9171a47-e21b-4132-8b13-7d9bf06f083a After: https://github.com/user-attachments/assets/e06c8353-41f8-4124-b9f4-1c44e462ab72
This commit is contained in:
parent
74f23bfca8
commit
1a5dd46acd
2 changed files with 192 additions and 60 deletions
|
|
@ -310,37 +310,42 @@ impl StoryTiles {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> DockItem {
|
) -> DockItem {
|
||||||
DockItem::tiles(
|
const PANELS: usize = 4;
|
||||||
vec![
|
let panels = (0..PANELS)
|
||||||
|
.map(|i| {
|
||||||
|
let story = if i % 2 == 0 {
|
||||||
|
Arc::new(StoryContainer::panel::<ButtonStory>(window, cx))
|
||||||
|
} else {
|
||||||
|
Arc::new(StoryContainer::panel::<IconStory>(window, cx))
|
||||||
|
};
|
||||||
DockItem::tab(
|
DockItem::tab(
|
||||||
ContainerPanel::new(
|
ContainerPanel::new(story, window, cx),
|
||||||
Arc::new(StoryContainer::panel::<ButtonStory>(window, cx)),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
),
|
|
||||||
dock_area,
|
dock_area,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
),
|
)
|
||||||
DockItem::tab(
|
})
|
||||||
ContainerPanel::new(
|
.collect::<Vec<_>>();
|
||||||
Arc::new(StoryContainer::panel::<IconStory>(window, cx)),
|
|
||||||
window,
|
// Panel size: 380x280, Gap: 20px, Starting position: (20, 20)
|
||||||
cx,
|
let panel_width = px(380.);
|
||||||
),
|
let panel_height = px(280.);
|
||||||
dock_area,
|
let gap = px(20.);
|
||||||
window,
|
let start_x = px(20.);
|
||||||
cx,
|
let start_y = px(20.);
|
||||||
),
|
let cols = 4;
|
||||||
],
|
|
||||||
vec![
|
let bounds = (0..PANELS)
|
||||||
Bounds::new(point(px(10.), px(10.)), size(px(610.), px(190.))),
|
.map(|i| {
|
||||||
Bounds::new(point(px(120.), px(10.)), size(px(650.), px(300.))),
|
let row = i / cols;
|
||||||
],
|
let col = i % cols;
|
||||||
dock_area,
|
let x = start_x + (panel_width + gap) * col as f32;
|
||||||
window,
|
let y = start_y + (panel_height + gap) * row as f32;
|
||||||
cx,
|
Bounds::new(point(x, y), size(panel_width, panel_height))
|
||||||
)
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
DockItem::tiles(panels, bounds, dock_area, window, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_local(cx: &mut App) -> Task<anyhow::Result<WindowHandle<Root>>> {
|
pub fn new_local(cx: &mut App) -> Task<anyhow::Result<WindowHandle<Root>>> {
|
||||||
|
|
|
||||||
|
|
@ -240,53 +240,174 @@ impl Tiles {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_position(
|
/// Calculate magnetic snap position for the dragging panel
|
||||||
&mut self,
|
fn calculate_magnetic_snap(
|
||||||
mouse_position: Point<Pixels>,
|
&self,
|
||||||
_: &mut Window,
|
dragging_bounds: Bounds<Pixels>,
|
||||||
cx: &mut Context<'_, Self>,
|
item_ix: usize,
|
||||||
) {
|
snap_threshold: Pixels,
|
||||||
|
) -> (Option<Pixels>, Option<Pixels>) {
|
||||||
|
// Only check nearby panels
|
||||||
|
let search_bounds = Bounds {
|
||||||
|
origin: Point {
|
||||||
|
x: dragging_bounds.left() - snap_threshold,
|
||||||
|
y: dragging_bounds.top() - snap_threshold,
|
||||||
|
},
|
||||||
|
size: Size {
|
||||||
|
width: dragging_bounds.size.width + snap_threshold * 2.0,
|
||||||
|
height: dragging_bounds.size.height + snap_threshold * 2.0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut snap_x: Option<Pixels> = None;
|
||||||
|
let mut snap_y: Option<Pixels> = None;
|
||||||
|
let mut min_x_dist = snap_threshold;
|
||||||
|
let mut min_y_dist = snap_threshold;
|
||||||
|
|
||||||
|
// Pre-calculate dragging bounds edges to avoid repeated method calls
|
||||||
|
let drag_left = dragging_bounds.left();
|
||||||
|
let drag_right = dragging_bounds.right();
|
||||||
|
let drag_top = dragging_bounds.top();
|
||||||
|
let drag_bottom = dragging_bounds.bottom();
|
||||||
|
let drag_width = dragging_bounds.size.width;
|
||||||
|
let drag_height = dragging_bounds.size.height;
|
||||||
|
|
||||||
|
for (ix, other) in self.panels.iter().enumerate() {
|
||||||
|
if ix == item_ix {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-calculate other bounds edges
|
||||||
|
let other_left = other.bounds.left();
|
||||||
|
let other_right = other.bounds.right();
|
||||||
|
let other_top = other.bounds.top();
|
||||||
|
let other_bottom = other.bounds.bottom();
|
||||||
|
|
||||||
|
// Skip panels that are far away
|
||||||
|
if other_right < search_bounds.left()
|
||||||
|
|| other_left > search_bounds.right()
|
||||||
|
|| other_bottom < search_bounds.top()
|
||||||
|
|| other_top > search_bounds.bottom()
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal snapping (X axis) - find closest snap point
|
||||||
|
if snap_x.is_none() {
|
||||||
|
let candidates = [
|
||||||
|
((drag_left - other_left).abs(), other_left),
|
||||||
|
((drag_left - other_right).abs(), other_right),
|
||||||
|
((drag_right - other_left).abs(), other_left - drag_width),
|
||||||
|
((drag_right - other_right).abs(), other_right - drag_width),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (dist, snap_pos) in candidates {
|
||||||
|
if dist < min_x_dist {
|
||||||
|
min_x_dist = dist;
|
||||||
|
snap_x = Some(snap_pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertical snapping (Y axis) - find closest snap point
|
||||||
|
if snap_y.is_none() {
|
||||||
|
let candidates = [
|
||||||
|
((drag_top - other_top).abs(), other_top),
|
||||||
|
((drag_top - other_bottom).abs(), other_bottom),
|
||||||
|
((drag_bottom - other_top).abs(), other_top - drag_height),
|
||||||
|
(
|
||||||
|
(drag_bottom - other_bottom).abs(),
|
||||||
|
other_bottom - drag_height,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (dist, snap_pos) in candidates {
|
||||||
|
if dist < min_y_dist {
|
||||||
|
min_y_dist = dist;
|
||||||
|
snap_y = Some(snap_pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Early exit if both axes are snapped
|
||||||
|
if snap_x.is_some() && snap_y.is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(snap_x, snap_y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply boundary constraints to the panel origin
|
||||||
|
fn apply_boundary_constraints(&self, mut origin: Point<Pixels>) -> Point<Pixels> {
|
||||||
|
// Top boundary
|
||||||
|
if origin.y < px(0.) {
|
||||||
|
origin.y = px(0.);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left boundary (allow partial off-screen but keep 64px visible)
|
||||||
|
let min_left = -self.dragging_initial_bounds.size.width + px(64.);
|
||||||
|
if origin.x < min_left {
|
||||||
|
origin.x = min_left;
|
||||||
|
}
|
||||||
|
|
||||||
|
origin
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_position(&mut self, mouse_position: Point<Pixels>, cx: &mut Context<Self>) {
|
||||||
let Some(dragging_id) = self.dragging_id else {
|
let Some(dragging_id) = self.dragging_id else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(item) = self.panels.iter_mut().find(|p| p.id == dragging_id) else {
|
let Some(item_ix) = self.panels.iter().position(|p| p.id == dragging_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let previous_bounds = item.bounds;
|
let previous_bounds = self.panels[item_ix].bounds;
|
||||||
let adjusted_position = mouse_position - self.bounds.origin;
|
let adjusted_position = mouse_position - self.bounds.origin;
|
||||||
let delta = adjusted_position - self.dragging_initial_mouse;
|
let delta = adjusted_position - self.dragging_initial_mouse;
|
||||||
let mut new_origin = self.dragging_initial_bounds.origin + delta;
|
let mut new_origin = self.dragging_initial_bounds.origin + delta;
|
||||||
|
|
||||||
// Avoid out of bounds
|
// Apply magnetic snap before boundary checks
|
||||||
if new_origin.y < px(0.) {
|
let snap_threshold = cx.theme().tile_grid_size;
|
||||||
new_origin.y = px(0.);
|
let dragging_bounds = Bounds {
|
||||||
|
origin: new_origin,
|
||||||
|
size: self.dragging_initial_bounds.size,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (snap_x, snap_y) =
|
||||||
|
self.calculate_magnetic_snap(dragging_bounds, item_ix, snap_threshold);
|
||||||
|
|
||||||
|
// Apply snapping
|
||||||
|
if let Some(x) = snap_x {
|
||||||
|
new_origin.x = x;
|
||||||
}
|
}
|
||||||
let min_left = -self.dragging_initial_bounds.size.width + px(64.);
|
if let Some(y) = snap_y {
|
||||||
if new_origin.x < min_left {
|
new_origin.y = y;
|
||||||
new_origin.x = min_left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let final_origin = round_point_to_nearest_ten(new_origin, cx);
|
// Apply boundary constraints after snapping
|
||||||
// Only push to history if bounds have changed
|
new_origin = self.apply_boundary_constraints(new_origin);
|
||||||
if final_origin != previous_bounds.origin {
|
|
||||||
item.bounds.origin = final_origin;
|
// Update position without grid rounding (smooth dragging)
|
||||||
|
if new_origin != previous_bounds.origin {
|
||||||
|
self.panels[item_ix].bounds.origin = new_origin;
|
||||||
|
let item = &self.panels[item_ix];
|
||||||
|
let bounds = item.bounds;
|
||||||
|
let entity_id = item.panel.view().entity_id();
|
||||||
|
|
||||||
// Only push if not during history operations
|
|
||||||
if !self.history.ignore {
|
if !self.history.ignore {
|
||||||
self.history.push(TileChange {
|
self.history.push(TileChange {
|
||||||
tile_id: item.panel.view().entity_id(),
|
tile_id: entity_id,
|
||||||
old_bounds: Some(previous_bounds),
|
old_bounds: Some(previous_bounds),
|
||||||
new_bounds: Some(item.bounds),
|
new_bounds: Some(bounds),
|
||||||
old_order: None,
|
old_order: None,
|
||||||
new_order: None,
|
new_order: None,
|
||||||
version: 0,
|
version: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resize(
|
fn resize(
|
||||||
|
|
@ -868,16 +989,16 @@ impl Tiles {
|
||||||
cx.new(|_| drag.clone())
|
cx.new(|_| drag.clone())
|
||||||
})
|
})
|
||||||
.on_drag_move(
|
.on_drag_move(
|
||||||
cx.listener(move |this, e: &DragMoveEvent<DragMoving>, window, cx| {
|
cx.listener(
|
||||||
match e.drag(cx) {
|
move |this, e: &DragMoveEvent<DragMoving>, _, cx| match e.drag(cx) {
|
||||||
DragMoving(id) => {
|
DragMoving(id) => {
|
||||||
if *id != entity_id {
|
if *id != entity_id {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.update_position(e.event.position, window, cx);
|
this.update_position(e.event.position, cx);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}),
|
),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
@ -936,16 +1057,22 @@ impl Tiles {
|
||||||
|
|
||||||
// Handle dragging
|
// Handle dragging
|
||||||
if let Some(dragging_id) = self.dragging_id {
|
if let Some(dragging_id) = self.dragging_id {
|
||||||
if let Some(item) = self.panel(&dragging_id) {
|
if let Some(idx) = self.panels.iter().position(|p| p.id == dragging_id) {
|
||||||
let initial_bounds = self.dragging_initial_bounds;
|
let initial_bounds = self.dragging_initial_bounds;
|
||||||
let current_bounds = item.bounds;
|
let current_bounds = self.panels[idx].bounds;
|
||||||
if initial_bounds.origin != current_bounds.origin
|
|
||||||
|
// Apply grid alignment to final position
|
||||||
|
let aligned_origin = round_point_to_nearest_ten(current_bounds.origin, cx);
|
||||||
|
|
||||||
|
if initial_bounds.origin != aligned_origin
|
||||||
|| initial_bounds.size != current_bounds.size
|
|| initial_bounds.size != current_bounds.size
|
||||||
{
|
{
|
||||||
|
self.panels[idx].bounds.origin = aligned_origin;
|
||||||
|
|
||||||
changes_to_push.push(TileChange {
|
changes_to_push.push(TileChange {
|
||||||
tile_id: item.panel.view().entity_id(),
|
tile_id: self.panels[idx].panel.view().entity_id(),
|
||||||
old_bounds: Some(initial_bounds),
|
old_bounds: Some(initial_bounds),
|
||||||
new_bounds: Some(current_bounds),
|
new_bounds: Some(self.panels[idx].bounds),
|
||||||
old_order: None,
|
old_order: None,
|
||||||
new_order: None,
|
new_order: None,
|
||||||
version: 0,
|
version: 0,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue