diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e5a75d2..71f08f21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..68068150 --- /dev/null +++ b/.vscode/launch.json @@ -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}" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index c058e2c6..c583e6b5 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -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 + } } ] -} +} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100755 index 00000000..15cd74aa --- /dev/null +++ b/Makefile @@ -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 diff --git a/Package.swift b/Package.swift index 0a7a20f8..da974886 100644 --- a/Package.swift +++ b/Package.swift @@ -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: [ diff --git a/Sources/CGTK/CGTK-Bridging-Header.h b/Sources/CGTK/CGTK-Bridging-Header.h new file mode 100644 index 00000000..74a31b23 --- /dev/null +++ b/Sources/CGTK/CGTK-Bridging-Header.h @@ -0,0 +1 @@ +#include \ No newline at end of file diff --git a/Sources/CGTK/module.modulemap b/Sources/CGTK/module.modulemap new file mode 100644 index 00000000..db09f743 --- /dev/null +++ b/Sources/CGTK/module.modulemap @@ -0,0 +1,8 @@ +module CGTK { + header "./termios-Header.h" + header "./CGTK-Bridging-Header.h" + + link "gtk-3" + + export * +} \ No newline at end of file diff --git a/Sources/CGTK/termios-Header.h b/Sources/CGTK/termios-Header.h new file mode 100644 index 00000000..497e4058 --- /dev/null +++ b/Sources/CGTK/termios-Header.h @@ -0,0 +1 @@ +#include \ No newline at end of file diff --git a/Sources/TokamakCore/App/_AnyApp.swift b/Sources/TokamakCore/App/_AnyApp.swift index 68422e91..488cf2f2 100644 --- a/Sources/TokamakCore/App/_AnyApp.swift +++ b/Sources/TokamakCore/App/_AnyApp.swift @@ -23,7 +23,7 @@ public struct _AnyApp: App { let bodyClosure: (Any) -> _AnyScene let bodyType: Any.Type - init(_ app: A) { + public init(_ app: A) { self.app = app type = A.self // swiftlint:disable:next force_cast diff --git a/Sources/TokamakCore/Views/Containers/ForEach.swift b/Sources/TokamakCore/Views/Containers/ForEach.swift index f075afc0..fa97cd58 100644 --- a/Sources/TokamakCore/Views/Containers/ForEach.swift +++ b/Sources/TokamakCore/Views/Containers/ForEach.swift @@ -103,12 +103,14 @@ public extension EnvironmentValues { public protocol _AnyIDView { var anyId: AnyHashable { get } + var anyContent: AnyView { get } } struct IDView: 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 diff --git a/Sources/TokamakCore/Views/Navigation/NavigationLink.swift b/Sources/TokamakCore/Views/Navigation/NavigationLink.swift index 219a11b5..b5e86be3 100644 --- a/Sources/TokamakCore/Views/Navigation/NavigationLink.swift +++ b/Sources/TokamakCore/Views/Navigation/NavigationLink.swift @@ -80,13 +80,16 @@ public struct _NavigationLinkProxy 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 diff --git a/Sources/TokamakCore/Views/Navigation/NavigationView.swift b/Sources/TokamakCore/Views/Navigation/NavigationView.swift index c33b9044..f673a75e 100644 --- a/Sources/TokamakCore/Views/Navigation/NavigationView.swift +++ b/Sources/TokamakCore/Views/Navigation/NavigationView.swift @@ -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 { public init(_ subject: NavigationView) { 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) } } diff --git a/Sources/TokamakCore/Views/Selectors/Picker.swift b/Sources/TokamakCore/Views/Selectors/Picker.swift index 99ca5095..05887b8b 100644 --- a/Sources/TokamakCore/Views/Selectors/Picker.swift +++ b/Sources/TokamakCore/Views/Selectors/Picker.swift @@ -12,19 +12,28 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct _PickerContainer: View { +public protocol _PickerContainerProtocol { + var elements: [_AnyIDView] { get } +} + +public struct _PickerContainer: 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, 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: 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) ?? [] + } +} diff --git a/Sources/TokamakDemo/TokamakDemo.swift b/Sources/TokamakDemo/TokamakDemo.swift index 633cae10..6ea74106 100644 --- a/Sources/TokamakDemo/TokamakDemo.swift +++ b/Sources/TokamakDemo/TokamakDemo.swift @@ -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 } diff --git a/Sources/TokamakGTK/App/App.swift b/Sources/TokamakGTK/App/App.swift new file mode 100644 index 00000000..fa000b4e --- /dev/null +++ b/Sources/TokamakGTK/App/App.swift @@ -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 { + CurrentValueSubject(.active).eraseToAnyPublisher() + } + + var _colorSchemePublisher: AnyPublisher { + CurrentValueSubject(.light).eraseToAnyPublisher() + } +} + +extension UnsafeMutablePointer where Pointee == GApplication { + @discardableResult + func connect( + signal: UnsafePointer, + data: UnsafeMutableRawPointer? = nil, + handler: @convention(c) @escaping (UnsafeMutablePointer?, 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, + closure: @escaping () -> () + ) -> Int { + let closureBox = Unmanaged.passRetained(ClosureBox(closure)).toOpaque() + return connect(signal: signal, data: closureBox) { _, closureBox in + let unpackedAction = Unmanaged>.fromOpaque(closureBox) + unpackedAction.takeRetainedValue().closure() + return true + } + } +} diff --git a/Sources/TokamakGTK/Core.swift b/Sources/TokamakGTK/Core.swift new file mode 100644 index 00000000..accfdc55 --- /dev/null +++ b/Sources/TokamakGTK/Core.swift @@ -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) + } +} diff --git a/Sources/TokamakGTK/GSignal.swift b/Sources/TokamakGTK/GSignal.swift new file mode 100644 index 00000000..318c470b --- /dev/null +++ b/Sources/TokamakGTK/GSignal.swift @@ -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, + data: gpointer? = nil, + handler: @convention(c) @escaping (UnsafeMutablePointer?, 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, + closure: @escaping () -> () + ) -> Int { + let closureBox = Unmanaged.passRetained(ClosureBox(closure)).toOpaque() + return connect(signal: signal, data: closureBox, handler: { _, closureBox in + let unpackedAction = Unmanaged>.fromOpaque(closureBox) + unpackedAction.takeUnretainedValue().closure() + return true + }, destroy: { closureBox, _ in + let unpackedAction = Unmanaged>.fromOpaque(closureBox) + unpackedAction.release() + }) + } + + /// Connect with a context-capturing closure (with the GtkWidget passed through) + @discardableResult + func connect( + signal: UnsafePointer, + closure: @escaping (UnsafeMutablePointer?) -> () + ) -> Int { + let closureBox = Unmanaged.passRetained(SingleParamClosureBox(closure)).retain().toOpaque() + return connect(signal: signal, data: closureBox, handler: { widget, closureBox in + let unpackedAction = Unmanaged?, ()>> + .fromOpaque(closureBox) + if let widget = widget { + unpackedAction.takeUnretainedValue().closure(widget) + } + return true + }, destroy: { closureBox, _ in + let unpackedAction = Unmanaged?, ()>> + .fromOpaque(closureBox) + unpackedAction.release() + }) + } + + func disconnect( + gtype: GType, + signal: UnsafePointer + ) { + // 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 { + let closure: () -> U + + init(_ closure: @escaping () -> U) { self.closure = closure } +} + +final class SingleParamClosureBox { + let closure: (T) -> U + + init(_ closure: @escaping (T) -> U) { self.closure = closure } +} diff --git a/Sources/TokamakGTK/GTKRenderer.swift b/Sources/TokamakGTK/GTKRenderer.swift new file mode 100644 index 00000000..c608289f --- /dev/null +++ b/Sources/TokamakGTK/GTKRenderer.swift @@ -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? + private var gtkAppRef: UnsafeMutablePointer + static var sharedWindow: UnsafeMutablePointer! + + init( + _ 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 + 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 + 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() + } +} diff --git a/Sources/TokamakGTK/GtkContainer+forEach.swift b/Sources/TokamakGTK/GtkContainer+forEach.swift new file mode 100644 index 00000000..a1253c44 --- /dev/null +++ b/Sources/TokamakGTK/GtkContainer+forEach.swift @@ -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?) -> () + ) { + let closureBox = Unmanaged.passRetained(SingleParamClosureBox(closure)).toOpaque() + let handler: @convention(c) (UnsafeMutablePointer?, UnsafeRawPointer) + -> Bool = { (ref: UnsafeMutablePointer?, data: UnsafeRawPointer) -> Bool in + let unpackedAction = Unmanaged?, ()>> + .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() + } +} diff --git a/Sources/TokamakGTK/Modifiers/LayoutModifiers.swift b/Sources/TokamakGTK/Modifiers/LayoutModifiers.swift new file mode 100644 index 00000000..431aa67c --- /dev/null +++ b/Sources/TokamakGTK/Modifiers/LayoutModifiers.swift @@ -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) { + 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) { + 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)) + } +} diff --git a/Sources/TokamakGTK/Modifiers/WidgetModifier.swift b/Sources/TokamakGTK/Modifiers/WidgetModifier.swift new file mode 100644 index 00000000..046481d4 --- /dev/null +++ b/Sources/TokamakGTK/Modifiers/WidgetModifier.swift @@ -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) +} + +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) + } +} diff --git a/Sources/TokamakGTK/Scenes/SceneContainerView.swift b/Sources/TokamakGTK/Scenes/SceneContainerView.swift new file mode 100644 index 00000000..03fe7eca --- /dev/null +++ b/Sources/TokamakGTK/Scenes/SceneContainerView.swift @@ -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: View, AnyWidget { + let content: Content + + var body: Never { + neverBody("SceneContainerView") + } + + func new(_ application: UnsafeMutablePointer) -> UnsafeMutablePointer { + print("Making window") + let window: UnsafeMutablePointer + 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) {} +} diff --git a/Sources/TokamakGTK/Scenes/WindowGroup.swift b/Sources/TokamakGTK/Scenes/WindowGroup.swift new file mode 100644 index 00000000..92e31829 --- /dev/null +++ b/Sources/TokamakGTK/Scenes/WindowGroup.swift @@ -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)) + } +} diff --git a/Sources/TokamakGTK/Tokens/BuiltinColors.swift b/Sources/TokamakGTK/Tokens/BuiltinColors.swift new file mode 100644 index 00000000..1fee3e95 --- /dev/null +++ b/Sources/TokamakGTK/Tokens/BuiltinColors.swift @@ -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) + } + } + } +} diff --git a/Sources/TokamakGTK/Views/Button.swift b/Sources/TokamakGTK/Views/Button.swift new file mode 100644 index 00000000..def8f641 --- /dev/null +++ b/Sources/TokamakGTK/Views/Button.swift @@ -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) -> UnsafeMutablePointer { + 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) { + btn.connect(signal: "clicked", closure: action) + } + + public var children: [AnyView] { + [AnyView(label)] + } +} diff --git a/Sources/TokamakGTK/Views/List.swift b/Sources/TokamakGTK/Views/List.swift new file mode 100644 index 00000000..58d944af --- /dev/null +++ b/Sources/TokamakGTK/Views/List.swift @@ -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) -> 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