From 6a1c7c0b8422e7274fcc038bd837f025666afc43 Mon Sep 17 00:00:00 2001 From: Bhupathi Reddy Date: Tue, 31 Mar 2026 21:27:46 +0530 Subject: [PATCH] Add overlay transparency with blur and slider Introduce an overlay transparency mode that uses a blurred background and adjustable tint opacity. - Add NotchBlurView (NSVisualEffectView wrapper) to provide behind-window blur. - Update NotchOverlayView and preview to render either a solid black island or a blurred, clipped island with a dark tint driven by opacity. - Add NotchSettings properties overlayTransparency and overlayTransparencyOpacity, persisted via UserDefaults (keys: "overlayTransparency", "overlayTransparencyOpacity") and initialized with sensible defaults. - Expose a Toggle and an opacity Slider in SettingsView to enable transparency and control amount; update reset defaults to include the new settings. This enables a see-through notch overlay option where desktop content shows through while keeping text readable via a configurable dark tint. --- .../Textream/NotchOverlayController.swift | 48 +++++++++++-- Textream/Textream/NotchSettings.swift | 11 +++ Textream/Textream/SettingsView.swift | 72 +++++++++++++++++-- 3 files changed, 119 insertions(+), 12 deletions(-) diff --git a/Textream/Textream/NotchOverlayController.swift b/Textream/Textream/NotchOverlayController.swift index 330d202..abf4a72 100644 --- a/Textream/Textream/NotchOverlayController.swift +++ b/Textream/Textream/NotchOverlayController.swift @@ -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 { @@ -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 { diff --git a/Textream/Textream/NotchSettings.swift b/Textream/Textream/NotchSettings.swift index 1a02c9d..40a287b 100644 --- a/Textream/Textream/NotchSettings.swift +++ b/Textream/Textream/NotchSettings.swift @@ -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") } } @@ -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") diff --git a/Textream/Textream/SettingsView.swift b/Textream/Textream/SettingsView.swift index 581526b..a7be78f 100644 --- a/Textream/Textream/SettingsView.swift +++ b/Textream/Textream/SettingsView.swift @@ -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) @@ -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 { @@ -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