Skip to content

Commit f0fcd33

Browse files
author
tom
committed
build: drag card physically follows pointer (edge-only autoscroll)
1 parent 50a12e8 commit f0fcd33

1 file changed

Lines changed: 67 additions & 64 deletions

File tree

‎src/ui/components.py‎

Lines changed: 67 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,12 @@ def _drag_canvas(self):
301301
except Exception:
302302
return None
303303

304+
def _drag_siblings(self):
305+
"""All draggable cards in our list, ordered by on-screen position."""
306+
cards = [w for w in self.master.winfo_children() if isinstance(w, ActionCard)]
307+
cards.sort(key=lambda w: w.winfo_rooty())
308+
return cards
309+
304310
def _start_drag(self, event):
305311
"""Begin a drag operation. Motion is captured on the toplevel so the drag
306312
stays smooth even when the pointer crosses over neighbouring cards."""
@@ -309,13 +315,13 @@ def _start_drag(self, event):
309315

310316
self._drag_active = True
311317
self._drag_start_index = self.index
312-
self._drag_last_pos = None # target slot the indicator currently shows
313-
self._drag_pointer_y = None
318+
self._drag_start_slot = self.index
319+
self._drag_slot = self.index
320+
self._drag_pointer_y = event.y_root
314321
self._drag_autoscroll_job = None
315322

316-
# Highlight the card being dragged (elevated look). We deliberately do NOT
317-
# repack it during the drag — the real reorder happens once on release,
318-
# which is what keeps the motion fluid.
323+
# Highlight the card being dragged so it is unmistakable among similar
324+
# looking siblings while the pointer moves it.
319325
self.configure(style='DragCard.TFrame')
320326
self.header_frame.configure(style='DragCard.TFrame')
321327
try:
@@ -328,50 +334,15 @@ def _start_drag(self, event):
328334
toplevel.bind('<B1-Motion>', self._drag_motion)
329335
toplevel.bind('<ButtonRelease-1>', self._end_drag)
330336

331-
def _drag_insertion_y(self, cards, pos):
332-
"""Canvas-Y where the insertion indicator should sit for slot ``pos``
333-
(the gap the dragged card would land in)."""
334-
if pos >= len(cards):
335-
last = cards[-1]
336-
return last.winfo_y() + last.winfo_height()
337-
return cards[pos].winfo_y()
338-
339-
def _draw_drag_indicator(self, event):
340-
"""Draw (and move) the insertion line showing exactly where the dragged
341-
card will land. Only redraws when the target slot changes so ordinary
342-
motion stays cheap and fluid; the card itself does not move."""
343-
cards = [w for w in self.master.winfo_children() if isinstance(w, ActionCard)]
344-
cards.sort(key=lambda w: w.winfo_rooty())
345-
pos = 0
346-
for c in cards:
347-
if c is self:
348-
continue
349-
cy = c.winfo_rooty() + c.winfo_height() // 2
350-
if (self._drag_pointer_y or 0) > cy:
351-
pos += 1
352-
if pos == self._drag_last_pos:
353-
return
354-
self._drag_last_pos = pos
355-
356-
canvas = self._drag_canvas()
357-
if canvas is None:
358-
return
359-
canvas.delete('_drag_ind')
360-
target = self._drag_insertion_y(cards, pos)
361-
w = canvas.winfo_width()
362-
if w > 1:
363-
canvas.create_line(0, target, w, target, fill=COLORS['primary'],
364-
width=4, tags='_drag_ind')
365-
canvas.update_idletasks()
366-
367337
def _drag_motion(self, event):
368-
"""Smooth drag: show a live insertion indicator where the card will land,
369-
auto-scrolling ONLY while the pointer is pressed against the canvas edge
370-
(the list otherwise stays put)."""
338+
"""Smooth drag: the card physically follows the pointer. The list is only
339+
repacked when the drop slot actually changes (so ordinary motion stays
340+
cheap), and it auto-scrolls ONLY while the pointer is pressed against the
341+
top/bottom edge band — the list otherwise stays put."""
371342
if not self._drag_active:
372343
return
373344
self._drag_pointer_y = event.y_root
374-
self._draw_drag_indicator(event)
345+
self._move_to_pointer_slot()
375346

376347
canvas = self._drag_canvas()
377348
if canvas is None:
@@ -382,16 +353,52 @@ def _drag_motion(self, event):
382353
edge = 40
383354
if event.y_root < top + edge:
384355
depth = (top + edge - event.y_root) / float(edge)
385-
self._set_autoscroll(canvas, -1, depth)
356+
self._start_autoscroll(canvas, -1, depth)
386357
elif event.y_root > top + height - edge:
387358
depth = (event.y_root - (top + height - edge)) / float(edge)
388-
self._set_autoscroll(canvas, 1, depth)
359+
self._start_autoscroll(canvas, 1, depth)
389360
else:
390361
self._stop_autoscroll()
391362
except Exception:
392363
pass
393364

394-
def _set_autoscroll(self, canvas, direction, depth):
365+
def _slot_from_pointer(self):
366+
"""Target slot (gap between other cards) the card would land in right
367+
now, based on the last known pointer root-Y. Other cards' slots are the
368+
gaps; the dragged card itself is skipped."""
369+
pos = 0
370+
for c in self._drag_siblings():
371+
if c is self:
372+
continue
373+
cy = c.winfo_rooty() + c.winfo_height() // 2
374+
if self._drag_pointer_y > cy:
375+
pos += 1
376+
return pos
377+
378+
def _move_to_pointer_slot(self):
379+
"""Repack the dragged card so it visually tracks the pointer. Only does
380+
real layout work when the target slot changes (the cheap path)."""
381+
cards = self._drag_siblings()
382+
pos = self._drag_slot_from_pointer(cards)
383+
if pos == self._drag_slot:
384+
return
385+
self._drag_slot = pos
386+
self._repack_at_slot(cards, pos)
387+
388+
def _repack_at_slot(self, cards, pos):
389+
parent = self.master
390+
others = [c for c in cards if c is not self]
391+
if pos >= len(others):
392+
# Move to the end (below every sibling)
393+
self.pack(fill=tk.X, pady=1)
394+
else:
395+
self.pack(fill=tk.X, pady=1, before=others[pos])
396+
try:
397+
parent.update_idletasks()
398+
except Exception:
399+
pass
400+
401+
def _start_autoscroll(self, canvas, direction, depth):
395402
"""Smooth auto-scroll loop while the pointer hovers in the edge band.
396403
Velocity grows with pointer depth; the loop self-cancels once the pointer
397404
leaves the band or the drag ends."""
@@ -415,7 +422,10 @@ def _tick():
415422
return
416423
speed = max(1, int(depth * 6))
417424
canvas.yview_scroll(direction * speed, "units")
418-
self._draw_drag_indicator(None)
425+
# The list moved under the pointer — re-slot so the dragged card
426+
# keeps following even while the band auto-scrolls.
427+
cards = self._drag_siblings()
428+
self._repack_at_slot(cards, self._drag_slot_from_pointer(cards))
419429
except Exception:
420430
self._drag_autoscroll_job = None
421431
return
@@ -433,7 +443,8 @@ def _stop_autoscroll(self):
433443
self._drag_autoscroll_job = None
434444

435445
def _end_drag(self, event):
436-
"""End drag: remove the indicator, then commit the reorder if it changed."""
446+
"""End drag: restore the card's look, then commit the reorder if it
447+
changed. A click without real movement toggles the editor instead."""
437448
toplevel = self.winfo_toplevel()
438449
toplevel.unbind('<B1-Motion>')
439450
toplevel.unbind('<ButtonRelease-1>')
@@ -443,14 +454,6 @@ def _end_drag(self, event):
443454
return
444455
self._drag_active = False
445456

446-
# Remove the insertion indicator
447-
canvas = self._drag_canvas()
448-
if canvas is not None:
449-
try:
450-
canvas.delete('_drag_ind')
451-
except Exception:
452-
pass
453-
454457
# Restore the card's normal colors
455458
self.configure(style=self.card_style)
456459
self.header_frame.configure(style=self.card_style)
@@ -462,19 +465,19 @@ def _end_drag(self, event):
462465
pass
463466

464467
start = self._drag_start_index
465-
target = self._drag_last_pos
468+
target = self._drag_slot
466469
self._drag_start_index = None
467-
self._drag_last_pos = None
470+
self._drag_slot = None
471+
self._drag_pointer_y = None
468472

469-
# No real motion (a click, or a drag with no slot change) toggles the editor
470-
if target is None or target == start:
473+
# A click without real movement toggles the editor
474+
if target == start:
471475
self.toggle_expand()
472476
return
473477

474-
if target != start and 'move_to' in self.callbacks:
478+
if target is not None and target != start and 'move_to' in self.callbacks:
475479
self.callbacks['move_to'](start, target)
476480

477-
478481
# --- Context Menu ---
479482
def show_context_menu(self, event):
480483
"""Show right-click context menu."""

0 commit comments

Comments
 (0)