jmi2k

/text

ria

  1. Introducing libdraw
    1. High-level overview
    2. Initializing everything
    3. Handling resizes reshapes
    4. Threads and synchronization
    5. The mouse thread
  2. Window handling

Introducing libdraw

Although rio is fairly simple, the job of a window manager rectangle multiplexor1 involves quite a few independent parts. Let's start with something simpler!

High-level overview

These are all the data structures, functions and variables we are going to implement during this first iteration:

void		resized(void);
void		mousethread(void*);
void		initstyle(void);
void		usage(void);
void		threadmain(int, char*[]);

int			abortonerror;
Channel*	exitchan;
Mousectl*	mousectl;
Screen*		wscreen;
Image*		riaback;
int			basecolhex;

I promise we will cover all of them — eventually.

Here you have some helper functions which we will be using:

void
error(char err[])
{
	fprint(2, "%s: %s: %r\n", argv0, err);
	if(abortonerror)
		abort();
	threadexitsall("error");
}

Image*
eallocimage(Display *d, Rectangle r, ulong chan, int repl, ulong col)
{
	Image *i;

	i = allocimage(d, r, chan, repl, col);
	if(i == nil)
		error("can't allocate image");
	return i;
}

Initializing everything

Let's start with defining our main function. ria will make use of libthread (more on that later), so instead of calling it main we have to call it threadmain. We will also define an usage function, to be called if the parameters passed are incorrect. Nothing fancy yet:

#include <u.h>
#include <libc.h>
#include <draw.h>
#include <thread.h>
#include <mouse.h>

enum
{
	STACK = 8192,
};

int			abortonerror;
Mousectl*	mousectl;
Channel*	exitchan;

void
usage(void)
{
	fprint(2, "usage: %s\n", argv0);
	exits("usage");
}

void
threadmain(int argc, char *argv[])
{
	ARGBEGIN{
	default:
		usage();
	}ARGEND

	if(argc > 0)
		usage();
}

The first step is to setup libdraw and the mouse.

	if(geninitdraw(nil, nil, nil, argv0, nil, Refnone) < 0)
		error("can't open display");
	mousectl = initmouse(nil, screen);
	if(mousectl == nil)
		error("can't open mouse");

Usually, you would use the initdraw function instead of the more explicit geninitdraw. However, rio uses the latter (presumably to set the refresh function to Refnone.

The next step is initializing all the resources needed to style ria. In this example, all of them are plain colors (1×1 images with the repl bit set), but as far as libdraw is concerned, they are tiled bitmaps.

Image*		riaback;
int			basecolhex;

void
initstyle(void)
{
	Rectangle r₁;

	r₁			= Rect(0, 0, 1, 1);
	basecolhex	= 0xFFFFFFFF;
	riaback		= allocimage(display, r₁, CMAP8, 1, basecolhex);
}

Just call it from threadmain:

	initstyle();

From this point onward, any error must abort the program (so you can attach a debugger to it later). This behavior of error is controlled by the abortonerror flag. Let's set it now:

	abortonerror = 1;

Handling resizes reshapes2

After any change in the screen rectangle, we have to adjust our internal state and redraw everything again (so everything stays consistent). We still have little to draw, but we will come back later to add more stuff.

This simple implementation will just make libdraw update its internal state:

Image*		wscreen;

void
resized(void)
{
	if(getwindow(display, Refnone) < 0)
		error("cannot get display");
}

Now it's time to recreate something called the screen. The screen (not to be confused with the screen variable, which is our destination image) is an structure which can be used to manipulate libdraw windows. We will call that screen wscreen.

In order to set it up, we need a fill image to be displayed below the windows. Let's add some code for freeing the old stuff and recreating it:

	Image *fill;

	if(wscreen != nil)
		freeimage(wscreen->fill);
	freescreen(wscreen);
	fill = eallocimage(display, screen->r, screen->chan, 1, basecolhex);
	wscreen = allocscreen(screen, fill, 0);
	if(wscreen == nil)
		error("cannot allocate screen");

You could think that passing the background image directly would be enough. And you would be right, but only if the background is a replicated pattern and you don't care about its origin being misaligned. This intermediate image makes positioning possible.

Now it's time to transfer riaback to fill, and fill to screen. Let's just insert the appropiate draw calls:

	if(wscreen != nil)
		freeimage(wscreen->fill);
	freescreen(wscreen);
	fill = eallocimage(display, screen->r, screen->chan, 1, basecolhex);
	draw(fill, screen->r, riaback, nil, riaback->r.min);
	wscreen = allocscreen(screen, fill, 0);
	if(wscreen == nil)
		error("cannot allocate screen");
	draw(screen, screen->r, fill, nil, fill->r.min);

Finally, as we have drawn stuff to the screen, we must flush the display:

	flushimage(display, 1);

Now our resize function is ready to be called from threadmain. Stick a call right where we left before:

	abortonerror = 1;
	resized();

Threads and synchronization

rio is multithreaded, and makes use of communication channels called, ugh, Channel*. These allow different threads to exchange information safely. We're not going to get into much detail now (as this first iteration doesn't make heavy use of threads), but we'll start using these two primitives:

  1. A channel to send an exit signal.
  2. A thread dedicated to mouse handling.

None of those are really needed right now, but rio has them and for good reason. They will be useful once we add more threads and channels to the mix.

	exitchan = chancreate(sizeof(int), 0);
	threadcreate(mousethread, nil, STACK);
	recv(exitchan, nil);
	threadexitsall(nil);

And that's all! The exit channel will be created, the mouse thread will be spawned and the main thread will wait until someone uses exitchan to gracefully exit from all threads.

The mouse thread

Let's start with a simple thread:

void
mousethread(void*)
{
	threadsetname("mousethread");
	for(;;);
}

It just sits there. Looping forever. Doing nothing. Not good, right? Let's make it handle mouse events!

Remember the mousectl structure we initialized before? It interacts with /dev/mouse and /dev/mousectl in the background, providing an easy-to-use interface using channels. Fortunately, libthread provides a nice way of handling messages coming from multiple channels:

enum
{
	Reshapec,
	Mousec,
};

void
mousethread(void*)
{
	Alt alts[] = {
		{mousectl->resizec, nil, CHANRCV},			/* Reshapec */
		{mousectl->c, &mousectl->Mouse, CHANRCV},	/* Mousec */
		{nil, nil, CHANEND},
	};

	threadsetname("mousethread");

	for(;;) switch(alt(alts)){
	case Reshapec:
		resized();
		break;
	case Mousec:
		break;
	}
}

It loops forever, handling incoming data from the mousectl->resizec and mousectl->c channels. The first one emits reshape events, and the second one emits regular mouse events.

Finally, let's make it exit after pressing the left mouse button:

	case Mousec:
		if(mousectl->buttons & 1)
			send(exitchan, nil);
		break;

That's all, we have finished! Although it doesn't do much, now we have a foundation to build upon.

Suggested hacks

  1. Customize the background image — make it bigger and draw stuff to it.
  2. Load the background image from disk.
  3. Change the button used to exit from ria.

Window handling