diff --git a/.swiftlint.yml b/.swiftlint.yml index bd7da2d..12e02e3 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -118,14 +118,6 @@ custom_rules: message: 'Spaces should be used instead of tabs.' 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 # "empty_first_line" # https://github.com/coteditor/CotEditor/blob/main/.swiftlint.yml diff --git a/Documentation/Reference/README.md b/Documentation/Reference/README.md index eb9fb5f..ace6cf4 100644 --- a/Documentation/Reference/README.md +++ b/Documentation/Reference/README.md @@ -14,10 +14,17 @@ - [Binding](structs/Binding.md) - [Button](structs/Button.md) - [EitherView](structs/EitherView.md) +- [HStack](structs/HStack.md) - [HeaderBar](structs/HeaderBar.md) - [InspectorWrapper](structs/InspectorWrapper.md) +- [List](structs/List.md) +- [NavigationSplitView](structs/NavigationSplitView.md) +- [ScrollView](structs/ScrollView.md) - [State](structs/State.md) +- [StateWrapper](structs/StateWrapper.md) +- [StatusPage](structs/StatusPage.md) - [Text](structs/Text.md) +- [ToolbarView](structs/ToolbarView.md) - [UpdateObserver](structs/UpdateObserver.md) - [VStack](structs/VStack.md) - [Window](structs/Window.md) diff --git a/Documentation/Reference/classes/ViewStorage.md b/Documentation/Reference/classes/ViewStorage.md index 11d060f..3e0cdea 100644 --- a/Documentation/Reference/classes/ViewStorage.md +++ b/Documentation/Reference/classes/ViewStorage.md @@ -15,7 +15,7 @@ The view's content. ### `state` -The view's state (used in `VStack`). +The view's state (used in `StateWrapper`). ## Methods ### `init(_:content:state:)` diff --git a/Documentation/Reference/extensions/Array.md b/Documentation/Reference/extensions/Array.md index b530cdf..be7bef5 100644 --- a/Documentation/Reference/extensions/Array.md +++ b/Documentation/Reference/extensions/Array.md @@ -2,6 +2,11 @@ # `Array` +## Properties +### `view` + +The array's view body is the array itself. + ## Methods ### `widget()` diff --git a/Documentation/Reference/extensions/String.md b/Documentation/Reference/extensions/String.md index 05a18a0..2705ad9 100644 --- a/Documentation/Reference/extensions/String.md +++ b/Documentation/Reference/extensions/String.md @@ -10,3 +10,7 @@ A label for main content in a view storage. ### `transition` 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. diff --git a/Documentation/Reference/extensions/View.md b/Documentation/Reference/extensions/View.md index 4b3564b..ffdce33 100644 --- a/Documentation/Reference/extensions/View.md +++ b/Documentation/Reference/extensions/View.md @@ -44,6 +44,18 @@ Enable or disable the vertical expansion. - Parameter enabled: Whether it is enabled or disabled. - 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:)` Set the view's minimal width or height. @@ -64,6 +76,40 @@ Set the view's transition. - Parameter transition: The transition. - 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(_:)` Run a function when the view gets an update. diff --git a/Documentation/Reference/structs/HStack.md b/Documentation/Reference/structs/HStack.md new file mode 100644 index 0000000..dfa5f93 --- /dev/null +++ b/Documentation/Reference/structs/HStack.md @@ -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. diff --git a/Documentation/Reference/structs/List.md b/Documentation/Reference/structs/List.md new file mode 100644 index 0000000..9cf821e --- /dev/null +++ b/Documentation/Reference/structs/List.md @@ -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. diff --git a/Documentation/Reference/structs/NavigationSplitView.md b/Documentation/Reference/structs/NavigationSplitView.md new file mode 100644 index 0000000..a58e25c --- /dev/null +++ b/Documentation/Reference/structs/NavigationSplitView.md @@ -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. diff --git a/Documentation/Reference/structs/ScrollView.md b/Documentation/Reference/structs/ScrollView.md new file mode 100644 index 0000000..2d978ab --- /dev/null +++ b/Documentation/Reference/structs/ScrollView.md @@ -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. diff --git a/Documentation/Reference/structs/StateWrapper.md b/Documentation/Reference/structs/StateWrapper.md new file mode 100644 index 0000000..4346fb8 --- /dev/null +++ b/Documentation/Reference/structs/StateWrapper.md @@ -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. diff --git a/Documentation/Reference/structs/StatusPage.md b/Documentation/Reference/structs/StatusPage.md new file mode 100644 index 0000000..5ba82f0 --- /dev/null +++ b/Documentation/Reference/structs/StatusPage.md @@ -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. diff --git a/Documentation/Reference/structs/ToolbarView.md b/Documentation/Reference/structs/ToolbarView.md new file mode 100644 index 0000000..4268a4e --- /dev/null +++ b/Documentation/Reference/structs/ToolbarView.md @@ -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. diff --git a/Documentation/Reference/structs/VStack.md b/Documentation/Reference/structs/VStack.md index da6a23e..89a7b65 100644 --- a/Documentation/Reference/structs/VStack.md +++ b/Documentation/Reference/structs/VStack.md @@ -9,23 +9,12 @@ A GtkBox equivalent. The content. -### `state` - -The state information (from properties with the `State` wrapper). - ## Methods ### `init(content:)` Initialize a `VStack`. - Parameter content: The view content. -### `init(content:state:)` - -Initialize a `VStack`. -- Parameters: - - content: The view content. - - state: The state information. - ### `update(_:)` Update a view storage. diff --git a/Icons/Demo.png b/Icons/Demo.png new file mode 100644 index 0000000..96f06e9 Binary files /dev/null and b/Icons/Demo.png differ diff --git a/Icons/Screenshot.png b/Icons/Screenshot.png index 728711b..2dc277d 100644 Binary files a/Icons/Screenshot.png and b/Icons/Screenshot.png differ diff --git a/Package.swift b/Package.swift index fa41d03..d59034f 100644 --- a/Package.swift +++ b/Package.swift @@ -26,7 +26,7 @@ let package = Package( dependencies: [.product(name: "GTUI", package: "swiftgui")] ), .executableTarget( - name: "Counter", + name: "Swift Adwaita Demo", dependencies: ["Adwaita"], path: "Tests" ) diff --git a/README.md b/README.md index bc6911c..fcaca08 100644 --- a/README.md +++ b/README.md @@ -27,16 +27,19 @@ struct Example: 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 - } - } Text("\(count)") + .style("title-1") .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] +More examples are available in the [Demo app][1]: + +![Demo App][image-2] + ## Table of Contents -- [Goals][1] -- [Widgets][2] -- [Installation][3] -- [Usage][4] -- [Thanks][5] +- [Goals][2] +- [Widgets][3] +- [Installation][4] +- [Usage][5] +- [Thanks][6] ## 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 -| Name | Description | Widget | -| ---------- | ----------------------------------------------------------------- | ------------ | -| 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 | -| HeaderBar | A widget for creating custom title bars for windows. | GtkHeaderBar | -| Text | A widget for displaying a small amount of text. | GtkLabel | -| VStack | A widget which arranges child widgets into a single column. | GtkBox | +| Name | Description | Widget | +| -------------------- | ----------------------------------------------------------------- | ---------------------- | +| 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 | +| HeaderBar | A widget for creating custom title bars for windows. | GtkHeaderBar | +| Text | A widget for displaying a small amount of text. | GtkLabel | +| 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 | Syntax | Description | | ---------------------------- | -------------------------------------------------------------------------------------- | -| `inspect(_:)` | Edit the underlying [GTUI][9] widget. | +| `inspect(_:)` | Edit the underlying [GTUI][10] widget. | | `padding(_:_:)` | Add empty space around a view. | | `hexpand(_:)` | Enable or disable the horizontal 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 view’s minimal width or height. | | `frame(maxSize:)` | Set the view’s 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. | +| `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 ### 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. On macOS, follow these steps: -1. Install [Homebrew][10]. +1. Install [Homebrew][11]. 2. Install Libadwaita (and thereby GTK 4): ``` brew install libadwaita @@ -104,45 +124,52 @@ brew install libadwaita ## Usage -* [Getting Started][11] +* [Getting Started][12] ### Basics -* [Hello World][12] -* [Creating Views][13] -* [Windows][14] +* [Hello World][13] +* [Creating Views][14] +* [Windows][15] + +### Advanced + +* [Creating Widgets][16] ## Thanks ### Dependencies -- [SwiftGui][15] licensed under the [GPL-3.0 license][16] +- [SwiftGui][17] licensed under the [GPL-3.0 license][18] ### Other Thanks -- The [contributors][17] -- [SwiftLint][18] for checking whether code style conventions are violated -- The programming language [Swift][19] -- [SourceDocs][20] used for generating the [docs][21] - -[1]: #goals -[2]: #widgets -[3]: #installation -[4]: #usage -[5]: #thanks -[6]: https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/index.html -[7]: https://docs.gtk.org/gtk4/ -[8]: https://github.com/david-swift/Adwaita/issues -[9]: https://github.com/JCWasmx86/SwiftGui -[10]: https://brew.sh -[11]: user-manual/GettingStarted.md -[12]: user-manual/Basics/HelloWorld.md -[13]: user-manual/Basics/CreatingViews.md -[14]: user-manual/Basics/Windows.md -[15]: https://github.com/JCWasmx86/SwiftGui -[16]: https://github.com/JCWasmx86/SwiftGui/blob/main/COPYING -[17]: Contributors.md -[18]: https://github.com/realm/SwiftLint -[19]: https://github.com/apple/swift -[20]: https://github.com/SourceDocs/SourceDocs -[21]: Documentation/Reference/README.md +- The [contributors][19] +- [SwiftLint][20] for checking whether code style conventions are violated +- The programming language [Swift][21] +- [SourceDocs][22] used for generating the [docs][23] + +[1]: Tests/ +[2]: #goals +[3]: #widgets +[4]: #installation +[5]: #usage +[6]: #thanks +[7]: https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/index.html +[8]: https://docs.gtk.org/gtk4/ +[9]: https://github.com/david-swift/Adwaita/issues +[10]: https://github.com/JCWasmx86/SwiftGui +[11]: https://brew.sh +[12]: user-manual/GettingStarted.md +[13]: user-manual/Basics/HelloWorld.md +[14]: user-manual/Basics/CreatingViews.md +[15]: user-manual/Basics/Windows.md +[16]: user-manual/Advanced/CreatingWidgets.md +[17]: https://github.com/JCWasmx86/SwiftGui +[18]: https://github.com/JCWasmx86/SwiftGui/blob/main/COPYING +[19]: Contributors.md +[20]: https://github.com/realm/SwiftLint +[21]: https://github.com/apple/swift +[22]: https://github.com/SourceDocs/SourceDocs +[23]: Documentation/Reference/README.md [image-1]: Icons/Screenshot.png +[image-2]: Icons/Demo.png diff --git a/SUMMARY.md b/SUMMARY.md index 089dd30..60e3751 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -9,8 +9,13 @@ * [Creating Views][4] * [Windows][5] +## Advanced + +* [Creating Widgets][6] + [1]: README.md [2]: user-manual/GettingStarted.md [3]: user-manual/Basics/HelloWorld.md -[4]: user-manual/Basics/CreatingViews.md -[5]: user-manual/Basics/Windows.md +[4]: user-manual/Basics/CreatingViews.md +[5]: user-manual/Basics/Windows.md +[6]: user-manual/Advanced/CreatingWidgets.md diff --git a/Sources/Adwaita/Model/Extensions/Array.swift b/Sources/Adwaita/Model/Extensions/Array.swift index 912a4e0..05e08cf 100644 --- a/Sources/Adwaita/Model/Extensions/Array.swift +++ b/Sources/Adwaita/Model/Extensions/Array.swift @@ -7,7 +7,10 @@ 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. /// - Returns: A widget. diff --git a/Sources/Adwaita/Model/Extensions/String.swift b/Sources/Adwaita/Model/Extensions/String.swift index 5fa1911..d922699 100644 --- a/Sources/Adwaita/Model/Extensions/String.swift +++ b/Sources/Adwaita/Model/Extensions/String.swift @@ -11,5 +11,7 @@ extension String { static var mainContent: Self { "main" } /// A label for the transition data in a GTUI widget's fields. static var transition: Self { "transition" } + /// A label for the navigation label in a GTUI widget's fields. + static var navigationLabel: Self { "navigation-label" } } diff --git a/Sources/Adwaita/Model/User Interface/View.swift b/Sources/Adwaita/Model/User Interface/View.swift index c6b20c7..a9ba7aa 100644 --- a/Sources/Adwaita/Model/User Interface/View.swift +++ b/Sources/Adwaita/Model/User Interface/View.swift @@ -41,7 +41,7 @@ extension View { 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) { if let widget = self as? Widget { widget.update(storage) + } else { + StateWrapper { self }.update(storage) } } diff --git a/Sources/Adwaita/Model/User Interface/ViewStorage.swift b/Sources/Adwaita/Model/User Interface/ViewStorage.swift index 1033858..9ccc345 100644 --- a/Sources/Adwaita/Model/User Interface/ViewStorage.swift +++ b/Sources/Adwaita/Model/User Interface/ViewStorage.swift @@ -14,7 +14,7 @@ public class ViewStorage { public var view: NativeWidgetPeer /// The view's content. public var content: [String: [ViewStorage]] - /// The view's state (used in `VStack`). + /// The view's state (used in `StateWrapper`). public var state: [String: StateProtocol] /// Initialize a view storage. diff --git a/Sources/Adwaita/View/HStack.swift b/Sources/Adwaita/View/HStack.swift new file mode 100644 index 0000000..9b461cf --- /dev/null +++ b/Sources/Adwaita/View/HStack.swift @@ -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]) + } + +} diff --git a/Sources/Adwaita/View/List.swift b/Sources/Adwaita/View/List.swift new file mode 100644 index 0000000..1f4917d --- /dev/null +++ b/Sources/Adwaita/View/List.swift @@ -0,0 +1,76 @@ +// +// List.swift +// Adwaita +// +// Created by david-swift on 25.09.23. +// + +import GTUI + +/// A list box widget. +public struct List: 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, + @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") + } + +} diff --git a/Sources/Adwaita/View/Modifiers/InspectorWrapper.swift b/Sources/Adwaita/View/Modifiers/InspectorWrapper.swift index ed0d5cb..0e65c87 100644 --- a/Sources/Adwaita/View/Modifiers/InspectorWrapper.swift +++ b/Sources/Adwaita/View/Modifiers/InspectorWrapper.swift @@ -63,6 +63,20 @@ extension View { 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. /// - Parameters: /// - minWidth: The minimal width. @@ -86,4 +100,25 @@ extension View { 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() } + } + } diff --git a/Sources/Adwaita/View/Modifiers/ToolbarView.swift b/Sources/Adwaita/View/Modifiers/ToolbarView.swift new file mode 100644 index 0000000..6efa8dc --- /dev/null +++ b/Sources/Adwaita/View/Modifiers/ToolbarView.swift @@ -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) + } + +} diff --git a/Sources/Adwaita/View/NavigationSplitView.swift b/Sources/Adwaita/View/NavigationSplitView.swift new file mode 100644 index 0000000..752b1ec --- /dev/null +++ b/Sources/Adwaita/View/NavigationSplitView.swift @@ -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) + } + } + +} diff --git a/Sources/Adwaita/View/ScrollView.swift b/Sources/Adwaita/View/ScrollView.swift new file mode 100644 index 0000000..bd44b4c --- /dev/null +++ b/Sources/Adwaita/View/ScrollView.swift @@ -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]]) + } + +} diff --git a/Sources/Adwaita/View/StateWrapper.swift b/Sources/Adwaita/View/StateWrapper.swift new file mode 100644 index 0000000..e9d94bf --- /dev/null +++ b/Sources/Adwaita/View/StateWrapper.swift @@ -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) + } + +} diff --git a/Sources/Adwaita/View/StatusPage.swift b/Sources/Adwaita/View/StatusPage.swift new file mode 100644 index 0000000..ffd0734 --- /dev/null +++ b/Sources/Adwaita/View/StatusPage.swift @@ -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]] + ) + } + +} diff --git a/Sources/Adwaita/View/Text.swift b/Sources/Adwaita/View/Text.swift index aa7831f..08ae833 100644 --- a/Sources/Adwaita/View/Text.swift +++ b/Sources/Adwaita/View/Text.swift @@ -1,5 +1,5 @@ // -// HeaderBar.swift +// Text.swift // Adwaita // // Created by david-swift on 23.08.23. diff --git a/Sources/Adwaita/View/VStack.swift b/Sources/Adwaita/View/VStack.swift index 3dc12da..7807b7b 100644 --- a/Sources/Adwaita/View/VStack.swift +++ b/Sources/Adwaita/View/VStack.swift @@ -1,5 +1,5 @@ // -// EitherView.swift +// VStack.swift // Adwaita // // Created by david-swift on 23.08.23. @@ -12,8 +12,6 @@ public struct VStack: Widget { /// The content. var content: () -> Body - /// The state information (from properties with the `State` wrapper). - var state: [String: StateProtocol] = [:] /// Initialize a `VStack`. /// - Parameter content: The view content. @@ -21,23 +19,9 @@ public struct VStack: Widget { 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. /// - 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 - } - } content().update(storage.content[.mainContent] ?? []) } @@ -51,7 +35,7 @@ public struct VStack: Widget { _ = box.append(widget.view) content.append(widget) } - return .init(box, content: [.mainContent: content], state: state) + return .init(box, content: [.mainContent: content]) } } diff --git a/Tests/CounterDemo.swift b/Tests/CounterDemo.swift new file mode 100644 index 0000000..7416496 --- /dev/null +++ b/Tests/CounterDemo.swift @@ -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 diff --git a/Tests/Demo.swift b/Tests/Demo.swift new file mode 100644 index 0000000..51bf81e --- /dev/null +++ b/Tests/Demo.swift @@ -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 diff --git a/Tests/Page.swift b/Tests/Page.swift new file mode 100644 index 0000000..3018381 --- /dev/null +++ b/Tests/Page.swift @@ -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 diff --git a/Tests/WelcomeDemo.swift b/Tests/WelcomeDemo.swift new file mode 100644 index 0000000..420f294 --- /dev/null +++ b/Tests/WelcomeDemo.swift @@ -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 diff --git a/Tests/WindowsDemo.swift b/Tests/WindowsDemo.swift new file mode 100644 index 0000000..bb25f41 --- /dev/null +++ b/Tests/WindowsDemo.swift @@ -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 diff --git a/Tests/main.swift b/Tests/main.swift deleted file mode 100644 index b28c77c..0000000 --- a/Tests/main.swift +++ /dev/null @@ -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 diff --git a/user-manual/Advanced/CreatingWidgets.md b/user-manual/Advanced/CreatingWidgets.md new file mode 100644 index 0000000..e96d9a0 --- /dev/null +++ b/user-manual/Advanced/CreatingWidgets.md @@ -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