Add support for GtkDropDown

This commit is contained in:
david-swift 2025-04-09 16:06:57 +02:00
parent 62b3aa93b2
commit a898efcdf8
4 changed files with 291 additions and 3 deletions

View File

@ -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<Element>(
selection: Binding<Element.ID>,
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<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

@ -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.
///
/// <picture><source srcset="drop-down-dark.png" media="(prefers-color-scheme: dark)"><img alt="An example GtkDropDown" src="drop-down.png"></picture>
///
/// 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
/// <object class="GtkDropDown"><property name="model"><object class="GtkStringList"><items><item translatable="yes">Factory</item><item translatable="yes">Home</item><item translatable="yes">Subway</item></items></object></property></object>
/// ```
///
/// 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<UInt>?
/// 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>(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<Data>(_ 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<UInt>?) -> 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
}
}

View File

@ -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)
}
}

View File

@ -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.