Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented GenUI for NiGui #5

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions examples/example_92_genui_macro.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import nigui#, genui
import tables

var buttons = initTable[string, Button]()

proc clickHandler(event: ClickEvent) =
echo "Clicked"

app.init()

## TODO: Write a more functional and interesting example, maybe copy the wxNim genui threads example?
genui:
Window[width = 800, height = 600, show]:
LayoutContainer(Layout_vertical):
{buttons["button_1"] = @r}Button("Hello world")
{buttons["button_2"] = @result}Button("Second button")
[width = 800, height = 800, show]{(var exported* = @result)}("Test")Window:
Button

app.run()

91 changes: 91 additions & 0 deletions genui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Genui
This module provides the genui macro for the NiGui toolkit. Genui is a way to specify graphical interfaces in a hierarchical way to more clearly show the structure of the interface as well as simplifying the code. Genui is currently implemented for wxWidgets, libui, and nigui. The format focuses on being a soft conversion meaning that there are few to no assumptions and most code can be seen as a 1:1 conversion. This makes it easy to look at existing examples for your framework of choice when creating interfaces in genui. Because of this the genui format differs a bit from framework to framework, but aims to bring many of the same features. What follows is the genui format as used with nigui.

## Creating widgets
The most basic operation is to create widgets and add them together in a hierarchy. NiGui uses a very simple style of `newButton` to create a widget of type Button and `parent.add child` to add a child to a parent. In genui this translates to:

```
Window:
LayoutContainer:
Button
Button
```

The above snippet should create a window with a layout container containing two buttons. Although there is one problem, the procedure newLayoutContainer takes a parameter dictating the direction of the layout.

## Passing initialiser parameters
In order to pass parameters to an initialiser you simply enclose them in regular "()" brackets. Genui uses brackets to denote the various things you can do, and the order of the bracketed expressions doesn't matter. So to pass a `Layout` to the `LayoutContainer` simply do:

```
Window:
LayoutContainer(Layout_vertical):
Button
Button
```

To put text on the buttons you would similarily use `Button("Hello World")`. But now we're faced with a new challenge. NiGui requires us to call a `show` procedure on our window, but the window isn't assigned to a variable we can use.

## Calling procedures
Many configuration options in genui requires this pattern of creating a widget and assigning values to it's fields or calling it's procedures. To avoid having to assign variable names to all your widgets just for configuration genui offers a format to create so-called dot-expressions. This uses the "[]" brackets and all statements in there will have a dot and the temporary variable name prepended to them. So to call the `show` procedure and set `width` and `height` of the window we simply do:

```
Window[width = 800, height = 600, show]:
LayoutContainer(Layout_vertical):
Button
Button
```

Now our window shows up with two empty buttons one below the other. But user interfaces aren't always static so we need to be able to assign variable names to out widgets.

## Running code
Previous version of genui (for wxwidgets and libui) used a % notation in which an identifier could be assigned to the widget for later use. The % symbol was chosen as the assignment didn't directly convert to the Nimassignment as to avoid confusion. But this format proved a bit weird, and for data structures like a list or a table you would need to create these variables simply to use them once, something genui was created to avoid.

So this version of genui introduces a new concept. It's still a bit of a work in progress but it shows promise. By using the "{}" brackets arbitrary code can be executed. In these blocks the special symbol `@result` can be used, and will be replaced by the temporary variable name for the widget (a shorthand `@r` also exists as `@result` can get a bit terse). This means that anything from simple assignment to adding to complex data structures is possible. So for example adding our two buttons to a table of buttons would be:

```
Window[width = 800, height = 600, show]:
LayoutContainer(Layout_vertical):
{buttons["button_1"] = @result} Button
{buttons["button_2"] = @r} Button
```

As mentioned this is still a bit of a work in progress and not all code works, this has to do with how Nim parses curly brackets. There are two workarounds for this, the simplest is to add regular parenthesis around your code (which Nim silently ignores when converting to code). Or, should that not work either you can wrap code in a string. So converting the above code statements to these two workaround would look like this:

```
Window[width = 800, height = 600, show]:
LayoutContainer(Layout_vertical):
{(buttons["button_1"] = @result)} Button
{"buttons[\"button_2\"] = @r"} Button
```


## A note on order
As mentioned in the section about initialisation parameters the order of the brackets doesn't matter. So if you want to place the "{}" brackets on the end of your line, or if you want to put the "()" before the Widget name doesn't matter. But as an "official" suggestion I typically use this order:

```
{var myButton = @result} Button("Hello World!")[onClick = clickHandler]
```

The exception to this would be for code snippets which can tend to push the widget name too far along the line for readability. In that case they go in the back.

## Adding elements to a widget
Sometimes you want to add widgets to a parent to indicate some change of state in your program. In order to facilitate this genui also comes with the procedure `addElements` which takes a container and genui formatted code like this:

```
myExistingContainer.addElements:
Layout(Layout_vertical):
Button
Button
```

# Quick reference
Don't care about the details? Here is a quick reference to the genui format:

| Bracket | Function | Example | Generates |
|---------|---------------------------|------------------------------|------------------------------------|
| `()` | Initialisation parameters | `Button("Hello World")` | `newButton("Hello World")` |
| `[]` | Dot-expressions | `Window[height = 300, show]` | `window.height = 300; window.show` |
| `{}` | Pure code insertion | `{var b = @result} Button` | `var b = newButton()` |

`genui` creates new code, addElements creates the same code but with `add` statements for top-level widgets. `{}` is still a work in progress, code that doesn't parse in it can be added as a string instead.

1 change: 1 addition & 0 deletions src/nigui.nim
Original file line number Diff line number Diff line change
Expand Up @@ -2252,3 +2252,4 @@ method `wrap=`(textArea: TextArea, wrap: bool) =

when useWindows(): include "nigui/private/windows/platform_impl"
when useGtk(): include "nigui/private/gtk3/platform_impl"
include "nigui/private/genui/genui"
150 changes: 150 additions & 0 deletions src/nigui/private/genui/genui.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import macros, deques

proc `[]`(s: NimNode, x: Slice[int]): seq[NimNode] =
## slice operation for NimNodes.
var a = x.a
var L = x.b - a + 1
newSeq(result, L)
for i in 0.. <L: result[i] = s[i + a]

proc high(s: NimNode):int =
## high operation for NimNodes
s.len-1

type ParsedWidget = ref object
## Object that holds all the required information about a widget as learned from parsing the input code
pureCode: NimNode
name: string
dotCalls: seq[NimNode]
initParameters: seq[NimNode]
children: seq[ParsedWidget]
parent: ParsedWidget
generatedSym: NimNode

proc parseNode(node: NimNode): ParsedWidget
proc parseChildren(p: ParsedWidget, stmtlist:NimNode): seq[ParsedWidget] =
result = @[]
for child in stmtList:
var node = parseNode(child)
node.parent = p
result.add node

proc parseNode(node: NimNode): ParsedWidget =
new result
var
toParse = initDeque[NimNode]()
cnode = node
if cnode.kind == nnkIdent:
result.name = $cnode.ident
cnode = nil
template checkName() =
if cnode[0].kind == nnkIdent:
result.name = $cnode[0].ident
else:
toParse.addFirst cnode[0]
while cnode != nil:
echo "Parsing: " & $cnode.kind
if cnode.len != 0 and cnode[cnode.high].kind == nnkStmtList:
toParse.addFirst cnode[cnode.high]
cnode.del cnode.high
case cnode.kind:
of nnkCurlyExpr:
result.pureCode = cnode[1]
checkName()
of nnkCurly:
result.pureCode = cnode[0]
of nnkBracketExpr:
result.dotCalls = cnode[1..cnode.high]
checkName()
of nnkBracket:
result.dotCalls = cnode[0 .. ^1]
of nnkPar:
result.initParameters = cnode[0 .. ^1]
of nnkCall:
result.initParameters = cnode[1..cnode.high]
checkName()
of nnkCommand:
if cnode[cnode.high].kind == nnkIdent:
result.name = $cnode[cnode.high].ident
for i in countdown(cnode.high-1, 0):
toParse.addFirst cnode[i]
else:
for i in countdown(cnode.high, 0):
toParse.addFirst cnode[i]
of nnkStmtList:
result.children = result.parseChildren cnode
else:
for child in countdown(cnode.high, 0):
toParse.addFirst cnode[child]
if toParse.len != 0:
cnode = toParse.peekFirst()
discard toParse.popFirst()
else:
cnode = nil
echo "Returning"

proc createWidget(widget: ParsedWidget, parent: NimNode = nil): NimNode =
result = newStmtList()
echo "Hello"
echo widget.name
var call = newCall("new" & widget.name)
for param in widget.initParameters:
call.add param
widget.generatedSym = genSym(nskVar)
result.add nnkVarSection.newTree(
nnkIdentDefs.newTree(
widget.generatedSym,
newEmptyNode(),
call
)
)
proc replacePlaceholder(n: NimNode): bool =
for i in 0 .. n.high:
let child = n[i]
if child.kind == nnkPrefix and child[0].kind == nnkIdent and child[1].kind == nnkIdent and
child[0].ident == !"@" and (child[1].ident == !"result" or child[1].ident == !"r"):
n[i] = widget.generatedSym
return true
let done = child.replacePlaceholder()
if done:
return true

for dotCall in widget.dotCalls:
let call = "@result." & dotCall.repr
echo call
let callExpr = call.parseExpr
discard replacePlaceholder(callExpr)
result.add callExpr

if widget.pureCode != nil:
if widget.pureCode.kind == nnkStrLit:
widget.pureCode = widget.pureCode.strVal.parseExpr
widget.pureCode = widget.pureCode.repr.parseExpr
discard replacePlaceholder(widget.pureCode)
result.add(widget.pureCode)

for child in widget.children:
for node in createWidget(child, widget.generatedSym):
result.add node

if parent != nil:
result.add newCall("add", parent, widget.generatedSym)

macro genui*(widgetCode: untyped): untyped =
## Macro to create NiGui code from the genui syntax (see documentation)
echo widgetCode.treeRepr
let parsed = nil.parseChildren(widgetCode)
result = newStmtList()
for widget in parsed:
result.add createWidget(widget)
echo result.repr

macro addElements*(parent:untyped, widgetCode: untyped): untyped=
## Macro to create NiGui code from the genui syntax (see documentation) and create add calls for the resulting widgets for the given parent
echo widgetCode.treeRepr
let parsed = nil.parseChildren(widgetCode)
result = newStmtList()
for widget in parsed:
result.add createWidget(widget, parent)
echo result.repr