window order

This commit is contained in:
Ian Gulliver
2026-10-18 21:55:53 -07:00
parent d02f4c7280
commit 327aab069d
2 changed files with 35 additions and 3 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ Focus lives in `Store`. In-app `#/` links go through `Store.route`: `#/task/<id>
## Window and layout
One window per saved server, sharing no UI: the windows are a `WindowGroup` (id `main`) whose value is the server's origin, so opening a server's window again brings forward the one already open. The windows are tabs of one window: each sets `tabbingMode = .preferred` and, once shown, joins the tab group of another visible server window (`addTabbedWindow`), so the windows opened at launch end up tabbed together as New Server Window's do. Each window has its own `Overlays`, so its switcher, dialogs, left-pane tab and key monitor act only on it, and its own pane-focus reporting. The window's title is the server's host, so the Window menu lists the servers, and its content sits inside a 3-point border in the server's colour (`Servers.color`, one of the task colours, picked from a row of swatches in Settings under Window colour and kept in the server's `ServerRecord`, so every Mac shows the same colour). A window with no server (New Server Window, or after Forget Server) shows the connection screen with no border. The panes follow the "UI layout" rules in `AGENTS.md`: an `HStack` of two panes of exactly equal width at every size, no sidebar, no navbar, nothing above them but the identity banner when the user has no git identity. The title bar is the standard one; no byre controls go in it or in a toolbar.
One window per saved server, sharing no UI: the windows are a `WindowGroup` (id `main`) whose value is the server's origin, so opening a server's window again brings forward the one already open. The windows are tabs of one window: each sets `tabbingMode = .preferred` and, once shown, joins the tab group of another visible server window (`addTabbedWindow`), so the windows opened at launch end up tabbed together as New Server Window's do. `addTabbedWindow` puts the new tab right after the host's, so the order would follow which windows came up first; instead, each time a window joins or its server changes, `Servers.arrange` moves the tabs with a server into saved order (`ServerRecord.saved`, the order Previous and Next Server follow) with `NSWindowTabGroup.insertWindow`, then restores the selected tab, which `insertWindow` changes. Tabs with no server stay after them. Each window has its own `Overlays`, so its switcher, dialogs, left-pane tab and key monitor act only on it, and its own pane-focus reporting. The window's title is the server's host, so the Window menu lists the servers, and its content sits inside a 3-point border in the server's colour (`Servers.color`, one of the task colours, picked from a row of swatches in Settings under Window colour and kept in the server's `ServerRecord`, so every Mac shows the same colour). A window with no server (New Server Window, or after Forget Server) shows the connection screen with no border. The panes follow the "UI layout" rules in `AGENTS.md`: an `HStack` of two panes of exactly equal width at every size, no sidebar, no navbar, nothing above them but the identity banner when the user has no git identity. The title bar is the standard one; no byre controls go in it or in a toolbar.
At launch the first window takes the oldest saved server and opens a window for each of the others. A CloudKit import that brings a server saved on another Mac starts its connection, and a window showing an empty connection screen takes it. SwiftUI's own window restoration is off (`restorationBehavior(.disabled)`); the scene keeps the id `main` that the single-window builds used, because AppKit restores their saved window by that id and SwiftUI then opens no launch window at all when the id names no scene.
+34 -2
View File
@@ -12,6 +12,7 @@ final class Servers {
private(set) var entries: [(origin: String, record: ServerRecord)] = []
private(set) var connections: [String: Connection] = [:]
@ObservationIgnored var shown: Set<String> = []
@ObservationIgnored var windows: [String: WindowSlot] = [:]
@ObservationIgnored var launched = false
@ObservationIgnored var openWindow: ((String?) -> Void)?
@@ -92,6 +93,18 @@ final class Servers {
return origins[(i + step + origins.count) % origins.count]
}
func arrange(_ group: NSWindowTabGroup?) {
guard let group else {
return
}
let selected = group.selectedWindow
let ordered = origins.compactMap { windows[$0]?.window }.filter { group.windows.contains($0) }
for (i, window) in ordered.enumerated() where group.windows[i] !== window {
group.insertWindow(window, at: i)
}
group.selectedWindow = selected
}
func openAll() {
if origins.isEmpty {
openWindow?(nil)
@@ -102,6 +115,11 @@ final class Servers {
}
}
@MainActor
final class WindowSlot {
weak var window: NSWindow?
}
@MainActor
@Observable
final class Connection {
@@ -341,6 +359,7 @@ struct ServerWindow: View {
@Binding var origin: String?
@ViewState private var overlays = Overlays()
@ViewState private var form = ConnectForm()
@ViewState private var slot = WindowSlot()
@Environment(\.openWindow) private var openWindow
@Environment(\.dismiss) private var dismissWindow
@@ -414,16 +433,27 @@ struct ServerWindow: View {
.onDisappear {
if let origin {
Servers.shared.shown.remove(origin)
release(origin)
}
}
}
private func moved(_ old: String?, _ new: String?) {
let servers = Servers.shared
if let old {
Servers.shared.shown.remove(old)
servers.shown.remove(old)
release(old)
}
if let new {
Servers.shared.shown.insert(new)
servers.shown.insert(new)
servers.windows[new] = slot
}
servers.arrange(slot.window?.tabGroup)
}
private func release(_ origin: String) {
if Servers.shared.windows[origin] === slot {
Servers.shared.windows[origin] = nil
}
}
@@ -449,11 +479,13 @@ struct ServerWindow: View {
private func joinTabs(_ window: NSWindow) {
window.tabbingMode = .preferred
slot.window = window
DispatchQueue.main.async {
let host = NSApp.windows.first { $0 !== window && $0.isVisible && $0.tabbingIdentifier == window.tabbingIdentifier }
if let host, !(host.tabbedWindows ?? []).contains(window) {
host.addTabbedWindow(window, ordered: .above)
}
Servers.shared.arrange(window.tabGroup)
}
}