Writing a Responsive UI

Responding to changes in orientation and size in an 8th GUI application.

Introduction

Application developers often encounter the problem of writing a "responsive UI". That term means a user-interface which adapts itself to varying device dimensions and orientations. So for example, an application may arrange buttons and other controls differently, depending on whether the device is in "portrait" or "landscape" mode.

The technique

Examining the source file presented below, you will note that 8th uses a dialect of JSON to describe a UI. Among other things, one describes the "callbacks" to be made when different UI "events" occur. In our example, we create a handler for the "size" event in the main window, e.g. the window which contains the rest of our UI.

The size handler looks like this:

\ This is called when the main window resizes.  
\ We calculate the orientation and save the max width and height:
: winsize   \ gui wide high
  2dup high ! wide !                        \ save the window size (if we need it later)
  n:> landscape !                           \ if wide > high, it's landscape mode
  ...
  \ tell everyone to resize appropriately
  0 landscape @ if n:1+ then  g:user!  ;    \ broadcast a 0 for portrait or a 1 for landscape	

So what happens is that when the main window is resized, our handler decides if the layout is portrait or landscape, and sends a notification event to all this UI's child elements. Each element which needs to modify its position upon such a change, handles the "user" event. Our sample code uses the same code for all elements:

: onuser \ gui index --
  >r                                \ save the index value
  "bnd" g:m@                        \ grab the "bnd" element from our UI's map
  r> a:@ nip g:bounds ;             \ get the index, and then get and use the bounds	

When this "onuser" word (e.g. function) is invoked, it is given the index (which is 0 for portrait or 1 for landscape), as well as the gui item which wants to be adjusted. Note that we are being a bit clever here, in that we store the "bounds" strings to be used (which determine the actual layout) in the gui item's map. We retrieve the array of bounds strings using "bnd" g:m@ and then extract the string we want and apply it as the item's new bounds.

Inside our item's JSON description, we define "bnd" like this:

"bnd" :     \ the key "bnd" in the gui item's map has an array value:
[
  \ portrait bounds:
  "0,0,parent.width,30",

  \ landscape bounds:
  "0,0,parent.width/3,30",
]

The value of the "bnd" key is an array with two strings, one for portrait and one for landscape. When the "user" handler applies the specific "bounds", the change in layout is immediate.

Conclusion

We hope to have adequately demonstrated a useful 8th technique which is applicable in other languages as well. There are many other methods for producing the same results, though the one shown is elegant. For example, one might define multiple UI layouts and simply create the correct ones at need. We leave it to you to experiment and see what techniques work best for your needs.

The code

\ A "responsive" GUI changes layout depending upon size and orientation
true app:isgui !

\ state variables
false var, landscape
var wide var high

\ This is called when the main window resizes.  We calculate the orientation and
\ save the max width and height:
: winsize   \ gui wide high
  2dup high ! wide !
  n:> landscape !
  landscape @ if "landscape" else "portrait" then high @ wide @ 
  "Window is %d X %d, %s" s:strfmt . cr
  \ tell everyone to resize appropriately
  0 landscape @ if n:1+ then  g:user!  ;

\ this is called for every other item when the main window resizes
: onuser \ gui index --
  >r "bnd" g:m@ 
  r> a:@ nip g:bounds ;

\ Define the gui
{
  "kind" : "win",
  "size" : "winsize",
  "wide" : 640,
  "high" : 480,
  "bg"   : "beige",
  "children" :
  [
    {
      "kind" : "btn",
      "label" : "One",
      "click" : ( "Number one!" g:say ) ,
      "bg" : "red:50",
      "name" : "b1",
      "bnd" :
      [
        \ portrait bounds:
        "0,0,parent.width,30",
        \ landscape bounds:
        "0,0,parent.width/3,30",
      ]
      "user" : "onuser",
    },
    {
      "kind" : "btn",
      "label" : "Two",
      "click" : ( "Too bad" g:say ) ,
      "bg" : "yellow:50",
      "name" : "b2",
      "bnd" :
      [
        \ portrait bounds:
        "0,b1.bottom+10,parent.width,top+30",
        \ landscape bounds:
        "b1.right+10,0,left+parent.width/3,30",
      ]
      "user" : "onuser",
    },
    {
      "kind" : "btn",
      "label" : "Three",
      "click" : ( "Three's a crowd" g:say ) ,
      "bg" : "green:50",
      "name" : "b3",
      "bnd" :
      [
        \ portrait bounds:
        "0,b2.bottom+10,parent.width,top+30",
        \ landscape bounds:
        "b2.right+10,0,parent.width-10,30",
      ]
      "user" : "onuser",
    }
  ]
} var, layout

: app:main
  layout @ g:new ;