Add basic GTK renderer code (#333)
Based on the work discussed in #306. * TokamakGTK implementation * Fix macOS GTK Renderer impl * Always release text in Picker. Use 'destroy_data' parameter to release closure boxes in GSignal.swift * Revert commenting out this code * Specify the product explicitly in Makefile * Add GTK renderer build for macOS on CI * Prevent xcodebuild from seeing GTK code Co-authored-by: Carson Katri <carson.katri@gmail.com> Co-authored-by: Morten Bek Ditlevsen <morten@ka-ching.dk>
This commit is contained in:
parent
8e5ad7f67f
commit
bd38866cb2
|
@ -15,7 +15,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
shell-action: carton bundle --product TokamakDemo
|
shell-action: carton bundle --product TokamakDemo
|
||||||
|
|
||||||
macos_build:
|
core_macos_build:
|
||||||
runs-on: macos-11.0
|
runs-on: macos-11.0
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
@ -29,6 +29,8 @@ jobs:
|
||||||
swift build --product TokamakPackageTests
|
swift build --product TokamakPackageTests
|
||||||
`xcrun --find xctest` .build/debug/TokamakPackageTests.xctest
|
`xcrun --find xctest` .build/debug/TokamakPackageTests.xctest
|
||||||
|
|
||||||
|
rm -rf Sources/TokamakGTKCHelpers/*.c
|
||||||
|
|
||||||
xcodebuild -version
|
xcodebuild -version
|
||||||
|
|
||||||
# make sure Tokamak can be built on macOS so that Xcode autocomplete works
|
# make sure Tokamak can be built on macOS so that Xcode autocomplete works
|
||||||
|
@ -40,3 +42,18 @@ jobs:
|
||||||
xcodebuild -scheme iOS -destination 'generic/platform=iOS' \
|
xcodebuild -scheme iOS -destination 'generic/platform=iOS' \
|
||||||
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
|
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
|
||||||
xcpretty --color
|
xcpretty --color
|
||||||
|
|
||||||
|
gtk_macos_build:
|
||||||
|
runs-on: macos-11.0
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Build the GTK renderer on macOS
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -ex
|
||||||
|
sudo xcode-select --switch /Applications/Xcode_12.2.app/Contents/Developer/
|
||||||
|
|
||||||
|
brew install gtk+3
|
||||||
|
|
||||||
|
make build
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"preLaunchTask": "make",
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug",
|
||||||
|
"program": "${workspaceFolder}/.build/debug/TokamakGTKDemo",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -17,6 +17,26 @@
|
||||||
"label": "carton dev",
|
"label": "carton dev",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "carton dev --product TokamakDemo"
|
"command": "carton dev --product TokamakDemo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "make",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "make",
|
||||||
|
"problemMatcher": [],
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "make run",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "make run",
|
||||||
|
"problemMatcher": [],
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
LINKER_FLAGS := $(shell pkg-config --libs gtk+-3.0)
|
||||||
|
C_FLAGS := $(shell pkg-config --cflags gtk+-3.0)
|
||||||
|
SWIFT_LINKER_FLAGS ?= -Xlinker $(shell echo $(LINKER_FLAGS) | sed -e "s/ / -Xlinker /g" | sed -e "s/-Xlinker -Wl,-framework,/-Xlinker -framework -Xlinker /g")
|
||||||
|
SWIFT_C_FLAGS ?= -Xcc $(shell echo $(C_FLAGS) | sed -e "s/ / -Xcc /g")
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
build:
|
||||||
|
swift build --product TokamakGTKDemo $(SWIFT_C_FLAGS) $(SWIFT_LINKER_FLAGS)
|
||||||
|
|
||||||
|
run: build
|
||||||
|
.build/debug/TokamakGTKDemo
|
|
@ -21,10 +21,6 @@ let package = Package(
|
||||||
name: "TokamakDOM",
|
name: "TokamakDOM",
|
||||||
targets: ["TokamakDOM"]
|
targets: ["TokamakDOM"]
|
||||||
),
|
),
|
||||||
.library(
|
|
||||||
name: "TokamakShim",
|
|
||||||
targets: ["TokamakShim"]
|
|
||||||
),
|
|
||||||
.library(
|
.library(
|
||||||
name: "TokamakStaticHTML",
|
name: "TokamakStaticHTML",
|
||||||
targets: ["TokamakStaticHTML"]
|
targets: ["TokamakStaticHTML"]
|
||||||
|
@ -33,6 +29,18 @@ let package = Package(
|
||||||
name: "TokamakStaticDemo",
|
name: "TokamakStaticDemo",
|
||||||
targets: ["TokamakStaticDemo"]
|
targets: ["TokamakStaticDemo"]
|
||||||
),
|
),
|
||||||
|
.library(
|
||||||
|
name: "TokamakGTK",
|
||||||
|
targets: ["TokamakGTK"]
|
||||||
|
),
|
||||||
|
.executable(
|
||||||
|
name: "TokamakGTKDemo",
|
||||||
|
targets: ["TokamakGTKDemo"]
|
||||||
|
),
|
||||||
|
.library(
|
||||||
|
name: "TokamakShim",
|
||||||
|
targets: ["TokamakShim"]
|
||||||
|
),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
// Dependencies declare other packages that this package depends on.
|
// Dependencies declare other packages that this package depends on.
|
||||||
|
@ -62,6 +70,34 @@ let package = Package(
|
||||||
name: "TokamakCore",
|
name: "TokamakCore",
|
||||||
dependencies: ["CombineShim", "Runtime"]
|
dependencies: ["CombineShim", "Runtime"]
|
||||||
),
|
),
|
||||||
|
.target(
|
||||||
|
name: "TokamakShim",
|
||||||
|
dependencies: [
|
||||||
|
.target(name: "TokamakDOM", condition: .when(platforms: [.wasi])),
|
||||||
|
.target(name: "TokamakGTK", condition: .when(platforms: [.linux])),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.systemLibrary(
|
||||||
|
name: "CGTK",
|
||||||
|
pkgConfig: "gtk+-3.0",
|
||||||
|
providers: [
|
||||||
|
.apt(["libgtk+-3.0", "gtk+-3.0"]),
|
||||||
|
// .yum(["gtk3-devel"]),
|
||||||
|
.brew(["gtk+3"]),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "TokamakGTKCHelpers",
|
||||||
|
dependencies: ["CGTK"]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "TokamakGTK",
|
||||||
|
dependencies: ["TokamakCore", "CGTK", "TokamakGTKCHelpers", "CombineShim"]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "TokamakGTKDemo",
|
||||||
|
dependencies: ["TokamakGTK"]
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "TokamakStaticHTML",
|
name: "TokamakStaticHTML",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
@ -82,10 +118,6 @@ let package = Package(
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.target(
|
|
||||||
name: "TokamakShim",
|
|
||||||
dependencies: [.target(name: "TokamakDOM", condition: .when(platforms: [.wasi]))]
|
|
||||||
),
|
|
||||||
.target(
|
.target(
|
||||||
name: "TokamakDemo",
|
name: "TokamakDemo",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
#include <gtk/gtk.h>
|
|
@ -0,0 +1,8 @@
|
||||||
|
module CGTK {
|
||||||
|
header "./termios-Header.h"
|
||||||
|
header "./CGTK-Bridging-Header.h"
|
||||||
|
|
||||||
|
link "gtk-3"
|
||||||
|
|
||||||
|
export *
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
#include <termios.h>
|
|
@ -23,7 +23,7 @@ public struct _AnyApp: App {
|
||||||
let bodyClosure: (Any) -> _AnyScene
|
let bodyClosure: (Any) -> _AnyScene
|
||||||
let bodyType: Any.Type
|
let bodyType: Any.Type
|
||||||
|
|
||||||
init<A: App>(_ app: A) {
|
public init<A: App>(_ app: A) {
|
||||||
self.app = app
|
self.app = app
|
||||||
type = A.self
|
type = A.self
|
||||||
// swiftlint:disable:next force_cast
|
// swiftlint:disable:next force_cast
|
||||||
|
|
|
@ -103,12 +103,14 @@ public extension EnvironmentValues {
|
||||||
|
|
||||||
public protocol _AnyIDView {
|
public protocol _AnyIDView {
|
||||||
var anyId: AnyHashable { get }
|
var anyId: AnyHashable { get }
|
||||||
|
var anyContent: AnyView { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
struct IDView<Content, ID>: View, _AnyIDView where Content: View, ID: Hashable {
|
struct IDView<Content, ID>: View, _AnyIDView where Content: View, ID: Hashable {
|
||||||
let content: Content
|
let content: Content
|
||||||
let id: ID
|
let id: ID
|
||||||
var anyId: AnyHashable { AnyHashable(id) }
|
var anyId: AnyHashable { AnyHashable(id) }
|
||||||
|
var anyContent: AnyView { AnyView(content) }
|
||||||
|
|
||||||
init(_ content: Content, id: ID) {
|
init(_ content: Content, id: ID) {
|
||||||
self.content = content
|
self.content = content
|
||||||
|
|
|
@ -80,13 +80,16 @@ public struct _NavigationLinkProxy<Label, Destination> where Label: View, Destin
|
||||||
self.subject = subject
|
self.subject = subject
|
||||||
}
|
}
|
||||||
|
|
||||||
public var label: AnyView {
|
public var label: some View {
|
||||||
subject.style.makeBody(configuration: .init(
|
subject.style.makeBody(configuration: .init(
|
||||||
body: AnyView(subject.label),
|
body: AnyView(subject.label),
|
||||||
isSelected: isSelected
|
isSelected: isSelected
|
||||||
))
|
))
|
||||||
|
// subject.label
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var context: NavigationContext { subject.navigationContext }
|
||||||
|
|
||||||
public var style: _AnyNavigationLinkStyle { subject.style }
|
public var style: _AnyNavigationLinkStyle { subject.style }
|
||||||
public var isSelected: Bool {
|
public var isSelected: Bool {
|
||||||
subject.destination === subject.navigationContext.destination
|
subject.destination === subject.navigationContext.destination
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
// Created by Jed Fox on 06/30/2020.
|
// Created by Jed Fox on 06/30/2020.
|
||||||
//
|
//
|
||||||
|
|
||||||
final class NavigationContext: ObservableObject {
|
public final class NavigationContext: ObservableObject {
|
||||||
@Published var destination = NavigationLinkDestination(EmptyView())
|
@Published var destination = NavigationLinkDestination(EmptyView())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,14 +39,16 @@ public struct _NavigationViewProxy<Content: View> {
|
||||||
|
|
||||||
public init(_ subject: NavigationView<Content>) { self.subject = subject }
|
public init(_ subject: NavigationView<Content>) { self.subject = subject }
|
||||||
|
|
||||||
|
public var context: NavigationContext { subject.context }
|
||||||
|
|
||||||
public var content: some View {
|
public var content: some View {
|
||||||
subject.content
|
subject.content
|
||||||
.environmentObject(subject.context)
|
.environmentObject(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var destination: some View {
|
public var destination: some View {
|
||||||
subject.context.destination.view
|
subject.context.destination.view
|
||||||
.environmentObject(subject.context)
|
.environmentObject(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,19 +12,28 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
public struct _PickerContainer<Label: View, SelectionValue: Hashable, Content: View>: View {
|
public protocol _PickerContainerProtocol {
|
||||||
|
var elements: [_AnyIDView] { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct _PickerContainer<Label: View, SelectionValue: Hashable, Content: View>: View,
|
||||||
|
_PickerContainerProtocol
|
||||||
|
{
|
||||||
@Binding public var selection: SelectionValue
|
@Binding public var selection: SelectionValue
|
||||||
public let label: Label
|
public let label: Label
|
||||||
public let content: Content
|
public let content: Content
|
||||||
|
public let elements: [_AnyIDView]
|
||||||
@Environment(\.pickerStyle) public var style
|
@Environment(\.pickerStyle) public var style
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
selection: Binding<SelectionValue>,
|
selection: Binding<SelectionValue>,
|
||||||
label: Label,
|
label: Label,
|
||||||
|
elements: [_AnyIDView],
|
||||||
@ViewBuilder content: () -> Content
|
@ViewBuilder content: () -> Content
|
||||||
) {
|
) {
|
||||||
_selection = selection
|
_selection = selection
|
||||||
self.label = label
|
self.label = label
|
||||||
|
self.elements = elements
|
||||||
self.content = content()
|
self.content = content()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +70,7 @@ public struct Picker<Label: View, SelectionValue: Hashable, Content: View>: View
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
let children = self.children
|
let children = self.children
|
||||||
|
|
||||||
return _PickerContainer(selection: selection, label: label) {
|
return _PickerContainer(selection: selection, label: label, elements: elements) {
|
||||||
// Need to implement a special behavior here. If one of the children is `ForEach`
|
// Need to implement a special behavior here. If one of the children is `ForEach`
|
||||||
// and its `Data.Element` type is the same as `SelectionValue` type, then we can
|
// and its `Data.Element` type is the same as `SelectionValue` type, then we can
|
||||||
// update the binding.
|
// update the binding.
|
||||||
|
@ -100,3 +109,14 @@ extension Picker: ParentView {
|
||||||
(content as? GroupView)?.children ?? [AnyView(content)]
|
(content as? GroupView)?.children ?? [AnyView(content)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Picker: _PickerContainerProtocol {
|
||||||
|
public var elements: [_AnyIDView] {
|
||||||
|
(content as? ForEachProtocol)?.children
|
||||||
|
.compactMap {
|
||||||
|
mapAnyView($0, transform: { (v: _AnyIDView) in v })
|
||||||
|
} ?? []
|
||||||
|
// .filter { $0.elementType == SelectionValue.self }
|
||||||
|
// .map(\.children) ?? []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -52,11 +52,11 @@ struct NavItem: View {
|
||||||
Text(id)
|
Text(id)
|
||||||
#elseif os(macOS)
|
#elseif os(macOS)
|
||||||
Text(id).opacity(0.5)
|
Text(id).opacity(0.5)
|
||||||
#else
|
#elseif os(Linux)
|
||||||
HStack {
|
HStack {
|
||||||
Text(id)
|
Text(id)
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("unavailable").opacity(0.5)
|
Text("unavailable")
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
// Copyright 2020 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Created by Carson Katri on 10/10/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CGTK
|
||||||
|
import CombineShim
|
||||||
|
import Dispatch
|
||||||
|
import TokamakCore
|
||||||
|
|
||||||
|
public extension App {
|
||||||
|
static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues) {
|
||||||
|
_ = Unmanaged.passRetained(GTKRenderer(app, rootEnvironment))
|
||||||
|
}
|
||||||
|
|
||||||
|
static func _setTitle(_ title: String) {
|
||||||
|
GTKRenderer.sharedWindow.withMemoryRebound(to: GtkWindow.self, capacity: 1) {
|
||||||
|
gtk_window_set_title($0, title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _phasePublisher: AnyPublisher<ScenePhase, Never> {
|
||||||
|
CurrentValueSubject(.active).eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> {
|
||||||
|
CurrentValueSubject(.light).eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UnsafeMutablePointer where Pointee == GApplication {
|
||||||
|
@discardableResult
|
||||||
|
func connect(
|
||||||
|
signal: UnsafePointer<gchar>,
|
||||||
|
data: UnsafeMutableRawPointer? = nil,
|
||||||
|
handler: @convention(c) @escaping (UnsafeMutablePointer<GtkApplication>?, UnsafeRawPointer)
|
||||||
|
-> Bool
|
||||||
|
) -> Int {
|
||||||
|
let handler = unsafeBitCast(handler, to: GCallback.self)
|
||||||
|
return Int(g_signal_connect_data(self, signal, handler, data, nil, GConnectFlags(rawValue: 0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect with a context-capturing closure.
|
||||||
|
@discardableResult
|
||||||
|
func connect(
|
||||||
|
signal: UnsafePointer<gchar>,
|
||||||
|
closure: @escaping () -> ()
|
||||||
|
) -> Int {
|
||||||
|
let closureBox = Unmanaged.passRetained(ClosureBox(closure)).toOpaque()
|
||||||
|
return connect(signal: signal, data: closureBox) { _, closureBox in
|
||||||
|
let unpackedAction = Unmanaged<ClosureBox<()>>.fromOpaque(closureBox)
|
||||||
|
unpackedAction.takeRetainedValue().closure()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
// Copyright 2020 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Created by Carson Katri on 10/10/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CGTK
|
||||||
|
import TokamakCore
|
||||||
|
|
||||||
|
// MARK: Environment & State
|
||||||
|
|
||||||
|
public typealias Environment = TokamakCore.Environment
|
||||||
|
public typealias EnvironmentObject = TokamakCore.EnvironmentObject
|
||||||
|
|
||||||
|
public typealias Binding = TokamakCore.Binding
|
||||||
|
public typealias ObservableObject = TokamakCore.ObservableObject
|
||||||
|
public typealias ObservedObject = TokamakCore.ObservedObject
|
||||||
|
public typealias Published = TokamakCore.Published
|
||||||
|
public typealias State = TokamakCore.State
|
||||||
|
public typealias StateObject = TokamakCore.StateObject
|
||||||
|
|
||||||
|
// MARK: Modifiers & Styles
|
||||||
|
|
||||||
|
public typealias ViewModifier = TokamakCore.ViewModifier
|
||||||
|
public typealias ModifiedContent = TokamakCore.ModifiedContent
|
||||||
|
|
||||||
|
public typealias DefaultTextFieldStyle = TokamakCore.DefaultTextFieldStyle
|
||||||
|
public typealias PlainTextFieldStyle = TokamakCore.PlainTextFieldStyle
|
||||||
|
public typealias RoundedBorderTextFieldStyle = TokamakCore.RoundedBorderTextFieldStyle
|
||||||
|
public typealias SquareBorderTextFieldStyle = TokamakCore.SquareBorderTextFieldStyle
|
||||||
|
|
||||||
|
public typealias DefaultListStyle = TokamakCore.DefaultListStyle
|
||||||
|
public typealias PlainListStyle = TokamakCore.PlainListStyle
|
||||||
|
public typealias InsetListStyle = TokamakCore.InsetListStyle
|
||||||
|
public typealias GroupedListStyle = TokamakCore.GroupedListStyle
|
||||||
|
public typealias InsetGroupedListStyle = TokamakCore.InsetGroupedListStyle
|
||||||
|
public typealias SidebarListStyle = TokamakCore.SidebarListStyle
|
||||||
|
|
||||||
|
public typealias DefaultPickerStyle = TokamakCore.DefaultPickerStyle
|
||||||
|
public typealias PopUpButtonPickerStyle = TokamakCore.PopUpButtonPickerStyle
|
||||||
|
public typealias RadioGroupPickerStyle = TokamakCore.RadioGroupPickerStyle
|
||||||
|
public typealias SegmentedPickerStyle = TokamakCore.SegmentedPickerStyle
|
||||||
|
public typealias WheelPickerStyle = TokamakCore.WheelPickerStyle
|
||||||
|
|
||||||
|
public typealias ToggleStyle = TokamakCore.ToggleStyle
|
||||||
|
public typealias ToggleStyleConfiguration = TokamakCore.ToggleStyleConfiguration
|
||||||
|
|
||||||
|
public typealias ButtonStyle = TokamakCore.ButtonStyle
|
||||||
|
public typealias ButtonStyleConfiguration = TokamakCore.ButtonStyleConfiguration
|
||||||
|
public typealias DefaultButtonStyle = TokamakCore.DefaultButtonStyle
|
||||||
|
|
||||||
|
public typealias ColorScheme = TokamakCore.ColorScheme
|
||||||
|
|
||||||
|
// MARK: Shapes
|
||||||
|
|
||||||
|
public typealias Shape = TokamakCore.Shape
|
||||||
|
|
||||||
|
public typealias Capsule = TokamakCore.Capsule
|
||||||
|
public typealias Circle = TokamakCore.Circle
|
||||||
|
public typealias Ellipse = TokamakCore.Ellipse
|
||||||
|
public typealias Path = TokamakCore.Path
|
||||||
|
public typealias Rectangle = TokamakCore.Rectangle
|
||||||
|
public typealias RoundedRectangle = TokamakCore.RoundedRectangle
|
||||||
|
|
||||||
|
// MARK: Primitive values
|
||||||
|
|
||||||
|
public typealias Color = TokamakCore.Color
|
||||||
|
public typealias Font = TokamakCore.Font
|
||||||
|
|
||||||
|
public typealias CGAffineTransform = TokamakCore.CGAffineTransform
|
||||||
|
public typealias CGPoint = TokamakCore.CGPoint
|
||||||
|
public typealias CGRect = TokamakCore.CGRect
|
||||||
|
public typealias CGSize = TokamakCore.CGSize
|
||||||
|
|
||||||
|
// MARK: Views
|
||||||
|
|
||||||
|
public typealias Button = TokamakCore.Button
|
||||||
|
public typealias DisclosureGroup = TokamakCore.DisclosureGroup
|
||||||
|
public typealias Divider = TokamakCore.Divider
|
||||||
|
public typealias ForEach = TokamakCore.ForEach
|
||||||
|
public typealias GeometryReader = TokamakCore.GeometryReader
|
||||||
|
public typealias GridItem = TokamakCore.GridItem
|
||||||
|
public typealias Group = TokamakCore.Group
|
||||||
|
public typealias HStack = TokamakCore.HStack
|
||||||
|
public typealias LazyHGrid = TokamakCore.LazyHGrid
|
||||||
|
public typealias LazyVGrid = TokamakCore.LazyVGrid
|
||||||
|
public typealias List = TokamakCore.List
|
||||||
|
public typealias NavigationLink = TokamakCore.NavigationLink
|
||||||
|
public typealias NavigationView = TokamakCore.NavigationView
|
||||||
|
public typealias OutlineGroup = TokamakCore.OutlineGroup
|
||||||
|
public typealias Picker = TokamakCore.Picker
|
||||||
|
public typealias ScrollView = TokamakCore.ScrollView
|
||||||
|
public typealias Section = TokamakCore.Section
|
||||||
|
public typealias SecureField = TokamakCore.SecureField
|
||||||
|
public typealias Slider = TokamakCore.Slider
|
||||||
|
public typealias Spacer = TokamakCore.Spacer
|
||||||
|
public typealias Text = TokamakCore.Text
|
||||||
|
public typealias TextField = TokamakCore.TextField
|
||||||
|
public typealias Toggle = TokamakCore.Toggle
|
||||||
|
public typealias VStack = TokamakCore.VStack
|
||||||
|
public typealias ZStack = TokamakCore.ZStack
|
||||||
|
|
||||||
|
// MARK: Special Views
|
||||||
|
|
||||||
|
public typealias View = TokamakCore.View
|
||||||
|
public typealias AnyView = TokamakCore.AnyView
|
||||||
|
public typealias EmptyView = TokamakCore.EmptyView
|
||||||
|
|
||||||
|
// MARK: App & Scene
|
||||||
|
|
||||||
|
public typealias App = TokamakCore.App
|
||||||
|
public typealias Scene = TokamakCore.Scene
|
||||||
|
public typealias WindowGroup = TokamakCore.WindowGroup
|
||||||
|
public typealias ScenePhase = TokamakCore.ScenePhase
|
||||||
|
public typealias AppStorage = TokamakCore.AppStorage
|
||||||
|
public typealias SceneStorage = TokamakCore.SceneStorage
|
||||||
|
|
||||||
|
// MARK: Misc
|
||||||
|
|
||||||
|
public typealias ViewBuilder = TokamakCore.ViewBuilder
|
||||||
|
|
||||||
|
// FIXME: I would put this inside TokamakCore, but for
|
||||||
|
// some reason it doesn't get exported with the typealias
|
||||||
|
public extension Text {
|
||||||
|
static func + (lhs: Self, rhs: Self) -> Self {
|
||||||
|
_concatenating(lhs: lhs, rhs: rhs)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
// Copyright 2020 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Created by Carson Katri on 10/10/2020.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CGTK
|
||||||
|
|
||||||
|
extension UnsafeMutablePointer where Pointee == GtkWidget {
|
||||||
|
/// Connect with a c function pointer.
|
||||||
|
@discardableResult
|
||||||
|
func connect(
|
||||||
|
signal: UnsafePointer<gchar>,
|
||||||
|
data: gpointer? = nil,
|
||||||
|
handler: @convention(c) @escaping (UnsafeMutablePointer<GtkWidget>?, UnsafeRawPointer) -> Bool,
|
||||||
|
destroy: @convention(c) @escaping (UnsafeRawPointer, UnsafeRawPointer) -> ()
|
||||||
|
) -> Int {
|
||||||
|
let handler = unsafeBitCast(handler, to: GCallback.self)
|
||||||
|
let destroy = unsafeBitCast(destroy, to: GClosureNotify.self)
|
||||||
|
return Int(g_signal_connect_data(
|
||||||
|
self,
|
||||||
|
signal,
|
||||||
|
handler,
|
||||||
|
data,
|
||||||
|
destroy,
|
||||||
|
GConnectFlags(rawValue: 0)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect with a context-capturing closure.
|
||||||
|
@discardableResult
|
||||||
|
func connect(
|
||||||
|
signal: UnsafePointer<gchar>,
|
||||||
|
closure: @escaping () -> ()
|
||||||
|
) -> Int {
|
||||||
|
let closureBox = Unmanaged.passRetained(ClosureBox(closure)).toOpaque()
|
||||||
|
return connect(signal: signal, data: closureBox, handler: { _, closureBox in
|
||||||
|
let unpackedAction = Unmanaged<ClosureBox<()>>.fromOpaque(closureBox)
|
||||||
|
unpackedAction.takeUnretainedValue().closure()
|
||||||
|
return true
|
||||||
|
}, destroy: { closureBox, _ in
|
||||||
|
let unpackedAction = Unmanaged<ClosureBox<()>>.fromOpaque(closureBox)
|
||||||
|
unpackedAction.release()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect with a context-capturing closure (with the GtkWidget passed through)
|
||||||
|
@discardableResult
|
||||||
|
func connect(
|
||||||
|
signal: UnsafePointer<gchar>,
|
||||||
|
closure: @escaping (UnsafeMutablePointer<GtkWidget>?) -> ()
|
||||||
|
) -> Int {
|
||||||
|
let closureBox = Unmanaged.passRetained(SingleParamClosureBox(closure)).retain().toOpaque()
|
||||||
|
return connect(signal: signal, data: closureBox, handler: { widget, closureBox in
|
||||||
|
let unpackedAction = Unmanaged<SingleParamClosureBox<UnsafeMutablePointer<GtkWidget>?, ()>>
|
||||||
|
.fromOpaque(closureBox)
|
||||||
|
if let widget = widget {
|
||||||
|
unpackedAction.takeUnretainedValue().closure(widget)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}, destroy: { closureBox, _ in
|
||||||
|
let unpackedAction = Unmanaged<SingleParamClosureBox<UnsafeMutablePointer<GtkWidget>?, ()>>
|
||||||
|
.fromOpaque(closureBox)
|
||||||
|
unpackedAction.release()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func disconnect(
|
||||||
|
gtype: GType,
|
||||||
|
signal: UnsafePointer<gchar>
|
||||||
|
) {
|
||||||
|
// Find the signal ID from the signal `gchar` for the specified `GtkWidget` type.
|
||||||
|
let sigId = g_signal_lookup(signal, gtype)
|
||||||
|
// Get the bound handler ID from the instance.
|
||||||
|
let handlerId = g_signal_handler_find(self, G_SIGNAL_MATCH_ID, sigId, 0, nil, nil, nil)
|
||||||
|
// Disconnect the handler from the instance.
|
||||||
|
g_signal_handler_disconnect(self, handlerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ClosureBox<U> {
|
||||||
|
let closure: () -> U
|
||||||
|
|
||||||
|
init(_ closure: @escaping () -> U) { self.closure = closure }
|
||||||
|
}
|
||||||
|
|
||||||
|
final class SingleParamClosureBox<T, U> {
|
||||||
|
let closure: (T) -> U
|
||||||
|
|
||||||
|
init(_ closure: @escaping (T) -> U) { self.closure = closure }
|
||||||
|
}
|
|
@ -0,0 +1,136 @@
|
||||||
|
// Copyright 2020 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Created by Carson Katri on 10/10/2020.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CGTK
|
||||||
|
import Dispatch
|
||||||
|
import TokamakCore
|
||||||
|
|
||||||
|
extension EnvironmentValues {
|
||||||
|
/// Returns default settings for the GTK environment
|
||||||
|
static var defaultEnvironment: Self {
|
||||||
|
var environment = EnvironmentValues()
|
||||||
|
environment[_ColorSchemeKey] = .light
|
||||||
|
// environment._defaultAppStorage = LocalStorage.standard
|
||||||
|
// _DefaultSceneStorageProvider.default = SessionStorage.standard
|
||||||
|
|
||||||
|
return environment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class GTKRenderer: Renderer {
|
||||||
|
private(set) var reconciler: StackReconciler<GTKRenderer>?
|
||||||
|
private var gtkAppRef: UnsafeMutablePointer<GtkApplication>
|
||||||
|
static var sharedWindow: UnsafeMutablePointer<GtkWidget>!
|
||||||
|
|
||||||
|
init<A: App>(
|
||||||
|
_ app: A,
|
||||||
|
_ rootEnvironment: EnvironmentValues? = nil
|
||||||
|
) {
|
||||||
|
gtkAppRef = gtk_application_new(nil, G_APPLICATION_FLAGS_NONE)
|
||||||
|
|
||||||
|
gtkAppRef.withMemoryRebound(to: GApplication.self, capacity: 1) { gApp in
|
||||||
|
gApp.connect(signal: "activate") {
|
||||||
|
let window: UnsafeMutablePointer<GtkWidget>
|
||||||
|
window = gtk_application_window_new(self.gtkAppRef)
|
||||||
|
window.withMemoryRebound(to: GtkWindow.self, capacity: 1) {
|
||||||
|
gtk_window_set_default_size($0, 200, 100)
|
||||||
|
}
|
||||||
|
gtk_widget_show_all(window)
|
||||||
|
|
||||||
|
GTKRenderer.sharedWindow = window
|
||||||
|
|
||||||
|
self.reconciler = StackReconciler(
|
||||||
|
app: app,
|
||||||
|
target: Widget(window),
|
||||||
|
environment: .defaultEnvironment,
|
||||||
|
renderer: self,
|
||||||
|
scheduler: { next in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
next()
|
||||||
|
gtk_widget_show_all(window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = g_application_run(gApp, 0, nil)
|
||||||
|
exit(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func mountTarget(
|
||||||
|
before sibling: Widget?,
|
||||||
|
to parent: Widget,
|
||||||
|
with host: MountedHost
|
||||||
|
) -> Widget? {
|
||||||
|
guard let anyWidget = mapAnyView(
|
||||||
|
host.view,
|
||||||
|
transform: { (widget: AnyWidget) in widget }
|
||||||
|
) else {
|
||||||
|
// handle cases like `TupleView`
|
||||||
|
if mapAnyView(host.view, transform: { (view: ParentView) in view }) != nil {
|
||||||
|
return parent
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let ctor = anyWidget.new
|
||||||
|
|
||||||
|
let widget: UnsafeMutablePointer<GtkWidget>
|
||||||
|
switch parent.storage {
|
||||||
|
case let .application(app):
|
||||||
|
widget = ctor(app)
|
||||||
|
case let .widget(parentWidget):
|
||||||
|
widget = ctor(gtkAppRef)
|
||||||
|
parentWidget.withMemoryRebound(to: GtkContainer.self, capacity: 1) {
|
||||||
|
gtk_container_add($0, widget)
|
||||||
|
if let stack = mapAnyView(parent.view, transform: { (view: StackProtocol) in view }) {
|
||||||
|
gtk_widget_set_valign(widget, stack.alignment.vertical.gtkValue)
|
||||||
|
gtk_widget_set_halign(widget, stack.alignment.horizontal.gtkValue)
|
||||||
|
if anyWidget.expand {
|
||||||
|
gtk_widget_set_hexpand(widget, gtk_true())
|
||||||
|
gtk_widget_set_vexpand(widget, gtk_true())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gtk_widget_show(widget)
|
||||||
|
return Widget(host.view, widget)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(target: Widget, with host: MountedHost) {
|
||||||
|
guard let widget = mapAnyView(host.view, transform: { (widget: AnyWidget) in widget })
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
widget.update(widget: target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmount(
|
||||||
|
target: Widget,
|
||||||
|
from parent: Widget,
|
||||||
|
with host: MountedHost,
|
||||||
|
completion: @escaping () -> ()
|
||||||
|
) {
|
||||||
|
defer { completion() }
|
||||||
|
|
||||||
|
guard mapAnyView(host.view, transform: { (widget: AnyWidget) in widget }) != nil
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
target.destroy()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
// Copyright 2020 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Created by Carson Katri on 10/10/2020.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CGTK
|
||||||
|
import TokamakGTKCHelpers
|
||||||
|
|
||||||
|
extension UnsafeMutablePointer where Pointee == GtkContainer {
|
||||||
|
/// Iterate over the children
|
||||||
|
func forEach(
|
||||||
|
_ closure: @escaping (UnsafeMutablePointer<GtkWidget>?) -> ()
|
||||||
|
) {
|
||||||
|
let closureBox = Unmanaged.passRetained(SingleParamClosureBox(closure)).toOpaque()
|
||||||
|
let handler: @convention(c) (UnsafeMutablePointer<GtkWidget>?, UnsafeRawPointer)
|
||||||
|
-> Bool = { (ref: UnsafeMutablePointer<GtkWidget>?, data: UnsafeRawPointer) -> Bool in
|
||||||
|
let unpackedAction = Unmanaged<SingleParamClosureBox<UnsafeMutablePointer<GtkWidget>?, ()>>
|
||||||
|
.fromOpaque(data)
|
||||||
|
unpackedAction.takeRetainedValue().closure(ref)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
let cHandler = unsafeBitCast(handler, to: GtkCallback.self)
|
||||||
|
gtk_container_foreach(self, cHandler, closureBox)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UnsafeMutablePointer where Pointee == GtkWidget {
|
||||||
|
func isContainer() -> Bool {
|
||||||
|
tokamak_gtk_widget_is_container(self) == gtk_true()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isStack() -> Bool {
|
||||||
|
tokamak_gtk_widget_is_stack(self) == gtk_true()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright 2020 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Created by Carson Katri on 10/13/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CGTK
|
||||||
|
import TokamakCore
|
||||||
|
|
||||||
|
extension _FrameLayout: WidgetModifier {
|
||||||
|
public func modify(widget: UnsafeMutablePointer<GtkWidget>) {
|
||||||
|
gtk_widget_set_size_request(widget, Int32(width ?? -1), Int32(height ?? -1))
|
||||||
|
// gtk_widget_set_halign(widget, alignment.horizontal.gtkValue)
|
||||||
|
// gtk_widget_set_valign(widget, alignment.vertical.gtkValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _FlexFrameLayout: WidgetModifier {
|
||||||
|
public func modify(widget: UnsafeMutablePointer<GtkWidget>) {
|
||||||
|
gtk_widget_set_halign(widget, alignment.horizontal.gtkValue)
|
||||||
|
gtk_widget_set_valign(widget, alignment.vertical.gtkValue)
|
||||||
|
if maxWidth == .infinity {
|
||||||
|
print("Setting hexpand")
|
||||||
|
gtk_widget_set_hexpand(widget, gtk_true())
|
||||||
|
gtk_widget_set_halign(widget, GTK_ALIGN_FILL)
|
||||||
|
}
|
||||||
|
if maxHeight == .infinity {
|
||||||
|
print("Setting vexpand")
|
||||||
|
gtk_widget_set_vexpand(widget, gtk_true())
|
||||||
|
gtk_widget_set_valign(widget, GTK_ALIGN_FILL)
|
||||||
|
}
|
||||||
|
gtk_widget_set_size_request(widget, Int32(idealWidth ?? -1), Int32(idealHeight ?? -1))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
// Copyright 2020 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Created by Carson Katri on 10/13/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CGTK
|
||||||
|
import TokamakCore
|
||||||
|
|
||||||
|
protocol WidgetModifier {
|
||||||
|
func modify(widget: UnsafeMutablePointer<GtkWidget>)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ModifiedContent: ViewDeferredToRenderer where Content: View {
|
||||||
|
public var deferredBody: AnyView {
|
||||||
|
if let widgetModifier = modifier as? WidgetModifier {
|
||||||
|
if let anyView = content as? ViewDeferredToRenderer,
|
||||||
|
let anyWidget = mapAnyView(
|
||||||
|
anyView.deferredBody,
|
||||||
|
transform: { (widget: AnyWidget) in widget }
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return AnyView(WidgetView {
|
||||||
|
let contentWidget = anyWidget.new($0)
|
||||||
|
widgetModifier.modify(widget: contentWidget)
|
||||||
|
return contentWidget
|
||||||
|
} content: {
|
||||||
|
if let parentView = anyWidget as? ParentView {
|
||||||
|
ForEach(Array(parentView.children.enumerated()), id: \.offset) { _, view in
|
||||||
|
view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if let anyWidget = content as? AnyWidget {
|
||||||
|
return AnyView(WidgetView {
|
||||||
|
let contentWidget = anyWidget.new($0)
|
||||||
|
widgetModifier.modify(widget: contentWidget)
|
||||||
|
return contentWidget
|
||||||
|
} content: {
|
||||||
|
if let parentView = anyWidget as? ParentView {
|
||||||
|
ForEach(Array(parentView.children.enumerated()), id: \.offset) { _, view in
|
||||||
|
view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AnyView(content)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright 2020 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Created by Carson Katri on 10/10/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CGTK
|
||||||
|
import TokamakCore
|
||||||
|
|
||||||
|
struct SceneContainerView<Content: View>: View, AnyWidget {
|
||||||
|
let content: Content
|
||||||
|
|
||||||
|
var body: Never {
|
||||||
|
neverBody("SceneContainerView")
|
||||||
|
}
|
||||||
|
|
||||||
|
func new(_ application: UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget> {
|
||||||
|
print("Making window")
|
||||||
|
let window: UnsafeMutablePointer<GtkWidget>
|
||||||
|
window = gtk_application_window_new(application)
|
||||||
|
print("window.new")
|
||||||
|
window.withMemoryRebound(to: GtkWindow.self, capacity: 1) {
|
||||||
|
gtk_window_set_title($0, "Welcome to GNOME")
|
||||||
|
gtk_window_set_default_size($0, 200, 100)
|
||||||
|
}
|
||||||
|
print("Window made")
|
||||||
|
// gtk_widget_show_all(window)
|
||||||
|
return window
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(widget: Widget) {}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
// Copyright 2020 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Created by Carson Katri on 10/10/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import TokamakCore
|
||||||
|
|
||||||
|
extension WindowGroup: SceneDeferredToRenderer {
|
||||||
|
public var deferredBody: AnyView {
|
||||||
|
AnyView(VStack(alignment: .center) {
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
content
|
||||||
|
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||||
|
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
// Copyright 2020 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Created by Carson Katri on 8/4/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import TokamakCore
|
||||||
|
|
||||||
|
// MARK: List Colors
|
||||||
|
|
||||||
|
extension Color {
|
||||||
|
static var listSectionHeader: Self {
|
||||||
|
Color._withScheme {
|
||||||
|
switch $0 {
|
||||||
|
case .light: return Color(0xDDDDDD)
|
||||||
|
case .dark: return Color(0x323234)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var groupedListBackground: Self {
|
||||||
|
Color._withScheme {
|
||||||
|
switch $0 {
|
||||||
|
case .light: return Color(0xEEEEEE)
|
||||||
|
case .dark: return .clear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var listGroupBackground: Self {
|
||||||
|
Color._withScheme {
|
||||||
|
switch $0 {
|
||||||
|
case .light: return .white
|
||||||
|
case .dark: return Color(0x444444)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var sidebarBackground: Self {
|
||||||
|
Color._withScheme {
|
||||||
|
switch $0 {
|
||||||
|
case .light: return Color(0xF2F2F7)
|
||||||
|
case .dark: return Color(0x2D2B30)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright 2020 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Created by Carson Katri on 10/10/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CGTK
|
||||||
|
import Foundation
|
||||||
|
import TokamakCore
|
||||||
|
|
||||||
|
extension _Button: AnyWidget, ParentView {
|
||||||
|
func new(_ application: UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget> {
|
||||||
|
let btn = gtk_button_new()!
|
||||||
|
bindAction(to: btn)
|
||||||
|
return btn
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(widget: Widget) {
|
||||||
|
if case let .widget(w) = widget.storage {
|
||||||
|
w.disconnect(gtype: gtk_button_get_type(), signal: "clicked")
|
||||||
|
bindAction(to: w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindAction(to btn: UnsafeMutablePointer<GtkWidget>) {
|
||||||
|
btn.connect(signal: "clicked", closure: action)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var children: [AnyView] {
|
||||||
|
[AnyView(label)]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
// Copyright 2020 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import CGTK
|
||||||
|
import TokamakCore
|
||||||
|
|
||||||
|
extension List: ViewDeferredToRenderer {
|
||||||
|
@ViewBuilder
|
||||||
|
func iterateAsRow(_ content: [AnyView]) -> some View {
|
||||||
|
ForEach(Array(content.enumerated()), id: \.offset) { _, row in
|
||||||
|
if let parentView = mapAnyView(row, transform: { (view: ParentView) in view }) {
|
||||||
|
AnyView(iterateAsRow(parentView.children))
|
||||||
|
} else {
|
||||||
|
WidgetView(build: { _ in
|
||||||
|
gtk_list_box_row_new()
|
||||||
|
}) {
|
||||||
|
row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var deferredBody: AnyView {
|
||||||
|
let proxy = _ListProxy(self)
|
||||||
|
return AnyView(ScrollView {
|
||||||
|
WidgetView(build: { _ in
|
||||||
|
gtk_list_box_new()
|
||||||
|
}) {
|
||||||
|
if let content = proxy.content as? ParentView {
|
||||||
|
iterateAsRow(content.children)
|
||||||
|
} else {
|
||||||
|
WidgetView(build: { _ in
|
||||||
|
gtk_list_box_row_new()
|
||||||
|
}) {
|
||||||
|
proxy.content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PlainListStyle: ListStyleDeferredToRenderer {
|
||||||
|
public func sectionHeader<Header>(_ header: Header) -> AnyView where Header: View {
|
||||||
|
AnyView(
|
||||||
|
header
|
||||||
|
.font(.system(size: 17, weight: .medium))
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.padding(.leading)
|
||||||
|
.background(Color.listSectionHeader)
|
||||||
|
.frame(minWidth: 0, maxWidth: .infinity)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func sectionFooter<Footer>(_ footer: Footer) -> AnyView where Footer: View {
|
||||||
|
AnyView(
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Divider()
|
||||||
|
_ListRow.listRow(footer, self, isLast: true)
|
||||||
|
}
|
||||||
|
.padding(.leading)
|
||||||
|
.frame(minWidth: 0, maxWidth: .infinity)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func sectionBody<SectionBody>(_ section: SectionBody) -> AnyView where SectionBody: View {
|
||||||
|
// AnyView(section.padding(.leading).frame(minWidth: 0, maxWidth: .infinity))
|
||||||
|
AnyView(section)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func listRow<Row>(_ row: Row) -> AnyView where Row: View {
|
||||||
|
// AnyView(row.padding(.vertical))
|
||||||
|
AnyView(
|
||||||
|
WidgetView(build: { _ in
|
||||||
|
gtk_list_box_row_new()
|
||||||
|
}) {
|
||||||
|
row
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func listBody<ListBody>(_ content: ListBody) -> AnyView where ListBody: View {
|
||||||
|
AnyView(
|
||||||
|
WidgetView(build: { _ in
|
||||||
|
gtk_list_box_new()
|
||||||
|
}) {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
// Copyright 2020 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import CGTK
|
||||||
|
import TokamakCore
|
||||||
|
|
||||||
|
protocol GtkStackProtocol {}
|
||||||
|
|
||||||
|
// extension NavigationView: AnyWidget, ParentView, GtkStackProtocol {
|
||||||
|
// var expand: Bool { true }
|
||||||
|
|
||||||
|
// func new(_ application: UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget> {
|
||||||
|
// let box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0)!
|
||||||
|
// let stack = gtk_stack_new()!
|
||||||
|
// let sidebar = gtk_stack_sidebar_new()!
|
||||||
|
// sidebar.withMemoryRebound(to: GtkStackSidebar.self, capacity: 1) { reboundSidebar in
|
||||||
|
// stack.withMemoryRebound(to: GtkStack.self, capacity: 1) { reboundStack in
|
||||||
|
// gtk_stack_sidebar_set_stack(reboundSidebar, reboundStack)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// box.withMemoryRebound(to: GtkBox.self, capacity: 1) {
|
||||||
|
// gtk_box_pack_start($0, sidebar, gtk_true(), gtk_true(), 0)
|
||||||
|
// gtk_box_pack_start($0, stack, gtk_true(), gtk_true(), 0)
|
||||||
|
// }
|
||||||
|
// return box
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func update(widget: Widget) {}
|
||||||
|
|
||||||
|
// // public var deferredBody: AnyView {
|
||||||
|
// // AnyView(HTML("div", [
|
||||||
|
// // "class": "_tokamak-navigationview",
|
||||||
|
// // ]) {
|
||||||
|
// // _NavigationViewProxy(self).content
|
||||||
|
// // HTML("div", [
|
||||||
|
// // "class": "_tokamak-navigationview-content",
|
||||||
|
// // ]) {
|
||||||
|
// // _NavigationViewProxy(self).destination
|
||||||
|
// // }
|
||||||
|
// // })
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// public var children: [AnyView] {
|
||||||
|
// [AnyView(_NavigationViewProxy(self).content)]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
extension NavigationView: ViewDeferredToRenderer {
|
||||||
|
public var deferredBody: AnyView {
|
||||||
|
let proxy = _NavigationViewProxy(self)
|
||||||
|
return AnyView(HStack {
|
||||||
|
proxy.content
|
||||||
|
.environmentObject(proxy.context)
|
||||||
|
proxy.destination
|
||||||
|
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NavigationLink: ViewDeferredToRenderer {
|
||||||
|
public var deferredBody: AnyView {
|
||||||
|
let proxy = _NavigationLinkProxy(self)
|
||||||
|
return AnyView(Button(action: { proxy.activate() }) {
|
||||||
|
proxy.label
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extension NavigationLink: AnyWidget, ParentView {
|
||||||
|
// func new(_ application: UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget> {
|
||||||
|
// let btn = gtk_button_new()!
|
||||||
|
// bindAction(to: btn)
|
||||||
|
// return btn
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func bindAction(to btn: UnsafeMutablePointer<GtkWidget>) {
|
||||||
|
// btn.connect(signal: "clicked", closure: {
|
||||||
|
// _NavigationLinkProxy(self).activate()
|
||||||
|
// print("Activated")
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func update(widget: Widget) {
|
||||||
|
// if case let .widget(w) = widget.storage {
|
||||||
|
// w.disconnect(gtype: gtk_button_get_type(), signal: "clicked")
|
||||||
|
// bindAction(to: w)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public var children: [AnyView] {
|
||||||
|
// let proxy = _NavigationLinkProxy(self)
|
||||||
|
// print("Making label: \(proxy.label)")
|
||||||
|
// return [AnyView(proxy.label)]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// extension NavigationLink: AnyWidget, ParentView {
|
||||||
|
// func new(_ application: UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget> {
|
||||||
|
// print("Creating NavLink widget")
|
||||||
|
// let btn = gtk_button_new()!
|
||||||
|
// bindAction(to: btn)
|
||||||
|
// return btn
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func update(widget: Widget) {
|
||||||
|
// if case let .widget(w) = widget.storage {
|
||||||
|
// w.disconnect(gtype: gtk_button_get_type(), signal: "clicked")
|
||||||
|
// bindAction(to: w)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func bindAction(to btn: UnsafeMutablePointer<GtkWidget>) {
|
||||||
|
// btn.connect(signal: "clicked", closure: {
|
||||||
|
// _NavigationLinkProxy(self).activate()
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public var children: [AnyView] {
|
||||||
|
// [AnyView(_NavigationLinkProxy(self).label)]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// extension NavigationLink: ViewDeferredToRenderer {
|
||||||
|
// public var deferredBody: AnyView {
|
||||||
|
// let proxy = _NavigationLinkProxy(self)
|
||||||
|
// print("Selected: \(proxy.isSelected)")
|
||||||
|
// return AnyView(Button {
|
||||||
|
// proxy.activate()
|
||||||
|
// } label: {
|
||||||
|
// proxy.label
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
|
@ -0,0 +1,73 @@
|
||||||
|
// Copyright 2020 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import CGTK
|
||||||
|
import TokamakCore
|
||||||
|
|
||||||
|
extension _PickerContainer: AnyWidget {
|
||||||
|
func new(_ application: UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget> {
|
||||||
|
let comboBox = gtk_combo_box_text_new()!
|
||||||
|
comboBox.withMemoryRebound(to: GtkComboBoxText.self, capacity: 1) { gtkComboBox in
|
||||||
|
for element in elements {
|
||||||
|
if let text = mapAnyView(element.anyContent, transform: { (view: Text) in view }) {
|
||||||
|
gtk_combo_box_text_append_text(gtkComboBox, _TextProxy(text).rawText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateSelection(of: comboBox)
|
||||||
|
setupSignal(for: comboBox)
|
||||||
|
return comboBox
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(widget: Widget) {
|
||||||
|
if case let .widget(comboBox) = widget.storage {
|
||||||
|
comboBox.disconnect(gtype: gtk_combo_box_text_get_type(), signal: "changed")
|
||||||
|
updateSelection(of: comboBox)
|
||||||
|
setupSignal(for: comboBox)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSelection(of comboBox: UnsafeMutablePointer<GtkWidget>) {
|
||||||
|
comboBox.withMemoryRebound(to: GtkComboBox.self, capacity: 1) {
|
||||||
|
guard let activeElement = elements.firstIndex(where: {
|
||||||
|
guard let selectedValue = $0.anyId as? SelectionValue else { return false }
|
||||||
|
return selectedValue == selection
|
||||||
|
}) else { return }
|
||||||
|
gtk_combo_box_set_active($0, Int32(activeElement))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupSignal(for comboBox: UnsafeMutablePointer<GtkWidget>) {
|
||||||
|
comboBox.connect(signal: "changed") { box in
|
||||||
|
box?.withMemoryRebound(to: GtkComboBox.self, capacity: 1) { plainComboBox in
|
||||||
|
if gtk_combo_box_get_active(plainComboBox) != 0 {
|
||||||
|
plainComboBox.withMemoryRebound(to: GtkComboBoxText.self, capacity: 1) { comboBoxText in
|
||||||
|
let activeElement = gtk_combo_box_text_get_active_text(comboBoxText)!
|
||||||
|
defer {
|
||||||
|
g_free(activeElement)
|
||||||
|
}
|
||||||
|
let element = elements.first {
|
||||||
|
guard let text = mapAnyView($0.anyContent, transform: { (view: Text) in view })
|
||||||
|
else { return false }
|
||||||
|
return _TextProxy(text).rawText == String(cString: activeElement)
|
||||||
|
}
|
||||||
|
if let selectedValue = element?.anyId as? SelectionValue {
|
||||||
|
selection = selectedValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright 2020 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Created by Carson Katri on 10/13/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CGTK
|
||||||
|
import TokamakCore
|
||||||
|
|
||||||
|
extension ScrollView: ViewDeferredToRenderer {
|
||||||
|
public var deferredBody: AnyView {
|
||||||
|
AnyView(WidgetView(build: { _ in
|
||||||
|
gtk_scrolled_window_new(nil, nil)
|
||||||
|
}) {
|
||||||
|
if children.count > 1 {
|
||||||
|
VStack {
|
||||||
|
ForEach(Array(children.enumerated()), id: \.offset) { _, view in
|
||||||
|
view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ForEach(Array(children.enumerated()), id: \.offset) { _, view in
|
||||||
|
view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
// Copyright 2020 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Created by Carson Katri on 10/10/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CGTK
|
||||||
|
import Foundation
|
||||||
|
import TokamakCore
|
||||||
|
|
||||||
|
protocol StackProtocol {
|
||||||
|
var alignment: Alignment { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Box<Content: View>: View, ParentView, AnyWidget, StackProtocol {
|
||||||
|
let content: Content
|
||||||
|
let orientation: GtkOrientation
|
||||||
|
let spacing: TokamakCore.CGFloat
|
||||||
|
let alignment: Alignment
|
||||||
|
|
||||||
|
let expand = true
|
||||||
|
|
||||||
|
func new(_ application: UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget> {
|
||||||
|
let grid = gtk_grid_new()!
|
||||||
|
gtk_orientable_set_orientation(OpaquePointer(grid), orientation)
|
||||||
|
grid.withMemoryRebound(to: GtkGrid.self, capacity: 1) {
|
||||||
|
gtk_grid_set_row_spacing($0, UInt32(spacing))
|
||||||
|
gtk_grid_set_column_spacing($0, UInt32(spacing))
|
||||||
|
}
|
||||||
|
return grid
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(widget: Widget) {}
|
||||||
|
|
||||||
|
var body: Never {
|
||||||
|
neverBody("Box")
|
||||||
|
}
|
||||||
|
|
||||||
|
public var children: [AnyView] {
|
||||||
|
[AnyView(content)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VStack: ViewDeferredToRenderer {
|
||||||
|
public var deferredBody: AnyView {
|
||||||
|
AnyView(
|
||||||
|
Box(
|
||||||
|
content: content,
|
||||||
|
orientation: GTK_ORIENTATION_VERTICAL,
|
||||||
|
spacing: spacing ?? 8,
|
||||||
|
alignment: .init(horizontal: alignment, vertical: .center)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HStack: ViewDeferredToRenderer {
|
||||||
|
public var deferredBody: AnyView {
|
||||||
|
AnyView(
|
||||||
|
Box(
|
||||||
|
content: content,
|
||||||
|
orientation: GTK_ORIENTATION_HORIZONTAL,
|
||||||
|
spacing: spacing ?? 8,
|
||||||
|
alignment: .init(horizontal: .center, vertical: alignment)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HorizontalAlignment {
|
||||||
|
var gtkValue: GtkAlign {
|
||||||
|
switch self {
|
||||||
|
case .center: return GTK_ALIGN_CENTER
|
||||||
|
case .leading: return GTK_ALIGN_START
|
||||||
|
case .trailing: return GTK_ALIGN_END
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VerticalAlignment {
|
||||||
|
var gtkValue: GtkAlign {
|
||||||
|
switch self {
|
||||||
|
case .center: return GTK_ALIGN_CENTER
|
||||||
|
case .top: return GTK_ALIGN_START
|
||||||
|
case .bottom: return GTK_ALIGN_END
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
// Copyright 2020 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Created by Carson Katri on 10/10/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CGTK
|
||||||
|
import Foundation
|
||||||
|
import TokamakCore
|
||||||
|
|
||||||
|
extension Text: AnyWidget {
|
||||||
|
func new(_ application: UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget> {
|
||||||
|
let proxy = _TextProxy(self)
|
||||||
|
return gtk_label_new(proxy.rawText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(widget: Widget) {
|
||||||
|
if case let .widget(w) = widget.storage {
|
||||||
|
w.withMemoryRebound(to: GtkLabel.self, capacity: 1) {
|
||||||
|
gtk_label_set_text($0, _TextProxy(self).rawText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
// Copyright 2020 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import CGTK
|
||||||
|
import TokamakCore
|
||||||
|
|
||||||
|
protocol AnyWidget {
|
||||||
|
var expand: Bool { get }
|
||||||
|
func new(_ application: UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget>
|
||||||
|
func update(widget: Widget)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AnyWidget {
|
||||||
|
var expand: Bool { false }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WidgetView<Content: View>: View, AnyWidget, ParentView {
|
||||||
|
let build: (UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget>
|
||||||
|
let content: Content
|
||||||
|
let expand: Bool
|
||||||
|
|
||||||
|
init(build: @escaping (UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget>,
|
||||||
|
expand: Bool = false,
|
||||||
|
@ViewBuilder content: () -> Content)
|
||||||
|
{
|
||||||
|
self.build = build
|
||||||
|
self.expand = expand
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
func new(_ application: UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget> {
|
||||||
|
build(application)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(widget: Widget) {
|
||||||
|
// Rebuild from scratch
|
||||||
|
if case let .widget(w) = widget.storage {
|
||||||
|
widget.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: Never {
|
||||||
|
neverBody("WidgetView")
|
||||||
|
}
|
||||||
|
|
||||||
|
var children: [AnyView] {
|
||||||
|
[AnyView(content)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension WidgetView where Content == EmptyView {
|
||||||
|
init(build: @escaping (UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget>,
|
||||||
|
expand: Bool = false)
|
||||||
|
{
|
||||||
|
self.init(build: build, expand: expand) { EmptyView() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Widget: Target {
|
||||||
|
enum Storage {
|
||||||
|
case application(UnsafeMutablePointer<GtkApplication>)
|
||||||
|
case widget(UnsafeMutablePointer<GtkWidget>)
|
||||||
|
}
|
||||||
|
|
||||||
|
let storage: Storage
|
||||||
|
var view: AnyView
|
||||||
|
|
||||||
|
/*
|
||||||
|
let window: UnsafeMutablePointer<GtkWidget>
|
||||||
|
window = gtk_application_window_new(app)
|
||||||
|
label = gtk_label_new("Hello GNOME!")
|
||||||
|
window.withMemoryRebound(to: GtkContainer.self, capacity: 1) {
|
||||||
|
gtk_container_add($0, label)
|
||||||
|
}
|
||||||
|
window.withMemoryRebound(to: GtkWindow.self, capacity: 1) {
|
||||||
|
gtk_window_set_title($0, "Welcome to GNOME")
|
||||||
|
gtk_window_set_default_size($0, 200, 100)
|
||||||
|
}
|
||||||
|
gtk_widget_show_all(window)
|
||||||
|
*/
|
||||||
|
|
||||||
|
init<V: View>(_ view: V, _ ref: UnsafeMutablePointer<GtkWidget>) {
|
||||||
|
storage = .widget(ref)
|
||||||
|
self.view = AnyView(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ ref: UnsafeMutablePointer<GtkWidget>) {
|
||||||
|
storage = .widget(ref)
|
||||||
|
view = AnyView(EmptyView())
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ ref: UnsafeMutablePointer<GtkApplication>) {
|
||||||
|
storage = .application(ref)
|
||||||
|
view = AnyView(EmptyView())
|
||||||
|
}
|
||||||
|
|
||||||
|
func destroy() {
|
||||||
|
switch storage {
|
||||||
|
case .application:
|
||||||
|
fatalError("Attempt to destroy root Application.")
|
||||||
|
case let .widget(widget):
|
||||||
|
gtk_widget_destroy(widget)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
#include <termios.h>
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
gboolean tokamak_gtk_widget_is_container(GtkWidget *widget);
|
||||||
|
gboolean tokamak_gtk_widget_is_stack(GtkWidget *widget);
|
|
@ -0,0 +1,9 @@
|
||||||
|
#include "type_check.h"
|
||||||
|
|
||||||
|
gboolean tokamak_gtk_widget_is_container(GtkWidget *widget) {
|
||||||
|
return GTK_IS_CONTAINER(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean tokamak_gtk_widget_is_stack(GtkWidget *widget) {
|
||||||
|
return GTK_IS_STACK(widget);
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
// Copyright 2020 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Created by Carson Katri on 10/10/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import TokamakGTK
|
||||||
|
|
||||||
|
struct Counter: View {
|
||||||
|
@State private var count: Int = 0
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
Text("\(count)")
|
||||||
|
HStack {
|
||||||
|
Button("Decrement") { count -= 1 }
|
||||||
|
Button("Increment") { count += 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PickerDemo: View {
|
||||||
|
@State private var chosenValue: Int = 3
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
Text("Chose \(chosenValue)")
|
||||||
|
Picker("Choose", selection: $chosenValue) {
|
||||||
|
ForEach(0..<5) {
|
||||||
|
Text("\($0)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TokamakGTKDemo: App {
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup("Test Scene") {
|
||||||
|
List {
|
||||||
|
Counter()
|
||||||
|
PickerDemo()
|
||||||
|
ForEach(1..<100) {
|
||||||
|
Text("Item #\($0)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TokamakGTKDemo.main()
|
|
@ -16,4 +16,6 @@
|
||||||
@_exported import SwiftUI
|
@_exported import SwiftUI
|
||||||
#elseif os(WASI)
|
#elseif os(WASI)
|
||||||
@_exported import TokamakDOM
|
@_exported import TokamakDOM
|
||||||
|
#elseif os(Linux)
|
||||||
|
@_exported import TokamakGTK
|
||||||
#endif
|
#endif
|
||||||
|
|
Loading…
Reference in New Issue