Sol on Immediate Mode GUIs (IMGUI)

06 - Keyboard

Source code for this chapter

I've avoided the keyboard handling so far to keep things as simple as possible. As it happens, the keyboard focus is not too difficult to include, but requires small changes all over the place.

The UIState structure gets four new members:

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

The kbditem stores the id of the widget that has the keyboard focus, and keyentered stores the key that was pressed. Keymod stores the key modifier flags (such as shift pressed), and the lastwidget will store the id of the last widget that was processed.

Note that if the frame rate is too low, some keys may be missed. Value zero in kbditem means that no widget has keyboard focus, and zero in keyentered means no key was pressed.

Also note the four new initializer zeros.

First new bits in the button function appear after the 'hotness' check:

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

// If we have keyboard focus, show it
if (uistate.kbditem == id)
  drawrect(x-6, y-6, 84, 68, 0xff0000);

If no widget has keyboard focus, the first one should grab it. We're also rendering a red rectangle behind the widget to make it clear that the widget has keyboard focus:

After the rendering is done, but before the click check we'll see if we need to process any keys:

// 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_RETURN:
      // Had keyboard focus, received return,
      // so we'll act as if we were clicked.
      return 1;
    }
  }

  uistate.lastwidget = id;

The tab logic is pretty simple. The widget that grabs the tab key simply lets go of the keyboard focus, and the next widget that comes along gets the focus. This way the focus also loops around.

Shift-tabbing is also easy (although it does need a couple new variables to work). If shift-tab is detected, we simply give the keyboard focus to the previous widget. This also loops around correctly. The results most likely only appear on the next frame, but you were running at 60 frames per second anyway, right?

Pressing return on a button has the same effect as clicking it.

After the keys are processed, we're updating the lastwidget value.

The scrollbar's changes start pretty much the same way; after the mouse hotness check you'll find the following:

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

  // If we have keyboard focus, show it
  if (uistate.kbditem == id)
    drawrect(x-4, y-4, 40, 280, 0xff0000);

Same as before, the widget needs to grab keyboard focus if nobody has it yet.

For the slider, the keyboard handling is a bit more complicated:

// 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_UP:
      // Slide slider up (if not at zero)
      if (value > 0)
      {
        value--;
        return 1;
      }
      break;
    case SDLK_DOWN:
      // Slide slider down (if not at max)
      if (value < max)
      {
        value++;
        return 1;
      }
      break;
    }
  }

  uistate.lastwidget = id;

The tab processing is same as with the button, but instead of processing the return key, the slider takes the up and down cursor keys, and changes the values accordingly.

We also update the lastwidget id here.

The imgui_finish() function has a couple small additions:

// If no widget grabbed tab, clear focus
if (uistate.keyentered == SDLK_TAB)
  uistate.kbditem = 0;
// Clear the entered key
uistate.keyentered = 0;

If no widget has grabbed the TAB key, we're resetting the keyboard focus. This should never happen, as long as you remember to reset the keyboard focus whenever the UI changes.

We're also clearing the entered key so that the scroll bars don't keep scrolling forever (among other things).

Surprisingly, the render function doesn't have any changes.

The main function has a couple of changes. First, right after the screen creation, we're enabling the key repeat:

// Enable keyboard repeat to make sliders more tolerable
SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL);

Like the comment says, this is to make the sliders more tolerable. You may wish to enable and disable the key repeat when entering in-game (if applicable) if your game logic gets confused due to the extranous SDL_KEYDOWN messages. Speaking of which..

case SDL_KEYDOWN:
  // If a key is pressed, report it to the widgets
  uistate.keyentered = event.key.keysym.sym;
  uistate.keymod = event.key.keysym.mod;
  break;

The last change is to report the key down events.

The two widgets that we have share some code (namely the tab processing, hotness and keyboard focus checks), but making these separate functions would probably be an overkill. And who knows, maybe you need a non-rectangle 'hot' region.

Also note that it's possible to create widgets that never have focus, i.e. progress bars, static text, etc.

There are some edge cases that are not automatically handled by this code which would be trivial to include (like some logic to check that the currently selected widget actually exists), but for the sake of simplicity I've left them out.

Next we'll look at a simple text input field.

Next: 07 - Text field

Any comments etc. can be emailed to me.