denim_ui

The Denim UI library


Keywords
gui, web, cross-platform, library, reactive, observables, dsl
License
MIT
Install
nimble install denim_ui

Documentation

Midio UI framework

A custom cross platform UI framework focused on fast and easy prototyping by the use of a custom DSL.

Reference docs

https://nortero-code.github.io/midio-ui/

DSL

The syntax for creating GUIs using the DSL is as follows:

elemType(attribute1 = value1, attribute2 = value2):
  child1(attrib1 ...
  ...

The basic type is the Element type, which is what the entire GUI is created from.

panel(), for example, creates an element with the layout semantics of a panel.

Attributes available to all element types:

  HorizontalAlignment* {.pure.} = enum
    Stretch, Center, Left, Right

  VerticalAlignment* {.pure.} = enum
    Stretch, Center, Top, Bottom

  Visibility* {.pure.} = enum
    Visible, Collapsed, Hidden
  width*: Option[float]
  height*: Option[float]
  maxWidth*: Option[float]
  minWidth*: Option[float]
  maxHeight*: Option[float]
  minHeight*: Option[float]
  x*: Option[float]
  y*: Option[float]
  xOffset*: Option[float]
  yOffset*: Option[float]
  margin*: Option[Thickness[float]]
  horizontalAlignment*: Option[HorizontalAlignment]
  verticalAlignment*: Option[VerticalAlignment]
  visibility*: Option[Visibility]

Layout primitives

All elements

TODO

Panel

TODO

Dock

TODO

Stack

TODO

Grid (TODO)

TODO

Visual primitives

TODO

Rectangle

TODO

Circle

TODO

Text

TODO

Path

TODO

Behaviors

TODO

onClick

TODO

onPressed

TODO

onReleased

TODO

onPointerMoved

TODO

onDrag

TODO

Data binding

For dynamic data, we use the Observable pattern, which works pretty much as RX observables (http://reactivex.io/intro.html), sans some missing operators.

We can bind observables to attributes using the <- operator:

let widthValue = behaviorSubject(100.0)
panel(width <- widthValue)

We can also have dynamic children using the spread operator ...:

let someChildren = observableCollection(@[panel(), text(), rectangle()])
panel:
  ...someChildren

Note that the spread operator currently works for the following types:

  • seq[Element]
  • Subject[Element]
  • Subject[Option[Element]]
  • Subject[seq[Element]]
  • Observable[Element]
  • CollectionSubject[Element]

More can be supported by simply creating a proc with the following signature:

proc bindChildCollection*(self: Element, items: THE_TYPE_TO_SUPPORT): void =
   ...

This proc should set up the necessary subscriptions that manipulate the elements children using addChild and removeChild.

Here is an example of how the implementation for Subject[Element] works:

proc bindChildCollection*(self: Element, item: Subject[Element]): void =
  var prevElem: Element
  discard item.subscribe(
    proc(e: Element): void =
      if not isNil(prevElem):
        self.removeChild(prevElem)
      prevElem = e
      self.addChild(e)
  )

Components

Components lets us create reusable element types more easily, and can be defined like so:

component ComponentName(prop1: AttrType1, prop2: AttrType2):
  let foo = "bar"

  panel:
    text(text = foo)

Component bodies can contain whatever code you want, as long as it return an element.

The component can then be used with the DSL syntax like any other element:

panel:
  ComponentName(prop1 = ....

NOTE: Databinding doesn't work for component properties

Since the properties are just passed by value as parameters to the component body, if you want property values to be changed dynamically, they need to be passed as Observables:

component MyDynamicComp(val1: Observabie[float]):
  panel(width <- val1)