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.
/// - Parameters:
/// - title: The row's title.
/// - selection: The selected value.
/// - values: The available values.
public init<Element>(
selection: Binding<Element.ID>,
values: [Element]
) 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 = self.selected(.init {
.init(values.firstIndex { $0.id == selection.wrappedValue } ?? 0)
.init(values.firstIndex { $0[keyPath: id] == selection.wrappedValue } ?? 0)
} set: { index in
if let id = values[safe: .init(index)]?.id {
if let id = values[safe: .init(index)]?[keyPath: id] {
selection.wrappedValue = id
}
})
appearFunctions.append { storage, _ in
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
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:
/// - storage: The view storage.
/// - values: The elements.
/// - element: The type of the elements.
static func updateContent<Element>(
/// - id: The keypath to the id.
/// - description: The keypath to the description.
static func updateContent<Element, Identifier>(
storage: ViewStorage,
values: [Element],
element: Element.Type
) where Element: Identifiable, Element: CustomStringConvertible {
id: KeyPath<Element, Identifier>,
description: KeyPath<Element, String>
) where Identifier: Equatable {
if let list = storage.fields[Self.stringList] as? OpaquePointer {
let old = storage.fields[Self.values] as? [Element] ?? []
old.identifiableTransform(
old.transform(
to: values,
id: id,
functions: .init { index in
gtk_string_list_remove(list, .init(index))
} insert: { _, element in
gtk_string_list_append(list, element.description)
gtk_string_list_append(list, element[keyPath: description])
}
)
storage.fields[Self.values] = values

View File

@ -17,19 +17,21 @@ extension FlowBox {
/// Initialize `FlowBox`.
/// - Parameters:
/// - elements: The elements.
/// - id: The element identifier keypath.
/// - 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,
id: KeyPath<Element, Identifier>,
selection: Binding<Identifier>? = nil,
@ViewBuilder content: @escaping (Element) -> Body
) {
self.init(elements, content: content)
let id: (ViewStorage, [Element]) -> Element.ID? = { storage, elements in
self.init(elements, id: id, content: content)
let getID: (ViewStorage, [Element]) -> Identifier? = { storage, elements in
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())
return elements[safe: storage.content[.mainContent]?
.firstIndex { $0.opaquePointer?.cast() == element }]?.id
.firstIndex { $0.opaquePointer?.cast() == element }]?[keyPath: id]
}
return nil
}
@ -37,12 +39,12 @@ extension FlowBox {
updateFunctions.append { storage, _, _ in
storage.connectSignal(name: "selected_children_changed", id: Self.selectionField) {
if let elements = storage.fields[Self.elementsField] as? [Element],
let id = id(storage, elements) {
let id = getID(storage, elements) {
selection.wrappedValue = id
}
}
if selection.wrappedValue != id(storage, elements),
let index = elements.firstIndex(where: { $0.id == selection.wrappedValue })?.cInt {
if selection.wrappedValue != getID(storage, elements),
let index = elements.firstIndex(where: { $0[keyPath: id] == selection.wrappedValue })?.cInt {
gtk_flow_box_select_child(
storage.opaquePointer,
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
/// 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.
var elements: [Element]
@ -19,12 +19,20 @@ public struct ForEach<Element>: AdwaitaWidget where Element: Identifiable {
var horizontal: Bool
/// Whether the children should all be the same size.
var homogeneous: Bool?
/// The path to the identifier.
var id: KeyPath<Element, Identifier>
/// 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.content = content
self.horizontal = horizontal
self.id = id
}
/// The view storage.
@ -56,8 +64,9 @@ public struct ForEach<Element>: AdwaitaWidget where Element: Identifiable {
var contentStorage: [ViewStorage] = storage.content[.mainContent] ?? []
let old = storage.fields["element"] as? [Element] ?? []
let widget: UnsafeMutablePointer<GtkBox>? = storage.opaquePointer?.cast()
old.identifiableTransform(
old.transform(
to: elements,
id: id,
functions: .init { index in
let child = contentStorage[safe: index]?.opaquePointer
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>,
values: [Element]
) 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.selected(.init {
.init(values.firstIndex { $0.id == selection.wrappedValue } ?? 0)
.init(values.firstIndex { $0[keyPath: id] == selection.wrappedValue } ?? 0)
} set: { index in
if let id = values[safe: .init(index)]?.id {
if let id = values[safe: .init(index)]?[keyPath: id] {
selection.wrappedValue = id
}
})
@ -39,34 +54,10 @@ extension ComboRow {
storage.fields[Self.stringList] = list
adw_combo_row_set_model(storage.opaquePointer?.cast(), list)
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
Self.updateContent(storage: storage, values: values, element: Element.self)
}
}
/// 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
DropDown.updateContent(storage: storage, values: values, id: id, description: description)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
// Carousel.swift
// Adwaita
//
// Created by auto-generation on 03.02.26.
// Created by auto-generation on 04.02.26.
//
import CAdw
@ -19,7 +19,7 @@ import LevenshteinTransformations
/// 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
/// Additional update functions for type extensions.
@ -70,11 +70,14 @@ public struct Carousel<Element>: AdwaitaWidget where Element: Identifiable {
var elements: [Element]
/// The dynamic widget content.
var content: (Element) -> Body
/// The dynamic widget identifier key path.
var id: KeyPath<Element, Identifier>
/// 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.content = content
self.id = id
}
/// The view storage.
@ -126,8 +129,9 @@ public struct Carousel<Element>: AdwaitaWidget where Element: Identifiable {
var contentStorage: [ViewStorage] = storage.content[.mainContent] ?? []
let old = storage.fields["element"] as? [Element] ?? []
old.identifiableTransform(
old.transform(
to: elements,
id: id,
functions: .init { index in
adw_carousel_remove(widget, adw_carousel_get_nth_page(widget, UInt(index).cInt))
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
// Adwaita
//
// Created by auto-generation on 03.02.26.
// Created by auto-generation on 04.02.26.
//
import CAdw

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
// ListBox.swift
// Adwaita
//
// Created by auto-generation on 03.02.26.
// Created by auto-generation on 04.02.26.
//
import CAdw
@ -34,7 +34,7 @@ import LevenshteinTransformations
/// 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
/// Additional update functions for type extensions.
@ -107,11 +107,14 @@ public struct ListBox<Element>: AdwaitaWidget where Element: Identifiable {
var elements: [Element]
/// The dynamic widget content.
var content: (Element) -> Body
/// The dynamic widget identifier key path.
var id: KeyPath<Element, Identifier>
/// 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.content = content
self.id = id
}
/// The view storage.
@ -186,8 +189,9 @@ public struct ListBox<Element>: AdwaitaWidget where Element: Identifiable {
var contentStorage: [ViewStorage] = storage.content[.mainContent] ?? []
let old = storage.fields["element"] as? [Element] ?? []
old.identifiableTransform(
old.transform(
to: elements,
id: id,
functions: .init { index in
gtk_list_box_remove(widget, gtk_list_box_get_row_at_index(widget, index.cInt)?.cast())
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
// Adwaita
//
// Created by auto-generation on 03.02.26.
// Created by auto-generation on 04.02.26.
//
import CAdw

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
// WindowTitle.swift
// Adwaita
//
// Created by auto-generation on 03.02.26.
// Created by auto-generation on 04.02.26.
//
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`.
/// - Parameters:
/// - elements: The elements.
/// - id: The key path to the elements' identifiers.
/// - 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>?,
id: KeyPath<Element, Identifier>,
selection: Binding<Identifier>?,
@ViewBuilder content: @escaping (Element) -> Body
) {
self.init(elements, content: content)
let id: (ViewStorage, [Element]) -> Element.ID? = { storage, elements in
self.init(elements, id: id, content: content)
let getID: (ViewStorage, [Element]) -> Identifier? = { storage, elements in
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
}
@ -39,13 +41,13 @@ extension List {
updateFunctions.append { storage, _, _ in
storage.connectSignal(name: "selected_rows_changed", id: Self.selectionField) {
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),
let index = elements.firstIndex(where: { $0.id == selection.wrappedValue })?.cInt {
if selection.wrappedValue != getID(storage, elements),
let index = elements.firstIndex(where: { $0[keyPath: id] == selection.wrappedValue })?.cInt {
gtk_list_box_select_row(
storage.opaquePointer,
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>,
values: [Element]
) 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()
appearFunctions.append { storage, _ in
storage.notify(name: "active-name", id: "init") {
if let name = adw_toggle_group_get_active_name(storage.opaquePointer),
let values = storage.fields[Self.values] as? [Element],
let value = values.first(where: { $0.id.description == String(cString: name) }) {
selection.wrappedValue = value.id
let identifier = values
.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
Self.updateContent(
storage: storage,
selection: selection.wrappedValue,
values: values,
id: id,
label: label,
icon: icon,
showLabel: showLabel,
updateProperties: updateProperties
)
}
}
// swiftlint:disable function_parameter_count
/// Update the combo row's content.
/// - Parameters:
/// - storage: The view storage.
/// - values: The elements.
/// - updateProperties: Whether to update the properties.
static func updateContent<Element>(
static func updateContent<Element, Identifier>(
storage: ViewStorage,
selection: Element.ID,
selection: Identifier,
values: [Element],
id: KeyPath<Element, Identifier>,
label: KeyPath<Element, String>,
icon: KeyPath<Element, Icon?>?,
showLabel: KeyPath<Element, Bool>?,
updateProperties: Bool
) where Element: ToggleGroupItem {
) where Identifier: Equatable {
guard updateProperties else {
return
}
let old = storage.fields[Self.values] as? [Element] ?? []
old.identifiableTransform(
old.transform(
to: values,
id: id,
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 {
adw_toggle_group_remove(storage.opaquePointer, toggle)
}
} insert: { _, element in
let toggle = adw_toggle_new()
adw_toggle_set_name(toggle, element.id.description)
if element.showLabel {
adw_toggle_set_label(toggle, element.id.description)
adw_toggle_set_name(toggle, element[keyPath: label])
if let showLabel, !element[keyPath: showLabel] {
adw_toggle_set_tooltip(toggle, element[keyPath: label])
} 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)
}
storage.fields[Self.toggle + element.id.description] = toggle
storage.fields[Self.toggle + element[keyPath: label]] = toggle
adw_toggle_group_add(storage.opaquePointer, toggle)
}
)
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,7 +15,12 @@ struct ToggleGroupDemo: View {
@State private var selection: Subview = .view1
var view: Body {
ToggleGroup(selection: $selection, values: Subview.allCases)
ToggleGroup(
selection: $selection,
values: Subview.allCases,
id: \.self,
label: \.rawValue
)
.padding()
VStack {
Text(selection.rawValue)
@ -26,19 +31,11 @@ struct ToggleGroupDemo: View {
.padding()
}
enum Subview: String, ToggleGroupItem, CaseIterable, CustomStringConvertible {
enum Subview: String, CaseIterable, Equatable {
case view1 = "View 1"
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) }
var initializer = "\(config.internalInitializer ? "" : "public ")init("
if config.dynamicWidget != nil {
initializer.append("_ elements: [Element], ")
initializer.append("_ elements: [Element], id: KeyPath<Element, Identifier>, ")
}
for property in requiredProperties {
initializer.append("\(property.parameter(config: config, genConfig: genConfig)), ")
@ -46,6 +46,7 @@ extension Class {
self.elements = elements
self.content = content
self.id = id
"""
)
}
@ -167,6 +168,8 @@ extension Class {
var elements: [Element]
/// The dynamic widget content.
var content: (Element) -> Body
/// The dynamic widget identifier key path.
var id: KeyPath<Element, Identifier>
"""
}
content += staticWidgetProperties(namespace: namespace, configs: configs)
@ -226,8 +229,9 @@ extension Class {
return """
var contentStorage: [ViewStorage] = storage.content[.mainContent] ?? []
let old = storage.fields["element"] as? [Element] ?? []
old.identifiableTransform(
old.transform(
to: elements,
id: id,
functions: .init { index in
\(dynamicWidget.remove)(\(widget), \(dynamicWidget.getElement))
contentStorage.remove(at: index)

View File

@ -64,10 +64,25 @@ struct Class: ClassLike, Decodable {
dateFormatter.dateFormat = "dd.MM.yy"
let widgetName = config.name ?? config.class
let definition: String
var extensions = ""
if config.dynamicWidget == nil {
definition = "\(widgetName): AdwaitaWidget"
} 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 """
//
@ -135,7 +150,7 @@ struct Class: ClassLike, Decodable {
}
\(generateModifiers(config: config, genConfig: genConfig, namespace: namespace, configs: configs))
}
\(extensions)
"""
}
// swiftlint:enable function_body_length line_length