Skip to content

GUI Layout

Ward edited this page Oct 5, 2019 · 1 revision

Simple Layout

Code

Here we create two buttons and add a layout procedure:

import wNim/[wApp, wFrame, wPanel, wButton]

let app = App()
let frame = Frame(title="wNim Frame", size=(400, 300))
let panel = Panel(frame)
let button1 = Button(panel, label="Button1")
let button2 = Button(panel, label="Button2")

proc layout() =
  button1.size = (panel.size.width div 2, panel.size.height)
  button1.position = (0, 0)

  button2.size = (panel.size.width div 2, panel.size.height)
  button2.position = (panel.size.width div 2, 0)

layout()
frame.center()
frame.show()
app.mainLoop()

Screenshot

Image

It seems good, but after resizing the frame:

Image

Modified Code

The problem is that the layout don't change after resizing, so we add a event handler for that:

import wNim/[wApp, wFrame, wPanel, wButton]

let app = App()
let frame = Frame(title="wNim Frame", size=(400, 300))
let panel = Panel(frame)
let button1 = Button(panel, label="Button1")
let button2 = Button(panel, label="Button2")

proc layout() =
  button1.size = (panel.size.width div 2, panel.size.height)
  button1.position = (0, 0)

  button2.size = (panel.size.width div 2, panel.size.height)
  button2.position = (panel.size.width div 2, 0)

# frame.wEvent_Size already has it's default event handler to resize the panel.
# see https://github.com/khchen/wNim/wiki/Event-Handling#skip-event

panel.wEvent_Size do ():
  layout()

layout()
frame.center()
frame.show()
app.mainLoop()

Now it works well.

Layout DSL and AutoLayout VFL

Code

If we create a lot of elements, counting the size and position for every elements is a bored thing. We can use layout DSL to count them automatically.

import wNim/[wApp, wFrame, wPanel, wButton]

let app = App()
let frame = Frame(title="wNim Frame", size=(400, 300))
let panel = Panel(frame)
let button1 = Button(panel, label="Button1")
let button2 = Button(panel, label="Button2")

proc layout() =
  panel.layout:
    button1:
      top = panel.top
      left = panel.left
      bottom = panel.bottom
      right = button2.left

    button2:
      top = panel.top
      right = panel.right
      bottom = panel.bottom
      width = button1.width

panel.wEvent_Size do ():
  layout()

layout()
frame.center()
frame.show()
app.mainLoop()

Layout DSL is easy to use. But sometimes it need more code than counting manually. We can use autolayout module to create the layout DSL at compiling time.

import wNim/[wApp, wFrame, wPanel, wButton]

let app = App()
let frame = Frame(title="wNim Frame", size=(400, 300))
let panel = Panel(frame)
let button1 = Button(panel, label="Button1")
let button2 = Button(panel, label="Button2")

proc layout() =
  panel.autolayout """
    H:|[button1][button2(button1)]|
    V:|[button1,button2]|
  """

panel.wEvent_Size do ():
  layout()

layout()
frame.center()
frame.show()
app.mainLoop()

With autolayout, it is easier to design the layout. For example, if we want to add some spacing:

import wNim/[wApp, wFrame, wPanel, wButton]

let app = App()
let frame = Frame(title="wNim Frame", size=(400, 300))
let panel = Panel(frame)
let button1 = Button(panel, label="Button1")
let button2 = Button(panel, label="Button2")

proc layout() =
  panel.autolayout """
    H:|-[button1]-[button2(button1)]-|
    V:|-[button1,button2]-|
  """

panel.wEvent_Size do ():
  layout()

layout()
frame.center()
frame.show()
app.mainLoop()

Screenshot

Image

Debug

When we use autolayout, it will be translated to layout DSL, and then the layout DSL will be translated to nim code which create a wResizer object internally. All code translation is done at compiling time, and the wResizer uses Yuriy Glukhov's Kiwi constraint solving library to arrange the associated elements at running time.

We can see the output DSL by using autolayoutDump or autolayoutDebug macros:

Code

  panel.autolayoutDump """
    H:|-[button1]-[button2(button1)]-|
    V:|-[button1,button2]-|
  """

Output

panel.layout:
  button1:
    left = panel.left + 10
    top = panel.top + 10
    bottom = panel.bottom - 10

  button2:
    left = button1.right + 10
    width = button1.width
    right = panel.right - 10
    top = panel.top + 10
    bottom = panel.bottom - 10

We can also see the output nim code by using layoutDump or layoutDebug macros:

Code

  panel.layoutDump:
    button1:
      left = panel.left + 10
      top = panel.top + 10
      bottom = panel.bottom - 10

    button2:
      left = button1.right + 10
      width = button1.width
      right = panel.right - 10
      top = panel.top + 10
      bottom = panel.bottom - 10

Output

self = button1
resizer.addObject(button1)
resizer.addConstraint(self.left == panel.left + 10)
resizer.addConstraint(self.top == panel.top + 10)
resizer.addConstraint(self.bottom == panel.bottom - 10)
self = button2
resizer.addObject(button2)
resizer.addConstraint(self.left == button1.right + 10)
resizer.addConstraint(self.width == button1.width)
resizer.addConstraint(self.right == panel.right - 10)
resizer.addConstraint(self.top == panel.top + 10)
resizer.addConstraint(self.bottom == panel.bottom - 10)

Layout and Relayout

Code

Consider following example:

import random
import wNim/[wApp, wFrame, wPanel, wButton]

randomize()

let app = App()
let frame = Frame(title="wNim Frame", size=(400, 300))
let panel = Panel(frame)
let button1 = Button(panel, label="Button1")
let button2 = Button(panel, label="Button2")

proc layout() =
  let gap = rand(50)

  panel.layout:
    button1:
      top = panel.top
      left = panel.left
      bottom = panel.bottom
      right + gap = button2.left

    button2:
      top = panel.top
      right = panel.right
      bottom = panel.bottom
      width = button1.width

panel.wEvent_Size do ():
  layout()

layout()
frame.center()
frame.show()
app.mainLoop()

Here we add a random gap between two buttons. the "gap" variable should be different every time when layout() is called. However, in this example, we need to restart the program to get a new gap. This is because layout DSL by default only initiate the constraint once for performance. If a new value is really needed every time, we can use relayout macro to replace the layout macro:

  panel.relayout:
    button1:
      top = panel.top
      left = panel.left
      bottom = panel.bottom
      right + gap = button2.left

    button2:
      top = panel.top
      right = panel.right
      bottom = panel.bottom
      width = button1.width