shithub: docs.9front.org

ref: bedac6438461449392694dd8c4a7028ccdc46019
dir: docs.9front.org/programming-gui.md

View raw version
# Writing Interactive Graphical Programs

This tutorial will explain how to write an interactive graphical
program for 9front using libdraw and libthread, written especially for
beginning programmers.

Some alternative, contributed, graphical libraries not covered here:

* [microui](https://github.com/ftrvxmtrx/microui)
* [libnuklear](https://github.com/Plan9-Archive/libnuklear)
* [nuklear-demo](https://github.com/Plan9-Archive/nuklear-demo)

## The man Pages

The important man pages are as follows.  Please read up before
continuing, even if you don't fully understand what they mean.

* _graphics(2)_
* _draw(2)_
* _allocimage(2)_
* _addpt(2)_
* _thread(2)_
* _mouse(2)_
* _keyboard(2)_

Other useful man pages are listed in the See Also section of _graphics(2)_.

## Draw Something Simple

Before we can draw, we need to set up all of the boilerplate.  The man
pages of 9front list the header files that we need include in order to
use the library.  (cursor.h isn't actually necessary, unless you plan
on changing the cursor image.)

	#include <u.h>
	#include <libc.h>
	#include <draw.h>
	#include <cursor.h>

If the program takes in arguments, we will need to use _arg(2)_.  The
macros function like a switch statement.  If an invalid argument is
passed, it will go to default, where we will print to stderr and exit.
`argv0` is a special variable set by _ARGBEGIN_ that is the program's
name.  If you're not using _arg(2)_, it is in `argv[0]`.

	void
	main(int argc, char *argv[])
	{
		ulong co; /* a color */
		Image *im1, *im2, *bg; /* pointers to images we will draw, named "image1", "image2" and "background" */
		
		co = 0xFF0000FF; /* fully red, no green, no blue, fully opaque (alpha). Ever heard of RGBA? */
		
		ARGBEGIN{
		case 'b':
			co = DBlue; /* using an enum definition from allocimage(2) */
		default:
			fprint(2, "usage: %s [-b]\n", argv0); /* file descriptor 2 is stderr */
			exits("usage"); /* return with an exit string, because the program didn't successfully end */
		}ARGEND;

A common pattern in C is to provide initialization routines for
libraries that handle a lot of state.  These libraries will set
_errstr(2)_ and return -1 if they fail, so we need to catch and abort
if it happens.  _Initdraw_ connects us to the display and initializes
the global `display` variable.  We use the default error function by
passing nil, another nil pointer to use the default font, and the name
of the program as the label.

		if(initdraw(nil, nil, argv0) < 0)
			sysfatal("%s: %r", argv0);

We now need to connect to the window by using _getwindow_.
This needs to be done every time the window is resized, but we will
handle that case when we get to mouse control.  As it turns out,
_initdraw_ already calls _getwindow_ for us!  No need to call it yet.

Let's draw!

First, we will need to allocate an image by using _allocimage_.
Images are allocated on the drawing device, _display_.  If you are
connected to a cpu server, the drawing device is still local to your
machine.  We will take advantage of this fact to make sure drawing
happens as fast as possible.

We shall allocate a 10x10 size image on the display.  The image
supports red, green, blue, for a total of 24 bits.  The `repl` flag is
cleared, so this image will not tile and only draw once.  The image
fill is set to yellow (an enum in _allocimage(2)_).

		im1 = allocimage(display, Rect(0,0,100,100), RGB24, 0, DYellow);

We do a similar thing, but with a different color that is fully green
but half transparent (this is called premultiplied alpha).  Colors are
defined in _color(2)_ and _color(6)_.

		im2 = allocimage(display, Rect(0,0,100,100), RGBA32, 0, 0x007F007F);

We shall also allocate a 1x1 size image on the display.  The image is
opaque, only supporting red, green, and blue, 8 bits per channel.  The
`repl` flag is set!  Drawing this image will repeat itself over and
over again to fill up the whole window.  We use the color that we
defined earlier as the default image fill.

		bg = allocimage(display, Rect(0,0,1,1), RGB24, 1, co);

Because allocating can fail, we must check if the returned images are nil.

		if(im1 == nil || im2 == nil || bg == nil)
			sysfatal("get more memory, bub");

Now we need to composite the image using the _draw_ function.  We will
first draw `bg`, then `im1`, and lastly `im2`.

We first pass a pointer of the destination image, the global `screen`
variable.  Then, we pass in the area that our source image is allowed
to draw into, which will be the destination's size.  Then, the source
image pointer is given.  A mask image could be given, but we pass a
nil pointer for it instead.  ZP is the zero point, {0,0}.  See
_draw(2)_ for a description of how _draw_ handles the point argument.

		draw(screen, screen->r, bg, nil, ZP);

We then composite the next images on top, im1 below im2.  We use the
src image rectangle for this, but then we need to add the dst minimum
point, and an offset.

		draw(screen, rectaddpt(im1->r, addpt(screen->r.min, Pt(40,40))), im1, nil, ZP);
		draw(screen, rectaddpt(im2->r, addpt(screen->r.min, Pt(20,20))), im2, nil, ZP);

This can also be written as follows.  The reason for the negated
offset and what the `p` argument does is for sprites.  This is
explained further in AdvancedLibdrawTips.

		draw(screen, screen->r, im1, nil, Pt(-40,-40));
		draw(screen, screen->r, im2, nil, Pt(-20,-20));

These draw commands are not immediately written to the screen.  We
need to push these Remote Procedure Calls (RPCs) to the draw device
using _flushimage_ (see _draw(2)_).  We then call sleep so the image
stays for a while, and then exit.

		flushimage(display, Refnone);
		sleep(10000);
		exits(nil);
	}

Compile the program using 6c, link with 6l, and run it.  You should
see a red screen, a yellow square, and a transparent green square on
top.

## About Libthread

Libthread is the library that provides easy concurrency and parallel
program execution.  These threads are scheduled cooperatively; when
one thread is blocked, it gives up execution, and a different thread
is run.  Note that this is not parallelism, as these threads are
within the same proc (process).  Procs are preemptively scheduled by
the kernel, which may interrupt execution at any time (a great source
of both bugs and learning!).  Libthread also supports creating
parallel procs with shared memory, though this tutorial will not
explore that.  Procs contain the promise of faster speeds on manycore
architectures, but the difficulty of managing them isn't always worth
the effort.  Threads simply allow coroutines, which are suitable for
organizing certain problems.

Threads and procs can control shared memory using locked variables or
channels.  Know when to use either: channels are used for 2-way
communication between threads and procs, locks are generally used for
multi-proc global variables where parallel execution can mess up
memory access.

## Mouse and Keyboard Input ##

_mouse(2)_ and _keyboard(2)_ both use threads to execute, and
communicate with channels.  Both open files in /dev, read it into
convenient C data structures, and send the data through channels.

Let's get started with including the header files.  Our program will
start similarly as before, through with some extra declarations.
Check the man pages for what the `Mousectl` and `Keyboardctl` structs
are.  _main_ is changed to threadmain, as libthread defines its own
_main_ function and then calls _threadmain_.

	#include <u.h>
	#include <libc.h>
	#include <draw.h>
	#include <cursor.h>
	#include <thread.h>
	#include <mouse.h>
	#include <keyboard.h>
	
	void
	threadmain(int argc, char *argv[])
	{
		ulong co;
		Image *im1, *im2, *bg;
		Mousectl *mctl;
		Keyboardctl *kctl;
		Mouse mouse;
		int resize[2];
		Rune kbd;
		uchar magenta[3] = {0xFF, 0x00, 0xFF};
		uchar cyan[3] = {0xFF, 0xFF, 0x00};
		uchar purple[3] = {0xFF, 0x00, 0xA0};
		uchar orange[3] = {0x00, 0x7F, 0xFF};
		
		co = 0xFF0000FF;
		
		ARGBEGIN{
		case 'b':
			co = DBlue;
		default:
			fprint(2, "usage: %s [-b]\n", argv0);
			exits("usage");
		}ARGEND;
	
		if(initdraw(nil, nil, argv0) < 0)
			sysfatal("%s: %r", argv0);
		im1 = allocimage(display, Rect(0,0,100,100), RGB24, 0, DYellow);
		im2 = allocimage(display, Rect(0,0,100,100), RGBA32, 0, 0x007F007F);
		bg = allocimage(display, Rect(0,0,1,1), RGB24, 1, co);
		if(im1 == nil || im2 == nil || bg == nil)
			sysfatal("get more memory, bub");
		draw(screen, screen->r, bg, nil, ZP);
		draw(screen, screen->r, im1, nil, Pt(-40,-40));
		draw(screen, screen->r, im2, nil, Pt(-20,-20));
		flushimage(display, Refnone);

But now we need to initialize the mouse and keyboard, similarly to
_initdraw_.  Using very compact C notation, a function, assignment,
comparison, and branch are all performed within the same line.  See
the associated man pages for what each argument means.

		if((mctl = initmouse(nil, screen)) == nil)
			sysfatal("%s: %r", argv0);
		if((kctl = initkeyboard(nil)) == nil)
			sysfatal("%s: %r", argv0);

It's entirely possible to use _nbrecv_ (no-block receive) to check all
of the channels before determining what to do with any received data.
The `Alt` struct and function is a convenience to do all of this.  The
function will read each Alt structure in an array, put the read data
into the pointer, and return with the index of that Alt struct in the
array.  We will define our Alt array at declaration, for convenience.
Note the convention of channels ending with a 'c'.

		enum{MOUSE, RESIZE, KEYBD, NONE};
		Alt alts[4] = {
			{mctl->c, &mouse, CHANRCV},
			{mctl->resizec, &resize, CHANRCV},
			{kctl->c, &kbd, CHANRCV},
			{nil, nil, CHANEND},
		};

We then use the _alt_ function to read through every Alt and return
one of the indices.  This is used in a switch statement to decide what
to do next.  This is all done inside of a forever loop to create an
event loop.

		for(;;){
			switch(alt(alts)){

Our first case is `MOUSE`.  Let's change the bg color to magenta or
cyan depending on the mouse click and redraw.  Note that loadimage
loads in little endian byte order (blue first).

			case MOUSE:
				if(mouse.buttons == 1)
					loadimage(bg, bg->r, magenta, sizeof(magenta));
				else if(mouse.buttons == 4)
					loadimage(bg, bg->r, cyan, sizeof(cyan));
				draw(screen, screen->r, bg, nil, ZP);
				draw(screen, screen->r, im1, nil, Pt(-40,-40));
				draw(screen, screen->r, im2, nil, Pt(-20,-20));
				flushimage(display, Refnone);
				break;

If the window gets resized, we lose window control, have to call
_getwindow_, and redraw.

			case RESIZE:
				if(getwindow(display, Refnone) < 0)
					sysfatal("%s: %r", argv0);
				draw(screen, screen->r, bg, nil, ZP);
				draw(screen, screen->r, im1, nil, Pt(-40,-40));
				draw(screen, screen->r, im2, nil, Pt(-20,-20));
				flushimage(display, Refnone);
				break;

Next, let's handle a keyboard input.  If the character is the delete
key, we will exit the program (as is the 9front convention), however
using _threadexitsall_ instead of _exits_.  Note that if the program
is capturing keyboard input and the delete key case is not handled,
there will be no way to exit the program (besides deleting the
window).

			case KEYBD:
				if(kbd == 'a')
					loadimage(bg, bg->r, purple, sizeof(purple));
				else if(kbd == 'b')
					loadimage(bg, bg->r, orange, sizeof(orange));
				else if(kbd == '')
					threadexitsall(nil);
				draw(screen, screen->r, bg, nil, ZP);
				draw(screen, screen->r, im1, nil, Pt(-40,-40));
				draw(screen, screen->r, im2, nil, Pt(-20,-20));
				flushimage(display, Refnone);
				break;

In case there's any errors in the alt, do nothing.

			case NONE:
				break;
			}
		}
	}

And that's it!

If you've noticed, a lot of the redrawing can be factored out into a
separate function, following the rule of Don't Repeat Yourself.  If
there's some things I had glossed over (like channels), be sure to
read the man pages.  Happy hacking!

### Other Options

An alternative to using libdraw is to use
[libnuklear](https://github.com/vurtun/nuklear).  Find it in
[ports](https://code.9front.org/hg/ports) under /draw-libs/libnuklear.

Other Plan 9 libraries include _event(2)_ and _control(2)_.  Libevent
is a very old input library, and while it is still capable, libthread
has largely superseded it.  Libcontrol tries to provide a complete
toolkit for creating GUIs.  However, the Plan 9 team left it
unfinished.  Personally, I (Amavect) have tried using it, but I didn't
find it particularly easy to create my own control elements.

### See Also

[libdraw-tips](libdraw-tips.html)