Skip to content

Honor screenEdgeGapTopNotch when it is set to 0 - #1911

Merged
rxhanson merged 2 commits into
rxhanson:mainfrom
efim0v:fix/1658-notch-gap-zero
Oct 16, 2026
Merged

rxhanson merged 2 commits into
rxhanson:mainfrom
efim0v:fix/1658-notch-gap-zero

Conversation

@efim0v

@efim0v efim0v commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Fixes #1658.

Problem

screenEdgeGapTopNotch was a FloatDefault, which reads UserDefaults.float(forKey:). That returns 0 when the key is missing, so a missing key and an explicit 0 looked the same. adjustedVisibleFrame used value != 0 to mean "set", so screenEdgeGapTopNotch = 0 fell back to screenEdgeGapTop on screens with a notch. With screenEdgeGapTop = 30, the only way to get no top gap on the built-in display was a tiny non-zero value such as -0.00001.

Fix

  • Add OptionalDoubleDefault, modeled on DoubleDefault. Its value is nil when the key is missing, and setting it back to nil removes the key.
  • Use it for screenEdgeGapTopNotch. On a screen with a notch, any set value, including 0, replaces screenEdgeGapTop. When it is not set, screenEdgeGapTop is used as before. Non-zero values and screens without a notch behave as before.
  • Config export writes a set value as double and an unset value as an empty entry ({}). Importing the empty entry clears the setting, so "unset" and 0 stay distinct through export and import.
  • Configs exported by earlier versions contain "screenEdgeGapTopNotch": {"float": 0} even when it was never set, so a legacy float 0 is still imported as "not set", following how DoubleDefault handles legacy float values (Allow zero to disable minimum window size limits #1802). Other legacy values are imported as they are.
  • TerminalCommands.md now describes the 0 case and how to go back to the fallback with defaults delete.

One behavior change to be aware of: importing one of those older configs used to store 0 for this key. Someone who imported such a config before this change, has screenEdgeGapTop set and uses a notched display will get no top gap on that display after updating. defaults delete com.knollsoft.Rectangle screenEdgeGapTopNotch restores the fallback.

Tests

Added ScreenEdgeGapTests. It calls adjustedVisibleFrame() on NSScreen test doubles with and without a top safe-area inset, with screenEdgeGapTop = 30:

  • notch gap unset: 30 on both screens
  • notch gap 0: 0 on the notched screen, 30 on the other. On main this fails with XCTAssertEqual failed: ("30.0") is not equal to ("0.0").
  • notch gap 10: 10 on the notched screen, 30 on the other
  • unset and 0 both survive reloading the default and a config export/import
  • a legacy {"float": 0} entry imports as not set (30 on the notched screen), and a legacy non-zero value is used

I ran the full RectangleTests bundle against a Debug build (Xcode 27.0, macOS 27.0): 572 tests, and the new tests pass. Some existing tests fail in my setup on both main and this branch, so they are unrelated to this change: ShortcutRecordingObserverTests.testRecordingAMouseButtonEndsRecording and two tests in WindowActionShortcutViewTests fail on every run, and three tests in SnappingManagerSessionTests fail in some runs (they read the real front window's full-screen state).

screenEdgeGapTopNotch was read with UserDefaults.float(forKey:), which
returns 0 for a missing key, so adjustedVisibleFrame treated 0 as "not
set" and applied screenEdgeGapTop on notched screens instead.

Store it as an optional double so an unset key still falls back to
screenEdgeGapTop, while an explicit value, including 0, is used on
screens with a notch. An unset value is exported as an empty entry and
clears the setting on import. Configs exported by earlier versions
always contain a float 0, so a legacy float 0 is still imported as not
set.

Fixes #1658
@rxhanson

Copy link
Copy Markdown
Owner

Thanks for working on this problem! I think this could also be solved by using a DoubleDefault with a default value of, say, -1 since DoubleDefault applies the default value only if the object is nil? The benefit of doing it that way is we don't have to introduce yet another default class, although it does make the usage a little awkward. If it does make more sense to keep the OptionalDoubleDefault, CGFloat and Double are the same thing, so you would simply read the double value of it.

@efim0v

efim0v commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

Good call, that's simpler. Switched to DoubleDefault with a default of -1 and dropped OptionalDoubleDefault: a negative value means it isn't set, so screenEdgeGapTop applies, and 0 now works. Older exported configs that stored 0 for this key still import as not set.

Drops OptionalDoubleDefault. A negative screenEdgeGapTopNotch means it isn't set and the regular top gap applies; 0 now removes the gap on notched screens. Legacy exports that stored 0 still import as not set.

Co-authored-by: Ryan Hanson <ryan@ryanhanson.dev>
@efim0v
efim0v force-pushed the fix/1658-notch-gap-zero branch from e3d3007 to dfbeca4 Compare October 14, 2026 22:48
@rxhanson
rxhanson merged commit 15834c9 into rxhanson:main Oct 16, 2026
1 check passed
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.

screenEdgeGapTopNotch doesn't appear to work

2 participants