diff --git a/mac/Sources/Byre/Pages.swift b/mac/Sources/Byre/Pages.swift index c26b5fc..59d5a30 100644 --- a/mac/Sources/Byre/Pages.swift +++ b/mac/Sources/Byre/Pages.swift @@ -5,7 +5,7 @@ import Observation import SwiftUI import WebKit -enum Page: Equatable { +enum Page: Hashable { case file(taskId: String, path: String) case preview(taskId: String, port: Int) @@ -15,6 +15,13 @@ enum Page: Equatable { } } + var isFile: Bool { + if case .file = self { + return true + } + return false + } + @MainActor func title(_ store: Store) -> String { switch self { @@ -38,11 +45,43 @@ func isPage(_ path: String) -> Bool { pageExts.contains((path as NSString).pathExtension.lowercased()) } +struct ShownPages { + var pages: [Page] + var current: Page +} + extension Store { func openPage(_ page: Page) { - shownPages[page.taskId] = page + var shown = shownPages[page.taskId] ?? ShownPages(pages: [], current: page) + if !shown.pages.contains(page) { + shown.pages.append(page) + } + shown.current = page + shownPages[page.taskId] = shown sheet = Sheet(owner: .task(page.taskId), view: .tab(.page)) } + + func closePage(_ page: Page) { + guard var shown = shownPages[page.taskId], let i = shown.pages.firstIndex(of: page) else { + return + } + shown.pages.remove(at: i) + if shown.pages.isEmpty { + shownPages[page.taskId] = nil + if sheet?.tab == .page { + sheet = nil + } + return + } + if shown.current == page { + shown.current = shown.pages[min(i, shown.pages.count - 1)] + } + shownPages[page.taskId] = shown + } + + func currentPage() -> Page? { + selectedTaskId.flatMap { shownPages[$0]?.current } + } } @MainActor @@ -239,23 +278,37 @@ private final class PageWatch { } } -struct PageView: View { - let page: Page +struct PageTabs: View { + let shown: ShownPages @Environment(Store.self) private var store @ViewState private var watch = PageWatch() var body: some View { - Group { - if let url = page.url(store) { - WebPage(url: url, reloads: watch.reloads) - } else { - EmptyText(text: "Loading…") + VStack(spacing: 0) { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 4) { + ForEach(shown.pages, id: \.self) { page in + PageTabButton(page: page, active: page == shown.current) + } + } + .padding(.horizontal, 12) + } + .fixedSize(horizontal: false, vertical: true) + .overlay(alignment: .bottom) { + Rectangle().fill(Palette.border).frame(height: 1) + } + ZStack { + ForEach(shown.pages, id: \.self) { page in + if let url = page.url(store) { + WebPage(url: url, reloads: page.isFile ? watch.reloads : 0, shown: page == shown.current) + } else if page == shown.current { + EmptyText(text: "Loading…") + } + } } } - .task(id: page) { - guard case .file(let taskId, _) = page else { - return - } + .task(id: shown.current.taskId) { + let taskId = shown.current.taskId let watch = watch let api = store.api let pages = store.pages @@ -271,9 +324,38 @@ struct PageView: View { } } +private struct PageTabButton: View { + let page: Page + let active: Bool + @Environment(Store.self) private var store + @ViewState private var hover = false + + var body: some View { + HStack(spacing: 0) { + PaneTabButton(label: page.title(store), active: active) { + store.openPage(page) + } + Button { + store.closePage(page) + } label: { + Text("✕") + .font(Fonts.font(11)) + .foregroundStyle(hover ? Palette.text : Palette.muted) + .padding(.trailing, 6) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .help("Close this page") + .onHover { hover = $0 } + } + .help(page.url(store)?.absoluteString ?? "") + } +} + private struct WebPage: NSViewRepresentable { let url: URL let reloads: Int + let shown: Bool final class Coordinator: NSObject, WKUIDelegate { var url: URL? @@ -300,6 +382,7 @@ private struct WebPage: NSViewRepresentable { } func updateNSView(_ view: WKWebView, context: Context) { + view.isHidden = !shown let c = context.coordinator if c.url != url { c.url = url diff --git a/mac/Sources/Byre/Panes.swift b/mac/Sources/Byre/Panes.swift index 3559e1a..71f6b3d 100644 --- a/mac/Sources/Byre/Panes.swift +++ b/mac/Sources/Byre/Panes.swift @@ -232,8 +232,8 @@ struct TaskTabBody: View { case .files: FilesView() case .page: - if let page = store.selectedTaskId.flatMap({ store.shownPages[$0] }) { - PageView(page: page) + if let shown = store.selectedTaskId.flatMap({ store.shownPages[$0] }) { + PageTabs(shown: shown) } case .terminal: TerminalTab() diff --git a/mac/Sources/Byre/Sheet.swift b/mac/Sources/Byre/Sheet.swift index 652f8ba..deac024 100644 --- a/mac/Sources/Byre/Sheet.swift +++ b/mac/Sources/Byre/Sheet.swift @@ -81,7 +81,7 @@ private struct SheetPanel: View { .lineLimit(1) .truncationMode(.tail) Spacer(minLength: 0) - if sheet.tab == .page, let page = store.selectedTaskId.flatMap({ store.shownPages[$0] }), let url = page.url(store) { + if sheet.tab == .page, let url = store.currentPage()?.url(store) { Button { NSWorkspace.shared.open(url) } label: { diff --git a/mac/Sources/Byre/Store.swift b/mac/Sources/Byre/Store.swift index 3cabe16..d70b029 100644 --- a/mac/Sources/Byre/Store.swift +++ b/mac/Sources/Byre/Store.swift @@ -71,7 +71,7 @@ final class Store { var connected = false private(set) var focus = Focus() var sheet: Sheet? - var shownPages: [String: Page] = [:] + var shownPages: [String: ShownPages] = [:] var leadSessions: [AgentSession] = [] let leadFeed: Feed var jump: Jump?