Spaces:
Running
Running
File size: 6,550 Bytes
4fc4790 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | import AppKit
import OpenClawIPC
extension CanvasWindowController {
// MARK: - Window
static func makeWindow(for presentation: CanvasPresentation, contentView: NSView) -> NSWindow {
switch presentation {
case .window:
let window = NSWindow(
contentRect: NSRect(origin: .zero, size: CanvasLayout.windowSize),
styleMask: [.titled, .closable, .resizable, .miniaturizable],
backing: .buffered,
defer: false)
window.title = "OpenClaw Canvas"
window.isReleasedWhenClosed = false
window.contentView = contentView
window.center()
window.minSize = NSSize(width: 880, height: 680)
return window
case .panel:
let panel = CanvasPanel(
contentRect: NSRect(origin: .zero, size: CanvasLayout.panelSize),
styleMask: [.borderless, .resizable],
backing: .buffered,
defer: false)
// Keep Canvas below the Voice Wake overlay panel.
panel.level = NSWindow.Level(rawValue: NSWindow.Level.statusBar.rawValue - 1)
panel.hasShadow = true
panel.isMovable = false
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
panel.titleVisibility = .hidden
panel.titlebarAppearsTransparent = true
panel.backgroundColor = .clear
panel.isOpaque = false
panel.contentView = contentView
panel.becomesKeyOnlyIfNeeded = true
panel.hidesOnDeactivate = false
panel.minSize = CanvasLayout.minPanelSize
return panel
}
}
func presentAnchoredPanel(anchorProvider: @escaping () -> NSRect?) {
guard case .panel = self.presentation, let window else { return }
self.repositionPanel(using: anchorProvider)
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
window.makeFirstResponder(self.webView)
VoiceWakeOverlayController.shared.bringToFrontIfVisible()
self.onVisibilityChanged?(true)
}
func repositionPanel(using anchorProvider: () -> NSRect?) {
guard let panel = self.window else { return }
let anchor = anchorProvider()
let targetScreen = Self.screen(forAnchor: anchor)
?? Self.screenContainingMouseCursor()
?? panel.screen
?? NSScreen.main
?? NSScreen.screens.first
let restored = Self.loadRestoredFrame(sessionKey: self.sessionKey)
let restoredIsValid = if let restored, let targetScreen {
Self.isFrameMeaningfullyVisible(restored, on: targetScreen)
} else {
restored != nil
}
var frame = if let restored, restoredIsValid {
restored
} else {
Self.defaultTopRightFrame(panel: panel, screen: targetScreen)
}
// Apply agent placement as partial overrides:
// - If agent provides x/y, override origin.
// - If agent provides width/height, override size.
// - If agent provides only size, keep the remembered origin.
if let placement = self.preferredPlacement {
if let x = placement.x { frame.origin.x = x }
if let y = placement.y { frame.origin.y = y }
if let w = placement.width { frame.size.width = max(CanvasLayout.minPanelSize.width, CGFloat(w)) }
if let h = placement.height { frame.size.height = max(CanvasLayout.minPanelSize.height, CGFloat(h)) }
}
self.setPanelFrame(frame, on: targetScreen)
}
static func defaultTopRightFrame(panel: NSWindow, screen: NSScreen?) -> NSRect {
let w = max(CanvasLayout.minPanelSize.width, panel.frame.width)
let h = max(CanvasLayout.minPanelSize.height, panel.frame.height)
return WindowPlacement.topRightFrame(
size: NSSize(width: w, height: h),
padding: CanvasLayout.defaultPadding,
on: screen)
}
func setPanelFrame(_ frame: NSRect, on screen: NSScreen?) {
guard let panel = self.window else { return }
guard let s = screen ?? panel.screen ?? NSScreen.main ?? NSScreen.screens.first else {
panel.setFrame(frame, display: false)
self.persistFrameIfPanel()
return
}
let constrained = Self.constrainFrame(frame, toVisibleFrame: s.visibleFrame)
panel.setFrame(constrained, display: false)
self.persistFrameIfPanel()
}
static func screen(forAnchor anchor: NSRect?) -> NSScreen? {
guard let anchor else { return nil }
let center = NSPoint(x: anchor.midX, y: anchor.midY)
return NSScreen.screens.first { screen in
screen.frame.contains(anchor.origin) || screen.frame.contains(center)
}
}
static func screenContainingMouseCursor() -> NSScreen? {
let point = NSEvent.mouseLocation
return NSScreen.screens.first { $0.frame.contains(point) }
}
static func isFrameMeaningfullyVisible(_ frame: NSRect, on screen: NSScreen) -> Bool {
frame.intersects(screen.visibleFrame.insetBy(dx: 12, dy: 12))
}
static func constrainFrame(_ frame: NSRect, toVisibleFrame bounds: NSRect) -> NSRect {
if bounds == .zero { return frame }
var next = frame
next.size.width = min(max(CanvasLayout.minPanelSize.width, next.size.width), bounds.width)
next.size.height = min(max(CanvasLayout.minPanelSize.height, next.size.height), bounds.height)
let maxX = bounds.maxX - next.size.width
let maxY = bounds.maxY - next.size.height
next.origin.x = maxX >= bounds.minX ? min(max(next.origin.x, bounds.minX), maxX) : bounds.minX
next.origin.y = maxY >= bounds.minY ? min(max(next.origin.y, bounds.minY), maxY) : bounds.minY
next.origin.x = round(next.origin.x)
next.origin.y = round(next.origin.y)
return next
}
// MARK: - NSWindowDelegate
func windowWillClose(_: Notification) {
self.onVisibilityChanged?(false)
}
func windowDidMove(_: Notification) {
self.persistFrameIfPanel()
}
func windowDidEndLiveResize(_: Notification) {
self.persistFrameIfPanel()
}
func persistFrameIfPanel() {
guard case .panel = self.presentation, let window else { return }
Self.storeRestoredFrame(window.frame, sessionKey: self.sessionKey)
}
}
|