import Adw /// Converts `duration` to the whole seconds `adw_toast_set_timeout` expects. /// /// A positive duration never rounds down to `0`, because `0` means "never /// auto-dismiss" in libadwaita and silently turning a brief toast into a /// permanent one would be worse than rounding up. `.zero` and negative /// durations map to `0` deliberately. /// /// - Parameter duration: The requested on-screen time. /// - Returns: Whole seconds, clamped to at least `1` for any positive duration. private func toastTimeoutSeconds(_ duration: Duration) -> UInt32 { let components = duration.components guard components.seconds > 0 || (components.seconds == 0 && components.attoseconds > 0) else { return 0 } let rounded = components.seconds + (components.attoseconds >= 500_000_000_000_000_000 ? 1 : 0) return UInt32(clamping: max(1, rounded)) } extension Adw.Toast { /// Creates a fully configured toast in one expression. /// /// ```swift /// toasts.addToast(Toast(title: "File saved", buttonLabel: "Undo") { undo() }) /// ``` /// /// `onClick` has no default value, so the inherited `Toast(title:)` /// initializer stays reachable and unambiguous for the bare case. /// /// - Parameters: /// - title: The toast text, interpreted as Pango markup unless `useMarkup` /// is `false`. /// - timeout: How long the toast stays on screen. `nil` keeps libadwaita's /// default. `.zero` (or a negative duration) means the toast never /// dismisses itself. `AdwToast` counts whole seconds, so the value is /// rounded to the nearest second and any positive duration is clamped to /// at least one second rather than collapsing to "never". /// - priority: How the toast behaves when one is already displayed. `nil` /// keeps libadwaita's default. /// - buttonLabel: Label for the toast's action button. `nil` shows no button. /// - useMarkup: Whether `title` is Pango markup. `nil` keeps libadwaita's /// default. /// - onClick: Runs when the action button is clicked. The /// `::button-clicked` handler is owned by the toast and released with it, /// so there is nothing to unregister - but avoid capturing the toast /// itself, which would keep it alive forever. public convenience init( title: String, timeout: Duration? = nil, priority: ToastPriority? = nil, buttonLabel: String? = nil, useMarkup: Bool? = nil, _ onClick: @escaping () -> Void ) { self.init(title: title) if let timeout { setTimeout(timeout: toastTimeoutSeconds(timeout)) } if let priority { setPriority(priority: priority) } if let buttonLabel { setButtonLabel(buttonLabel: buttonLabel) } if let useMarkup { setUseMarkup(useMarkup: useMarkup) } _ = connectButtonClicked { _ in onClick() } } }