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(_ 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( 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) -> 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) }) } }