Improve window management

This commit is contained in:
david-swift 2023-09-24 15:24:59 +02:00
parent 81426e2e0c
commit 2611e0c448
24 changed files with 670 additions and 81 deletions

View File

@ -7,6 +7,7 @@
- [View](protocols/View.md)
- [Widget](protocols/Widget.md)
- [WindowScene](protocols/WindowScene.md)
- [WindowSceneGroup](protocols/WindowSceneGroup.md)
## Structs
@ -19,6 +20,7 @@
- [Text](structs/Text.md)
- [UpdateObserver](structs/UpdateObserver.md)
- [VStack](structs/VStack.md)
- [Window](structs/Window.md)
## Classes
@ -42,6 +44,8 @@
- [String](extensions/String.md)
- [View](extensions/View.md)
- [Widget](extensions/Widget.md)
- [WindowScene](extensions/WindowScene.md)
- [WindowSceneGroup](extensions/WindowSceneGroup.md)
## Typealiases

View File

@ -28,3 +28,15 @@ Initialize the GTUI application.
### `onActivate()`
The entry point of the application.
### `showWindow(_:)`
Focus the window with a certain id. Create the window if it doesn't already exist.
- Parameters:
- id: The window's id.
### `addWindow(_:)`
Add a new window with the content of the window with a certain id.
- Parameters:
- id: The window's id.

View File

@ -5,6 +5,14 @@
A storage for an app's window.
## Properties
### `id`
The window's identifier.
### `destroy`
Whether the reference to the window should disappear in the next update.
### `window`
The GTUI window.
@ -14,9 +22,10 @@ The GTUI window.
The content's storage.
## Methods
### `init(window:view:)`
### `init(id:window:view:)`
Initialize a window storage.
- Parameters:
- id: The window's identifier.
- window: The GTUI window.
- view: The content's storage.

View File

@ -14,6 +14,11 @@ Update a collection of views with a collection of view storages.
- Parameters:
- storage: The collection of view storages.
### `windows()`
Get the content of an array of window scene groups.
- Returns: The array of windows.
### `checkIndex(_:)`
Check if a given index is valid for the array.

View File

@ -2,10 +2,7 @@
# `WindowScene`
## Methods
### `getWindow(app:)`
## Properties
### `body`
Get the `GTUI.Window` with the content.
- Parameters:
- app: The application.
- Returns: The window.
The window scene's body is itself.

View File

@ -0,0 +1,14 @@
**EXTENSION**
# `WindowSceneGroup`
## Methods
### `windows()`
Get the windows described by the group.
- Returns: The windows.
### `update(_:)`
Update the windows described by the group.
- Parameter storage: The window's storage.

View File

@ -2,4 +2,25 @@
# `WindowScene`
A structure conforming to `WindowScene` can be added to an app's `scene`.
A structure representing the content for a certain window type.
## Properties
### `id`
The window type's identifier.
### `open`
The number of instances of the window at the startup.
## Methods
### `createWindow(app:)`
Get the storage for the window.
- Parameter app: The application.
- Returns: The storage.
### `update(_:)`
Update a window storage's content.
- Parameter storage: The storage to update.

View File

@ -0,0 +1,10 @@
**PROTOCOL**
# `WindowSceneGroup`
A structure conforming to `WindowScene` can be added to an app's `scene`.
## Properties
### `body`
The group's content.

View File

@ -0,0 +1,52 @@
**STRUCT**
# `Window`
A structure representing a simple window type.
Note that multiple instances of a window can be opened at the same time.
## Properties
### `id`
The window's identifier.
### `content`
The window's content.
### `open`
Whether an instance of the window type should be opened when the app is starting up.
## Methods
### `init(id:open:content:)`
Create a window type with a certain identifier and user interface.
- Parameters:
- id: The identifier.
- open: The number of instances of the window type when the app is starting.
- content: The window's content.
### `createWindow(app:)`
Get the storage for the window.
- Parameter app: The application.
- Returns: The storage.
### `createGTUIWindow(app:)`
Get the window.
- Parameter app: The application.
- Returns: The window.
### `getViewStorage(window:)`
Get the storage of the content view.
- Parameter window: The window.
- Returns: The storage of the content of the window.
### `update(_:)`
Update a window storage's content.
- Parameter storage: The storage to update.

BIN
Icons/HelloWorld.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
Icons/TwoWindows.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -108,18 +108,20 @@ brew install libadwaita
### Basics
* [Creating Views][12]
* [Hello World][12]
* [Creating Views][13]
* [Windows][14]
## Thanks
### Dependencies
- [SwiftGui][13] licensed under the [GPL-3.0 license][14]
- [SwiftGui][15] licensed under the [GPL-3.0 license][16]
### Other Thanks
- The [contributors][15]
- [SwiftLint][16] for checking whether code style conventions are violated
- The programming language [Swift][17]
- [SourceDocs][18] used for generating the [docs][19]
- 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
@ -132,13 +134,15 @@ brew install libadwaita
[9]: https://github.com/JCWasmx86/SwiftGui
[10]: https://brew.sh
[11]: user-manual/GettingStarted.md
[12]: user-manual/Basics/CreatingViews.md
[13]: https://github.com/JCWasmx86/SwiftGui
[14]: https://github.com/JCWasmx86/SwiftGui/blob/main/COPYING
[15]: Contributors.md
[16]: https://github.com/realm/SwiftLint
[17]: https://github.com/apple/swift
[18]: https://github.com/SourceDocs/SourceDocs
[19]: Documentation/Reference/README.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
[image-1]: Icons/Screenshot.png

View File

@ -5,8 +5,12 @@
## Basics
* [Creating Views][3]
* [Hello World][3]
* [Creating Views][4]
* [Windows][5]
[1]: README.md
[2]: user-manual/GettingStarted.md
[3]: user-manual/Basics/CreatingViews.md
[3]: user-manual/Basics/HelloWorld.md
[4]: user-manual/Basics/CreatingViews.md
[5]: user-manual/Basics/Windows.md

View File

@ -32,6 +32,16 @@ extension Array where Element == View {
}
extension Array where Element == WindowSceneGroup {
/// Get the content of an array of window scene groups.
/// - Returns: The array of windows.
public func windows() -> [WindowScene] {
flatMap { $0.windows() }
}
}
extension Array {
/// Accesses the element at the specified position safely.

View File

@ -46,11 +46,17 @@ extension App {
var appInstance = self.init()
appInstance.app = GTUIApp(appInstance.id) { appInstance }
GTUIApp.updateHandlers.append {
for (windowIndex, window) in appInstance.scene.enumerated() {
if let stored = appInstance.app.sceneStorage[safe: windowIndex] {
window.widget().updateStorage(stored.view)
var removeIndices: [Int] = []
for (index, window) in appInstance.app.sceneStorage.enumerated() {
if window.destroy {
removeIndices.insert(index, at: 0)
} else if let scene = appInstance.scene.windows().first(where: { $0.id == window.id }) {
scene.update(window)
}
}
for index in removeIndices {
appInstance.app.sceneStorage.remove(at: index)
}
}
appInstance.app.run()
}

View File

@ -30,13 +30,27 @@ public class GTUIApp: Application {
/// The entry point of the application.
override public func onActivate() {
let body = body()
for windowScene in body.scene {
let window = GTUI.Window(app: self)
let child = windowScene.storage()
let view = child.view
window.setChild(view)
sceneStorage.append(.init(window: window, view: child))
window.show()
for windowScene in body.scene.windows() {
for _ in 0..<windowScene.open {
sceneStorage.append(windowScene.createWindow(app: self))
}
}
}
/// Focus the window with a certain id. Create the window if it doesn't already exist.
/// - Parameters:
/// - id: The window's id.
public func showWindow(_ id: String) {
sceneStorage.last { $0.id == id && !$0.destroy }?.window.show() ?? addWindow(id)
}
/// Add a new window with the content of the window with a certain id.
/// - Parameters:
/// - id: The window's id.
public func addWindow(_ id: String) {
if let window = body().scene.windows().first(where: { $0.id == id }) {
sceneStorage.append(window.createWindow(app: self))
showWindow(id)
}
}
}

View File

@ -7,10 +7,26 @@
import GTUI
/// A structure conforming to `WindowScene` can be added to an app's `scene`.
public protocol WindowScene: View { }
/// A structure representing the content for a certain window type.
public protocol WindowScene: WindowSceneGroup {
/// `Scene` is an array of windows.
public typealias Scene = [WindowScene]
/// A builder for the `Scene`
public typealias SceneBuilder = ArrayBuilder<WindowScene>
/// The window type's identifier.
var id: String { get }
/// The number of instances of the window at the startup.
var `open`: Int { get }
/// Get the storage for the window.
/// - Parameter app: The application.
/// - Returns: The storage.
func createWindow(app: GTUIApp) -> WindowStorage
/// Update a window storage's content.
/// - Parameter storage: The storage to update.
func update(_ storage: WindowStorage)
}
extension WindowScene {
/// The window scene's body is itself.
@SceneBuilder public var body: Scene { self }
}

View File

@ -0,0 +1,47 @@
//
// WindowSceneGroup.swift
// Adwaita
//
// Created by david-swift on 14.09.23.
//
/// A structure conforming to `WindowScene` can be added to an app's `scene`.
public protocol WindowSceneGroup {
/// The group's content.
@SceneBuilder var body: Scene { get }
}
extension WindowSceneGroup {
/// Get the windows described by the group.
/// - Returns: The windows.
func windows() -> [WindowScene] {
var content: [WindowScene] = []
for element in body {
if let window = element as? WindowScene {
content.append(window)
} else {
content += element.windows()
}
}
return content
}
/// Update the windows described by the group.
/// - Parameter storage: The window's storage.
func update(_ storage: [WindowStorage]) {
for (index, window) in windows().enumerated() {
if let storage = storage[safe: index] {
window.update(storage)
}
}
}
}
/// `Scene` is an array of windows.
public typealias Scene = [WindowSceneGroup]
/// A builder for the `Scene`
public typealias SceneBuilder = ArrayBuilder<WindowSceneGroup>

View File

@ -8,18 +8,24 @@
import GTUI
/// A storage for an app's window.
class WindowStorage {
public class WindowStorage {
/// The window's identifier.
public var id: String
/// Whether the reference to the window should disappear in the next update.
public var destroy = false
/// The GTUI window.
var window: Window
public var window: GTUI.Window
/// The content's storage.
var view: ViewStorage
public var view: ViewStorage
/// Initialize a window storage.
/// - Parameters:
/// - id: The window's identifier.
/// - window: The GTUI window.
/// - view: The content's storage.
init(window: Window, view: ViewStorage) {
public init(id: String, window: GTUI.Window, view: ViewStorage) {
self.id = id
self.window = window
self.view = view
}

View File

@ -0,0 +1,71 @@
//
// Window.swift
// Adwaita
//
// Created by david-swift on 14.09.23.
//
import GTUI
/// A structure representing a simple window type.
///
/// Note that multiple instances of a window can be opened at the same time.
public struct Window: WindowScene {
/// The window's identifier.
public var id: String
/// The window's content.
var content: (GTUI.Window) -> Body
/// Whether an instance of the window type should be opened when the app is starting up.
public var `open`: Int
/// Create a window type with a certain identifier and user interface.
/// - Parameters:
/// - id: The identifier.
/// - open: The number of instances of the window type when the app is starting.
/// - content: The window's content.
public init(id: String, `open`: Int = 1, @ViewBuilder content: @escaping (GTUI.Window) -> Body) {
self.content = content
self.id = id
self.open = open
}
/// Get the storage for the window.
/// - Parameter app: The application.
/// - Returns: The storage.
public func createWindow(app: GTUIApp) -> WindowStorage {
let window = createGTUIWindow(app: app)
let storage = getViewStorage(window: window)
let windowStorage = WindowStorage(id: id, window: window, view: storage)
window.observeHide {
windowStorage.destroy = true
return false
}
return windowStorage
}
/// Get the window.
/// - Parameter app: The application.
/// - Returns: The window.
func createGTUIWindow(app: GTUIApp) -> GTUI.Window {
let window = GTUI.Window(app: app)
window.show()
return window
}
/// Get the storage of the content view.
/// - Parameter window: The window.
/// - Returns: The storage of the content of the window.
func getViewStorage(window: GTUI.Window) -> ViewStorage {
let storage = content(window).widget().container()
window.setChild(storage.view)
return storage
}
/// Update a window storage's content.
/// - Parameter storage: The storage to update.
public func update(_ storage: WindowStorage) {
content(storage.window).widget().updateStorage(storage.view)
}
}

View File

@ -16,12 +16,24 @@ struct Counter: App {
var app: GTUIApp!
var scene: Scene {
CounterWindow()
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 CounterWindow: WindowScene {
struct ContentView: View {
@State private var count = 0

View File

@ -1,26 +1,13 @@
# Creating Views
This is a beginner tutorial. We will create a simple "Hello, world!" app using _Adwaita_.
Views are the building blocks of your application.
A view can be as simple as the `Text` widget you have seen in the previous tutorial, or as complex as the whole content of a single window.
## Create the Swift Package
1. Open your terminal client and navigate to a directory you want to create the package in (e.g. `~/Documents/`).
2. Create a new folder for the package using `mkdir HelloWorld`.
3. Enter the newly created folder using `cd HelloWorld`.
4. Run `swift package init --type executable` for creating a new Swift package.
5. Open the Swift package. If you are using GNOME Builder, click on `Select a Folder…` in the view that appears after opening Builder and open the `HelloWorld` folder.
## Add the Dependency
1. Open the `Package.swift` file.
2. Add the following line of code after `name: "HelloWorld",`:
```
dependencies: [.package(url: "https://github.com/david-swift/Adwaita", from: "0.1.0")],
```
## Create the App
1. Navigate to `Sources/main.swift`.
2. An app that uses the _Adwaita_ framework has a structure that conforms to the `App` protocol. The `scene` property returns one or more windows which provide content for display. An `@main` attribute marks it as the entry point of the app.
Replace `print("Hello, world!")` by your first app. We will later define `HelloWindow`:
## Add Views to a Window
You've already seen how to add views to a window:
```swift
import Adwaita
@main
struct HelloWorld: App {
@ -28,29 +15,27 @@ struct HelloWorld: App {
var app: GTUIApp!
var scene: Scene {
HelloWindow()
Window(id: "content") { _ in
// These are the views:
HeaderBar.empty()
Text("Hello, world!")
.padding()
}
}
}
```
## Create a View
1. Now, we will define `HelloWindow`. `HelloWindow` is a view, that means it conforms to the `View` protocol. As it additionally is a window, well make it conform to the `WindowScene` which automatically adds conformance to `View`.
```swift
struct HelloWindow: WindowScene {
In this example, the widgets `HeaderBar` and `Text` are used.
`padding` is a view modifier, a function that modifies a view, which adds some padding around the text.
var view: Body {
Text("Hello, world!")
.padding()
}
}
```
2. Run the executable Swift package (in GNOME Builder, press the play button, on the command line, use `swift run`).
You can see that one important component of a window in GNOME is missing: The header bar.
3. If you add another view inside of the `view` property of `HelloWindow`, the views get aligned vertically:
## Create Custom Views
While directly adding widgets into the `Window`'s body might work for simple "Hello World" apps,
it can get very messy very quickly.
You can create custom views by declaring types that conform to the `View` protocol:
```swift
struct HelloWindow: WindowScene {
// A custom view named "ContentView":
struct ContentView: View {
var view: Body {
HeaderBar.empty()
@ -60,3 +45,91 @@ struct HelloWindow: WindowScene {
}
```
## Properties
As every structure in Swift, custom views can have properties:
```swift
struct HelloView: View {
// The property "text":
var text: String
var view: Body {
Text("Hello, \(text)!")
.padding()
}
}
```
This view can be called via `HelloView(text: "world")` in another view.
## State
Whenever you want to modify a property that is stored in the view's structure from your view,
wrap the property with the `@State` property wrapper:
```swift
struct MyView: View {
// This property can be modified form within the view:
@State private var text = "world"
var view: Body {
Text("Hello, \(text)!")
.padding()
Button("Change Text") {
text = Bool.random() ? "world" : "John"
}
.padding(10, .horizontal.add(.bottom))
}
}
```
In this example, the text property is set whenever you press the "Change Text" button.
## Change the State in Child Views
You can access state properties in child views in the same way as you would access any other property
if the child view cannot modify the state (`HelloView` is defined above):
```swift
struct MyView: View {
@State private var text = "world"
var view: Body {
// "HelloView" can read the "text" property:
HelloView(text: text)
Button("Change Text") {
text = Bool.random() ? "world" : "John"
}
.padding(10, .horizontal.add(.bottom))
}
}
```
If the child view should be able to modify the state, use the `Binding` property wrapper in the child view
and pass the property with a dollar sign (`$`) to that view.
```swift
struct MyView: View {
@State private var text = "world"
var view: Body {
HelloView(text: text)
// Pass the editable text property to the child view:
ChangeTextView(text: $text)
}
}
struct ChangeTextView: View {
// Accept the editable text property:
@Binding var text: String
var view: Body {
Button("Change Text") {
// Binding properties can be used the same way as state properties:
text = Bool.random() ? "world" : "John"
}
.padding(10, .horizontal.add(.bottom))
}
}
```
Whenever you modify a state property (directly or indirectly through bindings),
the user interface gets automatically updated to reflect that change.

View File

@ -0,0 +1,72 @@
# Hello World
![The "HelloWorld" app][image-1]
This is a beginner tutorial. We will create a simple "Hello, world!" app using _Adwaita_.
## Create the Swift Package
1. Open your terminal client and navigate to a directory you want to create the package in (e.g. `~/Documents/`).
2. Create a new folder for the package using `mkdir HelloWorld`.
3. Enter the newly created folder using `cd HelloWorld`.
4. Run `swift package init --type executable` for creating a new Swift package.
5. Open the Swift package. If you are using GNOME Builder, click on `Select a Folder…` in the view that appears after opening Builder and open the `HelloWorld` folder.
## Add the Dependency
1. Open the `Package.swift` file.
2. Add the following line of code after `name: "HelloWorld",`:
```
dependencies: [.package(url: "https://github.com/david-swift/Adwaita", from: "0.1.1")],
```
## Create the App
1. Navigate to `Sources/main.swift`.
2. An app that uses the _Adwaita_ framework has a structure that conforms to the `App` protocol. The `scene` property returns one or more windows which provide content for display. An `@main` attribute marks it as the entry point of the app.
Replace `print("Hello, world!")` by your first app:
```swift
import Adwaita
@main
struct HelloWorld: App {
let id = "io.github.david-swift.HelloWorld"
var app: GTUIApp!
var scene: Scene {
Window(id: "content") { _ in
Text("Hello, world!")
.padding()
}
}
}
```
## Test the App
1. Run the executable Swift package (in GNOME Builder, press the play button, on the command line, use `swift run`).
You can see that one important component of a window in GNOME is missing: The header bar.
## Add a Header Bar
1. If you add another view inside of the `Window`'s body, the views get aligned vertically:
```swift
import Adwaita
@main
struct HelloWorld: App {
let id = "io.github.david-swift.HelloWorld"
var app: GTUIApp!
var scene: Scene {
Window(id: "content") { _ in
// Add the header bar:
HeaderBar.empty()
Text("Hello, world!")
.padding()
}
}
}
```
2. Run the app.
[image-1]: ../../Icons/HelloWorld.png

View File

@ -0,0 +1,130 @@
# Windows
![Multiple windows in an app built with _Adwaita_][image-1]
Windows in _Adwaita_ are not actually single windows in the user interface,
but rather instructions on how to create one type of window.
## The Simplest Case
In the "HelloWorld" app, we have created a single window app.
Whenever that window was closed using the "x" button, the app terminated.
We can add multiple windows to an app.
Whenever the last one disappears, the app terminates.
```swift
@main
struct HelloWorld: App {
let id = "io.github.david-swift.HelloWorld"
var app: GTUIApp!
var scene: Scene {
Window(id: "content") { _ in
HeaderBar.empty()
Text("Hello, world!")
.padding()
}
// Add a second window:
Window(id: "window-2") { _ in
HeaderBar.empty()
Text("Window 2")
.padding()
}
}
}
```
## Showing Windows
Every app contains the property `app`.
You can use this property for running functions that affect the whole app, e.g. quitting the app.
Another use case is showing a window:
```swift
@main
struct HelloWorld: App {
let id = "io.github.david-swift.HelloWorld"
var app: GTUIApp!
var scene: Scene {
Window(id: "content") { _ in
HeaderBar.empty()
Text("Hello, world!")
.padding()
}
Window(id: "control") { _ in
HeaderBar.empty()
Button("Show Window") {
// Show the window with the identifier "content":
app.showWindow("content")
}
.padding()
}
}
}
```
"Showing" a window means creating an instance of the window type if there isn't one,
or focusing the window that already exists of that type.
It should be used for opening windows that cannot be presented more than once
and for moving a window that is already open into the foreground.
## Adding Windows
You can call the `addWindow(_:)` function instead of the `showWindow(_:)`
if you want to add and focus another instance of a window type:
```swift
@main
struct HelloWorld: App {
let id = "io.github.david-swift.HelloWorld"
var app: GTUIApp!
var scene: Scene {
Window(id: "content") { _ in
HeaderBar.empty()
Text("Hello, world!")
.padding()
}
Window(id: "control") { _ in
HeaderBar.empty()
Button("Add Window") {
// Add a new instance of the "content" window type
app.addWindow("content")
}
.padding()
}
}
}
```
## Customizing the Initial Number of Windows
By default, every window type of the app's scene appears once when the app starts.
It's possible to customize how many windows are being presented at the app's startup:
```swift
@main
struct HelloWorld: App {
let id = "io.github.david-swift.HelloWorld"
var app: GTUIApp!
var scene: Scene {
// Open no window of the "content" type
Window(id: "content", open: 0) { _ in
HeaderBar.empty()
Text("Hello, world!")
.padding()
}
// Open two windows of the "control" type
Window(id: "control", open: 2) { _ in
HeaderBar.empty()
Button("Show Window") {
app.addWindow("content")
}
.padding()
}
}
}
```
[image-1]: ../../Icons/TwoWindows.png