Skip to content

Commit 138fbf7

Browse files
committed
fix: crisp glyphs on translucent background
1 parent 3201720 commit 138fbf7

4 files changed

Lines changed: 63 additions & 25 deletions

File tree

Notchy/PanelContentView.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,13 @@ struct PanelContentView: View {
101101
}
102102

103103
/// When expanded + unfocused, make chrome backgrounds semi-transparent.
104-
/// Always scaled by the user's panel opacity setting.
104+
/// Scaled by the layered share of the user's opacity setting — the
105+
/// terminal cell fills contribute the other layer, so tint × fills
106+
/// composes to exactly the requested opacity over the terminal area
107+
/// (see TerminalManager.layeredPanelAlpha).
105108
private var chromeBackgroundOpacity: Double {
106109
let base = (!sessionStore.isWindowFocused && sessionStore.isTerminalExpanded) ? 0.5 : 1.0
107-
return base * sessionStore.panelOpacity
110+
return base * (1 - (1 - sessionStore.panelOpacity).squareRoot())
108111
}
109112

110113
/// Perceived luminance of the theme background — drives the blur's

Notchy/SessionStore.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class SessionStore {
5858
}() {
5959
didSet {
6060
UserDefaults.standard.set(panelOpacity, forKey: "panelOpacity")
61+
TerminalManager.shared.refreshPanelOpacity()
6162
}
6263
}
6364
/// Blur intensity (0–1) applied behind the panel while transparency is

Notchy/TerminalManager.swift

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1687,34 +1687,51 @@ class TerminalManager: NSObject, LocalProcessTerminalViewDelegate {
16871687
TerminalTheme.theme(forId: currentThemeId)
16881688
}
16891689

1690+
/// The panel's translucency is split across two stacked layers — the
1691+
/// SwiftUI theme tint below and the terminal's per-cell fills above —
1692+
/// each at 1−√(1−opacity) so the composite matches the user's setting.
1693+
/// The cell fills matter for text quality: SwiftTerm hard-enables font
1694+
/// smoothing per glyph, and glyphs rasterized against a clear context
1695+
/// get noisy fringed edges; a real fill color in the same context keeps
1696+
/// them clean. At opacity 1 this resolves to the fully opaque theme
1697+
/// background (crispest possible rendering).
1698+
static func layeredPanelAlpha() -> CGFloat {
1699+
let stored = UserDefaults.standard.object(forKey: "panelOpacity") as? Double
1700+
let opacity = min(max(stored ?? 1.0, 0.5), 1.0)
1701+
return CGFloat(1 - (1 - opacity).squareRoot())
1702+
}
1703+
16901704
private func applyTheme(to terminal: LocalProcessTerminalView) {
16911705
let theme = currentTheme
1692-
// Fully transparent terminal background: SwiftTerm paints its
1693-
// background three times (layer + per-run fill + gap fill), which
1694-
// double-composites any translucent color. With .clear all three are
1695-
// no-ops and the panel's SwiftUI background (theme color scaled by
1696-
// the user's transparency setting) is the single visible backdrop.
1697-
terminal.nativeBackgroundColor = .clear
1698-
// The setter above does NOT touch the view's CALayer — SwiftTerm only
1699-
// writes layer.backgroundColor during setupOptions() (init/font
1700-
// changes), so the layer keeps the opaque default until cleared here.
1701-
terminal.layer?.backgroundColor = NSColor.clear.cgColor
1706+
let alpha = Self.layeredPanelAlpha()
1707+
if alpha >= 1 {
1708+
terminal.nativeBackgroundColor = theme.background
1709+
terminal.layer?.backgroundColor = theme.background.cgColor
1710+
} else {
1711+
terminal.nativeBackgroundColor = theme.background.withAlphaComponent(alpha)
1712+
// Keep the CALayer clear: SwiftTerm's setter never updates it, and
1713+
// a colored layer would add a third composite on top of the fills.
1714+
terminal.layer?.backgroundColor = NSColor.clear.cgColor
1715+
}
17021716
terminal.nativeForegroundColor = theme.foreground
17031717
terminal.caretColor = theme.cursor
17041718
terminal.selectedTextBackgroundColor = theme.selection
17051719
terminal.installColors(theme.swiftTermColors())
17061720
}
17071721

1722+
/// Re-applies theme backgrounds when the transparency slider moves.
1723+
func refreshPanelOpacity() {
1724+
for terminal in terminals.values {
1725+
applyTheme(to: terminal)
1726+
terminal.setNeedsDisplay(terminal.bounds)
1727+
}
1728+
}
1729+
17081730
func setTheme(_ themeId: String) {
17091731
UserDefaults.standard.set(themeId, forKey: Self.themeKey)
17101732
let theme = TerminalTheme.theme(forId: themeId)
17111733
for terminal in terminals.values {
1712-
terminal.nativeBackgroundColor = .clear
1713-
terminal.layer?.backgroundColor = NSColor.clear.cgColor
1714-
terminal.nativeForegroundColor = theme.foreground
1715-
terminal.caretColor = theme.cursor
1716-
terminal.selectedTextBackgroundColor = theme.selection
1717-
terminal.installColors(theme.swiftTermColors())
1734+
applyTheme(to: terminal)
17181735
terminal.setNeedsDisplay(terminal.bounds)
17191736
}
17201737
Task { @MainActor in

Notchy/TerminalSessionView.swift

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,25 @@ import SwiftTerm
33

44
private class ClickThroughContainerView: NSView {
55
override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true }
6+
7+
// SwiftTerm derives every line's baseline from frame.height; SwiftUI
8+
// hands us fractional heights, which puts all glyph baselines off the
9+
// physical pixel grid and renders text slightly blurry. Size the
10+
// terminal manually, snapped to whole physical pixels.
11+
override func layout() {
12+
super.layout()
13+
guard let terminal = subviews.first else { return }
14+
let scale = window?.backingScaleFactor ?? 2
15+
let snapped = NSRect(
16+
x: 0,
17+
y: 0,
18+
width: floor(bounds.width * scale) / scale,
19+
height: floor(bounds.height * scale) / scale
20+
)
21+
if terminal.frame != snapped {
22+
terminal.frame = snapped
23+
}
24+
}
625
}
726

827
struct TerminalSessionView: NSViewRepresentable {
@@ -43,14 +62,12 @@ struct TerminalSessionView: NSViewRepresentable {
4362
// Remove from previous superview if it was in a different container
4463
terminal.removeFromSuperview()
4564

46-
terminal.translatesAutoresizingMaskIntoConstraints = false
65+
// Manual layout (see ClickThroughContainerView.layout) instead of
66+
// edge-anchored constraints, so the frame can pixel-snap.
67+
terminal.translatesAutoresizingMaskIntoConstraints = true
68+
terminal.autoresizingMask = []
4769
container.addSubview(terminal)
48-
NSLayoutConstraint.activate([
49-
terminal.leadingAnchor.constraint(equalTo: container.leadingAnchor),
50-
terminal.trailingAnchor.constraint(equalTo: container.trailingAnchor),
51-
terminal.topAnchor.constraint(equalTo: container.topAnchor),
52-
terminal.bottomAnchor.constraint(equalTo: container.bottomAnchor),
53-
])
70+
container.needsLayout = true
5471

5572
// Give terminal keyboard focus
5673
DispatchQueue.main.async {

0 commit comments

Comments
 (0)