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
43 changes: 42 additions & 1 deletion Textream/Textream/NotchOverlayController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,11 @@ class NotchOverlayController: NSObject {

let xPosition = screenFrame.midX - panelWidth / 2
let yPosition = screenFrame.midY - panelHeight / 2 + 100
let defaultFrame = NSRect(x: xPosition, y: yPosition, width: panelWidth, height: panelHeight)

// The panel opens at the size and position the window was last left at, so
// the content lays out against its final height right away
let initialFrame = restorableFloatingFrame(settings.floatingWindowFrame) ?? defaultFrame

let floatingView = FloatingOverlayView(
content: overlayContent,
Expand All @@ -359,7 +364,7 @@ class NotchOverlayController: NSObject {
let contentView = NSHostingView(rootView: floatingView)

let panel = NSPanel(
contentRect: NSRect(x: xPosition, y: yPosition, width: panelWidth, height: panelHeight),
contentRect: initialFrame,
styleMask: [.borderless, .nonactivatingPanel, .resizable],
backing: .buffered,
defer: false
Expand All @@ -379,9 +384,45 @@ class NotchOverlayController: NSObject {
panel.orderFrontRegardless()
self.panel = panel

// Observed after the panel is on screen so the notifications posted while
// it is first placed are not mistaken for user moves
observeFloatingFrame(panel)

installKeyMonitor()
}

/// The saved size is restored as-is, but the origin is nudged back onto the
/// display so no edge ends up past it. The overlay draws above the menu bar,
/// so the full screen frame is used and the window can sit flush with the top
/// of the display. A frame whose center no longer lands on a connected display
/// is discarded, so the window cannot return off-screen after a monitor is
/// disconnected.
private func restorableFloatingFrame(_ frame: NSRect?) -> NSRect? {
guard var frame = frame, frame.width > 0, frame.height > 0 else { return nil }
let center = NSPoint(x: frame.midX, y: frame.midY)
guard let screen = NSScreen.screens.first(where: { NSMouseInRect(center, $0.frame, false) }) else { return nil }

// Windows larger than the display keep their size and overflow the bottom
// right, so the top edge stays within reach.
let bounds = screen.frame
frame.origin.x = frame.width >= bounds.width
? bounds.minX
: min(max(frame.minX, bounds.minX), bounds.maxX - frame.width)
frame.origin.y = min(max(frame.minY, bounds.minY), bounds.maxY - frame.height)
return frame
}

private func observeFloatingFrame(_ panel: NSPanel) {
for name in [NSWindow.didMoveNotification, NSWindow.didResizeNotification] {
NotificationCenter.default.publisher(for: name, object: panel)
.sink { notification in
guard let window = notification.object as? NSWindow else { return }
NotchSettings.shared.floatingWindowFrame = window.frame
}
.store(in: &cancellables)
}
}

func dismiss() {
guard !isDismissing else { return }
isDismissing = true
Expand Down
14 changes: 14 additions & 0 deletions Textream/Textream/NotchSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,18 @@ class NotchSettings {
didSet { UserDefaults.standard.set(Int(pinnedScreenID), forKey: "pinnedScreenID") }
}

/// Not observed: nothing renders from this, and it is written on every drag
/// and resize step, which would otherwise redraw the overlay continuously.
@ObservationIgnored var floatingWindowFrame: NSRect? {
didSet {
if let floatingWindowFrame {
UserDefaults.standard.set(NSStringFromRect(floatingWindowFrame), forKey: "floatingWindowFrame")
} else {
UserDefaults.standard.removeObject(forKey: "floatingWindowFrame")
}
}
}

var floatingGlassEffect: Bool {
didSet { UserDefaults.standard.set(floatingGlassEffect, forKey: "floatingGlassEffect") }
}
Expand Down Expand Up @@ -480,6 +492,8 @@ class NotchSettings {
self.notchDisplayMode = NotchDisplayMode(rawValue: UserDefaults.standard.string(forKey: "notchDisplayMode") ?? "") ?? .followMouse
let savedPinnedScreenID = UserDefaults.standard.integer(forKey: "pinnedScreenID")
self.pinnedScreenID = UInt32(savedPinnedScreenID)
let savedFloatingFrame = NSRectFromString(UserDefaults.standard.string(forKey: "floatingWindowFrame") ?? "")
self.floatingWindowFrame = savedFloatingFrame.width > 0 && savedFloatingFrame.height > 0 ? savedFloatingFrame : nil
self.floatingGlassEffect = UserDefaults.standard.object(forKey: "floatingGlassEffect") as? Bool ?? false
let savedOpacity = UserDefaults.standard.double(forKey: "glassOpacity")
self.glassOpacity = savedOpacity > 0 ? savedOpacity : 0.15
Expand Down
1 change: 1 addition & 0 deletions Textream/Textream/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1407,6 +1407,7 @@ struct SettingsView: View {
settings.overlayMode = .pinned
settings.notchDisplayMode = .followMouse
settings.pinnedScreenID = 0
settings.floatingWindowFrame = nil
settings.floatingGlassEffect = false
settings.glassOpacity = 0.15
settings.overlayTransparency = false
Expand Down