Add widgets and demo application

This commit is contained in:
david-swift 2023-09-26 15:30:07 +02:00
parent 2611e0c448
commit cbbf08bd3b
40 changed files with 1142 additions and 172 deletions

View File

@ -118,14 +118,6 @@ custom_rules:
message: 'Spaces should be used instead of tabs.' message: 'Spaces should be used instead of tabs.'
severity: warning severity: warning
string_literals:
name: 'String Literals'
regex: '(".*")|("""(.|\n)*""")'
message: 'String literals should not be used. Disable this rule in String and LocalizedStringResource extensions.'
match_kinds:
- string
severity: warning
# Thanks to the creator of the SwiftLint rule # Thanks to the creator of the SwiftLint rule
# "empty_first_line" # "empty_first_line"
# https://github.com/coteditor/CotEditor/blob/main/.swiftlint.yml # https://github.com/coteditor/CotEditor/blob/main/.swiftlint.yml

View File

@ -14,10 +14,17 @@
- [Binding](structs/Binding.md) - [Binding](structs/Binding.md)
- [Button](structs/Button.md) - [Button](structs/Button.md)
- [EitherView](structs/EitherView.md) - [EitherView](structs/EitherView.md)
- [HStack](structs/HStack.md)
- [HeaderBar](structs/HeaderBar.md) - [HeaderBar](structs/HeaderBar.md)
- [InspectorWrapper](structs/InspectorWrapper.md) - [InspectorWrapper](structs/InspectorWrapper.md)
- [List](structs/List.md)
- [NavigationSplitView](structs/NavigationSplitView.md)
- [ScrollView](structs/ScrollView.md)
- [State](structs/State.md) - [State](structs/State.md)
- [StateWrapper](structs/StateWrapper.md)
- [StatusPage](structs/StatusPage.md)
- [Text](structs/Text.md) - [Text](structs/Text.md)
- [ToolbarView](structs/ToolbarView.md)
- [UpdateObserver](structs/UpdateObserver.md) - [UpdateObserver](structs/UpdateObserver.md)
- [VStack](structs/VStack.md) - [VStack](structs/VStack.md)
- [Window](structs/Window.md) - [Window](structs/Window.md)

View File

@ -15,7 +15,7 @@ The view's content.
### `state` ### `state`
The view's state (used in `VStack`). The view's state (used in `StateWrapper`).
## Methods ## Methods
### `init(_:content:state:)` ### `init(_:content:state:)`

View File

@ -2,6 +2,11 @@
# `Array` # `Array`
## Properties
### `view`
The array's view body is the array itself.
## Methods ## Methods
### `widget()` ### `widget()`

View File

@ -10,3 +10,7 @@ A label for main content in a view storage.
### `transition` ### `transition`
A label for the transition data in a GTUI widget's fields. A label for the transition data in a GTUI widget's fields.
### `navigationLabel`
A label for the navigation label in a GTUI widget's fields.

View File

@ -44,6 +44,18 @@ Enable or disable the vertical expansion.
- Parameter enabled: Whether it is enabled or disabled. - Parameter enabled: Whether it is enabled or disabled.
- Returns: A view. - Returns: A view.
### `halign(_:)`
Set the horizontal alignment.
- Parameter align: The alignment.
- Returns: A view.
### `valign(_:)`
Set the vertical alignment.
- Parameter align: The alignment.
- Returns: A view.
### `frame(minWidth:minHeight:)` ### `frame(minWidth:minHeight:)`
Set the view's minimal width or height. Set the view's minimal width or height.
@ -64,6 +76,40 @@ Set the view's transition.
- Parameter transition: The transition. - Parameter transition: The transition.
- Returns: A view. - Returns: A view.
### `navigationTitle(_:)`
Set the view's navigation title.
- Parameter label: The navigation title.
- Returns: A view.
### `style(_:)`
Add a style class to the view.
- Parameter style: The style class.
- Returns: A view.
### `onAppear(_:)`
Run a function when the view appears for the first time.
- Parameter closure: The function.
- Returns: A view.
### `topToolbar(visible:_:)`
Add a top toolbar to the view.
- Parameters:
- toolbar: The toolbar's content.
- visible: Whether the toolbar is visible.
- Returns: A view.
### `bottomToolbar(visible:_:)`
Add a bottom toolbar to the view.
- Parameters:
- toolbar: The toolbar's content.
- visible: Whether the toolbar is visible.
- Returns: A view.
### `onUpdate(_:)` ### `onUpdate(_:)`
Run a function when the view gets an update. Run a function when the view gets an update.

View File

@ -0,0 +1,26 @@
**STRUCT**
# `HStack`
A horizontal GtkBox equivalent.
## Properties
### `content`
The content.
## Methods
### `init(content:)`
Initialize a `HStack`.
- Parameter content: The view content.
### `update(_:)`
Update a view storage.
- Parameter storage: The view storage.
### `container()`
Get a view storage.
- Returns: The view storage.

View File

@ -0,0 +1,46 @@
**STRUCT**
# `List`
A list box widget.
## Properties
### `elements`
The elements.
### `content`
The content.
### `selection`
The identifier of the selected element.
## Methods
### `init(_:selection:content:)`
Initialize `ForEach`.
- Parameters:
- elements: The elements.
- selection: The identifier of the selected element.
- content: The view for an element.
### `update(_:)`
Update a view storage.
- Parameter storage: The view storage.
### `container()`
Get a view storage.
- Returns: The view storage.
### `updateSelection(box:)`
Update the list's selection.
- Parameter box: The list box.
### `sidebarStyle()`
Add the "navigation-sidebar" style class.

View File

@ -0,0 +1,40 @@
**STRUCT**
# `NavigationSplitView`
A navigation split view widget.
## Properties
### `sidebar`
The sidebar's content.
### `content`
The split view's main content.
### `sidebarID`
The sidebar content's id.
### `contentID`
The main content's id.
## Methods
### `init(sidebar:content:)`
Initialize a navigation split view.
- Parameters:
- sidebar: The sidebar content.
- content: The main content.
### `container()`
Get the container of the navigation split view widget.
- Returns: The view storage.
### `update(_:)`
Update the view storage of the navigation split view widget.
- Parameter storage: The view storage.

View File

@ -0,0 +1,26 @@
**STRUCT**
# `ScrollView`
A GtkScrolledWindow equivalent.
## Properties
### `content`
The content.
## Methods
### `init(content:)`
Initialize a `ScrollView`.
- Parameter content: The view content.
### `update(_:)`
Update a view storage.
- Parameter storage: The view storage.
### `container()`
Get a view storage.
- Returns: The view storage.

View File

@ -0,0 +1,37 @@
**STRUCT**
# `StateWrapper`
A storage for `@State` properties.
## Properties
### `content`
The content.
### `state`
The state information (from properties with the `State` wrapper).
## Methods
### `init(content:)`
Initialize a `StateWrapper`.
- Parameter content: The view content.
### `init(content:state:)`
Initialize a `StateWrapper`.
- Parameters:
- content: The view content.
- state: The state information.
### `update(_:)`
Update a view storage.
- Parameter storage: The view storage.
### `container()`
Get a view storage.
- Returns: The view storage.

View File

@ -0,0 +1,42 @@
**STRUCT**
# `StatusPage`
A status page widget.
## Properties
### `title`
The title.
### `description`
The description.
### `icon`
The icon.
### `content`
Additional content.
## Methods
### `init(_:icon:description:content:)`
Initialize a status page widget.
- Parameters:
- title: The title.
- icon: The icon.
- description: Additional details.
- content: Additional content.
### `update(_:)`
Update the view storage of the text widget.
- Parameter storage: The view storage.
### `container()`
Get the container of the text widget.
- Returns: The view storage.

View File

@ -0,0 +1,37 @@
**STRUCT**
# `ToolbarView`
A toolbar view widget.
## Properties
### `content`
The sidebar's content.
### `toolbar`
The toolbars.
### `bottom`
Whether the toolbars are bottom toolbars.
### `visible`
Whether the toolbar is visible.
### `toolbarID`
The identifier of the toolbar content.
## Methods
### `container()`
Get the container of the toolbar view widget.
- Returns: The view storage.
### `update(_:)`
Update the view storage of the toolbar view widget.
- Parameter storage: The view storage.

View File

@ -9,23 +9,12 @@ A GtkBox equivalent.
The content. The content.
### `state`
The state information (from properties with the `State` wrapper).
## Methods ## Methods
### `init(content:)` ### `init(content:)`
Initialize a `VStack`. Initialize a `VStack`.
- Parameter content: The view content. - Parameter content: The view content.
### `init(content:state:)`
Initialize a `VStack`.
- Parameters:
- content: The view content.
- state: The state information.
### `update(_:)` ### `update(_:)`
Update a view storage. Update a view storage.

BIN
Icons/Demo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -26,7 +26,7 @@ let package = Package(
dependencies: [.product(name: "GTUI", package: "swiftgui")] dependencies: [.product(name: "GTUI", package: "swiftgui")]
), ),
.executableTarget( .executableTarget(
name: "Counter", name: "Swift Adwaita Demo",
dependencies: ["Adwaita"], dependencies: ["Adwaita"],
path: "Tests" path: "Tests"
) )

137
README.md
View File

@ -27,16 +27,19 @@ struct Example: View {
@State private var count = 0 @State private var count = 0
var view: Body { var view: Body {
HeaderBar.start {
Button(icon: .default(icon: .goPrevious)) {
count -= 1
}
Button(icon: .default(icon: .goNext)) {
count += 1
}
}
Text("\(count)") Text("\(count)")
.style("title-1")
.padding(50) .padding(50)
.topToolbar {
HeaderBar.start {
Button(icon: .default(icon: .goPrevious)) {
count -= 1
}
Button(icon: .default(icon: .goNext)) {
count += 1
}
}
}
} }
} }
@ -46,49 +49,66 @@ Creates a simple counter view:
![Counter Example][image-1] ![Counter Example][image-1]
More examples are available in the [Demo app][1]:
![Demo App][image-2]
## Table of Contents ## Table of Contents
- [Goals][1] - [Goals][2]
- [Widgets][2] - [Widgets][3]
- [Installation][3] - [Installation][4]
- [Usage][4] - [Usage][5]
- [Thanks][5] - [Thanks][6]
## Goals ## Goals
_Adwaita_s main goal is to provide an easy-to-use interface for creating GNOME apps. The backend should stay as simple as possible, while not limiting the possibilities there are with [Libadwaita][6] and [GTK][7]. _Adwaita_s main goal is to provide an easy-to-use interface for creating GNOME apps. The backend should stay as simple as possible, while not limiting the possibilities there are with [Libadwaita][7] and [GTK][8].
If you want to use _Adwaita_ in a project, but there are widgets missing, open an [issue on GitHub][8]. If you want to use _Adwaita_ in a project, but there are widgets missing, open an [issue on GitHub][9].
## Widgets ## Widgets
| Name | Description | Widget | | Name | Description | Widget |
| ---------- | ----------------------------------------------------------------- | ------------ | | -------------------- | ----------------------------------------------------------------- | ---------------------- |
| Button | A widget that triggers a function when being clicked. | GtkButton | | Button | A widget that triggers a function when being clicked. | GtkButton |
| EitherView | A widget that displays one of its child views based on a boolean. | GtkStack | | EitherView | A widget that displays one of its child views based on a boolean. | GtkStack |
| HeaderBar | A widget for creating custom title bars for windows. | GtkHeaderBar | | HeaderBar | A widget for creating custom title bars for windows. | GtkHeaderBar |
| Text | A widget for displaying a small amount of text. | GtkLabel | | Text | A widget for displaying a small amount of text. | GtkLabel |
| VStack | A widget which arranges child widgets into a single column. | GtkBox | | VStack | A widget which arranges child widgets into a single column. | GtkBox |
| HStack | A widget which arranges child widgets into a single row. | GtkBox |
| List | A widget which arranges child widgets vertically into rows. | GtkListBox |
| NavigationSplitView | A widget presenting sidebar and content side by side. | AdwNavigationSplitView |
| ScrollView | A container that makes its child scrollable. | GtkScrolledWindow |
| StatusPage | A page with an icon, title, and optionally description and widget.| AdwStatusPage |
| StateWrapper | A wrapper not affecting the UI which stores state information. | - |
### View Modifiers ### View Modifiers
| Syntax | Description | | Syntax | Description |
| ---------------------------- | -------------------------------------------------------------------------------------- | | ---------------------------- | -------------------------------------------------------------------------------------- |
| `inspect(_:)` | Edit the underlying [GTUI][9] widget. | | `inspect(_:)` | Edit the underlying [GTUI][10] widget. |
| `padding(_:_:)` | Add empty space around a view. | | `padding(_:_:)` | Add empty space around a view. |
| `hexpand(_:)` | Enable or disable the horizontal expansion of a view. | | `hexpand(_:)` | Enable or disable the horizontal expansion of a view. |
| `vexpand(_:)` | Enable or disable the vertical expansion of a view. | | `vexpand(_:)` | Enable or disable the vertical expansion of a view. |
| `halign(_:)` | Set the horizontal alignment of a view. |
| `valign(_:)` | Set the vertical alignment of a view. |
| `frame(minWidth:minHeight:)` | Set the views minimal width or height. | | `frame(minWidth:minHeight:)` | Set the views minimal width or height. |
| `frame(maxSize:)` | Set the views maximal size. | | `frame(maxSize:)` | Set the views maximal size. |
| `transition(_:)` | Assign a transition with the view that is used if it is a direct child of a HeaderBar. | | `transition(_:)` | Assign a transition with the view that is used if it is a direct child of an EitherView. |
| `onUpdate(_:)` | Run a function every time a view gets updated. | | `onUpdate(_:)` | Run a function every time a view gets updated. |
| `navigationTitle(_:)` | Add a title that is used if the view is a direct child of a NavigationView. |
| `style(_:)` | Add a style class to the view. |
| `onAppear(_:)` | Run when the view is rendered for the first time. |
| `topToolbar(visible:_:)` | Add a native toolbar to the view. Normally, it contains a HeaderBar. |
| `bottomToolbar(visible:_:)` | Add a native bottom toolbar to the view. |
## Installation ## Installation
### Dependencies ### Dependencies
If you are using a Linux distribution, install `libadwaita-devel` or `libadwaita` (or something similar, based on the package manager) as well as `gtk4-devel`, `gtk4` or similar. If you are using a Linux distribution, install `libadwaita-devel` or `libadwaita` (or something similar, based on the package manager) as well as `gtk4-devel`, `gtk4` or similar.
On macOS, follow these steps: On macOS, follow these steps:
1. Install [Homebrew][10]. 1. Install [Homebrew][11].
2. Install Libadwaita (and thereby GTK 4): 2. Install Libadwaita (and thereby GTK 4):
``` ```
brew install libadwaita brew install libadwaita
@ -104,45 +124,52 @@ brew install libadwaita
## Usage ## Usage
* [Getting Started][11] * [Getting Started][12]
### Basics ### Basics
* [Hello World][12] * [Hello World][13]
* [Creating Views][13] * [Creating Views][14]
* [Windows][14] * [Windows][15]
### Advanced
* [Creating Widgets][16]
## Thanks ## Thanks
### Dependencies ### Dependencies
- [SwiftGui][15] licensed under the [GPL-3.0 license][16] - [SwiftGui][17] licensed under the [GPL-3.0 license][18]
### Other Thanks ### Other Thanks
- The [contributors][17] - The [contributors][19]
- [SwiftLint][18] for checking whether code style conventions are violated - [SwiftLint][20] for checking whether code style conventions are violated
- The programming language [Swift][19] - The programming language [Swift][21]
- [SourceDocs][20] used for generating the [docs][21] - [SourceDocs][22] used for generating the [docs][23]
[1]: #goals [1]: Tests/
[2]: #widgets [2]: #goals
[3]: #installation [3]: #widgets
[4]: #usage [4]: #installation
[5]: #thanks [5]: #usage
[6]: https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/index.html [6]: #thanks
[7]: https://docs.gtk.org/gtk4/ [7]: https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/index.html
[8]: https://github.com/david-swift/Adwaita/issues [8]: https://docs.gtk.org/gtk4/
[9]: https://github.com/JCWasmx86/SwiftGui [9]: https://github.com/david-swift/Adwaita/issues
[10]: https://brew.sh [10]: https://github.com/JCWasmx86/SwiftGui
[11]: user-manual/GettingStarted.md [11]: https://brew.sh
[12]: user-manual/Basics/HelloWorld.md [12]: user-manual/GettingStarted.md
[13]: user-manual/Basics/CreatingViews.md [13]: user-manual/Basics/HelloWorld.md
[14]: user-manual/Basics/Windows.md [14]: user-manual/Basics/CreatingViews.md
[15]: https://github.com/JCWasmx86/SwiftGui [15]: user-manual/Basics/Windows.md
[16]: https://github.com/JCWasmx86/SwiftGui/blob/main/COPYING [16]: user-manual/Advanced/CreatingWidgets.md
[17]: Contributors.md [17]: https://github.com/JCWasmx86/SwiftGui
[18]: https://github.com/realm/SwiftLint [18]: https://github.com/JCWasmx86/SwiftGui/blob/main/COPYING
[19]: https://github.com/apple/swift [19]: Contributors.md
[20]: https://github.com/SourceDocs/SourceDocs [20]: https://github.com/realm/SwiftLint
[21]: Documentation/Reference/README.md [21]: https://github.com/apple/swift
[22]: https://github.com/SourceDocs/SourceDocs
[23]: Documentation/Reference/README.md
[image-1]: Icons/Screenshot.png [image-1]: Icons/Screenshot.png
[image-2]: Icons/Demo.png

View File

@ -9,8 +9,13 @@
* [Creating Views][4] * [Creating Views][4]
* [Windows][5] * [Windows][5]
## Advanced
* [Creating Widgets][6]
[1]: README.md [1]: README.md
[2]: user-manual/GettingStarted.md [2]: user-manual/GettingStarted.md
[3]: user-manual/Basics/HelloWorld.md [3]: user-manual/Basics/HelloWorld.md
[4]: user-manual/Basics/CreatingViews.md [4]: user-manual/Basics/CreatingViews.md
[5]: user-manual/Basics/Windows.md [5]: user-manual/Basics/Windows.md
[6]: user-manual/Advanced/CreatingWidgets.md

View File

@ -7,7 +7,10 @@
import GTUI import GTUI
extension Array where Element == View { extension Array: View where Element == View {
/// The array's view body is the array itself.
public var view: Body { self }
/// Get a widget from a collection of views. /// Get a widget from a collection of views.
/// - Returns: A widget. /// - Returns: A widget.

View File

@ -11,5 +11,7 @@ extension String {
static var mainContent: Self { "main" } static var mainContent: Self { "main" }
/// A label for the transition data in a GTUI widget's fields. /// A label for the transition data in a GTUI widget's fields.
static var transition: Self { "transition" } static var transition: Self { "transition" }
/// A label for the navigation label in a GTUI widget's fields.
static var navigationLabel: Self { "navigation-label" }
} }

View File

@ -41,7 +41,7 @@ extension View {
state[label] = value state[label] = value
} }
} }
return VStack(content: { view }, state: state) return StateWrapper(content: { view }, state: state)
} }
} }
@ -50,6 +50,8 @@ extension View {
func updateStorage(_ storage: ViewStorage) { func updateStorage(_ storage: ViewStorage) {
if let widget = self as? Widget { if let widget = self as? Widget {
widget.update(storage) widget.update(storage)
} else {
StateWrapper { self }.update(storage)
} }
} }

View File

@ -14,7 +14,7 @@ public class ViewStorage {
public var view: NativeWidgetPeer public var view: NativeWidgetPeer
/// The view's content. /// The view's content.
public var content: [String: [ViewStorage]] public var content: [String: [ViewStorage]]
/// The view's state (used in `VStack`). /// The view's state (used in `StateWrapper`).
public var state: [String: StateProtocol] public var state: [String: StateProtocol]
/// Initialize a view storage. /// Initialize a view storage.

View File

@ -0,0 +1,41 @@
//
// HStack.swift
// Adwaita
//
// Created by david-swift on 26.09.23.
//
import GTUI
/// A horizontal GtkBox equivalent.
public struct HStack: Widget {
/// The content.
var content: () -> Body
/// Initialize a `HStack`.
/// - Parameter content: The view content.
public init(@ViewBuilder content: @escaping () -> Body) {
self.content = content
}
/// Update a view storage.
/// - Parameter storage: The view storage.
public func update(_ storage: ViewStorage) {
content().update(storage.content[.mainContent] ?? [])
}
/// Get a view storage.
/// - Returns: The view storage.
public func container() -> ViewStorage {
let box: Box = .init(horizontal: true)
var content: [ViewStorage] = []
for element in self.content() {
let widget = element.storage()
_ = box.append(widget.view)
content.append(widget)
}
return .init(box, content: [.mainContent: content])
}
}

View File

@ -0,0 +1,76 @@
//
// List.swift
// Adwaita
//
// Created by david-swift on 25.09.23.
//
import GTUI
/// A list box widget.
public struct List<Element>: Widget where Element: Identifiable {
/// The elements.
var elements: [Element]
/// The content.
var content: (Element) -> Body
/// The identifier of the selected element.
@Binding var selection: Element.ID
/// Initialize `ForEach`.
/// - Parameters:
/// - elements: The elements.
/// - selection: The identifier of the selected element.
/// - content: The view for an element.
public init(
_ elements: [Element],
selection: Binding<Element.ID>,
@ViewBuilder content: @escaping (Element) -> Body
) {
self.content = content
self.elements = elements
self._selection = selection
}
/// Update a view storage.
/// - Parameter storage: The view storage.
public func update(_ storage: ViewStorage) {
if let box = storage.view as? ListBox {
updateSelection(box: box)
}
}
/// Get a view storage.
/// - Returns: The view storage.
public func container() -> ViewStorage {
let box: ListBox = .init()
var content: [ViewStorage] = []
for element in elements {
let widget = self.content(element).widget().container()
_ = box.append(widget.view)
content.append(widget)
}
_ = box.handler {
let selection = box.getSelectedRow()
if let id = elements[safe: selection]?.id {
self.selection = id
}
}
updateSelection(box: box)
return .init(box, content: [.mainContent: content])
}
/// Update the list's selection.
/// - Parameter box: The list box.
func updateSelection(box: ListBox) {
if let index = elements.firstIndex(where: { $0.id == selection }) {
box.selectRow(at: index)
}
}
/// Add the "navigation-sidebar" style class.
public func sidebarStyle() -> View {
style("navigation-sidebar")
}
}

View File

@ -63,6 +63,20 @@ extension View {
inspect { _ = $0?.vexpand() } inspect { _ = $0?.vexpand() }
} }
/// Set the horizontal alignment.
/// - Parameter align: The alignment.
/// - Returns: A view.
public func halign(_ align: Alignment) -> View {
inspect { _ = $0?.halign(align) }
}
/// Set the vertical alignment.
/// - Parameter align: The alignment.
/// - Returns: A view.
public func valign(_ align: Alignment) -> View {
inspect { _ = $0?.valign(align) }
}
/// Set the view's minimal width or height. /// Set the view's minimal width or height.
/// - Parameters: /// - Parameters:
/// - minWidth: The minimal width. /// - minWidth: The minimal width.
@ -86,4 +100,25 @@ extension View {
inspect { $0?.fields[.transition] = transition } inspect { $0?.fields[.transition] = transition }
} }
/// Set the view's navigation title.
/// - Parameter label: The navigation title.
/// - Returns: A view.
public func navigationTitle(_ label: String) -> View {
inspect { $0?.fields[.navigationLabel] = label }
}
/// Add a style class to the view.
/// - Parameter style: The style class.
/// - Returns: A view.
public func style(_ style: String) -> View {
inspect { _ = $0?.addStyle(style) }
}
/// Run a function when the view appears for the first time.
/// - Parameter closure: The function.
/// - Returns: A view.
public func onAppear(_ closure: @escaping () -> Void) -> View {
inspect { _ in closure() }
}
} }

View File

@ -0,0 +1,90 @@
//
// ToolbarView.swift
// Adwaita
//
// Created by david-swift on 24.09.23.
//
import GTUI
/// A toolbar view widget.
struct ToolbarView: Widget {
/// The sidebar's content.
var content: View
/// The toolbars.
var toolbar: () -> Body
/// Whether the toolbars are bottom toolbars.
var bottom: Bool
/// Whether the toolbar is visible.
var visible: Bool
/// The identifier of the toolbar content.
let toolbarID = "toolbar"
/// Get the container of the toolbar view widget.
/// - Returns: The view storage.
func container() -> ViewStorage {
let content = content.storage()
let view = GTUI.ToolbarView(content.view)
var toolbarContent: [ViewStorage] = []
for item in toolbar() {
let storage = item.storage()
toolbarContent.append(storage)
if bottom {
_ = view.addBottomBar(storage.view)
} else {
_ = view.addTopBar(storage.view)
}
}
if bottom {
view.setRevealBottomBar(visible)
} else {
view.setRevealTopBar(visible)
}
return .init(view, content: [.mainContent: [content], toolbarID: toolbarContent])
}
/// Update the view storage of the toolbar view widget.
/// - Parameter storage: The view storage.
func update(_ storage: ViewStorage) {
if let mainContent = storage.content[.mainContent]?.first {
content.widget().update(mainContent)
}
if let toolbar = storage.content[toolbarID] {
for (index, content) in toolbar.enumerated() {
self.toolbar()[safe: index]?.updateStorage(content)
}
}
if let view = storage.view as? GTUI.ToolbarView {
if bottom {
view.setRevealBottomBar(visible)
} else {
view.setRevealTopBar(visible)
}
}
}
}
extension View {
/// Add a top toolbar to the view.
/// - Parameters:
/// - toolbar: The toolbar's content.
/// - visible: Whether the toolbar is visible.
/// - Returns: A view.
public func topToolbar(visible: Bool = true, @ViewBuilder _ toolbar: @escaping () -> Body) -> View {
ToolbarView(content: self, toolbar: toolbar, bottom: false, visible: visible)
}
/// Add a bottom toolbar to the view.
/// - Parameters:
/// - toolbar: The toolbar's content.
/// - visible: Whether the toolbar is visible.
/// - Returns: A view.
public func bottomToolbar(visible: Bool = true, @ViewBuilder _ toolbar: @escaping () -> Body) -> View {
ToolbarView(content: self, toolbar: toolbar, bottom: true, visible: visible)
}
}

View File

@ -0,0 +1,62 @@
//
// NavigationSplitView.swift
// Adwaita
//
// Created by david-swift on 24.09.23.
//
import GTUI
/// A navigation split view widget.
public struct NavigationSplitView: Widget {
/// The sidebar's content.
var sidebar: () -> Body
/// The split view's main content.
var content: () -> Body
/// The sidebar content's id.
let sidebarID = "sidebar"
/// The main content's id.
let contentID = "content"
/// Initialize a navigation split view.
/// - Parameters:
/// - sidebar: The sidebar content.
/// - content: The main content.
public init(@ViewBuilder sidebar: @escaping () -> Body, @ViewBuilder content: @escaping () -> Body) {
self.sidebar = sidebar
self.content = content
}
/// Get the container of the navigation split view widget.
/// - Returns: The view storage.
public func container() -> ViewStorage {
let splitView: GTUI.NavigationSplitView = .init()
var content: [String: [ViewStorage]] = [:]
let sidebar = sidebar().widget().container()
let label = sidebar.view.fields[.navigationLabel] as? String ?? ""
_ = splitView.sidebar(sidebar.view, title: label)
content[sidebarID] = [sidebar]
let mainContent = self.content().widget().container()
let mainLabel = mainContent.view.fields[.navigationLabel] as? String ?? ""
_ = splitView.content(mainContent.view, title: mainLabel)
content[contentID] = [mainContent]
return .init(splitView, content: content)
}
/// Update the view storage of the navigation split view widget.
/// - Parameter storage: The view storage.
public func update(_ storage: ViewStorage) {
if let storage = storage.content[contentID]?[safe: 0] {
content().widget().update(storage)
}
if let storage = storage.content[sidebarID]?[safe: 0] {
sidebar().widget().update(storage)
}
}
}

View File

@ -0,0 +1,37 @@
//
// ScrollView.swift
// Adwaita
//
// Created by david-swift on 26.09.23.
//
import GTUI
/// A GtkScrolledWindow equivalent.
public struct ScrollView: Widget {
/// The content.
var content: () -> Body
/// Initialize a `ScrollView`.
/// - Parameter content: The view content.
public init(@ViewBuilder content: @escaping () -> Body) {
self.content = content
}
/// Update a view storage.
/// - Parameter storage: The view storage.
public func update(_ storage: ViewStorage) {
if let first = storage.content[.mainContent]?.first {
content().widget().update(first)
}
}
/// Get a view storage.
/// - Returns: The view storage.
public func container() -> ViewStorage {
let container = content().widget().container()
return .init(Scrolled().setChild(container.view), content: [.mainContent: [container]])
}
}

View File

@ -0,0 +1,53 @@
//
// StateWrapper.swift
// Adwaita
//
// Created by david-swift on 26.09.23.
//
import GTUI
/// A storage for `@State` properties.
public struct StateWrapper: Widget {
/// The content.
var content: () -> Body
/// The state information (from properties with the `State` wrapper).
var state: [String: StateProtocol] = [:]
/// Initialize a `StateWrapper`.
/// - Parameter content: The view content.
public init(@ViewBuilder content: @escaping () -> Body) {
self.content = content
}
/// Initialize a `StateWrapper`.
/// - Parameters:
/// - content: The view content.
/// - state: The state information.
init(content: @escaping () -> Body, state: [String: StateProtocol]) {
self.content = content
self.state = state
}
/// Update a view storage.
/// - Parameter storage: The view storage.
public func update(_ storage: ViewStorage) {
for property in state {
if let value = storage.state[property.key]?.value {
property.value.value = value
}
}
if let storage = storage.content[.mainContent]?.first {
content().widget().update(storage)
}
}
/// Get a view storage.
/// - Returns: The view storage.
public func container() -> ViewStorage {
let content = content().widget().container()
return .init(content.view, content: [.mainContent: [content]], state: state)
}
}

View File

@ -0,0 +1,56 @@
//
// StatusPage.swift
// Adwaita
//
// Created by david-swift on 25.09.23.
//
import GTUI
/// A status page widget.
public struct StatusPage: Widget {
/// The title.
var title: String
/// The description.
var description: String
/// The icon.
var icon: Icon
/// Additional content.
var content: Body
/// Initialize a status page widget.
/// - Parameters:
/// - title: The title.
/// - icon: The icon.
/// - description: Additional details.
/// - content: Additional content.
public init(_ title: String, icon: Icon, description: String = "", @ViewBuilder content: () -> Body = { [] }) {
self.title = title
self.description = description
self.icon = icon
self.content = content()
}
/// Update the view storage of the text widget.
/// - Parameter storage: The view storage.
public func update(_ storage: ViewStorage) {
if let statusPage = storage.view as? GTUI.StatusPage {
_ = statusPage.title(title).description(description).icon(icon)
}
if let storage = storage.content[.mainContent]?.first {
content.widget().update(storage)
}
}
/// Get the container of the text widget.
/// - Returns: The view storage.
public func container() -> ViewStorage {
let child = content.widget().container()
return .init(
GTUI.StatusPage().title(title).description(description).icon(icon).child(child.view),
content: [.mainContent: [child]]
)
}
}

View File

@ -1,5 +1,5 @@
// //
// HeaderBar.swift // Text.swift
// Adwaita // Adwaita
// //
// Created by david-swift on 23.08.23. // Created by david-swift on 23.08.23.

View File

@ -1,5 +1,5 @@
// //
// EitherView.swift // VStack.swift
// Adwaita // Adwaita
// //
// Created by david-swift on 23.08.23. // Created by david-swift on 23.08.23.
@ -12,8 +12,6 @@ public struct VStack: Widget {
/// The content. /// The content.
var content: () -> Body var content: () -> Body
/// The state information (from properties with the `State` wrapper).
var state: [String: StateProtocol] = [:]
/// Initialize a `VStack`. /// Initialize a `VStack`.
/// - Parameter content: The view content. /// - Parameter content: The view content.
@ -21,23 +19,9 @@ public struct VStack: Widget {
self.content = content self.content = content
} }
/// Initialize a `VStack`.
/// - Parameters:
/// - content: The view content.
/// - state: The state information.
init(content: @escaping () -> Body, state: [String: StateProtocol]) {
self.content = content
self.state = state
}
/// Update a view storage. /// Update a view storage.
/// - Parameter storage: The view storage. /// - Parameter storage: The view storage.
public func update(_ storage: ViewStorage) { public func update(_ storage: ViewStorage) {
for property in state {
if let value = storage.state[property.key]?.value {
property.value.value = value
}
}
content().update(storage.content[.mainContent] ?? []) content().update(storage.content[.mainContent] ?? [])
} }
@ -51,7 +35,7 @@ public struct VStack: Widget {
_ = box.append(widget.view) _ = box.append(widget.view)
content.append(widget) content.append(widget)
} }
return .init(box, content: [.mainContent: content], state: state) return .init(box, content: [.mainContent: content])
} }
} }

37
Tests/CounterDemo.swift Normal file
View File

@ -0,0 +1,37 @@
//
// CounterDemo.swift
// Adwaita
//
// Created by david-swift on 25.09.23.
//
// swiftlint:disable missing_docs
import Adwaita
struct CounterDemo: View {
@State private var count = 0
var view: Body {
description
.topToolbar {
HeaderBar.start {
Button(icon: .default(icon: .goPrevious)) {
count -= 1
}
Button(icon: .default(icon: .goNext)) {
count += 1
}
}
}
}
@ViewBuilder private var description: Body {
Text("\(count)")
.style("title-1")
}
}
// swiftlint:enable missing_docs

68
Tests/Demo.swift Normal file
View File

@ -0,0 +1,68 @@
//
// Demo.swift
// Adwaita
//
// Created by david-swift on 25.09.23.
//
// swiftlint:disable missing_docs implicitly_unwrapped_optional no_magic_numbers
import Adwaita
import GTUI
@main
struct Demo: App {
let id = "io.github.david-swift.Demo"
var app: GTUIApp!
@State private var toolbar = false
var scene: Scene {
Window(id: "main") { window in
DemoContent(window: window, app: app)
}
Window(id: "content", open: 0) { window in
Text("This window exists at most once.")
.padding()
.topToolbar {
HeaderBar.empty()
}
.onAppear {
window.setDefaultSize(width: 400, height: 250)
}
}
}
struct DemoContent: View {
@State private var selection: Page = .welcome
var window: GTUI.Window
var app: GTUIApp!
var view: Body {
NavigationSplitView {
ScrollView {
List(Page.allCases, selection: $selection) { element in
Text(element.label)
.halign(.start)
.padding()
}
.sidebarStyle()
}
.topToolbar {
HeaderBar.empty()
}
.navigationTitle("Demo")
} content: {
selection.view(app: app)
}
.onAppear {
window.setDefaultSize(width: 650, height: 450)
}
}
}
}
// swiftlint:enable missing_docs implicitly_unwrapped_optional no_magic_numbers

40
Tests/Page.swift Normal file
View File

@ -0,0 +1,40 @@
//
// Page.swift
// Adwaita
//
// Created by david-swift on 25.09.23.
//
// swiftlint:disable missing_docs implicitly_unwrapped_optional
import Adwaita
enum Page: String, Identifiable, CaseIterable {
case welcome
case counter
case windows
var id: Self {
self
}
var label: String {
rawValue.capitalized
}
@ViewBuilder
func view(app: GTUIApp!) -> Body {
switch self {
case .welcome:
WelcomeDemo()
case .counter:
CounterDemo()
case .windows:
WindowsDemo(app: app)
}
}
}
// swiftlint:enable missing_docs implicitly_unwrapped_optional

29
Tests/WelcomeDemo.swift Normal file
View File

@ -0,0 +1,29 @@
//
// WelcomeDemo.swift
// Adwaita
//
// Created by david-swift on 25.09.23.
//
// swiftlint:disable missing_docs
import Adwaita
struct WelcomeDemo: View {
@State private var test = false
var view: Body {
StatusPage(
"Swift Adwaita Demo",
icon: .default(icon: .gnomeAdwaita1Demo),
description: "This is a collection of examples for the Swift Adwaita package."
)
.topToolbar {
HeaderBar.empty()
}
}
}
// swiftlint:enable missing_docs

34
Tests/WindowsDemo.swift Normal file
View File

@ -0,0 +1,34 @@
//
// WindowsDemo.swift
// Adwaita
//
// Created by david-swift on 25.09.23.
//
// swiftlint:disable missing_docs implicitly_unwrapped_optional no_magic_numbers
import Adwaita
struct WindowsDemo: View {
var app: GTUIApp!
var view: Body {
VStack {
Button("Show Window") {
app.showWindow("content")
}
.padding()
Button("Add Window") {
app.addWindow("main")
}
.padding(10, .horizontal.add(.bottom))
}
.topToolbar {
HeaderBar.empty()
}
}
}
// swiftlint:enable missing_docs implicitly_unwrapped_optional no_magic_numbers

View File

@ -1,72 +0,0 @@
//
// main.swift
// Adwaita
//
// Created by david-swift on 05.08.23.
//
// swiftlint:disable missing_docs implicitly_unwrapped_optional no_magic_numbers
import Adwaita
@main
struct Counter: App {
let id = "io.github.david-swift.Counter"
var app: GTUIApp!
var scene: Scene {
Window(id: "toggle") { _ in
Button("Add Window") {
app.addWindow("content-view")
}
.padding()
Button("Show Window") {
app.showWindow("content-view")
}
.padding(10, .horizontal.add(.bottom))
}
Window(id: "content-view", open: 0) { _ in
ContentView()
}
}
}
struct ContentView: View {
@State private var count = 0
var view: Body {
HeaderBar.start {
Button(icon: .default(icon: .goPrevious)) {
count -= 1
}
Button(icon: .default(icon: .goNext)) {
count += 1
}
}
description
}
@ViewBuilder private var description: Body {
VStack {
switch count {
case 1:
Text("One")
.transition(.slideUp)
case 0:
Text("Zero")
default:
Text("Hello, world, \(count)!")
}
}
.padding(50)
.onUpdate {
print(count)
}
}
}
// swiftlint:enable missing_docs implicitly_unwrapped_optional no_magic_numbers

View File

@ -0,0 +1,64 @@
# Creating Widgets
Widgets are special views that do not provide a collection of other views as a content,
but have functions that are called when creating or updating the view.
Normally, a widget manages a GTK or Libadwaita widget using [SwiftGui][1].
## Recreate the `Text` widget
In this tutorial, we will recreate the text widget.
A widget conforms to the `Widget` protocol:
```swift
struct CustomText: Widget { }
```
You can add properties to the widget:
```swift
struct CustomText: Widget {
var text: String
}
```
This widget can be called in a view body using `CustomText(text: "Hello, world!")`.
Now, add the two functions required by the protocol:
```swift
struct CustomText: Widget {
var text: String
public func container() -> ViewStorage { }
public func update(_ storage: ViewStorage) { }
}
```
## The `container()` Function
This function initializes the widget when the widget appears for the first time.
It expects a `ViewStorage` as the return type.
In our case, this function is very simple:
```swift
func container() -> ViewStorage {
.init(MarkupLabel(self.text))
}
```
`MarkupLabel` is defined in [SwiftGui][1].
## The `update(_:)` Function
Whenever a state of the app changes, the `update(_:)` function of the widget gets called.
You get the view storage that you have previously initialized as a parameter.
Update the storage to reflect the current state of the widget:
```swift
func update(_ storage: ViewStorage) {
if let label = storage.view as? MarkupLabel {
label.setText(text)
}
}
```
## Containers
Some widgets act as containers that accept other widgets as children.
In that case, use the `ViewStorage`'s `content` property for storing their view storages.
In the `update(_:)` function, update the children's storages.
An example showcasing how to implement containers is the [VStack][2].
[1]: https://github.com/JCWasmx86/SwiftGui
[2]: ../../Sources/Adwaita/View/VStack.swift