From 4dc5d074098d32b9b3341e2f8acf0be3f5756bc8 Mon Sep 17 00:00:00 2001 From: david-swift Date: Thu, 4 Jan 2024 10:58:33 +0100 Subject: [PATCH] Add support for grouping controls with forms --- Documentation/Reference/README.md | 7 + Documentation/Reference/extensions/View.md | 5 + Documentation/Reference/structs/ActionRow.md | 72 +++++++++ Documentation/Reference/structs/ComboRow.md | 87 +++++++++++ Documentation/Reference/structs/EntryRow.md | 87 +++++++++++ Documentation/Reference/structs/Form.md | 29 ++++ .../Reference/structs/FormSection.md | 64 ++++++++ Documentation/Reference/structs/SpinRow.md | 102 ++++++++++++ Documentation/Reference/structs/SwitchRow.md | 78 ++++++++++ Documentation/Reference/structs/Toggle.md | 14 ++ Package.swift | 2 +- Sources/Adwaita/View/Forms/ActionRow.swift | 99 ++++++++++++ Sources/Adwaita/View/Forms/ComboRow.swift | 132 ++++++++++++++++ Sources/Adwaita/View/Forms/EntryRow.swift | 128 +++++++++++++++ Sources/Adwaita/View/Forms/Form.swift | 47 ++++++ Sources/Adwaita/View/Forms/FormSection.swift | 89 +++++++++++ Sources/Adwaita/View/Forms/SpinRow.swift | 146 ++++++++++++++++++ Sources/Adwaita/View/Forms/SwitchRow.swift | 111 +++++++++++++ Sources/Adwaita/View/Modifiers/View+.swift | 17 ++ Sources/Adwaita/View/Toggle.swift | 42 ++++- Tests/Demo.swift | 6 + Tests/FormDemo.swift | 88 +++++++++++ Tests/ListDemo.swift | 3 +- Tests/Page.swift | 5 + user-manual/Information/Widgets.md | 58 ++++++- 25 files changed, 1509 insertions(+), 9 deletions(-) create mode 100644 Documentation/Reference/structs/ActionRow.md create mode 100644 Documentation/Reference/structs/ComboRow.md create mode 100644 Documentation/Reference/structs/EntryRow.md create mode 100644 Documentation/Reference/structs/Form.md create mode 100644 Documentation/Reference/structs/FormSection.md create mode 100644 Documentation/Reference/structs/SpinRow.md create mode 100644 Documentation/Reference/structs/SwitchRow.md create mode 100644 Sources/Adwaita/View/Forms/ActionRow.swift create mode 100644 Sources/Adwaita/View/Forms/ComboRow.swift create mode 100644 Sources/Adwaita/View/Forms/EntryRow.swift create mode 100644 Sources/Adwaita/View/Forms/Form.swift create mode 100644 Sources/Adwaita/View/Forms/FormSection.swift create mode 100644 Sources/Adwaita/View/Forms/SpinRow.swift create mode 100644 Sources/Adwaita/View/Forms/SwitchRow.swift create mode 100644 Sources/Adwaita/View/Modifiers/View+.swift create mode 100644 Tests/FormDemo.swift diff --git a/Documentation/Reference/README.md b/Documentation/Reference/README.md index f6ba66e..bd3378a 100644 --- a/Documentation/Reference/README.md +++ b/Documentation/Reference/README.md @@ -16,15 +16,20 @@ ## Structs - [AboutWindow](structs/AboutWindow.md) +- [ActionRow](structs/ActionRow.md) - [AppearObserver](structs/AppearObserver.md) - [Banner](structs/Banner.md) - [Binding](structs/Binding.md) - [Button](structs/Button.md) - [Carousel](structs/Carousel.md) - [Clamp](structs/Clamp.md) +- [ComboRow](structs/ComboRow.md) - [Container](structs/Container.md) - [ContentModifier](structs/ContentModifier.md) +- [EntryRow](structs/EntryRow.md) - [FileDialog](structs/FileDialog.md) +- [Form](structs/Form.md) +- [FormSection](structs/FormSection.md) - [HStack](structs/HStack.md) - [HeaderBar](structs/HeaderBar.md) - [InspectorWrapper](structs/InspectorWrapper.md) @@ -39,10 +44,12 @@ - [ProgressBar](structs/ProgressBar.md) - [ScrollView](structs/ScrollView.md) - [Signal](structs/Signal.md) +- [SpinRow](structs/SpinRow.md) - [State](structs/State.md) - [StateWrapper](structs/StateWrapper.md) - [StatusPage](structs/StatusPage.md) - [Submenu](structs/Submenu.md) +- [SwitchRow](structs/SwitchRow.md) - [Text](structs/Text.md) - [ToastOverlay](structs/ToastOverlay.md) - [Toggle](structs/Toggle.md) diff --git a/Documentation/Reference/extensions/View.md b/Documentation/Reference/extensions/View.md index 8e45b60..07160a8 100644 --- a/Documentation/Reference/extensions/View.md +++ b/Documentation/Reference/extensions/View.md @@ -177,3 +177,8 @@ Add a bottom toolbar to the view. - toolbar: The toolbar's content. - visible: Whether the toolbar is visible. - Returns: A view. + +### `verticalCenter()` + +Wrap the view in a vertical stack and center vertically. +- Returns: The view. diff --git a/Documentation/Reference/structs/ActionRow.md b/Documentation/Reference/structs/ActionRow.md new file mode 100644 index 0000000..7aa3fc9 --- /dev/null +++ b/Documentation/Reference/structs/ActionRow.md @@ -0,0 +1,72 @@ +**STRUCT** + +# `ActionRow` + +A form content row showing a title and optionally a subtitle and widgets. + +## Properties +### `title` + +The title. + +### `subtitle` + +The subtitle. + +### `prefix` + +The prefix. + +### `suffix` + +The suffix. + +### `prefixID` + +The identifier for the prefix content. + +### `suffixID` + +The identifier for the suffix content. + +## Methods +### `init(_:)` + +Initialize an action row. +- Parameter title: The row's title. + +### `update(_:modifiers:)` + +Update a view storage. +- Parameters: + - storage: The view storage. + - modifiers: Modify views before being updated. + +### `container(modifiers:)` + +Get a view storage. +- Parameter modifiers: Modify views before being updated. +- Returns: The view storage. + +### `update(row:)` + +Update the action row. +- Parameter row: The action row. + +### `subtitle(_:)` + +Set the action row's subtitle. +- Parameter subtitle: The subtitle. +- Returns: The action row. + +### `prefix(_:)` + +Set the action row's prefix view. +- Parameter prefix: The prefix. +- Returns: The action row. + +### `suffix(_:)` + +Set the action row's suffix view. +- Parameter suffix: The suffix. +- Returns: The action row. diff --git a/Documentation/Reference/structs/ComboRow.md b/Documentation/Reference/structs/ComboRow.md new file mode 100644 index 0000000..13c357b --- /dev/null +++ b/Documentation/Reference/structs/ComboRow.md @@ -0,0 +1,87 @@ +**STRUCT** + +# `ComboRow` + +A row for selecting an element out of a list of elements. + +## Properties +### `title` + +The title. + +### `selection` + +The selected element. + +### `content` + +The content. + +### `subtitle` + +The subtitle. + +### `prefix` + +The prefix. + +### `suffix` + +The suffix. + +### `prefixID` + +The identifier for the prefix content. + +### `suffixID` + +The identifier for the suffix content. + +### `elementsID` + +The identifier for the elements. + +## Methods +### `init(_:selection:values:)` + +Initialize a combo row. +- Parameters: + - title: The row's title. + - selection: The selected value. + - values: The available values. + +### `update(_:modifiers:)` + +Update a view storage. +- Parameters: + - storage: The view storage. + - modifiers: Modify views before being updated. + +### `container(modifiers:)` + +Get a view storage. +- Parameter modifiers: Modify views before being updated. +- Returns: The view storage. + +### `update(row:)` + +Update the combo row. +- Parameter row: The combo row. + +### `subtitle(_:)` + +Set the combo row's subtitle. +- Parameter subtitle: The subtitle. +- Returns: The combo row. + +### `prefix(_:)` + +Set the combo row's prefix view. +- Parameter prefix: The prefix. +- Returns: The combo row. + +### `suffix(_:)` + +Set the combo row's suffix view. +- Parameter suffix: The suffix. +- Returns: The combo row. diff --git a/Documentation/Reference/structs/EntryRow.md b/Documentation/Reference/structs/EntryRow.md new file mode 100644 index 0000000..8f03f37 --- /dev/null +++ b/Documentation/Reference/structs/EntryRow.md @@ -0,0 +1,87 @@ +**STRUCT** + +# `EntryRow` + +A form content row accepting text input. + +## Properties +### `title` + +The title. + +### `text` + +The text. + +### `prefix` + +The prefix. + +### `suffix` + +The suffix. + +### `onSubmit` + +The handler that gets executed when the user submits the content. + +### `password` + +Whether the password entry row should be used. + +### `prefixID` + +The identifier for the prefix content. + +### `suffixID` + +The identifier for the suffix content. + +## Methods +### `init(_:text:)` + +Initialize an entry row. +- Parameters: + - title: The row's title. + - text: The text. + +### `update(_:modifiers:)` + +Update a view storage. +- Parameters: + - storage: The view storage. + - modifiers: Modify views before being updated. + +### `container(modifiers:)` + +Get a view storage. +- Parameter modifiers: Modify views before being updated. +- Returns: The view storage. + +### `update(row:)` + +Update the entry row. +- Parameter row: The entry row. + +### `onSubmit(_:)` + +Set the entry row's subtitle. +- Parameter subtitle: The subtitle. +- Returns: The entry row. + +### `prefix(_:)` + +Set the entry row's prefix view. +- Parameter prefix: The prefix. +- Returns: The entry row. + +### `suffix(_:)` + +Set the entry row's suffix view. +- Parameter suffix: The suffix. +- Returns: The entry row. + +### `secure()` + +Let the user securely enter private text. +- Returns: The entry row. diff --git a/Documentation/Reference/structs/Form.md b/Documentation/Reference/structs/Form.md new file mode 100644 index 0000000..02b0bd7 --- /dev/null +++ b/Documentation/Reference/structs/Form.md @@ -0,0 +1,29 @@ +**STRUCT** + +# `Form` + +A list with no dynamic content styled as a boxed list. + +## Properties +### `content` + +The content. + +## Methods +### `init(content:)` + +Initialize a `Form`. +- Parameter content: The view content, usually different kind of rows. + +### `update(_:modifiers:)` + +Update a view storage. +- Parameters: + - storage: The view storage. + - modifiers: Modify views before being updated. + +### `container(modifiers:)` + +Get a view storage. +- Parameter modifiers: Modify views before being updated. +- Returns: The view storage. diff --git a/Documentation/Reference/structs/FormSection.md b/Documentation/Reference/structs/FormSection.md new file mode 100644 index 0000000..6540548 --- /dev/null +++ b/Documentation/Reference/structs/FormSection.md @@ -0,0 +1,64 @@ +**STRUCT** + +# `FormSection` + +A section usually groups forms. + +## Properties +### `title` + +The title. + +### `content` + +The content. + +### `description` + +The description. + +### `suffix` + +The suffix. + +### `suffixID` + +The identifier for the suffix content. + +## Methods +### `init(_:content:)` + +Initialize a form section. +- Parameters: + - title: The title. + - content: The content, usually one or more forms. + +### `update(_:modifiers:)` + +Update a view storage. +- Parameters: + - storage: The view storage. + - modifiers: Modify views before being updated. + +### `container(modifiers:)` + +Get a view storage. +- Parameter modifiers: Modify views before being updated. +- Returns: The view storage. + +### `update(group:)` + +Update the form section. +- Parameter group: The form section. + +### `description(_:)` + +Set the form section's description. +- Parameter description: The description. +- Returns: The form section. + +### `suffix(_:)` + +Set the form section's suffix view. +- Parameter suffix: The suffix. +- Returns: The form section. diff --git a/Documentation/Reference/structs/SpinRow.md b/Documentation/Reference/structs/SpinRow.md new file mode 100644 index 0000000..4d74efe --- /dev/null +++ b/Documentation/Reference/structs/SpinRow.md @@ -0,0 +1,102 @@ +**STRUCT** + +# `SpinRow` + +A spin row lets the user select an integer in a certain range. + +## Properties +### `title` + +The title. + +### `value` + +The selected value. + +### `min` + +The minimum value. + +### `max` + +The maximum value. + +### `step` + +The increase/decrease step. + +### `subtitle` + +The subtitle. + +### `prefix` + +The prefix. + +### `suffix` + +The suffix. + +### `prefixID` + +The identifier for the prefix content. + +### `suffixID` + +The identifier for the suffix content. + +### `configID` + +The identifier of the configuration field. + +## Methods +### `init(_:value:min:max:)` + +Initialize a spin row. +- Parameters: + - title: The row's title. + - value: The selected value. + - min: The minimum value. + - max: The maximum value. + +### `update(_:modifiers:)` + +Update a view storage. +- Parameters: + - storage: The view storage. + - modifiers: Modify views before being updated. + +### `container(modifiers:)` + +Get a view storage. +- Parameter modifiers: Modify views before being updated. +- Returns: The view storage. + +### `update(row:)` + +Update the spin row. +- Parameter row: The spin row. + +### `subtitle(_:)` + +Set the spin row's subtitle. +- Parameter subtitle: The subtitle. +- Returns: The spin row. + +### `prefix(_:)` + +Set the spin row's prefix view. +- Parameter prefix: The prefix. +- Returns: The spin row. + +### `suffix(_:)` + +Set the spin row's suffix view. +- Parameter suffix: The suffix. +- Returns: The spin row. + +### `step(_:)` + +Set the difference a single click on the increase/decrease buttons makes. +- Parameter step: The increase/decrease step. +- Returns: The spin row. diff --git a/Documentation/Reference/structs/SwitchRow.md b/Documentation/Reference/structs/SwitchRow.md new file mode 100644 index 0000000..f11bdac --- /dev/null +++ b/Documentation/Reference/structs/SwitchRow.md @@ -0,0 +1,78 @@ +**STRUCT** + +# `SwitchRow` + +A row representing a boolean state. + +## Properties +### `title` + +The title. + +### `isOn` + +Whether the switch is activated. + +### `subtitle` + +The subtitle. + +### `prefix` + +The prefix. + +### `suffix` + +The suffix. + +### `prefixID` + +The identifier for the prefix content. + +### `suffixID` + +The identifier for the suffix content. + +## Methods +### `init(_:isOn:)` + +Initialize a switch row. +- Parameters: + - title: The row's title. + - isOn: Whether the switch is on. + +### `update(_:modifiers:)` + +Update a view storage. +- Parameters: + - storage: The view storage. + - modifiers: Modify views before being updated. + +### `container(modifiers:)` + +Get a view storage. +- Parameter modifiers: Modify views before being updated. +- Returns: The view storage. + +### `update(row:)` + +Update the switch row. +- Parameter row: The switch row. + +### `subtitle(_:)` + +Set the switch row's subtitle. +- Parameter subtitle: The subtitle. +- Returns: The switch row. + +### `prefix(_:)` + +Set the switch row's prefix view. +- Parameter prefix: The prefix. +- Returns: The switch row. + +### `suffix(_:)` + +Set the switch row's suffix view. +- Parameter suffix: The suffix. +- Returns: The switch row. diff --git a/Documentation/Reference/structs/Toggle.md b/Documentation/Reference/structs/Toggle.md index 35cac4b..dbe8984 100644 --- a/Documentation/Reference/structs/Toggle.md +++ b/Documentation/Reference/structs/Toggle.md @@ -17,6 +17,10 @@ The button's icon. Whether the toggle is on. +### `isCheckButton` + +Whether to use GtkCheckButton instead of GtkToggleButton + ## Methods ### `init(_:icon:isOn:)` @@ -50,3 +54,13 @@ Get a button's view storage. Update the toggle's state. - Parameter toggle: The toggle. + +### `updateState(toggle:)` + +Update the check button's state. +- Parameter toggle: The toggle. + +### `checkButton()` + +Use the check button style. +- Returns: The toggle. diff --git a/Package.swift b/Package.swift index d5c97f3..16a12ae 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/AparokshaUI/Libadwaita", from: "0.1.5"), + .package(url: "https://github.com/AparokshaUI/Libadwaita", from: "0.1.6"), .package( url: "https://github.com/david-swift/LevenshteinTransformations", from: "0.1.1" diff --git a/Sources/Adwaita/View/Forms/ActionRow.swift b/Sources/Adwaita/View/Forms/ActionRow.swift new file mode 100644 index 0000000..caa5168 --- /dev/null +++ b/Sources/Adwaita/View/Forms/ActionRow.swift @@ -0,0 +1,99 @@ +// +// ActionRow.swift +// Adwaita +// +// Created by david-swift on 03.01.24. +// + +import Libadwaita + +/// A form content row showing a title and optionally a subtitle and widgets. +public struct ActionRow: Widget { + + /// The title. + var title: String + /// The subtitle. + var subtitle = "" + /// The prefix. + var prefix: Body = [] + /// The suffix. + var suffix: Body = [] + + /// The identifier for the prefix content. + let prefixID = "prefix" + /// The identifier for the suffix content. + let suffixID = "suffix" + + /// Initialize an action row. + /// - Parameter title: The row's title. + public init(_ title: String) { + self.title = title + } + + /// Update a view storage. + /// - Parameters: + /// - storage: The view storage. + /// - modifiers: Modify views before being updated. + public func update(_ storage: ViewStorage, modifiers: [(View) -> View]) { + if let row = storage.view as? Libadwaita.ActionRow { + update(row: row) + } + if let prefixStorage = storage.content[prefixID]?.first { + prefix.widget(modifiers: modifiers).update(prefixStorage, modifiers: modifiers) + } + if let suffixStorage = storage.content[suffixID]?.first { + suffix.widget(modifiers: modifiers).update(suffixStorage, modifiers: modifiers) + } + } + + /// Get a view storage. + /// - Parameter modifiers: Modify views before being updated. + /// - Returns: The view storage. + public func container(modifiers: [(View) -> View]) -> ViewStorage { + let row: Libadwaita.ActionRow = .init(title: title, subtitle: subtitle) + let prefixContent = prefix.widget(modifiers: modifiers).container(modifiers: modifiers) + let suffixContent = suffix.widget(modifiers: modifiers).container(modifiers: modifiers) + if !prefix.isEmpty { + _ = row.addPrefix(prefixContent.view) + } + if !suffix.isEmpty { + _ = row.addSuffix(suffixContent.view) + } + return .init(row, content: [prefixID: [prefixContent], suffixID: [suffixContent]]) + } + + /// Update the action row. + /// - Parameter row: The action row. + func update(row: Libadwaita.ActionRow) { + _ = row.title(title) + _ = row.subtitle(subtitle) + } + + /// Set the action row's subtitle. + /// - Parameter subtitle: The subtitle. + /// - Returns: The action row. + public func subtitle(_ subtitle: String) -> Self { + var newSelf = self + newSelf.subtitle = subtitle + return newSelf + } + + /// Set the action row's prefix view. + /// - Parameter prefix: The prefix. + /// - Returns: The action row. + public func prefix(@ViewBuilder _ prefix: @escaping () -> Body) -> Self { + var newSelf = self + newSelf.prefix = prefix() + return newSelf + } + + /// Set the action row's suffix view. + /// - Parameter suffix: The suffix. + /// - Returns: The action row. + public func suffix(@ViewBuilder _ suffix: @escaping () -> Body) -> Self { + var newSelf = self + newSelf.suffix = suffix() + return newSelf + } + +} diff --git a/Sources/Adwaita/View/Forms/ComboRow.swift b/Sources/Adwaita/View/Forms/ComboRow.swift new file mode 100644 index 0000000..4bcbb80 --- /dev/null +++ b/Sources/Adwaita/View/Forms/ComboRow.swift @@ -0,0 +1,132 @@ +// +// ComboRow.swift +// Adwaita +// +// Created by david-swift on 04.01.24. +// + +import Libadwaita + +/// A row for selecting an element out of a list of elements. +public struct ComboRow: Widget +where Element: CustomStringConvertible, Element: Identifiable, Element: Equatable { + + /// The title. + var title: String + /// The selected element. + @Binding var selection: Element.ID + /// The content. + var content: [Element] + /// The subtitle. + var subtitle = "" + /// The prefix. + var prefix: Body = [] + /// The suffix. + var suffix: Body = [] + + /// The identifier for the prefix content. + let prefixID = "prefix" + /// The identifier for the suffix content. + let suffixID = "suffix" + + /// The identifier for the elements. + let elementsID = "elements" + + /// Initialize a combo row. + /// - Parameters: + /// - title: The row's title. + /// - selection: The selected value. + /// - values: The available values. + public init(_ title: String, selection: Binding, values: [Element]) { + self.title = title + self._selection = selection + content = values + } + + /// Update a view storage. + /// - Parameters: + /// - storage: The view storage. + /// - modifiers: Modify views before being updated. + public func update(_ storage: ViewStorage, modifiers: [(View) -> View]) { + if let row = storage.view as? Libadwaita.ComboRow { + update(row: row) + } + if let prefixStorage = storage.content[prefixID]?.first { + prefix.widget(modifiers: modifiers).update(prefixStorage, modifiers: modifiers) + } + if let suffixStorage = storage.content[suffixID]?.first { + suffix.widget(modifiers: modifiers).update(suffixStorage, modifiers: modifiers) + } + } + + /// Get a view storage. + /// - Parameter modifiers: Modify views before being updated. + /// - Returns: The view storage. + public func container(modifiers: [(View) -> View]) -> ViewStorage { + let row: Libadwaita.ComboRow = .init(title: title, subtitle: subtitle) + let prefixContent = prefix.widget(modifiers: modifiers).container(modifiers: modifiers) + let suffixContent = suffix.widget(modifiers: modifiers).container(modifiers: modifiers) + if !prefix.isEmpty { + _ = row.addPrefix(prefixContent.view) + } + if !suffix.isEmpty { + _ = row.addSuffix(suffixContent.view) + } + update(row: row) + _ = row.onChange { + if let element = content.first(where: { $0.description == row.selected() }) { + selection = element.id + } + } + return .init(row, content: [prefixID: [prefixContent], suffixID: [suffixContent]]) + } + + /// Update the combo row. + /// - Parameter row: The combo row. + func update(row: Libadwaita.ComboRow) { + _ = row.title(title) + _ = row.subtitle(subtitle) + let oldElements = row.fields[elementsID] as? [Element] ?? [] + if oldElements != content { + for element in oldElements { + row.remove(at: 0) + } + for element in content { + row.append(element.description) + } + } + let index = content.firstIndex { $0.id == selection } ?? 0 + if row.selected() != content[safe: index]?.description { + row.select(at: index) + } + row.fields[elementsID] = content + } + + /// Set the combo row's subtitle. + /// - Parameter subtitle: The subtitle. + /// - Returns: The combo row. + public func subtitle(_ subtitle: String) -> Self { + var newSelf = self + newSelf.subtitle = subtitle + return newSelf + } + + /// Set the combo row's prefix view. + /// - Parameter prefix: The prefix. + /// - Returns: The combo row. + public func prefix(@ViewBuilder _ prefix: @escaping () -> Body) -> Self { + var newSelf = self + newSelf.prefix = prefix() + return newSelf + } + + /// Set the combo row's suffix view. + /// - Parameter suffix: The suffix. + /// - Returns: The combo row. + public func suffix(@ViewBuilder _ suffix: @escaping () -> Body) -> Self { + var newSelf = self + newSelf.suffix = suffix() + return newSelf + } + +} diff --git a/Sources/Adwaita/View/Forms/EntryRow.swift b/Sources/Adwaita/View/Forms/EntryRow.swift new file mode 100644 index 0000000..f76ab92 --- /dev/null +++ b/Sources/Adwaita/View/Forms/EntryRow.swift @@ -0,0 +1,128 @@ +// +// EntryRow.swift +// Adwaita +// +// Created by david-swift on 04.01.24. +// + +import Libadwaita + +/// A form content row accepting text input. +public struct EntryRow: Widget { + + /// The title. + var title: String + /// The text. + @Binding var text: String + /// The prefix. + var prefix: Body = [] + /// The suffix. + var suffix: Body = [] + /// The handler that gets executed when the user submits the content. + var onSubmit: (() -> Void)? + /// Whether the password entry row should be used. + var password = false + + /// The identifier for the prefix content. + let prefixID = "prefix" + /// The identifier for the suffix content. + let suffixID = "suffix" + + /// Initialize an entry row. + /// - Parameters: + /// - title: The row's title. + /// - text: The text. + public init(_ title: String, text: Binding) { + self.title = title + self._text = text + } + + /// Update a view storage. + /// - Parameters: + /// - storage: The view storage. + /// - modifiers: Modify views before being updated. + public func update(_ storage: ViewStorage, modifiers: [(View) -> View]) { + if let row = storage.view as? Libadwaita.EntryRow { + update(row: row) + } + if let prefixStorage = storage.content[prefixID]?.first { + prefix.widget(modifiers: modifiers).update(prefixStorage, modifiers: modifiers) + } + if let suffixStorage = storage.content[suffixID]?.first { + suffix.widget(modifiers: modifiers).update(suffixStorage, modifiers: modifiers) + } + } + + /// Get a view storage. + /// - Parameter modifiers: Modify views before being updated. + /// - Returns: The view storage. + public func container(modifiers: [(View) -> View]) -> ViewStorage { + let row: Libadwaita.EntryRow + if password { + row = PasswordEntryRow(title: title) + } else { + row = .init(title: title) + } + let prefixContent = prefix.widget(modifiers: modifiers).container(modifiers: modifiers) + let suffixContent = suffix.widget(modifiers: modifiers).container(modifiers: modifiers) + if !prefix.isEmpty { + _ = row.addPrefix(prefixContent.view) + } + if !suffix.isEmpty { + _ = row.addSuffix(suffixContent.view) + } + _ = row.changeHandler { + text = row.contents() + } + update(row: row) + return .init(row, content: [prefixID: [prefixContent], suffixID: [suffixContent]]) + } + + /// Update the entry row. + /// - Parameter row: The entry row. + func update(row: Libadwaita.EntryRow) { + _ = row.title(title) + if row.contents() != text { + row.setContents(text) + } + if let onSubmit { + _ = row.submitHandler(onSubmit) + } + } + + /// Set the entry row's subtitle. + /// - Parameter subtitle: The subtitle. + /// - Returns: The entry row. + public func onSubmit(_ onSubmit: @escaping () -> Void) -> Self { + var newSelf = self + newSelf.onSubmit = onSubmit + return newSelf + } + + /// Set the entry row's prefix view. + /// - Parameter prefix: The prefix. + /// - Returns: The entry row. + public func prefix(@ViewBuilder _ prefix: @escaping () -> Body) -> Self { + var newSelf = self + newSelf.prefix = prefix() + return newSelf + } + + /// Set the entry row's suffix view. + /// - Parameter suffix: The suffix. + /// - Returns: The entry row. + public func suffix(@ViewBuilder _ suffix: @escaping () -> Body) -> Self { + var newSelf = self + newSelf.suffix = suffix() + return newSelf + } + + /// Let the user securely enter private text. + /// - Returns: The entry row. + public func secure() -> Self { + var newSelf = self + newSelf.password = true + return newSelf + } + +} diff --git a/Sources/Adwaita/View/Forms/Form.swift b/Sources/Adwaita/View/Forms/Form.swift new file mode 100644 index 0000000..d172f3c --- /dev/null +++ b/Sources/Adwaita/View/Forms/Form.swift @@ -0,0 +1,47 @@ +// +// Form.swift +// Adwaita +// +// Created by david-swift on 03.01.24. +// + +import Libadwaita + +/// A list with no dynamic content styled as a boxed list. +public struct Form: Widget { + + /// The content. + var content: () -> Body + + /// Initialize a `Form`. + /// - Parameter content: The view content, usually different kind of rows. + public init(@ViewBuilder content: @escaping () -> Body) { + self.content = content + } + + /// Update a view storage. + /// - Parameters: + /// - storage: The view storage. + /// - modifiers: Modify views before being updated. + public func update(_ storage: ViewStorage, modifiers: [(View) -> View]) { + content().update(storage.content[.mainContent] ?? [], modifiers: modifiers) + } + + /// Get a view storage. + /// - Parameter modifiers: Modify views before being updated. + /// - Returns: The view storage. + public func container(modifiers: [(View) -> View]) -> ViewStorage { + let form: ListBox = .init() + _ = form + .noSelection() + .addStyle("boxed-list") + var content: [ViewStorage] = [] + for element in self.content() { + let widget = element.storage(modifiers: modifiers) + _ = form.append(widget.view) + content.append(widget) + } + return .init(form, content: [.mainContent: content]) + } + +} diff --git a/Sources/Adwaita/View/Forms/FormSection.swift b/Sources/Adwaita/View/Forms/FormSection.swift new file mode 100644 index 0000000..4936501 --- /dev/null +++ b/Sources/Adwaita/View/Forms/FormSection.swift @@ -0,0 +1,89 @@ +// +// FormSection.swift +// Adwaita +// +// Created by david-swift on 03.01.24. +// + +import Libadwaita + +/// A section usually groups forms. +public struct FormSection: Widget { + + /// The title. + var title: String + /// The content. + var content: Body + /// The description. + var description = "" + /// The suffix. + var suffix: Body = [] + + /// The identifier for the suffix content. + let suffixID = "suffix" + + /// Initialize a form section. + /// - Parameters: + /// - title: The title. + /// - content: The content, usually one or more forms. + public init(_ title: String, @ViewBuilder content: () -> Body) { + self.title = title + self.content = content() + } + + /// Update a view storage. + /// - Parameters: + /// - storage: The view storage. + /// - modifiers: Modify views before being updated. + public func update(_ storage: ViewStorage, modifiers: [(View) -> View]) { + if let group = storage.view as? Libadwaita.PreferencesGroup { + update(group: group) + } + if let storage = storage.content[.mainContent]?.first { + content.widget(modifiers: modifiers).update(storage, modifiers: modifiers) + } + if let suffixStorage = storage.content[suffixID]?.first { + suffix.widget(modifiers: modifiers).update(suffixStorage, modifiers: modifiers) + } + } + + /// Get a view storage. + /// - Parameter modifiers: Modify views before being updated. + /// - Returns: The view storage. + public func container(modifiers: [(View) -> View]) -> ViewStorage { + let group: Libadwaita.PreferencesGroup = .init(name: title, description: description) + let content = content.widget(modifiers: modifiers).container(modifiers: modifiers) + let suffixContent = suffix.widget(modifiers: modifiers).container(modifiers: modifiers) + group.add(content.view) + if !suffix.isEmpty { + _ = group.headerSuffix(suffixContent.view) + } + return .init(group, content: [.mainContent: [content], suffixID: [suffixContent]]) + } + + /// Update the form section. + /// - Parameter group: The form section. + func update(group: Libadwaita.PreferencesGroup) { + _ = group.title(title) + _ = group.description(description) + } + + /// Set the form section's description. + /// - Parameter description: The description. + /// - Returns: The form section. + public func description(_ description: String) -> Self { + var newSelf = self + newSelf.description = description + return newSelf + } + + /// Set the form section's suffix view. + /// - Parameter suffix: The suffix. + /// - Returns: The form section. + public func suffix(@ViewBuilder _ suffix: @escaping () -> Body) -> Self { + var newSelf = self + newSelf.suffix = suffix() + return newSelf + } + +} diff --git a/Sources/Adwaita/View/Forms/SpinRow.swift b/Sources/Adwaita/View/Forms/SpinRow.swift new file mode 100644 index 0000000..7fae276 --- /dev/null +++ b/Sources/Adwaita/View/Forms/SpinRow.swift @@ -0,0 +1,146 @@ +// +// SpinRow.swift +// Adwaita +// +// Created by david-swift on 04.01.24. +// + +import Libadwaita + +/// A spin row lets the user select an integer in a certain range. +public struct SpinRow: Widget { + + /// The title. + var title: String + /// The selected value. + @Binding var value: Int + /// The minimum value. + var min: Int + /// The maximum value. + var max: Int + /// The increase/decrease step. + var step = 1 + /// The subtitle. + var subtitle = "" + /// The prefix. + var prefix: Body = [] + /// The suffix. + var suffix: Body = [] + + /// The identifier for the prefix content. + let prefixID = "prefix" + /// The identifier for the suffix content. + let suffixID = "suffix" + + /// The identifier of the configuration field. + let configID = "config" + + /// Initialize a spin row. + /// - Parameters: + /// - title: The row's title. + /// - value: The selected value. + /// - min: The minimum value. + /// - max: The maximum value. + public init(_ title: String, value: Binding, min: Int, max: Int) { + self.title = title + self._value = value + self.min = min + self.max = max + } + + /// Update a view storage. + /// - Parameters: + /// - storage: The view storage. + /// - modifiers: Modify views before being updated. + public func update(_ storage: ViewStorage, modifiers: [(View) -> View]) { + if let row = storage.view as? Libadwaita.SpinRow { + update(row: row) + } + if let prefixStorage = storage.content[prefixID]?.first { + prefix.widget(modifiers: modifiers).update(prefixStorage, modifiers: modifiers) + } + if let suffixStorage = storage.content[suffixID]?.first { + suffix.widget(modifiers: modifiers).update(suffixStorage, modifiers: modifiers) + } + } + + /// Get a view storage. + /// - Parameter modifiers: Modify views before being updated. + /// - Returns: The view storage. + public func container(modifiers: [(View) -> View]) -> ViewStorage { + let row: Libadwaita.SpinRow = .init( + title: title, + subtitle: subtitle, + min: .init(min), + max: .init(max), + step: .init(step) + ) + row.fields[configID] = (min, max, step) + let prefixContent = prefix.widget(modifiers: modifiers).container(modifiers: modifiers) + let suffixContent = suffix.widget(modifiers: modifiers).container(modifiers: modifiers) + if !prefix.isEmpty { + _ = row.addPrefix(prefixContent.view) + } + if !suffix.isEmpty { + _ = row.addSuffix(suffixContent.view) + } + _ = row.onChange { + value = .init(row.getValue().rounded()) + } + update(row: row) + return .init(row, content: [prefixID: [prefixContent], suffixID: [suffixContent]]) + } + + // swiftlint:disable large_tuple + /// Update the spin row. + /// - Parameter row: The spin row. + func update(row: Libadwaita.SpinRow) { + _ = row.title(title) + _ = row.subtitle(subtitle) + if row.fields[configID] as? (Int, Int, Int) ?? (0, 0, 0) != (min, max, step) { + _ = row.configuration(min: .init(min), max: .init(max), step: .init(step)) + row.fields[configID] = (min, max, step) + } + if row.getValue() != .init(value) { + row.setValue(.init(value)) + } + } + // swiftlint:enable large_tuple + + /// Set the spin row's subtitle. + /// - Parameter subtitle: The subtitle. + /// - Returns: The spin row. + public func subtitle(_ subtitle: String) -> Self { + var newSelf = self + newSelf.subtitle = subtitle + return newSelf + } + + /// Set the spin row's prefix view. + /// - Parameter prefix: The prefix. + /// - Returns: The spin row. + public func prefix(@ViewBuilder _ prefix: @escaping () -> Body) -> Self { + var newSelf = self + newSelf.prefix = prefix() + return newSelf + } + + /// Set the spin row's suffix view. + /// - Parameter suffix: The suffix. + /// - Returns: The spin row. + public func suffix(@ViewBuilder _ suffix: @escaping () -> Body) -> Self { + var newSelf = self + newSelf.suffix = suffix() + return newSelf + } + + /// Set the difference a single click on the increase/decrease buttons makes. + /// - Parameter step: The increase/decrease step. + /// - Returns: The spin row. + public func step(_ step: Int) -> Self { + var newSelf = self + newSelf.step = step + return newSelf + } + +} diff --git a/Sources/Adwaita/View/Forms/SwitchRow.swift b/Sources/Adwaita/View/Forms/SwitchRow.swift new file mode 100644 index 0000000..b84850f --- /dev/null +++ b/Sources/Adwaita/View/Forms/SwitchRow.swift @@ -0,0 +1,111 @@ +// +// SwitchRow.swift +// Adwaita +// +// Created by david-swift on 04.01.24. +// + +import Libadwaita + +/// A row representing a boolean state. +public struct SwitchRow: Widget { + + /// The title. + var title: String + /// Whether the switch is activated. + @Binding var isOn: Bool + /// The subtitle. + var subtitle = "" + /// The prefix. + var prefix: Body = [] + /// The suffix. + var suffix: Body = [] + + /// The identifier for the prefix content. + let prefixID = "prefix" + /// The identifier for the suffix content. + let suffixID = "suffix" + + /// Initialize a switch row. + /// - Parameters: + /// - title: The row's title. + /// - isOn: Whether the switch is on. + public init(_ title: String, isOn: Binding) { + self.title = title + self._isOn = isOn + } + + /// Update a view storage. + /// - Parameters: + /// - storage: The view storage. + /// - modifiers: Modify views before being updated. + public func update(_ storage: ViewStorage, modifiers: [(View) -> View]) { + if let row = storage.view as? Libadwaita.SwitchRow { + update(row: row) + } + if let prefixStorage = storage.content[prefixID]?.first { + prefix.widget(modifiers: modifiers).update(prefixStorage, modifiers: modifiers) + } + if let suffixStorage = storage.content[suffixID]?.first { + suffix.widget(modifiers: modifiers).update(suffixStorage, modifiers: modifiers) + } + } + + /// Get a view storage. + /// - Parameter modifiers: Modify views before being updated. + /// - Returns: The view storage. + public func container(modifiers: [(View) -> View]) -> ViewStorage { + let row: Libadwaita.SwitchRow = .init(title: title, subtitle: subtitle) + let prefixContent = prefix.widget(modifiers: modifiers).container(modifiers: modifiers) + let suffixContent = suffix.widget(modifiers: modifiers).container(modifiers: modifiers) + if !prefix.isEmpty { + _ = row.addPrefix(prefixContent.view) + } + if !suffix.isEmpty { + _ = row.addSuffix(suffixContent.view) + } + _ = row.onChange { + isOn = row.getActive() + } + update(row: row) + return .init(row, content: [prefixID: [prefixContent], suffixID: [suffixContent]]) + } + + /// Update the switch row. + /// - Parameter row: The switch row. + func update(row: Libadwaita.SwitchRow) { + _ = row.title(title) + _ = row.subtitle(subtitle) + if row.getActive() != isOn { + row.setActive(isOn) + } + } + + /// Set the switch row's subtitle. + /// - Parameter subtitle: The subtitle. + /// - Returns: The switch row. + public func subtitle(_ subtitle: String) -> Self { + var newSelf = self + newSelf.subtitle = subtitle + return newSelf + } + + /// Set the switch row's prefix view. + /// - Parameter prefix: The prefix. + /// - Returns: The switch row. + public func prefix(@ViewBuilder _ prefix: @escaping () -> Body) -> Self { + var newSelf = self + newSelf.prefix = prefix() + return newSelf + } + + /// Set the switch row's suffix view. + /// - Parameter suffix: The suffix. + /// - Returns: The switch row. + public func suffix(@ViewBuilder _ suffix: @escaping () -> Body) -> Self { + var newSelf = self + newSelf.suffix = suffix() + return newSelf + } + +} diff --git a/Sources/Adwaita/View/Modifiers/View+.swift b/Sources/Adwaita/View/Modifiers/View+.swift new file mode 100644 index 0000000..91b3023 --- /dev/null +++ b/Sources/Adwaita/View/Modifiers/View+.swift @@ -0,0 +1,17 @@ +// +// View+.swift +// Adwaita +// +// Created by david-swift on 03.01.24. +// + +extension View { + + /// Wrap the view in a vertical stack and center vertically. + /// - Returns: The view. + public func verticalCenter() -> View { + VStack { self } + .valign(.center) + } + +} diff --git a/Sources/Adwaita/View/Toggle.swift b/Sources/Adwaita/View/Toggle.swift index 7713bbd..2660c08 100644 --- a/Sources/Adwaita/View/Toggle.swift +++ b/Sources/Adwaita/View/Toggle.swift @@ -16,6 +16,8 @@ public struct Toggle: Widget { var icon: Icon? /// Whether the toggle is on. @Binding var isOn: Bool + /// Whether to use GtkCheckButton instead of GtkToggleButton + var isCheckButton = false // swiftlint:disable function_default_parameter_at_end /// Initialize a toggle button. @@ -23,7 +25,7 @@ public struct Toggle: Widget { /// - label: The button's label. /// - icon: The button's icon. /// - isOn: Whether the toggle is on. - public init(_ label: String? = nil, icon: Icon, isOn: Binding) { + public init(_ label: String? = nil, icon: Icon? = nil, isOn: Binding) { self.label = label self.icon = icon self._isOn = isOn @@ -46,6 +48,8 @@ public struct Toggle: Widget { public func update(_ storage: ViewStorage, modifiers: [(View) -> View]) { if let toggle = storage.view as? Libadwaita.ToggleButton { updateState(toggle: toggle) + } else if let toggle = storage.view as? Libadwaita.CheckButton { + updateState(toggle: toggle) } } @@ -53,12 +57,21 @@ public struct Toggle: Widget { /// - Parameter modifiers: Modify views before being updated. /// - Returns: The button's view storage. public func container(modifiers: [(View) -> View]) -> ViewStorage { - let toggle: Libadwaita.ToggleButton = .init(label ?? "") - updateState(toggle: toggle) - toggle.handler { - self.isOn.toggle() + if isCheckButton { + let toggle: Libadwaita.CheckButton = .init(label ?? "") + updateState(toggle: toggle) + _ = toggle.handler { + self.isOn.toggle() + } + return .init(toggle) + } else { + let toggle: Libadwaita.ToggleButton = .init(label ?? "") + updateState(toggle: toggle) + _ = toggle.handler { + self.isOn.toggle() + } + return .init(toggle) } - return .init(toggle) } /// Update the toggle's state. @@ -72,4 +85,21 @@ public struct Toggle: Widget { toggle.setActive(isOn) } + /// Update the check button's state. + /// - Parameter toggle: The toggle. + func updateState(toggle: Libadwaita.CheckButton) { + if let label { + toggle.setLabel(label) + } + toggle.setActive(isOn) + } + + /// Use the check button style. + /// - Returns: The toggle. + public func checkButton() -> Self { + var newSelf = self + newSelf.isCheckButton = true + return newSelf + } + } diff --git a/Tests/Demo.swift b/Tests/Demo.swift index 0fee9e4..7ad34c0 100644 --- a/Tests/Demo.swift +++ b/Tests/Demo.swift @@ -56,6 +56,12 @@ struct Demo: App { .defaultSize(width: 600, height: 400) .resizable(false) .title("View Switcher Demo") + Window(id: "form-demo", open: 0) { _ in + FormDemo.WindowContent() + } + .closeShortcut() + .defaultSize(width: 400, height: 250) + .title("Form Demo") } } diff --git a/Tests/FormDemo.swift b/Tests/FormDemo.swift new file mode 100644 index 0000000..6922116 --- /dev/null +++ b/Tests/FormDemo.swift @@ -0,0 +1,88 @@ +// +// FormDemo.swift +// Adwaita +// +// Created by david-swift on 03.01.24. +// + +// swiftlint:disable missing_docs no_magic_numbers + +import Adwaita +import Libadwaita + +struct FormDemo: View { + + var app: GTUIApp + + var view: Body { + VStack { + Button("View Demo") { + app.showWindow("form-demo") + } + .style("suggested-action") + .frame(maxSize: 100) + } + } + + struct WindowContent: View { + + @State private var text = "They also have a subtitle" + @State private var password = "Password" + @State private var value = 0 + @State private var isOn = true + @State private var selection = "Hello" + + let values: [ListDemo.Element] = [.init(id: "Hello"), .init(id: "World")] + + var view: Body { + ScrollView { + VStack { + Form { + ActionRow("Rows have a title") + .subtitle(text) + ActionRow("Rows can have suffix widgets") + .suffix { + Button("Action") { } + .verticalCenter() + } + } + .padding() + FormSection("Entry Rows") { + Form { + EntryRow("Entry Row", text: $text) + .suffix { + Button(icon: .default(icon: .editCopy)) { Clipboard.copy(text) } + .style("flat") + .verticalCenter() + } + EntryRow("Password", text: $password) + .secure() + } + } + .padding() + rowDemo("Spin Rows", row: SpinRow("Spin Row", value: $value, min: 0, max: 100)) + rowDemo("Switch Rows", row: SwitchRow("Switch Row", isOn: $isOn)) + rowDemo("Combo Rows", row: ComboRow("Combo Row", selection: $selection, values: values)) + } + .padding() + .frame(maxSize: 400) + } + .topToolbar { + HeaderBar.empty() + } + } + + func rowDemo(_ title: String, row: View) -> View { + FormSection(title) { + Form { + row + } + } + .padding() + } + + } + +} + +// swiftlint:enable missing_docs no_magic_numbers diff --git a/Tests/ListDemo.swift b/Tests/ListDemo.swift index dc12cc4..0d97670 100644 --- a/Tests/ListDemo.swift +++ b/Tests/ListDemo.swift @@ -45,9 +45,10 @@ struct ListDemo: View { } } - struct Element: Identifiable { + struct Element: Identifiable, CustomStringConvertible, Equatable { var id: String + var description: String { id } } diff --git a/Tests/Page.swift b/Tests/Page.swift index 622ef90..4902a7a 100644 --- a/Tests/Page.swift +++ b/Tests/Page.swift @@ -23,6 +23,7 @@ enum Page: String, Identifiable, CaseIterable, Codable { case list case carousel case viewSwitcher + case form var id: Self { self @@ -72,6 +73,8 @@ enum Page: String, Identifiable, CaseIterable, Codable { return "Scroll horizontally on a touchpad or touchscreen, or scroll down on your mouse wheel." case .viewSwitcher: return "Switch the window's view." + case .form: + return "Group controls used for data entry." } } @@ -101,6 +104,8 @@ enum Page: String, Identifiable, CaseIterable, Codable { CarouselDemo() case .viewSwitcher: ViewSwitcherDemo(app: app) + case .form: + FormDemo(app: app) } } // swiftlint:enable cyclomatic_complexity diff --git a/user-manual/Information/Widgets.md b/user-manual/Information/Widgets.md index c1c5b14..04af915 100644 --- a/user-manual/Information/Widgets.md +++ b/user-manual/Information/Widgets.md @@ -23,6 +23,13 @@ This is an overview of the available widgets and other components in _Adwaita_. | ProgressBar | A bar showing a progress. | GtkProgressBar | | Banner | A bar showing contextual information. | AdwBanner | | StateWrapper | A wrapper not affecting the UI which stores state information. | - | +| FormSection | A titled section, usually containing one or multiple forms. | AdwPreferencesGroup | +| Form | A static boxed list, usually containing one or multiple rows. | GtkListBox | +| ActionRow | The most basic row displaying text and optionally other views. | AdwActionRow | +| ComboRow | A row displaying an array, letting the user choose one element. | AdwComboRow | +| EntryRow | A row for text input. | AdwEntryRow | +| SpinRow | A row for selecting an integer in a range. | AdwSpinRow | +| SwitchRow | A row controlling a simple boolean value. | AdwSwitchRow | ### View Modifiers @@ -51,6 +58,7 @@ This is an overview of the available widgets and other components in _Adwaita_. | `overlay(_:)` | Overlay a view with another view. | | `insensitive(_:)` | Make a view unable to detect actions. This is especially useful for overlays. | | `onClick(handler:)` | Run a function when the user clicks on the widget. | +| `verticalCenter()` | Wrap a view in a `VStack` and center vertically. | ### `Button` Modifiers | Syntax | Description | @@ -63,6 +71,11 @@ This is an overview of the available widgets and other components in _Adwaita_. | ---------------------------- | --------------------------------------------------------------------------------------- | | `headerBarTitle(view:)` | Customize the title view in the header bar. | +### `Toggle` Modifiers +| Syntax | Description | +| ---------------------------- | --------------------------------------------------------------------------------------- | +| `checkButton()` | Use a check button design instead of a toggle. | + ### `List` Modifiers | Syntax | Description | | ---------------------------- | --------------------------------------------------------------------------------------- | @@ -78,11 +91,54 @@ This is an overview of the available widgets and other components in _Adwaita_. | ---------------------------- | --------------------------------------------------------------------------------------- | | `wideDesign(_:)` | Whether the wide view switcher design is used. | -## `Banner` Modifiers +### `Banner` Modifiers | Syntax | Description | | ---------------------------- | --------------------------------------------------------------------------------------- | | `button(_:handler)` | Show the banner's button and set its title and handler. | +### `FormSection` Modifiers +| Syntax | Description | +| ---------------------------- | --------------------------------------------------------------------------------------- | +| `description(_:)` | Set the section's description. | +| `suffix(_:)` | Set the section's suffix view. | + +### `ActionRow` Modifiers +| Syntax | Description | +| ---------------------------- | --------------------------------------------------------------------------------------- | +| `subtitle(_:)` | Set the row's subtitle. | +| `prefix(_:)` | Set the row's prefix view. | +| `suffix(_:)` | Set the row's suffix view. | + +### `ComboRow` Modifiers +| Syntax | Description | +| ---------------------------- | --------------------------------------------------------------------------------------- | +| `subtitle(_:)` | Set the row's subtitle. | +| `prefix(_:)` | Set the row's prefix view. | +| `suffix(_:)` | Set the row's suffix view. | + +### `EntryRow` Modifiers +| Syntax | Description | +| ---------------------------- | --------------------------------------------------------------------------------------- | +| `onSubmit(_:)` | Add a submit button to the entry row. Run the provided closure when it gets pressed. | +| `prefix(_:)` | Set the row's prefix view. | +| `suffix(_:)` | Set the row's suffix view. | +| `secure()` | Use the secure design for password inputs etc. | + +### `SpinRow` Modifiers +| Syntax | Description | +| ---------------------------- | --------------------------------------------------------------------------------------- | +| `subtitle(_:)` | Set the row's subtitle. | +| `prefix(_:)` | Set the row's prefix view. | +| `suffix(_:)` | Set the row's suffix view. | +| `step(_:)` | Set the increase/decrease rate of the buttons. | + +### `SwitchRow` Modifiers +| Syntax | Description | +| ---------------------------- | --------------------------------------------------------------------------------------- | +| `subtitle(_:)` | Set the row's subtitle. | +| `prefix(_:)` | Set the row's prefix view. | +| `suffix(_:)` | Set the row's suffix view. | + ### Window Types | Name | Description | Widget | | -------------------- | ----------------------------------------------------------------- | ---------------------- |