ref: 488cbb480ec16eb03431f7b491275229b368c307
parent: 3afb1d0db57feeada8fb704a45c2b6cd55219f2f
author: kvik <[email protected]>
date: Thu Oct 29 13:16:58 EDT 2020
programming-gui: add article from old wiki (thanks Amavect)
--- /dev/null
+++ b/programming-gui.md
@@ -1,0 +1,341 @@
+# 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)