Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 41 additions & 7 deletions Textream/Textream/NotchOverlayController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,19 @@ struct StopButtonView: View {
}
}

// MARK: - Notch Blur View (for transparency mode)

struct NotchBlurView: NSViewRepresentable {
func makeNSView(context: Context) -> NSVisualEffectView {
let v = NSVisualEffectView()
v.blendingMode = .behindWindow
v.material = .hudWindow
v.state = .active
return v
}
func updateNSView(_ nsView: NSVisualEffectView, context: Context) {}
}

// MARK: - Dynamic Island Shape (concave top corners, convex bottom corners)

struct DynamicIslandShape: Shape {
Expand Down Expand Up @@ -699,13 +712,34 @@ struct NotchOverlayView: View {
let currentWidth = notchWidth + (geo.size.width - notchWidth) * expansion

ZStack(alignment: .top) {
// Container shape
DynamicIslandShape(
topInset: currentTopInset,
bottomRadius: currentBottomRadius
)
.fill(.black)
.frame(width: currentWidth, height: currentHeight)
// Container shape — solid black or transparent with blur
let isTransparent = NotchSettings.shared.overlayTransparency
let transparencyOpacity = NotchSettings.shared.overlayTransparencyOpacity

if isTransparent {
// Blurred background layer clipped to the Dynamic Island shape
NotchBlurView()
.clipShape(DynamicIslandShape(
topInset: currentTopInset,
bottomRadius: currentBottomRadius
))
.frame(width: currentWidth, height: currentHeight)

// Dark tint overlay so text remains readable
DynamicIslandShape(
topInset: currentTopInset,
bottomRadius: currentBottomRadius
)
.fill(.black.opacity(1.0 - transparencyOpacity))
.frame(width: currentWidth, height: currentHeight)
} else {
DynamicIslandShape(
topInset: currentTopInset,
bottomRadius: currentBottomRadius
)
.fill(.black)
.frame(width: currentWidth, height: currentHeight)
}

// Content - appears after container expands
if contentVisible {
Expand Down
11 changes: 11 additions & 0 deletions Textream/Textream/NotchSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,14 @@ class NotchSettings {
didSet { UserDefaults.standard.set(glassOpacity, forKey: "glassOpacity") }
}

var overlayTransparency: Bool {
didSet { UserDefaults.standard.set(overlayTransparency, forKey: "overlayTransparency") }
}

var overlayTransparencyOpacity: Double {
didSet { UserDefaults.standard.set(overlayTransparencyOpacity, forKey: "overlayTransparencyOpacity") }
}

var followCursorWhenUndocked: Bool {
didSet { UserDefaults.standard.set(followCursorWhenUndocked, forKey: "followCursorWhenUndocked") }
}
Expand Down Expand Up @@ -472,6 +480,9 @@ class NotchSettings {
self.floatingGlassEffect = UserDefaults.standard.object(forKey: "floatingGlassEffect") as? Bool ?? false
let savedOpacity = UserDefaults.standard.double(forKey: "glassOpacity")
self.glassOpacity = savedOpacity > 0 ? savedOpacity : 0.15
self.overlayTransparency = UserDefaults.standard.object(forKey: "overlayTransparency") as? Bool ?? false
let savedTransparencyOpacity = UserDefaults.standard.double(forKey: "overlayTransparencyOpacity")
self.overlayTransparencyOpacity = savedTransparencyOpacity > 0 ? savedTransparencyOpacity : 0.85
self.followCursorWhenUndocked = UserDefaults.standard.object(forKey: "followCursorWhenUndocked") as? Bool ?? false
self.externalDisplayMode = ExternalDisplayMode(rawValue: UserDefaults.standard.string(forKey: "externalDisplayMode") ?? "") ?? .off
let savedScreenID = UserDefaults.standard.integer(forKey: "externalScreenID")
Expand Down
72 changes: 67 additions & 5 deletions Textream/Textream/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,11 +159,29 @@ struct NotchPreviewContent: View {

ZStack(alignment: .top) {
// Shape: concave corners flatten via cornerPhase, then cross-fade to rounded via offsetPhase
DynamicIslandShape(
topInset: 16 * (1 - cornerPhase),
bottomRadius: 18
)
.fill(.black)
let isTransparent = settings.overlayTransparency && settings.overlayMode == .pinned
Group {
if isTransparent {
ZStack {
NotchBlurView()
DynamicIslandShape(
topInset: 16 * (1 - cornerPhase),
bottomRadius: 18
)
.fill(.black.opacity(1.0 - settings.overlayTransparencyOpacity))
}
.clipShape(DynamicIslandShape(
topInset: 16 * (1 - cornerPhase),
bottomRadius: 18
))
} else {
DynamicIslandShape(
topInset: 16 * (1 - cornerPhase),
bottomRadius: 18
)
.fill(.black)
}
}
.opacity(Double(1 - offsetPhase))
.frame(width: currentWidth, height: contentHeight)

Expand Down Expand Up @@ -796,6 +814,48 @@ struct SettingsView: View {
onRefresh: { refreshOverlayScreens() }
)
}

Divider()

Toggle(isOn: $settings.overlayTransparency) {
Text("Transparency")
.font(.system(size: 13, weight: .medium))
}
.toggleStyle(.switch)
.controlSize(.small)

Text("Makes the overlay see-through so desktop content shows through.")
.font(.system(size: 11))
.foregroundStyle(.secondary)

if settings.overlayTransparency {
VStack(alignment: .leading, spacing: 6) {
HStack {
Text("Amount")
.font(.system(size: 12))
.foregroundStyle(.secondary)
Spacer()
Text("\(Int(settings.overlayTransparencyOpacity * 100))%")
.font(.system(size: 11, weight: .regular, design: .monospaced))
.foregroundStyle(.tertiary)
}
Slider(
value: $settings.overlayTransparencyOpacity,
in: 0.2...0.95,
step: 0.05
)
HStack {
Text("More transparent")
.font(.system(size: 10))
.foregroundStyle(.tertiary)
Spacer()
Text("Less transparent")
.font(.system(size: 10))
.foregroundStyle(.tertiary)
}
}
.transition(.opacity.combined(with: .move(edge: .top)))
}
}

if settings.overlayMode == .floating {
Expand Down Expand Up @@ -1328,6 +1388,8 @@ struct SettingsView: View {
settings.pinnedScreenID = 0
settings.floatingGlassEffect = false
settings.glassOpacity = 0.15
settings.overlayTransparency = false
settings.overlayTransparencyOpacity = 0.85
settings.followCursorWhenUndocked = false
settings.fullscreenScreenID = 0
settings.externalDisplayMode = .off
Expand Down