@@ -370,85 +370,164 @@ impl MonitorView { |
| 370 | 370 | } |
| 371 | 371 | |
| 372 | 372 | /// Apply snapping to a monitor after drag ends. |
| 373 | | - /// Snaps edges to nearby edges while preserving the user's intended position. |
| 373 | + /// Snaps edges to nearby edges while ensuring adjacency (no gaps). |
| 374 | 374 | fn apply_snap(&mut self, dragged_idx: usize) { |
| 375 | 375 | if self.monitors.len() < 2 { |
| 376 | 376 | return; |
| 377 | 377 | } |
| 378 | 378 | |
| 379 | 379 | let mut dragged = self.monitors[dragged_idx].scaled_rect; |
| 380 | + |
| 381 | + // Step 1: Find best edge snaps within threshold |
| 382 | + let (snapped_x, snapped_y) = self.find_edge_snaps(dragged_idx, dragged); |
| 383 | + |
| 384 | + if let Some(x) = snapped_x { |
| 385 | + dragged.x = x; |
| 386 | + } |
| 387 | + if let Some(y) = snapped_y { |
| 388 | + dragged.y = y; |
| 389 | + } |
| 390 | + |
| 391 | + // Check for overlap |
| 392 | + if self.would_overlap(dragged, dragged_idx) { |
| 393 | + return; |
| 394 | + } |
| 395 | + |
| 396 | + // Step 2: Check if result is adjacent to any monitor |
| 397 | + if self.is_adjacent_to_any(dragged, dragged_idx) { |
| 398 | + self.monitors[dragged_idx].scaled_rect = dragged; |
| 399 | + return; |
| 400 | + } |
| 401 | + |
| 402 | + // Step 3: Not adjacent - snap to nearest adjacency position |
| 403 | + // Preserve as much of the user's position as possible |
| 404 | + if let Some(adj_pos) = self.find_nearest_adjacent_position(dragged_idx, dragged) { |
| 405 | + self.monitors[dragged_idx].scaled_rect = adj_pos; |
| 406 | + } |
| 407 | + } |
| 408 | + |
| 409 | + /// Find edge snaps within threshold for each axis. |
| 410 | + fn find_edge_snaps(&self, dragged_idx: usize, dragged: Rect) -> (Option<i32>, Option<i32>) { |
| 380 | 411 | let dragged_right = dragged.x + dragged.width as i32; |
| 381 | 412 | let dragged_bottom = dragged.y + dragged.height as i32; |
| 382 | 413 | |
| 383 | | - // Find best edge snaps independently for each axis |
| 384 | | - let mut best_x_snap: Option<(i32, i32)> = None; // (new_x, distance) |
| 385 | | - let mut best_y_snap: Option<(i32, i32)> = None; // (new_y, distance) |
| 414 | + let mut best_x: Option<(i32, i32)> = None; |
| 415 | + let mut best_y: Option<(i32, i32)> = None; |
| 386 | 416 | |
| 387 | 417 | for (i, other) in self.monitors.iter().enumerate() { |
| 388 | 418 | if i == dragged_idx { |
| 389 | 419 | continue; |
| 390 | 420 | } |
| 391 | 421 | |
| 392 | | - let other_rect = other.scaled_rect; |
| 393 | | - let other_right = other_rect.x + other_rect.width as i32; |
| 394 | | - let other_bottom = other_rect.y + other_rect.height as i32; |
| 422 | + let r = other.scaled_rect; |
| 423 | + let r_right = r.x + r.width as i32; |
| 424 | + let r_bottom = r.y + r.height as i32; |
| 395 | 425 | |
| 396 | | - // X-axis edge snaps (for adjacency - touching edges) |
| 426 | + // X snaps: adjacency (left-to-right, right-to-left) and alignment (left-to-left, right-to-right) |
| 397 | 427 | let x_snaps = [ |
| 398 | | - // Dragged left edge to other right edge (place right of other) |
| 399 | | - (other_right, (dragged.x - other_right).abs()), |
| 400 | | - // Dragged right edge to other left edge (place left of other) |
| 401 | | - (other_rect.x - dragged.width as i32, (dragged_right - other_rect.x).abs()), |
| 402 | | - // Dragged left edge to other left edge (align left edges) |
| 403 | | - (other_rect.x, (dragged.x - other_rect.x).abs()), |
| 404 | | - // Dragged right edge to other right edge (align right edges) |
| 405 | | - (other_right - dragged.width as i32, (dragged_right - other_right).abs()), |
| 428 | + (r_right, (dragged.x - r_right).abs()), // left edge to right edge |
| 429 | + (r.x - dragged.width as i32, (dragged_right - r.x).abs()), // right edge to left edge |
| 430 | + (r.x, (dragged.x - r.x).abs()), // left to left |
| 431 | + (r_right - dragged.width as i32, (dragged_right - r_right).abs()), // right to right |
| 406 | 432 | ]; |
| 407 | 433 | |
| 408 | 434 | for (new_x, dist) in x_snaps { |
| 409 | | - if dist < SNAP_THRESHOLD { |
| 410 | | - if best_x_snap.map_or(true, |(_, best_dist)| dist < best_dist) { |
| 411 | | - best_x_snap = Some((new_x, dist)); |
| 412 | | - } |
| 435 | + if dist < SNAP_THRESHOLD && best_x.map_or(true, |(_, d)| dist < d) { |
| 436 | + best_x = Some((new_x, dist)); |
| 413 | 437 | } |
| 414 | 438 | } |
| 415 | 439 | |
| 416 | | - // Y-axis edge snaps |
| 440 | + // Y snaps |
| 417 | 441 | let y_snaps = [ |
| 418 | | - // Dragged top edge to other bottom edge (place below other) |
| 419 | | - (other_bottom, (dragged.y - other_bottom).abs()), |
| 420 | | - // Dragged bottom edge to other top edge (place above other) |
| 421 | | - (other_rect.y - dragged.height as i32, (dragged_bottom - other_rect.y).abs()), |
| 422 | | - // Dragged top edge to other top edge (align top edges) |
| 423 | | - (other_rect.y, (dragged.y - other_rect.y).abs()), |
| 424 | | - // Dragged bottom edge to other bottom edge (align bottom edges) |
| 425 | | - (other_bottom - dragged.height as i32, (dragged_bottom - other_bottom).abs()), |
| 442 | + (r_bottom, (dragged.y - r_bottom).abs()), // top to bottom |
| 443 | + (r.y - dragged.height as i32, (dragged_bottom - r.y).abs()), // bottom to top |
| 444 | + (r.y, (dragged.y - r.y).abs()), // top to top |
| 445 | + (r_bottom - dragged.height as i32, (dragged_bottom - r_bottom).abs()), // bottom to bottom |
| 426 | 446 | ]; |
| 427 | 447 | |
| 428 | 448 | for (new_y, dist) in y_snaps { |
| 429 | | - if dist < SNAP_THRESHOLD { |
| 430 | | - if best_y_snap.map_or(true, |(_, best_dist)| dist < best_dist) { |
| 431 | | - best_y_snap = Some((new_y, dist)); |
| 432 | | - } |
| 449 | + if dist < SNAP_THRESHOLD && best_y.map_or(true, |(_, d)| dist < d) { |
| 450 | + best_y = Some((new_y, dist)); |
| 433 | 451 | } |
| 434 | 452 | } |
| 435 | 453 | } |
| 436 | 454 | |
| 437 | | - // Apply snaps |
| 438 | | - if let Some((new_x, _)) = best_x_snap { |
| 439 | | - dragged.x = new_x; |
| 440 | | - } |
| 441 | | - if let Some((new_y, _)) = best_y_snap { |
| 442 | | - dragged.y = new_y; |
| 455 | + (best_x.map(|(x, _)| x), best_y.map(|(y, _)| y)) |
| 456 | + } |
| 457 | + |
| 458 | + /// Check if rect is adjacent to any other monitor. |
| 459 | + fn is_adjacent_to_any(&self, rect: Rect, exclude_idx: usize) -> bool { |
| 460 | + for (i, other) in self.monitors.iter().enumerate() { |
| 461 | + if i == exclude_idx { |
| 462 | + continue; |
| 463 | + } |
| 464 | + if Self::rects_adjacent(rect, other.scaled_rect) { |
| 465 | + return true; |
| 466 | + } |
| 443 | 467 | } |
| 468 | + false |
| 469 | + } |
| 444 | 470 | |
| 445 | | - // Check for overlap and resolve if needed |
| 446 | | - if self.would_overlap(dragged, dragged_idx) { |
| 447 | | - // Revert to original position if we'd overlap |
| 448 | | - return; |
| 471 | + /// Check if two rects are adjacent (touching edges with overlap on perpendicular axis). |
| 472 | + fn rects_adjacent(a: Rect, b: Rect) -> bool { |
| 473 | + let a_right = a.x + a.width as i32; |
| 474 | + let a_bottom = a.y + a.height as i32; |
| 475 | + let b_right = b.x + b.width as i32; |
| 476 | + let b_bottom = b.y + b.height as i32; |
| 477 | + |
| 478 | + // Horizontal adjacency: right edge touches left edge (or vice versa) with vertical overlap |
| 479 | + let h_adjacent = (a_right == b.x || a.x == b_right) |
| 480 | + && a.y < b_bottom && a_bottom > b.y; |
| 481 | + |
| 482 | + // Vertical adjacency: bottom edge touches top edge (or vice versa) with horizontal overlap |
| 483 | + let v_adjacent = (a_bottom == b.y || a.y == b_bottom) |
| 484 | + && a.x < b_right && a_right > b.x; |
| 485 | + |
| 486 | + h_adjacent || v_adjacent |
| 487 | + } |
| 488 | + |
| 489 | + /// Find the nearest position that makes the rect adjacent to another monitor. |
| 490 | + fn find_nearest_adjacent_position(&self, dragged_idx: usize, dragged: Rect) -> Option<Rect> { |
| 491 | + let mut best: Option<(Rect, i32)> = None; |
| 492 | + |
| 493 | + for (i, other) in self.monitors.iter().enumerate() { |
| 494 | + if i == dragged_idx { |
| 495 | + continue; |
| 496 | + } |
| 497 | + |
| 498 | + let r = other.scaled_rect; |
| 499 | + let r_right = r.x + r.width as i32; |
| 500 | + let r_bottom = r.y + r.height as i32; |
| 501 | + |
| 502 | + // Try 4 adjacent positions, preserving the perpendicular coordinate |
| 503 | + let candidates = [ |
| 504 | + // Right of other (preserve y) |
| 505 | + Rect::new(r_right, dragged.y, dragged.width, dragged.height), |
| 506 | + // Left of other (preserve y) |
| 507 | + Rect::new(r.x - dragged.width as i32, dragged.y, dragged.width, dragged.height), |
| 508 | + // Below other (preserve x) |
| 509 | + Rect::new(dragged.x, r_bottom, dragged.width, dragged.height), |
| 510 | + // Above other (preserve x) |
| 511 | + Rect::new(dragged.x, r.y - dragged.height as i32, dragged.width, dragged.height), |
| 512 | + ]; |
| 513 | + |
| 514 | + for candidate in candidates { |
| 515 | + // Must be adjacent and not overlap |
| 516 | + if !Self::rects_adjacent(candidate, r) { |
| 517 | + continue; |
| 518 | + } |
| 519 | + if self.would_overlap(candidate, dragged_idx) { |
| 520 | + continue; |
| 521 | + } |
| 522 | + |
| 523 | + let dist = (candidate.x - dragged.x).abs() + (candidate.y - dragged.y).abs(); |
| 524 | + if best.map_or(true, |(_, d)| dist < d) { |
| 525 | + best = Some((candidate, dist)); |
| 526 | + } |
| 527 | + } |
| 449 | 528 | } |
| 450 | 529 | |
| 451 | | - self.monitors[dragged_idx].scaled_rect = dragged; |
| 530 | + best.map(|(rect, _)| rect) |
| 452 | 531 | } |
| 453 | 532 | |
| 454 | 533 | /// Check if a rect overlaps with any monitor except the specified one. |