Skip to content
This repository was archived by the owner on May 28, 2026. It is now read-only.

Commit d4db473

Browse files
author
Barut
committed
Centralize Niri window removal handling
Add a transactional removeWindows path that removes tiles and columns while preserving selection, active column state, viewport offsets, and animation cleanup. Reuse the column removal animation helper from both legacy and transactional paths, and route sync removal through the new result object. Cover active-column fallback, previous-column offset restoration, batch removal, in-column tile removal, and no-op selection behavior.
1 parent a764174 commit d4db473

5 files changed

Lines changed: 695 additions & 110 deletions

File tree

Sources/OmniWM/Core/Controller/NiriLayoutHandler.swift

Lines changed: 32 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,7 @@ enum NiriWindowMoveResult {
3333
struct RemovalContext {
3434
var existingHandleIds: Set<WindowToken>
3535
var wasEmptyBeforeSync: Bool
36-
var columnRemovalResult: NiriLayoutEngine.ColumnRemovalResult?
37-
var precomputedFallback: NodeId?
38-
var originalColumnIndex: Int?
36+
var removalResult: NiriLayoutEngine.NiriRemovalResult
3937
}
4038

4139
var scrollAnimationByDisplay: [CGDirectDisplayID: WorkspaceDescriptor.ID] = [:]
@@ -407,55 +405,24 @@ enum NiriWindowMoveResult {
407405
removedNodeIds: [NodeId]
408406
) -> RemovalContext {
409407
let existingHandleIds = pass.engine.root(for: pass.wsId)?.windowIdSet ?? []
410-
let currentHandleIds = Set(windowTokens)
411-
let removedHandleIds = existingHandleIds.subtracting(currentHandleIds)
412-
let removedNodeIdSet = Set(removedNodeIds)
413-
414-
var precomputedFallback: NodeId?
415-
var originalColumnIndex: Int?
416-
var columnRemovalResult: NiriLayoutEngine.ColumnRemovalResult?
417-
408+
let removedHandleIds = existingHandleIds.subtracting(Set(windowTokens))
418409
let wasEmptyBeforeSync = pass.engine.columns(in: pass.wsId).isEmpty
419410

420-
for removedHandleId in removedHandleIds {
421-
guard let window = pass.engine.findNode(for: removedHandleId),
422-
let col = pass.engine.column(of: window),
423-
let colIdx = pass.engine.columnIndex(of: col, in: pass.wsId) else { continue }
424-
425-
let allWindowsInColumnRemoved = col.windowNodes.allSatisfy { w in
426-
!currentHandleIds.contains(w.token)
427-
}
428-
429-
if allWindowsInColumnRemoved && columnRemovalResult == nil {
430-
originalColumnIndex = colIdx
431-
columnRemovalResult = pass.engine.animateColumnsForRemoval(
432-
columnIndex: colIdx,
433-
in: pass.wsId,
434-
motion: motion,
435-
state: &state,
436-
gaps: pass.gap
437-
)
438-
}
439-
440-
let shouldPrecomputeFallback = if removedNodeIdSet.isEmpty {
441-
window.id == currentSelection
442-
} else {
443-
removedNodeIdSet.contains(window.id)
444-
}
445-
if shouldPrecomputeFallback {
446-
precomputedFallback = pass.engine.fallbackSelectionOnRemoval(
447-
removing: window.id,
448-
in: pass.wsId
449-
)
450-
}
451-
}
411+
let removalResult = pass.engine.removeWindows(
412+
removedHandleIds,
413+
in: pass.wsId,
414+
state: &state,
415+
motion: motion,
416+
workingFrame: pass.insetFrame,
417+
gaps: pass.gap,
418+
selectedNodeId: currentSelection,
419+
removedNodeIds: removedNodeIds
420+
)
452421

453422
return RemovalContext(
454423
existingHandleIds: existingHandleIds,
455424
wasEmptyBeforeSync: wasEmptyBeforeSync,
456-
columnRemovalResult: columnRemovalResult,
457-
precomputedFallback: precomputedFallback,
458-
originalColumnIndex: originalColumnIndex
425+
removalResult: removalResult
459426
)
460427
}
461428

@@ -497,7 +464,7 @@ enum NiriWindowMoveResult {
497464

498465
let originalActiveIdx = state.activeColumnIndex
499466
let insertedBeforeActive = newColumnData.filter { $0.colIdx <= originalActiveIdx }
500-
if !insertedBeforeActive.isEmpty, removal.columnRemovalResult == nil {
467+
if !insertedBeforeActive.isEmpty, removal.removalResult.removedColumnIndicesBefore.isEmpty {
501468
let totalInsertedWidth = insertedBeforeActive.reduce(CGFloat(0)) { total, data in
502469
total + data.col.cachedWidth + pass.gap
503470
}
@@ -531,25 +498,11 @@ enum NiriWindowMoveResult {
531498
) -> (viewportNeedsRecalc: Bool, rememberedFocusToken: WindowToken?) {
532499
state.displayRefreshRate = snapshot.displayRefreshRate
533500

534-
if let result = removal.columnRemovalResult {
535-
if let prevOffset = state.activatePrevColumnOnRemoval {
536-
state.viewOffsetPixels = .static(prevOffset)
537-
state.activatePrevColumnOnRemoval = nil
538-
}
539-
540-
if let fallback = result.fallbackSelectionId {
541-
state.selectedNodeId = fallback
542-
} else if let selectedId = state.selectedNodeId, pass.engine.findNode(by: selectedId) == nil {
543-
state.selectedNodeId = removal.precomputedFallback
544-
?? pass.engine.validateSelection(selectedId, in: pass.wsId)
545-
}
546-
} else {
547-
if let selectedId = state.selectedNodeId {
548-
if pass.engine.findNode(by: selectedId) == nil {
549-
state.selectedNodeId = removal.precomputedFallback
550-
?? pass.engine.validateSelection(selectedId, in: pass.wsId)
551-
}
552-
}
501+
if let finalSelectionId = removal.removalResult.finalSelectionId {
502+
state.selectedNodeId = finalSelectionId
503+
} else if let selectedId = state.selectedNodeId,
504+
pass.engine.findNode(by: selectedId) == nil {
505+
state.selectedNodeId = pass.engine.validateSelection(selectedId, in: pass.wsId)
553506
}
554507

555508
if state.selectedNodeId == nil {
@@ -566,7 +519,7 @@ enum NiriWindowMoveResult {
566519
}
567520

568521
let offsetBefore = state.viewOffsetPixels.current()
569-
var viewportNeedsRecalc = false
522+
var viewportNeedsRecalc = removal.removalResult.viewportNeedsRecalc
570523

571524
let isGestureOrAnimation = state.viewOffsetPixels.isGesture || state.viewOffsetPixels.isAnimating
572525

@@ -580,21 +533,19 @@ enum NiriWindowMoveResult {
580533
!isGestureOrAnimation,
581534
snapshot.isActiveWorkspace,
582535
let selectedId = state.selectedNodeId,
583-
let selectedNode = pass.engine.findNode(by: selectedId)
536+
let selectedNode = pass.engine.findNode(by: selectedId),
537+
!removal.removalResult.visibilityWasCorrected,
538+
removal.removalResult.removedTokens.isEmpty || removal.removalResult.fromIndexForVisibility != nil
584539
{
585-
if let restoreOffset = removal.columnRemovalResult?.restorePreviousViewOffset {
586-
state.viewOffsetPixels = .static(restoreOffset)
587-
} else {
588-
pass.engine.ensureSelectionVisible(
589-
node: selectedNode,
590-
in: pass.wsId,
591-
motion: motion,
592-
state: &state,
593-
workingFrame: pass.insetFrame,
594-
gaps: pass.gap,
595-
fromContainerIndex: removal.originalColumnIndex
596-
)
597-
}
540+
pass.engine.ensureSelectionVisible(
541+
node: selectedNode,
542+
in: pass.wsId,
543+
motion: motion,
544+
state: &state,
545+
workingFrame: pass.insetFrame,
546+
gaps: pass.gap,
547+
fromContainerIndex: removal.removalResult.fromIndexForVisibility
548+
)
598549
if abs(state.viewOffsetPixels.current() - offsetBefore) > 1 {
599550
viewportNeedsRecalc = true
600551
}

Sources/OmniWM/Core/Layout/Niri/NiriLayoutEngine+Animation.swift

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,35 +27,13 @@ extension NiriLayoutEngine {
2727
- columnX(at: removedIdx, columns: cols, gaps: gaps)
2828
let postRemovalCount = cols.count - 1
2929

30-
if activeIdx <= removedIdx {
31-
for col in cols[(removedIdx + 1)...] {
32-
if col.hasMoveAnimationRunning {
33-
col.offsetMoveAnimCurrent(offset)
34-
} else {
35-
col.animateMoveFrom(
36-
displacement: CGPoint(x: offset, y: 0),
37-
clock: animationClock,
38-
config: windowMovementAnimationConfig,
39-
displayRefreshRate: displayRefreshRate,
40-
animated: motion.animationsEnabled
41-
)
42-
}
43-
}
44-
} else {
45-
for col in cols[..<removedIdx] {
46-
if col.hasMoveAnimationRunning {
47-
col.offsetMoveAnimCurrent(-offset)
48-
} else {
49-
col.animateMoveFrom(
50-
displacement: CGPoint(x: -offset, y: 0),
51-
clock: animationClock,
52-
config: windowMovementAnimationConfig,
53-
displayRefreshRate: displayRefreshRate,
54-
animated: motion.animationsEnabled
55-
)
56-
}
57-
}
58-
}
30+
animateColumnsAroundRemoval(
31+
columns: cols,
32+
removedIdx: removedIdx,
33+
activeIdx: activeIdx,
34+
offset: offset,
35+
motion: motion
36+
)
5937

6038
let removingNode = cols[removedIdx].windowNodes.first
6139
let fallback = removingNode.flatMap { fallbackSelectionOnRemoval(removing: $0.id, in: workspaceId) }
@@ -94,6 +72,41 @@ extension NiriLayoutEngine {
9472
}
9573
}
9674

75+
func animateColumnsAroundRemoval(
76+
columns cols: [NiriContainer],
77+
removedIdx: Int,
78+
activeIdx: Int,
79+
offset: CGFloat,
80+
motion: MotionSnapshot
81+
) {
82+
guard removedIdx >= 0, removedIdx < cols.count else { return }
83+
84+
let animatedColumns: ArraySlice<NiriContainer>
85+
let displacement: CGFloat
86+
if activeIdx <= removedIdx {
87+
guard removedIdx + 1 < cols.count else { return }
88+
animatedColumns = cols[(removedIdx + 1)...]
89+
displacement = offset
90+
} else {
91+
animatedColumns = cols[..<removedIdx]
92+
displacement = -offset
93+
}
94+
95+
for col in animatedColumns {
96+
if col.hasMoveAnimationRunning {
97+
col.offsetMoveAnimCurrent(displacement)
98+
} else {
99+
col.animateMoveFrom(
100+
displacement: CGPoint(x: displacement, y: 0),
101+
clock: animationClock,
102+
config: windowMovementAnimationConfig,
103+
displayRefreshRate: displayRefreshRate,
104+
animated: motion.animationsEnabled
105+
)
106+
}
107+
}
108+
}
109+
97110
func animateColumnsForAddition(
98111
columnIndex addedIdx: Int,
99112
in workspaceId: WorkspaceDescriptor.ID,

Sources/OmniWM/Core/Layout/Niri/NiriLayoutEngine+ColumnOps.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ extension NiriLayoutEngine {
212212
) {
213213
guard column.children.isEmpty else { return }
214214

215+
// Window-close removals use removeWindows(...); this is structural cleanup for move/consume paths.
215216
column.remove()
216217
}
217218

0 commit comments

Comments
 (0)