VertexGUI 是一个 Swift 框架,用于编写跨平台的 GUI 应用程序。
VertexGUI 使用 Skia 2D 渲染引擎来绘制 Widgets,并使用 Fireblade 游戏引擎的一部分来管理多个平台上的窗口。
目前支持 Linux、MacOS 和 Windows。 Skia 支持更多平台:Android、iOS、ChromeOS。 因此,VertexGUI 也可以在不太多的工作量下支持这些平台。
要运行演示应用程序,请按照下面的安装说明,克隆存储库,然后在根目录中执行 swift run TaskOrganizerDemo
。
演示应用程序的代码可以在 Sources/TaskOrganizerDemo 中找到
VertexGUI 依赖 SDL2 来创建窗口并接收键盘和鼠标事件。 SDL2 需要以二进制文件的形式存在于您的系统中。 设置 SDL2 最方便的方法是使用您平台的包管理器
在 Ubuntu 上,使用以下命令安装它
sudo apt-get install libsdl2-dev
在 MacOS 上(通过 homebrew)
brew install sdl2
对于其他平台,请参阅:安装 SDL。
Skia 是用于绘制 VertexGUI 小部件的 2D 图形库。 它也需要以二进制形式存在。
要下载预构建的二进制文件或自己构建 skia,请按照为 SkiaKit 编写的说明进行操作(SkiaKit 是 Skia c++ API 的包装库)。
这个项目正在紧张的开发中。 在 API 稳定之前,我不会创建任何版本。
只需使用 master 分支
dependencies: [
...,
.package(name: "VertexGUI", url: "https://github.com/VertexUI/VertexGUI", .branch("master")),
],
targets: [
...,
.target(name: "SomeTarget", dependencies: ["VertexGUI", ...])
]
需要 Swift 5.5 工具链。
结果
import VertexGUI
public class MainView: ComposedWidget {
@State
private var counter = 0
@Compose override public var content: ComposedContent {
Container().with(classes: ["container"]).withContent { [unowned self] in
Button().onClick {
counter += 1
}.withContent {
Text(ImmutableBinding($counter.immutable, get: { "counter: \($0)" }))
}
}
}
// you can define themes, so this can also be done in three lines
override public var style: Style {
let primaryColor = Color(77, 255, 154, 255)
return Style("&") {
(\.$background, Color(10, 20, 30, 255))
} nested: {
Style(".container", Container.self) {
(\.$alignContent, .center)
(\.$justifyContent, .center)
}
Style("Button") {
(\.$padding, Insets(all: 16))
(\.$background, primaryColor)
(\.$foreground, .black)
(\.$fontWeight, .bold)
} nested: {
Style("&:hover") {
(\.$background, primaryColor.darkened(20))
}
Style("&:active") {
(\.$background, primaryColor.darkened(40))
}
}
}
}
}
当您按下按钮时,计数器应递增。
需要一些额外的设置代码才能显示窗口。 您可以在 Sources/MinimalDemo 中找到所有代码
使用 Swift 的 函数/结果构建器。
Container().withContent {
Button().withContent {
Text("Hello World")
$0.iconSlot {
Icon(identifier: .party)
}
}
}
List(items).withContent {
$0.itemSlot { itemData in
Text(itemData)
}
}
创建由多个 Widgets 组成的可重用视图。 通过使用 slots 将子 Widgets 传递给您的自定义 Widget 实例。 组合 API 的某些部分将来可能会重命名。
class MyCustomView: ComposedWidget, SlotAcceptingWidgetProtocol {
static let childSlot = Slot(key: "child", data: String.self)
let childSlotManager = SlotContentManager(MyCustomView.childSlot)
@Compose override var content: ComposedContent {
Container().withContent {
Text("some text 1")
childSlotManager("the data passed to the child slot definition")
Button().withContent {
Text("this Text Widget goes to the default slot of the Button")
}
}
}
}
// use your custom Widget
Container() {
Text("any other place in your code")
MyCustomView().withContent {
$0.childSlot { data in
// this Text Widget will receive the String
// passed to the childSlotManager() call above
Text(data)
}
}
}
LeafWidgets 直接绘制到屏幕上。 它们没有子项。
class MyCustomLeafWidget: LeafWidget {
override func draw(_ drawingContext: DrawingContext) {
drawingContext.drawRect(rect: ..., paint: Paint(color: ..., strokeWidth: ...))
drawingContext.drawLine(...)
drawingContext.drawText(...)
}
}
Container().with(classes: ["container"]) {
Button().withContent {
Text("Hello World")
}
}
// select by class
Style(".container") {
(\.$background, .white)
// foreground is similar to color in css, color of text = foreground
(\.$foreground, Color(120, 40, 0, 255))
} nested: {
// select by Widget type
Style("Text") {
// inherit is the default for foreground, so this is not necessary
(\.$foreground, .inherit)
(\.$fontWeight, .bold)
}
// & references the parent style, in this case .container and extends it
// the currently supported pseudo classes are :hover and :active
Style("&:hover") {
(\.$background, .black)
}
}
class MyCustomWidget {
...
@StyleProperty
public var myCustomStyleProperty: Double = 0.0
...
}
// somewhere else in your code
Style(".class-applied-to-my-custom-widget") {
(\.$myCustomStyleProperty, 1.0)
}
当数据更改时,更新您的 Widgets 的内容和结构。
class MyCustomWidget: ComposedWidget {
@State private var someState: Int = 0
@ImmutableBinding private var someStateFromTheOutside: String
public init(_ outsideStateBinding: ImmutableBinding<String>) {
self._someStateFromTheOutside = outSideStateBinding
}
@Compose override var content: ComposedContent {
Container().withContent { [unowned self] in
// use Dynamic for changing the structure of a Widget
Dynamic($someState) {
if someState == 0 {
Button().onClick {
someState += 1
}.withContent {
Text("change someState")
}
} else {
Text("someState is not 0")
}
}
// pass a Binding to a child to have it always reflect the latest state
Text($someStateFromTheOutside.immutable)
// you can construct proxy bindings
// in this case the proxy converts the Int property to a String
Text(ImmutableBinding($someState.immutable, get: { String($0) }))
}
}
}
应该更改此设置,以便也可以使用属性包装器来提供依赖项。 通过比较键(如果给定)和类型来解析依赖项。
class MyCustomWidget: ComposedWidget {
...
@Inject(key: <nil or a String>) private var myDependency: String
}
class MyCustomParentWidget: ComposedWidget {
// API will be changed, so that this dependency can be provided by doing:
// @Provide(key: <nil or a String>)
let providedDependency: String = "dependency"
@Compose override var content: ComposedContent {
Container().withContent {
MyCustomWidget()
}.provide(dependencies: providedDependency)
}
}
该方法类似于 Vuex。 将 mutations 和 actions 定义为 enum case 而不是方法允许自动记录在何处以及何时对状态进行了哪些更改。
class MyAppStore: Store<MyAppState, MyAppMutation, MyAppAction> {
init() {
super.init(initialState: MyAppState(
stateProperty1: "initial"))
}
override func perform(mutation: Mutation, state: SetterProxy) {
switch mutation {
case let .setStateProperty1(value):
state.stateProperty1 = value
}
}
override func perform(action: Action) {
switch action {
case .doSomeAsynchronousOperation:
// ... do stuff
// when finished:
commit(.setStateProperty1(resultOfOperation))
}
}
}
struct MyAppState {
var stateProperty1: String
}
enum MyAppMutation {
case .setStateProperty1(String)
}
enum MyAppAction {
case .doSomeAsynchronousOperation
}
现在您可以在整个应用程序中使用 store,如下所示
class TheRootView: ComposedWidget {
let store = MyAppStore()
@Compose override var content: ComposedContent {
Container().provide(dependencies: store).withContent {
...
// can be deeply nested
MyCustomWidget()
...
}
}
}
class MyCustomWidget: ComposedWidget {
@Inject var store: MyAppStore
@Compose override var content: ComposedContent {
Container().withContent { [unowned self] in
// the store exposes reactive bindings
// to every state property via store.$state
Text(store.$state.stateProperty1.immutable)
Dynamic(store.$state.stateProperty1) {
// ... everything inside here will be rebuilt
// when stateProperty1 changes
}
Button().onClick {
store.commit(.setStateProperty1("changed by button click"))
}.withContent {
Text("change stateProperty1")
}
}
}
}
目前贡献的主要方式是功能请求、对 API 设计的意见和报告错误。 没有指导方针。 只需打开一个 issue。
复制自:github.com/ewconnell/swiftrt
安装以下扩展
Swift Language (Martin Kase)
CodeLLDB (Vadim Chugunov)
非常重要的是,settings.json 包含以下条目以从工具链中获取正确的 lldb 版本。 将 PathToSwiftToolchain 替换为您安装工具链的任何位置。 { "lldb.library": "PathToSwiftToolchain/usr/lib/liblldb.so" }
SourceKit-LSP (Pavel Vasek)
该服务器的一个版本已经是工具链的一部分,因此您无需构建它。 确保配置扩展 "sourcekit-lsp.serverPath": "PathToSwiftToolchain/usr/bin/sourcekit-lsp"。
此包依赖于
GL (用 Swift 编写的 OpenGL 加载器): github.com/kelvin13/swift-opengl
CombineX (Apple 的 Combine 框架的开源实现)
Swim (图像处理): github.com/t-ae/swim.git