TRACK_ZERO


Windowing Systems by Example: 6 - Control Issues Part I


So here's where our window manager starts to actually get really exciting: We have a desktop, and the desktop contains windows. But the windows themselves contain bupkis. What the hell good to anyone is a window that doesn't actually have anything in it? It's time to change that. It's time to implement controls.

There's a really obvious thought staring us in the face that you might not even have realized: What if our windows could have windows in them? I mean, what's so special about a desktop? It's a rectangular region with windows inside of it. Why couldn't we do the same with a window itself?

So your immediate thought might be: 'Well, that's not so weird, but I thought we were going to be talking about controls, not nested windows'. But think about it this way: What if our controls were windows?

A window's defining feature is that it has a location, has a size, displays visible content within its area, can contain other objects (controls or child windows), and responds when you interact with it. A button has a location, has a size, displays visible content within its area, can contain other objects (say an image) and responds when you interact with it. A desktop has a location -- (0, 0) --, a size, displays visible content within its area, can contain other objects (windows), and responds when you click it. These are all basically the same thing, with minor differences in the details. So why not unify them?

WARNING: This week, we will not end up with functioning code. We have a lot of work to do to get controls happening, hence why this is being broken into two parts. Today, we'll modify our classes so that everything is a window and we'll update our Context to support our nested drawing. Next week we'll implement the actual drawing and interception of mouse events and end up with a working system.

 

The Desktop Shall Inherit the Window

The takeaway: Everything's going to be a window. Most importantly, Windows need to be able to do a lot of the stuff that the Desktop has been doing that we mentioned above, namely containing other windows, drawing them and passing mouse events to them.

But we still have some stuff that our Desktop is going to do that Windows, in general, don't. Like keeping track of and drawing the mouse cursor. If you're familiar with an OOP language like C++, you'd probably recognize this as a good candidate for using inheritance. But is there a way to do that in C?

There is, and to show you how we do it, I'll show you how we're going to rewrite our Desktop struct:

typedef struct Desktop_struct {  
    Window window; //Inherits window class
    uint16_t mouse_x;
    uint16_t mouse_y;
} Desktop;

If you've never dealt with this design before, it's one of the cooler legal hacks we can use in C to allow for a type of inheritance. According to the C spec, the address of the first element of a struct and the address of the struct itself will always be equivalent. Therefore, by putting a Window at the beginning of the desktop struct we can cast any Desktop pointer into a Window pointer and pass it to any Window_... function without the function being any the wiser.

 

So, we've almost completely eviscerated our desktop struct. To make up for that, we're going to continue our work by inserting the properties we just stripped out of Desktop into the Window class. As mentioned, most importantly this means giving Windows each a list of child windows.

However, while we're at it, we're also going to throw in three new things: The first is a reference to a parent window -- since we're going to be nesting windows inside windows, we're going to want this to traverse the tree of windows in our code. The second is an unsigned int called 'flags' that we're going to use as a bit field. For now, we'll just be using it to store whether our window is going to be decorated or not since buttons don't have titlebars. The final addition is going to be a function pointer called 'paint_function' -- just put it in for now, and I'll explain what that's for down below.

//Forward struct declaration for function type declarations
struct Window_struct;

//Callback function type declarations
typedef void (*WindowPaintHandler)(struct Window_struct*);

typedef struct Window_struct {  
    struct Window_struct* parent; //New
    int16_t x;
    int16_t y;
    uint16_t width;
    uint16_t height;
    uint16_t flags; //New
    Context* context; 
    List* children; //New
    uint8_t last_button_state; //New
    WindowPaintHandler paint_function; //New
    struct Window_struct* drag_child; //New
    int16_t drag_off_x; //New
    int16_t drag_off_y; //New
} Window;

 

While we're screwing around with our class declaration stuff, we're going to add a few more defines that will help us out later. To help us do math involving the window border (or lack thereof) later on, we'll centralize its dimensions, and we'll also define the flag bit for turning a window's decoration off.

#define WIN_TITLEHEIGHT 31 
#define WIN_BORDERWIDTH 3

//Some flags to define our window behavior
#define WIN_NODECORATION 0x1

 

Now, what's the story with that paint_function function pointer?

So far, all of our windows have used the exact same function in the exact same way to draw the exact same thing. It may be obvious: This is completely untenable for a real system. Different kinds of controls need to draw themselves differently, and custom user controls need to give the user complete control over how they're drawn. Also, now that our desktop is just a kind of window we certainly can't draw the same thing there that we do in the rest of our windows.

paint_function is our way of handling that. What we'll do is split window painting into two concerns: Context setup and actual drawing. The setup will always be the same for every window, so we'll bake that into a fixed function later on. But once the setup is finished, to let the window draw whatever it needs to, we'll call whatever its paint_function points to. Then that function can be set or overridden in a constructor or elsewhere.

For now, let's throw together a 'default' window painting function so that we have something to assign that pointer to when we first initialize a vanilla window:

//This is the default paint method for a new window
void Window_paint_handler(Window* window) {

    //Fill in the window background
    Context_fill_rect(window->context, 0, 0,
                      window->width, window->height, WIN_BGCOLOR);
}

In the same way, we can make a similar function that we can use later to draw the desktop:

//Paint the desktop 
void Desktop_paint_handler(Window* desktop_window) {

    //Fill the desktop
    Context_fill_rect(desktop_window->context, 0, 0, desktop_window->context->width, desktop_window->context->height, 0xFFFF9933);
}

 

Now that we've changed all of its members, we need to look into modifying the window constructor a bit. To begin with, of course, we need to update its arguments to take the initial window flags, and we also need to add the initialization of our new properties. But beyond that, we're also going to split out the initialization of the class from the allocation of the class.

This has to do with the way our inheritance works for the Desktop class. The Window part of the desktop initialization will be exactly the same as for any other window, so for that we'll just have our Desktop constructor use the exact same initialization code that normal Windows use. But a Desktop structure is bigger than a Window structure, so the malloc(sizeof(Window)) we use in Window_new() is no good for allocating the memory needed for a Desktop. If we split out the initialization of a Window from our Window constructor, we can let both constructors use it and deal with setting up the right allocation size independently.

//Updated window constructor
Window* Window_new(int16_t x, int16_t y, uint16_t width,  
                   uint16_t height, uint16_t flags, Context* context) {

    //Try to allocate space for a new WindowObj and fail through if malloc fails
    Window* window;
    if(!(window = (Window*)malloc(sizeof(Window))))
        return window;

    //Attempt to initialize the new window 
    if(!Window_init(window, x, y, width, height, flags, context)) {

        free(window);
        return (Window*)0;
    }

    return window;
}

//Seperate object allocation from initialization so we can implement
//our inheritance scheme
int Window_init(Window* window, int16_t x, int16_t y, uint16_t width,  
                uint16_t height, uint16_t flags, Context* context) {

    //Moved over here from the desktop 
    //Create child list or clean up and fail
    if(!(window->children = List_new()))
        return 0;

    //Assign the property values
    window->x = x;
    window->y = y;
    window->width = width;
    window->height = height;
    window->context = context;
    window->flags = flags;
    window->parent = (Window*)0; //We'll assign a parent later -- or not
    window->last_button_state = 0;
    window->paint_function = Window_paint_handler;
    window->drag_child = (Window*)0;
    window->drag_off_x = 0;
    window->drag_off_y = 0;

    return 1;
}

 

Now that we've modified our Window constructor to reflect our merging of Desktop functionalities, let's do the Desktop constructor as well. For the most part, we're going to now let Window_init() handle most of our initialization now that Desktop is just a glorified, undecorated Window. But still after that we still have to do the minor housekeeping of updating the paint_function to the desktop painting handler we wrote earlier and then initializing the desktop-unique properties:

Desktop* Desktop_new(Context* context) {

    //Malloc or fail 
    Desktop* desktop;
    if(!(desktop = (Desktop*)malloc(sizeof(Desktop))))
        return desktop;

    //Initialize the Window bits of our desktop (just an
    //undecorated window the size of the drawing context)
    if(!Window_init((Window*)desktop, 0, 0, context->width, context->height, WIN_NODECORATION, context)) {

        free(desktop);
        return (Desktop*)0;
    }

    //Here's where we override that paint function to draw the
    //desktop background instead of the default window background
    desktop->window.paint_function = Desktop_paint_handler;

    //Now continue by filling out the desktop-unique properties 
    desktop->mouse_x = desktop->window.context->width / 2;
    desktop->mouse_y = desktop->window.context->height / 2;

    return desktop;
}

 

Next thing we have to do in order to integrate the common functions of Window and Desktop is move a few functions we want to apply to both over from the desktop class into the window class. Namely:

  • List* Desktop_get_windows_above(Desktop* desktop, Window* child)
  • void Desktop_process_mouse(Desktop* desktop, uint16_t mouse_x, uint16_t mouse_y, uint8_t mouse_buttons)
  • Window* Desktop_create_window(Desktop* desktop, int16_t x, int16_t y, uint16_t width, int16_t height, uint16_t flags)

Literally all you have to do is go through those functions, rename them all to Window_... instead of Desktop_... and replace all instances of Desktop* desktop with Window* window. Not even going to cover that much, because it's pretty trivial. But when you're done with that, we have some minor changes to make.

We'll be completely deleting Desktop_paint() as well, but we won't be moving it over to the Window class. We'll be completely rewriting Window_paint() later and we're going to use that to handle both the desktop drawing and all of the window drawing.

The first minor change we need to make to the functions we just moved: The way we're currently handling top-down drawing using the desktop. As it was, we were handling all mouse input in the Desktop_process_mouse() and, at the end of that routine, forcing a redraw of all our children which was always finalized by drawing the mouse over everything.

For now, we still want that to be the case, but unfortunately we moved that function over to the Window class. That much is fine, because we want our windows eventually to handle their own mouse events no matter how deeply nested. But if we keep drawing the desktop in that function, the desktop redraw is going to get called on every single window that gets a mouse event, which is certainly not what we want. So, to start with, let's clip this bit of code out of Window_process_mouse():

//Tail end of Window_process_mouse():

    //Desktop_paint(desktop); <- Remove this bit
                                 (this function doesn't even exist anymore)

    //Update the stored mouse button state to match the current state 
    desktop->last_button_state = mouse_buttons;
}

There, now window mouse events won't wrongfully trigger a full-screen redraw. But we do need to do that somewhere. So we'll write a small function for the Desktop which we'll be able to call in our main() that will call Window_process_mouse() to do the normal Desktop mouse handling, but, when that's complete, do the screen redraw and then draw that mouse we stripped out:

//Our overload of the Window_process_mouse function used to capture the screen mouse position 
void Desktop_process_mouse(Desktop* desktop, uint16_t mouse_x,  
                           uint16_t mouse_y, uint8_t mouse_buttons) {

    //Capture the mouse location in order to draw it later
    desktop->mouse_x = mouse_x;
    desktop->mouse_y = mouse_y;

    //Do the old generic mouse handling
    Window_process_mouse((Window*)desktop, mouse_x, mouse_y, mouse_buttons);

    //Now that we've handled any changes the mouse may have caused, we need to
    //update the screen to reflect those changes 
    Window_paint((Window*)desktop);

    //And finally draw the hacky mouse, as usual
    Context_fill_rect(desktop->window.context, desktop->mouse_x, desktop->mouse_y, 10, 10, 0xFF000000);
}

 

One more small change before we're done with the basic task of making windows nestable: We need to set that window->parent property at some point so that we can navigate the tree of windows in our code. Easy enough, this is just a quick change to our Window_create_window() function:

//The tail end of Window_create_window():

    //Set the new child's parent 
    new_window->parent = window;

    return new_window;
}

 

Okay! So that's that! We've moved the bulk of our desktop properties and functions over to windows and made desktops a subclass of window, so now windows can contain windows can contain windows. Now we just have to tackle those minor Context changes.

 

Actually Getting This Junk on the Screen

We still have a bunch of work to do when it comes to window painting -- and even then, we won't actually start painting anything until Part II. The thing is, now that we're allowing for child windows, we have to change the way we're doing our drawing yet again. And before we can do that, we have to do a bit of surgery on our Context to support those changes.

First and easiest: When we call those paint_functions on nested child windows, we want them to not have to know anything about where they are on the screen. We only want the window manager to care about that -- as far as our windows will be concerned their top left corner should be (0, 0). To do this, we can add simple support to our Context for changing the location of the drawing origin. To do that, let's start by adding a translate_x and translate_y property to the context:

typedef struct Context_struct {  
    uint32_t* buffer; 
    uint16_t width; 
    uint16_t height; 
    int translate_x; //Our new translation values
    int translate_y;
    List* clip_rects;
} Context;

 

The idea here is that before we call the paint_function on a window, we can set the context's translate_x and translate_y to the top-left corner of that window and then the paint_function doesn't have to do any weird calculation themselves to determine where things go. To make that actually happen, we just need to use those values to shift things when we draw our clipped rectangles:

void Context_clipped_rect(Context* context, int x, int y, unsigned int width,  
                          unsigned int height, Rect* clip_area, uint32_t color) {

    int cur_x;
    int max_x = x + width;
    int max_y = y + height;

    //NEW: Translate the rectangle coordinates by the context translation values
    x += context->translate_x;
    y += context->translate_y;
    max_x += context->translate_x;
    max_y += context->translate_y;

    //And then the usual stuff:
    if(x < clip_area->left)
        x = clip_area->left;

    if(y < clip_area->top)
        y = clip_area->top;

    if(max_x > clip_area->right + 1)
        max_x = clip_area->right + 1;

    if(max_y > clip_area->bottom + 1)
        max_y = clip_area->bottom + 1;

    for(; y < max_y; y++) 
        for(cur_x = x; cur_x < max_x; cur_x++) 
            context->buffer[y * context->width + cur_x] = color;
}

With that, since all of our current drawing ultimately calls down to that function, everything we draw will be properly offset now.

 

We have another little bit of graphics groundwork to cover before I can let you go for the day, though. To do what we need to do when we actually start getting into putting pixels on the screen in Part II, we need to add one more boolean operation to the context's clipping rectangles: Intersection.

Just like subtraction and addition, we'll start out by implementing a simple function that does intersection just between two rectangles. We'll do this by basically doing the same thing as our Rect_split() code, but this time we'll use the shrinking rectangle as our output:

/*
 ___a
|  _|_b      _c
|_|_| | --> |_| 
  |___|
*/
Rect* Rect_intersect(Rect* rect_a, Rect* rect_b) {

    Rect* result_rect;

    //If the two rectangles don't overlap, there's no result
    if(!(rect_a->left <= rect_b->right &&
       rect_a->right >= rect_b->left &&
       rect_a->top <= rect_b->bottom &&
       rect_a->bottom >= rect_b->top))
        return (Rect*)0;

    //The result rectangle starts out as a copy of the first input rect
    if(!(result_rect = Rect_new(rect_a->top, rect_a->left,
                                rect_a->bottom, rect_a->right)))
        return (Rect*)0;

    //Shrink to the right-most left edge
    if(rect_b->left >= result_rect->left && rect_b->left <= result_rect->right) 
        result_rect->left = rect_b->left;

    //Shrink to the bottom-most top edge
    if(rect_b->top >= result_rect->top && rect_b->top <= result_rect->bottom) 
        result_rect->top = rect_b->top;

    //Shrink to the left-most right edge
    if(rect_b->right >= result_rect->left && rect_b->right <= result_rect->right)
        result_rect->right = rect_b->right;

    //Shrink to the top-most bottom edge
    if(rect_b->bottom >= result_rect->top && rect_b->bottom <= result_rect->bottom)
        result_rect->bottom = rect_b->bottom;

    return result_rect;
}

 

Now, to use it to create an intersection against our Context's whole clipping rectangle collection. The intersection of the whole set of clipping rectangles is just the collective results of the intersections of the input rectangle and each clipping rectangle in turn:

//Update the clipping rectangles to only include those areas within both the
//existing clipping region AND the passed Rect
void Context_intersect_clip_rect(Context* context, Rect* rect) {

    int i;
    List* output_rects;
    Rect* current_rect;
    Rect* intersect_rect;

    //Allocate a new list of rectangles into which we'll put
    //our intersection results
    if(!(output_rects = List_new()))
        return;

    //Do an intersection against the passed rectangle for each 
    //rectangle in clip_rects
    for(i = 0; i < context->clip_rects->count; i++) {

        //Get the next clip_rect and do the intersection
        current_rect = (Rect*)List_get_at(context->clip_rects, i);
        intersect_rect = Rect_intersect(current_rect, rect);

        //If there was a result, put it in the output list
        if(intersect_rect)
            List_add(output_rects, intersect_rect);
    }

    //Now that we're done, we can delete the original list of 
    //clipping rectangles
    while(context->clip_rects->count)
        List_remove_at(context->clip_rects, 0);
    free(context->clip_rects);

    //And replace it with the intersection results
    context->clip_rects = output_rects;

    //Now that we're done with the input rect, we'll free it
    free(rect);
}

 

The Unsatisfying End

That's all for today. A whole lot of infrastructure and nothing to show for it. It's a little frustrating, I know, but there's a lot of changes that need to be made for this particular subject. By moving a lot of the window management functionality from the desktop into the windows themselves, we've got the structural framework now to manage and interact with nested windows and controls. And with the small changes we've made to our Context we'll be able to properly clip and draw those objects to any arbitrary nesting.

I tried to put all of the changes into one article at first, but it would've been twice as long as the longest article I've done so far, and I don't want to kill you. Part of the reason I was a bit late this week, too.

But still, feel free to let your excitement build, because you have a lot to look forward to in Part II. By the end of Part II we'll have implemented the drawing and mouse event handling of sub-windows/controls and written some controls that, like we've done with Desktop in this article, inherit from Window to get the bulk of their functionality done. And it's not long from there that our windowing system will be, for most intents and purposes, finished!


AUTHOR'S NOTE: Normally, this is the part where I link to the code. But frankly, the code isn't quite ready and won't be until I post Part II. Bummer, I know. That said, you probably already know where the repo is if you've been reading these, so you're probably going to look at it anyway. Whatever.