summaryrefslogtreecommitdiff
path: root/ui/windows
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-06-03 16:35:15 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-06-03 16:35:15 -0600
commitf60de21eeaa50f847108a9c103934cc8da7352fb (patch)
treed1fa7ed98529ae86a4445c094c0a01148e13f9eb /ui/windows
parentf007593abc9a666df85120e08c7f1d327d9215da (diff)
Three-zone drop system: squeeze, split, center
Zone layout (outer to inner per edge): 0–1/6 squeeze (amber): insert dropped tile between two existing tiles 1/6–1/3 split (blue): bisect the target tile 50/50 1/3–2/3 center (green): swap/move Squeeze behaviour: find the tile adjacent to the target in the drop direction; insert dragged tile to the opposite side of that neighbor so it lands visually between them. Falls back to split when no neighbor exists (edge tile). - _zone_at: returns "squeeze_*"/"split_*"/"center" with dominant-axis selection (nearest edge wins) - _adjacent_tile: finds geometrically adjacent tile by pixel proximity - _do_drop: routes squeeze→insert-at-neighbor, split→bisect, center→swap - paintEvent: amber/blue/green per zone type Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'ui/windows')
-rw-r--r--ui/windows/plot_window.py151
1 files changed, 128 insertions, 23 deletions
diff --git a/ui/windows/plot_window.py b/ui/windows/plot_window.py
index 615b7ca..15d509b 100644
--- a/ui/windows/plot_window.py
+++ b/ui/windows/plot_window.py
@@ -460,14 +460,25 @@ _LC_HEIGHT = 160 # fixed canvas height
def _zone_overlay_rect(r: QRect, zone: str) -> QRect:
- if zone == "left":
+ # split_* → highlight half of tile
+ if zone == "split_left":
return QRect(r.x(), r.y(), r.width() // 2, r.height())
- if zone == "right":
+ if zone == "split_right":
return QRect(r.x() + r.width() // 2, r.y(), r.width() - r.width() // 2, r.height())
- if zone == "top":
+ if zone == "split_top":
return QRect(r.x(), r.y(), r.width(), r.height() // 2)
- if zone == "bottom":
+ if zone == "split_bottom":
return QRect(r.x(), r.y() + r.height() // 2, r.width(), r.height() - r.height() // 2)
+ # squeeze_* → thin bar at the edge (insert-between indicator)
+ BAR = 10
+ if zone == "squeeze_left":
+ return QRect(r.x(), r.y(), BAR, r.height())
+ if zone == "squeeze_right":
+ return QRect(r.right() - BAR, r.y(), BAR, r.height())
+ if zone == "squeeze_top":
+ return QRect(r.x(), r.y(), r.width(), BAR)
+ if zone == "squeeze_bottom":
+ return QRect(r.x(), r.bottom() - BAR, r.width(), BAR)
# center (move)
return r
@@ -586,28 +597,81 @@ class LayoutCanvas(QFrame):
# ── Drop zone detection ───────────────────────────────────────────────────
def _zone_at(self, pos: QPoint) -> Optional[Tuple[int, str]]:
+ """
+ Returns (pane_idx, zone) where zone is one of:
+ squeeze_left/right/top/bottom — outer 1/6, insert between tiles
+ split_left/right/top/bottom — 1/6 to 1/3, bisect tile
+ center — inner 1/3, swap/move
+ """
dragging_idx = self._drag_tile.pane_index if self._drag_tile else -1
for pane_idx, rect in self._tile_rects.items():
if pane_idx == dragging_idx:
continue
if not rect.contains(pos):
continue
- rel_x = pos.x() - rect.x()
- rel_y = pos.y() - rect.y()
+ rx = pos.x() - rect.x()
+ ry = pos.y() - rect.y()
w, h = rect.width(), rect.height()
- zone_w = max(1, w // 3)
- zone_h = max(1, h // 3)
- if rel_x < zone_w:
- return (pane_idx, "left")
- if rel_x > w - zone_w:
- return (pane_idx, "right")
- if rel_y < zone_h:
- return (pane_idx, "top")
- if rel_y > h - zone_h:
- return (pane_idx, "bottom")
- return (pane_idx, "center") # center — move/swap
+ sq_w = max(1, w // 6)
+ sp_w = max(1, w // 3)
+ sq_h = max(1, h // 6)
+ sp_h = max(1, h // 3)
+
+ # X-axis zone
+ if rx < sq_w: x_zone = "squeeze_left"
+ elif rx < sp_w: x_zone = "split_left"
+ elif rx > w - sq_w: x_zone = "squeeze_right"
+ elif rx > w - sp_w: x_zone = "split_right"
+ else: x_zone = "center"
+
+ # Y-axis zone
+ if ry < sq_h: y_zone = "squeeze_top"
+ elif ry < sp_h: y_zone = "split_top"
+ elif ry > h - sq_h: y_zone = "squeeze_bottom"
+ elif ry > h - sp_h: y_zone = "split_bottom"
+ else: y_zone = "center"
+
+ # Pick dominant axis (nearest edge wins; center loses to any edge)
+ if x_zone == "center" and y_zone == "center":
+ return (pane_idx, "center")
+ if x_zone == "center":
+ return (pane_idx, y_zone)
+ if y_zone == "center":
+ return (pane_idx, x_zone)
+ # Both non-center: whichever is closer to the edge
+ dx = min(rx, w - rx)
+ dy = min(ry, h - ry)
+ return (pane_idx, x_zone if dx <= dy else y_zone)
return None
+ def _adjacent_tile(self, pane_idx: int, direction: str) -> Optional[int]:
+ """Find the tile immediately adjacent to pane_idx in the given direction."""
+ r = self._tile_rects.get(pane_idx)
+ if r is None:
+ return None
+ dragging_idx = self._drag_tile.pane_index if self._drag_tile else -1
+ tol = _LC_GAP + 3
+ best: Optional[int] = None
+ best_dist = float("inf")
+ for other_idx, other_r in self._tile_rects.items():
+ if other_idx in (pane_idx, dragging_idx):
+ continue
+ if direction == "left":
+ dist = r.left() - other_r.right()
+ overlap = min(r.bottom(), other_r.bottom()) - max(r.top(), other_r.top())
+ elif direction == "right":
+ dist = other_r.left() - r.right()
+ overlap = min(r.bottom(), other_r.bottom()) - max(r.top(), other_r.top())
+ elif direction == "top":
+ dist = r.top() - other_r.bottom()
+ overlap = min(r.right(), other_r.right()) - max(r.left(), other_r.left())
+ else: # bottom
+ dist = other_r.top() - r.bottom()
+ overlap = min(r.right(), other_r.right()) - max(r.left(), other_r.left())
+ if 0 <= dist <= tol and overlap > 4 and dist < best_dist:
+ best, best_dist = other_idx, dist
+ return best
+
# ── Drag ─────────────────────────────────────────────────────────────────
def mousePressEvent(self, event) -> None:
@@ -651,14 +715,52 @@ class LayoutCanvas(QFrame):
if dragged_idx == target_idx or self._tree is None:
self._reposition_tiles()
return
+
if zone == "center":
new_tree = _tree_swap(self._tree, dragged_idx, target_idx)
- else:
- new_tree = _tree_remove(self._tree, dragged_idx)
- if new_tree is None:
+ self._tree = new_tree
+ if self._cfg_ref is not None:
+ self._cfg_ref.tree = new_tree
+ self._reposition_tiles()
+ self.arrangement_changed.emit()
+ return
+
+ # Determine insert direction from zone name
+ # split_* → bisect target tile
+ # squeeze_* → insert next to adjacent tile (fall back to split if no neighbor)
+ _OPPOSITE = {"left": "right", "right": "left", "top": "bottom", "bottom": "top"}
+ _SPLIT_DIR = {"split_left": "left", "split_right": "right",
+ "split_top": "top", "split_bottom": "bottom",
+ "squeeze_left": "left","squeeze_right": "right",
+ "squeeze_top": "top", "squeeze_bottom": "bottom"}
+
+ direction = _SPLIT_DIR.get(zone, "left")
+
+ if zone.startswith("squeeze_"):
+ neighbor = self._adjacent_tile(target_idx, direction)
+ if neighbor is not None:
+ # Insert dragged tile to the opposite side of the neighbor
+ # so it lands between neighbor and target
+ new_tree = _tree_remove(self._tree, dragged_idx)
+ if new_tree is None:
+ self._reposition_tiles(); return
+ new_tree = _tree_equalize_ratios(
+ _tree_insert(new_tree, neighbor, dragged_idx, _OPPOSITE[direction])
+ )
+ self._tree = new_tree
+ if self._cfg_ref is not None:
+ self._cfg_ref.tree = new_tree
self._reposition_tiles()
+ self.arrangement_changed.emit()
return
- new_tree = _tree_equalize_ratios(_tree_insert(new_tree, target_idx, dragged_idx, zone))
+ # No neighbor — fall through to split behaviour
+
+ # split (or squeeze with no neighbor)
+ new_tree = _tree_remove(self._tree, dragged_idx)
+ if new_tree is None:
+ self._reposition_tiles()
+ return
+ new_tree = _tree_equalize_ratios(_tree_insert(new_tree, target_idx, dragged_idx, direction))
self._tree = new_tree
if self._cfg_ref is not None:
self._cfg_ref.tree = new_tree
@@ -681,7 +783,10 @@ class LayoutCanvas(QFrame):
if zone == "center":
painter.setPen(QPen(QColor("#22c55e"), 2))
painter.setBrush(QColor(34, 197, 94, 60))
- else:
+ elif zone.startswith("squeeze_"):
+ painter.setPen(QPen(QColor("#f59e0b"), 2))
+ painter.setBrush(QColor(245, 158, 11, 80))
+ else: # split_*
painter.setPen(QPen(QColor("#3b82f6"), 2))
painter.setBrush(QColor(59, 130, 246, 70))
painter.drawRoundedRect(overlay, 4, 4)
@@ -765,7 +870,7 @@ class PlotWindow(QWidget):
canvas_bar = QWidget(); canvas_bar.setObjectName("layoutCanvasBar")
cb_lay = QVBoxLayout(canvas_bar)
cb_lay.setContentsMargins(10, 6, 10, 8); cb_lay.setSpacing(4)
- clbl = QLabel("LAYOUT — drag tiles to edges to split, center to move")
+ clbl = QLabel("LAYOUT — edge: split (blue) | edge gap: insert between (amber) | center: move (green)")
clbl.setObjectName("layoutCanvasLabel")
cb_lay.addWidget(clbl)
self._canvas = LayoutCanvas(self.cfg.panes, self.cfg.tree, self)