Add support for keypaths where sensible #74
All checks were successful
SwiftLint / SwiftLint (push) Successful in 7s
Deploy Docs / publish (push) Successful in 1h49m51s

This commit is contained in:
david-swift 2026-02-04 16:36:51 +01:00
parent c08a9dc3bd
commit 2072c16040
60 changed files with 326 additions and 178 deletions

View File

@ -17,27 +17,40 @@ extension DropDown {
/// Initialize a combo row. /// Initialize a combo row.
/// - Parameters: /// - Parameters:
/// - title: The row's title.
/// - selection: The selected value. /// - selection: The selected value.
/// - values: The available values. /// - values: The available values.
public init<Element>( public init<Element>(
selection: Binding<Element.ID>, selection: Binding<Element.ID>,
values: [Element] values: [Element]
) where Element: Identifiable, Element: CustomStringConvertible { ) where Element: Identifiable, Element: CustomStringConvertible {
self.init(selection: selection, values: values, id: \.id, description: \.description)
}
/// Initialize a combo row.
/// - Parameters:
/// - title: The row's title.
/// - selection: The selected value.
/// - values: The available values.
public init<Element, Identifier>(
selection: Binding<Identifier>,
values: [Element],
id: KeyPath<Element, Identifier>,
description: KeyPath<Element, String>
) where Identifier: Equatable {
self.init() self.init()
self = self.selected(.init { self = self.selected(.init {
.init(values.firstIndex { $0.id == selection.wrappedValue } ?? 0) .init(values.firstIndex { $0[keyPath: id] == selection.wrappedValue } ?? 0)
} set: { index in } set: { index in
if let id = values[safe: .init(index)]?.id { if let id = values[safe: .init(index)]?[keyPath: id] {
selection.wrappedValue = id selection.wrappedValue = id
} }
}) })
appearFunctions.append { storage, _ in appearFunctions.append { storage, _ in
storage.fields[Self.stringList] = gtk_drop_down_get_model(storage.opaquePointer) storage.fields[Self.stringList] = gtk_drop_down_get_model(storage.opaquePointer)
Self.updateContent(storage: storage, values: values, element: Element.self) Self.updateContent(storage: storage, values: values, id: id, description: description)
} }
updateFunctions.append { storage, _, _ in updateFunctions.append { storage, _, _ in
Self.updateContent(storage: storage, values: values, element: Element.self) Self.updateContent(storage: storage, values: values, id: id, description: description)
} }
} }
@ -45,20 +58,23 @@ extension DropDown {
/// - Parameters: /// - Parameters:
/// - storage: The view storage. /// - storage: The view storage.
/// - values: The elements. /// - values: The elements.
/// - element: The type of the elements. /// - id: The keypath to the id.
static func updateContent<Element>( /// - description: The keypath to the description.
static func updateContent<Element, Identifier>(
storage: ViewStorage, storage: ViewStorage,
values: [Element], values: [Element],
element: Element.Type id: KeyPath<Element, Identifier>,
) where Element: Identifiable, Element: CustomStringConvertible { description: KeyPath<Element, String>
) where Identifier: Equatable {
if let list = storage.fields[Self.stringList] as? OpaquePointer { if let list = storage.fields[Self.stringList] as? OpaquePointer {
let old = storage.fields[Self.values] as? [Element] ?? [] let old = storage.fields[Self.values] as? [Element] ?? []
old.identifiableTransform( old.transform(
to: values, to: values,
id: id,
functions: .init { index in functions: .init { index in
gtk_string_list_remove(list, .init(index)) gtk_string_list_remove(list, .init(index))
} insert: { _, element in } insert: { _, element in
gtk_string_list_append(list, element.description) gtk_string_list_append(list, element[keyPath: description])
} }
) )
storage.fields[Self.values] = values storage.fields[Self.values] = values

View File

@ -17,19 +17,21 @@ extension FlowBox {
/// Initialize `FlowBox`. /// Initialize `FlowBox`.
/// - Parameters: /// - Parameters:
/// - elements: The elements. /// - elements: The elements.
/// - id: The element identifier keypath.
/// - selection: The identifier of the selected element. Selection disabled if `nil`. /// - selection: The identifier of the selected element. Selection disabled if `nil`.
/// - content: The view for an element. /// - content: The view for an element.
public init( public init(
_ elements: [Element], _ elements: [Element],
selection: Binding<Element.ID>? = nil, id: KeyPath<Element, Identifier>,
selection: Binding<Identifier>? = nil,
@ViewBuilder content: @escaping (Element) -> Body @ViewBuilder content: @escaping (Element) -> Body
) { ) {
self.init(elements, content: content) self.init(elements, id: id, content: content)
let id: (ViewStorage, [Element]) -> Element.ID? = { storage, elements in let getID: (ViewStorage, [Element]) -> Identifier? = { storage, elements in
if let child = g_list_nth_data(gtk_flow_box_get_selected_children(storage.opaquePointer), 0) { if let child = g_list_nth_data(gtk_flow_box_get_selected_children(storage.opaquePointer), 0) {
let element = gtk_flow_box_child_get_child(child.cast()) let element = gtk_flow_box_child_get_child(child.cast())
return elements[safe: storage.content[.mainContent]? return elements[safe: storage.content[.mainContent]?
.firstIndex { $0.opaquePointer?.cast() == element }]?.id .firstIndex { $0.opaquePointer?.cast() == element }]?[keyPath: id]
} }
return nil return nil
} }
@ -37,12 +39,12 @@ extension FlowBox {
updateFunctions.append { storage, _, _ in updateFunctions.append { storage, _, _ in
storage.connectSignal(name: "selected_children_changed", id: Self.selectionField) { storage.connectSignal(name: "selected_children_changed", id: Self.selectionField) {
if let elements = storage.fields[Self.elementsField] as? [Element], if let elements = storage.fields[Self.elementsField] as? [Element],
let id = id(storage, elements) { let id = getID(storage, elements) {
selection.wrappedValue = id selection.wrappedValue = id
} }
} }
if selection.wrappedValue != id(storage, elements), if selection.wrappedValue != getID(storage, elements),
let index = elements.firstIndex(where: { $0.id == selection.wrappedValue })?.cInt { let index = elements.firstIndex(where: { $0[keyPath: id] == selection.wrappedValue })?.cInt {
gtk_flow_box_select_child( gtk_flow_box_select_child(
storage.opaquePointer, storage.opaquePointer,
gtk_flow_box_get_child_at_index(storage.opaquePointer, index) gtk_flow_box_get_child_at_index(storage.opaquePointer, index)
@ -57,3 +59,20 @@ extension FlowBox {
} }
} }
extension FlowBox where Element: Identifiable, Identifier == Element.ID {
/// Initialize `FlowBox`.
/// - Parameters:
/// - elements: The elements.
/// - selection: The identifier of the selected element. Selection disabled if `nil`.
/// - content: The view for an element.
public init(
_ elements: [Element],
selection: Binding<Element.ID>? = nil,
@ViewBuilder content: @escaping (Element) -> Body
) {
self.init(elements, id: \.id, selection: selection, content: content)
}
}

View File

@ -9,7 +9,7 @@ import CAdw
import LevenshteinTransformations import LevenshteinTransformations
/// A dynamic list but without a list design in the user interface. /// A dynamic list but without a list design in the user interface.
public struct ForEach<Element>: AdwaitaWidget where Element: Identifiable { public struct ForEach<Element, Identifier>: AdwaitaWidget where Identifier: Equatable {
/// The dynamic widget elements. /// The dynamic widget elements.
var elements: [Element] var elements: [Element]
@ -19,12 +19,20 @@ public struct ForEach<Element>: AdwaitaWidget where Element: Identifiable {
var horizontal: Bool var horizontal: Bool
/// Whether the children should all be the same size. /// Whether the children should all be the same size.
var homogeneous: Bool? var homogeneous: Bool?
/// The path to the identifier.
var id: KeyPath<Element, Identifier>
/// Initialize `ForEach`. /// Initialize `ForEach`.
public init(_ elements: [Element], horizontal: Bool = false, @ViewBuilder content: @escaping (Element) -> Body) { public init(
_ elements: [Element],
id: KeyPath<Element, Identifier>,
horizontal: Bool = false,
@ViewBuilder content: @escaping (Element) -> Body
) {
self.elements = elements self.elements = elements
self.content = content self.content = content
self.horizontal = horizontal self.horizontal = horizontal
self.id = id
} }
/// The view storage. /// The view storage.
@ -56,8 +64,9 @@ public struct ForEach<Element>: AdwaitaWidget where Element: Identifiable {
var contentStorage: [ViewStorage] = storage.content[.mainContent] ?? [] var contentStorage: [ViewStorage] = storage.content[.mainContent] ?? []
let old = storage.fields["element"] as? [Element] ?? [] let old = storage.fields["element"] as? [Element] ?? []
let widget: UnsafeMutablePointer<GtkBox>? = storage.opaquePointer?.cast() let widget: UnsafeMutablePointer<GtkBox>? = storage.opaquePointer?.cast()
old.identifiableTransform( old.transform(
to: elements, to: elements,
id: id,
functions: .init { index in functions: .init { index in
let child = contentStorage[safe: index]?.opaquePointer let child = contentStorage[safe: index]?.opaquePointer
gtk_box_remove(widget, child?.cast()) gtk_box_remove(widget, child?.cast())
@ -105,3 +114,15 @@ public struct ForEach<Element>: AdwaitaWidget where Element: Identifiable {
} }
} }
extension ForEach where Element: Identifiable, Identifier == Element.ID {
/// Initialize `ForEach`.
public init(_ elements: [Element], horizontal: Bool = false, @ViewBuilder content: @escaping (Element) -> Body) {
self.elements = elements
self.content = content
self.horizontal = horizontal
self.id = \.id
}
}

View File

@ -26,11 +26,26 @@ extension ComboRow {
selection: Binding<Element.ID>, selection: Binding<Element.ID>,
values: [Element] values: [Element]
) where Element: Identifiable, Element: CustomStringConvertible { ) where Element: Identifiable, Element: CustomStringConvertible {
self.init(title, selection: selection, values: values, id: \.id, description: \.description)
}
/// Initialize a combo row.
/// - Parameters:
/// - title: The row's title.
/// - selection: The selected value.
/// - values: The available values.
public init<Element, Identifier>(
_ title: String,
selection: Binding<Identifier>,
values: [Element],
id: KeyPath<Element, Identifier>,
description: KeyPath<Element, String>
) where Identifier: Equatable {
self = self.title(title) self = self.title(title)
self = self.selected(.init { self = self.selected(.init {
.init(values.firstIndex { $0.id == selection.wrappedValue } ?? 0) .init(values.firstIndex { $0[keyPath: id] == selection.wrappedValue } ?? 0)
} set: { index in } set: { index in
if let id = values[safe: .init(index)]?.id { if let id = values[safe: .init(index)]?[keyPath: id] {
selection.wrappedValue = id selection.wrappedValue = id
} }
}) })
@ -39,34 +54,10 @@ extension ComboRow {
storage.fields[Self.stringList] = list storage.fields[Self.stringList] = list
adw_combo_row_set_model(storage.opaquePointer?.cast(), list) adw_combo_row_set_model(storage.opaquePointer?.cast(), list)
g_object_unref(list?.cast()) g_object_unref(list?.cast())
Self.updateContent(storage: storage, values: values, element: Element.self) DropDown.updateContent(storage: storage, values: values, id: id, description: description)
} }
updateFunctions.append { storage, _, _ in updateFunctions.append { storage, _, _ in
Self.updateContent(storage: storage, values: values, element: Element.self) DropDown.updateContent(storage: storage, values: values, id: id, description: description)
}
}
/// Update the combo row's content.
/// - Parameters:
/// - storage: The view storage.
/// - values: The elements.
/// - element: The type of the elements.
static func updateContent<Element>(
storage: ViewStorage,
values: [Element],
element: Element.Type
) where Element: Identifiable, Element: CustomStringConvertible {
if let list = storage.fields[Self.stringList] as? OpaquePointer {
let old = storage.fields[Self.values] as? [Element] ?? []
old.identifiableTransform(
to: values,
functions: .init { index in
gtk_string_list_remove(list, .init(index))
} insert: { _, element in
gtk_string_list_append(list, element.description)
}
)
storage.fields[Self.values] = values
} }
} }

View File

@ -2,7 +2,7 @@
// ActionRow.swift // ActionRow.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// AspectFrame.swift // AspectFrame.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// Avatar.swift // Avatar.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// Banner.swift // Banner.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// Bin.swift // Bin.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// Box.swift // Box.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// Button.swift // Button.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// ButtonContent.swift // ButtonContent.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// Carousel.swift // Carousel.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw
@ -19,7 +19,7 @@ import LevenshteinTransformations
/// to provide page indicators for `AdwCarousel`. /// to provide page indicators for `AdwCarousel`.
/// ///
/// ///
public struct Carousel<Element>: AdwaitaWidget where Element: Identifiable { public struct Carousel<Element, Identifier>: AdwaitaWidget where Identifier: Equatable {
#if exposeGeneratedAppearUpdateFunctions #if exposeGeneratedAppearUpdateFunctions
/// Additional update functions for type extensions. /// Additional update functions for type extensions.
@ -70,11 +70,14 @@ public struct Carousel<Element>: AdwaitaWidget where Element: Identifiable {
var elements: [Element] var elements: [Element]
/// The dynamic widget content. /// The dynamic widget content.
var content: (Element) -> Body var content: (Element) -> Body
/// The dynamic widget identifier key path.
var id: KeyPath<Element, Identifier>
/// Initialize `Carousel`. /// Initialize `Carousel`.
public init(_ elements: [Element], @ViewBuilder content: @escaping (Element) -> Body) { public init(_ elements: [Element], id: KeyPath<Element, Identifier>, @ViewBuilder content: @escaping (Element) -> Body) {
self.elements = elements self.elements = elements
self.content = content self.content = content
self.id = id
} }
/// The view storage. /// The view storage.
@ -126,8 +129,9 @@ public struct Carousel<Element>: AdwaitaWidget where Element: Identifiable {
var contentStorage: [ViewStorage] = storage.content[.mainContent] ?? [] var contentStorage: [ViewStorage] = storage.content[.mainContent] ?? []
let old = storage.fields["element"] as? [Element] ?? [] let old = storage.fields["element"] as? [Element] ?? []
old.identifiableTransform( old.transform(
to: elements, to: elements,
id: id,
functions: .init { index in functions: .init { index in
adw_carousel_remove(widget, adw_carousel_get_nth_page(widget, UInt(index).cInt)) adw_carousel_remove(widget, adw_carousel_get_nth_page(widget, UInt(index).cInt))
contentStorage.remove(at: index) contentStorage.remove(at: index)
@ -212,3 +216,14 @@ public struct Carousel<Element>: AdwaitaWidget where Element: Identifiable {
} }
} }
extension Carousel where Element: Identifiable, Identifier == Element.ID {
/// Initialize `Carousel`.
public init(_ elements: [Element], @ViewBuilder content: @escaping (Element) -> Body) {
self.elements = elements
self.content = content
self.id = \.id
}
}

View File

@ -2,7 +2,7 @@
// CenterBox.swift // CenterBox.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// CheckButton.swift // CheckButton.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// Clamp.swift // Clamp.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// ComboRow.swift // ComboRow.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// DropDown.swift // DropDown.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// Entry.swift // Entry.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// EntryRow.swift // EntryRow.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// ExpanderRow.swift // ExpanderRow.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// Fixed.swift // Fixed.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// FlowBox.swift // FlowBox.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw
@ -37,7 +37,7 @@ import LevenshteinTransformations
/// Also see `Gtk.ListBox`. /// Also see `Gtk.ListBox`.
/// ///
/// ///
public struct FlowBox<Element>: AdwaitaWidget where Element: Identifiable { public struct FlowBox<Element, Identifier>: AdwaitaWidget where Identifier: Equatable {
#if exposeGeneratedAppearUpdateFunctions #if exposeGeneratedAppearUpdateFunctions
/// Additional update functions for type extensions. /// Additional update functions for type extensions.
@ -130,11 +130,14 @@ public struct FlowBox<Element>: AdwaitaWidget where Element: Identifiable {
var elements: [Element] var elements: [Element]
/// The dynamic widget content. /// The dynamic widget content.
var content: (Element) -> Body var content: (Element) -> Body
/// The dynamic widget identifier key path.
var id: KeyPath<Element, Identifier>
/// Initialize `FlowBox`. /// Initialize `FlowBox`.
init(_ elements: [Element], @ViewBuilder content: @escaping (Element) -> Body) { init(_ elements: [Element], id: KeyPath<Element, Identifier>, @ViewBuilder content: @escaping (Element) -> Body) {
self.elements = elements self.elements = elements
self.content = content self.content = content
self.id = id
} }
/// The view storage. /// The view storage.
@ -216,8 +219,9 @@ public struct FlowBox<Element>: AdwaitaWidget where Element: Identifiable {
var contentStorage: [ViewStorage] = storage.content[.mainContent] ?? [] var contentStorage: [ViewStorage] = storage.content[.mainContent] ?? []
let old = storage.fields["element"] as? [Element] ?? [] let old = storage.fields["element"] as? [Element] ?? []
old.identifiableTransform( old.transform(
to: elements, to: elements,
id: id,
functions: .init { index in functions: .init { index in
gtk_flow_box_remove(widget, gtk_flow_box_get_child_at_index(widget, index.cInt)?.cast()) gtk_flow_box_remove(widget, gtk_flow_box_get_child_at_index(widget, index.cInt)?.cast())
contentStorage.remove(at: index) contentStorage.remove(at: index)
@ -377,3 +381,14 @@ public struct FlowBox<Element>: AdwaitaWidget where Element: Identifiable {
} }
} }
extension FlowBox where Element: Identifiable, Identifier == Element.ID {
/// Initialize `FlowBox`.
public init(_ elements: [Element], @ViewBuilder content: @escaping (Element) -> Body) {
self.elements = elements
self.content = content
self.id = \.id
}
}

View File

@ -2,7 +2,7 @@
// HeaderBar.swift // HeaderBar.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// Image.swift // Image.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// Label.swift // Label.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// LevelBar.swift // LevelBar.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// LinkButton.swift // LinkButton.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// ListBox.swift // ListBox.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw
@ -34,7 +34,7 @@ import LevenshteinTransformations
/// as selected when the user tries to select it. /// as selected when the user tries to select it.
/// ///
/// ///
public struct ListBox<Element>: AdwaitaWidget where Element: Identifiable { public struct ListBox<Element, Identifier>: AdwaitaWidget where Identifier: Equatable {
#if exposeGeneratedAppearUpdateFunctions #if exposeGeneratedAppearUpdateFunctions
/// Additional update functions for type extensions. /// Additional update functions for type extensions.
@ -107,11 +107,14 @@ public struct ListBox<Element>: AdwaitaWidget where Element: Identifiable {
var elements: [Element] var elements: [Element]
/// The dynamic widget content. /// The dynamic widget content.
var content: (Element) -> Body var content: (Element) -> Body
/// The dynamic widget identifier key path.
var id: KeyPath<Element, Identifier>
/// Initialize `ListBox`. /// Initialize `ListBox`.
public init(_ elements: [Element], @ViewBuilder content: @escaping (Element) -> Body) { public init(_ elements: [Element], id: KeyPath<Element, Identifier>, @ViewBuilder content: @escaping (Element) -> Body) {
self.elements = elements self.elements = elements
self.content = content self.content = content
self.id = id
} }
/// The view storage. /// The view storage.
@ -186,8 +189,9 @@ public struct ListBox<Element>: AdwaitaWidget where Element: Identifiable {
var contentStorage: [ViewStorage] = storage.content[.mainContent] ?? [] var contentStorage: [ViewStorage] = storage.content[.mainContent] ?? []
let old = storage.fields["element"] as? [Element] ?? [] let old = storage.fields["element"] as? [Element] ?? []
old.identifiableTransform( old.transform(
to: elements, to: elements,
id: id,
functions: .init { index in functions: .init { index in
gtk_list_box_remove(widget, gtk_list_box_get_row_at_index(widget, index.cInt)?.cast()) gtk_list_box_remove(widget, gtk_list_box_get_row_at_index(widget, index.cInt)?.cast())
contentStorage.remove(at: index) contentStorage.remove(at: index)
@ -320,3 +324,14 @@ public struct ListBox<Element>: AdwaitaWidget where Element: Identifiable {
} }
} }
extension ListBox where Element: Identifiable, Identifier == Element.ID {
/// Initialize `ListBox`.
public init(_ elements: [Element], @ViewBuilder content: @escaping (Element) -> Body) {
self.elements = elements
self.content = content
self.id = \.id
}
}

View File

@ -2,7 +2,7 @@
// Menu.swift // Menu.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// NavigationView.swift // NavigationView.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// Overlay.swift // Overlay.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// OverlaySplitView.swift // OverlaySplitView.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// PasswordEntryRow.swift // PasswordEntryRow.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// Picture.swift // Picture.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// Popover.swift // Popover.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// PreferencesGroup.swift // PreferencesGroup.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// PreferencesPage.swift // PreferencesPage.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// PreferencesRow.swift // PreferencesRow.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// ProgressBar.swift // ProgressBar.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// ScrolledWindow.swift // ScrolledWindow.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// SearchBar.swift // SearchBar.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// SearchEntry.swift // SearchEntry.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// Separator.swift // Separator.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// SpinRow.swift // SpinRow.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// Spinner.swift // Spinner.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// SplitButton.swift // SplitButton.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// StatusPage.swift // StatusPage.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// SwitchRow.swift // SwitchRow.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// ToastOverlay.swift // ToastOverlay.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// ToggleButton.swift // ToggleButton.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// ToggleGroup.swift // ToggleGroup.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// ToolbarView.swift // ToolbarView.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -2,7 +2,7 @@
// WindowTitle.swift // WindowTitle.swift
// Adwaita // Adwaita
// //
// Created by auto-generation on 03.02.26. // Created by auto-generation on 04.02.26.
// //
import CAdw import CAdw

View File

@ -1,24 +0,0 @@
//
// List+.swift
// Adwaita
//
// Created by david-swift on 16.10.24.
//
extension List {
/// Add the "navigation-sidebar" style class.
/// - Parameter active: Whether the style is applied.
/// - Returns: A view.
public func sidebarStyle(_ active: Bool = true) -> AnyView {
style("navigation-sidebar", active: active)
}
/// Apply the boxed list style class.
/// - Parameter active: Whether the style is applied.
/// - Returns: A view.
public func boxedList(_ active: Bool = true) -> AnyView {
style("boxed-list", active: active)
}
}

View File

@ -21,17 +21,19 @@ extension List {
/// Initialize `List`. /// Initialize `List`.
/// - Parameters: /// - Parameters:
/// - elements: The elements. /// - elements: The elements.
/// - id: The key path to the elements' identifiers.
/// - selection: The identifier of the selected element. Selection disabled if `nil`. /// - selection: The identifier of the selected element. Selection disabled if `nil`.
/// - content: The view for an element. /// - content: The view for an element.
public init( public init(
_ elements: [Element], _ elements: [Element],
selection: Binding<Element.ID>?, id: KeyPath<Element, Identifier>,
selection: Binding<Identifier>?,
@ViewBuilder content: @escaping (Element) -> Body @ViewBuilder content: @escaping (Element) -> Body
) { ) {
self.init(elements, content: content) self.init(elements, id: id, content: content)
let id: (ViewStorage, [Element]) -> Element.ID? = { storage, elements in let getID: (ViewStorage, [Element]) -> Identifier? = { storage, elements in
if let row = gtk_list_box_get_selected_row(storage.opaquePointer) { if let row = gtk_list_box_get_selected_row(storage.opaquePointer) {
return elements[safe: .init(gtk_list_box_row_get_index(row))]?.id return elements[safe: .init(gtk_list_box_row_get_index(row))]?[keyPath: id]
} }
return nil return nil
} }
@ -39,13 +41,13 @@ extension List {
updateFunctions.append { storage, _, _ in updateFunctions.append { storage, _, _ in
storage.connectSignal(name: "selected_rows_changed", id: Self.selectionField) { storage.connectSignal(name: "selected_rows_changed", id: Self.selectionField) {
if let elements = storage.fields[Self.elementsField] as? [Element], if let elements = storage.fields[Self.elementsField] as? [Element],
let id = id(storage, elements), let id = getID(storage, elements),
selection.wrappedValue != id { selection.wrappedValue != id {
selection.wrappedValue = id selection.wrappedValue = id
} }
} }
if selection.wrappedValue != id(storage, elements), if selection.wrappedValue != getID(storage, elements),
let index = elements.firstIndex(where: { $0.id == selection.wrappedValue })?.cInt { let index = elements.firstIndex(where: { $0[keyPath: id] == selection.wrappedValue })?.cInt {
gtk_list_box_select_row( gtk_list_box_select_row(
storage.opaquePointer, storage.opaquePointer,
gtk_list_box_get_row_at_index(storage.opaquePointer, index) gtk_list_box_get_row_at_index(storage.opaquePointer, index)
@ -59,4 +61,35 @@ extension List {
} }
} }
/// Add the "navigation-sidebar" style class.
/// - Parameter active: Whether the style is applied.
/// - Returns: A view.
public func sidebarStyle(_ active: Bool = true) -> AnyView {
style("navigation-sidebar", active: active)
}
/// Apply the boxed list style class.
/// - Parameter active: Whether the style is applied.
/// - Returns: A view.
public func boxedList(_ active: Bool = true) -> AnyView {
style("boxed-list", active: active)
}
}
extension List where Element: Identifiable, Element.ID == Identifier {
/// Initialize `List`.
/// - Parameters:
/// - elements: The elements.
/// - selection: The identifier of the selected element. Selection disabled if `nil`.
/// - content: The view for an element.
public init(
_ elements: [Element],
selection: Binding<Element.ID>?,
@ViewBuilder content: @escaping (Element) -> Body
) {
self.init(elements, id: \.id, selection: selection, content: content)
}
} }

View File

@ -23,72 +23,103 @@ extension ToggleGroup {
selection: Binding<Element.ID>, selection: Binding<Element.ID>,
values: [Element] values: [Element]
) where Element: ToggleGroupItem { ) where Element: ToggleGroupItem {
self.init(
selection: selection,
values: values,
id: \.id,
label: \.id.description,
icon: \.icon,
showLabel: \.showLabel
)
}
/// Initialize a toggle group.
/// - Parameters:
/// - selection: The selected value.
/// - values: The available values.
/// - id: The path to the identifier.
/// - label: The path to the label.
/// - icon: The path to the icon.
/// - showLabel: The path to the boolean that defines whether to show an element's label.
public init<Element, Identifier>(
selection: Binding<Identifier>,
values: [Element],
id: KeyPath<Element, Identifier>,
label: KeyPath<Element, String>,
icon: KeyPath<Element, Icon?>? = nil,
showLabel: KeyPath<Element, Bool>? = nil
) where Identifier: Equatable {
self.init() self.init()
appearFunctions.append { storage, _ in appearFunctions.append { storage, _ in
storage.notify(name: "active-name", id: "init") { storage.notify(name: "active-name", id: "init") {
if let name = adw_toggle_group_get_active_name(storage.opaquePointer), if let name = adw_toggle_group_get_active_name(storage.opaquePointer),
let values = storage.fields[Self.values] as? [Element], let values = storage.fields[Self.values] as? [Element],
let value = values.first(where: { $0.id.description == String(cString: name) }) { let identifier = values
selection.wrappedValue = value.id .map({ $0[keyPath: label] }).first(where: { $0.description == String(cString: name) }),
let value = values.first(where: { $0[keyPath: label] == identifier }) {
selection.wrappedValue = value[keyPath: id]
} }
} }
Self.updateContent(
storage: storage,
selection: selection.wrappedValue,
values: values,
updateProperties: true
)
} }
updateFunctions.append { storage, _, updateProperties in updateFunctions.append { storage, _, updateProperties in
Self.updateContent( Self.updateContent(
storage: storage, storage: storage,
selection: selection.wrappedValue, selection: selection.wrappedValue,
values: values, values: values,
id: id,
label: label,
icon: icon,
showLabel: showLabel,
updateProperties: updateProperties updateProperties: updateProperties
) )
} }
} }
// swiftlint:disable function_parameter_count
/// Update the combo row's content. /// Update the combo row's content.
/// - Parameters: static func updateContent<Element, Identifier>(
/// - storage: The view storage.
/// - values: The elements.
/// - updateProperties: Whether to update the properties.
static func updateContent<Element>(
storage: ViewStorage, storage: ViewStorage,
selection: Element.ID, selection: Identifier,
values: [Element], values: [Element],
id: KeyPath<Element, Identifier>,
label: KeyPath<Element, String>,
icon: KeyPath<Element, Icon?>?,
showLabel: KeyPath<Element, Bool>?,
updateProperties: Bool updateProperties: Bool
) where Element: ToggleGroupItem { ) where Identifier: Equatable {
guard updateProperties else { guard updateProperties else {
return return
} }
let old = storage.fields[Self.values] as? [Element] ?? [] let old = storage.fields[Self.values] as? [Element] ?? []
old.identifiableTransform( old.transform(
to: values, to: values,
id: id,
functions: .init { index in functions: .init { index in
if let id = old[safe: index]?.id.description, if let id = old[safe: index]?[keyPath: label],
let toggle = storage.fields[Self.toggle + id] as? OpaquePointer { let toggle = storage.fields[Self.toggle + id] as? OpaquePointer {
adw_toggle_group_remove(storage.opaquePointer, toggle) adw_toggle_group_remove(storage.opaquePointer, toggle)
} }
} insert: { _, element in } insert: { _, element in
let toggle = adw_toggle_new() let toggle = adw_toggle_new()
adw_toggle_set_name(toggle, element.id.description) adw_toggle_set_name(toggle, element[keyPath: label])
if element.showLabel { if let showLabel, !element[keyPath: showLabel] {
adw_toggle_set_label(toggle, element.id.description) adw_toggle_set_tooltip(toggle, element[keyPath: label])
} else { } else {
adw_toggle_set_tooltip(toggle, element.id.description) adw_toggle_set_label(toggle, element[keyPath: label])
} }
if let icon = element.icon { if let icon, let icon = element[keyPath: icon] {
adw_toggle_set_icon_name(toggle, icon.string) adw_toggle_set_icon_name(toggle, icon.string)
} }
storage.fields[Self.toggle + element.id.description] = toggle storage.fields[Self.toggle + element[keyPath: label]] = toggle
adw_toggle_group_add(storage.opaquePointer, toggle) adw_toggle_group_add(storage.opaquePointer, toggle)
} }
) )
storage.fields[Self.values] = values storage.fields[Self.values] = values
adw_toggle_group_set_active_name(storage.opaquePointer, selection.description) if let selection = values.first(where: { $0[keyPath: id] == selection }) {
adw_toggle_group_set_active_name(storage.opaquePointer, selection[keyPath: label])
}
} }
// swiftlint:enable function_parameter_count
} }

View File

@ -15,8 +15,13 @@ struct ToggleGroupDemo: View {
@State private var selection: Subview = .view1 @State private var selection: Subview = .view1
var view: Body { var view: Body {
ToggleGroup(selection: $selection, values: Subview.allCases) ToggleGroup(
.padding() selection: $selection,
values: Subview.allCases,
id: \.self,
label: \.rawValue
)
.padding()
VStack { VStack {
Text(selection.rawValue) Text(selection.rawValue)
.padding() .padding()
@ -26,19 +31,11 @@ struct ToggleGroupDemo: View {
.padding() .padding()
} }
enum Subview: String, ToggleGroupItem, CaseIterable, CustomStringConvertible { enum Subview: String, CaseIterable, Equatable {
case view1 = "View 1" case view1 = "View 1"
case view2 = "View 2" case view2 = "View 2"
var id: Self { self }
var description: String { rawValue }
var icon: Icon? { nil }
var showLabel: Bool { true }
} }
} }

View File

@ -24,7 +24,7 @@ extension Class {
.filter { config.requiredProperties.contains($0.name) } .filter { config.requiredProperties.contains($0.name) }
var initializer = "\(config.internalInitializer ? "" : "public ")init(" var initializer = "\(config.internalInitializer ? "" : "public ")init("
if config.dynamicWidget != nil { if config.dynamicWidget != nil {
initializer.append("_ elements: [Element], ") initializer.append("_ elements: [Element], id: KeyPath<Element, Identifier>, ")
} }
for property in requiredProperties { for property in requiredProperties {
initializer.append("\(property.parameter(config: config, genConfig: genConfig)), ") initializer.append("\(property.parameter(config: config, genConfig: genConfig)), ")
@ -46,6 +46,7 @@ extension Class {
self.elements = elements self.elements = elements
self.content = content self.content = content
self.id = id
""" """
) )
} }
@ -167,6 +168,8 @@ extension Class {
var elements: [Element] var elements: [Element]
/// The dynamic widget content. /// The dynamic widget content.
var content: (Element) -> Body var content: (Element) -> Body
/// The dynamic widget identifier key path.
var id: KeyPath<Element, Identifier>
""" """
} }
content += staticWidgetProperties(namespace: namespace, configs: configs) content += staticWidgetProperties(namespace: namespace, configs: configs)
@ -226,8 +229,9 @@ extension Class {
return """ return """
var contentStorage: [ViewStorage] = storage.content[.mainContent] ?? [] var contentStorage: [ViewStorage] = storage.content[.mainContent] ?? []
let old = storage.fields["element"] as? [Element] ?? [] let old = storage.fields["element"] as? [Element] ?? []
old.identifiableTransform( old.transform(
to: elements, to: elements,
id: id,
functions: .init { index in functions: .init { index in
\(dynamicWidget.remove)(\(widget), \(dynamicWidget.getElement)) \(dynamicWidget.remove)(\(widget), \(dynamicWidget.getElement))
contentStorage.remove(at: index) contentStorage.remove(at: index)

View File

@ -64,10 +64,25 @@ struct Class: ClassLike, Decodable {
dateFormatter.dateFormat = "dd.MM.yy" dateFormatter.dateFormat = "dd.MM.yy"
let widgetName = config.name ?? config.class let widgetName = config.name ?? config.class
let definition: String let definition: String
var extensions = ""
if config.dynamicWidget == nil { if config.dynamicWidget == nil {
definition = "\(widgetName): AdwaitaWidget" definition = "\(widgetName): AdwaitaWidget"
} else { } else {
definition = "\(widgetName)<Element>: AdwaitaWidget where Element: Identifiable" definition = "\(widgetName)<Element, Identifier>: AdwaitaWidget where Identifier: Equatable"
extensions += """
extension \(widgetName) where Element: Identifiable, Identifier == Element.ID {
/// Initialize `\(widgetName)`.
public init(_ elements: [Element], @ViewBuilder content: @escaping (Element) -> Body) {
self.elements = elements
self.content = content
self.id = \\.id
}
}
"""
} }
return """ return """
// //
@ -135,7 +150,7 @@ struct Class: ClassLike, Decodable {
} }
\(generateModifiers(config: config, genConfig: genConfig, namespace: namespace, configs: configs)) \(generateModifiers(config: config, genConfig: genConfig, namespace: namespace, configs: configs))
} }
\(extensions)
""" """
} }
// swiftlint:enable function_body_length line_length // swiftlint:enable function_body_length line_length