import SwiftUI import Foundation struct TerminalSplitTreeView: View { @ObservedObject var tab: Tab let isTabActive: Bool @State private var config = GhosttyConfig.load() @EnvironmentObject var notificationStore: TerminalNotificationStore var body: some View { let appearance = SplitAppearance( dividerColor: Color(nsColor: config.resolvedSplitDividerColor), unfocusedOverlayColor: Color(nsColor: config.unfocusedSplitOverlayFill), unfocusedOverlayOpacity: config.unfocusedSplitOverlayOpacity ) Group { if let node = tab.splitTree.zoomed ?? tab.splitTree.root { TerminalSplitSubtreeView( node: node, isRoot: node == tab.splitTree.root, isSplit: tab.splitTree.isSplit, isTabActive: isTabActive, focusedSurfaceId: tab.focusedSurfaceId, appearance: appearance, tabId: tab.id, notificationStore: notificationStore, onFocus: { tab.focusSurface($0) }, onTriggerFlash: { tab.triggerDebugFlash(surfaceId: $0) }, onResize: { tab.updateSplitRatio(node: $0, ratio: $1) }, onEqualize: { tab.equalizeSplits() } ) .id(node.structuralIdentity) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(GeometryReader { proxy in Color.clear .onAppear { tab.updateSplitViewSize(proxy.size) } .onChange(of: proxy.size) { tab.updateSplitViewSize($0) } }) .onReceive(NotificationCenter.default.publisher(for: .ghosttyConfigDidReload)) { _ in config = GhosttyConfig.load() } } } fileprivate struct TerminalSplitSubtreeView: View { let node: SplitTree.Node let isRoot: Bool let isSplit: Bool let isTabActive: Bool let focusedSurfaceId: UUID? let appearance: SplitAppearance let tabId: UUID let notificationStore: TerminalNotificationStore let onFocus: (UUID) -> Void let onTriggerFlash: (UUID) -> Void let onResize: (SplitTree.Node, Double) -> Void let onEqualize: () -> Void var body: some View { switch node { case .leaf(let surface): let isFocused = isTabActive && focusedSurfaceId == surface.id TerminalSurfaceView( surface: surface, isFocused: isFocused, isSplit: isSplit, appearance: appearance, tabId: tabId, notificationStore: notificationStore, onFocus: { onFocus(surface.id) }, onTriggerFlash: { onTriggerFlash(surface.id) } ) case .split(let split): let splitViewDirection: SplitViewDirection = switch split.direction { case .horizontal: .horizontal case .vertical: .vertical } SplitView( splitViewDirection, .init(get: { CGFloat(split.ratio) }, set: { onResize(node, Double($0)) }), dividerColor: appearance.dividerColor, resizeIncrements: .init(width: 1, height: 1), left: { TerminalSplitSubtreeView( node: split.left, isRoot: false, isSplit: isSplit, isTabActive: isTabActive, focusedSurfaceId: focusedSurfaceId, appearance: appearance, tabId: tabId, notificationStore: notificationStore, onFocus: onFocus, onTriggerFlash: onTriggerFlash, onResize: onResize, onEqualize: onEqualize ) }, right: { TerminalSplitSubtreeView( node: split.right, isRoot: false, isSplit: isSplit, isTabActive: isTabActive, focusedSurfaceId: focusedSurfaceId, appearance: appearance, tabId: tabId, notificationStore: notificationStore, onFocus: onFocus, onTriggerFlash: onTriggerFlash, onResize: onResize, onEqualize: onEqualize ) }, onEqualize: { onEqualize() } ) } } } private struct SplitAppearance { let dividerColor: Color let unfocusedOverlayColor: Color let unfocusedOverlayOpacity: Double } private struct TerminalSurfaceView: View { @ObservedObject var surface: TerminalSurface let isFocused: Bool let isSplit: Bool let appearance: SplitAppearance let tabId: UUID let notificationStore: TerminalNotificationStore let onFocus: () -> Void let onTriggerFlash: () -> Void var body: some View { ZStack(alignment: .topLeading) { GhosttyTerminalView( terminalSurface: surface, isActive: isFocused, onFocus: { _ in onFocus() }, onTriggerFlash: onTriggerFlash ) .background(Color.clear) if isSplit && !isFocused && appearance.unfocusedOverlayOpacity > 0 { Rectangle() .fill(appearance.unfocusedOverlayColor) .opacity(appearance.unfocusedOverlayOpacity) .allowsHitTesting(false) } if notificationStore.hasUnreadNotification(forTabId: tabId, surfaceId: surface.id) { Rectangle() .stroke(Color(nsColor: .systemBlue), lineWidth: 2.5) .shadow(color: Color(nsColor: .systemBlue).opacity(0.35), radius: 3) .padding(2) .allowsHitTesting(false) } if let searchState = surface.searchState { SurfaceSearchOverlay( surface: surface, searchState: searchState, onClose: { surface.searchState = nil surface.hostedView.moveFocus() } ) } } } }