Yep. AI rewrote the whole thing.
This commit is contained in:
@@ -9,25 +9,11 @@ import SwiftTerm
|
||||
/// layering, no mismatched areas.
|
||||
struct ContentView: View {
|
||||
|
||||
@ObservedObject var vm: NotchViewModel
|
||||
@ObservedObject var terminalManager: TerminalManager
|
||||
@ObservedObject var screen: ScreenContext
|
||||
let orchestrator: NotchOrchestrator
|
||||
@ObservedObject private var settingsController = AppSettingsController.shared
|
||||
@ObservedObject private var screenRegistry = ScreenRegistry.shared
|
||||
|
||||
// MARK: - Settings
|
||||
|
||||
@AppStorage(NotchSettings.Keys.openNotchOnHover) private var openNotchOnHover = NotchSettings.Defaults.openNotchOnHover
|
||||
@AppStorage(NotchSettings.Keys.minimumHoverDuration) private var minimumHoverDuration = NotchSettings.Defaults.minimumHoverDuration
|
||||
|
||||
@AppStorage(NotchSettings.Keys.enableShadow) private var enableShadow = NotchSettings.Defaults.enableShadow
|
||||
@AppStorage(NotchSettings.Keys.shadowRadius) private var shadowRadius = NotchSettings.Defaults.shadowRadius
|
||||
@AppStorage(NotchSettings.Keys.shadowOpacity) private var shadowOpacity = NotchSettings.Defaults.shadowOpacity
|
||||
@AppStorage(NotchSettings.Keys.cornerRadiusScaling) private var cornerRadiusScaling = NotchSettings.Defaults.cornerRadiusScaling
|
||||
@AppStorage(NotchSettings.Keys.notchOpacity) private var notchOpacity = NotchSettings.Defaults.notchOpacity
|
||||
@AppStorage(NotchSettings.Keys.blurRadius) private var blurRadius = NotchSettings.Defaults.blurRadius
|
||||
|
||||
@AppStorage(NotchSettings.Keys.hoverSpringResponse) private var hoverSpringResponse = NotchSettings.Defaults.hoverSpringResponse
|
||||
@AppStorage(NotchSettings.Keys.hoverSpringDamping) private var hoverSpringDamping = NotchSettings.Defaults.hoverSpringDamping
|
||||
|
||||
@State private var hoverTask: Task<Void, Never>?
|
||||
@State private var resizeStartSize: CGSize?
|
||||
@State private var resizeStartMouseLocation: CGPoint?
|
||||
|
||||
@@ -36,18 +22,51 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
private var currentShape: NotchShape {
|
||||
vm.notchState == .open
|
||||
screen.notchState == .open
|
||||
? (cornerRadiusScaling ? .opened : NotchShape(topCornerRadius: 0, bottomCornerRadius: 14))
|
||||
: .closed
|
||||
}
|
||||
|
||||
private var enableShadow: Bool {
|
||||
settingsController.settings.appearance.enableShadow
|
||||
}
|
||||
|
||||
private var shadowRadius: Double {
|
||||
settingsController.settings.appearance.shadowRadius
|
||||
}
|
||||
|
||||
private var shadowOpacity: Double {
|
||||
settingsController.settings.appearance.shadowOpacity
|
||||
}
|
||||
|
||||
private var cornerRadiusScaling: Bool {
|
||||
settingsController.settings.appearance.cornerRadiusScaling
|
||||
}
|
||||
|
||||
private var notchOpacity: Double {
|
||||
settingsController.settings.appearance.notchOpacity
|
||||
}
|
||||
|
||||
private var blurRadius: Double {
|
||||
settingsController.settings.appearance.blurRadius
|
||||
}
|
||||
|
||||
private var hoverSpringResponse: Double {
|
||||
settingsController.settings.animation.hoverSpringResponse
|
||||
}
|
||||
|
||||
private var hoverSpringDamping: Double {
|
||||
settingsController.settings.animation.hoverSpringDamping
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
notchBody
|
||||
.accessibilityIdentifier("notch.container")
|
||||
.frame(
|
||||
width: vm.notchSize.width,
|
||||
height: vm.notchState == .open ? vm.notchSize.height : vm.closedNotchSize.height,
|
||||
width: screen.notchSize.width,
|
||||
height: screen.notchState == .open ? screen.notchSize.height : screen.closedNotchSize.height,
|
||||
alignment: .top
|
||||
)
|
||||
.background(.black)
|
||||
@@ -56,7 +75,7 @@ struct ContentView: View {
|
||||
Rectangle().fill(.black).frame(height: 1)
|
||||
}
|
||||
.overlay(alignment: .bottomTrailing) {
|
||||
if vm.notchState == .open {
|
||||
if screen.notchState == .open {
|
||||
resizeHandle
|
||||
}
|
||||
}
|
||||
@@ -68,22 +87,15 @@ struct ContentView: View {
|
||||
// so this one modifier makes it all uniformly transparent.
|
||||
.opacity(notchOpacity)
|
||||
.blur(radius: blurRadius)
|
||||
.animation(vm.notchState == .open ? vm.openAnimation : vm.closeAnimation, value: vm.notchState)
|
||||
.animation(sizeAnimation, value: vm.notchSize.width)
|
||||
.animation(sizeAnimation, value: vm.notchSize.height)
|
||||
.animation(screen.notchState == .open ? screen.openAnimation : screen.closeAnimation, value: screen.notchState)
|
||||
.animation(sizeAnimation, value: screen.notchSize.width)
|
||||
.animation(sizeAnimation, value: screen.notchSize.height)
|
||||
.onHover { handleHover($0) }
|
||||
.onChange(of: vm.isCloseTransitionActive) { _, isClosing in
|
||||
if isClosing {
|
||||
hoverTask?.cancel()
|
||||
} else {
|
||||
scheduleHoverOpenIfNeeded()
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
hoverTask?.cancel()
|
||||
resizeStartSize = nil
|
||||
resizeStartMouseLocation = nil
|
||||
vm.endInteractiveResize()
|
||||
screen.endInteractiveResize()
|
||||
orchestrator.handleHoverChange(false, for: screen.id)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
@@ -93,18 +105,20 @@ struct ContentView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var notchBody: some View {
|
||||
if vm.notchState == .open {
|
||||
openContent
|
||||
.transition(.opacity)
|
||||
} else {
|
||||
closedContent
|
||||
WorkspaceScopedView(screen: screen, screenRegistry: screenRegistry) { workspace in
|
||||
if screen.notchState == .open {
|
||||
openContent(workspace: workspace)
|
||||
.transition(.opacity)
|
||||
} else {
|
||||
closedContent(workspace: workspace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var closedContent: some View {
|
||||
private func closedContent(workspace: WorkspaceController) -> some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(abbreviate(terminalManager.activeTitle))
|
||||
Text(abbreviate(workspace.activeTitle))
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.foregroundStyle(.white.opacity(0.7))
|
||||
.lineLimit(1)
|
||||
@@ -128,15 +142,15 @@ struct ContentView: View {
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { value in
|
||||
if resizeStartSize == nil {
|
||||
resizeStartSize = vm.notchSize
|
||||
resizeStartSize = screen.notchSize
|
||||
resizeStartMouseLocation = NSEvent.mouseLocation
|
||||
vm.beginInteractiveResize()
|
||||
screen.beginInteractiveResize()
|
||||
}
|
||||
|
||||
guard let startSize = resizeStartSize,
|
||||
let startMouseLocation = resizeStartMouseLocation else { return }
|
||||
let currentMouseLocation = NSEvent.mouseLocation
|
||||
vm.resizeOpenNotch(
|
||||
screen.resizeOpenNotch(
|
||||
to: CGSize(
|
||||
width: startSize.width + ((currentMouseLocation.x - startMouseLocation.x) * 2),
|
||||
height: startSize.height + (startMouseLocation.y - currentMouseLocation.y)
|
||||
@@ -146,24 +160,25 @@ struct ContentView: View {
|
||||
.onEnded { _ in
|
||||
resizeStartSize = nil
|
||||
resizeStartMouseLocation = nil
|
||||
vm.endInteractiveResize()
|
||||
screen.endInteractiveResize()
|
||||
}
|
||||
}
|
||||
|
||||
private var sizeAnimation: Animation? {
|
||||
guard !vm.isUserResizing, !vm.isPresetResizing else { return nil }
|
||||
return vm.notchState == .open ? vm.openAnimation : vm.closeAnimation
|
||||
guard !screen.isUserResizing, !screen.isPresetResizing else { return nil }
|
||||
return screen.notchState == .open ? screen.openAnimation : screen.closeAnimation
|
||||
}
|
||||
|
||||
/// Open layout: VStack with toolbar row on top, terminal in the middle,
|
||||
/// tab bar at the bottom. Every section has a black background.
|
||||
private var openContent: some View {
|
||||
private func openContent(workspace: WorkspaceController) -> some View {
|
||||
VStack(spacing: 0) {
|
||||
// Toolbar row — right-aligned, solid black
|
||||
HStack {
|
||||
WorkspaceSwitcherView(screen: screen, orchestrator: orchestrator)
|
||||
Spacer()
|
||||
toolbarButton(icon: "arrow.up.forward.square", help: "Detach tab") {
|
||||
if let session = terminalManager.detachActiveTab() {
|
||||
if let session = workspace.detachActiveTab() {
|
||||
PopoutWindowController.shared.popout(session: session)
|
||||
}
|
||||
}
|
||||
@@ -172,12 +187,13 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
.padding(.top, 6)
|
||||
.padding(.leading, 10)
|
||||
.padding(.trailing, 10)
|
||||
.padding(.bottom, 2)
|
||||
.background(.black)
|
||||
|
||||
// Terminal — fills remaining space
|
||||
if let session = terminalManager.activeTab {
|
||||
if let session = workspace.activeTab {
|
||||
SwiftTermView(session: session)
|
||||
.id(session.id)
|
||||
.padding(.leading, 10)
|
||||
@@ -185,7 +201,7 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
// Tab bar
|
||||
TabBar(terminalManager: terminalManager)
|
||||
TabBar(workspace: workspace)
|
||||
}
|
||||
.background(.black)
|
||||
}
|
||||
@@ -199,38 +215,16 @@ struct ContentView: View {
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(help)
|
||||
.accessibilityIdentifier("notch.toolbar.\(icon)")
|
||||
.help(help)
|
||||
}
|
||||
|
||||
// MARK: - Hover
|
||||
|
||||
private func handleHover(_ hovering: Bool) {
|
||||
if hovering {
|
||||
withAnimation(hoverAnimation) { vm.isHovering = true }
|
||||
scheduleHoverOpenIfNeeded()
|
||||
} else {
|
||||
hoverTask?.cancel()
|
||||
withAnimation(hoverAnimation) { vm.isHovering = false }
|
||||
vm.clearHoverOpenSuppression()
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduleHoverOpenIfNeeded() {
|
||||
hoverTask?.cancel()
|
||||
guard openNotchOnHover,
|
||||
vm.notchState == .closed,
|
||||
!vm.isCloseTransitionActive,
|
||||
!vm.suppressHoverOpenUntilHoverExit,
|
||||
vm.isHovering else { return }
|
||||
|
||||
hoverTask = Task { @MainActor in
|
||||
try? await Task.sleep(nanoseconds: UInt64(minimumHoverDuration * 1_000_000_000))
|
||||
guard !Task.isCancelled,
|
||||
vm.isHovering,
|
||||
vm.notchState == .closed,
|
||||
!vm.isCloseTransitionActive,
|
||||
!vm.suppressHoverOpenUntilHoverExit else { return }
|
||||
vm.requestOpen?()
|
||||
withAnimation(hoverAnimation) {
|
||||
orchestrator.handleHoverChange(hovering, for: screen.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,3 +245,33 @@ private struct ResizeHandleShape: Shape {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
private struct WorkspaceScopedView<Content: View>: View {
|
||||
@ObservedObject var screen: ScreenContext
|
||||
@ObservedObject var screenRegistry: ScreenRegistry
|
||||
let content: (WorkspaceController) -> Content
|
||||
|
||||
init(
|
||||
screen: ScreenContext,
|
||||
screenRegistry: ScreenRegistry,
|
||||
@ViewBuilder content: @escaping (WorkspaceController) -> Content
|
||||
) {
|
||||
self.screen = screen
|
||||
self.screenRegistry = screenRegistry
|
||||
self.content = content
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
WorkspaceObservedView(workspace: screenRegistry.workspaceController(for: screen.id), content: content)
|
||||
.id(screen.workspaceID)
|
||||
}
|
||||
}
|
||||
|
||||
private struct WorkspaceObservedView<Content: View>: View {
|
||||
@ObservedObject var workspace: WorkspaceController
|
||||
let content: (WorkspaceController) -> Content
|
||||
|
||||
var body: some View {
|
||||
content(workspace)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user