Swiftpack.co -  VertexUI/VertexGUI as Swift Package
Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
VertexUI/VertexGUI
A framework for creating cross-platform GUI applications with Swift.
.package(url: "https://github.com/VertexUI/VertexGUI.git", from: "0.2.1")

VertexGUI

build-ubuntu build-macos

VertexGUI is a Swift framework for writing cross-platform GUI applications.


Demo

screenshot of demo app

Currently Linux and MacOS are supported. Windows support is planned.

To run the demo follow the installation instructions for SDL2 below, clone the repository and in the root directory execute swift run TaskOrganizerDemo.

The code for the demo app can be found in Sources/TaskOrganizerDemo

I'm working on a tutorial.


Table of Contents


Installation

SDL2

The framework depends on SDL2 to create windows and receive events.

On Ubuntu install it with:

sudo apt-get install libsdl2-dev

on MacOS (via homebrew):

brew install sdl2

for other platforms see: Installing SDL.


VertexGUI

This project is under heavy development. I will not create releases until there is some API stability.

Just use the master branch:

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

A Swift 5.3 toolchain is required.



Simple Code Example

Result:

screenshot of minimal demo app
import VertexGUI 

public class MainView: ContentfulWidget {
  @State
  private var counter = 0

  @DirectContentBuilder override public var content: DirectContent {
    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))
        }
      }
    }
  }
}

When you press the button, the counter should be incremented.

Some additional setup code is necessary to display the window. You can find all of it in Sources/MinimalDemo



Feature Overview


Declarative GUI Structure

Using Swift's function/result builders.

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

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

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

Custom Widgets

by composing other Widgets

Create reusable views consiting of multiple Widgets. Pass child Widgets to your custom Widget instances by using slots. Parts of the composition API might be renamed in the future.

class MyCustomView: ContentfulWidget, SlotAcceptingWidgetProtocol {

  static let childSlot = Slot(key: "child", data: String.self)
  let childSlotManager = SlotContentManager(MyCustomView.childSlot)

  @DirectContentBuilder override var content: DirectContent {
    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)
    }
  }
}

by drawing graphics primitives (LeafWidget)

LeafWidgets are directly drawn to the screen. They do not have children.

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

Styling API similar to CSS

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

custom Widgets can have special style properties

class MyCustomWidget {
  ...

  @StyleProperty
  public var myCustomStyleProperty: Double = 0.0

  ...
}

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

Reactive Widget Content

Update the content and structure of your Widgets when data changes.

class MyCustomWidget: ContentfulWidget {
  @State private var someState: Int = 0
  @ImmutableBinding private var someStateFromTheOutside: String

  public init(_ outsideStateBinding: ImmutableBinding<String>) {
    self._someStateFromTheOutside = outSideStateBinding
  }

  @DirectContentBuilder override var content: DirectContent {
    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) }))
    }
  }
}

Inject Dependencies Into Widgets

This should be changed so that providing dependencies can be done by using a property wrapper as well. Dependencies are resolved by comparing keys (if given) and types.

class MyCustomWidget: ContentfulWidget {
  ...

  @Inject(key: <nil or a String>) private var myDependency: String
}

class MyCustomParentWidget: ContentfulWidget {
  // API will be changed, so that this dependency can be provided by doing:
  // @Provide(key: <nil or a String>)
  let providedDependency: String = "dependency"

  @DirectContentBuilder override var content: DirectContent {
    Container().withContent {
      MyCustomWidget()
    }.provide(dependencies: providedDependency)
  }
}

Global App State Management

The approach is similar to Vuex. Defining mutations and actions as enum cases instead of methods allows for automatic recording where and when which change was made to the state.

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
}

Now you can use the store in your whole app like so:

class TheRootView: ContentfulWidget {
  let store = MyAppStore()

  @DirectContentBuilder override var content: DirectContent {
    Container().provide(dependencies: store).withContent {
      ...
      // can be deeply nested
      MyCustomWidget()
      ...
    }
  }
}

class MyCustomWidget: ContentfulWidget {
  @Inject var store: MyAppStore

  @DirectContentBuilder override var content: DirectContent {
    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")
      }
    }
  }
}

Current Limitations

  • only runs on Linux (tested on Ubuntu 20.04) and MacOS (tested on MacOS 10.15)
  • depends on SDL2 for handling cross platform window management
  • depends on NanoVG (specifically on the OpenGL 3.3 implementation of NanoVG) for rendering primitives (line, rect, ...)
  • a few core Widget types (Container, Button, Text, TextInput, ...) are available
  • the graphics api has only been implemented in so far as to be able to create the above demos
  • everything is redrawn on every frame
  • animations, transitions are not yet supported
  • only one layout type is well supported, very similar to CSS flexbox, but does not yet support line breaks



Roadmap

  • Windows support
  • WebAssembly support
  • extend drawing api
    • gradients
    • rounded rects
    • arbitrary paths
    • ...
  • consider adding Skia as additional drawing backend
  • more core Widgets
    • RadioButton
    • Checkbox
    • Image
    • Textarea
    • ...
  • full flexbox layout system
  • other layout systems
    • absolute
    • anchor
    • ...
  • transitions, animations
  • optimize drawing, only redraw on update

Contribute

The main ways to contribute currently are feature requests, opinions on API design and reporting bugs. There are no guidelines. Just open an issue.



VSCode Setup on Linux

Copied from: github.com/ewconnell/swiftrt

Install the following extensions:

Swift Language (Martin Kase)

CodeLLDB (Vadim Chugunov)

It is very important that settings.json contains the following entry to pickup the correct lldb version from the toolchain. Substituting PathToSwiftToolchain with wherever you installed the toolchain. { "lldb.library": "PathToSwiftToolchain/usr/lib/liblldb.so" }

SourceKit-LSP (Pavel Vasek)

There is a version of the server as part of the toolchain already, so you don't need to build it. Make sure to configure the extension "sourcekit-lsp.serverPath": "PathToSwiftToolchain/usr/bin/sourcekit-lsp".


Dependencies

This package depends on:

SDL2

NanoVG

GL (OpenGL loader written in Swift): github.com/kelvin13/swift-opengl

CombineX (open source implementation of Apple's Combine framework)

Swim (Image handling): github.com/t-ae/swim.git

Cnanovg (NanoVG wrapper for Swift): github.com/UnGast/Cnanovg.git

ColorizeSwift

GitHub

link
Stars: 45
Last commit: 20 hours ago

Ad: Job Offers

iOS Software Engineer @ Perry Street Software
Perry Street Software is Jack’d and SCRUFF. We are two of the world’s largest gay, bi, trans and queer social dating apps on iOS and Android. Our brands reach more than 20 million members worldwide so members can connect, meet and express themselves on a platform that prioritizes privacy and security. We invest heavily into SwiftUI and using Swift Packages to modularize the codebase.

Release Notes

Fixes
9 weeks ago

Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API