11import AppKit
22
3+ // ============================================================================
4+ // LAYOUT ENGINE ARCHITECTURE
5+ // ============================================================================
6+ // CodexBar menu rendering follows a strict two-phase deterministic pipeline:
7+ //
8+ // PHASE C — LAYOUT ENGINE
9+ // • All SwiftUI measurement, intrinsic sizing, fittingSize evaluation
10+ // • All content fingerprinting, height caching, LayoutGraph construction
11+ // • All menu-item property setup (title, action, target, submenu, …)
12+ // • View allocation: NSHostingView(rootView:) — exactly once per content
13+ // • Frame commit: hosting.frame = precomputedSize
14+ // • Output: frozen NSMenuItem snapshot (view + properties)
15+ // • MUST NOT touch NSMenu — output is the LayoutGraph
16+ //
17+ // PHASE A — RENDER LAYER
18+ // • ONLY responsibility: assign NSView to NSMenuItem
19+ // • NO measurement, NO SwiftUI interaction, NO invalidateIntrinsicContentSize
20+ // • NO fittingSize / intrinsicContentSize access
21+ // • NO layout computation of any kind
22+ // • NSMenu is a passive renderer; A-phase hands it precomputed views
23+ //
24+ // The boundary is enforced structurally: A-phase functions are 1-line assignments.
25+ // Any future code that wants to do "more" in A-phase must be justified against
26+ // this contract.
27+ // ============================================================================
28+
329/// Pre-harvest snapshot of one live content row, captured before card views are detached
430/// into the recycle pool so reconciliation can still compare row shapes afterwards.
531struct MenuRowShape {
@@ -60,12 +86,20 @@ extension StatusItemController {
6086 }
6187
6288 for offset in 0 ..< prefix {
63- self . updateMenuItemInPlace ( menu. items [ fromIndex + offset] , from: newItems [ offset] )
89+ let live = menu. items [ fromIndex + offset]
90+ let scratch = newItems [ offset]
91+ // Phase C: content sync (properties only, no view).
92+ self . applyMenuItemContent ( live, from: scratch)
93+ // Phase A: view handoff (precomputed view, no measurement).
94+ self . updateMenuItemInPlace ( live, from: scratch)
6495 }
6596 for offset in 0 ..< suffix {
66- self . updateMenuItemInPlace (
67- menu. items [ menu. items. count - 1 - offset] ,
68- from: newItems [ newItems. count - 1 - offset] )
97+ let live = menu. items [ menu. items. count - 1 - offset]
98+ let scratch = newItems [ newItems. count - 1 - offset]
99+ // Phase C: content sync.
100+ self . applyMenuItemContent ( live, from: scratch)
101+ // Phase A: view handoff.
102+ self . updateMenuItemInPlace ( live, from: scratch)
69103 }
70104
71105 let liveMiddleCount = shapes. count - prefix - suffix
@@ -106,6 +140,7 @@ extension StatusItemController {
106140 }
107141 displacedItems. append ( newItem)
108142 } else {
143+ // Phase A: structural replacement when shape doesn't match.
109144 menu. insertItem ( newItem, at: index)
110145 menu. removeItem ( liveItem)
111146 displacedItems. append ( liveItem)
@@ -144,20 +179,15 @@ extension StatusItemController {
144179 }
145180 }
146181
147- private func updateMenuItemInPlace( _ liveItem: NSMenuItem , from newItem: NSMenuItem ) {
182+ /// === PHASE C: LAYOUT ENGINE ===
183+ /// Applies the content payload of a freshly-built NSMenuItem onto an existing live
184+ /// NSMenuItem that already occupies its slot in the tracked menu. Pure property
185+ /// synchronization — no view transfer, no layout, no SwiftUI interaction.
186+ /// Called by A-phase render-layer callers before the view handoff.
187+ private func applyMenuItemContent( _ liveItem: NSMenuItem , from newItem: NSMenuItem ) {
148188 if liveItem. isSeparatorItem { return }
149- let remainsHighlighted = liveItem. menu. map {
150- self . highlightedMenuItems [ ObjectIdentifier ( $0) ] === liveItem
151- } ?? false
152- // Detach from the scratch item first so a view or submenu is never referenced by
153- // two menu items at once.
154- let view = newItem. view
155- newItem. view = nil
156- let submenu = newItem. submenu
189+ liveItem. submenu = newItem. submenu
157190 newItem. submenu = nil
158- liveItem. view = view
159- ( view as? MenuCardHighlighting ) ? . setHighlighted ( remainsHighlighted)
160- liveItem. submenu = submenu
161191 liveItem. title = newItem. title
162192 liveItem. attributedTitle = newItem. attributedTitle
163193 liveItem. action = newItem. action
@@ -183,8 +213,35 @@ extension StatusItemController {
183213 }
184214 }
185215
216+ /// === PHASE A: RENDER LAYER ===
217+ /// Pure view transfer. The precomputed NSHostingView from C-phase is moved from
218+ /// the scratch item onto the existing live item in the tracked menu. NO measurement,
219+ /// NO SwiftUI interaction, NO layout invalidation, NO fittingSize/intrinsicContentSize
220+ /// access. The view's frame is already authoritative from C-phase; the live item
221+ /// keeps its current submenu/property state (which C-phase updated via
222+ /// `applyMenuItemContent` before this handoff).
223+ private func updateMenuItemInPlace( _ liveItem: NSMenuItem , from newItem: NSMenuItem ) {
224+ if liveItem. isSeparatorItem { return }
225+ let remainsHighlighted = liveItem. menu. map {
226+ self . highlightedMenuItems [ ObjectIdentifier ( $0) ] === liveItem
227+ } ?? false
228+ // Single-assignment view handoff. Nothing else belongs in this function.
229+ let precomputedView = newItem. view
230+ newItem. view = nil
231+ liveItem. view = precomputedView
232+ ( precomputedView as? MenuCardHighlighting ) ? . setHighlighted ( remainsHighlighted)
233+ }
234+
186235 private func swapMenuItemContents( _ liveItem: NSMenuItem , _ cachedItem: NSMenuItem ) {
187236 let holder = NSMenuItem ( )
237+ // Phase C: three-way content rotation
238+ // holder ← liveItem (save live's state)
239+ // liveItem ← cachedItem (live now mirrors cached)
240+ // cachedItem ← holder (cached now mirrors live's old state)
241+ self . applyMenuItemContent ( holder, from: liveItem)
242+ self . applyMenuItemContent ( liveItem, from: cachedItem)
243+ self . applyMenuItemContent ( cachedItem, from: holder)
244+ // Phase A: three-way view rotation (same pattern)
188245 self . updateMenuItemInPlace ( holder, from: liveItem)
189246 self . updateMenuItemInPlace ( liveItem, from: cachedItem)
190247 self . updateMenuItemInPlace ( cachedItem, from: holder)
0 commit comments