From f60de21eeaa50f847108a9c103934cc8da7352fb Mon Sep 17 00:00:00 2001 From: Christian Kolset Date: Wed, 3 Jun 2026 16:35:15 -0600 Subject: Three-zone drop system: squeeze, split, center MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ui/windows/plot_window.py | 151 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 128 insertions(+), 23 deletions(-) (limited to 'ui/windows/plot_window.py') 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) -- cgit v1.2.3