Add support for grouping controls with forms

This commit is contained in:
david-swift 2024-01-04 10:58:33 +01:00
parent 1e6143acce
commit 4dc5d07409
25 changed files with 1509 additions and 9 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Element>: 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<Element.ID>, 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
}
}

View File

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

View File

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

View File

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

View File

@ -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<Int>, 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
}
}

View File

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

View File

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

View File

@ -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<Bool>) {
public init(_ label: String? = nil, icon: Icon? = nil, isOn: Binding<Bool>) {
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
}
}

View File

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

88
Tests/FormDemo.swift Normal file
View File

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

View File

@ -45,9 +45,10 @@ struct ListDemo: View {
}
}
struct Element: Identifiable {
struct Element: Identifiable, CustomStringConvertible, Equatable {
var id: String
var description: String { id }
}

View File

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

View File

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