fix(macos): sane chat window placement
This commit is contained in:
@@ -163,7 +163,14 @@ final class WebChatSwiftUIWindowController {
|
|||||||
|
|
||||||
private func reposition(using anchorProvider: () -> NSRect?) {
|
private func reposition(using anchorProvider: () -> NSRect?) {
|
||||||
guard let window else { return }
|
guard let window else { return }
|
||||||
guard let anchor = anchorProvider() else { return }
|
guard let anchor = anchorProvider() else {
|
||||||
|
window.setFrame(
|
||||||
|
WindowPlacement.topRightFrame(
|
||||||
|
size: WebChatSwiftUILayout.panelSize,
|
||||||
|
padding: WebChatSwiftUILayout.anchorPadding),
|
||||||
|
display: false)
|
||||||
|
return
|
||||||
|
}
|
||||||
let screen = NSScreen.screens.first { screen in
|
let screen = NSScreen.screens.first { screen in
|
||||||
screen.frame.contains(anchor.origin) || screen.frame.contains(NSPoint(x: anchor.midX, y: anchor.midY))
|
screen.frame.contains(anchor.origin) || screen.frame.contains(NSPoint(x: anchor.midX, y: anchor.midY))
|
||||||
} ?? NSScreen.main
|
} ?? NSScreen.main
|
||||||
@@ -227,6 +234,7 @@ final class WebChatSwiftUIWindowController {
|
|||||||
window.backgroundColor = .windowBackgroundColor
|
window.backgroundColor = .windowBackgroundColor
|
||||||
window.isOpaque = true
|
window.isOpaque = true
|
||||||
window.center()
|
window.center()
|
||||||
|
WindowPlacement.ensureOnScreen(window: window, defaultSize: WebChatSwiftUILayout.windowSize)
|
||||||
window.minSize = NSSize(width: 880, height: 680)
|
window.minSize = NSSize(width: 880, height: 680)
|
||||||
return window
|
return window
|
||||||
case .panel:
|
case .panel:
|
||||||
@@ -246,6 +254,11 @@ final class WebChatSwiftUIWindowController {
|
|||||||
panel.isOpaque = false
|
panel.isOpaque = false
|
||||||
panel.contentViewController = contentViewController
|
panel.contentViewController = contentViewController
|
||||||
panel.becomesKeyOnlyIfNeeded = true
|
panel.becomesKeyOnlyIfNeeded = true
|
||||||
|
panel.setFrame(
|
||||||
|
WindowPlacement.topRightFrame(
|
||||||
|
size: WebChatSwiftUILayout.panelSize,
|
||||||
|
padding: WebChatSwiftUILayout.anchorPadding),
|
||||||
|
display: false)
|
||||||
return panel
|
return panel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N
|
|||||||
window.title = "Clawd Web Chat"
|
window.title = "Clawd Web Chat"
|
||||||
window.contentView = wrappedContent
|
window.contentView = wrappedContent
|
||||||
window.center()
|
window.center()
|
||||||
|
WindowPlacement.ensureOnScreen(window: window, defaultSize: WebChatLayout.windowSize)
|
||||||
window.minSize = NSSize(width: 880, height: 680)
|
window.minSize = NSSize(width: 880, height: 680)
|
||||||
return window
|
return window
|
||||||
case .panel:
|
case .panel:
|
||||||
@@ -110,6 +111,11 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N
|
|||||||
panel.isOpaque = false
|
panel.isOpaque = false
|
||||||
panel.contentView = wrappedContent
|
panel.contentView = wrappedContent
|
||||||
panel.becomesKeyOnlyIfNeeded = true
|
panel.becomesKeyOnlyIfNeeded = true
|
||||||
|
panel.setFrame(
|
||||||
|
WindowPlacement.topRightFrame(
|
||||||
|
size: WebChatLayout.panelSize,
|
||||||
|
padding: WebChatLayout.anchorPadding),
|
||||||
|
display: false)
|
||||||
return panel
|
return panel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,7 +345,14 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N
|
|||||||
|
|
||||||
private func repositionPanel(using anchorProvider: () -> NSRect?) {
|
private func repositionPanel(using anchorProvider: () -> NSRect?) {
|
||||||
guard let panel = self.window else { return }
|
guard let panel = self.window else { return }
|
||||||
guard let anchor = anchorProvider() else { return }
|
guard let anchor = anchorProvider() else {
|
||||||
|
panel.setFrame(
|
||||||
|
WindowPlacement.topRightFrame(
|
||||||
|
size: WebChatLayout.panelSize,
|
||||||
|
padding: WebChatLayout.anchorPadding),
|
||||||
|
display: false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var frame = panel.frame
|
var frame = panel.frame
|
||||||
let screen = NSScreen.screens.first { screen in
|
let screen = NSScreen.screens.first { screen in
|
||||||
|
|||||||
54
apps/macos/Sources/Clawdis/WindowPlacement.swift
Normal file
54
apps/macos/Sources/Clawdis/WindowPlacement.swift
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import AppKit
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
enum WindowPlacement {
|
||||||
|
static func centeredFrame(size: NSSize, on screen: NSScreen? = NSScreen.main) -> NSRect {
|
||||||
|
let bounds = (screen?.visibleFrame ?? NSScreen.screens.first?.visibleFrame ?? .zero)
|
||||||
|
if bounds == .zero {
|
||||||
|
return NSRect(origin: .zero, size: size)
|
||||||
|
}
|
||||||
|
|
||||||
|
let clampedWidth = min(size.width, bounds.width)
|
||||||
|
let clampedHeight = min(size.height, bounds.height)
|
||||||
|
|
||||||
|
let x = round(bounds.minX + (bounds.width - clampedWidth) / 2)
|
||||||
|
let y = round(bounds.minY + (bounds.height - clampedHeight) / 2)
|
||||||
|
return NSRect(x: x, y: y, width: clampedWidth, height: clampedHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func topRightFrame(
|
||||||
|
size: NSSize,
|
||||||
|
padding: CGFloat,
|
||||||
|
on screen: NSScreen? = NSScreen.main) -> NSRect
|
||||||
|
{
|
||||||
|
let bounds = (screen?.visibleFrame ?? NSScreen.screens.first?.visibleFrame ?? .zero)
|
||||||
|
if bounds == .zero {
|
||||||
|
return NSRect(origin: .zero, size: size)
|
||||||
|
}
|
||||||
|
|
||||||
|
let clampedWidth = min(size.width, bounds.width)
|
||||||
|
let clampedHeight = min(size.height, bounds.height)
|
||||||
|
|
||||||
|
let x = round(bounds.maxX - clampedWidth - padding)
|
||||||
|
let y = round(bounds.maxY - clampedHeight - padding)
|
||||||
|
return NSRect(x: x, y: y, width: clampedWidth, height: clampedHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ensureOnScreen(
|
||||||
|
window: NSWindow,
|
||||||
|
defaultSize: NSSize,
|
||||||
|
fallback: ((NSScreen?) -> NSRect)? = nil)
|
||||||
|
{
|
||||||
|
let frame = window.frame
|
||||||
|
let targetScreens = NSScreen.screens.isEmpty ? [NSScreen.main].compactMap { $0 } : NSScreen.screens
|
||||||
|
let isVisibleSomewhere = targetScreens.contains { screen in
|
||||||
|
frame.intersects(screen.visibleFrame.insetBy(dx: 12, dy: 12))
|
||||||
|
}
|
||||||
|
|
||||||
|
if isVisibleSomewhere { return }
|
||||||
|
|
||||||
|
let screen = NSScreen.main ?? targetScreens.first
|
||||||
|
let next = fallback?(screen) ?? centeredFrame(size: defaultSize, on: screen)
|
||||||
|
window.setFrame(next, display: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user