97 lines
3.8 KiB
Swift
97 lines
3.8 KiB
Swift
import Adw
|
|
import Gtk
|
|
|
|
/// Ties `dialog`'s on-screen state to `isPresented`, in both directions.
|
|
///
|
|
/// Shared by ``View/dialog(isPresented:content:)`` and every `View.alert`
|
|
/// overload so both features get identical presentation semantics.
|
|
///
|
|
/// Presentation is deferred until `host` is mapped: `adw_dialog_present()`
|
|
/// falls back to a separate top-level window when the parent widget is not yet
|
|
/// rooted in an `AdwWindow`. Dismissal by any route - Escape, the close button,
|
|
/// swiping a bottom sheet down, or a programmatic `close()` - reaches `::closed`
|
|
/// and writes `false` back through the binding.
|
|
///
|
|
/// Three invariants are load-bearing rather than defensive:
|
|
///
|
|
/// - `isShown` gates `close()`. `adw_dialog_close()` logs a `g_critical` when
|
|
/// called on a dialog that is not currently presented.
|
|
/// - `isUpdating` breaks the `close() -> ::closed -> isPresented = false ->
|
|
/// close()` loop, because ``StateBox/set(_:)`` notifies unconditionally with
|
|
/// no equality check.
|
|
/// - The `close()` return value is honored. A `can-close = false` dialog, or one
|
|
/// whose `::close-attempt` handler vetoes, stays on screen; the binding is
|
|
/// restored rather than left desynchronized.
|
|
///
|
|
/// Every resource is registered on `ctx.registry`. The teardown token sets
|
|
/// `isUpdating` before force-closing, and ``NodeRegistry/teardown()`` cancels
|
|
/// tokens before disconnecting signal handles, so unmounting a still-presented
|
|
/// dialog cannot write back into user state that is already going away.
|
|
///
|
|
/// - Parameters:
|
|
/// - dialog: The dialog to show and hide. Captured strongly - Portico does not
|
|
/// otherwise retain it, and it is not in the widget tree until shown.
|
|
/// - isPresented: The two-way source of truth for visibility.
|
|
/// - host: The widget the modifier was applied to, used as the presentation
|
|
/// parent so libadwaita can find the enclosing `AdwWindow`.
|
|
/// - ctx: The mount context whose registry owns the subscriptions.
|
|
@MainActor
|
|
func bindDialogPresentation(
|
|
_ dialog: Adw.Dialog,
|
|
isPresented: Binding<Bool>,
|
|
host: Gtk.Widget,
|
|
ctx: MountContext
|
|
) {
|
|
var isShown = false
|
|
var isUpdating = false
|
|
|
|
// `parent` is a parameter rather than a capture so the `map` handler below
|
|
// does not strongly retain the very widget it is connected to.
|
|
let show: @MainActor (Gtk.Widget) -> Void = { parent in
|
|
guard !isShown, parent.getMapped() else { return }
|
|
isShown = true
|
|
dialog.present(parent: parent)
|
|
}
|
|
|
|
let hide: @MainActor () -> Void = {
|
|
guard isShown else { return }
|
|
if dialog.close() {
|
|
isShown = false
|
|
} else {
|
|
// Vetoed: the dialog is still on screen, so restore the binding
|
|
// instead of letting it desync. This write re-enters the subscriber
|
|
// below, which the `isUpdating` guard already suppresses.
|
|
isPresented.wrappedValue = true
|
|
}
|
|
}
|
|
|
|
ctx.registry.add(dialog.connectClosed { _ in
|
|
isShown = false
|
|
guard !isUpdating else { return }
|
|
isUpdating = true
|
|
isPresented.wrappedValue = false
|
|
isUpdating = false
|
|
})
|
|
|
|
ctx.registry.add(isPresented.subscribe { [host] shouldShow in
|
|
guard !isUpdating else { return }
|
|
isUpdating = true
|
|
if shouldShow { show(host) } else { hide() }
|
|
isUpdating = false
|
|
})
|
|
|
|
// Covers both "presented before the window was shown" and a later re-map.
|
|
ctx.registry.add(host.connectMap { mapped in
|
|
guard isPresented.untrackedValue else { return }
|
|
show(mapped)
|
|
})
|
|
|
|
// No-op unless the host is already mapped at mount time.
|
|
if isPresented.untrackedValue { show(host) }
|
|
|
|
ctx.registry.add(SubscriptionToken {
|
|
isUpdating = true // never reset: suppress write-back into user state
|
|
if isShown { dialog.forceClose() }
|
|
isShown = false
|
|
})
|
|
}
|