Skip to content

Add Niri scroll snap toggle#350

Open
Guria wants to merge 1 commit into
BarutSRB:mainfrom
Guria:disable-niri-scroll-snap
Open

Add Niri scroll snap toggle#350
Guria wants to merge 1 commit into
BarutSRB:mainfrom
Guria:disable-niri-scroll-snap

Conversation

@Guria
Copy link
Copy Markdown

@Guria Guria commented May 21, 2026

Closes #336

Summary

  • add persisted global and per-monitor Niri scroll snap setting
  • wire trackpad gesture finalization to respect the setting
  • expose controls in Niri settings and menu bar Controls popup

Testing

  • make build

Summary by CodeRabbit

  • New Features

    • Added a "Scroll Snap" setting for Niri with a global default (enabled) and optional per-monitor override; UI toggles in Settings and the status bar Controls menu.
  • Behavior

    • Scrolling/gesture completion now respects the Scroll Snap setting; when disabled, offsets and animations are preserved appropriately.
  • Tests

    • Added tests for defaults, persistence, and the status bar/menu control presence.

Review Change Stack

Copilot AI review requested due to automatic review settings May 21, 2026 08:18
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

📝 Walkthrough

Walkthrough

A new scrollSnapEnabled boolean configuration setting is added throughout the system to make trackpad scroll snapping optional. The setting is defined in TOML config, stored persistently in SettingsStore, exposed via UI toggles and status bar menu, resolved per monitor with fallback to global default, and applied to gesture handling to control column snapping behavior.

Changes

Scroll Snap Configuration and Control

Layer / File(s) Summary
Config schema and TOML parsing
Sources/OmniWM/Core/Config/CanonicalTOMLConfig.swift, Sources/OmniWM/Core/Config/MonitorNiriSettings.swift, Sources/OmniWM/Core/Config/SettingsExport.swift, Tests/OmniWMTests/Fixtures/canonical-settings.toml
CanonicalTOMLConfig.Niri, MonitorNiriSettings, and ResolvedNiriSettings each gain a scrollSnapEnabled boolean property with Codable support. TOML parsing decodes this field with recovery-mode defaults. Test fixture reflects the new config field.
Settings store persistence and resolution
Sources/OmniWM/Core/Config/SettingsStore.swift, Sources/OmniWM/Core/Config/SettingsExport.swift
SettingsStore adds a persistent niriScrollSnapEnabled property with save scheduling. Export/import wiring maps between SettingsExport.niriScrollSnapEnabled and SettingsStore.niriScrollSnapEnabled. The global default is true. Per-monitor resolvedNiriSettings() prefers monitor-level override and falls back to the global setting.
User interface toggles
Sources/OmniWM/UI/SettingsView.swift, Sources/OmniWM/UI/StatusBar/StatusBarMenu.swift
Global "Scroll Snap" toggle in settings binds to settings.niriScrollSnapEnabled and triggers config update on change. Per-monitor "Scroll Snap" override toggle allows per-monitor customization via MonitorNiriSettingsSection. Status bar menu adds a "Niri Scroll Snap" toggle in the Controls section that synchronizes with the setting and updates on config changes.
Engine/property wiring and controller APIs
Sources/OmniWM/Core/Controller/NiriLayoutHandler.swift, Sources/OmniWM/Core/Controller/WMController.swift, Sources/OmniWM/Core/Layout/Niri/NiriLayoutEngine.swift, Sources/OmniWM/Core/Layout/Niri/NiriLayoutEngine+Monitors.swift
Add scrollSnapEnabled property to NiriLayoutEngine (default true), extend updateConfiguration and controller updateNiriConfig APIs with optional scrollSnapEnabled parameter, and forward persisted value when applying settings.
Gesture handler scroll snap control
Sources/OmniWM/Core/Controller/MouseEventHandler.swift, Sources/OmniWM/Core/Layout/Niri/ViewportState+Gestures.swift
Mouse gesture finalization passes snapToColumn based on the resolved setting instead of always snapping. Viewport end-gesture paths were refactored to compute projected offsets/velocities earlier, preserve current offsets when not snapping, and optionally animate the final offset.
Testing and test helpers
Tests/OmniWMTests/NiriLayoutEngineTests.swift, Tests/OmniWMTests/SettingsStoreTests.swift, Tests/OmniWMTests/StatusBarMenuTests.swift, Tests/OmniWMTests/Fixtures/canonical-settings.toml
Test helper resolvedSettings() accepts optional scrollSnapEnabled override. Settings store tests verify the default value true is reflected in SettingsExport.defaults() and boot-time store initialization. Status bar menu tests verify the control label appears in the built menu. The canonical TOML fixture includes scrollSnapEnabled = true.

Sequence Diagram(s)

sequenceDiagram
  participant SettingsView
  participant SettingsStore
  participant NiriLayoutHandler
  participant NiriLayoutEngine
  participant ViewportState

  SettingsView->>SettingsStore: set niriScrollSnapEnabled(value)
  SettingsStore->>NiriLayoutHandler: updateNiriConfig(scrollSnapEnabled: value)
  NiriLayoutHandler->>NiriLayoutEngine: updateConfiguration(scrollSnapEnabled: value)
  ViewportState->>SettingsStore: resolvedNiriSettings(for: monitor)
  SettingsStore-->>ViewportState: ResolvedNiriSettings(scrollSnapEnabled)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through settings with a nibble and tap,
Toggles for snapping — no more forced snap-trap,
Per-monitor whispers, a global decree,
Gestures now gentle, let scrolls wander free,
I twitched my nose — the windows glide happily.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive The PR implements scroll snap control (#336) but via global/per-monitor toggles rather than the suggested modifier-key approach, which differs from the original request. Clarify if the toggle-based approach satisfies issue #336's objective of allowing manual positioning without snap, or if modifier-key functionality should also be implemented.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a toggle for Niri scroll snap functionality.
Out of Scope Changes check ✅ Passed All changes are scoped to adding scroll snap toggle functionality across config, UI, and gesture handling layers with appropriate test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a user-configurable “Niri Scroll Snap” setting (global + per-monitor override), persists it through the settings export/TOML pipeline, and exposes it in both the Settings UI and the status bar Controls menu. This aligns with issue #336 by allowing scroll gestures to finalize without forced snapping when the setting is disabled.

Changes:

  • Add persisted global niriScrollSnapEnabled plus per-monitor override MonitorNiriSettings.scrollSnapEnabled, including canonical TOML wiring.
  • Respect the resolved per-monitor scrollSnapEnabled when finalizing trackpad gestures (snapToColumn).
  • Expose a toggle in Settings (global + per-monitor) and in the status bar Controls popup; add/extend tests and fixture TOML.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
Tests/OmniWMTests/StatusBarMenuTests.swift Verifies status bar menu includes the new Niri scroll snap control.
Tests/OmniWMTests/SettingsStoreTests.swift Asserts default export/settings include niriScrollSnapEnabled = true.
Tests/OmniWMTests/NiriLayoutEngineTests.swift Updates fixture helper to include the new resolved setting.
Tests/OmniWMTests/Fixtures/canonical-settings.toml Adds scrollSnapEnabled to canonical Niri config fixture.
Sources/OmniWM/UI/StatusBar/StatusBarMenu.swift Adds a “Niri Scroll Snap” toggle row to the Controls section and keeps it in sync.
Sources/OmniWM/UI/SettingsView.swift Adds global toggle + per-monitor overridable toggle for scroll snap.
Sources/OmniWM/Core/Layout/Niri/NiriLayoutEngine+Monitors.swift Extends resolved settings construction to include scroll snap (but currently hard-coded).
Sources/OmniWM/Core/Controller/MouseEventHandler.swift Uses resolved per-monitor scroll snap setting for gesture finalization (snapToColumn).
Sources/OmniWM/Core/Config/SettingsStore.swift Persists niriScrollSnapEnabled and includes it in export/apply + resolved settings.
Sources/OmniWM/Core/Config/SettingsExport.swift Adds niriScrollSnapEnabled to the settings export and defaults.
Sources/OmniWM/Core/Config/MonitorNiriSettings.swift Adds per-monitor override field and includes it in coding + resolved settings model.
Sources/OmniWM/Core/Config/CanonicalTOMLConfig.swift Adds TOML decode/encode mapping for scrollSnapEnabled within [niri].
Scripts/build-metadata.env Updates Ghostty archive SHA256.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

maxWindowsPerColumn: maxWindowsPerColumn,
centerFocusedColumn: centerFocusedColumn,
alwaysCenterSingleColumn: alwaysCenterSingleColumn,
scrollSnapEnabled: true,
@Guria Guria force-pushed the disable-niri-scroll-snap branch from 4b0173e to dadd77a Compare May 21, 2026 09:47
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
Sources/OmniWM/Core/Layout/Niri/ViewportState+Gestures.swift (1)

435-441: 💤 Low value

Consider extracting positions computation to avoid duplication.

The positions array construction at lines 435-441 duplicates the logic in normalizedPreservedGestureOffset at lines 486-492. One option is to return the positions array from normalizedPreservedGestureOffset as part of PreservedGestureOffset, or extract a shared helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/OmniWM/Core/Layout/Niri/ViewportState`+Gestures.swift around lines
435 - 441, The positions array computation is duplicated; refactor by moving the
logic into a single helper or into the PreservedGestureOffset result so callers
reuse it. Update normalizedPreservedGestureOffset (and/or the
PreservedGestureOffset type) to produce and return the positions array, then
replace the local positions construction in ViewportState+Gestures.swift (the
loop that builds positions using columns and gap) with a call to that helper or
use the positions from PreservedGestureOffset, ensuring callers reference the
new positions property instead of recomputing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@Sources/OmniWM/Core/Layout/Niri/ViewportState`+Gestures.swift:
- Around line 435-441: The positions array computation is duplicated; refactor
by moving the logic into a single helper or into the PreservedGestureOffset
result so callers reuse it. Update normalizedPreservedGestureOffset (and/or the
PreservedGestureOffset type) to produce and return the positions array, then
replace the local positions construction in ViewportState+Gestures.swift (the
loop that builds positions using columns and gap) with a call to that helper or
use the positions from PreservedGestureOffset, ensuring callers reference the
new positions property instead of recomputing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3d7f039f-ef44-4e71-bb80-677315898204

📥 Commits

Reviewing files that changed from the base of the PR and between 4b0173e and dadd77a.

📒 Files selected for processing (13)
  • Sources/OmniWM/Core/Config/CanonicalTOMLConfig.swift
  • Sources/OmniWM/Core/Config/MonitorNiriSettings.swift
  • Sources/OmniWM/Core/Config/SettingsExport.swift
  • Sources/OmniWM/Core/Config/SettingsStore.swift
  • Sources/OmniWM/Core/Controller/MouseEventHandler.swift
  • Sources/OmniWM/Core/Layout/Niri/NiriLayoutEngine+Monitors.swift
  • Sources/OmniWM/Core/Layout/Niri/ViewportState+Gestures.swift
  • Sources/OmniWM/UI/SettingsView.swift
  • Sources/OmniWM/UI/StatusBar/StatusBarMenu.swift
  • Tests/OmniWMTests/Fixtures/canonical-settings.toml
  • Tests/OmniWMTests/NiriLayoutEngineTests.swift
  • Tests/OmniWMTests/SettingsStoreTests.swift
  • Tests/OmniWMTests/StatusBarMenuTests.swift
✅ Files skipped from review due to trivial changes (1)
  • Tests/OmniWMTests/SettingsStoreTests.swift

@Guria
Copy link
Copy Markdown
Author

Guria commented May 21, 2026

Just sharing mine local experiments on it. @BarutSRB Please feel free to decide what parts to use. Branch is open to changes from maintainers too.

@Guria Guria force-pushed the disable-niri-scroll-snap branch from dadd77a to 059226a Compare May 21, 2026 10:10
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Tests/OmniWMTests/SettingsStoreTests.swift (1)

896-931: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add assertion for niriScrollSnapEnabled to maintain test completeness.

The settingsStoreFallbackDefaultsMatchExportDefaults test verifies that SettingsStore defaults match SettingsExport defaults for each setting. The new niriScrollSnapEnabled setting should be included in this comparison to ensure the defaults remain synchronized.

Proposed fix
 `#expect`(settings.niriMaxWindowsPerColumn == exportDefaults.niriMaxWindowsPerColumn)
 `#expect`(settings.niriMaxVisibleColumns == exportDefaults.niriMaxVisibleColumns)
+#expect(settings.niriScrollSnapEnabled == exportDefaults.niriScrollSnapEnabled)
 `#expect`(settings.defaultLayoutType.rawValue == exportDefaults.defaultLayoutType)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/OmniWMTests/SettingsStoreTests.swift` around lines 896 - 931, Add an
assertion in the test function settingsStoreFallbackDefaultsMatchExportDefaults
to compare the new setting: assert that settings.niriScrollSnapEnabled ==
exportDefaults.niriScrollSnapEnabled; this keeps SettingsStore defaults in sync
with SettingsExport defaults and should be added alongside the other `#expect`
checks in the same test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Tests/OmniWMTests/SettingsStoreTests.swift`:
- Around line 896-931: Add an assertion in the test function
settingsStoreFallbackDefaultsMatchExportDefaults to compare the new setting:
assert that settings.niriScrollSnapEnabled ==
exportDefaults.niriScrollSnapEnabled; this keeps SettingsStore defaults in sync
with SettingsExport defaults and should be added alongside the other `#expect`
checks in the same test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4057d0a2-eaa8-4336-a849-a2cafa37b279

📥 Commits

Reviewing files that changed from the base of the PR and between dadd77a and 059226a.

📒 Files selected for processing (16)
  • Sources/OmniWM/Core/Config/CanonicalTOMLConfig.swift
  • Sources/OmniWM/Core/Config/MonitorNiriSettings.swift
  • Sources/OmniWM/Core/Config/SettingsExport.swift
  • Sources/OmniWM/Core/Config/SettingsStore.swift
  • Sources/OmniWM/Core/Controller/MouseEventHandler.swift
  • Sources/OmniWM/Core/Controller/NiriLayoutHandler.swift
  • Sources/OmniWM/Core/Controller/WMController.swift
  • Sources/OmniWM/Core/Layout/Niri/NiriLayoutEngine+Monitors.swift
  • Sources/OmniWM/Core/Layout/Niri/NiriLayoutEngine.swift
  • Sources/OmniWM/Core/Layout/Niri/ViewportState+Gestures.swift
  • Sources/OmniWM/UI/SettingsView.swift
  • Sources/OmniWM/UI/StatusBar/StatusBarMenu.swift
  • Tests/OmniWMTests/Fixtures/canonical-settings.toml
  • Tests/OmniWMTests/NiriLayoutEngineTests.swift
  • Tests/OmniWMTests/SettingsStoreTests.swift
  • Tests/OmniWMTests/StatusBarMenuTests.swift

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support gesture scroll without scroll snap

2 participants