118 lines
4.4 KiB
Swift
118 lines
4.4 KiB
Swift
import Adw
|
|
import Gtk
|
|
|
|
// MARK: - Destination Registry
|
|
|
|
/// Maps a destination value's dynamic type to the builder that renders it.
|
|
///
|
|
/// Registration happens at mount time: `.navigationDestination(for:)` looks the
|
|
/// registry up in the environment injected by the enclosing `NavigationView`.
|
|
/// A second registration for the same type replaces the first, so the innermost
|
|
/// (latest-mounted) declaration wins, matching SwiftUI.
|
|
@MainActor final class NavigationDestinationRegistry {
|
|
private var builders: [ObjectIdentifier: (AnyHashable) -> [AnyView]] = [:]
|
|
|
|
func register<D: Hashable>(_ type: D.Type, _ build: @escaping (D) -> [AnyView]) {
|
|
builders[ObjectIdentifier(type)] = { erased in
|
|
guard let value = erased.base as? D else { return [] }
|
|
return build(value)
|
|
}
|
|
}
|
|
|
|
/// The views for `value`, or `nil` when no builder is registered for its
|
|
/// exact dynamic type.
|
|
func build(_ value: AnyHashable) -> [AnyView]? {
|
|
builders[ObjectIdentifier(type(of: value.base))]?(value)
|
|
}
|
|
}
|
|
|
|
// MARK: - Environment slots
|
|
|
|
private enum NavigationDestinationsKey: EnvironmentKey {
|
|
static var defaultValue: NavigationDestinationRegistry? { nil }
|
|
}
|
|
|
|
private enum NavigationPageKey: EnvironmentKey {
|
|
static var defaultValue: Adw.NavigationPage? { nil }
|
|
}
|
|
|
|
extension EnvironmentValues {
|
|
/// The destination registry of the innermost enclosing `NavigationView`.
|
|
var navigationDestinations: NavigationDestinationRegistry? {
|
|
get { self[NavigationDestinationsKey.self] }
|
|
set { self[NavigationDestinationsKey.self] = newValue }
|
|
}
|
|
|
|
/// The `Adw.NavigationPage` this subtree is mounted into.
|
|
var navigationPage: Adw.NavigationPage? {
|
|
get { self[NavigationPageKey.self] }
|
|
set { self[NavigationPageKey.self] = newValue }
|
|
}
|
|
}
|
|
|
|
// MARK: - navigationDestination(for:destination:)
|
|
|
|
extension View {
|
|
/// Associates a destination view with a presented data type, for use within
|
|
/// the enclosing path-driven `NavigationView`.
|
|
///
|
|
/// Registration happens when this view mounts, so a destination declared
|
|
/// inside another destination is only available once that outer page has
|
|
/// been pushed - the same lazy-registration caveat SwiftUI has.
|
|
///
|
|
/// - Parameters:
|
|
/// - data: The exact type of value this destination renders. Lookup is by
|
|
/// dynamic type; subclasses are not matched.
|
|
/// - destination: Builds the page content from a value snapshot.
|
|
/// - Returns: The receiver, unchanged, with the destination registered.
|
|
public func navigationDestination<D: Hashable>(
|
|
for data: D.Type,
|
|
@ViewBuilder destination: @escaping (D) -> [AnyView]
|
|
) -> AnyView {
|
|
AnyView(makeWidget: { ctx in
|
|
ctx.environment.navigationDestinations?.register(data, destination)
|
|
return AnyView(self).makeWidget(ctx)
|
|
})
|
|
}
|
|
}
|
|
|
|
// MARK: - navigationTitle
|
|
|
|
extension View {
|
|
/// Sets the title of the `Adw.NavigationPage` this view is mounted into
|
|
/// (static, applied once at mount).
|
|
///
|
|
/// `Adw.HeaderBar` inside the page displays this title automatically, and
|
|
/// the next page's back button uses it as its tooltip. No-op outside a
|
|
/// `NavigationView` page.
|
|
public func navigationTitle(_ title: String) -> AnyView {
|
|
AnyView(makeWidget: { ctx in
|
|
ctx.environment.navigationPage?.setTitle(title: title)
|
|
return AnyView(self).makeWidget(ctx)
|
|
})
|
|
}
|
|
|
|
/// Sets the page title, tracking a ``Binding``.
|
|
public func navigationTitle(_ title: Binding<String>) -> AnyView {
|
|
AnyView(makeWidget: { ctx in
|
|
if let page = ctx.environment.navigationPage {
|
|
page.setTitle(title: title.wrappedValue)
|
|
ctx.registry.add(title.subscribe { [page] t in page.setTitle(title: t) })
|
|
}
|
|
return AnyView(self).makeWidget(ctx)
|
|
})
|
|
}
|
|
|
|
/// Sets the page title from a tracked closure, re-evaluated when any
|
|
/// `@State` or observable property it reads changes.
|
|
public func navigationTitle(_ title: @escaping () -> String) -> AnyView {
|
|
AnyView(makeWidget: { ctx in
|
|
if let page = ctx.environment.navigationPage {
|
|
let tracker = DependencyTracker { [page] in page.setTitle(title: title()) }
|
|
tracker.run()
|
|
ctx.registry.add(tracker)
|
|
}
|
|
return AnyView(self).makeWidget(ctx)
|
|
})
|
|
}
|
|
}
|