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

Commit a68c351

Browse files
author
Barut
committed
Align Niri viewport fitting by window mode
Extract shared mode-aware viewport fitting for normal, maximized, and fullscreen columns. Use the parent monitor area for maximized columns, working area plus gaps for normal columns, and anchor fullscreen columns at offset zero. Route focus, resize, center, gesture, new-window, and workspace move paths through the shared fitting helper so keyboard and gesture snapping agree. Add regression coverage for mode-aware offsets and inset working-area centering.
1 parent d4db473 commit a68c351

10 files changed

Lines changed: 577 additions & 265 deletions

Sources/OmniWM/Core/Controller/NiriLayoutHandler.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,7 +596,10 @@ enum NiriWindowMoveResult {
596596
motion: motion,
597597
animate: false,
598598
centerMode: settings.centerFocusedColumn,
599-
alwaysCenterSingleColumn: settings.alwaysCenterSingleColumn
599+
alwaysCenterSingleColumn: settings.alwaysCenterSingleColumn,
600+
scale: pass.engine.displayScale(in: pass.wsId),
601+
workingArea: pass.insetFrame,
602+
viewFrame: pass.monitor.frame
600603
)
601604
}
602605
} else if let newCol = pass.engine.column(of: newNode),

Sources/OmniWM/Core/Controller/WorkspaceNavigationHandler.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -820,12 +820,13 @@ final class WorkspaceNavigationHandler {
820820
{
821821
targetState.selectedNodeId = movedNode.id
822822
let gap = CGFloat(controller.workspaceManager.gaps)
823+
let workingFrame = controller.insetWorkingFrame(for: monitor)
823824
engine.ensureSelectionVisible(
824825
node: movedNode,
825826
in: target.id,
826827
motion: controller.motionPolicy.snapshot(),
827828
state: &targetState,
828-
workingFrame: monitor.visibleFrame,
829+
workingFrame: workingFrame,
829830
gaps: gap
830831
)
831832
}
@@ -935,12 +936,13 @@ final class WorkspaceNavigationHandler {
935936
targetState.selectedNodeId = movedNode.id
936937

937938
let gap = CGFloat(controller.workspaceManager.gaps)
939+
let workingFrame = controller.insetWorkingFrame(for: monitor)
938940
engine.ensureSelectionVisible(
939941
node: movedNode,
940942
in: targetWsId,
941943
motion: controller.motionPolicy.snapshot(),
942944
state: &targetState,
943-
workingFrame: monitor.visibleFrame,
945+
workingFrame: workingFrame,
944946
gaps: gap
945947
)
946948
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ extension NiriLayoutEngine {
4545
motion: motion,
4646
animate: false,
4747
centerMode: settings.centerFocusedColumn,
48-
alwaysCenterSingleColumn: settings.alwaysCenterSingleColumn
48+
alwaysCenterSingleColumn: settings.alwaysCenterSingleColumn,
49+
scale: displayScale(in: workspaceId),
50+
workingArea: workingFrame,
51+
viewFrame: monitorForWorkspace(workspaceId)?.frame
4952
)
5053

5154
return true

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

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,21 @@ extension NiriLayoutEngine {
1717

1818
cancelInteractiveResize(for: columns[activeIndex], in: workspaceId)
1919

20+
let scale = displayScale(in: workspaceId)
21+
let viewFrame = monitorForWorkspace(workspaceId)?.frame
2022
let targetOffset = state.computeCenteredOffset(
2123
columnIndex: activeIndex,
2224
columns: columns,
2325
gap: gaps,
24-
viewportWidth: workingFrame.width
26+
viewportWidth: workingFrame.width,
27+
workingArea: workingFrame,
28+
viewFrame: viewFrame,
29+
scale: scale
2530
)
2631
state.animateToOffset(
2732
targetOffset,
2833
motion: motion,
29-
scale: displayScale(in: workspaceId)
34+
scale: scale
3035
)
3136
return true
3237
}
@@ -52,16 +57,25 @@ extension NiriLayoutEngine {
5257
let activeIndex = state.activeColumnIndex.clamped(to: 0 ... (columns.count - 1))
5358
state.activeColumnIndex = activeIndex
5459

60+
let scale = displayScale(in: workspaceId)
61+
let viewFrame = monitorForWorkspace(workspaceId)?.frame
62+
let areas = state.normalizedFittingAreas(
63+
viewportSpan: workingFrame.width,
64+
workingArea: workingFrame,
65+
viewFrame: viewFrame,
66+
scale: scale
67+
)
5568
let viewStart = state.targetViewPosPixels(columns: columns, gap: gaps)
56-
let viewportWidth = workingFrame.width
69+
let workingStart = areas.origin(of: areas.working)
70+
let viewportWidth = areas.span(of: areas.working)
5771

5872
var widthTaken: CGFloat = 0
5973
var leftmostColumnX: CGFloat?
6074
var activeColumnX: CGFloat?
6175

6276
for (idx, column) in columns.enumerated() {
6377
let columnX = state.columnX(at: idx, columns: columns, gap: gaps)
64-
if columnX < viewStart + gaps {
78+
if columnX < viewStart + workingStart + gaps {
6579
continue
6680
}
6781

@@ -70,7 +84,7 @@ extension NiriLayoutEngine {
7084
}
7185

7286
let width = column.cachedWidth
73-
if viewStart + viewportWidth < columnX + width + gaps {
87+
if viewStart + workingStart + viewportWidth < columnX + width + gaps {
7488
break
7589
}
7690

@@ -86,9 +100,8 @@ extension NiriLayoutEngine {
86100
cancelInteractiveResize(for: columns[activeIndex], in: workspaceId)
87101

88102
let freeSpace = viewportWidth - widthTaken + gaps
89-
let newViewStart = leftmostColumnX - freeSpace / 2
103+
let newViewStart = leftmostColumnX - freeSpace / 2 - workingStart
90104
let targetOffset = newViewStart - activeColumnX
91-
let scale = displayScale(in: workspaceId)
92105

93106
state.animateToOffset(
94107
targetOffset,
@@ -105,7 +118,9 @@ extension NiriLayoutEngine {
105118
sizeKeyPath: \.cachedWidth,
106119
centerMode: settings.centerFocusedColumn,
107120
alwaysCenterSingleColumn: settings.alwaysCenterSingleColumn,
108-
scale: scale
121+
scale: scale,
122+
workingArea: workingFrame,
123+
viewFrame: viewFrame
109124
)
110125

111126
return true

Sources/OmniWM/Core/Layout/Niri/NiriNavigation.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ extension NiriLayoutEngine {
199199
}
200200

201201
let scale = displayScale(in: workspaceId)
202+
let viewFrame = monitorForWorkspace(workspaceId)?.frame
202203
let oldActivePos = previousActiveContainerPosition
203204
?? state.containerPosition(
204205
at: state.activeColumnIndex,
@@ -227,7 +228,10 @@ extension NiriLayoutEngine {
227228
alwaysCenterSingleColumn: settings.alwaysCenterSingleColumn,
228229
animationConfig: animationConfig,
229230
fromContainerIndex: prevIdx,
230-
scale: scale
231+
scale: scale,
232+
workingArea: workingFrame,
233+
viewFrame: viewFrame,
234+
orientation: orientation
231235
)
232236

233237
state.selectionProgress = 0.0

Sources/OmniWM/Core/Layout/Niri/ViewportState+ColumnTransitions.swift

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ extension ViewportState {
88
gap: CGFloat,
99
viewportWidth: CGFloat,
1010
motion: MotionSnapshot,
11-
animate: Bool = false
11+
animate: Bool = false,
12+
workingArea: CGRect? = nil,
13+
viewFrame: CGRect? = nil,
14+
scale: CGFloat = 2.0
1215
) {
1316
guard !columns.isEmpty else { return }
1417
let clampedIndex = index.clamped(to: 0 ... (columns.count - 1))
@@ -23,7 +26,10 @@ extension ViewportState {
2326
columnIndex: clampedIndex,
2427
columns: columns,
2528
gap: gap,
26-
viewportWidth: viewportWidth
29+
viewportWidth: viewportWidth,
30+
workingArea: workingArea,
31+
viewFrame: viewFrame,
32+
scale: scale
2733
)
2834

2935
if animate {
@@ -47,7 +53,9 @@ extension ViewportState {
4753
centerMode: CenterFocusedColumn,
4854
alwaysCenterSingleColumn: Bool = false,
4955
fromColumnIndex: Int? = nil,
50-
scale: CGFloat = 2.0
56+
scale: CGFloat = 2.0,
57+
workingArea: CGRect? = nil,
58+
viewFrame: CGRect? = nil
5159
) {
5260
guard !columns.isEmpty else { return }
5361
let clampedIndex = newIndex.clamped(to: 0 ... (columns.count - 1))
@@ -71,7 +79,9 @@ extension ViewportState {
7179
centerMode: centerMode,
7280
alwaysCenterSingleColumn: alwaysCenterSingleColumn,
7381
fromColumnIndex: fromColumnIndex ?? prevActiveColumn,
74-
scale: scale
82+
scale: scale,
83+
workingArea: workingArea,
84+
viewFrame: viewFrame
7585
)
7686

7787
let pixel: CGFloat = 1.0 / max(scale, 1.0)
@@ -105,7 +115,10 @@ extension ViewportState {
105115
alwaysCenterSingleColumn: Bool = false,
106116
animationConfig: SpringConfig? = nil,
107117
fromContainerIndex: Int? = nil,
108-
scale: CGFloat = 2.0
118+
scale: CGFloat = 2.0,
119+
workingArea: CGRect? = nil,
120+
viewFrame: CGRect? = nil,
121+
orientation: Monitor.Orientation = .horizontal
109122
) {
110123
guard !containers.isEmpty, containerIndex >= 0, containerIndex < containers.count else { return }
111124

@@ -124,7 +137,10 @@ extension ViewportState {
124137
centerMode: centerMode,
125138
alwaysCenterSingleColumn: alwaysCenterSingleColumn,
126139
fromContainerIndex: fromContainerIndex,
127-
scale: scale
140+
scale: scale,
141+
workingArea: workingArea,
142+
viewFrame: viewFrame,
143+
orientation: orientation
128144
)
129145

130146
if abs(targetOffset - stationaryOffset) <= pixelEpsilon {

0 commit comments

Comments
 (0)