From 891b4ec5f9bbc6bd1fe0db9ed20a1b3a16eccfe2 Mon Sep 17 00:00:00 2001 From: omartelo Date: Fri, 23 Oct 2026 22:14:36 -0300 Subject: [PATCH] fix(native): end a tab drag that can no longer hear its drop A keyboard drag listens for Space and Escape only on the focused item, so once focus moved elsewhere the tab stayed lifted for good and every other press was ignored. A pointer drag whose Sortable turned Disabled mid-drag (a layout tab's rename) lost its release the same way. Both now end the drag without dropping. --- CHANGELOG.md | 1 + native/ui/sortable.go | 10 ++++++++++ native/ui/sortable_test.go | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fc17ae6a..41cc91e34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A tab in the top bar can no longer get stuck lifted, drawn in the accent color as if it were being dragged, with no tab moving again until lich restarted. A tab focused by a click and then given Space or Enter was picked up to reorder from the keyboard, and once a click elsewhere took the keyboard it could neither be dropped nor cancelled; it now goes back to its place when it loses focus. A layout tab renamed in the middle of a drag no longer gets stuck the same way. - A session card no longer grows and shrinks as its agent starts and stops a tool, waits on you or compacts, pushing every card below it up and down. That line now takes the place of the card's path while it shows, and the path comes back when it goes. - The native window holds less memory for its fonts. A font file asked for more than once, such as the interface and the terminal set in the same family or the bold face that also stands in for semibold, was parsed again each time, about 30MB for a Nerd Font face; it is now parsed once and shared while in use. An interface in a language other than Chinese, Japanese or Korean no longer loads a CJK font at start, tens of megabytes: it does on the first CJK text on screen, drawn meanwhile with a system font. With one Nerd Font for the interface, the terminal and the editor and no CJK on screen, the fonts take 119MB where they took 246MB. - On macOS, opening lich from the Dock, Launchpad, Spotlight or Finder while it runs with its window closed opens the window again. lich started at login with `--no-window` included, where the click did nothing. Logging out now quits lich the way `lich quit` does, so the next launch does not read it as a crash. diff --git a/native/ui/sortable.go b/native/ui/sortable.go index 5beb8b48a..bb516e4be 100644 --- a/native/ui/sortable.go +++ b/native/ui/sortable.go @@ -82,6 +82,11 @@ type sortTarget struct { // what the button did, so a move and a release queued together drop where the // move went. func (s *Sortable) Update(gtx C) (SortDrop, bool) { + // Disabled items register no handler, so the release that would end a + // drag already in the air never arrives. + if s.Disabled && s.active != "" { + s.end() + } if s.Carry != nil { s.Carry.hover(gtx) if s.active != "" && !s.keyboard && s.Carry.carrying == "" { @@ -286,6 +291,11 @@ func (s *Sortable) handle(gtx C, t *sortTarget) { } else if s.focused == t.id { s.focused = "" } + // Only the focused item hears Space and Escape, so a keyboard + // drag that loses focus could never end. + if !e.Focus && s.keyboard && s.active == t.id { + s.end() + } case key.Event: s.key(t, e) } diff --git a/native/ui/sortable_test.go b/native/ui/sortable_test.go index feff5eb15..7fc2349b2 100644 --- a/native/ui/sortable_test.go +++ b/native/ui/sortable_test.go @@ -211,6 +211,38 @@ func TestSortableKeyboardEscapeDropsNothing(t *testing.T) { r.want(t) } +// The keyboard drops only into a focused item, so focus moving elsewhere (a +// click on the terminal) cancels the drag, or nothing could ever end it. +func TestSortableKeyboardDragEndsWhenFocusLeaves(t *testing.T) { + r := newSortRig() + r.press(onA) + r.release(onA) + r.keys(key.NameSpace) + r.router.Source().Execute(key.FocusCmd{}) + r.frame() + if r.s.Dragging() != "" { + t.Errorf("dragging %q after the item lost focus, want none", r.s.Dragging()) + } + r.want(t) +} + +// A tab that starts a rename mid-drag disables its Sortable, whose items then +// never hear the release: the drag ends there, and the next one lifts. +func TestSortableDisabledMidDragEndsIt(t *testing.T) { + r := newSortRig() + r.press(onA) + r.move(onA.Add(image.Pt(10, 0))) + r.s.Disabled = true + r.release(onB) + r.s.Disabled = false + r.frame() + if r.s.Dragging() != "" { + t.Fatalf("dragging %q after a drag was disabled, want none", r.s.Dragging()) + } + r.drag(onA, onA.Add(image.Pt(10, 0)), onB) + r.want(t, SortDrop{ID: "a", Over: "b"}) +} + func TestSortableDisabledLiftsNothing(t *testing.T) { r := newSortRig() r.s.Disabled = true