From a898efcdf84ab88d7b59dabe377bbd5c62513587 Mon Sep 17 00:00:00 2001 From: david-swift Date: Wed, 9 Apr 2025 16:06:57 +0200 Subject: [PATCH] Add support for GtkDropDown --- Sources/Core/View/DropDown+.swift | 68 +++++++ Sources/Core/View/Generated/DropDown.swift | 191 ++++++++++++++++++ Sources/Demo/Demo.swift | 19 +- .../Generation/GenerationConfiguration.swift | 16 +- 4 files changed, 291 insertions(+), 3 deletions(-) create mode 100644 Sources/Core/View/DropDown+.swift create mode 100644 Sources/Core/View/Generated/DropDown.swift diff --git a/Sources/Core/View/DropDown+.swift b/Sources/Core/View/DropDown+.swift new file mode 100644 index 0000000..bb2fc5e --- /dev/null +++ b/Sources/Core/View/DropDown+.swift @@ -0,0 +1,68 @@ +// +// DropDown+.swift +// Adwaita +// +// Created by david-swift on 09.04.25. +// + +import CAdw +import LevenshteinTransformations + +extension DropDown { + + /// The identifier for the values. + static var values: String { "values" } + /// The identifier for the string list. + static var stringList: String { "string-list" } + + /// Initialize a combo row. + /// - Parameters: + /// - title: The row's title. + /// - selection: The selected value. + /// - values: The available values. + public init( + selection: Binding, + values: [Element] + ) where Element: Identifiable, Element: CustomStringConvertible { + self.init() + self = self.selected(.init { + .init(values.firstIndex { $0.id == selection.wrappedValue } ?? 0) + } set: { index in + if let id = values[safe: .init(index)]?.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) + } + 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( + 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 + } + } + +} diff --git a/Sources/Core/View/Generated/DropDown.swift b/Sources/Core/View/Generated/DropDown.swift new file mode 100644 index 0000000..11cd49e --- /dev/null +++ b/Sources/Core/View/Generated/DropDown.swift @@ -0,0 +1,191 @@ +// +// DropDown.swift +// Adwaita +// +// Created by auto-generation on 09.04.25. +// + +import CAdw +import LevenshteinTransformations + +/// Allows the user to choose an item from a list of options. +/// +/// An example GtkDropDown +/// +/// The `GtkDropDown` displays the [selected][property@Gtk.DropDown:selected] +/// choice. +/// +/// The options are given to `GtkDropDown` in the form of `GListModel` +/// and how the individual options are represented is determined by +/// a [class@Gtk.ListItemFactory]. The default factory displays simple strings, +/// and adds a checkmark to the selected item in the popup. +/// +/// To set your own factory, use [method@Gtk.DropDown.set_factory]. It is +/// possible to use a separate factory for the items in the popup, with +/// [method@Gtk.DropDown.set_list_factory]. +/// +/// `GtkDropDown` knows how to obtain strings from the items in a +/// [class@Gtk.StringList]; for other models, you have to provide an expression +/// to find the strings via [method@Gtk.DropDown.set_expression]. +/// +/// `GtkDropDown` can optionally allow search in the popup, which is +/// useful if the list of options is long. To enable the search entry, +/// use [method@Gtk.DropDown.set_enable_search]. +/// +/// Here is a UI definition example for `GtkDropDown` with a simple model: +/// +/// ```xml +/// FactoryHomeSubway +/// ``` +/// +/// If a `GtkDropDown` is created in this manner, or with +/// [ctor@Gtk.DropDown.new_from_strings], for instance, the object returned from +/// [method@Gtk.DropDown.get_selected_item] will be a [class@Gtk.StringObject]. +/// +/// To learn more about the list widget framework, see the +/// [overview](section-list-widget.html). +/// +/// ## CSS nodes +/// +/// `GtkDropDown` has a single CSS node with name dropdown, +/// with the button and popover nodes as children. +/// +/// ## Accessibility +/// +/// `GtkDropDown` uses the [enum@Gtk.AccessibleRole.combo_box] role. +public struct DropDown: AdwaitaWidget { + + /// Additional update functions for type extensions. + var updateFunctions: [(ViewStorage, WidgetData, Bool) -> Void] = [] + /// Additional appear functions for type extensions. + var appearFunctions: [(ViewStorage, WidgetData) -> Void] = [] + + /// The accessible role of the given `GtkAccessible` implementation. + /// + /// The accessible role cannot be changed once set. + var accessibleRole: String? + /// Whether to show a search entry in the popup. + /// + /// Note that search requires [property@Gtk.DropDown:expression] + /// to be set. + var enableSearch: Bool? + /// The position of the selected item. + /// + /// If no item is selected, the property has the value + /// %GTK_INVALID_LIST_POSITION. + var selected: Binding? + /// Whether to show an arrow within the GtkDropDown widget. + var showArrow: Bool? + /// Emitted to when the drop down is activated. + /// + /// The `::activate` signal on `GtkDropDown` is an action signal and + /// emitting it causes the drop down to pop up its dropdown. + var activate: (() -> Void)? + + /// Initialize `DropDown`. + public init() { + } + + /// The view storage. + /// - Parameters: + /// - modifiers: Modify views before being updated. + /// - type: The view render data type. + /// - Returns: The view storage. + public func container(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData { + let storage = ViewStorage(gtk_drop_down_new(gtk_string_list_new(nil), nil)?.opaque()) + for function in appearFunctions { + function(storage, data) + } + update(storage, data: data, updateProperties: true, type: type) + + return storage + } + + /// Update the stored content. + /// - Parameters: + /// - storage: The storage to update. + /// - modifiers: Modify views before being updated + /// - updateProperties: Whether to update the view's properties. + /// - type: The view render data type. + public func update(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) where Data: ViewRenderData { + if let activate { + storage.connectSignal(name: "activate", argCount: 0) { + activate() + } + } + storage.modify { widget in + + storage.notify(name: "selected") { + let newValue = UInt(gtk_drop_down_get_selected(storage.opaquePointer)) +if let selected, newValue != selected.wrappedValue { + selected.wrappedValue = newValue +} + } + if let enableSearch, updateProperties, (storage.previousState as? Self)?.enableSearch != enableSearch { + gtk_drop_down_set_enable_search(widget, enableSearch.cBool) + } + if let selected, updateProperties, (UInt(gtk_drop_down_get_selected(storage.opaquePointer))) != selected.wrappedValue { + gtk_drop_down_set_selected(storage.opaquePointer, selected.wrappedValue.cInt) + } + if let showArrow, updateProperties, (storage.previousState as? Self)?.showArrow != showArrow { + gtk_drop_down_set_show_arrow(widget, showArrow.cBool) + } + + + + } + for function in updateFunctions { + function(storage, data, updateProperties) + } + if updateProperties { + storage.previousState = self + } + } + + /// The accessible role of the given `GtkAccessible` implementation. + /// + /// The accessible role cannot be changed once set. + public func accessibleRole(_ accessibleRole: String?) -> Self { + var newSelf = self + newSelf.accessibleRole = accessibleRole + return newSelf + } + + /// Whether to show a search entry in the popup. + /// + /// Note that search requires [property@Gtk.DropDown:expression] + /// to be set. + public func enableSearch(_ enableSearch: Bool? = true) -> Self { + var newSelf = self + newSelf.enableSearch = enableSearch + return newSelf + } + + /// The position of the selected item. + /// + /// If no item is selected, the property has the value + /// %GTK_INVALID_LIST_POSITION. + public func selected(_ selected: Binding?) -> Self { + var newSelf = self + newSelf.selected = selected + return newSelf + } + + /// Whether to show an arrow within the GtkDropDown widget. + public func showArrow(_ showArrow: Bool? = true) -> Self { + var newSelf = self + newSelf.showArrow = showArrow + return newSelf + } + + /// Emitted to when the drop down is activated. + /// + /// The `::activate` signal on `GtkDropDown` is an action signal and + /// emitting it causes the drop down to pop up its dropdown. + public func activate(_ activate: @escaping () -> Void) -> Self { + var newSelf = self + newSelf.activate = activate + return newSelf + } + +} diff --git a/Sources/Demo/Demo.swift b/Sources/Demo/Demo.swift index c21b5d9..a078eab 100644 --- a/Sources/Demo/Demo.swift +++ b/Sources/Demo/Demo.swift @@ -65,6 +65,19 @@ struct Demo: App { .title("Navigation View Demo") } + enum WindowName: String, CaseIterable, CustomStringConvertible, Identifiable { + + case demo = "Demo" + case alternative = "Alternative" + + var id: Self { self } + + var description: String { + rawValue + } + + } + struct DemoContent: WindowView { @State("selection") @@ -77,6 +90,7 @@ struct Demo: App { @State private var maximized = false @State private var about = false @State private var preferences = false + @State private var title: WindowName = .demo var window: AdwaitaWindow var app: AdwaitaApp! var pictureURL: URL? @@ -98,7 +112,7 @@ struct Demo: App { menu } .headerBarTitle { - WindowTitle(subtitle: "", title: "Demo") + WindowTitle(subtitle: "", title: title.description) } } } content: { @@ -111,6 +125,7 @@ struct Demo: App { HeaderBar { Toggle(icon: .default(icon: .sidebarShow), isOn: $sidebarVisible) .tooltip("Toggle Sidebar") + DropDown(selection: $title, values: WindowName.allCases) } end: { if sidebarVisible { Text("").transition(.crossfade) @@ -123,7 +138,7 @@ struct Demo: App { Text("") .transition(.crossfade) } else { - WindowTitle(subtitle: "Demo", title: selection.label) + WindowTitle(subtitle: title.description, title: selection.label) .transition(.crossfade) } } diff --git a/Sources/Generation/GenerationConfiguration.swift b/Sources/Generation/GenerationConfiguration.swift index 925a1b3..b37f065 100644 --- a/Sources/Generation/GenerationConfiguration.swift +++ b/Sources/Generation/GenerationConfiguration.swift @@ -292,7 +292,21 @@ struct GenerationConfiguration { initializer: "gtk_separator_new(GTK_ORIENTATION_VERTICAL)", excludeProperties: ["orientation"] ), - .init(class: "Fixed") + .init(class: "Fixed"), + .init( + class: "DropDown", + initializer: "gtk_drop_down_new(gtk_string_list_new(nil), nil)", + bindings: [.init(property: "selected")], + excludeProperties: [ + "expression", + "factory", + "header-factory", + "list-factory", + "model", + "search-match-mode", + "selected-item" + ] + ) ] /// The unshortening map.