VertexGUI

build Ubuntu 18.04 build Ubuntu 20.04 build MacOS

VertexGUI 是一个 Swift 框架,用于编写跨平台的 GUI 应用程序。


演示

screenshot of demo app

VertexGUI 使用 Skia 2D 渲染引擎来绘制 Widgets,并使用 Fireblade 游戏引擎的一部分来管理多个平台上的窗口。

目前支持 Linux、MacOS 和 Windows。 Skia 支持更多平台:Android、iOS、ChromeOS。 因此,VertexGUI 也可以在不太多的工作量下支持这些平台。

要运行演示应用程序,请按照下面的安装说明,克隆存储库,然后在根目录中执行 swift run TaskOrganizerDemo

演示应用程序的代码可以在 Sources/TaskOrganizerDemo 中找到


目录


安装

SDL2

VertexGUI 依赖 SDL2 来创建窗口并接收键盘和鼠标事件。 SDL2 需要以二进制文件的形式存在于您的系统中。 设置 SDL2 最方便的方法是使用您平台的包管理器

在 Ubuntu 上,使用以下命令安装它

sudo apt-get install libsdl2-dev

在 MacOS 上(通过 homebrew)

brew install sdl2

对于其他平台,请参阅:安装 SDL


Skia

Skia 是用于绘制 VertexGUI 小部件的 2D 图形库。 它也需要以二进制形式存在。

要下载预构建的二进制文件或自己构建 skia,请按照为 SkiaKit 编写的说明进行操作(SkiaKit 是 Skia c++ API 的包装库)。


VertexGUI

这个项目正在紧张的开发中。 在 API 稳定之前,我不会创建任何版本。

只需使用 master 分支

dependencies: [
  ...,
  .package(name: "VertexGUI", url: "https://github.com/VertexUI/VertexGUI", .branch("master")),
],
targets: [
  ...,
  .target(name: "SomeTarget", dependencies: ["VertexGUI", ...])
]

需要 Swift 5.5 工具链



简单代码示例

结果

screenshot of minimal demo app

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 中找到所有代码



功能概述


声明式 GUI 结构

使用 Swift 的 函数/结果构建器

Container().withContent {
  Button().withContent {
    Text("Hello World")

    $0.iconSlot {
      Icon(identifier: .party)
    }
  }
}

List(items).withContent {
  $0.itemSlot { itemData in
    Text(itemData)
  }
}

自定义 Widgets

通过组合其他 Widgets

创建由多个 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)
    }
  }
}

通过绘制图形基元 (LeafWidget)

LeafWidgets 直接绘制到屏幕上。 它们没有子项。

class MyCustomLeafWidget: LeafWidget {
  override func draw(_ drawingContext: DrawingContext) {
    drawingContext.drawRect(rect: ..., paint: Paint(color: ..., strokeWidth: ...))
    drawingContext.drawLine(...)
    drawingContext.drawText(...)
  }
}

类似于 CSS 的样式 API

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

自定义 Widgets 可以具有特殊的样式属性

class MyCustomWidget {
  ...

  @StyleProperty
  public var myCustomStyleProperty: Double = 0.0

  ...
}

// somewhere else in your code
Style(".class-applied-to-my-custom-widget") {
  (\.$myCustomStyleProperty, 1.0)
}

反应式 Widget 内容

当数据更改时,更新您的 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) }))
    }
  }
}

将依赖项注入到 Widgets 中

应该更改此设置,以便也可以使用属性包装器来提供依赖项。 通过比较键(如果给定)和类型来解析依赖项。

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。



Linux 上的 VSCode 设置

复制自: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"。


依赖项

此包依赖于

SDL2

NanoVG

GL (用 Swift 编写的 OpenGL 加载器): github.com/kelvin13/swift-opengl

CombineX (Apple 的 Combine 框架的开源实现)

Swim (图像处理): github.com/t-ae/swim.git

Cnanovg (NanoVG 的 Swift 包装器): github.com/UnGast/Cnanovg.git

ColorizeSwift