Improve resizing with draggable and hotkeys

This commit is contained in:
2026-03-12 23:57:31 +11:00
parent 9d05bc586a
commit 256998eb9f
9 changed files with 517 additions and 50 deletions

View File

@@ -18,6 +18,7 @@ class HotkeyManager {
var onNextTab: (() -> Void)?
var onPreviousTab: (() -> Void)?
var onDetachTab: (() -> Void)?
var onApplySizePreset: ((TerminalSizePreset) -> Void)?
var onSwitchToTab: ((Int) -> Void)?
/// Tab-level hotkeys only fire when the notch is open.
@@ -50,6 +51,9 @@ class HotkeyManager {
private var detachBinding: HotkeyBinding {
binding(for: NotchSettings.Keys.hotkeyDetachTab) ?? .cmdD
}
private var sizePresets: [TerminalSizePreset] {
TerminalSizePresetStore.load()
}
private func binding(for key: String) -> HotkeyBinding? {
guard let json = UserDefaults.standard.string(forKey: key) else { return nil }
@@ -211,6 +215,13 @@ class HotkeyManager {
onDetachTab?()
return true
}
for preset in sizePresets {
guard let binding = preset.hotkey else { continue }
if binding.matches(event) {
onApplySizePreset?(preset)
return true
}
}
// Cmd+1 through Cmd+9
if event.modifierFlags.contains(.command) {

View File

@@ -54,6 +54,9 @@ class ScreenManager: ObservableObject {
hk.onDetachTab = { [weak self] in
MainActor.assumeIsolated { self?.detachActiveTab() }
}
hk.onApplySizePreset = { [weak self] preset in
MainActor.assumeIsolated { self?.applySizePreset(preset) }
}
hk.onSwitchToTab = { index in
MainActor.assumeIsolated { tm.switchToTab(at: index) }
}
@@ -130,6 +133,19 @@ class ScreenManager: ObservableObject {
}
}
func applySizePreset(_ preset: TerminalSizePreset) {
guard let (screenUUID, vm) = viewModels.first(where: { $0.value.notchState == .open }) else {
UserDefaults.standard.set(preset.width, forKey: NotchSettings.Keys.openWidth)
UserDefaults.standard.set(preset.height, forKey: NotchSettings.Keys.openHeight)
return
}
withAnimation(vm.openAnimation) {
vm.applySizePreset(preset, notifyWindowResize: false)
}
updateWindowFrame(for: screenUUID, centerHorizontally: true)
}
// MARK: - Window creation
func rebuildWindows() {
@@ -149,21 +165,10 @@ class ScreenManager: ObservableObject {
private func createWindow(for screen: NSScreen) {
let uuid = screen.displayUUID
let vm = NotchViewModel(screenUUID: uuid)
let shadowPadding: CGFloat = 20
let openSize = vm.openNotchSize
let windowWidth = max(openSize.width + 40, screen.frame.width * 0.5)
let windowHeight = openSize.height + shadowPadding
let windowRect = NSRect(
x: screen.frame.origin.x + (screen.frame.width - windowWidth) / 2,
y: screen.frame.origin.y + screen.frame.height - windowHeight,
width: windowWidth,
height: windowHeight
)
let initialContentSize = vm.openNotchSize
let window = NotchWindow(
contentRect: windowRect,
contentRect: NSRect(origin: .zero, size: CGSize(width: initialContentSize.width + 40, height: initialContentSize.height + 20)),
styleMask: [.borderless, .nonactivatingPanel, .utilityWindow],
backing: .buffered,
defer: false
@@ -181,19 +186,29 @@ class ScreenManager: ObservableObject {
vm.requestClose = { [weak self] in
self?.closeNotch(screenUUID: uuid)
}
vm.requestWindowResize = { [weak self] in
self?.updateWindowFrame(for: uuid, centerHorizontally: true)
}
let hostingView = NSHostingView(
rootView: ContentView(vm: vm, terminalManager: TerminalManager.shared)
.preferredColorScheme(.dark)
)
hostingView.frame = NSRect(origin: .zero, size: windowRect.size)
window.contentView = hostingView
let containerView = NSView(frame: NSRect(origin: .zero, size: window.frame.size))
containerView.autoresizesSubviews = true
containerView.wantsLayer = true
containerView.layer?.backgroundColor = NSColor.clear.cgColor
window.setFrame(windowRect, display: true)
window.orderFrontRegardless()
hostingView.frame = containerView.bounds
hostingView.autoresizingMask = [.width, .height]
containerView.addSubview(hostingView)
window.contentView = containerView
windows[uuid] = window
viewModels[uuid] = vm
updateWindowFrame(for: uuid, centerHorizontally: true)
window.orderFrontRegardless()
}
// MARK: - Repositioning
@@ -205,21 +220,44 @@ class ScreenManager: ObservableObject {
vm.refreshClosedSize()
let shadowPadding: CGFloat = 20
let openSize = vm.openNotchSize
let windowWidth = max(openSize.width + 40, screen.frame.width * 0.5)
let windowHeight = openSize.height + shadowPadding
let newFrame = NSRect(
x: screen.frame.origin.x + (screen.frame.width - windowWidth) / 2,
y: screen.frame.origin.y + screen.frame.height - windowHeight,
width: windowWidth,
height: windowHeight
)
window.setFrame(newFrame, display: true)
updateWindowFrame(for: uuid, on: screen, window: window, centerHorizontally: true)
}
}
private func updateWindowFrame(for screenUUID: String, centerHorizontally: Bool = false) {
guard let screen = NSScreen.screens.first(where: { $0.displayUUID == screenUUID }),
let window = windows[screenUUID] else { return }
updateWindowFrame(for: screenUUID, on: screen, window: window, centerHorizontally: centerHorizontally)
}
private func updateWindowFrame(
for screenUUID: String,
on screen: NSScreen,
window: NotchWindow,
centerHorizontally: Bool = false
) {
guard let vm = viewModels[screenUUID] else { return }
let shadowPadding: CGFloat = 20
let openSize = vm.openNotchSize
let windowWidth = openSize.width + 40
let windowHeight = openSize.height + shadowPadding
let centeredX = screen.frame.origin.x + (screen.frame.width - windowWidth) / 2
let x: CGFloat = centerHorizontally || vm.notchState == .closed
? centeredX
: min(max(window.frame.minX, screen.frame.minX), screen.frame.maxX - windowWidth)
let frame = NSRect(
x: x,
y: screen.frame.origin.y + screen.frame.height - windowHeight,
width: windowWidth,
height: windowHeight
)
guard !window.frame.equalTo(frame) else { return }
window.setFrame(frame, display: false)
}
// MARK: - Cleanup
private func cleanupAllWindows() {