Compare commits

...

14 Commits

7 changed files with 486 additions and 78 deletions

7
.swiftformat Normal file
View File

@ -0,0 +1,7 @@
--exclude **/Generated
--type-blank-lines insert
--self init-only
--empty-braces spaced
--extension-acl on-declarations
--trailing-commas never
--disable indent

View File

@ -12,30 +12,15 @@ extension AnyView {
/// Add an about dialog to the parent window.
/// - Parameters:
/// - visible: Whether the dialog is presented.
/// - app: The app's name.
/// - developer: The developer's name.
/// - version: The version string.
/// - icon: The app icon.
/// - website: The app's website.
/// - issues: Website for reporting issues.
/// - configure: A closure that mutates the dialog configuration.
public func aboutDialog(
visible: Binding<Bool>,
app: String? = nil,
developer: String? = nil,
version: String? = nil,
icon: Icon? = nil,
website: URL? = nil,
issues: URL? = nil
configure: (inout AdwaitaAboutDialogConfig) -> Void
) -> AnyView {
AboutDialog(
visible: visible,
child: self,
appName: app,
developer: developer,
version: version,
icon: icon,
website: website,
issues: issues
configure: configure
)
}

View File

@ -0,0 +1,328 @@
//
// AdwaitaAboutDialogConfig.swift
// Adwaita
//
// Created by lambdaclan on 09.01.2026.
//
import CAdw
import Foundation
/// A link shown in the About dialog.
public struct AboutLink {
/// The link title.
public var title: String
/// The destination URL.
public var url: URL?
/// Create a new link.
/// - Parameters:
/// - title: The link title.
/// - url: The destination URL.
public init(title: String, url: URL?) {
self.title = title
self.url = url
}
}
/// The type of contribution made by a credited person.
public enum ContributionRole {
/// A person who contributed to development.
case developer
/// A person who contributed to design.
case designer
/// A person who contributed artwork or visuals.
case artist
/// A person who contributed translations.
case translator
}
/// A credited person and their contribution role.
public struct CreditEntry {
/// The contributor's role.
public var role: ContributionRole
/// The contributor's name.
public var name: String
/// Create a new credit entry.
/// - Parameters:
/// - role: The contributor's role.
/// - name: The contributor's name.
public init(role: ContributionRole, name: String) {
self.role = role
self.name = name
}
}
/// An acknowledgement entry.
public struct AcknowledgementEntry {
/// The acknowledgement section title.
public var title: String
/// Acknowledged person/organization name.
public var name: String
/// Create a new acknowledgement entry.
/// - Parameters:
/// - title: The acknowledgement section title.
/// - name: The acknowledged person or organization.
public init(title: String, name: String) {
self.title = title
self.name = name
}
}
/// Other (additional) application entry.
public struct OtherAppEntry {
/// The application identifier of the referenced app.
public var appID: String
/// The display name of the referenced app.
public var name: String
/// A short descriptive summary of the referenced app.
public var summary: String
/// Creates a new other application entry.
/// - Parameters:
/// - appID: The application identifier of the referenced app.
/// - name: The display name of the referenced app.
/// - summary: A short descriptive summary of the referenced app.
public init(appID: String, name: String, summary: String) {
self.appID = appID
self.name = name
self.summary = summary
}
}
/// Initialization options for the about dialog wrapper.
public struct AdwaitaAboutDialogConfig {
/// The app's name.
public var appName: String?
/// The developer's name.
public var developer: String?
/// The app version.
public var version: String?
/// The app icon.
public var icon: Icon?
/// The app's website.
public var website: URL?
/// The link for opening issues.
public var issues: URL?
/// The link for getting support.
public var support: URL?
/// Additional links related to the app.
public var links: [AboutLink]?
/// The app's copyright information.
public var copyright: String?
/// The app's license.
public var license: String?
/// The app's release notes.
public var releaseNotes: String?
/// The comments about the application.
public var comments: String?
/// Recognition by name and role of contributors.
public var credits: [CreditEntry]?
/// Acknowledgements to display in the dialog.
public var acknowledgements: [AcknowledgementEntry]?
/// Additional applications.
public var otherApps: [OtherAppEntry]?
/// Initialize the about dialog wrapper.
/// - Parameters:
/// - appName: The app's name.
/// - developer: The developer's name.
/// - version: The version string.
/// - icon: The app icon.
/// - website: The app's website.
/// - issues: Website for reporting issues.
/// - support: Website for getting support.
/// - links: Additional links related to the app.
/// - copyright: The app's copyright information.
/// - license: The app's license.
/// - releaseNotes: The app's release notes.
/// - comments: The comments about the application.
/// - credits: Recognition by name and role of contributors.
/// - acknowledgements: List of acknowledgements.
/// - otherApps: List of other applications.
public init(
appName: String? = nil,
developer: String? = nil,
version: String? = nil,
icon: Icon? = nil,
website: URL? = nil,
issues: URL? = nil,
support: URL? = nil,
links: [AboutLink]? = nil,
copyright: String? = nil,
license: String? = nil,
releaseNotes: String? = nil,
comments: String? = nil,
credits: [CreditEntry]? = nil,
acknowledgements: [AcknowledgementEntry]? = nil,
otherApps: [OtherAppEntry]? = nil
) {
self.appName = appName
self.developer = developer
self.version = version
self.icon = icon
self.website = website
self.issues = issues
self.support = support
self.links = links
self.copyright = copyright
self.license = license
self.releaseNotes = releaseNotes
self.comments = comments
self.credits = credits
self.acknowledgements = acknowledgements
self.otherApps = otherApps
}
/// Applies a string value to the dialog using the given setter.
/// - Parameters:
/// - value: The optional string to apply.
/// - setter: The C function that sets the value on the dialog.
/// - dialog: The dialog instance.
@inline(__always)
private func set(
_ value: String?,
using setter: (OpaquePointer, UnsafePointer<CChar>?) -> Void,
on dialog: OpaquePointer
) {
guard let value else {
return
}
setter(dialog, value)
}
/// Applies a list of links to the dialog.
/// - Parameters:
/// - links: The optional list of links.
/// - dialog: The dialog instance.
@inline(__always)
private func set(
_ links: [AboutLink]?,
on dialog: OpaquePointer
) {
links?.forEach { link in
adw_about_dialog_add_link(dialog, link.title, link.url?.absoluteString)
}
}
/// Applies credit entries to the dialog.
/// - Parameters:
/// - credits: The optional list of credit entries.
/// - dialog: The dialog instance.
@inline(__always)
private func set(_ credits: [CreditEntry]?, on dialog: OpaquePointer) {
guard let credits else {
return
}
let grouped = Dictionary(grouping: credits, by: \.role)
if let devs = grouped[.developer]?.map(\.name),
let ptr = devs.cMutableArray {
adw_about_dialog_set_developers(dialog, ptr)
}
if let designers = grouped[.designer]?.map(\.name),
let ptr = designers.cMutableArray {
adw_about_dialog_set_designers(dialog, ptr)
}
if let artists = grouped[.artist]?.map(\.name),
let ptr = artists.cMutableArray {
adw_about_dialog_set_artists(dialog, ptr)
}
if let translators = grouped[.translator]?.map(\.name),
!translators.isEmpty {
let joined = translators.joined(separator: "\n")
adw_about_dialog_set_translator_credits(dialog, joined)
}
}
/// Applies acknowledgement entries to the dialog.
/// - Parameters:
/// - acknowledgements: The optional list of acknowledgement entries.
/// - dialog: The dialog instance.
@inline(__always)
private func set(
_ acknowledgements: [AcknowledgementEntry]?,
on dialog: OpaquePointer
) {
guard let acknowledgements else {
return
}
let grouped = Dictionary(grouping: acknowledgements) { $0.title }
for (title, entries) in grouped {
let names = entries.map { $0.name }
guard let people = names.cMutableArray else { continue }
adw_about_dialog_add_acknowledgement_section(dialog, title, people)
}
}
/// Applies other apps entries to the dialog.
/// - Parameters:
/// - otherApps: The optional list of other app entries.
/// - dialog: The dialog instance.
@inline(__always)
private func set(
_ otherApps: [OtherAppEntry]?,
on dialog: OpaquePointer
) {
guard let otherApps else {
return
}
for entry in otherApps {
adw_about_dialog_add_other_app(
dialog,
entry.appID,
entry.name,
entry.summary
)
}
}
/// Apply the configuration values to the given dialog.
/// - Parameters:
/// - dialog: The underlying Adwaita dialog instance to update with the configuration.
func apply(to dialog: OpaquePointer) {
set(appName, using: adw_about_dialog_set_application_name, on: dialog)
set(developer, using: adw_about_dialog_set_developer_name, on: dialog)
set(version, using: adw_about_dialog_set_version, on: dialog)
set(icon?.string, using: adw_about_dialog_set_application_icon, on: dialog)
set(website?.absoluteString, using: adw_about_dialog_set_website, on: dialog)
set(issues?.absoluteString, using: adw_about_dialog_set_issue_url, on: dialog)
set(support?.absoluteString, using: adw_about_dialog_set_support_url, on: dialog)
set(copyright, using: adw_about_dialog_set_copyright, on: dialog)
set(license, using: adw_about_dialog_set_license, on: dialog)
set(releaseNotes, using: adw_about_dialog_set_release_notes, on: dialog)
set(comments, using: adw_about_dialog_set_comments, on: dialog)
set(links, on: dialog)
set(credits, on: dialog)
set(acknowledgements, on: dialog)
set(otherApps, on: dialog)
}
}

View File

@ -5,11 +5,13 @@
// Created by david-swift on 06.08.23.
//
import Foundation
extension Array where Element == String {
/// Get the C version of the array.
var cArray: UnsafePointer<UnsafePointer<CChar>?>? {
let cStrings = self.map { $0.utf8CString }
let cStrings = map { $0.utf8CString }
let cStringPointers = cStrings.map { $0.withUnsafeBufferPointer { $0.baseAddress } }
let optionalCStringPointers = cStringPointers + [nil]
var optionalCStringPointersCopy = optionalCStringPointers
@ -25,4 +27,20 @@ extension Array where Element == String {
return UnsafePointer(pointer)
}
/// Get the mutable C version of the array.
var cMutableArray: UnsafeMutablePointer<UnsafePointer<CChar>?>? {
let pointer = UnsafeMutablePointer<UnsafePointer<CChar>?>.allocate(
capacity: count + 1
)
for (index, string) in enumerated() {
let cstr = strdup(string)
pointer[index] = UnsafePointer(cstr)
}
pointer[count] = nil
return pointer
}
}

View File

@ -15,19 +15,8 @@ struct AboutDialog: AdwaitaWidget {
@Binding var visible: Bool
/// The wrapped view.
var child: AnyView
/// The app's name.
var appName: String?
/// The developer's name.
var developer: String?
/// The app version.
var version: String?
/// The app icon.
var icon: Icon?
/// The app's website.
var website: URL?
/// The link for opening issues.
var issues: URL?
/// The dialog configuration options.
var config: AdwaitaAboutDialogConfig
/// The ID for the dialog's storage.
let dialogID = "dialog"
@ -36,30 +25,18 @@ struct AboutDialog: AdwaitaWidget {
/// - Parameters:
/// - visible: The visibility.
/// - child: The child view.
/// - appName: The app's name.
/// - developer: The developer's name.
/// - version: The version.
/// - icon: The icon.
/// - website: The website's URL.
/// - issues: The link for opening issues.
/// - configure: A closure that mutates the dialog configuration.
init(
visible: Binding<Bool>,
child: AnyView,
appName: String? = nil,
developer: String? = nil,
version: String? = nil,
icon: Icon? = nil,
website: URL? = nil,
issues: URL? = nil
configure: (inout AdwaitaAboutDialogConfig) -> Void
) {
self._visible = visible
self.child = child
self.appName = appName
self.developer = developer
self.version = version
self.icon = icon
self.website = website
self.issues = issues
var cfg = AdwaitaAboutDialogConfig()
configure(&cfg)
self.config = cfg
}
/// The view storage.
@ -97,26 +74,12 @@ struct AboutDialog: AdwaitaWidget {
storage.opaquePointer?.cast()
)
}
let dialog = storage.content[dialogID]?.first?.opaquePointer
if let appName {
adw_about_dialog_set_application_name(dialog, appName)
guard let dialog = storage.content[dialogID]?.first?.opaquePointer else {
return
}
if let developer {
adw_about_dialog_set_developer_name(dialog, developer)
}
if let version {
adw_about_dialog_set_version(dialog, version)
}
if let icon {
adw_about_dialog_set_application_icon(dialog, icon.string)
}
if let website {
adw_about_dialog_set_website(dialog, website.absoluteString)
}
if let issues {
adw_about_dialog_set_issue_url(dialog, issues.absoluteString)
}
adw_dialog_set_content_height(dialog?.cast(), -1)
config.apply(to: dialog)
adw_dialog_set_content_height(dialog.cast(), -1)
} else {
if storage.content[dialogID]?.first != nil {
let dialog = storage.content[dialogID]?.first?.opaquePointer

View File

@ -0,0 +1,115 @@
//
// AboutDialogDemo.swift
// Adwaita
//
// Created by lambdaclan on 24.01.26.
//
// swiftlint:disable missing_docs
import Adwaita
import Foundation
enum AboutDialogDemo {
static var sample: (inout AdwaitaAboutDialogConfig) -> Void {
{ cfg in
applyDemoConfig(&cfg)
}
}
private static let demoReleaseNotes = """
<p>This template supports three structures: paragraphs using <code>&lt;p&gt;</code>, ordered lists using
<code>&lt;ol&gt;</code>, and unordered lists using <code>&lt;ul&gt;</code>.
Both list types must contain list items marked with <code>&lt;li&gt;</code>.</p>
<p>Within paragraphs and list items, you may use <code>&lt;em&gt;</code> to apply
<em>emphasis</em>(italic text) and <code>&lt;code&gt;</code> to mark <code>inline code</code>
for monospaced text.
These inline styles are supported only inside those elements.</p>
<p>Any text placed outside <code>&lt;p&gt;</code>, <code>&lt;ol&gt;</code>,
<code>&lt;ul&gt;</code>, or <code>&lt;li&gt;</code> tags is ignored by the template
processor.</p>
<ol>
<li>Ordered list items represent numbered content and may
include <code>&lt;em&gt;</code> for <em>emphasis</em> or <code>&lt;code&gt;</code> for inline code.</li>
<li>They follow the same rules as paragraphs regarding allowed inline styles.</li>
</ol>
<ul>
<li>Unordered list items represent bullet points and support the same inline styles.</li>
<li>They must contain only text and allowed inline formatting.</li>
</ul>
"""
private static let demoComments = """
This text demonstrates basic Pango markup along with helpful documentation links.
Comments shown in an Adwaita AboutDialog will appear on the Details page.
They can be long and detailed, and they may include links and Pango markup for
formatting.
Pango markup supports tags like:
<b>bold</b>
<i>italic</i>
<span foreground="steelblue">colored text</span>
Full reference: <a href="https://docs.gtk.org/Pango/pango_markup.html">Pango Markup
Reference</a>
Example markup:
<span font="14pt" weight="bold">Demo Title</span>
<span foreground="tomato">Highlighted text</span>
<u>Underlined text</u>
Useful links:
<a href="https://adwaita-swift.aparoksha.dev/documentation/adwaita">AdwaitaSwift Documentation</a>
You can embed these links directly in your UI using Pango markup.
"""
private static func applyDemoConfig(_ cfg: inout AdwaitaAboutDialogConfig) {
cfg.appName = "Demo"
cfg.developer = "david-swift"
cfg.version = "Test"
cfg.icon = .default(icon: .applicationXExecutable)
cfg.website = URL(string: "https://adwaita-swift.aparoksha.dev/tutorials/table-of-contents")
cfg.issues = URL(string: "https://git.aparoksha.dev/aparoksha/adwaita-swift/issues")
cfg.support = URL(string: "https://adwaita-swift.aparoksha.dev/")
cfg.links = [
.init(title: "Source Code", url: URL(string: "https://git.aparoksha.dev/aparoksha/adwaita-swift")),
.init(title: "Donate", url: URL(string: "https://ko-fi.com/david_swift"))
]
cfg.copyright = "© 2026 david-swift"
cfg.license = "MIT"
cfg.releaseNotes = demoReleaseNotes
cfg.comments = demoComments
cfg.credits = [
.init(role: .developer, name: "Jane Doe"),
.init(role: .developer, name: "John Roe"),
.init(role: .designer, name: "Mika Sato"),
.init(role: .artist, name: "Leo Martins"),
.init(role: .translator, name: "Yuki Nakamura"),
.init(role: .translator, name: "Tod Brown")
]
cfg.acknowledgements = [
.init(title: "Special Thanks", name: "GNOME Project"),
.init(title: "Special Thanks", name: "Swift Programming Language"),
.init(title: "Additional Support", name: "LibAdwaita Contributors")
]
cfg.otherApps = [
.init(
appID: "io.github.david_swift.Flashcards",
name: "Memorize",
summary: "An app for creating, studying, and importing flashcard sets with a builtin test mode."
)
]
}
}
// swiftlint:enable missing_docs

View File

@ -150,15 +150,7 @@ struct Demo: App {
}
.collapsed(!wide)
.breakpoint(minWidth: 550, matches: $wide)
.aboutDialog(
visible: $about,
app: "Demo",
developer: "david-swift",
version: "Test",
icon: .default(icon: .applicationXExecutable),
website: .init(string: "https://adwaita-swift.aparoksha.dev/"),
issues: .init(string: "https://git.aparoksha.dev/aparoksha/adwaita-swift/issues")
)
.aboutDialog(visible: $about, configure: AboutDialogDemo.sample)
.preferencesDialog(visible: $preferences)
.preferencesPage("Page 1", icon: .default(icon: .audioHeadset)) { page in
page