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:
Max Desiatov 2020-12-26 16:11:06 +00:00 committed by GitHub
parent 8e5ad7f67f
commit bd38866cb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1608 additions and 19 deletions

View File

@ -15,7 +15,7 @@ jobs:
with:
shell-action: carton bundle --product TokamakDemo
macos_build:
core_macos_build:
runs-on: macos-11.0
steps:
@ -29,6 +29,8 @@ jobs:
swift build --product TokamakPackageTests
`xcrun --find xctest` .build/debug/TokamakPackageTests.xctest
rm -rf Sources/TokamakGTKCHelpers/*.c
xcodebuild -version
# 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' \
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
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

17
.vscode/launch.json vendored Normal file
View File

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

22
.vscode/tasks.json vendored
View File

@ -17,6 +17,26 @@
"label": "carton dev",
"type": "shell",
"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
}
}
]
}
}

12
Makefile Executable file
View File

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

View File

@ -21,10 +21,6 @@ let package = Package(
name: "TokamakDOM",
targets: ["TokamakDOM"]
),
.library(
name: "TokamakShim",
targets: ["TokamakShim"]
),
.library(
name: "TokamakStaticHTML",
targets: ["TokamakStaticHTML"]
@ -33,6 +29,18 @@ let package = Package(
name: "TokamakStaticDemo",
targets: ["TokamakStaticDemo"]
),
.library(
name: "TokamakGTK",
targets: ["TokamakGTK"]
),
.executable(
name: "TokamakGTKDemo",
targets: ["TokamakGTKDemo"]
),
.library(
name: "TokamakShim",
targets: ["TokamakShim"]
),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
@ -62,6 +70,34 @@ let package = Package(
name: "TokamakCore",
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(
name: "TokamakStaticHTML",
dependencies: [
@ -82,10 +118,6 @@ let package = Package(
),
]
),
.target(
name: "TokamakShim",
dependencies: [.target(name: "TokamakDOM", condition: .when(platforms: [.wasi]))]
),
.target(
name: "TokamakDemo",
dependencies: [

View File

@ -0,0 +1 @@
#include <gtk/gtk.h>

View File

@ -0,0 +1,8 @@
module CGTK {
header "./termios-Header.h"
header "./CGTK-Bridging-Header.h"
link "gtk-3"
export *
}

View File

@ -0,0 +1 @@
#include <termios.h>

View File

@ -23,7 +23,7 @@ public struct _AnyApp: App {
let bodyClosure: (Any) -> _AnyScene
let bodyType: Any.Type
init<A: App>(_ app: A) {
public init<A: App>(_ app: A) {
self.app = app
type = A.self
// swiftlint:disable:next force_cast

View File

@ -103,12 +103,14 @@ public extension EnvironmentValues {
public protocol _AnyIDView {
var anyId: AnyHashable { get }
var anyContent: AnyView { get }
}
struct IDView<Content, ID>: View, _AnyIDView where Content: View, ID: Hashable {
let content: Content
let id: ID
var anyId: AnyHashable { AnyHashable(id) }
var anyContent: AnyView { AnyView(content) }
init(_ content: Content, id: ID) {
self.content = content

View File

@ -80,13 +80,16 @@ public struct _NavigationLinkProxy<Label, Destination> where Label: View, Destin
self.subject = subject
}
public var label: AnyView {
public var label: some View {
subject.style.makeBody(configuration: .init(
body: AnyView(subject.label),
isSelected: isSelected
))
// subject.label
}
public var context: NavigationContext { subject.navigationContext }
public var style: _AnyNavigationLinkStyle { subject.style }
public var isSelected: Bool {
subject.destination === subject.navigationContext.destination

View File

@ -15,7 +15,7 @@
// Created by Jed Fox on 06/30/2020.
//
final class NavigationContext: ObservableObject {
public final class NavigationContext: ObservableObject {
@Published var destination = NavigationLinkDestination(EmptyView())
}
@ -39,14 +39,16 @@ public struct _NavigationViewProxy<Content: View> {
public init(_ subject: NavigationView<Content>) { self.subject = subject }
public var context: NavigationContext { subject.context }
public var content: some View {
subject.content
.environmentObject(subject.context)
.environmentObject(context)
}
public var destination: some View {
subject.context.destination.view
.environmentObject(subject.context)
.environmentObject(context)
}
}

View File

@ -12,19 +12,28 @@
// See the License for the specific language governing permissions and
// 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
public let label: Label
public let content: Content
public let elements: [_AnyIDView]
@Environment(\.pickerStyle) public var style
public init(
selection: Binding<SelectionValue>,
label: Label,
elements: [_AnyIDView],
@ViewBuilder content: () -> Content
) {
_selection = selection
self.label = label
self.elements = elements
self.content = content()
}
@ -61,7 +70,7 @@ public struct Picker<Label: View, SelectionValue: Hashable, Content: View>: View
public var body: some View {
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`
// and its `Data.Element` type is the same as `SelectionValue` type, then we can
// update the binding.
@ -100,3 +109,14 @@ extension Picker: ParentView {
(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) ?? []
}
}

View File

@ -52,11 +52,11 @@ struct NavItem: View {
Text(id)
#elseif os(macOS)
Text(id).opacity(0.5)
#else
#elseif os(Linux)
HStack {
Text(id)
Spacer()
Text("unavailable").opacity(0.5)
Text("unavailable")
}
#endif
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,4 +16,6 @@
@_exported import SwiftUI
#elseif os(WASI)
@_exported import TokamakDOM
#elseif os(Linux)
@_exported import TokamakGTK
#endif