262 lines
8.8 KiB
Swift
262 lines
8.8 KiB
Swift
//
|
|
// AlertDialog.swift
|
|
// Adwaita
|
|
//
|
|
// Created by david-swift on 05.04.24.
|
|
//
|
|
|
|
import CAdw
|
|
import LevenshteinTransformations
|
|
|
|
/// The message dialog widget.
|
|
public struct AlertDialog: AdwaitaWidget {
|
|
|
|
/// The ID for the dialog's storage.
|
|
static let dialogID = "alert-dialog"
|
|
/// The ID for the dialog's responses' storage.
|
|
static let responsesID = "responses"
|
|
/// The ID for the visibility binding.
|
|
static let visibleID = "visible"
|
|
/// The ID for the callbacks.
|
|
static let callbacks = "callbacks"
|
|
|
|
/// Whether the dialog is visible.
|
|
@Binding var visible: Bool
|
|
/// An identifier used if multiple dialogs are on one view.
|
|
var id: String
|
|
/// The dialog's title.
|
|
var heading: String
|
|
/// The body text.
|
|
var body: String
|
|
/// The available responses.
|
|
var responses: [Response] = []
|
|
/// The child view.
|
|
var child: AnyView
|
|
|
|
/// Initialize an alert dialog wrapper.
|
|
/// - Parameters:
|
|
/// - visible: Whether the dialog is visible.
|
|
/// - child: The child view.
|
|
/// - id: A unique identifier for dialogs on the view.
|
|
/// - heading: The heading.
|
|
/// - body: The body text.
|
|
public init(
|
|
visible: Binding<Bool>,
|
|
child: AnyView,
|
|
id: String,
|
|
heading: String,
|
|
body: String
|
|
) {
|
|
self._visible = visible
|
|
self.child = child
|
|
self.id = id
|
|
self.heading = heading
|
|
self.body = body
|
|
}
|
|
|
|
/// Information about a response.
|
|
struct Response: Identifiable {
|
|
|
|
/// The title.
|
|
var title: String
|
|
/// The identifier.
|
|
var id: String { title }
|
|
/// The appearance.
|
|
var appearance: ResponseAppearance
|
|
/// The function for the keyboard shortcut, or no shortcut.
|
|
var role: ResponseRole?
|
|
/// The callback.
|
|
var action: () -> Void
|
|
|
|
}
|
|
|
|
/// The appearance of the response.
|
|
public enum ResponseAppearance {
|
|
|
|
/// The regular appearance.
|
|
case `default`
|
|
/// The suggested appearance.
|
|
case suggested
|
|
/// The destructive appearance.
|
|
case destructive
|
|
|
|
}
|
|
|
|
/// The role of the response, determining a keyboard shortcut.
|
|
public enum ResponseRole {
|
|
|
|
/// The close role.
|
|
case close
|
|
/// The default role.
|
|
case `default`
|
|
|
|
}
|
|
|
|
/// The view storage.
|
|
/// - Parameters:
|
|
/// - modifiers: Modify views before being updated.
|
|
/// - type: The view render data type.
|
|
/// - Returns: The view storage.
|
|
public func container<Data>(
|
|
data: WidgetData,
|
|
type: Data.Type
|
|
) -> ViewStorage where Data: ViewRenderData {
|
|
let storage = child.storage(data: data, type: type)
|
|
update(storage, data: data, updateProperties: true, type: type)
|
|
return storage
|
|
}
|
|
|
|
/// Update the stored content.
|
|
/// - Parameters:
|
|
/// - storage: The storage to update.
|
|
/// - modifiers: Modify views before being updated
|
|
/// - updateProperties: Whether to update the view's properties.
|
|
/// - type: The view render data type.
|
|
public func update<Data>(
|
|
_ storage: ViewStorage,
|
|
data: WidgetData,
|
|
updateProperties: Bool,
|
|
type: Data.Type
|
|
) where Data: ViewRenderData {
|
|
storage.fields[Self.visibleID + id] = _visible
|
|
child.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
|
|
guard updateProperties else {
|
|
return
|
|
}
|
|
if visible {
|
|
var present = false
|
|
if storage.content[Self.dialogID + id]?.first == nil {
|
|
createDialog(storage: storage)
|
|
present = true
|
|
}
|
|
let pointer = storage.content[Self.dialogID + id]?.first?.opaquePointer
|
|
adw_alert_dialog_set_heading(pointer?.cast(), heading)
|
|
adw_alert_dialog_set_body(pointer?.cast(), body)
|
|
let old = storage.fields[Self.responsesID + id] as? [Response] ?? []
|
|
old.identifiableTransform(
|
|
to: responses,
|
|
functions: .init { index, element in
|
|
adw_alert_dialog_remove_response(pointer?.cast(), responseID(old[safe: index]?.id))
|
|
adw_alert_dialog_add_response(pointer?.cast(), responseID(element.id), element.title)
|
|
} delete: { index in
|
|
adw_alert_dialog_remove_response(pointer?.cast(), responseID(old[safe: index]?.id))
|
|
} insert: { _, element in
|
|
adw_alert_dialog_add_response(pointer?.cast(), responseID(element.id), element.title)
|
|
}
|
|
)
|
|
storage.fields[Self.responsesID + id] = responses
|
|
var handlers: [String: () -> Void] = [:]
|
|
for response in responses {
|
|
handlers[responseID(response.id) ?? ""] = response.action
|
|
}
|
|
storage.fields[Self.callbacks + id] = handlers
|
|
responsesCosmetics(pointer: pointer)
|
|
if present {
|
|
gtui_alertdialog_choose(
|
|
.init(Int(bitPattern: pointer)),
|
|
unsafeBitCast(storage, to: UInt64.self),
|
|
.init(Int(bitPattern: storage.opaquePointer))
|
|
)
|
|
}
|
|
} else {
|
|
if storage.content[Self.dialogID + id]?.first != nil {
|
|
adw_dialog_close(storage.content[Self.dialogID + id]?.first?.opaquePointer?.cast())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Style the responses and add shortcuts if required.
|
|
/// - Parameter pointer: The pointer.
|
|
func responsesCosmetics(pointer: OpaquePointer?) {
|
|
for element in responses {
|
|
switch element.appearance {
|
|
case .default:
|
|
adw_alert_dialog_set_response_appearance(
|
|
pointer?.cast(),
|
|
responseID(element.id),
|
|
ADW_RESPONSE_DEFAULT
|
|
)
|
|
case .suggested:
|
|
adw_alert_dialog_set_response_appearance(
|
|
pointer?.cast(),
|
|
responseID(element.id),
|
|
ADW_RESPONSE_SUGGESTED
|
|
)
|
|
case .destructive:
|
|
adw_alert_dialog_set_response_appearance(
|
|
pointer?.cast(),
|
|
responseID(element.id),
|
|
ADW_RESPONSE_DESTRUCTIVE
|
|
)
|
|
}
|
|
}
|
|
if let closeResponse = responses.first(where: { $0.role == .close }) ?? responses.first {
|
|
adw_alert_dialog_set_close_response(pointer?.cast(), responseID(closeResponse.id))
|
|
}
|
|
if let defaultResponse = responses.first(where: { $0.role == .default }) {
|
|
adw_alert_dialog_set_default_response(pointer?.cast(), responseID(defaultResponse.id))
|
|
}
|
|
}
|
|
|
|
/// Create a new instance of the dialog.
|
|
/// - Parameter storage: The wrapped view's storage.
|
|
func createDialog(storage: ViewStorage) {
|
|
let pointer = adw_alert_dialog_new(nil, nil)
|
|
let dialog = ViewStorage(pointer?.opaque())
|
|
storage.content[Self.dialogID + id] = [dialog]
|
|
}
|
|
|
|
/// Get the identifier of a response which is combined with the dialog's id.
|
|
/// - Parameter id: The response identifier.
|
|
/// - Returns: The new identifier.
|
|
func responseID(_ id: String?) -> String? {
|
|
if let id {
|
|
return self.id + "...." + id
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/// Add a response to the alert dialog.
|
|
/// - Parameters:
|
|
/// - title: The response.
|
|
/// - appearance: The response's appearance.
|
|
/// - role: The response's shortcut, if any.
|
|
/// - action: The
|
|
public func response(
|
|
_ title: String,
|
|
appearance: ResponseAppearance = .default,
|
|
role: ResponseRole? = nil,
|
|
action: @escaping () -> Void
|
|
) -> Self {
|
|
var newSelf = self
|
|
newSelf.responses.append(.init(title: title, appearance: appearance, role: role, action: action))
|
|
return newSelf
|
|
}
|
|
|
|
}
|
|
|
|
/// Run when an alert dialog closes.
|
|
/// - Parameters:
|
|
/// - ptr: The pointer.
|
|
/// - answer: The identifier of the answer.
|
|
/// - userData: The alert dialog data.
|
|
@_cdecl("alertdialog_on_close_cb")
|
|
func alertdialog_on_close_cb(
|
|
ptr: UnsafeMutableRawPointer,
|
|
answer: UnsafePointer<CChar>?,
|
|
userData: UnsafeMutableRawPointer
|
|
) {
|
|
let storage = Unmanaged<ViewStorage>.fromOpaque(userData).takeUnretainedValue()
|
|
var id = ""
|
|
if let answer {
|
|
let answer = String(cString: answer)
|
|
id = .init(answer.components(separatedBy: "....").first ?? "")
|
|
(storage.fields[AlertDialog.callbacks + id] as? [String: () -> Void])?[answer]?()
|
|
}
|
|
storage.content[AlertDialog.dialogID + id] = []
|
|
storage.fields[AlertDialog.responsesID + id] = []
|
|
if let visible = storage.fields[AlertDialog.visibleID + id] as? Binding<Bool>, visible.wrappedValue {
|
|
visible.wrappedValue = false
|
|
}
|
|
}
|