Good UI design from the other side - Ultimate Coder

This week I've been thinking a lot about how to design a UI toolkit, and this is about to get very techy, because I would like to talk about API design.

I prefer C to C++ and I'm not particularly fond of Object orientation (Although i use it on occasion). UIs are an area that are often thought of as a place where Object Oriented design really shines, but I think that is because of how we think UIs should be designed. Let's have a look at how one would typically create a button in a UI system:


	void my_button_callback(void *user)

	{

	    printf("Button was clickedn");

	}


	{

	    UIContainer *c;

	    c = create_ui_container();

	    add_ui_button(c, x, y, "Click me!", my_button_callback, NULL);

	    

	    while(TRUE) /* our main loop */

	    {

	        manage_ui_container(c);

	    }

	}

	

The idea here is that we first describe our UI, in some kind of container, and a separate callback for the UI system to call, and then we let the UI system "manage" the UI for us. It's a fair bit of lines and indirection. This works OK if we want a "Fire and forget" UI where we define a static UI once and then use it over and over, but let's say we want to move the button around, then we need something like this:

	void my_button_callback(void *user)

	{

	    printf("Button was clickedn");

	}


	{

	    UIContainer *c;

	    UIElement *e;

	    c = create_ui_container();

	    e = add_ui_button(c, x, y, "Click me!", my_button_callback, NULL);

	    

	    while(TRUE) /* our main loop */

	    {

	        move_ui_element(e, sin(current_time), cos(current_time));

	        manage_ui_container(c);

	    }

	}

	

Now we need to have a lot of handles to manage our UI, We need "c", "e" and the callback "my_button_callback" it's getting very cumbersome. Some development environments prefers to use a special tool to build UIs often with a graphical user interface. They in turn output special UI files that are loaded in to the application, and then we need to read in and query, and we get something like this:

	void my_button_callback(void *user)

	{

	    printf("Button was clickedn");

	}


	{

	    UIContainer *c;

	    UIElement *e;

	    c = create_ui_from_file("my_ui_design.dat");

	    e = query_for_element("button");

	    if(e != NULL)

	        attach_callback_to_element(e, my_button_callback, NULL);


	    while(TRUE) /* our main loop */

	    {

	        if(e != NULL)

	            move_ui_element(e, sin(current_time), cos(current_time));

	        manage_ui_container(c);

	    }

	}

	

This is still more complicated since you have to deal with issues deriving from not knowing the contents of the UI description file, and even if the UI tool provides you with a nice UI it gives you no hints on how to hook it up to your application. Callbacks are especially scary since you can get weird errors if they are declared wrong. So I decided to create a UI system using immediate mode. The same code as above would then look like this:

	    while(TRUE) /* our main loop */

	    {

	        if(my_button(x, y, "Click me!"))

	            printf("Button was clickedn");

	    }

	

How easy was that? No callback, no setup, just a button function that makes a button and returns TRUE if the user clicks on it. The code is WAY more readable and easy to understand and there is no indirection. This all works brilliantly until you actually start making a real interface (I wish everything always worked as good as it does in theory....). If we look at the my_button function we will soon realize that it does two separate things, one is detecting a click, and the other one is to draw a button on the screen. Usually we would like to separate the two out so that we don't have to do them in the same frequency. Well, "Betray" has a nice model for this, it calls the main loop function with 3 different arguments, DRAW, EVENT and MAIN (Advance time) and it's part of the input structure. This means that we an write a main loop function that looks like this:

	void my_main_loop(BInputState *input, void *user_pointer)

	{

	    if(my_button(input, x, y, "Click me!"))

	        printf("Button was clickedn");


	}

	

The my_button function can now it self determine if it's in event, draw, or main mode. At a high level this UI code looks like it does one thing, but in fact it does 3 different things!

A button is a very simple UI element to implement in this way because in a single frame you can determine if it's triggered (you do this by checking if a pointer is over the button and if it's active this frame but wasn't the last), but what about something like a slider? If you grab hold of a slider, but then subsequent frames it needs to remain active, therefore it needs to store state that lasts for more then one frame, and now our immediate mode model breaks. We need a persistent ID for the slider. The code for this may look something like this:

	void my_main_loop(BInputState *input, void *user_pointer)

	{

	    static float value = 0;

	    static void *slider_handle = NULL;

	    

	    if(slider_handle == NULL)

	        slider_handle = create_slider_handle();


	    my_slider(input, slider_handle, &value, x, y, "slider!");

	}

	

It's starting to look an awful lot like the code in the beginning when we need to start keeping track of handles, especially since this code also omits freeing the handle. But wait a minute, if the slider handle only has to be a unique ID, and we don't use it to internally allocate data that needs to be freed, we could use a pointer to anything. In this case we can just use the static value itself.

	void my_main_loop(BInputState *input, void *user_pointer)

	{

	    static float value = 0;

	    my_slider(input, &value, &value, x, y, "slider!");

	}

	

Now everything is simple and pretty again! We can use the pointer to anything we want as unique identifiers, and if we ever need an id, and don't have one we can just use malloc to get more:

	void my_main_loop(BInputState *input, void *user_pointer)

	{

	    float *value;

	    static char *ids = NULL;

	    uint i;


	    value = user_pointer; /* let's assume user_pointer keeps changing for sake of argument. */

	    

	    if(ids == NULL)

	        ids = malloc(2600);

	    for(i = 0; i < 2600; i++)

	        my_slider(input, &ids[i], &value[i], x, y + i, "slider!");

	}

	

Brilliant, this solves everything. Well almost. When we build a UI we want to traverse the description of that data differently depending on what we do. Take this example:

	void my_main_loop(BInputState *input, void *user_pointer)

	{

	    draw_my_desktop(input, &ids[0], "picture_of_a_cat.jpg");

	    if(draw_icon_on_desktop(input, &ids[1], x, y, "software.exe"))

	        execute_software("software.exe");

	    draw_window(input, &ids[2], x2, y2, "files");

	    draw_content_in_window(input, &ids[3]);

	}

	

This all makes very much sense if we are trying to draw. We want to draw the desktop first and then over it we draw the icons, then windows and their content. But what if we are trying to implement the event functionality? When "draw_icon_on_desktop" is called it can't really know if it can be clicked because "draw_window" has not yet been executed, so it can't know if the user is clicking on the icon on the desktop or a window covering it. For event handling purposes it would be much better if the code was written in reverse order like this:

	void my_main_loop(BInputState *input, void *user_pointer)

	{

	    if(draw_content_in_window(input, &ids[0]))

	        return;


	    if(draw_window(input, &ids[1], x2, y2, "files"))

	        return;


	    if(draw_icon_on_desktop(input, &ids[2], x, y, "software"))

	    {

	        execute_software("software");

	        return;

	    }

	    draw_my_desktop(input, &ids[3], "pictiure_of_a_cat.jpg");

	}

	

But this breaks rendering, so does this kill the idea of a immediate mode UI toolkit? No not quite. Our Ids come to the rescue. If we think about why do we click on something? Because we have seen it. If at the time of drawing we store where everything is being drawn, and remove stuff as it is being covered, we can end up with an accurate map of what is clickable and what is covered. When any button wants to know if it is clickable it just looks up its id in to this buffer to know if it is click-able or not. In fact the button doesn't even have to do the look-up, because the structure can once look-up what each pointer is over and then all other widgets can be ignored. As you can imagine making an API deal with all this without the user even noticing it, is a lot of work, but if the goal is to build the ultimate UI toolkit, then work should be expected.

Normally for low latency you want to parse all your inputs first, and then draw it to screen, and this "hack" requires you to operate all input on the previous frame, not the current one. But if you think about it that makes more sense. What do you think the user is clicking on, something they have already seen, or something they expect to see next frame? Creating out collision model while drawing has another benefit, we can use the graphics systems transform to allow the buttons to move. For instance we can do this:

	void my_main_loop(BInputState *input, void *user_pointer)

	{

	    if(input->mode == BAM_DRAW)    

	    {

	        r_matrix_push(NULL); /* similar to glPushMatrix */

	        r_matrix_rotate(NULL, time, 0, 1, 0); /* similar to glRotate */

	    }


	    if(my_button(input, x, y, "Click me!"))

	        printf("Button was clickedn");


	    if(input->mode == BAM_DRAW)

	        r_matrix_pop(NULL); /*  similar to glPopMatrix */

	}

	

Now we can click on button spinning around the screen! OK so now we have made a very pretty system for people who likes to build UIs using code, but what if I want to build it in a nice tool? Well the solution is to build a tool that actually generates UI code. Then it can be used either to build UIs, or as a sample code generator for thous who like to write code.

Next week, we are going to take a look at how the UI I'm working on will actually look and feel.

Para obter informações mais completas sobre otimizações do compilador, consulte nosso aviso de otimização.