shithub: wiki.9front.org

Download patch

ref: 488cbb480ec16eb03431f7b491275229b368c307
parent: 3afb1d0db57feeada8fb704a45c2b6cd55219f2f
author: kvik <kvik@a-b.xyz>
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)