Skip to content

fix(macos): hide the scaled-UI hairlines; scene marks over the selection outline - #1477

Merged
marcinz606 merged 4 commits into
marcinz606:mainfrom
kurtwindisch:fix/macos-scaled-ui-hairlines
Oct 22, 2026
Merged

marcinz606 merged 4 commits into
marcinz606:mainfrom
kurtwindisch:fix/macos-scaled-ui-hairlines

Conversation

@kurtwindisch

Copy link
Copy Markdown
Contributor

Summary

A partial fix for #1475. At a UI scale below 100% on macOS, white hairlines show around controls after they repaint. This PR hides the white and leaves the cause open. The options for a full fix are in #1475.

It also fixes the Film Strip scene marks, which the red selection outline covered.

Hairlines at a UI scale below 100%

  • At a fractional device pixel ratio, a partial repaint on macOS leaves some device pixels unpainted. They show the NSWindow background, which AppKit makes light gray.
  • Each shown window now takes THEME.bg_dark as its NSWindow background (negpy/desktop/view/mac_window.py). The gaps show that dark color instead of white.
  • A translucent window, such as the tutorial overlay, keeps its clear background.
  • The background is set once per native window. The app event filter passes each Show event, and the function checks the platform and that the widget is a top-level window.
  • The photo canvas still draws at true screen pixels, and repaint cost does not change.

What it does not fix:

  • The gaps are still there. On surfaces lighter than bg_dark, such as a card (#121212) or a header (#161616), a faint dark line can still show.
  • The gap pixels show a few steps lighter than the color set (32 shows as 37 in a test window). The device color space gives the same result, so the sRGB color stays.
  • An alternate color theme must update this fix. The module docstring and a comment at bg_dark in theme.py say so.

Scene marks on the Film Strip and the Light Table

  • The selection outline painted over a scene's line and the bottom of its name chip. When the selected frame was the first of a scene, only a cut-off blue bar showed.
  • The view now paints a scene's line across each run of its frames, then its name chip, after the selection outlines.
  • The outline's two faded outer rings reach 1 to 2 pixels above the scene line, so a thin red edge still shows above it on a selected frame.

Tests

  • tests/test_light_table.py: the scene line shows on a selected frame, and the outline still frames its sides.
  • make all passes.
  • Checked in the app on macOS at 90% UI scale, with a sandboxed user directory, a test negative and the Exposure tab:
    • The main window and a dialog read back #0D0D0D as their NSWindow background, and a translucent window is not changed.
    • After repeated slider updates, the stray pixels are dark (16 to 18), not white.
    • A slider update takes about 4 ms, as before.

Open questions

🤖 Generated with Claude Code

kurtwindisch and others added 3 commits October 21, 2026 18:03
At a UI scale below 100% on a Retina display the device pixel ratio is
fractional (1.8 at 90%, 1.6 at 80%). On a partial repaint the macOS
backing store leaves device pixels unpainted between the repainted and
the kept regions, and they show the light gray NSWindow background as
white hairlines around controls. A plain PyQt6 window at 80% showed
about 3,400 such pixels, 0 at 100%.

Each shown window now takes THEME.bg_dark as its NSWindow background,
so the gaps blend into the panels. This is a mitigation, not a fix of
the gaps: alternatives and their costs are in marcinz606#1475. A translucent
window keeps its clear background.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The selection mat painted after the scene chip and over each frame's
scene line, so a selected first frame hid the bottom of the scene name
and broke the line. The view now paints a scene's line across each run
of its frames, then its name chip, after the mats.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The background is set once per native window instead of on every Show,
so tooltips and menus do not repeat the Objective-C calls. Each
objc_msgSend signature gets its own ctypes prototype instead of
rebinding argtypes on one shared function. The platform and top-level
checks live only in paint_window_background, and the module docstring
fits the four-line budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…airlines

# Conflicts:
#	negpy/desktop/view/sidebar/files.py
@marcinz606
marcinz606 merged commit 4f15ae6 into marcinz606:main Oct 22, 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.

2 participants