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:
|
||||
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
|
||||
|
|
|
@ -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",
|
||||
"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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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",
|
||||
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: [
|
||||
|
|
|
@ -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 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) ?? []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
#elseif os(WASI)
|
||||
@_exported import TokamakDOM
|
||||
#elseif os(Linux)
|
||||
@_exported import TokamakGTK
|
||||
#endif
|
||||
|
|
Loading…
Reference in New Issue