ref: b6a160edfb7683492504597fbd51e1e343e0cd2c
dir: /programming-gui.md/
# 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)