/***********************************************************
* Artsoft Retro-Game Library                               *
*----------------------------------------------------------*
* (c) 1994-2006 Artsoft Entertainment                      *
*               Holger Schemel                             *
*               Detmolder Strasse 189                      *
*               33604 Bielefeld                            *
*               Germany                                    *
*               e-mail: info@artsoft.org                   *
*----------------------------------------------------------*
* gadgets.c                                                *
***********************************************************/

#include <stdarg.h>
#include <string.h>

#include "gadgets.h"
#include "text.h"
#include "misc.h"


/* values for DrawGadget() */
#define DG_UNPRESSED		0
#define DG_PRESSED		1

#define DG_BUFFERED		0
#define DG_DIRECT		1

static struct GadgetInfo *gadget_list_first_entry = NULL;
static struct GadgetInfo *gadget_list_last_entry = NULL;
static struct GadgetInfo *last_info_gi = NULL;
static int next_free_gadget_id = 1;
static boolean gadget_id_wrapped = FALSE;

static void (*PlayGadgetSoundActivating)(void) = NULL;
static void (*PlayGadgetSoundSelecting)(void) = NULL;


void InitGadgetsSoundCallback(void (*activating_function)(void),
			      void (*selecting_function)(void))
{
  PlayGadgetSoundActivating = activating_function;
  PlayGadgetSoundSelecting = selecting_function;
}

static struct GadgetInfo *getGadgetInfoFromGadgetID(int id)
{
  struct GadgetInfo *gi = gadget_list_first_entry;

  while (gi && gi->id != id)
    gi = gi->next;

  return gi;
}

static int getNewGadgetID()
{
  int id = next_free_gadget_id++;

  if (next_free_gadget_id <= 0)		/* counter overrun */
  {
    gadget_id_wrapped = TRUE;		/* now we must check each ID */
    next_free_gadget_id = 0;
  }

  if (gadget_id_wrapped)
  {
    next_free_gadget_id++;
    while (getGadgetInfoFromGadgetID(next_free_gadget_id) != NULL)
      next_free_gadget_id++;
  }

  if (next_free_gadget_id <= 0)		/* cannot get new gadget id */
    Error(ERR_EXIT, "too much gadgets -- this should not happen");

  return id;
}

static struct GadgetInfo *getGadgetInfoFromMousePosition(int mx, int my,
							 int button)
{
  struct GadgetInfo *gi;

  /* first check for scrollbars in case of mouse scroll wheel button events */
  if (IS_WHEEL_BUTTON(button))
  {
    /* real horizontal wheel or vertical wheel with modifier key pressed */
    boolean check_horizontal = (IS_WHEEL_BUTTON_HORIZONTAL(button) ||
				GetKeyModState() & KMOD_Shift);

    /* check for the first active scrollbar with matching mouse wheel area */
    for (gi = gadget_list_first_entry; gi != NULL; gi = gi->next)
    {
      if (gi->mapped && gi->active &&
	  ((gi->type & GD_TYPE_SCROLLBAR_HORIZONTAL && check_horizontal) ||
	   (gi->type & GD_TYPE_SCROLLBAR_VERTICAL && !check_horizontal)) &&
	  mx >= gi->wheelarea.x && mx < gi->wheelarea.x + gi->wheelarea.width &&
	  my >= gi->wheelarea.y && my < gi->wheelarea.y + gi->wheelarea.height)
	return gi;
    }

    /* no active scrollbar found -- ignore this scroll wheel button event */
    return NULL;
  }

  /* open selectboxes may overlap other active gadgets, so check them first */
  for (gi = gadget_list_first_entry; gi != NULL; gi = gi->next)
  {
    if (gi->mapped && gi->active &&
	gi->type & GD_TYPE_SELECTBOX && gi->selectbox.open &&
	mx >= gi->selectbox.x && mx < gi->selectbox.x + gi->selectbox.width &&
	my >= gi->selectbox.y && my < gi->selectbox.y + gi->selectbox.height)
      return gi;
  }

  /* check all other gadgets */
  for (gi = gadget_list_first_entry; gi != NULL; gi = gi->next)
  {
    if (gi->mapped && gi->active &&
	mx >= gi->x && mx < gi->x + gi->width &&
	my >= gi->y && my < gi->y + gi->height)
      return gi;
  }

  return NULL;
}

static void setTextAreaCursorExt(struct GadgetInfo *gi, boolean set_cursor_pos)
{
  char *text = gi->textarea.value;
  int area_xsize = gi->textarea.xsize;
  int area_ysize = gi->textarea.ysize;
  int cursor_position = gi->textarea.cursor_position;
  int cursor_x = gi->textarea.cursor_x;
  int cursor_y = gi->textarea.cursor_y;
  int pos = 0;
  int x = 0;
  int y = 0;

  while (*text)
  {
    if (set_cursor_pos)		/* x/y => position */
    {
      if (y == cursor_y && (x == cursor_x || (x < cursor_x && *text == '\n')))
	break;
    }
    else			/* position => x/y */
    {
      if (pos == cursor_position)
	break;
    }

    if (x + 1 >= area_xsize || *text == '\n')
    {
      if (y + 1 >= area_ysize)
	break;

      x = 0;
      y++;
    }
    else
      x++;

    text++;
    pos++;
  }

  gi->textarea.cursor_x = x;
  gi->textarea.cursor_y = y;
  gi->textarea.cursor_x_preferred = x;
  gi->textarea.cursor_position = pos;
}

static void setTextAreaCursorXY(struct GadgetInfo *gi, int x, int y)
{
  gi->textarea.cursor_x = x;
  gi->textarea.cursor_y = y;

  setTextAreaCursorExt(gi, TRUE);
}

static void setTextAreaCursorPosition(struct GadgetInfo *gi, int pos)
{
  gi->textarea.cursor_position = pos;

  setTextAreaCursorExt(gi, FALSE);
}

static void default_callback_info(void *ptr)
{
  return;
}

static void default_callback_action(void *ptr)
{
  return;
}

static void DrawGadget(struct GadgetInfo *gi, boolean pressed, boolean direct)
{
  struct GadgetDesign *gd;
  int state = (pressed ? GD_BUTTON_PRESSED : GD_BUTTON_UNPRESSED);
  boolean redraw_selectbox = FALSE;

  if (gi == NULL)
    return;

  gd = (!gi->active ? &gi->alt_design[state] :
 	gi->checked ? &gi->alt_design[state] : &gi->design[state]);

  switch (gi->type)
  {
    case GD_TYPE_NORMAL_BUTTON:
    case GD_TYPE_CHECK_BUTTON:
    case GD_TYPE_RADIO_BUTTON:
      BlitBitmapOnBackground(gd->bitmap, drawto,
			     gd->x, gd->y, gi->width, gi->height,
			     gi->x, gi->y);
      if (gi->deco.design.bitmap)
	BlitBitmap(gi->deco.design.bitmap, drawto,
		   gi->deco.design.x, gi->deco.design.y,
		   gi->deco.width, gi->deco.height,
		   gi->x + gi->deco.x + (pressed ? gi->deco.xshift : 0),
		   gi->y + gi->deco.y + (pressed ? gi->deco.yshift : 0));
      break;

    case GD_TYPE_TEXT_BUTTON:
      {
	int i;
	int font_nr = (gi->active ? gi->font_active : gi->font);
	int font_width = getFontWidth(font_nr);
	int border_x = gi->border.xsize;
	int border_y = gi->border.ysize;
	int text_size = strlen(gi->textbutton.value);
	int text_start = (gi->width - text_size * font_width) / 2;

	/* left part of gadget */
	BlitBitmapOnBackground(gd->bitmap, drawto, gd->x, gd->y,
			       border_x, gi->height, gi->x, gi->y);

	/* middle part of gadget */
	for (i=0; i < gi->textbutton.size; i++)
	  BlitBitmapOnBackground(gd->bitmap, drawto, gd->x + border_x, gd->y,
				 font_width, gi->height,
				 gi->x + border_x + i * font_width, gi->y);

	/* right part of gadget */
	BlitBitmapOnBackground(gd->bitmap, drawto,
			       gd->x + gi->border.width - border_x, gd->y,
			       border_x, gi->height,
			       gi->x + gi->width - border_x, gi->y);

	/* gadget text value */
	DrawTextExt(drawto,
		    gi->x + text_start + (pressed ? gi->deco.xshift : 0),
		    gi->y + border_y   + (pressed ? gi->deco.yshift : 0),
		    gi->textbutton.value, font_nr, BLIT_MASKED);
      }
      break;

    case GD_TYPE_TEXT_INPUT_ALPHANUMERIC:
    case GD_TYPE_TEXT_INPUT_NUMERIC:
      {
	int i;
	char cursor_letter;
	char cursor_string[2];
	char text[MAX_GADGET_TEXTSIZE + 1];
	int font_nr = (pressed ? gi->font_active : gi->font);
	int font_width = getFontWidth(font_nr);
	int border_x = gi->border.xsize;
	int border_y = gi->border.ysize;

	/* left part of gadget */
	BlitBitmapOnBackground(gd->bitmap, drawto, gd->x, gd->y,
			       border_x, gi->height, gi->x, gi->y);

	/* middle part of gadget */
	for (i=0; i < gi->textinput.size + 1; i++)
	  BlitBitmapOnBackground(gd->bitmap, drawto, gd->x + border_x, gd->y,
				 font_width, gi->height,
				 gi->x + border_x + i * font_width, gi->y);

	/* right part of gadget */
	BlitBitmapOnBackground(gd->bitmap, drawto,
			       gd->x + gi->border.width - border_x, gd->y,
			       border_x, gi->height,
			       gi->x + gi->width - border_x, gi->y);

	/* set text value */
	strcpy(text, gi->textinput.value);
	strcat(text, " ");

	/* gadget text value */
	DrawTextExt(drawto,
		    gi->x + border_x, gi->y + border_y, text,
		    font_nr, BLIT_MASKED);

	cursor_letter = gi->textinput.value[gi->textinput.cursor_position];
	cursor_string[0] = (cursor_letter != '\0' ? cursor_letter : ' ');
	cursor_string[1] = '\0';

	/* draw cursor, if active */
	if (pressed)
	  DrawTextExt(drawto,
		      gi->x + border_x +
		      gi->textinput.cursor_position * font_width,
		      gi->y + border_y, cursor_string,
		      font_nr, BLIT_INVERSE);
      }
      break;

    case GD_TYPE_TEXT_AREA:
      {
	int i;
	char cursor_letter;
	char cursor_string[2];
	int font_nr = (pressed ? gi->font_active : gi->font);
	int font_width = getFontWidth(font_nr);
	int font_height = getFontHeight(font_nr);
	int border_x = gi->border.xsize;
	int border_y = gi->border.ysize;
	int gd_height = 2 * border_y + font_height;

	/* top left part of gadget border */
	BlitBitmapOnBackground(gd->bitmap, drawto, gd->x, gd->y,
			       border_x, border_y, gi->x, gi->y);

	/* top middle part of gadget border */
	for (i=0; i < gi->textarea.xsize; i++)
	  BlitBitmapOnBackground(gd->bitmap, drawto, gd->x + border_x, gd->y,
				 font_width, border_y,
				 gi->x + border_x + i * font_width, gi->y);

	/* top right part of gadget border */
	BlitBitmapOnBackground(gd->bitmap, drawto,
			       gd->x + gi->border.width - border_x, gd->y,
			       border_x, border_y,
			       gi->x + gi->width - border_x, gi->y);

	/* left and right part of gadget border for each row */
	for (i=0; i < gi->textarea.ysize; i++)
	{
	  BlitBitmapOnBackground(gd->bitmap, drawto, gd->x, gd->y + border_y,
				 border_x, font_height,
				 gi->x, gi->y + border_y + i * font_height);
	  BlitBitmapOnBackground(gd->bitmap, drawto,
				 gd->x + gi->border.width - border_x,
				 gd->y + border_y,
				 border_x, font_height,
				 gi->x + gi->width - border_x,
				 gi->y + border_y + i * font_height);
	}

	/* bottom left part of gadget border */
	BlitBitmapOnBackground(gd->bitmap, drawto,
			       gd->x, gd->y + gd_height - border_y,
			       border_x, border_y,
			       gi->x, gi->y + gi->height - border_y);

	/* bottom middle part of gadget border */
	for (i=0; i < gi->textarea.xsize; i++)
	  BlitBitmapOnBackground(gd->bitmap, drawto,
				 gd->x + border_x,
				 gd->y + gd_height - border_y,
				 font_width, border_y,
				 gi->x + border_x + i * font_width,
				 gi->y + gi->height - border_y);

	/* bottom right part of gadget border */
	BlitBitmapOnBackground(gd->bitmap, drawto,
			       gd->x + gi->border.width - border_x,
			       gd->y + gd_height - border_y,
			       border_x, border_y,
			       gi->x + gi->width - border_x,
			       gi->y + gi->height - border_y);

	ClearRectangleOnBackground(drawto,
				   gi->x + border_x,
				   gi->y + border_y,
				   gi->width - 2 * border_x,
				   gi->height - 2 * border_y);

	/* gadget text value */
#if 1
	DrawTextBuffer(gi->x + border_x, gi->y + border_y, gi->textarea.value,
		       font_nr, gi->textarea.xsize, -1, gi->textarea.ysize,
		       BLIT_ON_BACKGROUND, FALSE, FALSE, FALSE);
#else
	DrawTextToTextArea(gi->x + border_x, gi->y + border_y,
			   gi->textarea.value, font_nr, gi->textarea.xsize,
			   gi->textarea.xsize, gi->textarea.ysize,
			   BLIT_ON_BACKGROUND);
#endif

	cursor_letter = gi->textarea.value[gi->textarea.cursor_position];
	cursor_string[0] = (cursor_letter != '\0' ? cursor_letter : ' ');
	cursor_string[1] = '\0';

	/* draw cursor, if active */
	if (pressed)
	  DrawTextExt(drawto,
		      gi->x + border_x + gi->textarea.cursor_x * font_width,
		      gi->y + border_y + gi->textarea.cursor_y * font_height,
		      cursor_string,
		      font_nr, BLIT_INVERSE);
      }
      break;

    case GD_TYPE_SELECTBOX:
      {
	int i;
	char text[MAX_GADGET_TEXTSIZE + 1];
	int font_nr = (pressed ? gi->font_active : gi->font);
	int font_width = getFontWidth(font_nr);
	int font_height = getFontHeight(font_nr);
	int border_x = gi->border.xsize;
	int border_y = gi->border.ysize;
	int button = gi->border.xsize_selectbutton;
	int width_inner = gi->border.width - button - 2 * border_x;
	int box_width = gi->selectbox.width;
	int box_height = gi->selectbox.height;

	/* left part of gadget */
	BlitBitmapOnBackground(gd->bitmap, drawto, gd->x, gd->y,
			       border_x, gi->height, gi->x, gi->y);

	/* middle part of gadget */
	for (i=0; i < gi->selectbox.size; i++)
	  BlitBitmapOnBackground(gd->bitmap, drawto, gd->x + border_x, gd->y,
				 font_width, gi->height,
				 gi->x + border_x + i * font_width, gi->y);

	/* button part of gadget */
	BlitBitmapOnBackground(gd->bitmap, drawto,
			       gd->x + border_x + width_inner, gd->y,
			       button, gi->height,
			       gi->x + gi->width - border_x - button, gi->y);

	/* right part of gadget */
	BlitBitmapOnBackground(gd->bitmap, drawto,
			       gd->x + gi->border.width - border_x, gd->y,
			       border_x, gi->height,
			       gi->x + gi->width - border_x, gi->y);

	/* set text value */
	strncpy(text, gi->selectbox.options[gi->selectbox.index].text,
		gi->selectbox.size);
	text[gi->selectbox.size] = '\0';

	/* gadget text value */
	DrawTextExt(drawto, gi->x + border_x, gi->y + border_y, text,
		    font_nr, BLIT_MASKED);

	if (pressed)
	{
	  if (!gi->selectbox.open)
	  {
	    gi->selectbox.open = TRUE;
	    gi->selectbox.stay_open = FALSE;
	    gi->selectbox.current_index = gi->selectbox.index;

	    /* save background under selectbox */
	    BlitBitmap(drawto, gfx.field_save_buffer,
		       gi->selectbox.x,     gi->selectbox.y,
		       gi->selectbox.width, gi->selectbox.height,
		       gi->selectbox.x,     gi->selectbox.y);
	  }

	  /* draw open selectbox */

	  /* top left part of gadget border */
	  BlitBitmapOnBackground(gd->bitmap, drawto, gd->x, gd->y,
				 border_x, border_y,
				 gi->selectbox.x, gi->selectbox.y);

	  /* top middle part of gadget border */
	  for (i=0; i < gi->selectbox.size; i++)
	    BlitBitmapOnBackground(gd->bitmap, drawto, gd->x + border_x, gd->y,
				   font_width, border_y,
				   gi->selectbox.x + border_x + i * font_width,
				   gi->selectbox.y);

	  /* top button part of gadget border */
	  BlitBitmapOnBackground(gd->bitmap, drawto,
				 gd->x + border_x + width_inner, gd->y,
				 button, border_y,
				 gi->selectbox.x + box_width -border_x -button,
				 gi->selectbox.y);

	  /* top right part of gadget border */
	  BlitBitmapOnBackground(gd->bitmap, drawto,
				 gd->x + gi->border.width - border_x, gd->y,
				 border_x, border_y,
				 gi->selectbox.x + box_width - border_x,
				 gi->selectbox.y);

	  /* left and right part of gadget border for each row */
	  for (i=0; i < gi->selectbox.num_values; i++)
	  {
	    BlitBitmapOnBackground(gd->bitmap, drawto, gd->x, gd->y + border_y,
				   border_x, font_height,
				   gi->selectbox.x,
				   gi->selectbox.y + border_y + i*font_height);
	    BlitBitmapOnBackground(gd->bitmap, drawto,
				   gd->x + gi->border.width - border_x,
				   gd->y + border_y,
				   border_x, font_height,
				   gi->selectbox.x + box_width - border_x,
				   gi->selectbox.y + border_y + i*font_height);
	  }

	  /* bottom left part of gadget border */
	  BlitBitmapOnBackground(gd->bitmap, drawto,
				 gd->x, gd->y + gi->height - border_y,
				 border_x, border_y,
				 gi->selectbox.x,
				 gi->selectbox.y + box_height - border_y);

	  /* bottom middle part of gadget border */
	  for (i=0; i < gi->selectbox.size; i++)
	    BlitBitmapOnBackground(gd->bitmap, drawto,
				   gd->x + border_x,
				   gd->y + gi->height - border_y,
				   font_width, border_y,
				   gi->selectbox.x + border_x + i * font_width,
				   gi->selectbox.y + box_height - border_y);

	  /* bottom button part of gadget border */
	  BlitBitmapOnBackground(gd->bitmap, drawto,
				 gd->x + border_x + width_inner,
				 gd->y + gi->height - border_y,
				 button, border_y,
				 gi->selectbox.x + box_width -border_x -button,
				 gi->selectbox.y + box_height - border_y);

	  /* bottom right part of gadget border */
	  BlitBitmapOnBackground(gd->bitmap, drawto,
				 gd->x + gi->border.width - border_x,
				 gd->y + gi->height - border_y,
				 border_x, border_y,
				 gi->selectbox.x + box_width - border_x,
				 gi->selectbox.y + box_height - border_y);

	  ClearRectangleOnBackground(drawto,
				     gi->selectbox.x + border_x,
				     gi->selectbox.y + border_y,
				     gi->selectbox.width - 2 * border_x,
				     gi->selectbox.height - 2 * border_y);

	  /* selectbox text values */
	  for (i=0; i < gi->selectbox.num_values; i++)
	  {
	    int mask_mode = BLIT_MASKED;

	    strncpy(text, gi->selectbox.options[i].text, gi->selectbox.size);
	    text[gi->selectbox.size] = '\0';

	    if (i == gi->selectbox.current_index)
	    {
	      FillRectangle(drawto,
			    gi->selectbox.x + border_x,
			    gi->selectbox.y + border_y + i * font_height,
			    gi->selectbox.width - 2 * border_x, font_height,
			    gi->selectbox.inverse_color);

	      /* prevent use of cursor graphic by drawing at least two chars */
	      strcat(text, "  ");
	      text[gi->selectbox.size] = '\0';

	      mask_mode = BLIT_INVERSE;
	    }

	    DrawTextExt(drawto,
			gi->selectbox.x + border_x,
			gi->selectbox.y + border_y + i * font_height, text,
			font_nr, mask_mode);
	  }

	  redraw_selectbox = TRUE;
	}
	else if (gi->selectbox.open)
	{
	  gi->selectbox.open = FALSE;

	  /* restore background under selectbox */
	  BlitBitmap(gfx.field_save_buffer, drawto,
		     gi->selectbox.x,     gi->selectbox.y,
		     gi->selectbox.width, gi->selectbox.height,
		     gi->selectbox.x,     gi->selectbox.y);

	  /* redraw closed selectbox */
	  DrawGadget(gi, FALSE, FALSE);

	  redraw_selectbox = TRUE;
	}
      }
      break;

    case GD_TYPE_SCROLLBAR_VERTICAL:
      {
	int i;
	int xpos = gi->x;
	int ypos = gi->y + gi->scrollbar.position;
	int design_full = gi->width;
	int design_body = design_full - 2 * gi->border.ysize;
	int size_full = gi->scrollbar.size;
	int size_body = size_full - 2 * gi->border.ysize;
	int num_steps = size_body / design_body;
	int step_size_remain = size_body - num_steps * design_body;

	/* clear scrollbar area */
	ClearRectangleOnBackground(backbuffer, gi->x, gi->y,
				   gi->width, gi->height);

	/* upper part of gadget */
	BlitBitmapOnBackground(gd->bitmap, drawto,
			       gd->x, gd->y,
			       gi->width, gi->border.ysize,
			       xpos, ypos);

	/* middle part of gadget */
	for (i=0; i < num_steps; i++)
	  BlitBitmapOnBackground(gd->bitmap, drawto,
				 gd->x, gd->y + gi->border.ysize,
				 gi->width, design_body,
				 xpos,
				 ypos + gi->border.ysize + i * design_body);

	/* remaining middle part of gadget */
	if (step_size_remain > 0)
	  BlitBitmapOnBackground(gd->bitmap, drawto,
				 gd->x,  gd->y + gi->border.ysize,
				 gi->width, step_size_remain,
				 xpos,
				 ypos + gi->border.ysize
				 + num_steps * design_body);

	/* lower part of gadget */
	BlitBitmapOnBackground(gd->bitmap, drawto,
			       gd->x, gd->y + design_full - gi->border.ysize,
			       gi->width, gi->border.ysize,
			       xpos, ypos + size_full - gi->border.ysize);
      }
      break;

    case GD_TYPE_SCROLLBAR_HORIZONTAL:
      {
	int i;
	int xpos = gi->x + gi->scrollbar.position;
	int ypos = gi->y;
	int design_full = gi->height;
	int design_body = design_full - 2 * gi->border.xsize;
	int size_full = gi->scrollbar.size;
	int size_body = size_full - 2 * gi->border.xsize;
	int num_steps = size_body / design_body;
	int step_size_remain = size_body - num_steps * design_body;

	/* clear scrollbar area */
	ClearRectangleOnBackground(backbuffer, gi->x, gi->y,
				   gi->width, gi->height);

	/* left part of gadget */
	BlitBitmapOnBackground(gd->bitmap, drawto,
			       gd->x, gd->y,
			       gi->border.xsize, gi->height,
			       xpos, ypos);

	/* middle part of gadget */
	for (i=0; i < num_steps; i++)
	  BlitBitmapOnBackground(gd->bitmap, drawto,
				 gd->x + gi->border.xsize, gd->y,
				 design_body, gi->height,
				 xpos + gi->border.xsize + i * design_body,
				 ypos);

	/* remaining middle part of gadget */
	if (step_size_remain > 0)
	  BlitBitmapOnBackground(gd->bitmap, drawto,
				 gd->x + gi->border.xsize, gd->y,
				 step_size_remain, gi->height,
				 xpos + gi->border.xsize
				 + num_steps * design_body,
				 ypos);

	/* right part of gadget */
	BlitBitmapOnBackground(gd->bitmap, drawto,
			       gd->x + design_full - gi->border.xsize, gd->y,
			       gi->border.xsize, gi->height,
			       xpos + size_full - gi->border.xsize, ypos);
      }
      break;

    default:
      return;
  }

  if (direct)
  {
    BlitBitmap(drawto, window,
	       gi->x, gi->y, gi->width, gi->height, gi->x, gi->y);

    if (gi->type == GD_TYPE_SELECTBOX && redraw_selectbox)
      BlitBitmap(drawto, window,
		 gi->selectbox.x,     gi->selectbox.y,
		 gi->selectbox.width, gi->selectbox.height,
		 gi->selectbox.x,     gi->selectbox.y);
  }
  else
    redraw_mask |= (gi->x < gfx.sx + gfx.sxsize ? REDRAW_FIELD :
		    gi->y < gfx.dy + gfx.dysize ? REDRAW_DOOR_1 :
		    gi->y > gfx.vy ? REDRAW_DOOR_2 : REDRAW_DOOR_3);
}

static int get_minimal_size_for_numeric_input(int minmax_value)
{
  int min_size = 1;	/* value needs at least one digit */
  int i;

  /* add number of digits needed for absolute value */
  for (i = 10; i <= ABS(minmax_value); i *= 10)
    min_size++;

  /* if min/max value is negative, add one digit for minus sign */
  if (minmax_value < 0)
    min_size++;

  return min_size;
}

static void HandleGadgetTags(struct GadgetInfo *gi, int first_tag, va_list ap)
{
  int tag = first_tag;

  if (gi == NULL)
    return;

  while (tag != GDI_END)
  {
    switch(tag)
    {
      case GDI_CUSTOM_ID:
	gi->custom_id = va_arg(ap, int);
	break;

      case GDI_CUSTOM_TYPE_ID:
	gi->custom_type_id = va_arg(ap, int);
	break;

      case GDI_INFO_TEXT:
	{
	  int max_textsize = MAX_INFO_TEXTSIZE;
	  char *text = va_arg(ap, char *);

	  if (text != NULL)
	    strncpy(gi->info_text, text, max_textsize);
	  else
	    max_textsize = 0;

	  gi->info_text[max_textsize] = '\0';
	}
	break;

      case GDI_X:
	gi->x = va_arg(ap, int);
	break;

      case GDI_Y:
	gi->y = va_arg(ap, int);
	break;

      case GDI_WIDTH:
	gi->width = va_arg(ap, int);
	break;

      case GDI_HEIGHT:
	gi->height = va_arg(ap, int);
	break;

      case GDI_TYPE:
	gi->type = va_arg(ap, unsigned int);
	break;

      case GDI_STATE:
	gi->state = va_arg(ap, unsigned int);
	break;

      case GDI_ACTIVE:
	gi->active = (boolean)va_arg(ap, int);
	break;

      case GDI_DIRECT_DRAW:
	gi->direct_draw = (boolean)va_arg(ap, int);
	break;

      case GDI_CHECKED:
	gi->checked = (boolean)va_arg(ap, int);
	break;

      case GDI_RADIO_NR:
	gi->radio_nr = va_arg(ap, unsigned int);
	break;

      case GDI_NUMBER_VALUE:
	gi->textinput.number_value = va_arg(ap, int);
	sprintf(gi->textinput.value, "%d", gi->textinput.number_value);
	strcpy(gi->textinput.last_value, gi->textinput.value);
	gi->textinput.cursor_position = strlen(gi->textinput.value);
	break;

      case GDI_NUMBER_MIN:
	gi->textinput.number_min = va_arg(ap, int);
	if (gi->textinput.number_value < gi->textinput.number_min)
	{
	  gi->textinput.number_value = gi->textinput.number_min;
	  sprintf(gi->textinput.value, "%d", gi->textinput.number_value);
	  strcpy(gi->textinput.last_value, gi->textinput.value);
	}
	break;

      case GDI_NUMBER_MAX:
	gi->textinput.number_max = va_arg(ap, int);
	if (gi->textinput.number_value > gi->textinput.number_max)
	{
	  gi->textinput.number_value = gi->textinput.number_max;
	  sprintf(gi->textinput.value, "%d", gi->textinput.number_value);
	  strcpy(gi->textinput.last_value, gi->textinput.value);
	}
	break;

      case GDI_TEXT_VALUE:
	{
	  int max_textsize = MAX_GADGET_TEXTSIZE;

	  if (gi->textinput.size)
	    max_textsize = MIN(gi->textinput.size, MAX_GADGET_TEXTSIZE);

	  strncpy(gi->textinput.value, va_arg(ap, char *), max_textsize);
	  strcpy(gi->textinput.last_value, gi->textinput.value);

	  gi->textinput.value[max_textsize] = '\0';
	  gi->textinput.cursor_position = strlen(gi->textinput.value);

	  /* same tag also used for other gadget definitions */
	  strcpy(gi->textbutton.value, gi->textinput.value);
	  strcpy(gi->textarea.value, gi->textinput.value);
	  strcpy(gi->textarea.last_value, gi->textinput.value);
	}
	break;

      case GDI_TEXT_SIZE:
	{
	  int tag_value = va_arg(ap, int);
	  int max_textsize = MIN(tag_value, MAX_GADGET_TEXTSIZE);

	  gi->textinput.size = max_textsize;
	  gi->textinput.value[max_textsize] = '\0';
	  strcpy(gi->textinput.last_value, gi->textinput.value);

	  /* same tag also used for other gadget definitions */

	  gi->textarea.size = max_textsize;
	  gi->textarea.value[max_textsize] = '\0';
	  strcpy(gi->textarea.last_value, gi->textinput.value);

	  gi->textbutton.size = max_textsize;
	  gi->textbutton.value[max_textsize] = '\0';

	  gi->selectbox.size = gi->textinput.size;
	}
	break;

      case GDI_TEXT_FONT:
	gi->font = va_arg(ap, int);
	if (gi->font_active == 0)
	  gi->font_active = gi->font;
	break;

      case GDI_TEXT_FONT_ACTIVE:
	gi->font_active = va_arg(ap, int);
	break;

      case GDI_SELECTBOX_OPTIONS:
	gi->selectbox.options = va_arg(ap, struct ValueTextInfo *);
	break;

      case GDI_SELECTBOX_INDEX:
	gi->selectbox.index = va_arg(ap, int);
	break;

      case GDI_DESIGN_UNPRESSED:
	gi->design[GD_BUTTON_UNPRESSED].bitmap = va_arg(ap, Bitmap *);
	gi->design[GD_BUTTON_UNPRESSED].x = va_arg(ap, int);
	gi->design[GD_BUTTON_UNPRESSED].y = va_arg(ap, int);
	break;

      case GDI_DESIGN_PRESSED:
	gi->design[GD_BUTTON_PRESSED].bitmap = va_arg(ap, Bitmap *);
	gi->design[GD_BUTTON_PRESSED].x = va_arg(ap, int);
	gi->design[GD_BUTTON_PRESSED].y = va_arg(ap, int);
	break;

      case GDI_ALT_DESIGN_UNPRESSED:
	gi->alt_design[GD_BUTTON_UNPRESSED].bitmap= va_arg(ap, Bitmap *);
	gi->alt_design[GD_BUTTON_UNPRESSED].x = va_arg(ap, int);
	gi->alt_design[GD_BUTTON_UNPRESSED].y = va_arg(ap, int);
	break;

      case GDI_ALT_DESIGN_PRESSED:
	gi->alt_design[GD_BUTTON_PRESSED].bitmap = va_arg(ap, Bitmap *);
	gi->alt_design[GD_BUTTON_PRESSED].x = va_arg(ap, int);
	gi->alt_design[GD_BUTTON_PRESSED].y = va_arg(ap, int);
	break;

      case GDI_BORDER_SIZE:
	gi->border.xsize = va_arg(ap, int);
	gi->border.ysize = va_arg(ap, int);
	break;

      case GDI_BORDER_SIZE_SELECTBUTTON:
	gi->border.xsize_selectbutton = va_arg(ap, int);
	break;

      case GDI_DESIGN_WIDTH:
	gi->border.width = va_arg(ap, int);
	break;

      case GDI_DECORATION_DESIGN:
	gi->deco.design.bitmap = va_arg(ap, Bitmap *);
	gi->deco.design.x = va_arg(ap, int);
	gi->deco.design.y = va_arg(ap, int);
	break;

      case GDI_DECORATION_POSITION:
	gi->deco.x = va_arg(ap, int);
	gi->deco.y = va_arg(ap, int);
	break;

      case GDI_DECORATION_SIZE:
	gi->deco.width = va_arg(ap, int);
	gi->deco.height = va_arg(ap, int);
	break;

      case GDI_DECORATION_SHIFTING:
	gi->deco.xshift = va_arg(ap, int);
	gi->deco.yshift = va_arg(ap, int);
	break;

      case GDI_EVENT_MASK:
	gi->event_mask = va_arg(ap, unsigned int);
	break;

      case GDI_AREA_SIZE:
	gi->drawing.area_xsize = va_arg(ap, int);
	gi->drawing.area_ysize = va_arg(ap, int);

	/* determine dependent values for drawing area gadget, if needed */
	if (gi->width == 0 && gi->height == 0 &&
	    gi->drawing.item_xsize !=0 && gi->drawing.item_ysize !=0)
	{
	  gi->width = gi->drawing.area_xsize * gi->drawing.item_xsize;
	  gi->height = gi->drawing.area_ysize * gi->drawing.item_ysize;
	}
	else if (gi->drawing.item_xsize == 0 && gi->drawing.item_ysize == 0 &&
		 gi->width != 0 && gi->height != 0)
	{
	  gi->drawing.item_xsize = gi->width / gi->drawing.area_xsize;
	  gi->drawing.item_ysize = gi->height / gi->drawing.area_ysize;
	}

	/* same tag also used for other gadget definitions */
	gi->textarea.xsize = gi->drawing.area_xsize;
	gi->textarea.ysize = gi->drawing.area_ysize;

	if (gi->type & GD_TYPE_TEXT_AREA)	/* force recalculation */
	{
	  gi->width = 0;
	  gi->height = 0;
	}

	break;

      case GDI_ITEM_SIZE:
	gi->drawing.item_xsize = va_arg(ap, int);
	gi->drawing.item_ysize = va_arg(ap, int);

	/* determine dependent values for drawing area gadget, if needed */
	if (gi->width == 0 && gi->height == 0 &&
	    gi->drawing.area_xsize !=0 && gi->drawing.area_ysize !=0)
	{
	  gi->width = gi->drawing.area_xsize * gi->drawing.item_xsize;
	  gi->height = gi->drawing.area_ysize * gi->drawing.item_ysize;
	}
	else if (gi->drawing.area_xsize == 0 && gi->drawing.area_ysize == 0 &&
		 gi->width != 0 && gi->height != 0)
	{
	  gi->drawing.area_xsize = gi->width / gi->drawing.item_xsize;
	  gi->drawing.area_ysize = gi->height / gi->drawing.item_ysize;
	}
	break;

      case GDI_SCROLLBAR_ITEMS_MAX:
	gi->scrollbar.items_max = va_arg(ap, int);
	break;

      case GDI_SCROLLBAR_ITEMS_VISIBLE:
	gi->scrollbar.items_visible = va_arg(ap, int);
	break;

      case GDI_SCROLLBAR_ITEM_POSITION:
	gi->scrollbar.item_position = va_arg(ap, int);
	break;

      case GDI_WHEEL_AREA_X:
	gi->wheelarea.x = va_arg(ap, int);
	break;

      case GDI_WHEEL_AREA_Y:
	gi->wheelarea.y = va_arg(ap, int);
	break;

      case GDI_WHEEL_AREA_WIDTH:
	gi->wheelarea.width = va_arg(ap, int);
	break;

      case GDI_WHEEL_AREA_HEIGHT:
	gi->wheelarea.height = va_arg(ap, int);
	break;

      case GDI_CALLBACK_INFO:
	gi->callback_info = va_arg(ap, gadget_function);
	break;

      case GDI_CALLBACK_ACTION:
	gi->callback_action = va_arg(ap, gadget_function);
	break;

      default:
	Error(ERR_EXIT, "HandleGadgetTags(): unknown tag %d", tag);
    }

    tag = va_arg(ap, int);	/* read next tag */
  }

  /* check if gadget is complete */
  if (gi->type != GD_TYPE_DRAWING_AREA &&
      (!gi->design[GD_BUTTON_UNPRESSED].bitmap ||
       !gi->design[GD_BUTTON_PRESSED].bitmap))
    Error(ERR_EXIT, "gadget incomplete (missing Bitmap)");

  /* adjust gadget values in relation to other gadget values */

  if (gi->type & GD_TYPE_TEXT_INPUT)
  {
    int font_nr = gi->font_active;
    int font_width = getFontWidth(font_nr);
    int font_height = getFontHeight(font_nr);
    int border_xsize = gi->border.xsize;
    int border_ysize = gi->border.ysize;

    if (gi->type == GD_TYPE_TEXT_INPUT_NUMERIC)
    {
      int number_min = gi->textinput.number_min;
      int number_max = gi->textinput.number_max;
      int min_size_min = get_minimal_size_for_numeric_input(number_min);
      int min_size_max = get_minimal_size_for_numeric_input(number_max);
      int min_size = MAX(min_size_min, min_size_max);

      /* expand gadget text input size, if maximal value is too large */
      if (gi->textinput.size < min_size)
	gi->textinput.size = min_size;
    }

    gi->width  = 2 * border_xsize + (gi->textinput.size + 1) * font_width;
    gi->height = 2 * border_ysize + font_height;
  }

  if (gi->type & GD_TYPE_SELECTBOX)
  {
    int font_nr = gi->font_active;
    int font_width = getFontWidth(font_nr);
    int font_height = getFontHeight(font_nr);
    int border_xsize = gi->border.xsize;
    int border_ysize = gi->border.ysize;
    int button_size = gi->border.xsize_selectbutton;
    int bottom_screen_border = gfx.sy + gfx.sysize - font_height;
    Bitmap *src_bitmap;
    int src_x, src_y;

    gi->width  = 2 * border_xsize + gi->textinput.size*font_width +button_size;
    gi->height = 2 * border_ysize + font_height;

    if (gi->selectbox.options == NULL)
      Error(ERR_EXIT, "selectbox gadget incomplete (missing options array)");

    gi->selectbox.num_values = 0;
    while (gi->selectbox.options[gi->selectbox.num_values].text != NULL)
      gi->selectbox.num_values++;

    /* calculate values for open selectbox */
    gi->selectbox.width = gi->width;
    gi->selectbox.height =
      2 * border_ysize + gi->selectbox.num_values * font_height;

    gi->selectbox.x = gi->x;
    gi->selectbox.y = gi->y + gi->height;
    if (gi->selectbox.y + gi->selectbox.height > bottom_screen_border)
      gi->selectbox.y = gi->y - gi->selectbox.height;
    if (gi->selectbox.y < 0)
      gi->selectbox.y = bottom_screen_border - gi->selectbox.height;

    getFontCharSource(font_nr, FONT_ASCII_CURSOR, &src_bitmap, &src_x, &src_y);
    src_x += font_width / 2;
    src_y += font_height / 2;

    /* there may be esoteric cases with missing or too small font bitmap */
    if (src_bitmap != NULL &&
	src_x < src_bitmap->width && src_y < src_bitmap->height)
      gi->selectbox.inverse_color = GetPixel(src_bitmap, src_x, src_y);

    /* always start with closed selectbox */
    gi->selectbox.open = FALSE;
  }

  if (gi->type & GD_TYPE_TEXT_INPUT_NUMERIC)
  {
    struct GadgetTextInput *text = &gi->textinput;
    int value = text->number_value;

    text->number_value = (value < text->number_min ? text->number_min :
			  value > text->number_max ? text->number_max :
			  value);

    sprintf(text->value, "%d", text->number_value);
  }

  if (gi->type & GD_TYPE_TEXT_BUTTON)
  {
    int font_nr = gi->font_active;
    int font_width = getFontWidth(font_nr);
    int font_height = getFontHeight(font_nr);
    int border_xsize = gi->border.xsize;
    int border_ysize = gi->border.ysize;

    gi->width  = 2 * border_xsize + gi->textbutton.size * font_width;
    gi->height = 2 * border_ysize + font_height;
  }

  if (gi->type & GD_TYPE_SCROLLBAR)
  {
    struct GadgetScrollbar *gs = &gi->scrollbar;
    int scrollbar_size_cmp;

    if (gi->width == 0 || gi->height == 0 ||
	gs->items_max == 0 || gs->items_visible == 0)
      Error(ERR_EXIT, "scrollbar gadget incomplete (missing tags)");

    /* calculate internal scrollbar values */
    gs->size_min = (gi->type == GD_TYPE_SCROLLBAR_VERTICAL ?
		    gi->width : gi->height);
    gs->size_max = (gi->type == GD_TYPE_SCROLLBAR_VERTICAL ?
		    gi->height : gi->width);

    scrollbar_size_cmp = gs->size_max * gs->items_visible / gs->items_max;
    gs->size = MAX(scrollbar_size_cmp, gs->size_min);
    gs->size_max_cmp = (gs->size_max - (gs->size - scrollbar_size_cmp));

    gs->position = gs->size_max_cmp * gs->item_position / gs->items_max;
    gs->position_max = gs->size_max - gs->size;
    gs->correction = gs->size_max / gs->items_max / 2;

    /* finetuning for maximal right/bottom position */
    if (gs->item_position == gs->items_max - gs->items_visible)
      gs->position = gs->position_max;
  }

  if (gi->type & GD_TYPE_TEXT_AREA)
  {
    int font_nr = gi->font_active;
    int font_width = getFontWidth(font_nr);
    int font_height = getFontHeight(font_nr);
    int border_xsize = gi->border.xsize;
    int border_ysize = gi->border.ysize;

    if (gi->width == 0 || gi->height == 0)
    {
      gi->width  = 2 * border_xsize + gi->textarea.xsize * font_width;
      gi->height = 2 * border_ysize + gi->textarea.ysize * font_height;
    }
    else
    {
      gi->textarea.xsize = (gi->width  - 2 * border_xsize) / font_width;
      gi->textarea.ysize = (gi->height - 2 * border_ysize) / font_height;
    }
  }
}

void ModifyGadget(struct GadgetInfo *gi, int first_tag, ...)
{
  va_list ap;

  va_start(ap, first_tag);
  HandleGadgetTags(gi, first_tag, ap);
  va_end(ap);

  RedrawGadget(gi);
}

void RedrawGadget(struct GadgetInfo *gi)
{
  if (gi == NULL)
    return;

  if (gi->mapped)
    DrawGadget(gi, gi->state, gi->direct_draw);
}

struct GadgetInfo *CreateGadget(int first_tag, ...)
{
  struct GadgetInfo *new_gadget = checked_calloc(sizeof(struct GadgetInfo));
  va_list ap;

  /* always start with reliable default values */
  new_gadget->id = getNewGadgetID();
  new_gadget->callback_info = default_callback_info;
  new_gadget->callback_action = default_callback_action;
  new_gadget->active = TRUE;
  new_gadget->direct_draw = TRUE;

  new_gadget->next = NULL;

  va_start(ap, first_tag);
  HandleGadgetTags(new_gadget, first_tag, ap);
  va_end(ap);

  /* insert new gadget into global gadget list */
  if (gadget_list_last_entry)
  {
    gadget_list_last_entry->next = new_gadget;
    gadget_list_last_entry = gadget_list_last_entry->next;
  }
  else
    gadget_list_first_entry = gadget_list_last_entry = new_gadget;

  return new_gadget;
}

void FreeGadget(struct GadgetInfo *gi)
{
  struct GadgetInfo *gi_previous = gadget_list_first_entry;

  /* prevent "last_info_gi" from pointing to memory that will be freed */
  if (last_info_gi == gi)
    last_info_gi = NULL;

  while (gi_previous != NULL && gi_previous->next != gi)
    gi_previous = gi_previous->next;

  if (gi == gadget_list_first_entry)
    gadget_list_first_entry = gi->next;

  if (gi == gadget_list_last_entry)
    gadget_list_last_entry = gi_previous;

  if (gi_previous != NULL)
    gi_previous->next = gi->next;

  free(gi);
}

static void CheckRangeOfNumericInputGadget(struct GadgetInfo *gi)
{
  if (gi->type != GD_TYPE_TEXT_INPUT_NUMERIC)
    return;

  gi->textinput.number_value = atoi(gi->textinput.value);

  if (gi->textinput.number_value < gi->textinput.number_min)
    gi->textinput.number_value = gi->textinput.number_min;
  if (gi->textinput.number_value > gi->textinput.number_max)
    gi->textinput.number_value = gi->textinput.number_max;

  sprintf(gi->textinput.value, "%d", gi->textinput.number_value);

  if (gi->textinput.cursor_position < 0)
    gi->textinput.cursor_position = 0;
  else if (gi->textinput.cursor_position > strlen(gi->textinput.value))
    gi->textinput.cursor_position = strlen(gi->textinput.value);
}

/* global pointer to gadget actually in use (when mouse button pressed) */
static struct GadgetInfo *last_gi = NULL;

static void MapGadgetExt(struct GadgetInfo *gi, boolean redraw)
{
  if (gi == NULL || gi->mapped)
    return;

  gi->mapped = TRUE;

  if (redraw)
    DrawGadget(gi, DG_UNPRESSED, DG_BUFFERED);
}

void MapGadget(struct GadgetInfo *gi)
{
  MapGadgetExt(gi, TRUE);
}

void UnmapGadget(struct GadgetInfo *gi)
{
  if (gi == NULL || !gi->mapped)
    return;

  gi->mapped = FALSE;

  if (gi == last_gi)
    last_gi = NULL;
}

#define MAX_NUM_GADGETS		1024
#define MULTIMAP_UNMAP		(1 << 0)
#define MULTIMAP_REMAP		(1 << 1)
#define MULTIMAP_REDRAW		(1 << 2)
#define MULTIMAP_PLAYFIELD	(1 << 3)
#define MULTIMAP_DOOR_1		(1 << 4)
#define MULTIMAP_DOOR_2		(1 << 5)
#define MULTIMAP_ALL		(MULTIMAP_PLAYFIELD | \
				 MULTIMAP_DOOR_1 | \
				 MULTIMAP_DOOR_2)

static void MultiMapGadgets(int mode)
{
  struct GadgetInfo *gi = gadget_list_first_entry;
  static boolean map_state[MAX_NUM_GADGETS];
  int map_count = 0;

  while (gi != NULL)
  {
    if ((mode & MULTIMAP_PLAYFIELD &&
	 gi->x < gfx.sx + gfx.sxsize) ||
	(mode & MULTIMAP_DOOR_1 &&
	 gi->x >= gfx.dx && gi->y < gfx.dy + gfx.dysize) ||
	(mode & MULTIMAP_DOOR_2 &&
	 gi->x >= gfx.dx && gi->y > gfx.dy + gfx.dysize) ||
	(mode & MULTIMAP_ALL) == MULTIMAP_ALL)
    {
      if (mode & MULTIMAP_UNMAP)
      {
	map_state[map_count++ % MAX_NUM_GADGETS] = gi->mapped;
	UnmapGadget(gi);
      }
      else
      {
	if (map_state[map_count++ % MAX_NUM_GADGETS])
	  MapGadgetExt(gi, (mode & MULTIMAP_REDRAW));
      }
    }

    gi = gi->next;
  }
}

void UnmapAllGadgets()
{
  MultiMapGadgets(MULTIMAP_ALL | MULTIMAP_UNMAP);
}

void RemapAllGadgets()
{
  MultiMapGadgets(MULTIMAP_ALL | MULTIMAP_REMAP);
}

boolean anyTextInputGadgetActive()
{
  return (last_gi && (last_gi->type & GD_TYPE_TEXT_INPUT) && last_gi->mapped);
}

boolean anyTextAreaGadgetActive()
{
  return (last_gi && (last_gi->type & GD_TYPE_TEXT_AREA) && last_gi->mapped);
}

boolean anySelectboxGadgetActive()
{
  return (last_gi && (last_gi->type & GD_TYPE_SELECTBOX) && last_gi->mapped);
}

boolean anyScrollbarGadgetActive()
{
  return (last_gi && (last_gi->type & GD_TYPE_SCROLLBAR) && last_gi->mapped);
}

boolean anyTextGadgetActive()
{
  return (anyTextInputGadgetActive() ||
	  anyTextAreaGadgetActive() ||
	  anySelectboxGadgetActive());
}

static boolean insideSelectboxLine(struct GadgetInfo *gi, int mx, int my)
{
  return(gi != NULL &&
	 gi->type & GD_TYPE_SELECTBOX &&
	 mx >= gi->x && mx < gi->x + gi->width &&
	 my >= gi->y && my < gi->y + gi->height);
}

static boolean insideSelectboxArea(struct GadgetInfo *gi, int mx, int my)
{
  return(gi != NULL &&
	 gi->type & GD_TYPE_SELECTBOX &&
	 mx >= gi->selectbox.x && mx < gi->selectbox.x + gi->selectbox.width &&
	 my >= gi->selectbox.y && my < gi->selectbox.y + gi->selectbox.height);
}

void ClickOnGadget(struct GadgetInfo *gi, int button)
{
  if (!gi->mapped)
    return;

  /* simulate releasing mouse button over last gadget, if still pressed */
  if (button_status)
    HandleGadgets(-1, -1, 0);

  /* simulate pressing mouse button over specified gadget */
  HandleGadgets(gi->x, gi->y, button);

  /* simulate releasing mouse button over specified gadget */
  HandleGadgets(gi->x, gi->y, 0);
}

boolean HandleGadgets(int mx, int my, int button)
{
  static unsigned long pressed_delay = 0;
  static unsigned long pressed_delay_value = GADGET_FRAME_DELAY;
  static int last_button = 0;
  static int last_mx = 0, last_my = 0;
  static int pressed_mx = 0, pressed_my = 0;
  static boolean keep_selectbox_open = FALSE;
  static boolean gadget_stopped = FALSE;
  int scrollbar_mouse_pos = 0;
  struct GadgetInfo *new_gi, *gi;
  boolean press_event;
  boolean release_event;
  boolean mouse_moving;
  boolean mouse_inside_select_line;
  boolean mouse_inside_select_area;
  boolean mouse_released_where_pressed;
  boolean gadget_pressed;
  boolean gadget_pressed_repeated;
  boolean gadget_pressed_off_borders;
  boolean gadget_pressed_inside_select_line;
  boolean gadget_pressed_delay_reached;
  boolean gadget_moving;
  boolean gadget_moving_inside;
  boolean gadget_moving_off_borders;
  boolean gadget_draggable;
  boolean gadget_dragging;
  boolean gadget_released;
  boolean gadget_released_inside;
  boolean gadget_released_inside_select_line;
  boolean gadget_released_inside_select_area;
  boolean gadget_released_off_borders;
  boolean changed_position = FALSE;

  /* check if there are any gadgets defined */
  if (gadget_list_first_entry == NULL)
    return FALSE;

  /* simulated release of mouse button over last gadget */
  if (mx == -1 && my == -1 && button == 0)
  {
    mx = last_mx;
    my = last_my;
  }

  /* check which gadget is under the mouse pointer */
  new_gi = getGadgetInfoFromMousePosition(mx, my, button);

  /* check if button state has changed since last invocation */
  press_event   = (button != 0 && last_button == 0);
  release_event = (button == 0 && last_button != 0);
  last_button = button;

  /* check if mouse has been moved since last invocation */
  mouse_moving = ((mx != last_mx || my != last_my) && motion_status);
  last_mx = mx;
  last_my = my;

  if (press_event && new_gi != last_gi)
  {
    pressed_mx = mx;
    pressed_my = my;
  }

  mouse_released_where_pressed =
    (release_event && mx == pressed_mx && my == pressed_my);

  mouse_inside_select_line = insideSelectboxLine(new_gi, mx, my);
  mouse_inside_select_area = insideSelectboxArea(new_gi, mx, my);

  gadget_pressed_off_borders = (press_event && new_gi != last_gi);

  gadget_pressed_inside_select_line =
    (press_event && new_gi != NULL &&
     new_gi->type & GD_TYPE_SELECTBOX && new_gi->selectbox.open &&
     insideSelectboxLine(new_gi, mx, my));

  /* if mouse button pressed outside text or selectbox gadget, deactivate it */
  if (anyTextGadgetActive() &&
      (gadget_pressed_off_borders ||
       (gadget_pressed_inside_select_line && !mouse_inside_select_area)))
  {
    struct GadgetInfo *gi = last_gi;
    boolean gadget_changed = ((gi->event_mask & GD_EVENT_TEXT_LEAVING) != 0);

    /* check if text gadget has changed its value */
    if (gi->type & GD_TYPE_TEXT_INPUT)
    {
      CheckRangeOfNumericInputGadget(gi);

      if (!strEqual(gi->textinput.last_value, gi->textinput.value))
	strcpy(gi->textinput.last_value, gi->textinput.value);
      else
	gadget_changed = FALSE;
    }

    /* selectbox does not change its value when closed by clicking outside */
    if (gi->type & GD_TYPE_SELECTBOX)
      gadget_changed = FALSE;

    DrawGadget(gi, DG_UNPRESSED, gi->direct_draw);

    gi->event.type = GD_EVENT_TEXT_LEAVING;

    if (gadget_changed && !(gi->type & GD_TYPE_SELECTBOX))
      gi->callback_action(gi);

    last_gi = NULL;

    if (gadget_pressed_inside_select_line)
      new_gi = NULL;
  }

  gadget_pressed =
    (button != 0 && last_gi == NULL && new_gi != NULL && press_event);
  gadget_pressed_repeated =
    (button != 0 && last_gi != NULL && new_gi == last_gi);

  gadget_pressed_delay_reached =
    DelayReached(&pressed_delay, pressed_delay_value);

  gadget_released =		(release_event && last_gi != NULL);
  gadget_released_inside =	(gadget_released && new_gi == last_gi);
  gadget_released_off_borders =	(gadget_released && new_gi != last_gi);

  gadget_moving =	      (button != 0 && last_gi != NULL && mouse_moving);
  gadget_moving_inside =      (gadget_moving && new_gi == last_gi);
  gadget_moving_off_borders = (gadget_moving && new_gi != last_gi);

  /* when handling selectbox, set additional state values */
  if (gadget_released_inside && (last_gi->type & GD_TYPE_SELECTBOX))
  {
    gadget_released_inside_select_line = insideSelectboxLine(last_gi, mx, my);
    gadget_released_inside_select_area = insideSelectboxArea(last_gi, mx, my);
  }
  else
  {
    gadget_released_inside_select_line = FALSE;
    gadget_released_inside_select_area = FALSE;
  }

  /* setting state for handling over-large selectbox */
  if (keep_selectbox_open && (press_event || !mouse_inside_select_line))
    keep_selectbox_open = FALSE;

  /* if new gadget pressed, store this gadget  */
  if (gadget_pressed)
    last_gi = new_gi;

  /* 'gi' is actually handled gadget */
  gi = last_gi;

  /* if gadget is scrollbar, choose mouse position value */
  if (gi && gi->type & GD_TYPE_SCROLLBAR)
    scrollbar_mouse_pos =
      (gi->type == GD_TYPE_SCROLLBAR_HORIZONTAL ? mx - gi->x : my - gi->y);

  /* if mouse button released, no gadget needs to be handled anymore */
  if (gadget_released)
  {
    if (gi->type & GD_TYPE_SELECTBOX &&
	(keep_selectbox_open ||
	 mouse_released_where_pressed ||
	 !gadget_released_inside_select_area))	     /* selectbox stays open */
    {
      gi->selectbox.stay_open = TRUE;
      pressed_mx = 0;
      pressed_my = 0;
    }
    else if (!(gi->type & GD_TYPE_TEXT_INPUT ||
	       gi->type & GD_TYPE_TEXT_AREA))	    /* text input stays open */
      last_gi = NULL;
  }

  /* modify event position values even if no gadget is pressed */
  if (button == 0 && !release_event)
    gi = new_gi;

  /* if new gadget or if no gadget was pressed, release stopped processing */
  if (gadget_pressed || new_gi == NULL)
    gadget_stopped = FALSE;

  /* if gadget was stopped while being handled, stop gadget processing here */
  if (gadget_stopped)
    return TRUE;

  if (gi != NULL)
  {
    int last_x = gi->event.x;
    int last_y = gi->event.y;

    gi->event.x = mx - gi->x;
    gi->event.y = my - gi->y;

    if (gi->type == GD_TYPE_DRAWING_AREA)
    {
      gi->event.x /= gi->drawing.item_xsize;
      gi->event.y /= gi->drawing.item_ysize;

      if (last_x != gi->event.x || last_y != gi->event.y)
	changed_position = TRUE;
    }
    else if (gi->type & GD_TYPE_TEXT_INPUT && button != 0 && !motion_status)
    {
      int old_cursor_position = gi->textinput.cursor_position;

      /* if mouse button pressed inside activated text gadget, set cursor */
      gi->textinput.cursor_position =
	(mx - gi->x - gi->border.xsize) / getFontWidth(gi->font);

      if (gi->textinput.cursor_position < 0)
	gi->textinput.cursor_position = 0;
      else if (gi->textinput.cursor_position > strlen(gi->textinput.value))
	gi->textinput.cursor_position = strlen(gi->textinput.value);

      if (gi->textinput.cursor_position != old_cursor_position)
	DrawGadget(gi, DG_PRESSED, gi->direct_draw);
    }
    else if (gi->type & GD_TYPE_TEXT_AREA && button != 0 && !motion_status)
    {
      int old_cursor_position = gi->textarea.cursor_position;
      int x = (mx - gi->x - gi->border.xsize) / getFontWidth(gi->font);
      int y = (my - gi->y - gi->border.ysize) / getFontHeight(gi->font);

      x = (x < 0 ? 0 : x >= gi->textarea.xsize ? gi->textarea.xsize - 1 : x);
      y = (y < 0 ? 0 : y >= gi->textarea.ysize ? gi->textarea.ysize - 1 : y);

      setTextAreaCursorXY(gi, x, y);

      if (gi->textarea.cursor_position != old_cursor_position)
	DrawGadget(gi, DG_PRESSED, gi->direct_draw);
    }
    else if (gi->type & GD_TYPE_SELECTBOX && gi->selectbox.open &&
	     !keep_selectbox_open)
    {
      int old_index = gi->selectbox.current_index;

      /* if mouse moving inside activated selectbox, select value */
      if (my >= gi->selectbox.y && my < gi->selectbox.y + gi->selectbox.height)
	gi->selectbox.current_index =
	  (my - gi->selectbox.y - gi->border.ysize) / getFontHeight(gi->font);

      if (gi->selectbox.current_index < 0)
	gi->selectbox.current_index = 0;
      else if (gi->selectbox.current_index > gi->selectbox.num_values - 1)
	gi->selectbox.current_index = gi->selectbox.num_values - 1;

      if (gi->selectbox.current_index != old_index)
	DrawGadget(gi, DG_PRESSED, gi->direct_draw);
    }
  }

  /* handle gadget popup info text */
  if (last_info_gi != new_gi ||
      (new_gi && new_gi->type == GD_TYPE_DRAWING_AREA && changed_position))
  {
    if (new_gi != NULL && (button == 0 || new_gi == last_gi))
    {
      new_gi->event.type = GD_EVENT_INFO_ENTERING;
      new_gi->callback_info(new_gi);
    }
    else if (last_info_gi != NULL)
    {
      last_info_gi->event.type = GD_EVENT_INFO_LEAVING;
      last_info_gi->callback_info(last_info_gi);
    }

    last_info_gi = new_gi;
  }

  gadget_draggable = (gi && gi->type & GD_TYPE_SCROLLBAR);

  /* reset drag position for newly pressed scrollbar to "not dragging" */
  if (gadget_pressed && gadget_draggable)
    gi->scrollbar.drag_position = -1;

  gadget_dragging = (gadget_draggable && gi->scrollbar.drag_position != -1);

  /* clicking next to a scrollbar to move it is not considered "moving" */
  if (gadget_draggable && !gadget_dragging)
    gadget_moving = FALSE;

  /* when leaving scrollbar area when jump-scrolling, stop gadget processing */
  if (gadget_draggable && !gadget_dragging && gadget_moving_off_borders)
    gadget_stopped = TRUE;

  if ((gadget_pressed) ||
      (gadget_pressed_repeated && gadget_pressed_delay_reached))
  {
    if (gadget_pressed)		/* gadget pressed the first time */
    {
      /* initialize delay counter */
      DelayReached(&pressed_delay, 0);

      /* start gadget delay with longer delay after first click on gadget */
      pressed_delay_value = GADGET_FRAME_DELAY_FIRST;
    }
    else			/* gadget hold pressed for some time */
    {
      /* after first repeated gadget click, continue with shorter delay value */
      pressed_delay_value = GADGET_FRAME_DELAY;
    }

    if (gi->type & GD_TYPE_SCROLLBAR && !gadget_dragging)
    {
      int mpos = (gi->type == GD_TYPE_SCROLLBAR_HORIZONTAL ? mx    : my);
      int gpos = (gi->type == GD_TYPE_SCROLLBAR_HORIZONTAL ? gi->x : gi->y);
      int slider_start = gpos + gi->scrollbar.position;
      int slider_end   = gpos + gi->scrollbar.position + gi->scrollbar.size - 1;
      boolean inside_slider = (mpos >= slider_start && mpos <= slider_end);

      if (IS_WHEEL_BUTTON(button) || !inside_slider)
      {
	/* click scrollbar one scrollbar length up/left or down/right */

	struct GadgetScrollbar *gs = &gi->scrollbar;
	int old_item_position = gs->item_position;
	int item_steps = gs->items_visible - 1;
	int item_direction = (mpos < gpos + gi->scrollbar.position ? -1 : +1);

	if (IS_WHEEL_BUTTON(button))
	{
	  boolean scroll_single_step = ((GetKeyModState() & KMOD_Alt) != 0);

	  item_steps = (scroll_single_step ? 1 : DEFAULT_WHEEL_STEPS);
	  item_direction = (button == MB_WHEEL_UP ||
			    button == MB_WHEEL_LEFT ? -1 : +1);
	}

	changed_position = FALSE;

	gs->item_position += item_steps * item_direction;

	if (gs->item_position < 0)
	  gs->item_position = 0;
	else if (gs->item_position > gs->items_max - gs->items_visible)
	  gs->item_position = gs->items_max - gs->items_visible;

	if (old_item_position != gs->item_position)
	{
	  gi->event.item_position = gs->item_position;
	  changed_position = TRUE;
	}

	ModifyGadget(gi, GDI_SCROLLBAR_ITEM_POSITION, gs->item_position,
		     GDI_END);

	gi->state = GD_BUTTON_UNPRESSED;
	gi->event.type = GD_EVENT_MOVING;
	gi->event.off_borders = FALSE;

	if (gi->event_mask & GD_EVENT_MOVING && changed_position)
	  gi->callback_action(gi);

	return TRUE;
      }
      else
      {
	/* don't handle this scrollbar anymore when mouse position reached */
	if (gadget_pressed_repeated)
	{
	  gadget_stopped = TRUE;

	  return TRUE;
	}
      }
    }
  }

  if (gadget_pressed)
  {
    PlayGadgetSoundActivating();

    if (gi->type == GD_TYPE_CHECK_BUTTON)
    {
      gi->checked = !gi->checked;
    }
    else if (gi->type == GD_TYPE_RADIO_BUTTON)
    {
      struct GadgetInfo *rgi = gadget_list_first_entry;

      while (rgi)
      {
	if (rgi->mapped &&
	    rgi->type == GD_TYPE_RADIO_BUTTON &&
	    rgi->radio_nr == gi->radio_nr &&
	    rgi != gi)
	{
	  rgi->checked = FALSE;
	  DrawGadget(rgi, DG_UNPRESSED, rgi->direct_draw);
	}

	rgi = rgi->next;
      }

      gi->checked = TRUE;
    }
    else if (gi->type & GD_TYPE_SCROLLBAR)
    {
      int mpos = (gi->type == GD_TYPE_SCROLLBAR_HORIZONTAL ? mx    : my);
      int gpos = (gi->type == GD_TYPE_SCROLLBAR_HORIZONTAL ? gi->x : gi->y);
      int slider_start = gpos + gi->scrollbar.position;
      int slider_end   = gpos + gi->scrollbar.position + gi->scrollbar.size - 1;
      boolean inside_slider = (mpos >= slider_start && mpos <= slider_end);

      if (!IS_WHEEL_BUTTON(button) && inside_slider)
      {
	/* start dragging scrollbar */
	gi->scrollbar.drag_position =
	  scrollbar_mouse_pos - gi->scrollbar.position;
      }
    }
    else if (gi->type & GD_TYPE_SELECTBOX)
    {
      /* keep selectbox open in case of over-large selectbox */
      keep_selectbox_open = (mouse_inside_select_line &&
			     mouse_inside_select_area);
    }

    DrawGadget(gi, DG_PRESSED, gi->direct_draw);

    gi->state = GD_BUTTON_PRESSED;
    gi->event.type = GD_EVENT_PRESSED;
    gi->event.button = button;
    gi->event.off_borders = FALSE;

    if (gi->event_mask & GD_EVENT_PRESSED)
      gi->callback_action(gi);
  }

  if (gadget_pressed_repeated)
  {
    gi->event.type = GD_EVENT_PRESSED;

    if (gi->event_mask & GD_EVENT_REPEATED && gadget_pressed_delay_reached)
      gi->callback_action(gi);
  }

  if (gadget_moving)
  {
    if (gi->type & GD_TYPE_BUTTON)
    {
      if (gadget_moving_inside && gi->state == GD_BUTTON_UNPRESSED)
	DrawGadget(gi, DG_PRESSED, gi->direct_draw);
      else if (gadget_moving_off_borders && gi->state == GD_BUTTON_PRESSED)
	DrawGadget(gi, DG_UNPRESSED, gi->direct_draw);
    }
    else if (gi->type & GD_TYPE_SELECTBOX && !keep_selectbox_open)
    {
      int old_index = gi->selectbox.current_index;

      /* if mouse moving inside activated selectbox, select value */
      if (my >= gi->selectbox.y && my < gi->selectbox.y + gi->selectbox.height)
	gi->selectbox.current_index =
	  (my - gi->selectbox.y - gi->border.ysize) / getFontHeight(gi->font);

      if (gi->selectbox.current_index < 0)
	gi->selectbox.current_index = 0;
      else if (gi->selectbox.current_index > gi->selectbox.num_values - 1)
	gi->selectbox.current_index = gi->selectbox.num_values - 1;

      if (gi->selectbox.current_index != old_index)
	DrawGadget(gi, DG_PRESSED, gi->direct_draw);
    }
    else if (gi->type & GD_TYPE_SCROLLBAR)
    {
      struct GadgetScrollbar *gs = &gi->scrollbar;
      int old_item_position = gs->item_position;

      gs->position = scrollbar_mouse_pos - gs->drag_position;

      /* make sure to always precisely reach end positions when dragging */
      if (gs->position <= 0)
      {
	gs->position = 0;
	gs->item_position = 0;
      }
      else if (gs->position >= gs->position_max)
      {
	gs->position = gs->position_max;
	gs->item_position = gs->items_max - gs->items_visible;
      }
      else
      {
	gs->item_position =
	  gs->items_max * (gs->position + gs->correction) / gs->size_max_cmp;
      }

      if (gs->item_position < 0)
	gs->item_position = 0;
      if (gs->item_position > gs->items_max - 1)
	gs->item_position = gs->items_max - 1;

      if (old_item_position != gs->item_position)
      {
	gi->event.item_position = gs->item_position;
	changed_position = TRUE;
      }

      DrawGadget(gi, DG_PRESSED, gi->direct_draw);
    }

    gi->state = (gadget_moving_inside || gadget_draggable ?
		 GD_BUTTON_PRESSED : GD_BUTTON_UNPRESSED);
    gi->event.type = GD_EVENT_MOVING;
    gi->event.off_borders = gadget_moving_off_borders;

    if (gi->event_mask & GD_EVENT_MOVING && changed_position &&
	(gadget_moving_inside || gi->event_mask & GD_EVENT_OFF_BORDERS))
      gi->callback_action(gi);
  }

  if (gadget_released_inside)
  {
    boolean deactivate_gadget = TRUE;
    boolean gadget_changed = TRUE;

    if (gi->type & GD_TYPE_SELECTBOX)
    {
      if (keep_selectbox_open ||
	  mouse_released_where_pressed ||
	  !gadget_released_inside_select_area)	     /* selectbox stays open */
      {
	deactivate_gadget = FALSE;
	gadget_changed = FALSE;
      }
      else if (gi->selectbox.index != gi->selectbox.current_index)
	gi->selectbox.index = gi->selectbox.current_index;
      else
	gadget_changed = FALSE;
    }

    if (deactivate_gadget &&
	!(gi->type & GD_TYPE_TEXT_INPUT ||
	  gi->type & GD_TYPE_TEXT_AREA))	    /* text input stays open */
      DrawGadget(gi, DG_UNPRESSED, gi->direct_draw);

    gi->state = GD_BUTTON_UNPRESSED;
    gi->event.type = GD_EVENT_RELEASED;

    if ((gi->event_mask & GD_EVENT_RELEASED) && gadget_changed)
    {
      gi->callback_action(gi);
    }
  }

  if (gadget_released_off_borders)
  {
    if (gi->type & GD_TYPE_SCROLLBAR)
      DrawGadget(gi, DG_UNPRESSED, gi->direct_draw);

    gi->state = GD_BUTTON_UNPRESSED;
    gi->event.type = GD_EVENT_RELEASED;

    if (gi->event_mask & GD_EVENT_RELEASED &&
	gi->event_mask & GD_EVENT_OFF_BORDERS)
      gi->callback_action(gi);
  }

  /* handle gadgets unmapped/mapped between pressing and releasing */
  if (release_event && !gadget_released && new_gi)
    new_gi->state = GD_BUTTON_UNPRESSED;

  return (gadget_pressed || gadget_pressed_repeated ||
	  gadget_released || gadget_moving);
}

static void insertCharIntoTextArea(struct GadgetInfo *gi, char c)
{
  char text[MAX_GADGET_TEXTSIZE + 1];
  int cursor_position = gi->textarea.cursor_position;

  if (strlen(gi->textarea.value) >= MAX_GADGET_TEXTSIZE) /* no space left */
    return;

  strcpy(text, gi->textarea.value);
  strcpy(&gi->textarea.value[cursor_position + 1], &text[cursor_position]);
  gi->textarea.value[cursor_position] = c;

  setTextAreaCursorPosition(gi, gi->textarea.cursor_position + 1);
}

boolean HandleGadgetsKeyInput(Key key)
{
  struct GadgetInfo *gi = last_gi;

  if (gi == NULL || !gi->mapped ||
      !(gi->type & GD_TYPE_TEXT_INPUT ||
	gi->type & GD_TYPE_TEXT_AREA ||
	gi->type & GD_TYPE_SELECTBOX))
    return FALSE;

  if (key == KSYM_Return)	/* valid for both text input and selectbox */
  {
    boolean gadget_changed = ((gi->event_mask & GD_EVENT_TEXT_RETURN) != 0);

    if (gi->type & GD_TYPE_TEXT_INPUT)
    {
      CheckRangeOfNumericInputGadget(gi);

      if (!strEqual(gi->textinput.last_value, gi->textinput.value))
	strcpy(gi->textinput.last_value, gi->textinput.value);
      else
	gadget_changed = FALSE;
    }
    else if (gi->type & GD_TYPE_SELECTBOX)
    {
      if (gi->selectbox.index != gi->selectbox.current_index)
	gi->selectbox.index = gi->selectbox.current_index;
      else
	gadget_changed = FALSE;
    }

    if (gi->type & GD_TYPE_TEXT_AREA)
    {
      insertCharIntoTextArea(gi, '\n');

      DrawGadget(gi, DG_PRESSED, gi->direct_draw);
    }
    else
    {
      DrawGadget(gi, DG_UNPRESSED, gi->direct_draw);

      gi->event.type = GD_EVENT_TEXT_RETURN;

      last_gi = NULL;
    }

    if (gadget_changed)
      gi->callback_action(gi);
  }
  else if (gi->type & GD_TYPE_TEXT_INPUT)	/* only valid for text input */
  {
    char text[MAX_GADGET_TEXTSIZE + 1];
    int text_length = strlen(gi->textinput.value);
    int cursor_pos = gi->textinput.cursor_position;
    char letter = getCharFromKey(key);
    boolean legal_letter = (gi->type == GD_TYPE_TEXT_INPUT_NUMERIC ?
			    letter >= '0' && letter <= '9' :
			    letter != 0);

    if (legal_letter && text_length < gi->textinput.size)
    {
      strcpy(text, gi->textinput.value);
      strcpy(&gi->textinput.value[cursor_pos + 1], &text[cursor_pos]);
      gi->textinput.value[cursor_pos] = letter;
      gi->textinput.cursor_position++;

      DrawGadget(gi, DG_PRESSED, gi->direct_draw);
    }
    else if (key == KSYM_Left && cursor_pos > 0)
    {
      gi->textinput.cursor_position--;

      DrawGadget(gi, DG_PRESSED, gi->direct_draw);
    }
    else if (key == KSYM_Right && cursor_pos < text_length)
    {
      gi->textinput.cursor_position++;

      DrawGadget(gi, DG_PRESSED, gi->direct_draw);
    }
    else if (key == KSYM_BackSpace && cursor_pos > 0)
    {
      strcpy(text, gi->textinput.value);
      strcpy(&gi->textinput.value[cursor_pos - 1], &text[cursor_pos]);
      gi->textinput.cursor_position--;

      DrawGadget(gi, DG_PRESSED, gi->direct_draw);
    }
    else if (key == KSYM_Delete && cursor_pos < text_length)
    {
      strcpy(text, gi->textinput.value);
      strcpy(&gi->textinput.value[cursor_pos], &text[cursor_pos + 1]);

      DrawGadget(gi, DG_PRESSED, gi->direct_draw);
    }
  }
  else if (gi->type & GD_TYPE_TEXT_AREA)	/* only valid for text area */
  {
    char text[MAX_GADGET_TEXTSIZE + 1];
    int text_length = strlen(gi->textarea.value);
    int area_ysize = gi->textarea.ysize;
    int cursor_x_pref = gi->textarea.cursor_x_preferred;
    int cursor_y = gi->textarea.cursor_y;
    int cursor_pos = gi->textarea.cursor_position;
    char letter = getCharFromKey(key);
    boolean legal_letter = (letter != 0);

    if (legal_letter)
    {
      insertCharIntoTextArea(gi, letter);

      DrawGadget(gi, DG_PRESSED, gi->direct_draw);
    }
    else if (key == KSYM_Left && cursor_pos > 0)
    {
      setTextAreaCursorPosition(gi, gi->textarea.cursor_position - 1);

      DrawGadget(gi, DG_PRESSED, gi->direct_draw);
    }
    else if (key == KSYM_Right && cursor_pos < text_length)
    {
      setTextAreaCursorPosition(gi, gi->textarea.cursor_position + 1);

      DrawGadget(gi, DG_PRESSED, gi->direct_draw);
    }
    else if (key == KSYM_Up && cursor_y > 0)
    {
      setTextAreaCursorXY(gi, cursor_x_pref, cursor_y - 1);
      gi->textarea.cursor_x_preferred = cursor_x_pref;

      DrawGadget(gi, DG_PRESSED, gi->direct_draw);
    }
    else if (key == KSYM_Down && cursor_y < area_ysize - 1)
    {
      setTextAreaCursorXY(gi, cursor_x_pref, cursor_y + 1);
      gi->textarea.cursor_x_preferred = cursor_x_pref;

      DrawGadget(gi, DG_PRESSED, gi->direct_draw);
    }
    else if (key == KSYM_BackSpace && cursor_pos > 0)
    {
      strcpy(text, gi->textarea.value);
      strcpy(&gi->textarea.value[cursor_pos - 1], &text[cursor_pos]);

      setTextAreaCursorPosition(gi, gi->textarea.cursor_position - 1);

      DrawGadget(gi, DG_PRESSED, gi->direct_draw);
    }
    else if (key == KSYM_Delete && cursor_pos < text_length)
    {
      strcpy(text, gi->textarea.value);
      strcpy(&gi->textarea.value[cursor_pos], &text[cursor_pos + 1]);

      DrawGadget(gi, DG_PRESSED, gi->direct_draw);
    }
  }
  else if (gi->type & GD_TYPE_SELECTBOX)	/* only valid for selectbox */
  {
    int index = gi->selectbox.current_index;
    int num_values = gi->selectbox.num_values;

    if (key == KSYM_Up && index > 0)
    {
      gi->selectbox.current_index--;

      DrawGadget(gi, DG_PRESSED, gi->direct_draw);
    }
    else if (key == KSYM_Down && index < num_values - 1)
    {
      gi->selectbox.current_index++;

      DrawGadget(gi, DG_PRESSED, gi->direct_draw);
    }
  }

  return TRUE;
}
