import SwiftUI /// 顶层去处。设计定稿只有两处落点:首页(主机管理)与终端。 /// /// 关键:路由与**会话生命周期解耦** —— 回首页/最小化只改 route,会话照跑 /// (会话 surface 由 `SessionCanvas` 常驻挂载,见 ContentView)。 enum AppRoute: Equatable { case home case terminal(UUID) var sessionID: UUID? { if case .terminal(let id) = self { return id } return nil } } /// 侧边栏三态。同一条栏,元素不增不减,只是展示密度不同。 enum SidebarState: Equatable { /// 56pt 轨道:只显图标与两字缩写 case collapsed /// 288pt 面板**盖住**轨道,右侧终端压遮罩;点终端任意处收起。终端不 reflow、不触发 PTY resize。 case overlay /// 288pt 钉在左侧:遮罩消失,终端变窄。**只在钉住/解除时各 resize 一次**。 case pinned var isExpanded: Bool { self != .collapsed } /// 该状态下侧边栏占据的布局宽度(overlay 是浮层,不占位)。 var layoutWidth: CGFloat { switch self { case .collapsed, .overlay: TX.Layout.railWidth case .pinned: TX.Layout.sidebarWidth } } } /// 终端页上的模态浮层。放在 router 而不是视图 `@State`:⌘W/⌘D 快捷键与无头 URL 驱动 /// 都要能打开它,视图内部状态触达不到。 enum AppModal: Equatable { /// 关闭确认(3c) case closeConfirm /// 分屏菜单(3b)。`vertical` = 竖分(⌘D)/ 横分(⌘⇧D) case splitMenu(vertical: Bool) } /// 导航状态持有者。视图只读它、只经方法改它。 @MainActor final class AppRouter: ObservableObject { @Published var route: AppRoute = .home @Published var sidebar: SidebarState = .collapsed /// 当前模态浮层(关闭确认 / 分屏菜单)。 @Published var modal: AppModal? /// 状态切换动效:设计定稿 200–260ms easeOut(侧边栏浮层从左滑入 + 遮罩淡入)。 /// 所有侧边栏状态变更都经这里,键盘快捷键与点击拿到同一条曲线。 static let sidebarAnimation = Animation.easeOut(duration: 0.24) private func animateSidebar(_ change: () -> Void) { withAnimation(Self.sidebarAnimation, change) } /// 进入某会话(点会话卡 / 侧边栏会话行 / 新建连接后)。恢复焦点由会话自身持有。 func enter(_ sessionID: UUID) { route = .terminal(sessionID) } /// 回首页:会话完全不动(⌘⇧H / 侧边栏首页行)。 func goHome() { modal = nil route = .home if sidebar == .overlay { animateSidebar { sidebar = .collapsed } } } /// 开合侧边栏(⌘\ / 开合键)。折叠 ⇄ 遮罩;Pin 态视作展开,收起回折叠。 func toggleSidebar() { animateSidebar { switch sidebar { case .collapsed: sidebar = .overlay case .overlay, .pinned: sidebar = .collapsed } } } /// Pin 切换(只在展开态出现)。钉住/解除各触发一次终端 resize。 func togglePin() { animateSidebar { sidebar = sidebar == .pinned ? .overlay : .pinned } } /// 打开/关闭模态浮层(红点、⌘W、⌘D、⌘⇧D 与无头 URL 共用同一入口)。 func present(_ modal: AppModal) { self.modal = modal } func dismissModal() { modal = nil } /// 点终端任意处:遮罩态收起(Pin 态不受影响)。 func dismissOverlaySidebar() { if sidebar == .overlay { animateSidebar { sidebar = .collapsed } } } }