Sol on Immediate Mode GUIs (IMGUI)

07 - Text field

Source code for this chapter

Font data file for this chapter

Text input is the last really mandatory widget type to cover. There are other kinds of useful widgets out there (like selection boxes, spinners, how to handle dialog boxes and so on), but without a text input field it's a bit difficult to do many interesting things such as custom character names, highscore tables and so on.

To be able to make a text input field, we need a way to output text as well. For this we need a font. I've provided a simple (and ugly) font. The font format is quite simple, and the required code is also relatively simple.

First, we need a new global variable for the font surface:

// Font surface
SDL_Surface *gFont;

We also need to load the font data, so some new code appears in the main function, just before the main loop starts:

SDL_Surface *temp = SDL_LoadBMP("font14x24.bmp");
gFont = SDL_ConvertSurface(temp, gScreen->format, SDL_SWSURFACE);
SDL_FreeSurface(temp);

// Seeing through the black areas in the font may be a good idea..
SDL_SetColorKey(gFont, SDL_SRCCOLORKEY, 0);

The printing routines are extremely simple. The idea is the same as with my graphics tutorial simple font printing, but instead of plotting pixels ourselves, we're using SDL's blit here.

// Draw a single character.
// Characters are on top of each other in the font image, in ASCII order,
// so all this routine does is just set the coordinates for the character
// and use SDL to blit out.
void drawchar(char ch, int x, int y)
{
  SDL_Rect src, dst;
  src.w = 14;
  src.h = 24;
  src.x = 0;
  src.y = (ch - 32) * 24;
  dst.w = 14;
  dst.h = 24;
  dst.x = x;
  dst.y = y;
  SDL_BlitSurface(gFont, &src, gScreen, &dst);
}

// Draw the string. Characters are fixed width, so this is also
// deadly simple.
void drawstring(char * string, int x, int y)
{
  while (*string)
  {
    drawchar(*string,x,y);
    x += 14;
    string++;
  }
}

We can try these routines out by using drawstring() in the render function to draw a hello world somewhere.

In order to input text, we need to get the actual character wanted from SDL. We can't use the normal key system, as we don't have any idea what kind of character should appear if the user pressed right alt and 2 for instance - in a Finnish keyboard, that generates the @-character, but the results might be completely different on some other keyboard.

Luckily, SDL has the capability of translating key combinations into characters. It needs to be enabled though, as it takes some processing time. Like with the key repeat, you may wish to disable it when not handling UI elements. The following appears in main, after the key repeat bit:

// Enable keyboard UNICODE processing for the text field.
SDL_EnableUNICODE(1);

We have to expand the UI state again to receive the translated character data. The current UI state looks like this:

struct UIState
{
  int mousex;
  int mousey;
  int mousedown;

  int hotitem;
  int activeitem;

  int kbditem;
  int keyentered;
  int keymod;
  int keychar;
  
  int lastwidget;
} 
uistate = {0,0,0,0,0,0,0,0,0,0};

Next, in the SDL_KEYDOWN event, we need to grab the character. Since we won't be able to render all UNICODE characters, we'll ignore all non-ASCII keys:

// if key is ASCII, accept it as character input
if ((event.key.keysym.unicode & 0xFF80) == 0)
  uistate.keychar = event.key.keysym.unicode & 0x7f;

Finally, imgui_finish() has to clear the entered key so it won't get stuck.

uistate.keychar = 0;

The beginning of the textfield function should look rather familiar by now:

int textfield(int id, int x, int y, char *buffer)
{
  int len = strlen(buffer);
  int changed = 0;

  // Check for hotness
  if (regionhit(x-4, y-4, 30*14+8, 24+8))
  {
    uistate.hotitem = id;
    if (uistate.activeitem == 0 && uistate.mousedown)
      uistate.activeitem = id;
  }

  // If no widget has keyboard focus, take it
  if (uistate.kbditem == 0)
    uistate.kbditem = id;

In the beginning of the function we're using strlen() to calculate the current length of the string. Note that we'll need to add <string.h> include in order to have access to the strlen() function.

The rendering bit is also rather straight forward..

// If we have keyboard focus, show it
if (uistate.kbditem == id)
  drawrect(x-6, y-6, 30*14+12, 24+12, 0xff0000);

// Render the text field
if (uistate.activeitem == id || uistate.hotitem == id)
{
  drawrect(x-4, y-4, 30*14+8, 24+8, 0xaaaaaa);
}
else
{
  drawrect(x-4, y-4, 30*14+8, 24+8, 0x777777);
}

drawstring(buffer,x,y);

// Render cursor if we have keyboard focus
if (uistate.kbditem == id && (SDL_GetTicks() >> 8) & 1)
  drawstring(&quot;_&quot;,x + len * 14, y);</pre>

Note that the text field is limited to 30 characters, and hard-wired to that amount, for simplicity's sake. You most likely will want to make that configurable in a practical situation.

As an additional gimmick, we're rendering a blinking cursor after the text if the widget has focus.

Keyboard focus has the familiar tab handling, as well as backspace and new character inputs:

// If we have keyboard focus, we'll need to process the keys
if (uistate.kbditem == id)
{
  switch (uistate.keyentered)
  {
  case SDLK_TAB:
    // If tab is pressed, lose keyboard focus.
    // Next widget will grab the focus.
    uistate.kbditem = 0;
    // If shift was also pressed, we want to move focus
    // to the previous widget instead.
    if (uistate.keymod & KMOD_SHIFT)
      uistate.kbditem = uistate.lastwidget;
    // Also clear the key so that next widget
    // won't process it
    uistate.keyentered = 0;
    break;
  case SDLK_BACKSPACE:
    if (len > 0)
    {
      len--;
      buffer[len] = 0;
      changed = 1;
    }
    break;      
  }
  if (uistate.keychar >= 32 && uistate.keychar < 127 && len < 30)
  {
    buffer[len] = uistate.keychar;
    len++;
    buffer[len] = 0;
    changed = 1;
  }
}

The backspace simply overwrites the last character with a zero, and the new characters simply overwrite the last zero with the new character, and add a new zero after them.

Since it's nice to move focus with a mouse, we're checking if the widget was clicked (much like a button), and if yes, we're moving keyboard focus there. This may be desirable behavior for all widgets.

// If button is hot and active, but mouse button is not
// down, the user must have clicked the widget; give it 
// keyboard focus.
if (uistate.mousedown == 0 && 
  uistate.hotitem == id && 
  uistate.activeitem == id)
  uistate.kbditem = id;

Finally, the widget is stored as the latest widget and we're returning 1 if the text field changed or 0 if it didn't.

uistate.lastwidget = id;

  return changed;
}

What's left is to check if it actually works. The render()-function gets a new static variable with room to spare:

static char sometext[80] = "Some text";

And finally, the textfield function gets a call. I placed this after the buttons.

textfield(GEN_ID,50,250,sometext);

In addition to varying sizes of the text field, one could implement cursor movement (storing the cursor position on client side), multi-line text input, and so on.

I could (and possibly will) continue on with other kinds of widgets, like a tree view, but I've now covered most of the required bits.

The IMGUIs are definitely not perfect for all occasions, but they're relatively lightweight and simple to implement. I hope you've had fun with this tutorial - and as always, comments are appreciated.

That's all for now.

Next: Appendix A - Further Reading

Any comments etc. can be emailed to me.