Skip to content

Commit 2f5cf69

Browse files
committed
Reset card highlight state when harvesting recycled views
The highlight tracker unwinds through the outgoing item's view, which harvesting detaches, so a card highlighted at rebuild time would re-attach with stale highlight rendering and no path to clear it. Clear the highlight and drop the tracking entry before pooling the view.
1 parent db94427 commit 2f5cf69

2 files changed

Lines changed: 44 additions & 0 deletions

File tree

Sources/CodexBar/StatusItemController+MenuCardRecycling.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,18 @@ extension StatusItemController {
1919
self.menuCardViewRecyclePool.removeAll(keepingCapacity: true)
2020
guard self.canRecycleMenuCardViews(in: menu) else { return }
2121
guard fromIndex >= 0, fromIndex < menu.items.count else { return }
22+
let menuKey = ObjectIdentifier(menu)
2223
for item in menu.items[fromIndex...] {
2324
guard let id = item.representedObject as? String else { continue }
2425
guard let view = item.view, view is any MenuCardMeasuring else { continue }
2526
guard self.menuCardViewRecyclePool[id] == nil else { continue }
27+
// Unhighlight before detaching: the highlight tracker unwinds through the
28+
// outgoing item's `view`, which is about to become nil, so a recycled view
29+
// would otherwise re-attach visibly highlighted with no path to clear it.
30+
if self.highlightedMenuItems[menuKey] === item {
31+
self.highlightedMenuItems.removeValue(forKey: menuKey)
32+
}
33+
(view as? MenuCardHighlighting)?.setHighlighted(false)
2634
item.view = nil
2735
self.menuCardViewRecyclePool[id] = view
2836
}

Tests/CodexBarTests/MenuCardViewRecyclingTests.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,42 @@ extension StatusMenuTests {
138138
rebuiltView.setHighlighted(false)
139139
}
140140

141+
@Test
142+
func `harvesting a highlighted card clears its highlight and tracking entry`() {
143+
StatusItemController.setMenuRefreshEnabledForTesting(false)
144+
let previousRendering = StatusItemController.menuCardRenderingEnabled
145+
StatusItemController.menuCardRenderingEnabled = true
146+
defer { StatusItemController.menuCardRenderingEnabled = previousRendering }
147+
148+
let settings = self.makeSettings()
149+
settings.statusChecksEnabled = false
150+
settings.refreshFrequency = .manual
151+
let controller = self.makeRecyclingController(settings: settings)
152+
defer { controller.releaseStatusItemsForTesting() }
153+
154+
let menu = NSMenu()
155+
let item = controller.makeMenuCardItem(Text("card"), id: "menuCard", width: 300)
156+
menu.addItem(item)
157+
controller.menu(menu, willHighlight: item)
158+
guard let hosting = item.view as? MenuCardItemHostingView<MenuCardSectionContainerView<Text>>
159+
else {
160+
Issue.record("expected a card hosting view")
161+
return
162+
}
163+
#expect(hosting.highlightState.isHighlighted)
164+
#expect(controller.highlightedMenuItems[ObjectIdentifier(menu)] === item)
165+
166+
controller.harvestRecyclableMenuCardViews(in: menu, fromIndex: 0)
167+
defer { controller.clearMenuCardViewRecyclePool() }
168+
169+
#expect(!hosting.highlightState.isHighlighted)
170+
#expect(controller.highlightedMenuItems[ObjectIdentifier(menu)] == nil)
171+
172+
let rebuilt = controller.makeMenuCardItem(Text("rebuilt"), id: "menuCard", width: 300)
173+
#expect(rebuilt.view === hosting)
174+
#expect(!hosting.highlightState.isHighlighted)
175+
}
176+
141177
@Test
142178
func `same id with different content type builds a fresh view`() {
143179
StatusItemController.setMenuRefreshEnabledForTesting(false)

0 commit comments

Comments
 (0)