/**
 * \file main-sdl2.c
 * \brief Angband SDL2 port
 *
 * Copyright (c) 1997 Ben Harrison and others
 *
 * This work is free software; you can redistribute it and/or modify it
 * under the terms of either:
 *
 * a) the GNU General Public License as published by the Free Software
 *    Foundation, version 2, or
 *
 * b) the "Angband licence":
 *    This software may be copied and distributed for educational, research,
 *    and not for profit purposes provided that this copyright and statement
 *    are included in all such copies.  Other copyrights may also apply.
 */

#include "angband.h"

#ifdef USE_SDL2

#include "sdl2/pui-ctrl.h"
#include "sdl2/pui-dlg.h"
#include "sdl2/pui-misc.h"
#include "sdl2/pui-win.h"
#include "SDL_image.h"
#ifdef SOUND_SDL2
#include "SDL_mixer.h"
#endif
#include "SDL_revision.h"

#include "main.h"
#include "init.h"
#include "ui-term.h"
#include "buildid.h"
#include "ui-display.h"
#include "ui-command.h"
#include "player-calcs.h"
#include "ui-output.h"
#include "game-world.h"
#include "ui-input.h"
#include "ui-prefs.h"
#include "grafmode.h"
#include "ui-game.h"
#include "ui-map.h"
#include "parser.h"
#ifdef SOUND_SDL2
#include "sound.h"
#endif

#define MAX_SUBWINDOWS \
	ANGBAND_TERM_MAX
/* that should be plenty... */
#define MAX_WINDOWS 4

#define INIT_SDL_FLAGS \
	(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER)
#define INIT_IMG_FLAGS \
	(IMG_INIT_PNG)

/* this is the main term screen, where all the action takes place */
#define MAIN_SUBWINDOW 0
/* for symmetry with main subwindow */
#define MAIN_WINDOW 0
/* size of the keypress queue (term->key_queue) */
#define SUBWINDOW_KEYQ_SIZE(subwindow_ptr) \
	((subwindow_ptr)->index == MAIN_SUBWINDOW ? 1024 : 32)

#define DEFAULT_DISPLAY 0

#define DEFAULT_CONFIG_FILE "sdl2init.txt"
#define DEFAULT_CONFIG_FILE_DIR \
	ANGBAND_DIR_USER

#define DEFAULT_ALPHA_FULL 0xFF
#define ALPHA_PERCENT(p) \
	(DEFAULT_ALPHA_FULL * (p) / 100)
#define DEFAULT_ALPHA_LOW \
	ALPHA_PERCENT(80)
/* for "Alpha" button; in percents */
#define DEFAULT_ALPHA_STEP 10
#define DEFAULT_ALPHA_LOWEST 0

#define DEFAULT_WALLPAPER "att-128.png"
#define DEFAULT_WALLPAPER_DIR \
	ANGBAND_DIR_ICONS
#define DEFAULT_WINDOW_ICON "att-32.png"
#define DEFAULT_WINDOW_ICON_DIR \
	ANGBAND_DIR_ICONS
#define DEFAULT_ABOUT_ICON "att-128.png"
#define DEFAULT_ABOUT_ICON_DIR \
	ANGBAND_DIR_ICONS

#define DEFAULT_FONT_HINTING \
	TTF_HINTING_LIGHT
/* border of subwindows, in pixels */
#define DEFAULT_BORDER 8
#define DEFAULT_XTRA_BORDER \
	(DEFAULT_BORDER * 2)
#define DEFAULT_VISIBLE_BORDER 2

/* XXX hack: the widest character present in a font
 * for determining font advance (width) */
#define GLYPH_FOR_ADVANCE 'W'
#define DEFAULT_VECTOR_FONT_SIZE 12

#define DEFAULT_FONT "10x20x.fon"

#define DEFAULT_DIALOG_FONT "8x13x.fon"

#define MAX_VECTOR_FONT_SIZE 64
#define MIN_VECTOR_FONT_SIZE 4

/* update period in window delays (160 milliseconds, assuming 60 fps) */
#define DEFAULT_IDLE_UPDATE_PERIOD 10

#define DEFAULT_WINDOW_BG_COLOR \
	COLOUR_L_DARK
#define DEFAULT_SUBWINDOW_BG_COLOR \
	COLOUR_DARK
#define DEFAULT_SUBWINDOW_CURSOR_COLOR \
	COLOUR_YELLOW
#define DEFAULT_SHADE_COLOR \
	COLOUR_SHADE
#define DEFAULT_SUBWINDOW_BORDER_COLOR \
	COLOUR_SHADE

#define DEFAULT_ERROR_COLOR \
	COLOUR_RED

/* shockbolt's tiles are 64x64; dungeon is 198 tiles long;
 * 64 * 198 is 12672 which is bigger than any possible texture! */
#define REASONABLE_MAP_TILE_WIDTH 16
#define REASONABLE_MAP_TILE_HEIGHT 16

/* angband needs at least 80x24 main term, else severe bugs happen */
#define MIN_COLS_MAIN 80
#define MIN_ROWS_MAIN 24
/* some reasonable values - we dont want the player to resize
 * the term into nothing! */
#define MIN_COLS_OTHER 12
#define MIN_ROWS_OTHER 3

#define MIN_TILE_WIDTH 1
#define MAX_TILE_WIDTH 9
#define MIN_TILE_HEIGHT 1
#define MAX_TILE_HEIGHT 9

#define DEFAULT_SNAP_RANGE(window) (window->app->def_font_w)

enum wallpaper_mode {
	/* so that we won't forget to actually set wallpaper */
	WALLPAPER_INVALID = 0,
	WALLPAPER_DONT_SHOW,
	WALLPAPER_TILED,
	WALLPAPER_CENTERED,
	WALLPAPER_SCALED
};

enum font_type {
	FONT_TYPE_INVALID = 0,
	FONT_TYPE_RASTER,
	FONT_TYPE_VECTOR
};

struct ttf {
	TTF_Font *handle;
	struct { int w; int h; } glyph;
};

/* the array of ascii chars was generated by this perl script:
 * my $ascii = join '', map /\p{Print}/ ? $_ : ' ', map chr, 0 .. 127; 
 * for (my $i = 0; $i < length($ascii); $i += 32) {
 *     my $s = substr $ascii, $i, 32;
 *     $s =~ s#\\#\\\\#g;
 *     $s =~ s#"#\\"#g;
 *     print qq(\t"$s"\n);
 * }
 * and then manually hacked to substitute the escape codes for the non-null
 * control characters (0x01 to 0x1f, and 0x7f) so the graphics characters at
 * those indices in Angband's packaged .fon files remain available, if the
 * user wants to access them (those characters are likely the ones specified by
 * code page 437, https://en.wikipedia.org/wiki/Code_page_437 ).
 */
static const char g_ascii_codepoints_for_cache[] =
	" \x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
	"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
	" !\"#$%&'()*+,-./0123456789:;<=>?"
	"@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"
	"`abcdefghijklmnopqrstuvwxyz{|}~\x7f";
/* Simple font cache. Only for ascii (which is like 99.99% (?) of what the game
 * displays, anyway) */
#define ASCII_CACHE_SIZE \
		(N_ELEMENTS(g_ascii_codepoints_for_cache) - 1)
struct font_cache {
	SDL_Texture *texture;
	/* it wastes some space... so what? */
	SDL_Rect rects[ASCII_CACHE_SIZE];
};
/* 0 is also a valid codepoint, of course... that's just for finding bugs */
#define IS_CACHED_ASCII_CODEPOINT(c) \
		((c) > 0 && (c) < ASCII_CACHE_SIZE)

struct font {
	struct ttf ttf;
	char *name;
	char *path;
	int size;
	/* index of font in the application's fonts array */
	size_t index;

	struct font_cache cache;
};

struct subwindow_border {
	bool visible;
	bool error;
	SDL_Color color;
	int width;
};

struct subwindow_config {
	char *font_name;
	int font_size;
};

struct window_config {
	int renderer_flags;
	int renderer_index;
	int window_flags;

	char *wallpaper_path;
	char *font_name;
	int font_size;
};

/** struct subwindow is representation of angband's term */
struct subwindow {
	bool inited;
	bool loaded;
	bool linked;
	bool visible;

	struct subwindow_config *config;

	/** top in z-order */
	bool top;
	bool always_top;

	unsigned index;

	int rows;
	int cols;

	/** struct ttf also has this information; these members are
	 * just for convenience */
	int font_width;
	int font_height;

	/**
	 * These are transiently used during font selection for the subwindow.
	 * The font size must be >= min_font_size and < max_font_size to
	 * match the constraints of the subwindow's miniumum number of columns
	 * and rows and the size of the window containing the subwindow.
	 */
	int min_font_size, max_font_size;

	/** coordinates of full rect are relative to coordinates of window
	 * (basically, full rect is texture) */
	SDL_Rect full_rect;
	/** coordinates of inner rect are relative to that of full rect */
	SDL_Rect inner_rect;
	/** for use when resizing term */
	SDL_Rect sizing_rect;
	/** version of full_rect for the opposite setting of fullscreen */
	SDL_Rect stored_rect;
	/** a one pixel texture, mostly for displaying something when
	 * the player is resizing term */
	SDL_Texture *aux_texture;

	/** background color */
	SDL_Color color;

	struct subwindow_border borders;

	SDL_Texture *texture;

	struct font *font;

	struct sdlpui_window *window;
	struct term *term;
	struct my_app *app;
};

struct graphics {
	SDL_Texture *texture;
	int id;
	int tile_pixel_w;
	int tile_pixel_h;

	int overdraw_row;
	int overdraw_max;
};

/* thats for dragging terms */
struct move_state {
	bool active;
	bool moving;

	int originx;
	int originy;

	struct subwindow *subwindow;
};

/* thats for resizing terms */
struct size_state {
	bool active;
	bool sizing;

	int originx;
	int originy;

	bool left;
	bool top;

	struct subwindow *subwindow;
};

struct wallpaper {
	int w;
	int h;
	SDL_Texture *texture;
	enum wallpaper_mode mode;
};

/** struct sdlpui_window is a real window on screen, it has one or more
 * subwindows (terms) in it */
struct sdlpui_window {
	bool inited;
	bool loaded;

	/** id is SDL's id, for use with events */
	Uint32 id;
	/** and this is our id, mostly for debugging */
	unsigned index;
	/** index of transiently outlined subwindow */
	unsigned outlined_subwindow;

	struct window_config *config;

	/** window has changed and must be redrawn */
	bool dirty;

	/** limiter for frames */
	Uint32 next_redraw;
	/** from display mode */
	int delay;

	/** as reported by SDL_GetWindowFlags() */
	Uint32 flags;

	/** position and size of window as it is on display */
	SDL_Rect full_rect;
	/** size of window without status bar, basically */
	SDL_Rect inner_rect;
	/** version of full_rect for the opposite setting of fullscreen */
	SDL_Rect stored_rect;

	SDL_Color color;
	/** for making terms transparent while moving or sizing them */
	Uint8 alpha;

	SDL_Window *window;
	SDL_Renderer *renderer;
	/** The font to use for this window's dialogs and menus */
	struct font *dialog_font;
	/** The status bar (i.e. menu bar) for the window */
	struct sdlpui_dialog *status_bar;
	/** The button in the status bar for moving the window */
	struct sdlpui_control *move_button;
	/** The button in the status bar for resizing the window */
	struct sdlpui_control *size_button;
	/** The about dialog; NULL if not currently displayed */
	struct sdlpui_dialog *infod;
	/** The keyboard shortcut dialog; NULL if not currently displayed */
	struct sdlpui_dialog *shorte;
	/** The SDL details dialog; NULL if not currently displayed */
	struct sdlpui_dialog *detaild;

	int pixelformat;

	struct wallpaper wallpaper;
	struct sdlpui_stipple stipple;
	struct move_state move_state;
	struct size_state size_state;
	struct graphics graphics;

	struct subwindow *subwindows[MAX_SUBWINDOWS];

	/** Point back to the containing application */
	struct my_app *app;

	/**
	 * These are the head and tail of the stack of menus/dialogs for this
	 * window.  Both will be NULL if no menus/dialogs are active.
	 */
	struct sdlpui_dialog *d_head, *d_tail;

	/**
	 * These point to the dialog/menu that should receive mouse events or
	 * keyboard events, respectively.  If NULL, events will be directed
	 * to the game's core.
	 */
	struct sdlpui_dialog *d_mouse, *d_key;
};

struct font_info {
	char *name;
	char *path;
	int size;
	size_t index;
	enum font_type type;
};

struct shortcut_editor_data {
	struct sdlpui_control labels[MAX_WINDOWS];
	struct sdlpui_control shortcut_displays[MAX_WINDOWS];
	struct sdlpui_control change_buttons[MAX_WINDOWS];
	struct sdlpui_control clear_buttons[MAX_WINDOWS];
	struct sdlpui_control prompt_label;
	struct sdlpui_control close_button;
	struct sdlpui_control reset_button;
	int changing_shortcut;
};

struct my_app {
	/**
	 * The string ANGBAND_DIR_USER is freed before calling quit_hook(),
	 * so we need to save the path to the config file.
	 */
	char config_file[4096];
	/** the game's color table translated into what SDL expects */
	SDL_Color colors[MAX_COLORS];
	/**
	 * fonts from the game's lib/fonts directory that can be selected
	 * directly from the menus
	 */
	struct font_info *fonts;
	struct subwindow subwindows[MAX_SUBWINDOWS];
	struct sdlpui_window windows[MAX_WINDOWS];
	struct keypress menu_shortcuts[MAX_WINDOWS];
	/**
	 * point to the window that should receive mouse and key events,
	 * respectively
	 */
	struct sdlpui_window *w_mouse;
	struct sdlpui_window *w_key;
	/** Number of entries stored in fonts */
	int font_count;
	/** Number of entries allocated in fonts */
	int font_alloc;
	/** Width and height on screen for the default font */
	int def_font_w, def_font_h;
	/**
	 * true if KC_MOD_KEYPAD will be sent for numeric keypad keys at the
	 * expense of not handling some keyboard layouts properly
	 */
	bool kp_as_mod;
	/** true if details about the SDL environment are to be logged. */
	bool print_sdl_details;

	SDL_GameController *controller;
};

/* Forward declarations */

static void init_globals(struct my_app *a);
static void free_globals(struct my_app *a);
static bool read_config_file(struct my_app *a);
static void dump_config_file(const struct my_app *a);
static void init_colors(struct my_app *a);
static void start_windows(struct my_app *a);
static void start_window(struct sdlpui_window *window);
static void load_font(struct font *font);
static bool reload_font(struct subwindow *subwindow,
		const struct font_info *info);
static void free_font(struct font *font);
static const struct font_info *find_font_info(const struct font_info *fonts,
		int nfonts, const char *name);
static struct font *make_font(const struct sdlpui_window *window,
		const char *name, int size);
static struct sdlpui_window *get_new_window(struct my_app *a, unsigned index);
static void wipe_window(struct sdlpui_window *window, int display);
/* create default config for spawning a window via gui */
static void wipe_window_aux_config(struct sdlpui_window *window);
static void adjust_window_geometry(struct sdlpui_window *window);
static void free_window(struct sdlpui_window *window);
static struct sdlpui_window *get_window_by_id(struct my_app *a, Uint32 id);
static struct sdlpui_window *get_window_direct(struct my_app *a,
		unsigned index);
static void resize_window(struct sdlpui_window *window, int w, int h);
static void resize_subwindow(struct subwindow *subwindow);
static struct subwindow *get_new_subwindow(struct my_app *a, unsigned index);
static void load_subwindow(struct sdlpui_window *window,
		struct subwindow *subwindow);
static bool is_subwindow_loaded(struct my_app *a, unsigned index);
static struct subwindow *transfer_subwindow(struct sdlpui_window *window,
		unsigned index);
static struct subwindow *get_subwindow_by_xy(const struct sdlpui_window *window,
		int x, int y);
static struct subwindow *get_subwindow_by_index(
		const struct sdlpui_window *window, unsigned index,
		bool visible);
static struct subwindow *get_subwindow_direct(struct my_app *a, unsigned index);
/* this function loads new subwindow if it's not already loaded */
static struct subwindow *make_subwindow(struct sdlpui_window *window,
		unsigned index);
static void sort_to_top(struct sdlpui_window *window);
static void bring_to_top(struct sdlpui_window *window,
		struct subwindow *subwindow);
static void render_borders(struct subwindow *subwindow);
static SDL_Texture *load_image(const struct sdlpui_window *window,
		const char *path);
static void reload_all_graphics(struct my_app *a, graphics_mode *mode);
static void free_graphics(struct graphics *graphics);
static void load_terms(struct my_app *a);
static void load_term(struct subwindow *subwindow);
static void clear_pw_flag(struct subwindow *subwindow);
static bool adjust_subwindow_geometry(const struct sdlpui_window *window,
		struct subwindow *subwindow);
static bool is_ok_col_row(const struct subwindow *subwindow,
		const SDL_Rect *rect, int cell_w, int cell_h);
static void resize_rect(SDL_Rect *rect,
		int left, int top, int right, int bottom);
static void crop_rects(SDL_Rect *src, SDL_Rect *dst);
static bool is_point_in_rect(int x, int y, const SDL_Rect *rect);
static bool is_close_to(int a, int b, unsigned range);
static void handle_window_closed(struct my_app *a,
		struct sdlpui_window *window);
static void refresh_angband_terms(struct my_app *a);
static void handle_quit(void);
static void wait_anykey(struct my_app *a);
static void keyboard_event_to_angband_key(const SDL_KeyboardEvent *key,
		bool kp_as_mod, keycode_t *ch, uint8_t *mods);
static void textinput_event_to_angband_key(const SDL_TextInputEvent *key,
		bool kp_as_mod, keycode_t *ch, uint8_t *mods);
static void recreate_textures(struct my_app *a, bool all);

/* Global variables. */

const char help_sdl2[] = "SDL2 frontend, subopts -v";
static struct my_app g_app;
static Uint32 SHORTCUT_EDITOR_CODE;

/*
 * Provide the hooks needed by primitive UI toolkit for SDL2.
 */
SDL_Renderer *sdlpui_get_renderer(struct sdlpui_window *w)
{
	return w->renderer;
}

TTF_Font *sdlpui_get_ttf(struct sdlpui_window *w)
{
	assert(w->dialog_font && w->dialog_font->ttf.handle);
	return w->dialog_font->ttf.handle;
}

struct sdlpui_stipple *sdlpui_get_stipple(struct sdlpui_window *w)
{
	return &w->stipple;
}

const SDL_Color *sdlpui_get_color(struct sdlpui_window *w, int role)
{
	int idx;

	switch (role) {
	case SDLPUI_COLOR_MENU_BG:
	case SDLPUI_COLOR_COUNTERSINK:
		idx = COLOUR_DARK;
		break;

	case SDLPUI_COLOR_MENU_FG:
	case SDLPUI_COLOR_DIALOG_FG:
	case SDLPUI_COLOR_DIALOG_BORDER:
		idx = COLOUR_WHITE;
		break;

	case SDLPUI_COLOR_MENU_BORDER:
	case SDLPUI_COLOR_DIALOG_BG:
		idx = COLOUR_SHADE;
		break;

	default:
		/*
		 * Should not happen; assign a safe index in case assertions
		 * are optimized away.
		 */
		SDL_assert(0);
		idx = 0;
		break;
	}

	SDL_assert(idx >= 0 && idx < MAX_COLORS);
	return &w->app->colors[idx];
}

void sdlpui_signal_redraw(struct sdlpui_window *w)
{
	w->dirty = true;
}

void sdlpui_dialog_push_to_top(struct sdlpui_window *w, struct sdlpui_dialog *d)
{
	bool redraw = true;

	/* Unlink. */
	if (d->next) {
		d->next->prev = d->prev;
	} else if (w->d_tail == d) {
		w->d_tail = d->prev;
	}
	if (d->prev) {
		d->prev->next = d->next;
	} else if (w->d_head == d) {
		redraw = false;
		w->d_head = d->next;
	}

	/* Put at the top. */
	d->prev = NULL;
	d->next = w->d_head;
	if (d->next) {
		d->next->prev = d;
	} else {
		SDL_assert(!w->d_tail);
		w->d_tail = d;
	}
	w->d_head = d;

	if (redraw) {
		sdlpui_signal_redraw(w);
	}
}

void sdlpui_dialog_pop(struct sdlpui_window *w, struct sdlpui_dialog *d)
{
	if (w->d_mouse == d) {
		w->d_mouse = NULL;
	}
	if (w->d_key == d) {
		w->d_key = NULL;
	}
	if (d->next) {
		d->next->prev = d->prev;
	} else {
		SDL_assert(w->d_tail == d);
		w->d_tail = d->prev;
	}
	if (d->prev) {
		d->prev->next = d->next;
	} else {
		SDL_assert(w->d_head == d);
		w->d_head = d->next;
	}
	w->dirty = true;
}

void sdlpui_dialog_gain_key_focus(struct sdlpui_window *w,
		struct sdlpui_dialog *d)
{
	w->d_key = d;
}

void sdlpui_dialog_yield_key_focus(struct sdlpui_window *w,
		struct sdlpui_dialog *d)
{
	if (w->d_key == d) {
		w->d_key = NULL;
	}
}

void sdlpui_dialog_gain_mouse_focus(struct sdlpui_window *w,
		struct sdlpui_dialog *d)
{
	w->d_mouse = d;
}

void sdlpui_dialog_yield_mouse_focus(struct sdlpui_window *w,
		struct sdlpui_dialog *d)
{
	if (w->d_mouse == d) {
		w->d_mouse = NULL;
	}
}

struct sdlpui_dialog *sdlpui_dialog_with_key_focus(struct sdlpui_window *w)
{
	return w->d_key;
}

struct sdlpui_dialog *sdlpui_dialog_with_mouse_focus(struct sdlpui_window *w)
{
	return w->d_mouse;
}

void sdlpui_force_quit(void)
{
	quit("sdlpui failure");
}

/* Functions */

static void log_window_details(int my_index, SDL_Window *w, SDL_Renderer *r)
{
	Uint32 flags = SDL_GetWindowFlags(w);
	int wid, hgt, x, y, bt, bl, bb, br;
	SDL_RendererInfo r_info;

	SDL_Log("Window %d:", my_index);
	SDL_Log("    Display index and ID: %d %lu",
		SDL_GetWindowDisplayIndex(w),
		(unsigned long)SDL_GetWindowID(w));
	SDL_GetWindowPosition(w, &x, &y);
	SDL_GetWindowSize(w, &wid, &hgt);
	SDL_Log("    Geometry: (%d, %d) %d x %d", x, y, wid, hgt);
	SDL_GetWindowBordersSize(w, &bt, &bl, &bb, &br);
	SDL_Log("    Border sizes: %d %d %d %d", bt, bl, bb, br);
	/* Skipping the more transient flags about focus and grabs. */
	SDL_Log("    Flags:%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s",
		(flags & SDL_WINDOW_FULLSCREEN) ? " FULLSCREEN" : "",
		(flags & SDL_WINDOW_OPENGL) ? " OPENGL" : "",
		(flags & SDL_WINDOW_SHOWN) ? " SHOWN" : "",
		(flags & SDL_WINDOW_HIDDEN) ? " HIDDEN" : "",
		(flags & SDL_WINDOW_BORDERLESS) ? " BORDERLESS" : "",
		(flags & SDL_WINDOW_RESIZABLE) ? " RESIZABLE" : "",
		(flags & SDL_WINDOW_MINIMIZED) ? " MINIMIZED" : "",
		(flags & SDL_WINDOW_MAXIMIZED) ? " MAXIMIZED" : "",
		(flags & SDL_WINDOW_FULLSCREEN_DESKTOP)
		? " FULLSCREEN_DESKTOP" : "",
		(flags & SDL_WINDOW_FOREIGN) ? " FOREIGN" : "",
		(flags & SDL_WINDOW_ALLOW_HIGHDPI) ? " ALLOW_HIGHDPI" : "",
		(flags & SDL_WINDOW_ALWAYS_ON_TOP) ? " ALWAYS_ON_TOP" : "",
		(flags & SDL_WINDOW_SKIP_TASKBAR) ? " SKIP_TASKBAR" : "",
		(flags & SDL_WINDOW_UTILITY) ? " UTILITY" : "",
		(flags & SDL_WINDOW_POPUP_MENU) ? " POPUP_MENU" : "",
		(flags & SDL_WINDOW_VULKAN) ? " VULKAN" : "",
		(flags & SDL_WINDOW_METAL) ? " METAL" : "");

	if (!r) return;
	if (SDL_GetRendererInfo(r, &r_info)) {
		SDL_Log("    Renderer: No details available: %s",
			SDL_GetError());
	} else {
		SDL_Log("    Renderer: %s", r_info.name);
		if (SDL_GetRendererOutputSize(r, &wid, &hgt)) {
			SDL_Log("        Output size: %s", SDL_GetError());
		} else {
			SDL_Log("        Output size: %d x %d", wid, hgt);
		}
		SDL_Log("        Maximum texture size: %d x %d",
			r_info.max_texture_width, r_info.max_texture_height);
		SDL_Log("        Flags:%s%s%s%s",
			(r_info.flags & SDL_RENDERER_SOFTWARE)
			? " SOFTWARE" : "",
			(r_info.flags & SDL_RENDERER_ACCELERATED)
			? " ACCELERATED" : "",
			(r_info.flags & SDL_RENDERER_PRESENTVSYNC)
			? " PRESENTVSYNC" : "",
			(r_info.flags & SDL_RENDERER_TARGETTEXTURE)
			? " TARGETTEXTURE" : "");
	}
}

static void render_clear(const struct sdlpui_window *window,
		SDL_Texture *texture, const SDL_Color *color)
{
	SDL_SetRenderTarget(window->renderer, texture);
	SDL_SetRenderDrawColor(window->renderer,
			color->r, color->g, color->b, color->a);
	SDL_RenderClear(window->renderer);
}

static void render_wallpaper_tiled(const struct sdlpui_window *window)
{
	SDL_SetRenderTarget(window->renderer, NULL);

	SDL_Rect rect = {0, 0, window->wallpaper.w, window->wallpaper.h};
	for (rect.y = window->inner_rect.y;
			rect.y < window->inner_rect.h;
			rect.y += rect.h)
	{
		for (rect.x = window->inner_rect.x;
				rect.x < window->inner_rect.w;
				rect.x += rect.w)
		{
			SDL_RenderCopy(window->renderer, window->wallpaper.texture, NULL, &rect);
		}
	}
}

static void render_wallpaper_scaled(const struct sdlpui_window *window)
{
	SDL_SetRenderTarget(window->renderer, NULL);
	SDL_RenderCopy(window->renderer, window->wallpaper.texture, NULL, NULL);
}

static void render_wallpaper_centered(const struct sdlpui_window *window)
{
	SDL_Rect rect;

	rect.w = window->wallpaper.w;
	rect.h = window->wallpaper.h;
	rect.x = window->inner_rect.x + (window->inner_rect.w - rect.w) / 2;
	rect.y = window->inner_rect.y + (window->inner_rect.h - rect.h) / 2;

	SDL_SetRenderTarget(window->renderer, NULL);
	SDL_RenderCopy(window->renderer, window->wallpaper.texture, NULL, &rect);
}

static void render_background(const struct sdlpui_window *window)
{
	render_clear(window, NULL, &window->color);

	switch (window->wallpaper.mode) {
		case WALLPAPER_DONT_SHOW:
			return;
		case WALLPAPER_TILED:
			render_wallpaper_tiled(window);
			return;
		case WALLPAPER_CENTERED:
			render_wallpaper_centered(window);
			return;
		case WALLPAPER_SCALED:
			render_wallpaper_scaled(window);
			return;
		default:
			quit_fmt("bad wallpaper mode %d in window %u",
					window->wallpaper.mode, window->index);
			break;
	}
}

static void render_outline_rect(const struct sdlpui_window *window,
		SDL_Texture *texture, const SDL_Rect *rect, const SDL_Color *color)
{
	SDL_SetRenderTarget(window->renderer, texture);
	SDL_SetRenderDrawColor(window->renderer,
			color->r, color->g, color->b, color->a);
	SDL_RenderDrawRect(window->renderer, rect);
}

static void render_outline_rect_width(const struct sdlpui_window *window,
		SDL_Texture *texture, const SDL_Rect *rect, const SDL_Color *color, int width)
{
	SDL_Rect dst = *rect;

	for (int i = 0; i < width; i++) {
		render_outline_rect(window, texture, &dst, color);
		resize_rect(&dst, 1, 1, -1, -1);
	}
}

static void render_fill_rect(const struct sdlpui_window *window,
		SDL_Texture *texture, const SDL_Rect *rect, const SDL_Color *color)
{
	SDL_SetRenderTarget(window->renderer, texture);
	SDL_SetRenderDrawColor(window->renderer,
			color->r, color->g, color->b, color->a);
	SDL_RenderFillRect(window->renderer, rect);
}

static void render_all(struct sdlpui_window *window)
{
	size_t i;
	struct sdlpui_dialog *d;

	render_background(window);

	for (d = window->d_tail; d; d = d->prev) {
		if (d->texture) {
			SDL_RenderCopy(window->renderer, d->texture, NULL,
				&d->rect);
		} else if (d->ftb->render) {
			(*d->ftb->render)(d, window);
		}
	}

	for (i = 0; i < N_ELEMENTS(window->subwindows); i++) {
		struct subwindow *subwindow = window->subwindows[i];

		if (subwindow != NULL && subwindow->visible) {
			SDL_RenderCopy(window->renderer,
					subwindow->texture,
					NULL, &subwindow->full_rect);
		}
	}
}

static void render_status_bar(struct sdlpui_window *window)
{
	struct sdlpui_dialog *d;

	for (d = window->d_tail; d; d = d->prev) {
		if (d->dirty) {
			if (d->ftb->render) {
				(*d->ftb->render)(d, window);
			}
			if (d->texture) {
				SDL_RenderCopy(window->renderer, d->texture,
					NULL, &d->rect);
			}
		} else if (d->texture) {
			SDL_RenderCopy(window->renderer, d->texture, NULL,
				&d->rect);
		} else if (d->ftb->render) {
			(*d->ftb->render)(d, window);
		}
	}
}

static void render_window_while_menu_active(struct sdlpui_window *window)
{
	size_t i;
	struct sdlpui_dialog *d;

	render_background(window);

	SDL_SetRenderTarget(window->renderer, NULL);

	for (i = 0; i < N_ELEMENTS(window->subwindows); i++) {
		struct subwindow *subwindow = window->subwindows[i];
		if (subwindow != NULL && subwindow->visible) {
			if (subwindow->sizing_rect.w > 0 && subwindow->sizing_rect.h > 0) {
				SDL_SetRenderTarget(window->renderer, subwindow->aux_texture);
				/* in case subwindow's color changed */
				render_fill_rect(window,
						subwindow->aux_texture, NULL, &subwindow->color);

				SDL_SetRenderTarget(window->renderer, NULL);
				SDL_RenderCopy(window->renderer,
						subwindow->aux_texture, NULL, &subwindow->sizing_rect);
			}

			SDL_RenderCopy(window->renderer,
					subwindow->texture,
					NULL, &subwindow->full_rect);
			if (subwindow->index == window->outlined_subwindow) {
				int outline_width = (subwindow->full_rect.w
					- subwindow->inner_rect.w) / 2
					- subwindow->borders.width;
				SDL_Rect outline_rect = subwindow->full_rect;

				resize_rect(&outline_rect,
					subwindow->borders.width,
					subwindow->borders.width,
					-subwindow->borders.width,
					-subwindow->borders.width);
				render_outline_rect_width(window, NULL,
					&outline_rect,
					&subwindow->app->colors[DEFAULT_SUBWINDOW_BORDER_COLOR],
					outline_width);
			}
		}
	}

	/*
	 * Render any dialogs or menus last, back to front, so they can draw
	 * over subwindows.
	 */
	for (d = window->d_tail; d; d = d->prev) {
		if (d->ftb->render && (d->dirty || !d->texture)) {
			(*d->ftb->render)(d, window);
		}
		d->dirty = false;
		if (d->texture) {
			SDL_SetRenderTarget(window->renderer, NULL);
			SDL_RenderCopy(window->renderer, d->texture, NULL,
				&d->rect);
		}
	}
}

static void set_subwindow_alpha(struct subwindow *subwindow, int alpha)
{
	SDL_SetTextureAlphaMod(subwindow->texture, alpha);
	SDL_SetTextureAlphaMod(subwindow->aux_texture, alpha);
}

static void set_subwindows_alpha(const struct sdlpui_window *window, int alpha)
{
	for (size_t i = 0; i < N_ELEMENTS(window->subwindows); i++) {
		struct subwindow *subwindow = window->subwindows[i];
		if (subwindow != NULL) {
			set_subwindow_alpha(subwindow, alpha);
		}
	}
}

/** this function allows to perform special things that are not
 * needed while playing the game, like moving terms */
static void redraw_window_while_menu_active(struct sdlpui_window *window)
{
	set_subwindows_alpha(window, window->alpha);
	render_window_while_menu_active(window);
	SDL_RenderPresent(window->renderer);
	window->next_redraw = SDL_GetTicks() + window->delay;
}

/** this function is mostly used while normally playing the game */
static void redraw_window(struct sdlpui_window *window)
{
	if (window->move_state.moving || window->size_state.sizing
			|| window->d_mouse || window->d_key) {
		redraw_window_while_menu_active(window);
		return;
	}

	render_all(window);
	SDL_RenderPresent(window->renderer);
	window->next_redraw = SDL_GetTicks() + window->delay;
}

static void try_redraw_window(struct sdlpui_window *window)
{
	if (window->next_redraw < SDL_GetTicks()) {
		redraw_window(window);
	}
}

static void redraw_all_windows(struct my_app *a, bool dirty)
{
	for (unsigned i = 0; i < MAX_WINDOWS; i++) {
		struct sdlpui_window *window = get_window_direct(a, i);
		if (window != NULL && (dirty ? window->dirty : true)) {
			render_status_bar(window);
			redraw_window(window);
			window->dirty = false;
		}
	}
}

/** this function is typically called in a loop, so for efficiency it doesn't
 * SetRenderTarget; caller must do it (but it does SetTextureColorMod) */
static void render_glyph_mono(const struct sdlpui_window *window,
		const struct font *font, SDL_Texture *dst_texture,
		int x, int y, const SDL_Color *fg, uint32_t codepoint)
{
	if (codepoint == ' ') {
		return;
	}

	SDL_Rect dst = {x, y, font->ttf.glyph.w, font->ttf.glyph.h};

	if (IS_CACHED_ASCII_CODEPOINT(codepoint)) {
		SDL_Rect src = font->cache.rects[codepoint];

		crop_rects(&src, &dst);

		SDL_SetTextureColorMod(font->cache.texture, fg->r, fg->g, fg->b);

		SDL_RenderCopy(window->renderer,
				font->cache.texture, &font->cache.rects[codepoint], &dst);
	} else {
		SDL_Surface *surface = TTF_RenderGlyph_Blended(font->ttf.handle,
				(Uint16) codepoint, *fg);
		if (surface == NULL) {
			return;
		}

		SDL_Texture *texture = SDL_CreateTextureFromSurface(window->renderer, surface);
		assert(texture != NULL);

		SDL_Rect src = {0, 0, surface->w, surface->h};

		crop_rects(&src, &dst);

		SDL_RenderCopy(window->renderer, texture, &src, &dst);

		SDL_FreeSurface(surface);
		SDL_DestroyTexture(texture);
	}
}

static void render_cursor(struct subwindow *subwindow, 
		int col, int row, bool big)
{
	SDL_Color color = subwindow->app->colors[DEFAULT_SUBWINDOW_CURSOR_COLOR];
	SDL_Rect rect = {
		subwindow->inner_rect.x + subwindow->font_width * col,
		subwindow->inner_rect.y + subwindow->font_height * row,
		subwindow->font_width * (big ? tile_width : 1),
		subwindow->font_height * (big ? tile_height : 1)
	};

	render_outline_rect(subwindow->window, subwindow->texture,
			&rect, &color);
}

static void render_grid_cell_text(const struct subwindow *subwindow,
		SDL_Texture *texture, int x, int y)
{
	struct grid_data grid_data;
	int a;
	int ta;
	wchar_t c;
	wchar_t tc;

	map_info(loc(x, y), &grid_data);
	grid_data_as_text(&grid_data, &a, &c, &ta, &tc);
	/* apparently either the same as a or obscured by a */
	(void) tc;

	SDL_Color fg = subwindow->app->colors[a % MAX_COLORS];
	SDL_Color bg;

	switch (ta / MULT_BG) {
		case BG_BLACK:
			bg = subwindow->color;
			break;
		case BG_SAME:
			bg = fg;
			break;
		case BG_DARK:
			bg = subwindow->app->colors[DEFAULT_SHADE_COLOR];
			break;
		default:
			/* debugging */
			bg = subwindow->app->colors[DEFAULT_ERROR_COLOR];
	}

	SDL_Rect rect = {
		x * subwindow->font_width,
		y * subwindow->font_height,
		subwindow->font_width,
		subwindow->font_height
	};

	render_fill_rect(subwindow->window, texture, &rect, &bg);
	render_glyph_mono(subwindow->window,
			subwindow->font, texture, rect.x, rect.y, &fg, (uint32_t) c);
}

/** does not SetRenderTarget */
static void render_tile_rect_scaled(const struct subwindow *subwindow,
		int col, int row, SDL_Rect dst, int a, int c)
{
	struct graphics *graphics = &subwindow->window->graphics;

	SDL_Rect src = {0, 0, graphics->tile_pixel_w, graphics->tile_pixel_h};

	int src_row = a & 0x7f;
	int src_col = c & 0x7f;

	src.x = src_col * src.w;
	src.y = src_row * src.h;

	if (graphics->overdraw_row != 0
			&& src_row >= graphics->overdraw_row
			&& src_row <= graphics->overdraw_max)
	{
		src.y -= src.h;
		dst.y -= dst.h;
		dst.h *= 2;
		src.h *= 2;
	}

	SDL_RenderCopy(subwindow->window->renderer,
			graphics->texture, &src, &dst);
}

static void render_tile_font_scaled(const struct subwindow *subwindow,
		int col, int row, int a, int c, bool fill, int dhrclip)
{
	struct graphics *graphics = &subwindow->window->graphics;

	SDL_Rect dst = {
		subwindow->inner_rect.x + col * subwindow->font_width,
		subwindow->inner_rect.y + row * subwindow->font_height,
		subwindow->font_width * tile_width,
		subwindow->font_height * tile_height
	};

	if (fill) {
		render_fill_rect(subwindow->window, subwindow->texture, &dst, &subwindow->color);
	}

	SDL_Rect src = {0, 0, graphics->tile_pixel_w, graphics->tile_pixel_h};

	SDL_SetRenderTarget(subwindow->window->renderer, subwindow->texture);

	int src_row = a & 0x7f;
	int src_col = c & 0x7f;

	src.x = src_col * src.w;
	src.y = src_row * src.h;

	if (graphics->overdraw_row != 0
			&& row > dhrclip
			&& src_row >= graphics->overdraw_row
			&& src_row <= graphics->overdraw_max)
	{
		src.y -= src.h;
		dst.y -= dst.h;
		dst.h *= 2;
		src.h *= 2;

		SDL_RenderCopy(subwindow->window->renderer,
				graphics->texture, &src, &dst);
	} else {
		SDL_RenderCopy(subwindow->window->renderer,
				graphics->texture, &src, &dst);
	}
}

static void render_grid_cell_tile(const struct subwindow *subwindow,
		SDL_Texture *texture, SDL_Rect tile, int x, int y)
{
	struct grid_data grid_data;
	int a;
	int ta;
	wchar_t c;
	wchar_t tc;

	map_info(loc(x, y), &grid_data);
	grid_data_as_text(&grid_data, &a, &c, &ta, &tc);

	SDL_SetRenderTarget(subwindow->window->renderer, texture);

	render_tile_rect_scaled(subwindow, x, y, tile, ta, tc);

	if (a == ta && c == tc) {
		return;
	}

	render_tile_rect_scaled(subwindow, x, y, tile, a, c);
}

static void clear_all_borders(struct sdlpui_window *window)
{
	for (size_t i = 0; i < N_ELEMENTS(window->subwindows); i++) {
		struct subwindow *subwindow = window->subwindows[i];
		if (subwindow != NULL) {
			subwindow->borders.error = false;
			render_borders(subwindow);
		}
	}
}

static void render_borders(struct subwindow *subwindow)
{
	SDL_Rect rect = {0};
	SDL_QueryTexture(subwindow->texture, NULL, NULL, &rect.w, &rect.h);

	SDL_Color *color;
	if (!subwindow->borders.error) {
		if (subwindow->borders.visible) {
			color = &subwindow->borders.color;
		} else {
			color = &subwindow->color;
		}
	} else {
		color = &subwindow->app->colors[DEFAULT_ERROR_COLOR];
	}

	render_outline_rect_width(subwindow->window,
			subwindow->texture, &rect, color,
			subwindow->borders.width);
}

static SDL_Texture *make_subwindow_texture(const struct sdlpui_window *window,
		int w, int h)
{
	SDL_Texture *texture = SDL_CreateTexture(window->renderer,
			window->pixelformat, SDL_TEXTUREACCESS_TARGET, w, h);
	if (texture == NULL) {
		quit_fmt("cannot create texture for subwindow in window %u: %s",
				window->index, SDL_GetError());
	}

	if (SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND) != 0) {
		SDL_DestroyTexture(texture);
		quit_fmt("cannot set blend mode for texture in window %u: %s",
				window->index, SDL_GetError());
	}

	return texture;
}

static void get_minimum_subwindow_size(bool is_main, int cell_w, int cell_h,
		int *minw, int *minh)
{
	int min_cols, min_rows;

	if (is_main) {
		min_cols = MIN_COLS_MAIN;
		min_rows = MIN_ROWS_MAIN;
	} else {
		min_cols = MIN_COLS_OTHER;
		min_rows = MIN_ROWS_OTHER;
	}
	*minw = min_cols * cell_w + 2 * DEFAULT_BORDER;
	*minh = min_rows * cell_h + 2 * DEFAULT_BORDER;
}

static void get_minimum_window_size(struct sdlpui_window *window,
		int *minw, int *minh)
{
	int max_sub_w = 0, max_sub_h = 0;
	bool have_sub = false;
	int i;

	if (window->status_bar->ftb->query_minimum_size) {
		(*window->status_bar->ftb->query_minimum_size)(
			window->status_bar, window, minw, minh);
	} else {
		(*window->status_bar->ftb->query_natural_size)(
			window->status_bar, window, minw, minh);
	}

	for (i = 0; i < MAX_SUBWINDOWS; ++i) {
		if (window->subwindows[i]) {
			int this_min_w, this_min_h;

			get_minimum_subwindow_size(
				window->subwindows[i]->index == MAIN_SUBWINDOW,
				window->subwindows[i]->font->ttf.glyph.w,
				window->subwindows[i]->font->ttf.glyph.h,
				&this_min_w, &this_min_h);
			max_sub_w = MAX(max_sub_w, this_min_w);
			max_sub_h = MAX(max_sub_h, this_min_h);
			have_sub = true;
		}
	}

	if (!have_sub) {
		/*
		 * There's no currently configured subwindows; leave room
		 * for one.
		 */
		get_minimum_subwindow_size(false, window->app->def_font_w,
			window->app->def_font_h, &max_sub_w, &max_sub_h);
	}

	*minw = MAX(*minw, max_sub_w);
	*minh += max_sub_h;
}

/**
 * Return true if the given font (face and size) can be used for a subwindow.
 *
 * \param font is the font of interest.
 * \param subwindow is the subwindow of the interest.
 * \param sizing_rect will, if not NULL and the return value is true, be
 * dereferenced and the sizes set to the dimensions of the bounding rectangle
 * for the subwindow when using the given font.
 */
static bool is_usable_font_for_subwindow(const struct font *font,
		const struct subwindow *subwindow, SDL_Rect *sizing_rect)
{
	SDL_Rect bounds = subwindow->full_rect;

	if (!is_ok_col_row(subwindow, &bounds, font->ttf.glyph.w,
			font->ttf.glyph.h)) {
		get_minimum_subwindow_size(subwindow->index == MAIN_SUBWINDOW,
			font->ttf.glyph.w, font->ttf.glyph.h,
			&bounds.w, &bounds.h);
	}

	if (bounds.w > subwindow->window->inner_rect.w
			|| bounds.h > subwindow->window->inner_rect.h)
	{
		return false;
	}

	if (sizing_rect) {
		*sizing_rect = bounds;
	}
	return true;
}

/**
 * Calculate the range of font sizes that can be used for a subwindow.
 *
 * \param subwindow is the subwindow to use.
 * \param font is the font of interest.  If NULL, the current font for the
 * subwindow is used.
 * \param min_size is set to the minimum point size that can be used.  If
 * the font is not a vector font, this will be set to zero.
 * \param max_size is set to one past the maximum point size that can be used.
 * If no size works, *max_size will be the same as *min_size when the function
 * returns.  If the font is not a vector font and the subwindow can use the
 * font, *max_size will be one.
 */
static void calculate_subwindow_font_size_bounds(struct subwindow *subwindow,
		const struct font_info *font, int *min_size, int *max_size)
{
	struct font *trial_font;
	int lo, hi;

	if (!font) {
		assert(subwindow->font);
		font = &subwindow->app->fonts[subwindow->font->index];
	}

	if (font->type != FONT_TYPE_VECTOR) {
		*min_size = 0;
		trial_font = make_font(subwindow->window, font->name, 0);
		if (is_usable_font_for_subwindow(trial_font, subwindow, NULL)) {
			*max_size = 1;
		} else {
			*max_size = 0;
		}
		free_font(trial_font);
		return;
	}

	/* Find the smallest size that works. */
	lo = MIN_VECTOR_FONT_SIZE;
	while (1) {
		if (lo > MAX_VECTOR_FONT_SIZE) {
			/* No size works */
			*min_size = DEFAULT_VECTOR_FONT_SIZE;
			*max_size = DEFAULT_VECTOR_FONT_SIZE;
			return;
		}
		trial_font = make_font(subwindow->window, font->name, lo);
		if (is_usable_font_for_subwindow(trial_font, subwindow, NULL)) {
			free_font(trial_font);
			*min_size = lo;
			break;
		}
		free_font(trial_font);
		++lo;
	}

	/* Find the largest size that works using a binary search. */
	lo = *min_size;
	hi = MAX_VECTOR_FONT_SIZE + 1;
	while (1) {
		int try;

		if (lo == hi - 1) {
			*max_size = hi;
			return;
		}
		try = (lo + hi) / 2;
		trial_font = make_font(subwindow->window, font->name, try);
		if (is_usable_font_for_subwindow(trial_font, subwindow, NULL)) {
			lo = try;
		} else {
			hi = try;
		}
		free_font(trial_font);
	}
}

static bool handle_shortcut_editor_key(struct sdlpui_dialog *d,
		struct sdlpui_window *w, const struct SDL_KeyboardEvent *e)
{
	struct shortcut_editor_data *pse;
	keycode_t ch;
	uint8_t mods;

	SDL_assert(d->type_code == SHORTCUT_EDITOR_CODE && d->priv);
	pse = d->priv;
	if (pse->changing_shortcut == -1) {
		return sdlpui_dialog_handle_key(d, w, e);
	}
	keyboard_event_to_angband_key(e, w->app->kp_as_mod, &ch, &mods);
	if (ch) {
		char keypress_desc[40];

		SDL_assert(pse->changing_shortcut >= 0
			&& pse->changing_shortcut < MAX_WINDOWS);
		w->app->menu_shortcuts[pse->changing_shortcut].type = EVT_KBRD;
		w->app->menu_shortcuts[pse->changing_shortcut].code = ch;
		w->app->menu_shortcuts[pse->changing_shortcut].mods = mods;
		keypress_to_text(keypress_desc, sizeof(keypress_desc),
			&w->app->menu_shortcuts[pse->changing_shortcut], true);
		sdlpui_change_caption(
			&pse->shortcut_displays[pse->changing_shortcut],
			d, w, keypress_desc);
		pse->changing_shortcut = -1;
		sdlpui_change_caption(&pse->prompt_label, d, w, " ");
		return true;
	}
	return false;
}

static bool handle_shortcut_editor_textin(struct sdlpui_dialog *d,
		struct sdlpui_window *w, const struct SDL_TextInputEvent *e)
{
	struct shortcut_editor_data *pse;
	keycode_t ch;
	uint8_t mods;

	SDL_assert(d->type_code == SHORTCUT_EDITOR_CODE && d->priv);
	pse = d->priv;
	if (pse->changing_shortcut == -1) {
		return sdlpui_dialog_handle_textin(d, w, e);
	}
	textinput_event_to_angband_key(e, w->app->kp_as_mod, &ch, &mods);
	if (ch) {
		char keypress_desc[40];

		SDL_assert(pse->changing_shortcut >= 0
			&& pse->changing_shortcut < MAX_WINDOWS);
		w->app->menu_shortcuts[pse->changing_shortcut].type = EVT_KBRD;
		w->app->menu_shortcuts[pse->changing_shortcut].code = ch;
		w->app->menu_shortcuts[pse->changing_shortcut].mods = mods;
		keypress_to_text(keypress_desc, sizeof(keypress_desc),
			&w->app->menu_shortcuts[pse->changing_shortcut], true);
		sdlpui_change_caption(
			&pse->shortcut_displays[pse->changing_shortcut],
			d, w, keypress_desc);
		pse->changing_shortcut = -1;
		sdlpui_change_caption(&pse->prompt_label, d, w, " ");
		return true;
	}
	return false;
}

static void render_shortcut_editor(struct sdlpui_dialog *d,
		struct sdlpui_window *w)
{
	struct SDL_Renderer *r = sdlpui_get_renderer(w);
	SDL_Rect dst_r = d->rect;
	struct shortcut_editor_data *pse;
	const SDL_Color *color;
	int i;

	SDL_assert(d->type_code == SHORTCUT_EDITOR_CODE && d->priv);
	pse = d->priv;
	SDLPUI_RENDER_TRACER("shortcut editor", d, "(not extracted)", d->rect,
		d->rect, d->texture);

	SDL_SetRenderTarget(r, d->texture);
	color = sdlpui_get_color(w, SDLPUI_COLOR_DIALOG_BG);
	SDL_SetRenderDrawColor(r, color->r, color->g, color->b, color->a);
	if (d->texture) {
		dst_r.x = 0;
		dst_r.y = 0;
		SDL_RenderClear(r);
	} else {
		SDL_RenderFillRect(r, &dst_r);
	}
	for (i = 0; i < MAX_WINDOWS; ++i) {
		if (pse->labels[i].ftb->render) {
			(*pse->labels[i].ftb->render)(&pse->labels[i], d, w, r);
		}
		if (pse->shortcut_displays[i].ftb->render) {
			(*pse->shortcut_displays[i].ftb->render)(
				&pse->shortcut_displays[i], d, w, r);
		}
		if (pse->change_buttons[i].ftb->render) {
			(*pse->change_buttons[i].ftb->render)(
				&pse->change_buttons[i], d, w, r);
		}
		if (pse->clear_buttons[i].ftb->render) {
			(*pse->clear_buttons[i].ftb->render)(
				&pse->clear_buttons[i], d, w, r);
		}
	}
	if (pse->prompt_label.ftb->render) {
		(*pse->prompt_label.ftb->render)(&pse->prompt_label, d, w, r);
	}
	if (pse->close_button.ftb->render) {
		(*pse->close_button.ftb->render)(&pse->close_button, d, w, r);
	}
	if (pse->reset_button.ftb->render) {
		(*pse->reset_button.ftb->render)(&pse->reset_button, d, w, r);
	}
	d->dirty = false;
}

static void goto_shortcut_editor_first_control(struct sdlpui_dialog *d,
		struct sdlpui_window *w)
{
	struct shortcut_editor_data *pse;

	SDL_assert(d->type_code == SHORTCUT_EDITOR_CODE && d->priv);
	pse = d->priv;
	SDL_assert(pse->change_buttons[0].ftb->gain_key);
	SDL_assert(!d->c_key || d->c_key == &pse->change_buttons[0]);
	(*pse->change_buttons[0].ftb->gain_key)(
		&pse->change_buttons[0], d, w, 0);
	d->c_key = &pse->change_buttons[0];
	sdlpui_dialog_gain_key_focus(w, d);
}

static void step_shortcut_editor_control(struct sdlpui_dialog *d,
		struct sdlpui_window *w, struct sdlpui_control *c, bool forward)
{
	struct shortcut_editor_data *pse;
	int i = 0;
	struct sdlpui_control *new_c = NULL;

	SDL_assert(d->type_code == SHORTCUT_EDITOR_CODE && d->priv);
	pse = d->priv;
	while (1) {
		if (i >= MAX_WINDOWS) {
			if (c == &pse->close_button) {
				new_c = (forward) ?
					&pse->reset_button :
					&pse->clear_buttons[MAX_WINDOWS - 1];
			} else if (c == &pse->reset_button) {
				new_c = (forward) ?
					&pse->change_buttons[0] :
					&pse->close_button;
			} else {
				/*
				 * c is not a button in the dialog.  Just go
				 * to the first button.
				 */
				new_c = &pse->change_buttons[0];
			}
			break;
		}
		if (c == &pse->change_buttons[i]) {
			new_c = (forward) ? &pse->clear_buttons[i] :
				((i > 0) ? &pse->clear_buttons[i - 1] :
				&pse->close_button);
			break;
		} else if (c == &pse->clear_buttons[i]) {
			new_c = (forward) ? ((i < MAX_WINDOWS - 1) ?
				&pse->change_buttons[i + 1] :
				&pse->close_button) : &pse->change_buttons[i];
			break;
		}
		++i;
	}
	if (d->c_key && d->c_key != new_c &&
			d->c_key->ftb->lose_key) {
		(*d->c_key->ftb->lose_key)(d->c_key, d, w, new_c, d);
	}
	SDL_assert(new_c->ftb->gain_key);
	(*new_c->ftb->gain_key)(new_c, d, w, 0);
	d->c_key = new_c;
}

static struct sdlpui_control *find_shortcut_editor_control_containing(
		struct sdlpui_dialog* d, struct sdlpui_window *w, Sint32 x,
		Sint32 y, int *comp_ind)
{
	struct shortcut_editor_data *pse;
	struct sdlpui_control *c = NULL;

	SDL_assert(d->type_code == SHORTCUT_EDITOR_CODE && d->priv);
	pse = d->priv;

	/* Make the coordinates relative to the dialog. */
	x -= d->rect.x;
	y -= d->rect.y;

	*comp_ind = 0;
	if (y < pse->close_button.rect.y) {
		if (y >= pse->change_buttons[0].rect.y) {
			int i = (y - pse->change_buttons[0].rect.y) /
				(pse->change_buttons[1].rect.y
					- pse->change_buttons[0].rect.y);

			if (i < MAX_WINDOWS && i >= 0
					&& y >= pse->change_buttons[i].rect.y
					&& y < pse->change_buttons[i].rect.y
					+ pse->change_buttons[i].rect.h) {
				if (x >= pse->change_buttons[i].rect.x
						&& x < pse->change_buttons[i].rect.x
						+ pse->change_buttons[i].rect.w) {
					c = &pse->change_buttons[i];
				} else if (x >= pse->clear_buttons[i].rect.x
						&& x < pse->clear_buttons[i].rect.x
						+ pse->clear_buttons[i].rect.w) {
					c = &pse->clear_buttons[i];
				}
			}
		}
	} else if (y < pse->close_button.rect.y + pse->close_button.rect.h) {
		if (x >= pse->close_button.rect.x
				&& x < pse->close_button.rect.x
					+ pse->close_button.rect.w) {
			c = &pse->close_button;
		} else if (x >= pse->reset_button.rect.x
				&& x < pse->reset_button.rect.x
					+ pse->reset_button.rect.w) {
			c = &pse->reset_button;
		}
	}
	return c;
}

static void query_shortcut_editor_natural_size(struct sdlpui_dialog *d,
		struct sdlpui_window *w, int *width, int *height)
{
	struct shortcut_editor_data *pse;
	int dw = 0, dh = 0, cw, ch, roww, rowh;

	SDL_assert(d->type_code == SHORTCUT_EDITOR_CODE && d->priv);
	pse = d->priv;
	(*pse->labels[0].ftb->query_natural_size)(&pse->labels[0],
		d, w, &roww, &rowh);
	/*
	 * The shortcut display will be three times the width of the label that
	 * precedes it.
	 */
	roww *= 3;
	(*pse->change_buttons[0].ftb->query_natural_size)(
		&pse->change_buttons[0], d, w, &cw, &ch);
	/*
	 * Leave 1/8 of the width (rounded up) as space between the change and
	 * clear buttons.
	 */
	roww += (9 * cw + 7) / 8;
	rowh = MAX(rowh, ch);
	(*pse->clear_buttons[0].ftb->query_natural_size)(&pse->clear_buttons[0],
		d, w, &cw, &ch);
	roww += cw;
	rowh = MAX(rowh, ch);
	dw = MAX(dw, roww);
	dh += MAX_WINDOWS * rowh;
	(*pse->close_button.ftb->query_natural_size)(&pse->close_button,
		d, w, &roww, &rowh);
	(*pse->reset_button.ftb->query_natural_size)(&pse->reset_button,
		d, w, &cw, &ch);
	roww = 4 * MAX(roww, cw);
	rowh = MAX(rowh, ch);
	dw = MAX(dw, roww);
	/*
	 * Leave space between the buttons at the bottom of the dialog and the
	 * rest.
	 */
	dh += 3 * rowh;
	*width = dw;
	*height = dh;
}

static void resize_shortcut_editor(struct sdlpui_dialog *d,
		struct sdlpui_window *w, int width, int height)
{
	struct shortcut_editor_data *pse;
	int i, x, y, cw, ch, rowh;

	SDL_assert(d->type_code == SHORTCUT_EDITOR_CODE && d->priv);
	pse = d->priv;
#ifndef NDEBUG
	{
		int dw, dh;

		query_shortcut_editor_natural_size(d, w, &dw, &dh);
		SDL_assert(width >= dw && height >= dh);
	}
#endif

	y = 0;
	for (i = 0; i < MAX_WINDOWS; ++i) {
		x = 0;
		rowh = 0;
		(*pse->labels[i].ftb->query_natural_size)(&pse->labels[i], d, w,
			&cw, &ch);
		if (pse->labels[i].ftb->resize) {
			(*pse->labels[i].ftb->resize)(&pse->labels[i], d, w,
				cw, ch);
		} else {
			pse->labels[i].rect.w = cw;
			pse->labels[i].rect.h = ch;
		}
		pse->labels[i].rect.x = x;
		pse->labels[i].rect.y = y;
		x += cw;
		rowh = MAX(rowh, ch);

		if (pse->shortcut_displays[i].ftb->resize) {
			(*pse->shortcut_displays[i].ftb->resize)(
				&pse->shortcut_displays[i], d, w, cw * 2, ch);
		} else {
			pse->shortcut_displays[i].rect.w = cw * 2;
			pse->shortcut_displays[i].rect.h = ch;
		}
		pse->shortcut_displays[i].rect.x = x;
		pse->shortcut_displays[i].rect.y = y;
		x += cw * 2;

		(*pse->change_buttons[i].ftb->query_natural_size)(
			&pse->change_buttons[i], d, w, &cw, &ch);
		if (pse->change_buttons[i].ftb->resize) {
			(*pse->change_buttons[i].ftb->resize)(
				&pse->change_buttons[i], d, w, cw, ch);
		} else {
			pse->change_buttons[i].rect.w = cw;
			pse->change_buttons[i].rect.h = ch;
		}
		pse->change_buttons[i].rect.x = x;
		pse->change_buttons[i].rect.y = y;
		/*
		 * Leave 1/8 of the width as space between the change and clear
		 * buttons.
		 */
		x += (9 * cw + 7) / 8;
		rowh = MAX(rowh, ch);

		(*pse->clear_buttons[i].ftb->query_natural_size)(
			&pse->clear_buttons[i], d, w, &cw, &ch);
		if (pse->clear_buttons[i].ftb->resize) {
			(*pse->clear_buttons[i].ftb->resize)(
				&pse->clear_buttons[i], d, w, cw, ch);
		} else {
			pse->clear_buttons[i].rect.w = cw;
			pse->clear_buttons[i].rect.h = ch;
		}
		pse->clear_buttons[i].rect.x = x;
		pse->clear_buttons[i].rect.y = y;
		x += cw;
		rowh = MAX(rowh, ch);

		y += rowh;
	}

	(*pse->prompt_label.ftb->query_natural_size)(&pse->prompt_label, d, w,
		&cw, &ch);
	SDL_assert(width >= cw);
	if (pse->prompt_label.ftb->resize) {
		(*pse->prompt_label.ftb->resize)(&pse->prompt_label, d, w,
			width, ch);
	} else {
		pse->prompt_label.rect.w = width;
		pse->prompt_label.rect.h = ch;
	}
	pse->prompt_label.rect.x = 0;
	pse->prompt_label.rect.y = y;
	y += ch;

	x = width / 8;
	rowh = 0;
	(*pse->close_button.ftb->query_natural_size)(&pse->close_button, d, w,
		&cw, &ch);
	SDL_assert(width >= cw * 4);
	cw = MAX(cw, width / 4);
	rowh = MAX(rowh, ch);
	if (pse->close_button.ftb->resize) {
		(*pse->close_button.ftb->resize)(
			&pse->close_button, d, w, cw, ch);
	} else {
		pse->close_button.rect.w = cw;
		pse->close_button.rect.h = ch;
	}
	pse->close_button.rect.x = x;
	x += cw + width / 4;
	(*pse->reset_button.ftb->query_natural_size)(&pse->reset_button, d, w,
		&cw, &ch);
	SDL_assert(width >= cw * 4);
	cw = MAX(cw, width / 4);
	rowh = MAX(rowh, ch);
	if (pse->reset_button.ftb->resize) {
		(*pse->reset_button.ftb->resize)(
			&pse->reset_button, d, w, cw, ch);
	} else {
		pse->reset_button.rect.w = cw;
		pse->reset_button.rect.h = ch;
	}
	pse->reset_button.rect.x = x;
	SDL_assert(y + rowh <= height);
	pse->close_button.rect.y = height - rowh;
	pse->reset_button.rect.y = height - rowh;

	d->rect.w = width;
	d->rect.h = height;
}

static void cleanup_shortcut_editor(struct sdlpui_dialog *d)
{
	struct shortcut_editor_data *pse;
	int i;

	SDL_assert(d->type_code == SHORTCUT_EDITOR_CODE && d->priv);
	pse = d->priv;
	for (i = 0; i < MAX_WINDOWS; ++i) {
		if (pse->labels[i].ftb->cleanup) {
			(*pse->labels[i].ftb->cleanup)(&pse->labels[i]);
		}
		if (pse->shortcut_displays[i].ftb->cleanup) {
			(*pse->shortcut_displays[i].ftb->cleanup)(
				&pse->shortcut_displays[i]);
		}
		if (pse->change_buttons[i].ftb->cleanup) {
			(*pse->change_buttons[i].ftb->cleanup)(
				&pse->change_buttons[i]);
		}
		if (pse->clear_buttons[i].ftb->cleanup) {
			(*pse->clear_buttons[i].ftb->cleanup)(
				&pse->clear_buttons[i]);
		}
	}
	if (pse->prompt_label.ftb->cleanup) {
		(*pse->prompt_label.ftb->cleanup)(&pse->prompt_label);
	}
	if (pse->close_button.ftb->cleanup) {
		(*pse->close_button.ftb->cleanup)(&pse->close_button);
	}
	if (pse->reset_button.ftb->cleanup) {
		(*pse->reset_button.ftb->cleanup)(&pse->reset_button);
	}
	SDL_free(pse);
}

static void handle_shortcut_change(struct sdlpui_control *c,
		struct sdlpui_dialog *d, struct sdlpui_window *w)
{
	struct shortcut_editor_data *pse;
	int tag = sdlpui_get_tag(c);

	SDL_assert(d->type_code == SHORTCUT_EDITOR_CODE && d->priv);
	pse = d->priv;
	SDL_assert(tag >= 0 && tag < MAX_WINDOWS);
	pse->changing_shortcut = tag;
	sdlpui_change_caption(&pse->prompt_label, d, w,
		format("Press key combination for window %d's new shortcut",
		tag + 1));
}

static void handle_shortcut_clear(struct sdlpui_control *c,
		struct sdlpui_dialog *d, struct sdlpui_window *w)
{
	struct shortcut_editor_data *pse;
	int tag = sdlpui_get_tag(c);

	SDL_assert(d->type_code == SHORTCUT_EDITOR_CODE && d->priv);
	pse = d->priv;
	SDL_assert(tag >= 0 && tag < MAX_WINDOWS);
	if (w->app->menu_shortcuts[tag].type != EVT_NONE) {
		w->app->menu_shortcuts[tag].type = EVT_NONE;
		w->app->menu_shortcuts[tag].code = 0;
		w->app->menu_shortcuts[tag].mods = 0;
		sdlpui_change_caption(&pse->shortcut_displays[tag], d, w,
			"None");
	}
	if (pse->changing_shortcut != -1) {
		/* Reset to no prompt for a keystroke. */
		pse->changing_shortcut = -1;
		sdlpui_change_caption(&pse->prompt_label, d, w, " ");
	}
}

static void handle_shortcut_editor_close(struct sdlpui_control *c,
		struct sdlpui_dialog *d, struct sdlpui_window *w)
{
	sdlpui_popdown_dialog(d, w, false);
}

static void handle_shortcut_editor_reset(struct sdlpui_control *c,
		struct sdlpui_dialog *d, struct sdlpui_window *w)
{
	struct shortcut_editor_data *pse;
	int i;

	SDL_assert(d->type_code == SHORTCUT_EDITOR_CODE && d->priv);
	pse = d->priv;
	for (i = 0; i < MAX_WINDOWS; ++i) {
		if (w->app->menu_shortcuts[i].type == EVT_NONE) {
			continue;
		}
		w->app->menu_shortcuts[i].type = EVT_NONE;
		w->app->menu_shortcuts[i].code = 0;
		w->app->menu_shortcuts[i].mods = 0;
		sdlpui_change_caption(&pse->shortcut_displays[i], d, w, "None");
	}
	if (pse->changing_shortcut != -1) {
		/* Reset to no prompt for a keystroke. */
		pse->changing_shortcut = -1;
		sdlpui_change_caption(&pse->prompt_label, d, w, " ");
	}
}

static void hide_shortcut_editor(struct sdlpui_dialog *d,
		struct sdlpui_window *w, bool up)
{
	if (!up) {
		SDL_assert(w->shorte == d);
		w->shorte = NULL;
	}
}

static void show_shortcut_editor(struct sdlpui_window *w, int x, int y)
{
	static const struct sdlpui_dialog_funcs shortcut_editor_funcs = {
		handle_shortcut_editor_key,
		handle_shortcut_editor_textin,
		NULL,
		sdlpui_dialog_handle_mouseclick,
		sdlpui_dialog_handle_mousemove,
		sdlpui_dialog_handle_mousewheel,
		sdlpui_dialog_handle_loses_mouse,
		sdlpui_dialog_handle_loses_key,
		sdlpui_dialog_handle_window_loses_mouse,
		sdlpui_dialog_handle_window_loses_key,
		render_shortcut_editor,
		NULL,
		goto_shortcut_editor_first_control,
		step_shortcut_editor_control,
		find_shortcut_editor_control_containing,
		NULL,
		NULL,
		NULL,
		NULL,
		resize_shortcut_editor,
		query_shortcut_editor_natural_size,
		NULL,
		cleanup_shortcut_editor
	};
	struct shortcut_editor_data *pse;
	int i;
	int dw, dh;

	if (w->shorte) {
		sdlpui_popup_dialog(w->shorte, w, true);
		return;
	}
	w->shorte = SDL_malloc(sizeof(*w->shorte));
	pse = SDL_malloc(sizeof(*pse));
	for (i = 0; i < MAX_WINDOWS; ++i) {
		char keypress_desc[40];

		sdlpui_create_label(&pse->labels[i],
			format("Window %d menu", i + 1),  SDLPUI_HOR_LEFT);
		if (w->app->menu_shortcuts[i].type == EVT_KBRD) {
			keypress_to_text(keypress_desc, sizeof(keypress_desc),
				&w->app->menu_shortcuts[i], true);
		} else {
			(void)my_strcpy(keypress_desc, "None",
				sizeof(keypress_desc));
		}
		sdlpui_create_label(&pse->shortcut_displays[i], keypress_desc,
			SDLPUI_HOR_LEFT);
		sdlpui_create_push_button(&pse->change_buttons[i],
			"Change", SDLPUI_HOR_CENTER, handle_shortcut_change,
			i, false);
		sdlpui_create_push_button(&pse->clear_buttons[i],
			"Clear", SDLPUI_HOR_CENTER, handle_shortcut_clear,
			i, false);
	}
	sdlpui_create_label(&pse->prompt_label, "", SDLPUI_HOR_CENTER);
	sdlpui_create_push_button(&pse->close_button, "Close",
		SDLPUI_HOR_CENTER, handle_shortcut_editor_close, 0, false);
	sdlpui_create_push_button(&pse->reset_button, "Reset",
		SDLPUI_HOR_CENTER, handle_shortcut_editor_reset, 0,
		false);
	pse->changing_shortcut = -1;

	w->shorte->ftb = &shortcut_editor_funcs;
	w->shorte->pop_callback = hide_shortcut_editor;
	w->shorte->recreate_textures_callback = NULL;
	w->shorte->next = NULL;
	w->shorte->prev = NULL;
	w->shorte->texture = NULL;
	w->shorte->c_mouse = NULL;
	w->shorte->c_key = NULL;
	w->shorte->priv = pse;
	w->shorte->type_code = SHORTCUT_EDITOR_CODE;
	w->shorte->tag = 0;
	w->shorte->pinned = false;
	w->shorte->dirty = true;

	(*w->shorte->ftb->query_natural_size)(w->shorte, w, &dw, &dh);
	if (w->shorte->ftb->resize) {
		(*w->shorte->ftb->resize)(w->shorte, w, dw, dh);
	} else {
		w->shorte->rect.w = dw;
		w->shorte->rect.h = dh;
	}
	w->shorte->rect.x = x;
	w->shorte->rect.y = y;
	sdlpui_popup_dialog(w->shorte, w, true);
}

static void hide_about(struct sdlpui_dialog *d, struct sdlpui_window *w,
		bool up)
{
	if (!up) {
		SDL_assert(w->infod == d);
		w->infod = NULL;
	}
}

static void recreate_about_dialog_textures(struct sdlpui_dialog *d,
		struct sdlpui_window *w, bool all)
{
	struct sdlpui_simple_info *psi;
	int i;

	if (!all) {
		return;
	}

	SDL_assert(d->type_code == SDLPUI_DIALOG_SIMPLE_INFO);
	psi = (struct sdlpui_simple_info*)d->priv;

	/* Recreate the static texture for the one image in the dialog. */
	for (i = 0; i < psi->number; ++i) {
		if (psi->labels[i].type_code == SDLPUI_CTRL_IMAGE) {
			char path[4096];
			struct sdlpui_image *pi =
				(struct sdlpui_image*)psi->labels[i].priv;

			if (pi->image) {
				SDL_DestroyTexture(pi->image);
			}
			path_build(path, sizeof(path), DEFAULT_ABOUT_ICON_DIR,
				DEFAULT_ABOUT_ICON);
			pi->image = load_image(w, path);

			d->dirty = true;
			sdlpui_signal_redraw(w);
			break;
		}
	}
}

static void show_about(struct sdlpui_window *window, int x, int y)
{
	if (!window->infod) {
		char path[4096];
		SDL_Texture *texture;

		window->infod = sdlpui_start_simple_info("Ok", NULL,
			recreate_about_dialog_textures, 0);
		path_build(path, sizeof(path), DEFAULT_ABOUT_ICON_DIR,
			DEFAULT_ABOUT_ICON);
		texture = load_image(window, path);
		sdlpui_simple_info_add_image(window->infod, texture,
			SDLPUI_HOR_CENTER, DEFAULT_XTRA_BORDER, 0,
			DEFAULT_XTRA_BORDER, DEFAULT_XTRA_BORDER);
		sdlpui_simple_info_add_label(window->infod, buildid,
			SDLPUI_HOR_CENTER);
		sdlpui_simple_info_add_label(window->infod,
			"See http://www.rephial.org", SDLPUI_HOR_CENTER);
		sdlpui_simple_info_add_label(window->infod,
			"Visit our forum at https://angband.live/forums/",
			SDLPUI_HOR_CENTER);
		sdlpui_complete_simple_info(window->infod, window);
		window->infod->pop_callback = hide_about;
		window->infod->rect.x = x;
		window->infod->rect.y = y;
	}
	sdlpui_popup_dialog(window->infod, window, true);
}

static void hide_sdl_details(struct sdlpui_dialog *d, struct sdlpui_window *w,
		bool up)
{
	if (!up) {
		SDL_assert(w->detaild == d);
		w->detaild = NULL;
	}
}

static void show_sdl_details(struct sdlpui_window *window, int x, int y)
{
	if (!window->detaild) {
		char *label;
		int wid, hgt, wx, wy, bt, bl, bb, br;
		Uint32 flags;

		window->detaild = sdlpui_start_simple_info("Ok", NULL, NULL, 0);

		if (window->index == MAIN_WINDOW) {
			const char *driver_name;
			const SDL_version *pvr;
			SDL_version vr, vc;
			int num_displays, i;

			SDL_GetVersion(&vr);
			SDL_VERSION(&vc);
			label = format("SDL version: %u.%u.%u (runtime; %s) "
				"%u.%u.%u (compiled; %s)", vr.major, vr.minor,
				vr.patch, SDL_GetRevision(),
				vc.major, vc.minor, vc.patch, SDL_REVISION);
			sdlpui_simple_info_add_label(window->detaild, label,
				SDLPUI_HOR_LEFT);
			pvr = IMG_Linked_Version();
			SDL_IMAGE_VERSION(&vc);
			label = format("SDL_image version; %u.%u.%u (runtime) "
				"%u.%u.%u (compiled)",
				pvr->major, pvr->minor, pvr->patch,
				vc.major, vr.minor, vc.patch);
			sdlpui_simple_info_add_label(window->detaild, label,
				SDLPUI_HOR_LEFT);
			pvr = TTF_Linked_Version();
			SDL_TTF_VERSION(&vc);
			label = format("SDL_ttf version; %u.%u.%u (runtime) "
				"%u.%u.%u (compiled)",
				pvr->major, pvr->minor, pvr->patch,
				vc.major, vr.minor, vc.patch);
			sdlpui_simple_info_add_label(window->detaild, label,
				SDLPUI_HOR_LEFT);
#ifdef SOUND_SDL2
			if (is_sound_inited()) {
				pvr = Mix_Linked_Version();
				SDL_MIXER_VERSION(&vc);
				label = format("SDL_mixer version: %u.%u.%u "
					"(runtime) %u.%u.%u (compiled)",
					pvr->major, pvr->minor, pvr->patch,
					vc.major, vc.minor, vc.patch);
				sdlpui_simple_info_add_label(window->detaild,
					label, SDLPUI_HOR_LEFT);
			}
#endif
			driver_name = SDL_GetCurrentVideoDriver();
			label = format("Platform and video driver: \"%s\" "
				"\"%s\"", SDL_GetPlatform(), (driver_name) ?
				driver_name : "Not initialized");
			sdlpui_simple_info_add_label(window->detaild, label,
				SDLPUI_HOR_LEFT);
			num_displays = SDL_GetNumVideoDisplays();
			if (num_displays < 0) {
				label = format("No available displays: %s",
					SDL_GetError());
				sdlpui_simple_info_add_label(window->detaild,
					label, SDLPUI_HOR_LEFT);
			}
			for (i = 0; i < num_displays; ++i) {
				const char *name = SDL_GetDisplayName(i);

				if (name) {
					SDL_DisplayMode mode;

					label = format("Display %d: %s:", i,
						name);
					sdlpui_simple_info_add_label(
						window->detaild, label,
						SDLPUI_HOR_LEFT);
					if (SDL_GetCurrentDisplayMode(i,
							&mode)) {
						label = format("    Mode "
							"unavailable: %s",
							SDL_GetError());
						sdlpui_simple_info_add_label(
							window->detaild, label,
							SDLPUI_HOR_LEFT);
					} else {
						label = format("    Size: %d x "
							"%d", mode.w, mode.h);
						sdlpui_simple_info_add_label(
							window->detaild, label,
							SDLPUI_HOR_LEFT);
						label = format("    Refresh "
							"rate and pixel "
							"format: %d %lu",
							mode.refresh_rate,
							(unsigned long)mode.format);
						sdlpui_simple_info_add_label(
							window->detaild, label,
							SDLPUI_HOR_LEFT);
					}
				} else {
					label = format("Display %d: no name: "
						"%s", i, SDL_GetError());
					sdlpui_simple_info_add_label(
						window->detaild, label,
						SDLPUI_HOR_LEFT);
				}
			}
		}
		label = format("Window %u:", window->index);
		sdlpui_simple_info_add_label(window->detaild, label,
			SDLPUI_HOR_LEFT);
		label = format("    Display index and ID: %d %lu",
			SDL_GetWindowDisplayIndex(window->window),
			(unsigned long)SDL_GetWindowID(window->window));
		sdlpui_simple_info_add_label(window->detaild, label,
			SDLPUI_HOR_LEFT);
		SDL_GetWindowPosition(window->window, &wx, &wy);
		SDL_GetWindowSize(window->window, &wid, &hgt);
		label = format("    Geometry: (%d, %d) %d x %d", wx, wy, wid,
			hgt);
		sdlpui_simple_info_add_label(window->detaild, label,
			SDLPUI_HOR_LEFT);
		SDL_GetWindowBordersSize(window->window, &bt, &bl, &bb, &br);
		label = format("    Border sizes: %d %d %d %d", bt, bl, bb, br);
		sdlpui_simple_info_add_label(window->detaild, label,
			SDLPUI_HOR_LEFT);
		flags = SDL_GetWindowFlags(window->window);
		label = format("    Flags:%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s",
			(flags & SDL_WINDOW_FULLSCREEN) ? " FULLSCREEN" : "",
			(flags & SDL_WINDOW_OPENGL) ? " OPENGL" : "",
			(flags & SDL_WINDOW_SHOWN) ? " SHOWN" : "",
			(flags & SDL_WINDOW_HIDDEN) ? " HIDDEN" : "",
			(flags & SDL_WINDOW_BORDERLESS) ?  " BORDERLESS" : "",
			(flags & SDL_WINDOW_RESIZABLE) ? " RESIZABLE" : "",
			(flags & SDL_WINDOW_MINIMIZED) ? " MINIMIZED" : "",
			(flags & SDL_WINDOW_MAXIMIZED) ? " MAXIMIZED" : "",
			(flags & SDL_WINDOW_FULLSCREEN_DESKTOP)
			? " FULLSCREEN_DESKTOP" : "",
			(flags & SDL_WINDOW_FOREIGN) ? " FOREIGN" : "",
			(flags & SDL_WINDOW_ALLOW_HIGHDPI)
			? " ALLOW_HIGHDPI" : "",
			(flags & SDL_WINDOW_ALWAYS_ON_TOP)
			? "ALWAYS_ON_TOP" : "",
			(flags & SDL_WINDOW_SKIP_TASKBAR)
			? " SKIP_TASKBAR" : "",
			(flags & SDL_WINDOW_UTILITY) ? " UTILITY" : "",
			(flags & SDL_WINDOW_POPUP_MENU) ? " POPUP_MENU" : "",
			(flags & SDL_WINDOW_VULKAN) ? " VULKAN" : "",
			(flags & SDL_WINDOW_METAL) ? " METAL" : "");
		sdlpui_simple_info_add_label(window->detaild, label,
			SDLPUI_HOR_LEFT);
		if (window->renderer) {
			SDL_RendererInfo r_info;

			if (SDL_GetRendererInfo(window->renderer, &r_info)) {
				label = format("    Renderer: No details "
					"available: %s", SDL_GetError());
				sdlpui_simple_info_add_label(window->detaild,
					label, SDLPUI_HOR_LEFT);
			} else {
				label = format("    Renderer: %s", r_info.name);
				sdlpui_simple_info_add_label(window->detaild,
					label, SDLPUI_HOR_LEFT);
				if (SDL_GetRendererOutputSize(window->renderer,
						&wid, &hgt)) {
					label = format("        Output size: "
						"%s", SDL_GetError());
				} else {
					label = format("        Output size: "
						"%d x %d", wid, hgt);
				}
				sdlpui_simple_info_add_label(window->detaild,
					label, SDLPUI_HOR_LEFT);
				label = format("        Maximum texture size: "
					"%d x %d", r_info.max_texture_width,
					r_info.max_texture_height);
				sdlpui_simple_info_add_label(window->detaild,
					label, SDLPUI_HOR_LEFT);
				label = format("        Flags:%s%s%s%s",
					(r_info.flags & SDL_RENDERER_SOFTWARE)
					? " SOFTWARE" : "",
					(r_info.flags & SDL_RENDERER_ACCELERATED)
					? " ACCELERATED" : "",
					(r_info.flags & SDL_RENDERER_PRESENTVSYNC)
					? " PRESENTVSYNC" : "",
					(r_info.flags & SDL_RENDERER_TARGETTEXTURE)
					? " TARGETTEXTURE" : "");
				sdlpui_simple_info_add_label(window->detaild,
					label, SDLPUI_HOR_LEFT);
			}
		}
#ifdef SOUND_SDL2
		if (window->index == MAIN_WINDOW && is_sound_inited()) {
			const char *driver_name;
			int freq, n_chan;
			Uint16 fmt;

			driver_name = SDL_GetCurrentAudioDriver();
			label = format("Audio driver: %s", (driver_name)
				? driver_name : "Not initialized");
			sdlpui_simple_info_add_label(window->detaild, label,
				SDLPUI_HOR_LEFT);
			if (Mix_QuerySpec(&freq, &fmt, &n_chan)) {
				label = format("Mixer channels, frequency, and "
					"format: %d %d %lu", n_chan, freq,
					(unsigned long)fmt);
			} else {
				label = format("Mixer channels, frequency, and "
					"format: %s", SDL_GetError());
			}
			sdlpui_simple_info_add_label(window->detaild, label,
				SDLPUI_HOR_LEFT);
		}
#endif
		sdlpui_complete_simple_info(window->detaild, window);
		window->detaild->pop_callback = hide_sdl_details;
		window->detaild->rect.x = x;
		window->detaild->rect.y = y;
	}
	sdlpui_popup_dialog(window->detaild, window, true);
}

static void signal_move_state(struct sdlpui_window *window)
{
	bool was_active = window->move_state.active;

	SDL_assert(!window->size_state.active);
	window->alpha = was_active ? DEFAULT_ALPHA_FULL : DEFAULT_ALPHA_LOW;
	if (was_active) {
		window->move_state.active = false;
		window->move_state.moving = false;
		window->move_state.subwindow = NULL;
		set_subwindows_alpha(window, window->alpha);
	} else {
		window->move_state.active = true;
	}
	if (window->move_button) {
		struct sdlpui_menu_button *mb;

		SDL_assert(window->move_button->type_code
			== SDLPUI_CTRL_MENU_BUTTON);
		mb = (struct sdlpui_menu_button*)window->move_button->priv;
		SDL_assert(mb->subtype_code == SDLPUI_MB_TOGGLE);
		mb->v.toggled = window->move_state.active;
		window->status_bar->dirty = true;
	}

	SDL_SetWindowGrab(window->window, was_active ? SDL_FALSE : SDL_TRUE);
}

static void signal_size_state(struct sdlpui_window *window)
{
	bool was_active = window->size_state.active;

	SDL_assert(!window->move_state.active);
	window->alpha = was_active ? DEFAULT_ALPHA_FULL : DEFAULT_ALPHA_LOW;
	if (was_active) {
		window->size_state.active = false;
		window->size_state.sizing = false;
		if (window->size_state.subwindow != NULL) {
			memset(&window->size_state.subwindow->sizing_rect, 0,
				sizeof(window->size_state.subwindow->sizing_rect));
			window->size_state.subwindow = NULL;
		}
		set_subwindows_alpha(window, window->alpha);
	} else {
		window->size_state.active = true;
	}
	if (window->size_button) {
		struct sdlpui_menu_button *mb;

		SDL_assert(window->size_button->type_code
			== SDLPUI_CTRL_MENU_BUTTON);
		mb = (struct sdlpui_menu_button*)window->size_button->priv;
		SDL_assert(mb->subtype_code == SDLPUI_MB_TOGGLE);
		mb->v.toggled = window->size_state.active;
		window->status_bar->dirty = true;
	}

	SDL_SetWindowGrab(window->window, was_active ? SDL_FALSE : SDL_TRUE);
}

static void handle_button_movesize(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window)
{
	SDL_assert(ctrl->ftb->get_tag);
	if ((*ctrl->ftb->get_tag)(ctrl) == 0) {
		if (window->size_state.active) {
			/* toggle size button */
			signal_size_state(window);
		}
		signal_move_state(window);
	} else {
		if (window->move_state.active) {
			/* toggle move button */
			signal_move_state(window);
		}
		signal_size_state(window);
	}
}

static void handle_menu_shortcuts(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window)
{
	int x = ctrl->rect.x, y = ctrl->rect.y;

	sdlpui_popdown_dialog(dlg, window, true);
	show_shortcut_editor(window, x, y);
}

static void handle_menu_window(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window)
{
	int tag;
	struct sdlpui_window *other;

	SDL_assert(ctrl->ftb->get_tag);
	tag = (*ctrl->ftb->get_tag)(ctrl);
	SDL_assert(tag >= 0);
	sdlpui_popdown_dialog(dlg, window, true);
	other = get_window_direct(window->app, (unsigned int)tag);
	if (other == NULL) {
		other = get_new_window(window->app, (unsigned int)tag);
		SDL_assert(other != NULL);
		wipe_window_aux_config(other);
		start_window(other);
	} else {
		/* Previous versions didn't close.  Is that better? */
		handle_window_closed(window->app, other);
	}
}

static struct sdlpui_dialog *handle_menu_windows(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window,
		int ul_x_win, int ul_y_win)
{
	struct sdlpui_dialog *result = sdlpui_start_simple_menu(
		dlg, ctrl, MAX_WINDOWS, true, false, NULL, NULL, 0);
	unsigned int i;

	for (i = 1; i < MAX_WINDOWS; ++i) {
		struct sdlpui_control *c = sdlpui_get_simple_menu_next_unused(
			result, SDLPUI_MFLG_NONE);

		sdlpui_create_menu_toggle(c, format("Window-%u", i),
			SDLPUI_HOR_LEFT, handle_menu_window, i, false,
			get_window_direct(window->app, i) != NULL);
	}
	sdlpui_complete_simple_menu(result, window);
	result->rect.x = ul_x_win;
	result->rect.y = ul_y_win;

	return result;
}

static void handle_menu_fullscreen(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window)
{
	bool was_fullscreen = (window->flags & SDL_WINDOW_FULLSCREEN_DESKTOP);
	SDL_Rect tmp_rect;

	sdlpui_popdown_dialog(dlg, window, true);

	SDL_GetWindowSize(window->window, &tmp_rect.w, &tmp_rect.h);
	SDL_GetWindowPosition(window->window, &tmp_rect.x, &tmp_rect.y);
	if (!SDL_SetWindowFullscreen(window->window, (was_fullscreen) ?
			0 : SDL_WINDOW_FULLSCREEN_DESKTOP)) {
		/* Succeeded.  Swap cached sizes. */
		size_t i;

		window->full_rect = window->stored_rect;
		window->stored_rect = tmp_rect;
		for (i = 0; i < N_ELEMENTS(window->subwindows); ++i) {
			struct subwindow *subwindow = window->subwindows[i];

			if (subwindow != NULL) {
				tmp_rect = subwindow->stored_rect;
				subwindow->stored_rect = subwindow->full_rect;
				subwindow->full_rect = tmp_rect;
				if (!subwindow->full_rect.w
						|| !subwindow->full_rect.h) {
					/*
					 * Nothing configured so far for this
					 * mode, so use the configuration from
					 * the other mode.
					 */
					subwindow->full_rect =
						subwindow->stored_rect;
				} else if (subwindow->full_rect.w
						!= subwindow->stored_rect.w
						|| subwindow->full_rect.h
						!= subwindow->stored_rect.h) {
					subwindow->sizing_rect =
						subwindow->full_rect;
					resize_subwindow(subwindow);
				}
			}
		}

		if (was_fullscreen) {
			int minw, minh;

			get_minimum_window_size(window, &minw, &minh);
			SDL_SetWindowMinimumSize(window->window, minw, minh);
			/*
			 * If there is a previously configured size, use it.
			 * Otherwise, rely on SDL's default behavior.
			 */
			if (window->full_rect.w && window->full_rect.h) {
				SDL_SetWindowSize(window->window,
					window->full_rect.w,
					window->full_rect.h);
				resize_window(window, window->full_rect.w,
					window->full_rect.h);
				SDL_SetWindowPosition(window->window,
					window->full_rect.x,
					window->full_rect.y);
			}
		}
		window->flags = SDL_GetWindowFlags(window->window);
	} else {
		SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_WARNING,
			"Fullscreen failure",
			format("Could not change fullscreen setting:\n%s",
			SDL_GetError()), window->window);
	}
}

static void handle_menu_kp_mod(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window)
{
	sdlpui_popdown_dialog(dlg, window, true);
	window->app->kp_as_mod = !window->app->kp_as_mod;
}

static void handle_menu_about(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window)
{
	int x = ctrl->rect.x, y = ctrl->rect.y;

	sdlpui_popdown_dialog(dlg, window, true);
	show_about(window, x, y);
}

static void handle_menu_sdl_details(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window)
{
	int x = ctrl->rect.x, y = ctrl->rect.y;

	sdlpui_popdown_dialog(dlg, window, true);
	show_sdl_details(window, x, y);
}

static void handle_menu_quit(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window)
{
	sdlpui_popdown_dialog(dlg, window, true);
	handle_quit();
}

static void handle_menu_tile_set(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window)
{
	int new_id;

	SDL_assert(ctrl->ftb->get_tag);
	new_id = (*ctrl->ftb->get_tag)(ctrl);
	if (new_id == current_graphics_mode->grafID) {
		/*
		 * There's no change from the current graphics mode.
		 * Leave that button toggled on.
		 */
		struct sdlpui_menu_button *mb;

		SDL_assert(ctrl->type_code == SDLPUI_CTRL_MENU_BUTTON);
		mb = (struct sdlpui_menu_button*)ctrl->priv;
		SDL_assert(mb->subtype_code == SDLPUI_MB_TOGGLE);
		mb->v.toggled = true;
		dlg->dirty = true;
	} else {
		/* Change the graphics mode.  Toggle off the old mode. */
		int old_id = current_graphics_mode->grafID;
		graphics_mode *mode = get_graphics_mode(new_id);
		struct sdlpui_simple_menu *sm;
		int i;

		SDL_assert(mode);
		SDL_assert(dlg->type_code == SDLPUI_DIALOG_SIMPLE_MENU);
		sm = (struct sdlpui_simple_menu*)dlg->priv;
		for (i = 0; i < sm->number; ++i) {
			struct sdlpui_menu_button *mb;

			SDL_assert(sm->controls[i].type_code
				== SDLPUI_CTRL_MENU_BUTTON);
			mb = (struct sdlpui_menu_button*)sm->controls[i].priv;
			SDL_assert(mb->subtype_code == SDLPUI_MB_TOGGLE);
			if (mb->tag == old_id) {
				mb->v.toggled = false;
				dlg->dirty = true;
				break;
			}
		}
		reload_all_graphics(window->app, mode);
		refresh_angband_terms(window->app);
	}
}

static void handle_menu_tile_size(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window)
{
	struct sdlpui_menu_button *mb;
	int tag;

	SDL_assert(ctrl->type_code == SDLPUI_CTRL_MENU_BUTTON);
	mb = (struct sdlpui_menu_button*)ctrl->priv;
	SDL_assert(mb->subtype_code == SDLPUI_MB_RANGED_INT);
	SDL_assert(ctrl->ftb->get_tag);
	tag = (*ctrl->ftb->get_tag)(ctrl);
	if (tag) {
		tile_height = mb->v.ranged_int.curr;
	} else {
		tile_width = mb->v.ranged_int.curr;
	}
	refresh_angband_terms(window->app);
}

static struct sdlpui_dialog *handle_menu_tile_sizes(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window,
		int ul_x_win, int ul_y_win)
{
	/*
	 * Disable the menu entries to change the tile multipliers if not
	 * using tiles or if not at a command prompt in game (the latter
	 * avoids multiplier changes causing blank screens in in-game menus
	 * or display artifacts when the in-game menu is dismissed sometime
	 * after the multiplier change).
	 */
	bool disabled = (window->graphics.id == GRAPHICS_NONE
		|| !character_generated || !inkey_flag);
	struct sdlpui_dialog *result = sdlpui_start_simple_menu(dlg, ctrl, 2,
		true, false, NULL, NULL, 0);
	struct sdlpui_control *c;

	c = sdlpui_get_simple_menu_next_unused(result, SDLPUI_MFLG_NONE);
	sdlpui_create_menu_ranged_int(c, "- Tile width  %d +",
		SDLPUI_HOR_LEFT, handle_menu_tile_size, 0, disabled,
		tile_width, MIN_TILE_WIDTH, MAX_TILE_WIDTH);
	c = sdlpui_get_simple_menu_next_unused(result, SDLPUI_MFLG_NONE);
	sdlpui_create_menu_ranged_int(c, "- Tile height %d +",
		SDLPUI_HOR_LEFT, handle_menu_tile_size, 1, disabled,
		tile_height, MIN_TILE_HEIGHT, MAX_TILE_HEIGHT);
	sdlpui_complete_simple_menu(result, window);
	result->rect.x = ul_x_win;
	result->rect.y = ul_y_win;

	return result;
}

static struct sdlpui_dialog *handle_menu_tile_sets(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window,
		int ul_x_win, int ul_y_win)
{
	/*
	 * Only allow changes to the graphics mode when at a command prompt
	 * in game.  Could also allow while at the splash screen, but that
	 * isn't possible to test for with character_generated and
	 * character_dungeon.  In other situations, the saved screens for
	 * overlayed menus could have tile references that become outdated
	 * when the graphics mode is changed.
	 */
	bool disabled = !character_generated || !inkey_flag;
	struct sdlpui_dialog *result = sdlpui_start_simple_menu(dlg, ctrl,
		0, true, false, NULL, NULL, 0);
	graphics_mode *mode = graphics_modes;

	while (mode) {
		struct sdlpui_control *c = sdlpui_get_simple_menu_next_unused(
			result, SDLPUI_MFLG_NONE);

		sdlpui_create_menu_toggle(c, mode->menuname, SDLPUI_HOR_LEFT,
			handle_menu_tile_set, mode->grafID, disabled,
			current_graphics_mode->grafID == mode->grafID);
		mode = mode->pNext;
	}
	sdlpui_complete_simple_menu(result, window);
	result->rect.x = ul_x_win;
	result->rect.y = ul_y_win;

	return result;
}

static struct sdlpui_dialog *handle_menu_tiles(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window,
		int ul_x_win, int ul_y_win)
{
	struct sdlpui_dialog *result = sdlpui_start_simple_menu(dlg, ctrl, 2,
		true, false, NULL, NULL, 0);
	struct sdlpui_control *c;

	c = sdlpui_get_simple_menu_next_unused(result, SDLPUI_MFLG_NONE);
	sdlpui_create_submenu_button(c, "Set", SDLPUI_HOR_LEFT,
		handle_menu_tile_sets, SDLPUI_CHILD_MENU_RIGHT, 0, false);
	c = sdlpui_get_simple_menu_next_unused(result, SDLPUI_MFLG_NONE);
	sdlpui_create_submenu_button(c, "Size", SDLPUI_HOR_LEFT,
		handle_menu_tile_sizes, SDLPUI_CHILD_MENU_RIGHT, 0, false);
	sdlpui_complete_simple_menu(result, window);
	result->rect.x = ul_x_win;
	result->rect.y = ul_y_win;

	return result;
}

static void handle_menu_pw(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window)
{
	int tag, subw_idx, flag_idx;
	struct sdlpui_menu_button *mb;
	uint32_t *new_flags;

	SDL_assert(ctrl->ftb->get_tag);
	tag = (*ctrl->ftb->get_tag)(ctrl);
	SDL_assert(tag >= 0);
	subw_idx = tag / (int)N_ELEMENTS(window_flag_desc);
	SDL_assert(subw_idx >= 0 && subw_idx != MAIN_SUBWINDOW
		&& subw_idx < (int)N_ELEMENTS(window_flag));
	flag_idx = tag % (int)N_ELEMENTS(window_flag_desc);
	SDL_assert(ctrl->type_code == SDLPUI_CTRL_MENU_BUTTON);
	mb = (struct sdlpui_menu_button*)ctrl->priv;
	SDL_assert(mb->subtype_code == SDLPUI_MB_TOGGLE);
	new_flags = mem_alloc(N_ELEMENTS(window_flag) * sizeof(*new_flags));
	memcpy(new_flags, window_flag, N_ELEMENTS(window_flag)
		* sizeof(*new_flags));
	if (mb->v.toggled) {
		new_flags[subw_idx] |= (uint32_t)1 << flag_idx;
	} else {
		new_flags[subw_idx] &= ~((uint32_t)1 << flag_idx);
	}
	subwindows_set_flags(new_flags, N_ELEMENTS(window_flag));
	mem_free(new_flags);
	refresh_angband_terms(window->app);
}

static void handle_menu_font_name(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window)
{
	int tag, index, old_index;
	struct subwindow *subwindow;
	const struct font_info *font_info;

	SDL_assert(ctrl->ftb->get_tag);
	tag = (*ctrl->ftb->get_tag)(ctrl);
	subwindow = get_subwindow_by_index(window,
		(unsigned int)(tag / (2 * window->app->font_count) - 1), false);
	SDL_assert(subwindow);
	index = (unsigned int)tag
		% (unsigned int)(2 * window->app->font_count);
	old_index = subwindow->font->index;
	font_info = &window->app->fonts[index];

	if (old_index == index) {
		/*
		 * Already selected; pushing the button doesn't toggle it off.
		 */
		struct sdlpui_menu_button *mb;

		SDL_assert(ctrl->type_code == SDLPUI_CTRL_MENU_BUTTON);
		mb = (struct sdlpui_menu_button*)ctrl->priv;
		SDL_assert(mb->subtype_code == SDLPUI_MB_TOGGLE);
		mb->v.toggled = true;
		dlg->dirty = true;
		return;
	}

	if (reload_font(subwindow, font_info)) {
		/* Set the previous selected font in the menu to off. */
		int target_tag = tag
			- (tag % (2 * window->app->font_count))
			+ old_index;
		bool searching = true;
		int minw, minh;

		while (searching) {
			struct sdlpui_simple_menu *sm;
			struct sdlpui_control *pc;
			int i;

			SDL_assert(dlg->type_code == SDLPUI_DIALOG_SIMPLE_MENU);
			sm = (struct sdlpui_simple_menu*)dlg->priv;
			for (i = 0; i < sm->number; ++i) {
				struct sdlpui_menu_button *mb;

				SDL_assert(sm->controls[i].type_code
					== SDLPUI_CTRL_MENU_BUTTON);
				mb = (struct sdlpui_menu_button*)sm->controls[i].priv;
				if (mb->tag == target_tag) {
					mb->v.toggled = false;
					dlg->dirty = true;
					searching = false;
					break;
				}
			}

			pc = sdlpui_get_dialog_parent_ctrl(dlg);
			if (pc) {
				struct sdlpui_menu_button *mbp;

				SDL_assert(pc->type_code
					== SDLPUI_CTRL_MENU_BUTTON);
				mbp = pc->priv;
				if (streq(mbp->caption, "More")) {
					dlg = sdlpui_get_dialog_parent(dlg);
					SDL_assert(dlg);
				} else {
					searching = false;
				}
			} else {
				searching = false;
			}
		}

		/*
		 * With a font change, the minimum window size can be different.
		 */
		get_minimum_window_size(window, &minw, &minh);
		SDL_SetWindowMinimumSize(window->window, minw, minh);
	} else {
		/*
		 * Subwindow can't be resized to accommodate the font so
		 * disable it.
		 */
		struct sdlpui_menu_button *mb;

		SDL_assert(ctrl->type_code == SDLPUI_CTRL_MENU_BUTTON);
		mb = (struct sdlpui_menu_button*)ctrl->priv;
		SDL_assert(mb->subtype_code == SDLPUI_MB_TOGGLE);
		mb->disabled = true;
		mb->v.toggled = false;
		dlg->dirty = true;
	}
}

static void handle_menu_font_size(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window)
{
	int tag;
	struct subwindow *subwindow;
	struct sdlpui_menu_button *mb;
	struct font_info *info;

	SDL_assert(ctrl->ftb->get_tag);
	tag = (*ctrl->ftb->get_tag)(ctrl);
	subwindow = get_subwindow_by_index(window, (unsigned int)tag, false);
	SDL_assert(subwindow && subwindow->font);
	info = &window->app->fonts[subwindow->font->index];
	SDL_assert(ctrl->type_code == SDLPUI_CTRL_MENU_BUTTON);
	mb = (struct sdlpui_menu_button*)ctrl->priv;
	SDL_assert(mb->subtype_code == SDLPUI_MB_RANGED_INT);
	info->size = mb->v.ranged_int.curr;
	if (reload_font(subwindow, info)) {
		int minw, minh;

		/* A change to the font can change the minimum window size. */
		get_minimum_window_size(window, &minw, &minh);
		SDL_SetWindowMinimumSize(window->window, minw, minh);
	} else {
		/*
		 * The range of sizes was limited in handle_menu_font_sizes(),
		 * so the call to reload_font() should have worked.
		 */
		SDL_assert(0);
	}
}

static struct sdlpui_dialog *handle_menu_font_sizes(
		struct sdlpui_control *ctrl, struct sdlpui_dialog *dlg,
		struct sdlpui_window *window, int ul_x_win, int ul_y_win)
{
	int tag;
	struct subwindow *subwindow;
	bool is_vector_font;
	struct sdlpui_dialog *result;
	struct sdlpui_control *c;

	SDL_assert(ctrl->ftb->get_tag);
	tag = (*ctrl->ftb->get_tag)(ctrl);
	subwindow = get_subwindow_by_index(window, (unsigned int)tag, false);
	SDL_assert(subwindow);
	is_vector_font = window->app->fonts[subwindow->font->index].type
		== FONT_TYPE_VECTOR;
	calculate_subwindow_font_size_bounds(subwindow, NULL,
		&subwindow->min_font_size, &subwindow->max_font_size);
	SDL_assert(subwindow);
	result = sdlpui_start_simple_menu(dlg, ctrl, 2, true, false, NULL,
		NULL, 0);
	c = sdlpui_get_simple_menu_next_unused(result, SDLPUI_MFLG_NONE);
	sdlpui_create_menu_ranged_int(c, "- %2d points +", SDLPUI_HOR_LEFT,
		handle_menu_font_size, tag, !is_vector_font
		|| subwindow->min_font_size >= subwindow->max_font_size,
		subwindow->font->size, subwindow->min_font_size,
		(subwindow->min_font_size < subwindow->max_font_size) ?
		subwindow->max_font_size - 1 : subwindow->min_font_size);
	sdlpui_complete_simple_menu(result, window);
	result->rect.x = ul_x_win;
	result->rect.y = ul_y_win;

	return result;
}

static struct sdlpui_dialog *handle_menu_font_names(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window,
		int ul_x_win, int ul_y_win)
{
	int tag;
	SDL_Renderer *renderer;
	struct subwindow *subwindow;
	struct sdlpui_dialog *result;
	struct sdlpui_control *c;
	bool more_nesting = false;
	int win_w, win_h, start, count, i;

	renderer = sdlpui_get_renderer(window);
	SDL_GetRendererOutputSize(renderer, &win_w, &win_h);
	SDL_assert(ctrl->ftb->get_tag);
	tag = (*ctrl->ftb->get_tag)(ctrl);
	if (tag < 2 * window->app->font_count) {
		/* At the top level */
		subwindow = get_subwindow_by_index(window, tag, false);
		tag = (tag + 1) * 2 * window->app->font_count;
		start = 0;
	} else {
		/* Nested */
		subwindow = get_subwindow_by_index(window,
			tag / (2 * window->app->font_count) - 1, false);
		start = tag % (2 * window->app->font_count)
			- window->app->font_count;
		assert(start >= 0);
		tag -= start + window->app->font_count;
	}
	SDL_assert(subwindow);
	SDL_assert(start <= window->app->font_count);
	/*
	 * Figure out how many entries can fit.  Use the size of the parent
	 * control as a proxy for the size of the individual menu elements.
	 */
	SDL_assert(ctrl->rect.h > 0);
	count = (win_h - ul_y_win) / ctrl->rect.h;
	if (count < window->app->font_count - start
			&& count > 2
			&& win_w - ul_x_win >= (ctrl->rect.w + 1) / 2) {
		--count;
		more_nesting = true;
	} else if (count > window->app->font_count - start) {
		count = window->app->font_count - start;
	}
	result = sdlpui_start_simple_menu(dlg, ctrl,
		count + ((more_nesting) ? 1 : 0), true, false, NULL, NULL, 0);
	if (more_nesting) {
		c = sdlpui_get_simple_menu_next_unused(result,
			SDLPUI_MFLG_NONE);
		sdlpui_create_submenu_button(c, "More", SDLPUI_HOR_LEFT,
			handle_menu_font_names, SDLPUI_CHILD_MENU_RIGHT,
			tag + window->app->font_count + start + count, false);
	}
	for (i = start; i < start + count; ++i) {
		c = sdlpui_get_simple_menu_next_unused(result,
			SDLPUI_MFLG_NONE);
		/*
		 * Optimistically assume that it'll be possible to resize
		 * the subwindow for this font.
		 */
		sdlpui_create_menu_toggle(c, window->app->fonts[i].name,				SDLPUI_HOR_LEFT, handle_menu_font_name, tag + i,
			false, subwindow->font->index == (unsigned int)i);
	}
	sdlpui_complete_simple_menu(result, window);
	result->rect.x = ul_x_win;
	result->rect.y = ul_y_win;

	return result;
}

static struct sdlpui_dialog *handle_menu_purpose(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window,
		int ul_x_win, int ul_y_win)
{
	int i = 0, subw_idx;
	struct sdlpui_dialog *result;

	SDL_assert(ctrl->ftb->get_tag);
	subw_idx = (*ctrl->ftb->get_tag)(ctrl);
	SDL_assert(subw_idx >= 0 && subw_idx != MAIN_SUBWINDOW);
	result = sdlpui_start_simple_menu(dlg, ctrl,
		(int)N_ELEMENTS(window_flag_desc), true, false, NULL, NULL, 0);
	while (i < (int)N_ELEMENTS(window_flag_desc)) {
		if (window_flag_desc[i]) {
			struct sdlpui_control *c =
				sdlpui_get_simple_menu_next_unused(result,
				SDLPUI_MFLG_NONE);

			sdlpui_create_menu_toggle(c, window_flag_desc[i],
				SDLPUI_HOR_LEFT, handle_menu_pw, subw_idx *
				(int)N_ELEMENTS(window_flag_desc) + i, false,
				window_flag[subw_idx] & ((uint32_t)1 << i));
		}
		++i;
	}
	sdlpui_complete_simple_menu(result, window);
	result->rect.x = ul_x_win;
	result->rect.y = ul_y_win;

	return result;
}

static struct sdlpui_dialog *handle_menu_font(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window,
		int ul_x_win, int ul_y_win)
{
	int tag;
	struct sdlpui_dialog *result;
	struct sdlpui_control *c;

	SDL_assert(ctrl->ftb->get_tag);
	tag = (*ctrl->ftb->get_tag)(ctrl);
	result = sdlpui_start_simple_menu(dlg, ctrl, 2, true, false, NULL,
		NULL, 0);
	c = sdlpui_get_simple_menu_next_unused(result, SDLPUI_MFLG_NONE);
	sdlpui_create_submenu_button(c, "Name", SDLPUI_HOR_LEFT,
		handle_menu_font_names, SDLPUI_CHILD_MENU_RIGHT, tag, false);
	c = sdlpui_get_simple_menu_next_unused(result, SDLPUI_MFLG_NONE);
	sdlpui_create_submenu_button(c, "Size", SDLPUI_HOR_LEFT,
		handle_menu_font_sizes, SDLPUI_CHILD_MENU_RIGHT, tag, false);
	sdlpui_complete_simple_menu(result, window);
	result->rect.x = ul_x_win;
	result->rect.y = ul_y_win;

	return result;
}

static void handle_menu_borders(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window)
{
	int tag;
	struct subwindow *subwindow;

	SDL_assert(ctrl->ftb->get_tag);
	tag = (*ctrl->ftb->get_tag)(ctrl);
	SDL_assert(tag >= 0);
	subwindow = get_subwindow_by_index(window, (unsigned int)tag, false);
	SDL_assert(subwindow);
	subwindow->borders.visible = !subwindow->borders.visible;
	render_borders(subwindow);
}

static void handle_menu_subwindow_alpha(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window)
{
	int tag;
	struct subwindow *subwindow;
	struct sdlpui_simple_menu *sm;
	int new_alpha, i;

	SDL_assert(ctrl->ftb->get_tag);
	tag = (*ctrl->ftb->get_tag)(ctrl);
	SDL_assert(tag >= 0);
	subwindow = get_subwindow_by_index(window, (unsigned int)tag / 101,
		false);
	SDL_assert(subwindow);
	new_alpha = ALPHA_PERCENT((tag % 101));
	if (is_close_to(new_alpha, subwindow->color.a,
			DEFAULT_ALPHA_STEP / 2)) {
		/*
		 * Already selected; pushing the button doesn't toggle it off.
		 */
		struct sdlpui_menu_button *mb;

		SDL_assert(ctrl->type_code == SDLPUI_CTRL_MENU_BUTTON);
		mb = (struct sdlpui_menu_button*)ctrl->priv;
		SDL_assert(mb->subtype_code == SDLPUI_MB_TOGGLE);
		mb->v.toggled = true;
		dlg->dirty = true;
		return;
	}
	/* Toggle off the previous setting. */
	SDL_assert(dlg->type_code == SDLPUI_DIALOG_SIMPLE_MENU);
	sm = (struct sdlpui_simple_menu*)dlg->priv;
	for (i = 0; i < sm->number; ++i) {
		struct sdlpui_menu_button *mb;
		int alpha;

		SDL_assert(sm->controls[i].type_code
			== SDLPUI_CTRL_MENU_BUTTON);
		mb = (struct sdlpui_menu_button*)sm->controls[i].priv;
		alpha = ALPHA_PERCENT((mb->tag % 101));
		if (is_close_to(alpha, subwindow->color.a,
				DEFAULT_ALPHA_STEP / 2)) {
			mb->v.toggled = false;
			dlg->dirty = true;
			break;
		}
	}
	/* Change to the new setting. */
	subwindow->color.a = new_alpha;
	render_clear(subwindow->window, subwindow->texture, &subwindow->color);
	render_borders(subwindow);
	refresh_angband_terms(subwindow->app);
}

static struct sdlpui_dialog *handle_menu_alpha(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window,
		int ul_x_win, int ul_y_win)
{
	int tag, nstep, i;
	struct subwindow *subwindow;
	struct sdlpui_dialog *result;

	SDL_assert(ctrl->ftb->get_tag);
	tag = (*ctrl->ftb->get_tag)(ctrl);
	SDL_assert(tag >= 0);
	subwindow = get_subwindow_by_index(window, (unsigned int)tag, false);
	SDL_assert(subwindow);
	nstep = 1 + ((100 - DEFAULT_ALPHA_LOWEST + (DEFAULT_ALPHA_STEP - 1))
		/ DEFAULT_ALPHA_STEP);
	result = sdlpui_start_simple_menu(dlg, ctrl, nstep, true, false,
		NULL, NULL, 0);
	for (i = 0; i < nstep; ++i) {
		int alpha_pct = MIN(100,
			DEFAULT_ALPHA_LOWEST + i * DEFAULT_ALPHA_STEP);
		int alpha = ALPHA_PERCENT(alpha_pct);
		struct sdlpui_control *c = sdlpui_get_simple_menu_next_unused(
			result, SDLPUI_MFLG_NONE);

		sdlpui_create_menu_toggle(c, format(" %3d%% ", alpha_pct),
			SDLPUI_HOR_LEFT, handle_menu_subwindow_alpha,
			101 * tag + alpha_pct, false,
			is_close_to(alpha, subwindow->color.a,
			DEFAULT_ALPHA_STEP / 2));
	}
	sdlpui_complete_simple_menu(result, window);
	result->rect.x = ul_x_win;
	result->rect.y = ul_y_win;

	return result;
}

static void handle_menu_top(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window)
{
	int tag;
	struct subwindow *subwindow;

	SDL_assert(ctrl->ftb->get_tag);
	tag = (*ctrl->ftb->get_tag)(ctrl);
	SDL_assert(tag >= 0);
	subwindow = get_subwindow_by_index(window, (unsigned int)tag, false);
	SDL_assert(subwindow);
	subwindow->always_top = !subwindow->always_top;
	sort_to_top(subwindow->window);
}

static void handle_menu_term_pop(struct sdlpui_dialog *dlg,
		struct sdlpui_window *window, bool up)
{
	struct subwindow *subwindow = get_subwindow_by_index(window,
		(unsigned int)dlg->tag, false);

	/*
	 * When the menu is visible, signal that the subwindow should have a
	 * pronounced border so it is easy to see where the changes will be
	 * made.
	 */
	window->outlined_subwindow = (up) ? subwindow->index : (unsigned)-1;
}

static struct sdlpui_dialog *handle_menu_terms(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window,
		int ul_x_win, int ul_y_win)
{
	int tag;
	struct subwindow *subwindow;
	struct sdlpui_dialog *result;
	struct sdlpui_control *c;

	SDL_assert(ctrl->ftb->get_tag);
	tag = (*ctrl->ftb->get_tag)(ctrl);
	SDL_assert(tag >= 0);
	subwindow = get_subwindow_by_index(window, (unsigned int)tag, false);
	SDL_assert(subwindow);
	result = sdlpui_start_simple_menu(dlg, ctrl,
		(subwindow->index == MAIN_SUBWINDOW) ? 4 : 5, true, false,
		handle_menu_term_pop, NULL, tag);
	c = sdlpui_get_simple_menu_next_unused(result, SDLPUI_MFLG_NONE);
	sdlpui_create_submenu_button(c, "Font", SDLPUI_HOR_LEFT,
		handle_menu_font, SDLPUI_CHILD_MENU_RIGHT, tag, false);
	if (subwindow->index == MAIN_SUBWINDOW) {
		c = sdlpui_get_simple_menu_next_unused(result,
			SDLPUI_MFLG_NONE);
		sdlpui_create_submenu_button(c, "Tiles", SDLPUI_HOR_LEFT,
			handle_menu_tiles, SDLPUI_CHILD_MENU_RIGHT, 0, false);
	} else {
		c = sdlpui_get_simple_menu_next_unused(result,
			SDLPUI_MFLG_NONE);
		sdlpui_create_submenu_button(c, "Purpose", SDLPUI_HOR_LEFT,
			handle_menu_purpose, SDLPUI_CHILD_MENU_RIGHT, tag,
			false);
		c = sdlpui_get_simple_menu_next_unused(result,
			SDLPUI_MFLG_NONE);
		sdlpui_create_submenu_button(c, "Alpha", SDLPUI_HOR_LEFT,
			handle_menu_alpha, SDLPUI_CHILD_MENU_RIGHT, tag, false);
	}
	c = sdlpui_get_simple_menu_next_unused(result, SDLPUI_MFLG_NONE);
	sdlpui_create_menu_toggle(c, "Borders", SDLPUI_HOR_LEFT,
		handle_menu_borders, tag, false, subwindow->borders.visible);
	c = sdlpui_get_simple_menu_next_unused(result, SDLPUI_MFLG_NONE);
	sdlpui_create_menu_toggle(c, "Top", SDLPUI_HOR_LEFT,
		handle_menu_top, tag, false, subwindow->always_top);
	sdlpui_complete_simple_menu(result, window);
	result->rect.x = ul_x_win;
	result->rect.y = ul_y_win;

	return result;
}

static struct sdlpui_dialog *handle_menu_button(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *parent, struct sdlpui_window *window,
		int ul_x_win, int ul_y_win)
{
	struct sdlpui_dialog *result = sdlpui_start_simple_menu(
		parent, ctrl, 3 + (int)N_ELEMENTS(angband_term_name)
		+ ((window->index == MAIN_WINDOW) ? 2 : 0), true, false,
		NULL, NULL, 0);
	unsigned int i;
	struct sdlpui_control *c;

	for (i = 0; i < (unsigned int)N_ELEMENTS(angband_term_name); ++i) {
		struct subwindow *subwindow =
			get_subwindow_by_index(window, i, true);

		if (!subwindow) {
			continue;
		}
		c = sdlpui_get_simple_menu_next_unused(result,
			SDLPUI_MFLG_NONE);
		sdlpui_create_submenu_button(c, angband_term_name[i],
			SDLPUI_HOR_LEFT, handle_menu_terms,
			SDLPUI_CHILD_MENU_RIGHT, (int)i, false);
	}
	c = sdlpui_get_simple_menu_next_unused(result, SDLPUI_MFLG_NONE);
	sdlpui_create_menu_toggle(c, "Fullscreen", SDLPUI_HOR_LEFT,
		handle_menu_fullscreen, 0, false,
		window->flags & SDL_WINDOW_FULLSCREEN_DESKTOP);
	if (window->index == MAIN_WINDOW) {
		c = sdlpui_get_simple_menu_next_unused(result,
			SDLPUI_MFLG_NONE);
		sdlpui_create_menu_toggle(c, "Send Keypad Modifier",
			SDLPUI_HOR_LEFT, handle_menu_kp_mod, 0, false,
			window->app->kp_as_mod);
		c = sdlpui_get_simple_menu_next_unused(result,
			SDLPUI_MFLG_NONE);
		sdlpui_create_menu_button(c, "Menu Shortcuts...",
			SDLPUI_HOR_LEFT, handle_menu_shortcuts, 0, false);
		c = sdlpui_get_simple_menu_next_unused(result,
			SDLPUI_MFLG_NONE);
		sdlpui_create_submenu_button(c, "Windows", SDLPUI_HOR_LEFT,
			handle_menu_windows, SDLPUI_CHILD_MENU_RIGHT, 0, false);
	}
	c = sdlpui_get_simple_menu_next_unused(result, SDLPUI_MFLG_NONE);
	sdlpui_create_menu_button(c, "About...", SDLPUI_HOR_LEFT,
		handle_menu_about, 0, false);
	c = sdlpui_get_simple_menu_next_unused(result, SDLPUI_MFLG_NONE);
	sdlpui_create_menu_button(c, "SDL Details...", SDLPUI_HOR_LEFT,
		handle_menu_sdl_details, 0, false);
	c = sdlpui_get_simple_menu_next_unused(result, SDLPUI_MFLG_NONE);
	sdlpui_create_menu_button(c, "Quit", SDLPUI_HOR_LEFT,
		handle_menu_quit, 0, false);
	sdlpui_complete_simple_menu(result, window);
	result->rect.x = ul_x_win;
	result->rect.y = ul_y_win;

	return result;
}

static bool is_close_to(int a, int b, unsigned range)
{
	if (a > 0 && b > 0) {
		return (unsigned) ABS(a - b) < range;
	} else if (a < 0 && b < 0) {
		return (unsigned) ABS(ABS(a) - ABS(b)) < range;
	} else {
		return (unsigned) (ABS(a) + ABS(b)) < range;
	}
}

static bool is_point_in_rect(int x, int y, const SDL_Rect *rect)
{
	return x >= rect->x && x < rect->x + rect->w
		&& y >= rect->y && y < rect->y + rect->h;
}

static bool is_rect_in_rect(const SDL_Rect *small, const SDL_Rect *big)
{
	return small->x >= big->x
		&& small->x + small->w <= big->x + big->w
		&& small->y >= big->y
		&& small->y + small->h <= big->y + big->h;
}

static void fit_rect_in_rect_by_hw(SDL_Rect *small, const SDL_Rect *big)
{
	if (small->x < big->x) {
		small->w -= big->x - small->x;
		small->x = big->x;
	}
	if (small->x + small->w > big->x + big->w) {
		small->w = big->x + big->w - small->x;
	}
	if (small->y < big->y) {
		small->h -= big->y - small->y;
		small->y = big->y;
	}
	if (small->y + small->h > big->y + big->h) {
		small->h = big->y + big->h - small->y;
	}
}

static void fit_rect_in_rect_by_xy(SDL_Rect *small, const SDL_Rect *big)
{
	if (small->x < big->x) {
		small->x = big->x;
	}
	if (small->y < big->y) {
		small->y = big->y;
	}
	if (small->x + small->w > big->x + big->w) {
		small->x = MAX(big->x, big->x + big->w - small->w);
	}
	if (small->y + small->h > big->y + big->h) {
		small->y = MAX(big->y, big->y + big->h - small->h);
	}
}

static void fit_rect_in_rect_proportional(SDL_Rect *small, const SDL_Rect *big)
{
	if (small->x < big->x) {
		small->x = big->x;
	}
	if (small->y < big->y) {
		small->y = big->y;
	}
	if (small->w > big->w) {
		small->h = small->h * big->w / small->w;
		small->w = big->w;
	}
	if (small->h > big->h) {
		small->w = small->w * big->h / small->h;
		small->h = big->h;
	}
}

/**
 * Like fit_rect_in_rect_by_xy() but allow changing the size as well as position
 * of small.  If not possible to fit one dimension of small in big while leaving
 * the size at least the minimum size specified, leave the size in that
 * dimension as it is.
 */
static void coerce_rect_in_rect(SDL_Rect *small, const SDL_Rect *big,
		int min_w, int min_h)
{
	if (small->x < big->x) {
		small->x = big->x;
	}
	if (small->y < big->y) {
		small->y = big->y;
	}
	if (small->x + small->w > big->x + big->w) {
		if (small->w <= big->w) {
			small->x = big->x + big->w - small->w;
		} else {
			small->x = big->x;
			if (min_w <= big->w) {
				small->w = big->w;
			}
		}
	}
	if (small->y + small->h > big->y + big->h) {
		if (small->h <= big->h) {
			small->y = big->y + big->h - small->h;
		} else {
			small->y = big->y;
			if (min_h <= big->h) {
				small->h = big->h;
			}
		}
	}
}

static void resize_rect(SDL_Rect *rect,
		int left, int top, int right, int bottom)
{
	if (rect->w - left + right <= 0
			|| rect->h - top + bottom <= 0)
	{
		return;
	}
	rect->x += left;
	rect->w -= left;

	rect->y += top;
	rect->h -= top;

	rect->w += right;
	rect->h += bottom;
}

static void crop_rects(SDL_Rect *src, SDL_Rect *dst)
{
	if (src->w == dst->w) {
		/* most common case - do nothing */
	} else if (src->w > dst->w) {
		/* second most common case - font glyph is too large */
		src->x += (src->w - dst->w) / 2;
		src->w = dst->w;
	} else {
		/* uncommon case - font glyph is too small */ 
		dst->x += (dst->w - src->w) / 2;
		dst->w = src->w;
	}

	if (src->h == dst->h) {
		;
	} else if (src->h > dst->h) {
		src->y += (src->h - dst->h) / 2;
		src->h = dst->h;
	} else  {
		dst->y += (dst->h - src->h) / 2;
		dst->h = src->h;
	}
}

/** tries to snap to other term in such a way so that their
 * (visible) borders overlap */
static void try_snap(struct sdlpui_window *window,
		struct subwindow *subwindow, SDL_Rect *rect)
{
	for (size_t i = N_ELEMENTS(window->subwindows); i > 0; i--) {
		struct subwindow *other = window->subwindows[i - 1];
		if (other == NULL
				|| !other->visible
				|| other->index == subwindow->index)
		{
			continue;
		}

		int ox = other->full_rect.x;
		int oy = other->full_rect.y;
		int ow = other->full_rect.w;
		int oh = other->full_rect.h;

		if (oy < rect->y + rect->h && rect->y < oy + oh) {
			if (is_close_to(rect->x, ox + ow, DEFAULT_SNAP_RANGE(window))) {
				rect->x = ox + ow - DEFAULT_VISIBLE_BORDER;
			}
			if (is_close_to(rect->x + rect->w, ox, DEFAULT_SNAP_RANGE(window))) {
				rect->x = ox - rect->w + DEFAULT_VISIBLE_BORDER;
			}
		}
		if (ox < rect->x + rect->w && rect->x < ox + ow) {
			if (is_close_to(rect->y, oy + oh, DEFAULT_SNAP_RANGE(window))) {
				rect->y = oy + oh - DEFAULT_VISIBLE_BORDER;
			}
			if (is_close_to(rect->y + rect->h, oy, DEFAULT_SNAP_RANGE(window))) {
				rect->y = oy - rect->h + DEFAULT_VISIBLE_BORDER;
			}
		}
	}
}

static void start_moving(struct sdlpui_window *window,
		struct subwindow *subwindow, const SDL_MouseButtonEvent *mouse)
{
	assert(!window->size_state.active);

	bring_to_top(window, subwindow);

	window->move_state.originx = mouse->x;
	window->move_state.originy = mouse->y;

	window->move_state.subwindow = subwindow;
	window->move_state.moving = true;
}

static void start_sizing(struct sdlpui_window *window,
		struct subwindow *subwindow, const SDL_MouseButtonEvent *mouse)
{
	assert(!window->move_state.active);

	bring_to_top(window, subwindow);

	subwindow->sizing_rect = subwindow->full_rect;

	int x = mouse->x - (subwindow->full_rect.x + subwindow->full_rect.w / 2);
	int y = mouse->y - (subwindow->full_rect.y + subwindow->full_rect.h / 2);

	window->size_state.left = x < 0 ? true : false;
	window->size_state.top  = y < 0 ? true : false;

	window->size_state.originx = mouse->x;
	window->size_state.originy = mouse->y;

	window->size_state.subwindow = subwindow;
	window->size_state.sizing = true;
}

static void handle_window_closed(struct my_app *a, struct sdlpui_window *window)
{
	assert(window != NULL);

	if (window->index == MAIN_WINDOW) {
		handle_quit();
	} else {
		for (size_t i = 0; i < N_ELEMENTS(window->subwindows); i++) {
			struct subwindow *subwindow = window->subwindows[i];
			if (subwindow != NULL) {
				clear_pw_flag(subwindow);
			}
		}
		free_window(window);
		if (a->w_mouse == window) {
			a->w_mouse = NULL;
		}
		if (a->w_key == window) {
			a->w_key = NULL;
		}
	}
}

static void handle_window_focus(struct my_app *a, const SDL_WindowEvent *event)
{
	struct sdlpui_window *new_w;

	switch (event->event) {
		case SDL_WINDOWEVENT_ENTER:
			new_w = get_window_by_id(a, event->windowID);
			SDLPUI_EVENT_TRACER("window", new_w,
				"(not extracted)", "mouse entered");
			if (a->w_mouse && a->w_mouse != new_w
					&& a->w_mouse->d_mouse) {
				if (a->w_mouse->d_mouse->ftb->handle_window_loses_mouse) {
					(*a->w_mouse->d_mouse->ftb->handle_window_loses_mouse)(
						a->w_mouse->d_mouse,
						a->w_mouse);
				}
				a->w_mouse->d_mouse = NULL;
			}
			a->w_mouse = new_w;
			break;
		case SDL_WINDOWEVENT_LEAVE:
			SDLPUI_EVENT_TRACER("window", a->w_mouse,
				"(not extracted)", "mouse left");
			if (a->w_mouse && a->w_mouse->d_mouse) {
				if (a->w_mouse->d_mouse->ftb->handle_window_loses_mouse) {
					(*a->w_mouse->d_mouse->ftb->handle_window_loses_mouse)(
						a->w_mouse->d_mouse,
						a->w_mouse);
				}
				a->w_mouse->d_mouse = NULL;
			}
			a->w_mouse = NULL;
			break;
		case SDL_WINDOWEVENT_FOCUS_GAINED:
			new_w = get_window_by_id(a, event->windowID);
			SDLPUI_EVENT_TRACER("window", new_w,
				"(not extracted)", "gained key focus");
			if (a->w_key && a->w_key != new_w
					&& a->w_key->d_key) {
				if (a->w_key->d_key->ftb->handle_window_loses_key) {
					(*a->w_key->d_key->ftb->handle_window_loses_key)(
						a->w_key->d_key, a->w_key);
				}
				a->w_key->d_key = NULL;
			}
			a->w_key = new_w;
			break;
		case SDL_WINDOWEVENT_FOCUS_LOST:
			SDLPUI_EVENT_TRACER("window", a->w_key,
				"(not extracted)", "lost key focus");
			if (a->w_key && a->w_key->d_key) {
				if (a->w_key->d_key->ftb->handle_window_loses_key) {
					(*a->w_key->d_key->ftb->handle_window_loses_key)(
						a->w_key->d_key, a->w_key);
				}
				a->w_key->d_key = NULL;
			}
			a->w_key = NULL;
			break;
		default:
			assert(0);
	}
}

static void handle_last_resize_event(struct my_app *a, int num_events,
		const SDL_Event *events)
{
	assert(num_events > 0);

	for (int i = num_events - 1; i >= 0; i--) {
		if (events[i].window.event == SDL_WINDOWEVENT_RESIZED) {
			const SDL_WindowEvent event = events[i].window;

			struct sdlpui_window *window =
				get_window_by_id(a, event.windowID);
			assert(window != NULL);
			resize_window(window, event.data1, event.data2);

			return;
		}
	}
}

static void handle_windowevent(struct my_app *a, const SDL_WindowEvent *event)
{
	SDL_Event events[128];
	events[0].window = *event;

	int num_events = 1 + SDL_PeepEvents(events + 1, (int) N_ELEMENTS(events) - 1,
			SDL_GETEVENT, SDL_WINDOWEVENT, SDL_WINDOWEVENT);

	bool resize = false;

	for (int i = 0; i < num_events; i++) {
		struct sdlpui_window *window;

		window = get_window_by_id(a, events[i].window.windowID);
		if (!window) {
			continue;
		}
		if (window->move_state.active) {
			signal_move_state(window);
		} else if (window->size_state.active) {
			signal_size_state(window);
		}
		switch (events[i].window.event) {
			case SDL_WINDOWEVENT_RESIZED:
				/* just for efficiency */
				resize = true;
				break;
			case SDL_WINDOWEVENT_CLOSE:
				handle_window_closed(a, window);
				break;
			case SDL_WINDOWEVENT_ENTER: /* fallthrough */
			case SDL_WINDOWEVENT_LEAVE:
			case SDL_WINDOWEVENT_FOCUS_GAINED:
			case SDL_WINDOWEVENT_FOCUS_LOST:
				handle_window_focus(a, &events[i].window);
				break;
		}
	}

	if (resize) {
		handle_last_resize_event(a, num_events, events);
	}

	redraw_all_windows(a, false);
}

static void resize_subwindow(struct subwindow *subwindow)
{
	SDL_DestroyTexture(subwindow->texture);

	subwindow->full_rect = subwindow->sizing_rect;
	if (!adjust_subwindow_geometry(subwindow->window, subwindow)) {
		quit_fmt("bad_geometry of subwindow %u in window %u",
				subwindow->index, subwindow->window->index);
	}

	subwindow->texture = make_subwindow_texture(subwindow->window,
			subwindow->full_rect.w, subwindow->full_rect.h);

	render_clear(subwindow->window, subwindow->texture, &subwindow->color);
	render_borders(subwindow);

	term *old = Term;
	Term_activate(subwindow->term);
	Term_resize(subwindow->cols, subwindow->rows);
	/* XXX if we don't redraw the term, resizing in birth screen is buggy */
	Term_redraw();
	Term_activate(old);

	refresh_angband_terms(subwindow->app);
}

static void do_sizing(struct sdlpui_window *window, int x, int y)
{
	struct size_state *size_state = &window->size_state;
	int newx = x - size_state->originx;
	int newy = y - size_state->originy;
	int left = size_state->left ? newx : 0;
	int top = size_state->top  ? newy : 0;
	int right = size_state->left ? 0 : newx;
	int bottom = size_state->top  ? 0 : newy;
	SDL_Rect rect;

	assert(size_state->subwindow != NULL);
	rect = size_state->subwindow->sizing_rect;

	resize_rect(&rect, left, top, right, bottom);
	fit_rect_in_rect_by_hw(&rect, &window->inner_rect);

	if (is_ok_col_row(size_state->subwindow,
				&rect,
				size_state->subwindow->font_width,
				size_state->subwindow->font_height)
			&& !SDL_RectEquals(&rect, &size_state->subwindow->sizing_rect)) {
		size_state->subwindow->sizing_rect = rect;
		window->dirty = true;
	}

	size_state->originx = x;
	size_state->originy = y;
}

static void do_moving(struct sdlpui_window *window, int x, int y)
{
	struct move_state *move_state = &window->move_state;
	SDL_Rect old_rect = move_state->subwindow->full_rect;
	SDL_Rect *rect;

	assert(move_state->subwindow != NULL);
	rect = &move_state->subwindow->full_rect;

	rect->x += x - move_state->originx;
	rect->y += y - move_state->originy;

	try_snap(window, move_state->subwindow, rect);
	fit_rect_in_rect_by_xy(rect, &window->inner_rect);
	if (!SDL_RectEquals(&old_rect, rect)) {
		window->dirty = true;
	}

	move_state->originx = x;
	move_state->originy = y;
}

static bool handle_mousemotion(struct my_app *a,
		const SDL_MouseMotionEvent *mouse)
{
	struct sdlpui_dialog *d;
	SDL_Event pendm[4];

	if (!a->w_mouse) {
		return false;
	}

	/*
	 * If more than one consecutive motion event is queued, only process
	 * the last one.
	 */
	while (1) {
		int npend = SDL_PeepEvents(pendm,
			(int)(sizeof(pendm) / sizeof(pendm[0])),
			SDL_GETEVENT, SDL_MOUSEMOTION, SDL_MOUSEMOTION);

		if (npend <= 0) {
			if (npend < 0) {
				SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
					"SDL_PeepEvents() for pending motion "
					"events failed: %s", SDL_GetError());
			}
			break;
		}
		SDL_assert(npend <= (int)(sizeof(pendm) / sizeof(pendm[0]))
			&& pendm[npend - 1].type == SDL_MOUSEMOTION);
		mouse = &pendm[npend - 1].motion;
	}

	if (a->w_mouse->move_state.moving) {
		do_moving(a->w_mouse, mouse->x, mouse->y);
		return true;
	}
	if (a->w_mouse->size_state.sizing) {
		do_sizing(a->w_mouse, mouse->x, mouse->y);
		return true;
	}
	/* Have a menu or dialog handle the event if appropriate. */
	if (a->w_mouse->d_mouse
			&& a->w_mouse->d_mouse->ftb->handle_mousemove
			&& (*a->w_mouse->d_mouse->ftb->handle_mousemove)(
				a->w_mouse->d_mouse, a->w_mouse, mouse)) {
		return true;
	}

	/*
	 * Ignore motion events while a mouse button is pressed (at
	 * least up to the point that the mouse leaves the window).
	 */
	if (mouse->state != 0) {
		return true;
	}

	/*
	 * Has the motion entered any of the active dialogs/menus?
	 * Check from front to back.
	 */
	d = a->w_mouse->d_head;
	while (1) {
		struct sdlpui_dialog *tgt = d;

		if (!tgt) {
			break;
		}
		d = tgt->next;
		if (sdlpui_is_in_dialog(tgt, mouse->x, mouse->y)
				&& tgt->ftb->find_control_containing) {
			int comp_ind;
			struct sdlpui_control *c =
				(*tgt->ftb->find_control_containing)(
				tgt, a->w_mouse, mouse->x, mouse->y,
				&comp_ind);

			if (a->w_mouse->d_mouse != tgt) {
				struct sdlpui_dialog *old_d =
					a->w_mouse->d_mouse;

				if (old_d && old_d->ftb->handle_loses_mouse) {
					(*old_d->ftb->handle_loses_mouse)(
						old_d, a->w_mouse, c, tgt);
				}
				a->w_mouse->d_mouse = tgt;
			}
			/* Have key focus follow mouse. */
			if (a->w_key != a->w_mouse) {
				if (a->w_key && a->w_key->d_key
						&& a->w_key->d_key->ftb->handle_loses_key) {
					(*a->w_key->d_key->ftb->handle_loses_key)(
						a->w_key->d_key, a->w_key, c,
						tgt);
				}
				if (a->w_key) {
					a->w_key->d_key = NULL;
				}
				SDL_assert(!a->w_key
					|| !a->w_key->d_mouse);
				a->w_key = a->w_mouse;
			} else if (a->w_key->d_key && a->w_key->d_key != tgt) {
				if (a->w_key->d_key->ftb->handle_loses_key) {
					(*a->w_key->d_key->ftb->handle_loses_key)(
						a->w_key->d_key, a->w_key,
						c, tgt);
				}
			}
			a->w_mouse->d_key = tgt;
			if (a->w_mouse->d_mouse
					&& a->w_mouse->d_mouse->ftb->handle_mousemove
					&& (*a->w_mouse->d_mouse->ftb->handle_mousemove)(
					a->w_mouse->d_mouse, a->w_mouse, mouse)) {
				return true;
			}
			break;
		}
	}

	return false;
}

/** x and y are relative to window */
static bool get_colrow_from_xy(const struct subwindow *subwindow,
		int x, int y, int *col, int *row)
{
	SDL_Rect rect = {
		subwindow->full_rect.x + subwindow->inner_rect.x,
		subwindow->full_rect.y + subwindow->inner_rect.y,
		subwindow->inner_rect.w,
		subwindow->inner_rect.h
	};

	if (!is_point_in_rect(x, y, &rect)) {
		return false;
	}

	*col = (x - rect.x) / subwindow->font_width;
	*row = (y - rect.y) / subwindow->font_height;

	return true;
}

static uint8_t translate_key_mods(Uint16 mods)
{
#define TRANSLATE_K_MOD(m, k) ((m) & mods ? (k) : 0)
	uint8_t angband_mods =
		TRANSLATE_K_MOD(KMOD_SHIFT, KC_MOD_SHIFT)
		| TRANSLATE_K_MOD(KMOD_CTRL, KC_MOD_CONTROL)
		| TRANSLATE_K_MOD(KMOD_ALT, KC_MOD_ALT)
		| TRANSLATE_K_MOD(KMOD_GUI, KC_MOD_META);
#undef TRANSLATE_K_MOD
	return angband_mods;
}

static bool handle_mousebutton(struct my_app *a,
		const SDL_MouseButtonEvent *mouse)
{
	struct subwindow *subwindow;
	int button, col, row;
	uint8_t mods;
	term *old;
	bool touched;

	if (!a->w_mouse) {
		return false;
	}

	/* Terminate moving/sizing on a mouse release. */
	if (mouse->state == SDL_RELEASED
			&& (a->w_mouse->move_state.moving
			|| a->w_mouse->size_state.sizing)) {
		if (a->w_mouse->move_state.moving) {
			SDL_assert(a->w_mouse->move_state.active);
			a->w_mouse->move_state.moving = false;
		} else {
			SDL_assert(a->w_mouse->size_state.active);
			a->w_mouse->size_state.sizing = false;
			if (a->w_mouse->size_state.subwindow) {
				resize_subwindow(a->w_mouse->size_state.subwindow);
			}
		}
		return true;
	}

	/* Have a menu or dialog handle the event if appropriate. */
	touched = false;
	if (a->w_mouse->d_mouse) {
		/*
		 * Press events outside of the dialog will act as if the dialog 
		 * lost mouse focus to another unknown dialog.  Do not do the
		 * same for release events in case the press happens in a
		 * dialog, followed by mouse motion, and then the release
		 * happens outside the dialog.
		 */
		if (mouse->state == SDL_PRESSED && !sdlpui_is_in_dialog(
				a->w_mouse->d_mouse, mouse->x, mouse->y)) {
			if (a->w_mouse->d_mouse->ftb->handle_loses_mouse) {
				(*a->w_mouse->d_mouse->ftb->handle_loses_mouse)(
					a->w_mouse->d_mouse, a->w_mouse,
					NULL, NULL);
			}
			touched = true;
		} else if (a->w_mouse->d_mouse->ftb->handle_mouseclick
				&& (*a->w_mouse->d_mouse->ftb->handle_mouseclick)(
				a->w_mouse->d_mouse, a->w_mouse, mouse)) {
			return true;
		}
	}

	/* If requested, start moving/sizing on a press. */
	if (mouse->state != SDL_RELEASED
			&& (a->w_mouse->move_state.active
			|| a->w_mouse->size_state.active)) {
		subwindow = get_subwindow_by_xy(a->w_mouse, mouse->x, mouse->y);
		if (subwindow && is_rect_in_rect(&subwindow->full_rect,
				&a->w_mouse->inner_rect)) {
			if (a->w_mouse->move_state.active
					&& !a->w_mouse->move_state.moving) {
				start_moving(a->w_mouse, subwindow, mouse);
			} else if (a->w_mouse->size_state.active
					&& !a->w_mouse->size_state.sizing) {
				start_sizing(a->w_mouse, subwindow, mouse);
			}
			return true;
		}
	}

	/* Otherwise only react to the button press and not the release. */
	if (mouse->state == SDL_RELEASED) {
		return touched;
	}

	subwindow = get_subwindow_by_xy(a->w_mouse, mouse->x, mouse->y);
	if (subwindow == NULL) {
		/* not an error, the user clicked in some random place */
		return touched;
	}
	if (!subwindow->top) {
		bring_to_top(a->w_mouse, subwindow);
		redraw_window(a->w_mouse);
		return true;
	}

	/*
	 * Terms that are not main do not react to events, and main term
	 * lives in the main window.
	 */
	if (a->w_mouse->index != MAIN_WINDOW) {
		return touched;
	}

	/* all magic numbers are from ui-term.c and ui-context.c :) */
	switch (mouse->button) {
		case SDL_BUTTON_LEFT:
			button = 1;
			break;
		case SDL_BUTTON_RIGHT:
			button = 2;
			break;
		default:
			/* XXX other buttons? */
			return touched;
	}

	if (!get_colrow_from_xy(subwindow, mouse->x, mouse->y, &col, &row)) {
		return touched;
	}

	mods = translate_key_mods(SDL_GetModState());
	/* apparently mouse buttons dont get this */
	mods &= ~KC_MOD_META;

	button |= mods << 4;

	old = Term;
	Term_activate(subwindow->term);
	Term_mousepress(col, row, button);
	Term_activate(old);

	return true;
}

static bool handle_mousewheel(struct my_app *a,
		const SDL_MouseWheelEvent *wheel)
{
	/* Have a menu or dialog handle the event if appropriate. */
	if (a->w_mouse && a->w_mouse->d_mouse
			&& a->w_mouse->d_mouse->ftb->handle_mousewheel
			&& (*a->w_mouse->d_mouse->ftb->handle_mousewheel)(
				a->w_mouse->d_mouse, a->w_mouse, wheel)) {
		return true;
	}

	/* Otherwise, nothing is done. */
	return false;
}

static void handle_controller_added(struct my_app *a,
		const SDL_ControllerDeviceEvent *event)
{
	/* Support only one controller */
	if (a->controller) return;
	a->controller = SDL_GameControllerOpen(event->which);
}

static void handle_controller_removed(struct my_app *a,
		const SDL_ControllerDeviceEvent *event)
{
	if (!a->controller) return;

	SDL_Joystick *joystick = SDL_GameControllerGetJoystick(a->controller);
	if (event->which == SDL_JoystickInstanceID(joystick)) {
		SDL_GameControllerClose(a->controller);
		a->controller = NULL;
	}
}

static bool handle_controller_button(struct my_app *a,
		 const SDL_ControllerButtonEvent *event)
{
	keycode_t ch;
	uint8_t mods = 0;

	if (event->state == SDL_RELEASED) {
		return false;
	}

	/* Translate controller buttons into keycodes */
	switch (event->button) {
	case SDL_CONTROLLER_BUTTON_A:
		ch = KC_ENTER;
		break;
	case SDL_CONTROLLER_BUTTON_B:
		ch = ESCAPE;
		break;
	case SDL_CONTROLLER_BUTTON_BACK:
		ch = KC_BACKSPACE;
		break;
	case SDL_CONTROLLER_BUTTON_START:
		ch = '=';
		break;
	case SDL_CONTROLLER_BUTTON_GUIDE:
		ch = '?';
		break;
	case SDL_CONTROLLER_BUTTON_DPAD_UP:
		ch = ARROW_UP;
		break;
	case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
		ch = ARROW_DOWN;
		break;
	case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
		ch = ARROW_LEFT;
		break;
	case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
		ch = ARROW_RIGHT;
		break;
	default:
		return false;
	}

	Term_keypress(ch, mods);
	return true;
}

static bool handle_controller_axis(struct my_app *a,
		 const SDL_ControllerAxisEvent *event)
{
	keycode_t ch;
	uint8_t mods = 0;

	/* Translate controller axes into keycodes */
	if (event->axis == 0) {
		/* left-right */
		if (event->value > 1000) ch = ARROW_RIGHT;
		else if (event->value < -1000) ch = ARROW_LEFT;
		else return false;
	} else if (event->axis == 1) {
		/* up-down */
		if (event->value > 1000) ch = ARROW_UP;
		else if (event->value < -1000) ch = ARROW_DOWN;
		else return false;
	} else {
		return false;
	}

	Term_keypress(ch, mods);
	return true;
}

static bool trigger_menu_shortcut(struct my_app *a, keycode_t ch, uint8_t mods)
{
	/*
	 * Check if it matches a menu shortcut for an active window.  If so,
	 * give that window's menu bar key focus and return true.  Otherwise,
	 * return false.
	 */
	int i = 0;

	while (1) {
		if (i >= MAX_WINDOWS) {
			return false;
		}
		if (a->windows[i].loaded
				&& a->menu_shortcuts[i].type == EVT_KBRD
				&& a->menu_shortcuts[i].code == ch
				&& a->menu_shortcuts[i].mods == mods) {
			if (a->w_key != a->windows + i) {
				struct sdlpui_window *old_w = a->w_key;
				struct sdlpui_dialog *old_d = a->w_key->d_key;

				SDL_assert(!a->windows[i].d_key);
				SDL_assert(a->windows[i].status_bar->ftb->goto_first_control);
				(a->windows[i].status_bar->ftb->goto_first_control)(
					a->windows[i].status_bar, a->windows + i
				);
				if (old_w && old_d
						&& old_d->ftb->handle_loses_key) {
					(*old_d->ftb->handle_loses_key)(
						old_d, old_w,
						a->windows[i].status_bar->c_key,
						a->windows[i].status_bar);
				}
				if (old_w) {
					old_w->d_key = NULL;
				}
			} else if (a->w_key->d_key
					!= a->windows[i].status_bar) {
				struct sdlpui_dialog *old_d = a->w_key->d_key;

				SDL_assert(a->windows[i].status_bar->ftb->goto_first_control);
				(a->windows[i].status_bar->ftb->goto_first_control)(
					a->windows[i].status_bar, a->windows + i
				);
				if (old_d && old_d->ftb->handle_loses_key) {
					(*old_d->ftb->handle_loses_key)(
						old_d, a->windows + i,
						a->windows[i].status_bar->c_key,
						a->windows[i].status_bar);
				}
			}
			return true;
		}
		++i;
	}
}

/**
 * This function handles keys that don't produce text, and, if kp_as_mod
 * the keypad and keypresses that will produce the same characters as keypad
 * keypresses.  Others should be handled in textinput_event_to_angband_key.
 */
static void keyboard_event_to_angband_key(const SDL_KeyboardEvent *key,
		bool kp_as_mod, keycode_t *ch, uint8_t *mods)
{
	*mods = translate_key_mods(key->keysym.mod);
	*ch = 0;
	switch (key->keysym.sym) {
		/* arrow keys */
		case SDLK_UP:          *ch = ARROW_UP; break;
		case SDLK_DOWN:        *ch = ARROW_DOWN; break;
		case SDLK_LEFT:        *ch = ARROW_LEFT; break;
		case SDLK_RIGHT:       *ch = ARROW_RIGHT; break;
		/* text editing keys */
		case SDLK_BACKSPACE:   *ch = KC_BACKSPACE; break;
		case SDLK_PAGEDOWN:    *ch = KC_PGDOWN; break;
		case SDLK_PAGEUP:      *ch = KC_PGUP; break;
		case SDLK_INSERT:      *ch = KC_INSERT; break;
		case SDLK_DELETE:      *ch = KC_DELETE; break;
		case SDLK_RETURN:      *ch = KC_ENTER; break;
		case SDLK_ESCAPE:      *ch = ESCAPE; break;
		case SDLK_HOME:        *ch = KC_HOME; break;
		case SDLK_END:         *ch = KC_END; break;
		case SDLK_TAB:         *ch = KC_TAB; break;
		/* function keys */
		case SDLK_F1:          *ch = KC_F1; break;
		case SDLK_F2:          *ch = KC_F2; break;
		case SDLK_F3:          *ch = KC_F3; break;
		case SDLK_F4:          *ch = KC_F4; break;
		case SDLK_F5:          *ch = KC_F5; break;
		case SDLK_F6:          *ch = KC_F6; break;
		case SDLK_F7:          *ch = KC_F7; break;
		case SDLK_F8:          *ch = KC_F8; break;
		case SDLK_F9:          *ch = KC_F9; break;
		case SDLK_F10:         *ch = KC_F10; break;
		case SDLK_F11:         *ch = KC_F11; break;
		case SDLK_F12:         *ch = KC_F12; break;
		case SDLK_F13:         *ch = KC_F13; break;
		case SDLK_F14:         *ch = KC_F14; break;
		case SDLK_F15:         *ch = KC_F15; break;
	}

	if (kp_as_mod) {
		/* If numlock is set and shift is not pressed, numpad numbers
		 * produce regular numbers and not keypad numbers */
		uint8_t keypad_num_mod = ((key->keysym.mod & KMOD_NUM)
			&& !(key->keysym.mod & KMOD_SHIFT)) ?
			0x00 : KC_MOD_KEYPAD;

		switch (key->keysym.sym) {
			/* Keypad */
			case SDLK_KP_0:
				*ch = '0'; *mods |= keypad_num_mod; break;
			case SDLK_KP_1:
				*ch = '1'; *mods |= keypad_num_mod; break;
			case SDLK_KP_2:
				*ch = '2'; *mods |= keypad_num_mod; break;
			case SDLK_KP_3:
				*ch = '3'; *mods |= keypad_num_mod; break;
			case SDLK_KP_4:
				*ch = '4'; *mods |= keypad_num_mod; break;
			case SDLK_KP_5:
				*ch = '5'; *mods |= keypad_num_mod; break;
			case SDLK_KP_6:
				*ch = '6'; *mods |= keypad_num_mod; break;
			case SDLK_KP_7:
				*ch = '7'; *mods |= keypad_num_mod; break;
			case SDLK_KP_8:
				*ch = '8'; *mods |= keypad_num_mod; break;
			case SDLK_KP_9:
				*ch = '9'; *mods |= keypad_num_mod; break;

			case SDLK_KP_MULTIPLY:
				*ch = '*'; *mods |= KC_MOD_KEYPAD; break;
			case SDLK_KP_PERIOD:
				*ch = '.'; *mods |= KC_MOD_KEYPAD; break;
			case SDLK_KP_DIVIDE:
				*ch = '/'; *mods |= KC_MOD_KEYPAD; break;
			case SDLK_KP_EQUALS:
				*ch = '='; *mods |= KC_MOD_KEYPAD; break;
			case SDLK_KP_MINUS:
				*ch = '-'; *mods |= KC_MOD_KEYPAD; break;
			case SDLK_KP_PLUS:
				*ch = '+'; *mods |= KC_MOD_KEYPAD; break;
			case SDLK_KP_ENTER:
				*ch = KC_ENTER; *mods |= KC_MOD_KEYPAD; break;

			/*
			 * Keys that produce the same character as keypad keys
			 */
			case SDLK_ASTERISK: *ch = '*'; break;
			case SDLK_PLUS: *ch = '+'; break;
		}
		if (*mods & KC_MOD_SHIFT) {
			bool match = true;
			switch(key->keysym.sym) {
				/*
				 * Does not match every keyboard layout,
				 * unfortunately.
				 */
				case SDLK_8: *ch = '*'; break;
				case SDLK_EQUALS: *ch = '+'; break;
				default: match = false;
			}
			if (match) {
				*mods &= ~KC_MOD_SHIFT;
			}
		} else {
			switch(key->keysym.sym) {
				case SDLK_0: *ch = '0'; break;
				case SDLK_1: *ch = '1'; break;
				case SDLK_2: *ch = '2'; break;
				case SDLK_3: *ch = '3'; break;
				case SDLK_4: *ch = '4'; break;
				case SDLK_5: *ch = '5'; break;
				case SDLK_6: *ch = '6'; break;
				case SDLK_7: *ch = '7'; break;
				case SDLK_8: *ch = '8'; break;
				case SDLK_9: *ch = '9'; break;
				case SDLK_SLASH: *ch = '/'; break;
				case SDLK_EQUALS: *ch = '='; break;
				case SDLK_PERIOD: *ch = '.'; break;
				case SDLK_MINUS: *ch = '-'; break;
			}
		}
	} else if (key->keysym.sym == SDLK_KP_ENTER) {
		*ch = KC_ENTER;
	}

	/* encode control */
	if (*mods & KC_MOD_CONTROL) {
		bool match = true;
		switch (key->keysym.sym) {
			case SDLK_LEFTBRACKET:  *ch = KTRL('['); break;
			case SDLK_RIGHTBRACKET: *ch = KTRL(']'); break;
			case SDLK_BACKSLASH:    *ch = KTRL('\\'); break;
			case SDLK_a:            *ch = KTRL('A'); break;
			case SDLK_b:            *ch = KTRL('B'); break;
			case SDLK_c:            *ch = KTRL('C'); break;
			case SDLK_d:            *ch = KTRL('D'); break;
			case SDLK_e:            *ch = KTRL('E'); break;
			case SDLK_f:            *ch = KTRL('F'); break;
			case SDLK_g:            *ch = KTRL('G'); break;
			case SDLK_h:            *ch = KTRL('H'); break;
			case SDLK_i:            *ch = KTRL('I'); break;
			case SDLK_j:            *ch = KTRL('J'); break;
			case SDLK_k:            *ch = KTRL('K'); break;
			case SDLK_l:            *ch = KTRL('L'); break;
			case SDLK_m:            *ch = KTRL('M'); break;
			case SDLK_n:            *ch = KTRL('N'); break;
			case SDLK_o:            *ch = KTRL('O'); break;
			case SDLK_p:            *ch = KTRL('P'); break;
			case SDLK_q:            *ch = KTRL('Q'); break;
			case SDLK_r:            *ch = KTRL('R'); break;
			case SDLK_s:            *ch = KTRL('S'); break;
			case SDLK_t:            *ch = KTRL('T'); break;
			case SDLK_u:            *ch = KTRL('U'); break;
			case SDLK_v:            *ch = KTRL('V'); break;
			case SDLK_w:            *ch = KTRL('W'); break;
			case SDLK_x:            *ch = KTRL('X'); break;
			case SDLK_y:            *ch = KTRL('Y'); break;
			case SDLK_z:            *ch = KTRL('Z'); break;
			default: match = false;
		}
		if (match) {
			*mods &= ~KC_MOD_CONTROL;
		}
	}
}

static bool handle_key(struct my_app *a, const SDL_KeyboardEvent *key)
{
	uint8_t mods;
	keycode_t ch;

	/* Have a menu or dialog handle the event if appropriate. */
	if (a->w_key && a->w_key->d_key && a->w_key->d_key->ftb->handle_key
			&& (*a->w_key->d_key->ftb->handle_key)(a->w_key->d_key,
				a->w_key, key)) {
		return true;
	}

	/*
	 * For passing events to the game's core, react when the button is
	 * pressed and not when it is released.
	 */
	if (key->state == SDL_RELEASED) {
		return false;
	}

	/*
	 * SDL will give us both keydown and text input events in many cases.
	 * Between this function and handle_text_input we need to make sure that
	 * Term_keypress gets called exactly once for a given key press from the
	 * user.
	 */
	keyboard_event_to_angband_key(key, a->kp_as_mod, &ch, &mods);
	if (ch) {
		if (!trigger_menu_shortcut(a, ch, mods)) {
			Term_keypress(ch, mods);
		}
		return true;
	}
	return false;
}

static void textinput_event_to_angband_key(const SDL_TextInputEvent *input,
		bool kp_as_mod, keycode_t *ch, uint8_t *mods)
{
	*ch = sdlpui_utf8_to_codepoint(input->text);

	/*
	 * Do not handle any characters that can be produced by the keypad if
	 * they were handled in keyboard_event_to_angband_key.
	 */
	if (kp_as_mod) {
		switch (*ch) {
			case '0':
			case '1':
			case '2':
			case '3':
			case '4':
			case '5':
			case '6':
			case '7':
			case '8':
			case '9':
			case '/':
			case '*':
			case '-':
			case '+':
			case '.':
			case '=':
				*ch = '\0';
		}
	}

	*mods = translate_key_mods(SDL_GetModState());
	/* Shift is already encoded in characters we receive here */
	if (!MODS_INCLUDE_SHIFT(*ch)) {
		*mods &= ~KC_MOD_SHIFT;
	}
}

static bool handle_text_input(struct my_app *a, const SDL_TextInputEvent *input)
{
	keycode_t ch;
	uint8_t mods;

	/* Have a menu or dialog handle the event if appropriate. */
	if (a->w_key && a->w_key->d_key && a->w_key->d_key->ftb->handle_textin
			&& (*a->w_key->d_key->ftb->handle_textin)(
				a->w_key->d_key, a->w_key, input)) {
		return true;
	}

	textinput_event_to_angband_key(input, a->kp_as_mod, &ch, &mods);

	if (!ch) {
		return false;
	}
	if (!trigger_menu_shortcut(a, ch, mods)) {
		Term_keypress(ch, mods);
	}
	return true;
}

static bool handle_text_editing(struct my_app *a,
		const SDL_TextEditingEvent *edit)
{
	/* Have a menu or dialog handle the event if appropriate. */
	if (a->w_key && a->w_key->d_key && a->w_key->d_key->ftb->handle_textedit
			&& (*a->w_key->d_key->ftb->handle_textedit)(
				a->w_key->d_key, a->w_key, edit)) {
		return true;
	}

	/* Not passed on to the game's core. */
	return false;
}

static void wait_anykey(struct my_app *a)
{
	SDL_Event event;

	SDL_EventType expected = SDL_USEREVENT;
	while (true) {
		SDL_WaitEvent(&event);
		if (event.type == expected) {
			return;
		}

		switch (event.type) {
			case SDL_KEYDOWN:
				expected = SDL_KEYUP;
				break;
			case SDL_MOUSEBUTTONDOWN:
				expected = SDL_MOUSEBUTTONUP;
				break;
			case SDL_MOUSEMOTION:
				SDL_FlushEvent(SDL_MOUSEMOTION);
				break;
			case SDL_QUIT:
				handle_quit();
				break;
			case SDL_RENDER_TARGETS_RESET:
				recreate_textures(a, false);
				break;
			case SDL_RENDER_DEVICE_RESET:
				recreate_textures(a, true);
				break;
			case SDL_WINDOWEVENT:
				handle_windowevent(a, &event.window);
				return;
		}
	}
}

static void handle_quit(void)
{
	/* XXX copied from main-sdl.c */
	if (character_generated && inkey_flag) {
		/* no idea what that does :) */
		msg_flag = false;
		save_game();
	}

	quit(NULL);
}

static bool get_event(struct my_app *a)
{
	SDL_Event event;
	if (!SDL_PollEvent(&event)) {
		return false;
	}

	switch (event.type) {
		case SDL_KEYDOWN:
		case SDL_KEYUP:
			return handle_key(a, &event.key);
		case SDL_TEXTINPUT:
			return handle_text_input(a, &event.text);
		case SDL_TEXTEDITING:
			return handle_text_editing(a, &event.edit);
		case SDL_MOUSEMOTION:
			return handle_mousemotion(a, &event.motion);
		case SDL_MOUSEBUTTONDOWN:
		case SDL_MOUSEBUTTONUP:
			return handle_mousebutton(a, &event.button);
		case SDL_MOUSEWHEEL:
			return handle_mousewheel(a, &event.wheel);
		case SDL_CONTROLLERDEVICEADDED:
			handle_controller_added(a, &event.cdevice);
			return false;
		case SDL_CONTROLLERDEVICEREMOVED:
			handle_controller_removed(a, &event.cdevice);
			return false;
		case SDL_CONTROLLERBUTTONDOWN:
		case SDL_CONTROLLERBUTTONUP:
			return handle_controller_button(a, &event.cbutton);
		case SDL_CONTROLLERAXISMOTION:
			return handle_controller_axis(a, &event.caxis);
		case SDL_WINDOWEVENT:
			handle_windowevent(a, &event.window);
			return false;
		case SDL_RENDER_TARGETS_RESET:
			recreate_textures(a, false);
			return false;
		case SDL_RENDER_DEVICE_RESET:
			recreate_textures(a, true);
			return false;
		case SDL_QUIT:
			handle_quit();
			return false;
		default:
			return false;
	}
}

static void refresh_angband_terms(struct my_app *a)
{
	if (!character_dungeon) {
		return;
	}

	term *old = Term;
	Term_activate(term_screen);

	/* XXX XXX this is basically do_cmd_redraw(), just without EVENT_FLUSH_INPUT */
	{
		/* XXX XXX this works for refreshing monster's attrs */
		event_signal_point(EVENT_MAP, -1, -1);

		Term_flush();
		verify_panel();

		player->upkeep->notice |= (PN_COMBINE);
		player->upkeep->update |= (PU_TORCH | PU_INVEN);
		player->upkeep->update |= (PU_BONUS | PU_HP | PU_SPELLS);
		player->upkeep->update |= (PU_UPDATE_VIEW | PU_MONSTERS);
		player->upkeep->redraw |= (PR_BASIC | PR_EXTRA | PR_MAP | PR_INVEN |
								   PR_EQUIP | PR_MESSAGE | PR_MONSTER |
								   PR_OBJECT | PR_MONLIST | PR_ITEMLIST);

		Term_clear();
		handle_stuff(player);
		move_cursor_relative(player->grid.x, player->grid.y);

		Term_redraw_all();
	}

	Term_activate(old);

	redraw_all_windows(a, false);
}

static errr term_xtra_event(int v)
{
	struct subwindow *subwindow = Term->data;
	assert(subwindow != NULL);

	redraw_all_windows(subwindow->app, true);

	if (v) {
		while (true) {
			for (int i = 0; i < DEFAULT_IDLE_UPDATE_PERIOD; i++) {
				if (get_event(subwindow->app)) {
					return 0;
				}
				SDL_Delay(subwindow->window->delay);
			}
			idle_update();
		}
	} else {
		(void) get_event(subwindow->app);
	}

	return 0;
}

static errr term_xtra_flush(void)
{
	SDL_Event event;

	while (SDL_PollEvent(&event)) {
		switch (event.type) {
			case SDL_WINDOWEVENT:
				handle_windowevent(&g_app, &event.window);
				break;
		}
	}

	return 0;
}

static errr term_xtra_clear(void)
{
	struct subwindow *subwindow = Term->data;
	assert(subwindow != NULL);

	render_fill_rect(subwindow->window,
			subwindow->texture, &subwindow->inner_rect, &subwindow->color);

	subwindow->window->dirty = true;

	return 0;
}

static errr term_xtra_fresh(void)
{
	struct subwindow *subwindow = Term->data;
	assert(subwindow != NULL);

	if (!subwindow->window->d_mouse && !subwindow->window->d_key) {
		try_redraw_window(subwindow->window);
	}

	return 0;
}

static errr term_xtra_delay(int v)
{
	if (v > 0) {
		term_xtra_event(0);
		SDL_Delay(v);
	}

	return 0;
}

static errr term_xtra_react(void)
{
	init_colors(&g_app);

	return 0;
}

static errr term_xtra_hook(int n, int v)
{
	switch (n) {
		case TERM_XTRA_EVENT:
			return term_xtra_event(v);
		case TERM_XTRA_DELAY:
			return term_xtra_delay(v);
		case TERM_XTRA_FLUSH:
			return term_xtra_flush();
		case TERM_XTRA_CLEAR:
			return term_xtra_clear();
		case TERM_XTRA_FRESH:
			return term_xtra_fresh();
		case TERM_XTRA_REACT:
			return term_xtra_react();
		default:
			return 0;
	}
}

static errr term_curs_hook(int col, int row)
{
	struct subwindow *subwindow = Term->data;
	assert(subwindow != NULL);

	render_cursor(subwindow, col, row, false);

	subwindow->window->dirty = true;

	return 0;
}

static errr term_bigcurs_hook(int col, int row)
{
	struct subwindow *subwindow = Term->data;
	assert(subwindow != NULL);

	render_cursor(subwindow, col, row, true);

	subwindow->window->dirty = true;

	return 0;
}

static errr term_wipe_hook(int col, int row, int n)
{
	struct subwindow *subwindow = Term->data;
	assert(subwindow != NULL);

	SDL_Rect rect = {
		subwindow->inner_rect.x + col * subwindow->font_width,
		subwindow->inner_rect.y + row * subwindow->font_height,
		n * subwindow->font_width,
		subwindow->font_height
	};

	render_fill_rect(subwindow->window, subwindow->texture, &rect, &subwindow->color);

	subwindow->window->dirty = true;

	return 0;
}

static errr term_text_hook(int col, int row, int n, int a, const wchar_t *s)
{
	struct subwindow *subwindow = Term->data;
	assert(subwindow != NULL);

	SDL_Color fg = subwindow->app->colors[a % MAX_COLORS];
	SDL_Color bg;

	switch (a / MULT_BG) {
		case BG_BLACK:
			bg = subwindow->color;
			break;
		case BG_SAME:
			bg = fg;
			break;
		case BG_DARK:
			bg = subwindow->app->colors[DEFAULT_SHADE_COLOR];
			break;
		default:
			/* debugging */
			bg = subwindow->app->colors[DEFAULT_ERROR_COLOR];
			break;
	}

	bg.a = subwindow->color.a;

	SDL_Rect rect = {
		subwindow->inner_rect.x + col * subwindow->font_width,
		subwindow->inner_rect.y + row * subwindow->font_height,
		n * subwindow->font_width,
		subwindow->font_height
	};

	render_fill_rect(subwindow->window, subwindow->texture, &rect, &bg);

	rect.w = subwindow->font_width;
	for (int i = 0; i < n; i++) {
		render_glyph_mono(subwindow->window,
				subwindow->font, subwindow->texture,
				rect.x, rect.y, &fg, (uint32_t) s[i]);
		rect.x += subwindow->font_width;
	}

	subwindow->window->dirty = true;

	return 0;
}

static errr term_pict_hook(int col, int row, int n,
		const int *ap, const wchar_t *cp, const int *tap, const wchar_t *tcp)
{
	int dhrclip;
	struct subwindow *subwindow = Term->data;
	assert(subwindow != NULL);

	if (!current_graphics_mode || current_graphics_mode->grafID == GRAPHICS_NONE) {
		/*
		 * Do nothing unsuccessfully if asked to draw a tile while
		 * they're not enabled.  Could proceed in this function
		 * with no apparent ill effects, but that just wastes time.
		 */
		return -1;
	}
	assert(subwindow->window->graphics.texture != NULL);

	if (subwindow->window->graphics.overdraw_row) {
		dhrclip = Term_get_first_tile_row(Term) + tile_height - 1;
	} else {
		/*
		 * There's no double-height tiles so the value does not
		 * matter.
		 */
		dhrclip = 0;
	}

	for (int i = 0; i < n; i++) {
		render_tile_font_scaled(subwindow, col + i, row, tap[i], tcp[i], true, dhrclip);

		if (tap[i] == ap[i] && tcp[i] == cp[i]) {
			continue;
		}

		render_tile_font_scaled(subwindow, col + i, row, ap[i], cp[i], false, dhrclip);
	}

	subwindow->window->dirty = true;

	return 0;
}

static void term_view_map_shared(struct subwindow *subwindow,
		SDL_Texture *map, int w, int h)
{
	render_all(subwindow->window);

	SDL_Rect dst = {
		0, 0,
		w + 2 * DEFAULT_VISIBLE_BORDER,
		h + 2 * DEFAULT_VISIBLE_BORDER
	};
	SDL_Rect full = {
		0, 0,
		subwindow->window->full_rect.w,
		subwindow->window->full_rect.h
	};
	fit_rect_in_rect_proportional(&dst, &full);

	dst.x = (subwindow->window->full_rect.w - dst.w) / 2;
	dst.y = (subwindow->window->full_rect.h - dst.h) / 2;
	resize_rect(&dst,
			DEFAULT_VISIBLE_BORDER, DEFAULT_VISIBLE_BORDER,
			-DEFAULT_VISIBLE_BORDER, -DEFAULT_VISIBLE_BORDER);

	render_all(subwindow->window);
	SDL_SetRenderTarget(subwindow->window->renderer, NULL);
	SDL_RenderCopy(subwindow->window->renderer, map, NULL, &dst);

	/* render borders around the whole map */
	resize_rect(&dst,
			-DEFAULT_VISIBLE_BORDER, -DEFAULT_VISIBLE_BORDER,
			DEFAULT_VISIBLE_BORDER, DEFAULT_VISIBLE_BORDER);
	render_outline_rect_width(subwindow->window,
			NULL, &dst, &subwindow->borders.color, DEFAULT_VISIBLE_BORDER);

	SDL_RenderPresent(subwindow->window->renderer);

	wait_anykey(subwindow->app);
}

static void term_view_map_tile(struct subwindow *subwindow)
{
	assert(subwindow->window->graphics.id != GRAPHICS_NONE);

	SDL_Rect tile = {
		0, 0, REASONABLE_MAP_TILE_WIDTH, REASONABLE_MAP_TILE_HEIGHT
	};
	SDL_Rect source = {
		0, 0,
		subwindow->window->graphics.tile_pixel_w,
		subwindow->window->graphics.tile_pixel_h
	};

	fit_rect_in_rect_proportional(&tile, &source);

	int w = tile.w * cave->width;
	int h = tile.h * cave->height;

	SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");

	SDL_Texture *map = make_subwindow_texture(subwindow->window, w, h);
	assert(map != NULL);

	render_clear(subwindow->window, map, &subwindow->color);

	for (int y = 0; y < cave->height; y++) {
		tile.y = y * tile.w;
		for (int x = 0; x < cave->width; x++) {
			tile.x = x * tile.h;
			render_grid_cell_tile(subwindow, map, tile, x, y);
		}
	}

	SDL_Rect cursor = {player->grid.x * tile.w, player->grid.y * tile.h, tile.w,
					   tile.h};

	/* render cursor around player */
	render_outline_rect_width(subwindow->window,
			map, &cursor, &subwindow->app->colors[DEFAULT_SUBWINDOW_CURSOR_COLOR],
			/* XXX some arbitrary values that look ok at the moment */
			MIN(MIN(tile.w / 4, tile.h / 4),
				DEFAULT_VISIBLE_BORDER));

	term_view_map_shared(subwindow, map, w, h);

	SDL_DestroyTexture(map);

	SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0");
}

static void term_view_map_text(struct subwindow *subwindow)
{
	int w = subwindow->font_width * cave->width;
	int h = subwindow->font_height * cave->height;

	SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");

	SDL_Texture *map = make_subwindow_texture(subwindow->window, w, h);
	assert(map != NULL);

	render_clear(subwindow->window, map, &subwindow->color);

	for (int y = 0; y < cave->height; y++) {
		for (int x = 0; x < cave->width; x++) {
			render_grid_cell_text(subwindow, map, x, y);
		}
	}

	SDL_Rect cursor = {
		player->grid.x * subwindow->font_width,
		player->grid.y * subwindow->font_height,
		subwindow->font_width,
		subwindow->font_height
	};

	/* render cursor around player */
	render_outline_rect_width(subwindow->window,
			map, &cursor, &subwindow->app->colors[DEFAULT_SUBWINDOW_CURSOR_COLOR],
			/* XXX some arbitrary values that look reasonable at the moment */
			MIN(MIN(subwindow->font_width / 4,
					subwindow->font_height / 4),
				DEFAULT_VISIBLE_BORDER));

	term_view_map_shared(subwindow, map, w, h);

	SDL_DestroyTexture(map);

	SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0");
}

static void term_view_map_hook(term *terminal)
{
	struct subwindow *subwindow = terminal->data;
	if (subwindow->window->graphics.id == GRAPHICS_NONE) {
		term_view_map_text(subwindow);
	} else {
		term_view_map_tile(subwindow);
	}
}

#ifdef MSYS2_ENCODING_WORKAROUND
/*
 * Override the default character encoding in MSYS2 for better handling in
 * term_text_hook() of characters that don't fall in the ASCII range.  See
 * init_sdl2() for more details about the default character encoding in MSYS2.
 */

/**
 * Convert UTF-8 to UTF-16 with each UTF-16 in the native byte order and
 * lossily change any code point that requires a surrogate pair to U+FFFD.
 * Return the total number of code points that would be generated by
 * converting the * UTF-8 input.
 *
 * \param dest Points to the buffer in which to store the conversion.  May be
 * NULL.
 * \param src Is a null-terminated UTF-8 sequence.
 * \param n Is the maximum number of code points to store in dest.
 *
 * In case of malformed UTF-8, inserts a U+FFFD in the converted output at the
 * point of the error.
 */
static size_t term_mbcs_sdl2_msys2(wchar_t *dest, const char *src, int n)
{
	size_t nout = (n > 0) ? n : 0;
	size_t count = 0;

	while (1) {
		/*
		 * Default to U+FFFD to indicate an erroneous UTF-8 sequence
		 * that could not be decoded.  Follow "best practice"
		 * recommended by the Unicode 6 standard:  an erroneous
		 * sequence ends as soon as a disallowed byte is encountered.
		 */
		uint32_t decoded = 0xfffd;

		if (((unsigned int) *src & 0x80) == 0) {
			/*
			 * Encoded as a single byte:  U+0000 to U+007F ->
			 * 0xxxxxxx.
			 */
			if (*src == 0) {
				if (dest && count < nout) {
					dest[count] = 0;
				}
				break;
			}
			decoded = *src;
			++src;
		} else if (((unsigned int) *src & 0xe0) == 0xc0) {
			/*
			 * Encoded as two bytes:  U+0080 to U+07FF ->
			 * 110xxxxx 10xxxxxx.
			 */
			uint32_t part = ((uint32_t) *src & 0x1f) << 6;

			++src;
			/*
			 * Check that the first two bits of the continuation
			 * byte are valid and the encoding is not overlong.
			 */
			if (((unsigned int) *src & 0xc0) == 0x80
					&& part > 0x40) {
				decoded = part + ((uint32_t) *src & 0x3f);
				++src;
			}
		} else if (((unsigned int) *src & 0xf0) == 0xe0) {
			/*
			 * Encoded as three bytes:  U+0800 to U+FFFF ->
			 * 1110xxxx 10xxxxxx 10xxxxxx.
			 */
			uint32_t part = ((uint32_t) *src & 0xf) << 12;

			++src;
			if (((unsigned int) *src & 0xc0) == 0x80) {
				part += ((uint32_t) *src & 0x3f) << 6;
				++src;
				/*
				 * The second part of the test rejects
				 * overlong encodings.  The third part
				 * rejects encodings of U+D800 to U+DFFF,
				 * reserved for surrogate pairs.
				 */
				if (((unsigned int) *src & 0xc0) == 0x80
						&& part >= 0x800
						&& (part & 0xf800) != 0xd800) {
					decoded = part
						+ ((uint32_t) *src & 0x3f);
					++src;
				}
			}
		} else if (((unsigned int) *src & 0xf8) == 0xf0) {
			/*
			 * Encoded as four bytes:  U+10000 to U+1FFFFF ->
			 * 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx.
			 */
			uint32_t part = ((uint32_t) *src & 0x7) << 18;

			++src;
			if (((unsigned int) *src & 0xc0) == 0x80) {
				part += ((uint32_t) * src & 0x3f) << 12;
				++src;
				/*
				 * The second part of the test rejects
				 * overlong encodings.  The third part
				 * rejects code points beyond U+10FFFF which
				 * can't be encoded in UTF-16.
				 */
				if (((unsigned int) *src & 0xc0) == 0x80
						&& part >= 0x10000
						&& (part & 0xff0000)
						<= 0x100000) {
					part += ((uint32_t) *src & 0x3f) << 6;
					++src;
					if (((unsigned int) *src & 0xc0)
							== 0x80) {
						decoded = part
							+ ((uint32_t) *src
							& 0x3f);
						++src;
					}
				}
			}
		} else {
			/*
			 * Either an impossible byte or one that signals the
			 * start of a five byte or longer encoding.
			 */
			++src;
		}
		if (dest && count < nout) {
			if (decoded >= 0x10000) {
				/*
				 * Would require a surrogate pair to represent
				 * accurately.  Substitute U+FFFD instead.
				 */
				assert(decoded <= 0x10FFFF);
				dest[count] = (wchar_t) 0xfffd;
			} else {
				/*
				 * By the decoding logic, the decoded value
				 * should not match part of surrogate pair.
				 */
				assert(decoded < 0xd800 || decoded > 0xdfff);
				dest[count] = (wchar_t) decoded;
			}
		}
		++count;
	}
	return count;
}

/**
 * Convert a UTF-16 stored in the native byte order to UTF-8.
 * \param s Points to the buffer where the conversion should be stored.
 * That buffer must have at least term_wcsz_sdl2_msys2() bytes.
 * \param wchar Is the UTF-16 value to convert.
 * \return The returned value is the number of bytes written to s or -1
 * if the UTF-16 value could not be converted.
 *
 * This is a necessary counterpart to term_mbcs_sdl2_msys2():  since we
 * are overriding the default multibyte to wide character conversion, need
 * to override the reverse conversion as well.
 */
static int term_wctomb_sdl2_msys2(char *s, wchar_t wchar)
{
	if ((unsigned int) wchar <= 0x7f) {
		*s = wchar;
		return 1;
	}
	if ((unsigned int) wchar < 0x7ff) {
		*s++ = 0xc0 + (((unsigned int) wchar & 0x7c0) >> 6);
		*s++ = 0x80 + ((unsigned int) wchar & 0x3f);
		return 2;
	}
	/* Refuse to encode a value reserved for surrogate pairs in UTF-16. */
	if ((unsigned int) wchar >= 0xd800 && (unsigned int) wchar <= 0xdfff) {
		return -1;
	}
	*s++ = 0xe0 + (((unsigned int) wchar & 0xf000) >> 12);
	*s++ = 0x80 + (((unsigned int) wchar & 0xfc0) >> 6);
	*s++ = 0x80 + ((unsigned int) wchar & 0x3f);
	return 3;
}

/**
 * Return whether a UTF-16 value is printable.
 *
 * This is a necessary counterpart to term_mbcs_sdl2_msys2() so that
 * screening of wide characters in the core's text_out_to_screen() is
 * consistent with what term_mbcs_sdl2_msys2() does.
 */
static int term_iswprint_sdl2_msys2(wint_t wc)
{
	/*
	 * Upcast the UTF-16 value to UTF-32 since the UTF-16 value is either
	 * equal to the code point's value or is part of a surrogate pair.
	 */
	return utf32_isprint((uint32_t) wc);
}

/**
 * Return the maximum number of bytes needed for a multibyte encoding of a
 * wchar.
 */
static int term_wcsz_sdl2_msys2(void)
{
	/*
	 * UTF-8 takes at most 3 bytes to encode a code point that can be
	 * encoded with a single 16-bit quantity in UTF-16.
	 */
	return 3;
}

#endif /* MSYS2_ENCODING_WORKAROUND */

static SDL_Texture *load_image(const struct sdlpui_window *window,
		const char *path)
{
	SDL_Surface *surface = IMG_Load(path);
	if (surface == NULL) {
		quit_fmt("cannot load image '%s': %s", path, IMG_GetError());
	}
	SDL_Texture *texture = SDL_CreateTextureFromSurface(window->renderer, surface);
	if (texture == NULL) {
		quit_fmt("cannot create texture from image '%s': %s", path, SDL_GetError());
	}
	SDL_FreeSurface(surface);

	return texture;
}

static void load_wallpaper(struct sdlpui_window *window, const char *path)
{
	if (window->wallpaper.mode == WALLPAPER_DONT_SHOW) {
		return;
	}
	if (window->wallpaper.mode == WALLPAPER_INVALID) {
		quit_fmt("invalid wallpaper mode in window %u", window->index);
	}

	SDL_Texture *wallpaper = load_image(window, path);
	assert(wallpaper != NULL);

	if (window->wallpaper.mode == WALLPAPER_TILED) {
		int w;
		int h;
		SDL_QueryTexture(wallpaper, NULL, NULL, &w, &h);

		SDL_Rect dst = {0, 0, w, h};

		/* make wallpaper 1/4 to 1/2 of the window's width and height
		 * that would be 4 to 16 calls to RenderCopy in wallpaper rendering function */
		while (w < window->inner_rect.w / 4) {
			w *= 2;
		}
		while (h < window->inner_rect.h / 4) {
			h *= 2;
		}
		window->wallpaper.texture = make_subwindow_texture(window, w, h);
		window->wallpaper.w = w;
		window->wallpaper.h = h;

		SDL_Color color = {0};
		render_clear(window, window->wallpaper.texture, &color);

		for (dst.y = 0; dst.y < h; dst.y += dst.h) {
			for (dst.x = 0; dst.x < w; dst.x += dst.w) {
				SDL_RenderCopy(window->renderer, wallpaper, NULL, &dst);
			}
		}
		SDL_DestroyTexture(wallpaper);
	} else {
		SDL_QueryTexture(wallpaper, NULL, NULL,
				&window->wallpaper.w, &window->wallpaper.h);
		window->wallpaper.texture = wallpaper;
	}
}

static void load_default_wallpaper(struct sdlpui_window *window)
{
	if (window->wallpaper.mode == WALLPAPER_DONT_SHOW) {
		return;
	}

	char path[4096];
	path_build(path, sizeof(path), DEFAULT_WALLPAPER_DIR, DEFAULT_WALLPAPER);

	load_wallpaper(window, path);
}

static void load_default_window_icon(const struct sdlpui_window *window)
{
	char path[4096];
	path_build(path, sizeof(path), DEFAULT_WINDOW_ICON_DIR, DEFAULT_WINDOW_ICON);

	SDL_Surface *surface = IMG_Load(path);
	assert(surface != NULL);

	SDL_SetWindowIcon(window->window, surface);

	SDL_FreeSurface(surface);
}

static void load_graphics(struct sdlpui_window *window, graphics_mode *mode)
{
	assert(window->graphics.texture == NULL);

	current_graphics_mode = mode;
	use_graphics = mode->grafID;

	if (use_graphics == GRAPHICS_NONE) {
		tile_width = 1;
		tile_height = 1;
	} else {
		size_t i;
		char path[4096];
		path_build(path, sizeof(path), mode->path, mode->file);
		if (!file_exists(path)) {
			quit_fmt("cannot load graphics: file '%s' doesn't exist", path);
		}

		window->graphics.texture = load_image(window, path);
		assert(window->graphics.texture != NULL);

		window->graphics.tile_pixel_w = mode->cell_width;
		window->graphics.tile_pixel_h = mode->cell_height;

		window->graphics.overdraw_row = mode->overdrawRow;
		window->graphics.overdraw_max = mode->overdrawMax;

		for (i = 0; i < N_ELEMENTS(window->subwindows); i++) {
			if (window->subwindows[i] &&
					window->subwindows[i]->term) {
				window->subwindows[i]->term->dblh_hook =
					(window->graphics.overdraw_row) ?
					is_dh_tile : NULL;
			}
		}
	}

	if (character_dungeon) {
		reset_visuals(true);
	}

	window->graphics.id = mode->grafID;
}

static void reload_all_graphics(struct my_app *a, graphics_mode *mode)
{
	if (mode == NULL) {
		return;
	}

	for (size_t i = 0; i < MAX_WINDOWS; i++) {
		struct sdlpui_window *window = get_window_direct(a, i);
		if (window == NULL) {
			continue;
		}
		
		free_graphics(&window->graphics);
		memset(&window->graphics, 0, sizeof(window->graphics));
		window->graphics.texture = NULL;

		load_graphics(window, mode);
	}
}

static const struct font_info *find_font_info(const struct font_info *fonts,
		int nfonts, const char *name)
{
	int i;

	for (i = 0; i < nfonts; i++) {
		if (streq(fonts[i].name, name)) {
			return &fonts[i];
		}
	}

	return NULL;
}

static void make_font_cache(const struct sdlpui_window *window,
		struct font *font)
{
	const int glyph_w = font->ttf.glyph.w;
	const int glyph_h = font->ttf.glyph.h;
	/*
	 * Limit the horizontal size of the texture for the cached font to
	 * avoid bumping into limits in the renderer.
	 */
	const size_t ncol = 16;

	font->cache.texture = make_subwindow_texture(window,
		(int)ncol * glyph_w,
		(int)((ASCII_CACHE_SIZE + (ncol - 1)) / ncol) * glyph_h);
	assert(font->cache.texture != NULL);
		
	/* fill texture with white transparent pixels */
	SDL_Color white = {0xFF, 0xFF, 0xFF, 0};
	render_clear(window, font->cache.texture, &white);
	/* restore the alpha; we will render glyphs in white */
	white.a = 0xFF;

	for (size_t i = 0, icol = 0, irow = 0; i < ASCII_CACHE_SIZE; i++) {
		SDL_Surface *surface = TTF_RenderGlyph_Blended(font->ttf.handle,
				(Uint16) g_ascii_codepoints_for_cache[i], white);
		if (surface == NULL) {
			quit_fmt("font cache rendering failed for '%c'"
				" (ASCII %lu) in font '%s': %s",
				g_ascii_codepoints_for_cache[i],
				(unsigned long)g_ascii_codepoints_for_cache[i],
				font->name, TTF_GetError());
		}

		SDL_Texture *texture = SDL_CreateTextureFromSurface(window->renderer, surface);
		if (texture == NULL) {
			quit_fmt("cannot create texture for cache in font '%s': %s",
					font->name, SDL_GetError());
		}

		SDL_Rect src = {0, 0, surface->w, surface->h};
		SDL_Rect dst = {glyph_w * icol, glyph_h * irow, glyph_w, glyph_h};

		crop_rects(&src, &dst);

		SDL_RenderCopy(window->renderer, texture, &src, &dst);

		font->cache.rects[i] = dst;

		SDL_FreeSurface(surface);
		SDL_DestroyTexture(texture);

		if (icol < ncol - 1) {
			++icol;
		} else {
			icol = 0;
			++irow;
		}
	}
}

static struct font *make_font(const struct sdlpui_window *window,
		const char *name, int size)
{
	const struct font_info *info = find_font_info(window->app->fonts,
		window->app->font_count, name);
	if (info == NULL) {
		return NULL;
	}

	struct font *font = mem_zalloc(sizeof(*font));

	font->index = info->index;
	font->path = string_make(info->path);
	font->name = string_make(info->name);
	font->size = size;

	font->cache.texture = NULL;

	load_font(font);
	make_font_cache(window, font);

	return font;
}

static bool reload_font(struct subwindow *subwindow,
		const struct font_info *info)
{
	struct font *new_font =
		make_font(subwindow->window, info->name, info->size);
	int min_w, min_h;

	if (new_font == NULL) {
		return false;
	}

	if (!is_usable_font_for_subwindow(new_font, subwindow,
			&subwindow->sizing_rect)) {
		free_font(new_font);
		return false;
	}

	get_minimum_subwindow_size(subwindow->index == MAIN_SUBWINDOW,
		new_font->ttf.glyph.w, new_font->ttf.glyph.h, &min_w, &min_h);
	coerce_rect_in_rect(&subwindow->sizing_rect,
		&subwindow->window->inner_rect, min_w, min_h);

	free_font(subwindow->font);
	subwindow->font = new_font;

	resize_subwindow(subwindow);

	return true;
}

static void load_font(struct font *font)
{
	assert(font != NULL);
	assert(font->path != NULL);

	font->ttf.handle = TTF_OpenFont(font->path, font->size);
	if (font->ttf.handle == NULL) {
		quit_fmt("cannot open font '%s': %s", font->path, TTF_GetError());
	}

	font->ttf.glyph.h = TTF_FontHeight(font->ttf.handle);

	if (TTF_GlyphMetrics(font->ttf.handle, GLYPH_FOR_ADVANCE,
				NULL, NULL, NULL, NULL, &font->ttf.glyph.w) != 0)
	{
		quit_fmt("cannot query glyph metrics for font '%s': %s",
				font->path, TTF_GetError());
	}

	TTF_SetFontHinting(font->ttf.handle, DEFAULT_FONT_HINTING);
}

static void free_font(struct font *font)
{
	if (font->name != NULL) {
		mem_free(font->name);
	}
	if (font->path != NULL) {
		mem_free(font->path);
	}
	if (font->ttf.handle != NULL) {
		TTF_CloseFont(font->ttf.handle);
	}
	if (font->cache.texture != NULL) {
		SDL_DestroyTexture(font->cache.texture);
	}

	mem_free(font);
}

static void recreate_textures(struct my_app *a, bool all)
{
	int i;

	reload_all_graphics(a, current_graphics_mode);
	for (i = 0; i < MAX_WINDOWS; ++i) {
		struct sdlpui_window *w = &a->windows[i];
		struct sdlpui_dialog *d;
		int j;

		if (!w->loaded) {
			continue;
		}

		/*
		 * Recreate the dynamic texture used to cache the dialog font.
		 */
		if (w->dialog_font->cache.texture) {
			SDL_DestroyTexture(w->dialog_font->cache.texture);
			w->dialog_font->cache.texture = NULL;
			make_font_cache(w, w->dialog_font);
		}

		/* Recreate the static texture used for the stipple pattern. */
		if (all && w->stipple.texture) {
			SDL_DestroyTexture(w->stipple.texture);
			w->stipple = sdlpui_compute_stipple(w->renderer);
		}

		/*
		 * Recreate the texture used for the wallpaper.  That texture
		 * is dynamic if the wallpapering mode is WALLPAPER_TILED;
		 * otherwise, that texture is static.
		 */
		if (w->wallpaper.mode != WALLPAPER_DONT_SHOW
				&& w->wallpaper.texture && (all
				|| w->wallpaper.mode == WALLPAPER_TILED)) {
			SDL_DestroyTexture(w->wallpaper.texture);
			w->wallpaper.texture = NULL;
			if (w->config) {
				load_wallpaper(w, w->config->wallpaper_path);
			} else {
				load_default_wallpaper(w);
			}
		}

		/*
		 * Recreate the textures for subwindows; those are all
		 * dynamic.
		 */
		for (j = 0; j < MAX_SUBWINDOWS; ++j) {
			struct subwindow *sw = w->subwindows[j];

			if (!sw) {
				continue;
			}
			if (!sw->texture) {
				SDL_DestroyTexture(sw->texture);
				sw->texture = make_subwindow_texture(w,
					sw->full_rect.w, sw->full_rect.h);
				SDL_assert(sw->texture);
			}
			if (!sw->aux_texture) {
				SDL_DestroyTexture(sw->aux_texture);
				sw->aux_texture =
					make_subwindow_texture(w, 1, 1);
				SDL_assert(sw->aux_texture);
			}
			if (sw->font->cache.texture) {
				SDL_DestroyTexture(sw->font->cache.texture);
				sw->font->cache.texture = NULL;
				make_font_cache(w, sw->font);
			}
		}

		/*
		 * Recreate any textures used by dialogs or menus in the window.
		 */
		for (d = w->d_head; d; d = d->next) {
			if (d->recreate_textures_callback) {
				(*d->recreate_textures_callback)(d, w, all);
			}
		}
	}

	refresh_angband_terms(a);
}

static bool is_ok_col_row(const struct subwindow *subwindow,
		const SDL_Rect *rect, int cell_w, int cell_h)
{
	int min_w, min_h;

	get_minimum_subwindow_size(subwindow->index == MAIN_SUBWINDOW,
		cell_w, cell_h, &min_w, &min_h);

	if (rect->w < min_w || rect->h < min_h) {
		return false;
	}

	return true;
}

static bool adjust_subwindow_geometry(const struct sdlpui_window *window,
		struct subwindow *subwindow)
{
	if (!subwindow->loaded && subwindow->config == NULL) {
		/* lets center it then */
		subwindow->full_rect.x = MAX(window->inner_rect.x,
				(window->inner_rect.w - subwindow->full_rect.w) / 2);
		subwindow->full_rect.y = MAX(window->inner_rect.y,
				(window->inner_rect.h - subwindow->full_rect.h) / 2);
	}

	/* coordinates of inner rect are relative to that of outer rect
	 * (really, they are relative to subwindow's texture) */
	subwindow->inner_rect.x = 0;
	subwindow->inner_rect.y = 0;
	subwindow->inner_rect.w = subwindow->full_rect.w;
	subwindow->inner_rect.h = subwindow->full_rect.h;

	memset(&subwindow->sizing_rect, 0, sizeof(subwindow->sizing_rect));

	resize_rect(&subwindow->inner_rect,
			DEFAULT_BORDER, DEFAULT_BORDER,
			-DEFAULT_BORDER, -DEFAULT_BORDER);

	subwindow->borders.width = DEFAULT_VISIBLE_BORDER;

	subwindow->font_width = subwindow->font->ttf.glyph.w;
	subwindow->font_height = subwindow->font->ttf.glyph.h;

	subwindow->cols = subwindow->inner_rect.w / subwindow->font_width;
	subwindow->rows = subwindow->inner_rect.h / subwindow->font_height;

	subwindow->inner_rect.w = subwindow->cols * subwindow->font_width;
	subwindow->inner_rect.h = subwindow->rows * subwindow->font_height;

	subwindow->inner_rect.x = 
		(subwindow->full_rect.w - subwindow->inner_rect.w) / 2;
	subwindow->inner_rect.y =
		(subwindow->full_rect.h - subwindow->inner_rect.h) / 2;

	if (!is_ok_col_row(subwindow,
				&subwindow->full_rect, subwindow->font_width, subwindow->font_height))
	{
		return false;
	}

	if (!is_rect_in_rect(&subwindow->full_rect, &window->inner_rect)) {
		subwindow->borders.error = true;
	}

	return true;
}

static void sort_to_top_aux(struct sdlpui_window *window,
		size_t *next_subwindow, struct subwindow **subwindows,
		size_t num_subwindows, bool top, bool always_top)
{
	if (*next_subwindow == num_subwindows) {
		return;
	}

	for (size_t i = 0; i < N_ELEMENTS(window->subwindows); i++) {
		if (window->subwindows[i] != NULL
				&& window->subwindows[i]->top == top
				&& window->subwindows[i]->always_top == always_top)
		{
			subwindows[(*next_subwindow)++] = window->subwindows[i];
		}
	}

	assert(*next_subwindow <= num_subwindows);
}

static void sort_to_top(struct sdlpui_window *window)
{
	struct subwindow *tmp[N_ELEMENTS(window->subwindows)] = {NULL};
	assert(sizeof(window->subwindows) == sizeof(tmp));

	size_t num_subwindows = 0;
	for (size_t i = 0; i < N_ELEMENTS(window->subwindows); i++) {
		if (window->subwindows[i] != NULL) {
			num_subwindows++;
		}
	}

	/* and that's how we sort here :) */
	size_t current = 0;
	sort_to_top_aux(window, &current, tmp, num_subwindows, false, false);
	sort_to_top_aux(window, &current, tmp, num_subwindows, true,  false);
	sort_to_top_aux(window, &current, tmp, num_subwindows, false, true);
	sort_to_top_aux(window, &current, tmp, num_subwindows, true,  true);

	memcpy(window->subwindows, tmp, sizeof(window->subwindows));
}

static void bring_to_top(struct sdlpui_window *window,
		struct subwindow *subwindow)
{
#ifndef NDEBUG
	bool found_subwindow_in_window = false;
#endif

	assert(subwindow->window == window);
	for (size_t i = 0; i < N_ELEMENTS(window->subwindows); i++) {
		if (window->subwindows[i] != NULL) {
			window->subwindows[i]->top = false;
#ifndef NDEBUG
			if (window->subwindows[i] == subwindow) {
				found_subwindow_in_window = true;
			}
#endif
		}
	}
	assert(found_subwindow_in_window);

	subwindow->top = true;

	sort_to_top(window);
}

static void adjust_status_bar_geometry(struct sdlpui_window *window)
{
	int mw, mh;

	if (window->status_bar->ftb->query_minimum_size) {
		(*window->status_bar->ftb->query_minimum_size)(
			window->status_bar, window, &mw, &mh);
	} else {
		(*window->status_bar->ftb->query_natural_size)(
			window->status_bar, window, &mw, &mh);
	}
	assert(mw <= window->full_rect.w);
	if (window->status_bar->ftb->resize) {
		(*window->status_bar->ftb->resize)(window->status_bar, window,
			window->full_rect.w, mh);
	} else {
		window->status_bar->rect.w = window->full_rect.w;
		window->status_bar->rect.h = mh;
	}
}

static struct subwindow *get_subwindow_by_index(
		const struct sdlpui_window *window, unsigned index,
		bool visible)
{
	for (size_t i = 0; i < N_ELEMENTS(window->subwindows); i++) {
		struct subwindow *subwindow = window->subwindows[i];

		if (subwindow != NULL
				&& (visible ? subwindow->visible : true)
				&& subwindow->index == index)
		{
			return subwindow;
		}
	}
	
	return NULL;
}

static struct subwindow *get_subwindow_by_xy(
		const struct sdlpui_window *window, int x, int y)
{
	/* checking subwindows in z order */
	for (size_t i = N_ELEMENTS(window->subwindows); i > 0; i--) {
		struct subwindow *subwindow = window->subwindows[i - 1];

		if (subwindow != NULL && subwindow->visible) {
			if (is_point_in_rect(x, y, &subwindow->full_rect)) {
				return subwindow;
			}
		}
	}

	return NULL;
}

static void handle_button_open_subwindow(struct sdlpui_control *ctrl,
		struct sdlpui_dialog *dlg, struct sdlpui_window *window)
{
	int tag;
	unsigned int index;
	struct subwindow *subwindow;
	int minw, minh;

	assert(ctrl->ftb->get_tag);
	tag = (*ctrl->ftb->get_tag)(ctrl);
	assert(tag >= 0);
	index = (unsigned int)tag;
	subwindow = get_subwindow_by_index(window, index, false);
	if (subwindow != NULL) {
		subwindow->visible = !subwindow->visible;
		if (subwindow->visible) {
			bring_to_top(window, subwindow);
		}
	} else if (is_subwindow_loaded(window->app, index)) {
		subwindow = transfer_subwindow(window, index);
		subwindow->visible = true;
		bring_to_top(window, subwindow);
		refresh_angband_terms(subwindow->app);
	} else {
		subwindow = make_subwindow(window, index);
		assert(subwindow != NULL);
		bring_to_top(window, subwindow);
		refresh_angband_terms(subwindow->app);
	}

	/*
	 * Changing which subwindows are displayed can change the minimum size
	 * for the window.
	 */
	get_minimum_window_size(window, &minw, &minh);
	SDL_SetWindowMinimumSize(window->window, minw, minh);

	redraw_all_windows(subwindow->app, false);
}

static void load_status_bar(struct sdlpui_window *window)
{
	struct sdlpui_control *c;
	unsigned int i;
	int w, h;

	window->status_bar = sdlpui_start_simple_menu(NULL, NULL,
		2 + N_ELEMENTS(window->subwindows)
		+ ((window->index == MAIN_WINDOW) ? 1 : 0), false, true,
		NULL, NULL, 0);
	c = sdlpui_get_simple_menu_next_unused(window->status_bar,
		SDLPUI_MFLG_NONE);
	sdlpui_create_submenu_button(c, "Menu", SDLPUI_HOR_CENTER,
		handle_menu_button, SDLPUI_CHILD_MENU_BELOW, 0, false);
	if (window->index == MAIN_WINDOW) {
		/*
		 * For symmetry with the other windows, give the main window
		 * an entry in the menu.  As the main window is always visible
		 * while the application is hidden, that entry does not allow
		 * interaction.
		 */
		c = sdlpui_get_simple_menu_next_unused(window->status_bar,
			SDLPUI_MFLG_CAN_HIDE);
		sdlpui_create_menu_indicator(c, "A", SDLPUI_HOR_CENTER, 0,
			true);
	}
	for (i = 1; i < (unsigned int)N_ELEMENTS(window->subwindows); ++i) {
		struct subwindow *subw =
			get_subwindow_by_index(window, i, false);

		c = sdlpui_get_simple_menu_next_unused(window->status_bar,
			SDLPUI_MFLG_CAN_HIDE);
		sdlpui_create_menu_toggle(c, format("%u", i),
			SDLPUI_HOR_CENTER, handle_button_open_subwindow,
			(int)i, false, subw && subw->visible);
	}
	c = sdlpui_get_simple_menu_next_unused(window->status_bar,
		SDLPUI_MFLG_END_GRAVITY);
	window->move_button = c;
	sdlpui_create_menu_toggle(c, "Move", SDLPUI_HOR_CENTER,
		handle_button_movesize, 0, false, window->move_state.active);
	c = sdlpui_get_simple_menu_next_unused(window->status_bar,
		SDLPUI_MFLG_END_GRAVITY);
	window->size_button = c;
	sdlpui_create_menu_toggle(c, "Size", SDLPUI_HOR_CENTER,
		handle_button_movesize, 1, false, window->size_state.active);
	sdlpui_complete_simple_menu(window->status_bar, window);
	if (window->status_bar->ftb->query_minimum_size) {
		(*window->status_bar->ftb->query_minimum_size)(
			window->status_bar, window, &w, &h);
	} else {
		(*window->status_bar->ftb->query_natural_size)(
			window->status_bar, window, &w, &h);
	}
	if (w > window->full_rect.w) {
		quit("Window is too narrow for menu bar");
	} else if (w < window->full_rect.w) {
		if (window->status_bar->ftb->resize) {
			(*window->status_bar->ftb->resize)(window->status_bar,
				window, window->full_rect.w, h);
		} else {
			window->status_bar->rect.w = window->full_rect.w;
			window->status_bar->rect.h = h;
		}
	}
	/*
	 * Anchored to the top left corner of the window and not dismissed
	 * when submenus are popped down.
	 */
	window->status_bar->rect.x = 0;
	window->status_bar->rect.y = 0;
	window->status_bar->pinned = true;
	sdlpui_popup_dialog(window->status_bar, window, false);
}

static void fit_subwindow_in_window(const struct sdlpui_window *window,
		struct subwindow *subwindow)
{
	int min_w, min_h;

	get_minimum_subwindow_size(subwindow->index == MAIN_SUBWINDOW,
		subwindow->font->ttf.glyph.w, subwindow->font->ttf.glyph.h,
		&min_w, &min_h);
	coerce_rect_in_rect(&subwindow->full_rect, &window->inner_rect,
		min_w, min_h);
	if (!is_rect_in_rect(&subwindow->full_rect, &window->inner_rect)) {
		subwindow->borders.error = true;
		render_borders(subwindow);
	}
}

static void resize_window(struct sdlpui_window *window, int w, int h)
{
	if (window->full_rect.w == w
			&& window->full_rect.h == h
			&& window->status_bar->rect.w == w)
	{
		return;
	}

	window->full_rect.w = w;
	window->full_rect.h = h;

	adjust_status_bar_geometry(window);
	adjust_window_geometry(window);
	
	clear_all_borders(window);
	for (size_t i = 0; i < N_ELEMENTS(window->subwindows); i++) {
		struct subwindow *subwindow = window->subwindows[i];
		if (subwindow != NULL) {
			fit_subwindow_in_window(window, subwindow);
		}
	}

	redraw_window(window);
}

static void adjust_window_geometry(struct sdlpui_window *window)
{
	window->inner_rect.x = 0;
	window->inner_rect.y = 0;
	window->inner_rect.w = window->full_rect.w;
	window->inner_rect.h = window->full_rect.h;

	resize_rect(&window->inner_rect,
			0, window->status_bar->rect.h, 0, 0);

	if (window->inner_rect.w <= 0
			|| window->inner_rect.h <= 0) {
		quit_fmt("window %u is too small (%dx%d)",
				window->index, window->inner_rect.w, window->inner_rect.h);
	}
}

static void set_window_delay(struct sdlpui_window *window)
{
	assert(window->window != NULL);

	int display = SDL_GetWindowDisplayIndex(window->window);
	if (display < 0) {
		quit_fmt("cannot get display of window %u: %s",
				window->index, SDL_GetError());
	}

	SDL_DisplayMode mode;
	if (SDL_GetCurrentDisplayMode(display, &mode) != 0 ||
			mode.refresh_rate <= 0) {
		/* lets just guess; 60 fps is standard */
		mode.refresh_rate = 60;
	}
	/* delay in milliseconds; refresh rate in hz */
	window->delay = 1000 / mode.refresh_rate;
}

/** initialize miscellaneous things in window */
static void load_window(struct sdlpui_window *window)
{
	if (window->dialog_font == NULL) {
		const char *try_names[3];
		int try_sizes[3];
		int n_tries = 0, i = 0;

		if (window->config && window->config->font_name) {
			try_names[n_tries] = window->config->font_name;
			try_sizes[n_tries] = window->config->font_size;
			++n_tries;
		}
		try_names[n_tries] = DEFAULT_DIALOG_FONT;
		try_sizes[n_tries] = 0;
		++n_tries;
		if (window->app->font_count > 0 && window->app->fonts[0].name) {
			try_names[n_tries] = window->app->fonts[0].name;
			try_sizes[n_tries] = window->app->fonts[0].size;
			++n_tries;
		}

		while (1) {
			if (i >= n_tries) {
				quit_fmt("No usable status bar font for "
					"window %u; configured to use '%s'",
					window->index, try_names[0]);
			}
			window->dialog_font =
				make_font(window, try_names[i], try_sizes[i]);
			if (window->dialog_font) {
				if (i > 0) {
					plog_fmt("Font '%s' unusable; "
						"substituting '%s'",
						try_names[0], try_names[i]);
					if (window->config) {
						string_free(window->config->font_name);
						window->config->font_name =
							string_make(try_names[i]);
					}
				}
				break;
			}
			++i;
		}
	}
	window->stipple = sdlpui_compute_stipple(window->renderer);
	load_status_bar(window);
	window->infod = NULL;
	window->shorte = NULL;
	window->detaild = NULL;
	adjust_window_geometry(window);
	set_window_delay(window);
	if (window->wallpaper.mode != WALLPAPER_DONT_SHOW) {
		if (window->config == NULL) {
			load_default_wallpaper(window);
		} else {
			load_wallpaper(window, window->config->wallpaper_path);
		}
	}
	load_default_window_icon(window);
	if (window->graphics.id != GRAPHICS_NONE) {
		load_graphics(window, get_graphics_mode(window->graphics.id));
	}

	render_clear(window, NULL, &window->color);
	render_status_bar(window);

	window->loaded = true;
}

static bool choose_pixelformat(struct sdlpui_window *window,
		const struct SDL_RendererInfo *info)
{
#define TRY_SET_WINDOW_PIXELFORMAT(format) \
	case SDL_PIXELFORMAT_ ##format: window->pixelformat = SDL_PIXELFORMAT_ ##format;

	for (size_t i = 0; i < info->num_texture_formats; i++) {
		switch (info->texture_formats[i]) {
			TRY_SET_WINDOW_PIXELFORMAT(ARGB8888); return true;
			TRY_SET_WINDOW_PIXELFORMAT(RGBA8888); return true;
			TRY_SET_WINDOW_PIXELFORMAT(ABGR8888); return true;
			TRY_SET_WINDOW_PIXELFORMAT(BGRA8888); return true;
		}
	}
#undef TRY_SET_WINDOW_PIXELFORMAT

	return false;
}

static void start_window(struct sdlpui_window *window)
{
	int minw, minh;

	assert(!window->loaded);

	if (window->config == NULL) {
		window->window = SDL_CreateWindow(VERSION_NAME,
				window->full_rect.x, window->full_rect.y,
				window->full_rect.w, window->full_rect.h,
				SDL_WINDOW_FULLSCREEN_DESKTOP | SDL_WINDOW_RESIZABLE);
	} else {
		/*
		 * For newer configuration files, stored_rect will have the
		 * desired size for fullscreen.  Older configuration files
		 * only save the size for whatever mode (fullscreen, not
		 * fullscreen) the game was in so those will already have the
		 * right size in full_rect.
		 */
		if (window->config->window_flags
				& SDL_WINDOW_FULLSCREEN_DESKTOP) {
			if (window->stored_rect.w &&
					window->stored_rect.h) {
				SDL_Rect tmp_rect = window->full_rect;

				window->full_rect = window->stored_rect;
				window->stored_rect = tmp_rect;
			}
		}
		window->window = SDL_CreateWindow(VERSION_NAME,
				window->full_rect.x, window->full_rect.y,
				window->full_rect.w, window->full_rect.h,
				window->config->window_flags);
	}
	assert(window->window != NULL);

	if (window->config == NULL) {
		window->renderer = SDL_CreateRenderer(window->window,
				-1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_TARGETTEXTURE);
	} else {
		/* this is necessary for subwindows to have their own textures */
		window->config->renderer_flags |= SDL_RENDERER_TARGETTEXTURE;
		window->renderer = SDL_CreateRenderer(window->window,
				-1, window->config->renderer_flags);
	}
	if (window->renderer == NULL) {
		quit_fmt("cannot create renderer for window %u: %s",
				window->index, SDL_GetError());
	}

	SDL_RendererInfo info;
	if (SDL_GetRendererInfo(window->renderer, &info) != 0) {
		quit_fmt("cannot query renderer in window %u", window->index);
	}

	if (!choose_pixelformat(window, &info)) {
		quit_fmt("cannot choose pixelformat for window %u", window->index);
	}

	load_window(window);

	for (size_t i = 0; i < N_ELEMENTS(window->subwindows); i++) {
		if (window->subwindows[i] != NULL) {
			load_subwindow(window, window->subwindows[i]);
		}
	}

	get_minimum_window_size(window, &minw, &minh);
	SDL_SetWindowMinimumSize(window->window, minw, minh);

	window->flags = SDL_GetWindowFlags(window->window);
	window->id = SDL_GetWindowID(window->window);
}

static void wipe_window_aux_config(struct sdlpui_window *window)
{
	window->config = mem_zalloc(sizeof(*window->config));
	assert(window->config != NULL);

	const struct sdlpui_window *main_window =
		get_window_direct(window->app, MAIN_WINDOW);
	assert(main_window != NULL);

	SDL_RendererInfo rinfo;
	if (SDL_GetRendererInfo(main_window->renderer, &rinfo) != 0) {
		quit_fmt("cannot get renderer info for main window: %s", SDL_GetError());
	}
	window->config->renderer_flags = rinfo.flags;
	window->config->renderer_index = -1;

	window->config->window_flags= SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE;

	if (main_window->config == NULL) {
		char path[4096];
		path_build(path, sizeof(path), DEFAULT_WALLPAPER_DIR, DEFAULT_WALLPAPER);
		window->config->wallpaper_path = string_make(path);
		window->config->font_name = string_make(DEFAULT_DIALOG_FONT);
	} else {
		window->config->wallpaper_path = string_make(main_window->config->wallpaper_path);
		window->config->font_name = string_make(main_window->config->font_name);
		window->config->font_size = main_window->config->font_size;
	}

	int display = SDL_GetWindowDisplayIndex(main_window->window);
	if (display < 0) {
		quit_fmt("cannot get display from main window: %s", SDL_GetError());
	}

	/* center it on the screen */
	SDL_DisplayMode mode;
	SDL_GetCurrentDisplayMode(display, &mode);
	window->full_rect.w = mode.w / 2;
	window->full_rect.h = mode.h / 2;
	window->full_rect.x = mode.w / 4;
	window->full_rect.y = mode.h / 4;
	window->stored_rect.w = 0;
	window->stored_rect.h = 0;
	window->stored_rect.x = 0;
	window->stored_rect.y = 0;

	if (current_graphics_mode != NULL) {
		window->graphics.id = current_graphics_mode->grafID;
	}
}

/* initialize window with suitable hardcoded defaults */
static void wipe_window(struct sdlpui_window *window, int display)
{
	struct my_app *app = window->app;
	unsigned index = window->index;
	memset(window, 0, sizeof(*window));
	window->app = app;
	window->index = index;
	window->outlined_subwindow = (unsigned)-1;

	for (size_t j = 0; j < N_ELEMENTS(window->subwindows); j++) {
		window->subwindows[j] = NULL;
	}

	SDL_DisplayMode mode;
	if (SDL_GetCurrentDisplayMode(display, &mode) != 0) {
		quit_fmt("cannot get display mode for window %u: %s",
				window->index, SDL_GetError());
	}

	window->pixelformat = SDL_PIXELFORMAT_UNKNOWN;

	window->full_rect.w = mode.w;
	window->full_rect.h = mode.h;
	window->stored_rect.w = 0;
	window->stored_rect.h = 0;
	window->stored_rect.x = 0;
	window->stored_rect.y = 0;

	window->color = window->app->colors[DEFAULT_WINDOW_BG_COLOR];
	window->alpha = DEFAULT_ALPHA_FULL;

	window->wallpaper.texture = NULL;
	window->wallpaper.mode = WALLPAPER_TILED;

	window->stipple.texture = NULL;

	window->graphics.texture = NULL;
	window->graphics.id = GRAPHICS_NONE;

	window->dirty = true;

	window->config = NULL;
	window->inited = true;
}

static void dump_subwindow(const struct subwindow *subwindow, ang_file *config)
{
#define DUMP_SUBWINDOW(sym, fmt, ...) \
	file_putf(config, "subwindow-" sym ":%u:" fmt "\n", subwindow->index, __VA_ARGS__)
	DUMP_SUBWINDOW("window", "%u:%d", subwindow->window->index,
			(subwindow->visible) ? 1 : 0);
	if (subwindow->window->flags & SDL_WINDOW_FULLSCREEN_DESKTOP) {
		DUMP_SUBWINDOW("full-rect", "%d:%d:%d:%d",
			subwindow->stored_rect.x, subwindow->stored_rect.y,
			subwindow->stored_rect.w, subwindow->stored_rect.h);
		DUMP_SUBWINDOW("full-rect-fs", "%d:%d:%d:%d",
			subwindow->full_rect.x, subwindow->full_rect.y,
			subwindow->full_rect.w, subwindow->full_rect.h);
	} else {
		DUMP_SUBWINDOW("full-rect", "%d:%d:%d:%d",
			subwindow->full_rect.x, subwindow->full_rect.y,
			subwindow->full_rect.w, subwindow->full_rect.h);
		DUMP_SUBWINDOW("full-rect-fs", "%d:%d:%d:%d",
			subwindow->stored_rect.x, subwindow->stored_rect.y,
			subwindow->stored_rect.w, subwindow->stored_rect.h);
	}
	DUMP_SUBWINDOW("font", "%d:%s",
			subwindow->font->size, subwindow->font->name);
	DUMP_SUBWINDOW("borders", "%s",
			subwindow->borders.visible ? "true" : "false");
	DUMP_SUBWINDOW("top", "%s:%s",
			subwindow->top ? "true" : "false",
			subwindow->always_top ? "true" : "false");
	DUMP_SUBWINDOW("alpha", "%u", subwindow->color.a);
#undef DUMP_SUBWINDOW
	file_put(config, "\n");
}

static void dump_window(const struct sdlpui_window *window, ang_file *config)
{
#define DUMP_WINDOW(sym, fmt, ...) \
	file_putf(config, "window-" sym ":%u:" fmt "\n", window->index, __VA_ARGS__)
	DUMP_WINDOW("display", "%d", SDL_GetWindowDisplayIndex(window->window));

	int x;
	int y;
	SDL_GetWindowPosition(window->window, &x, &y);
	if (window->flags & SDL_WINDOW_FULLSCREEN_DESKTOP) {
		DUMP_WINDOW("full-rect", "%d:%d:%d:%d",
			window->stored_rect.x, window->stored_rect.y,
			window->stored_rect.w, window->stored_rect.h);
		DUMP_WINDOW("full-rect-fs", "%d:%d:%d:%d",
			x, y, window->full_rect.w, window->full_rect.h);
	} else {
		DUMP_WINDOW("full-rect", "%d:%d:%d:%d",
			x, y, window->full_rect.w, window->full_rect.h);
		DUMP_WINDOW("full-rect-fs", "%d:%d:%d:%d",
			window->stored_rect.x, window->stored_rect.y,
			window->stored_rect.w, window->stored_rect.h);
	}

	DUMP_WINDOW("fullscreen", "%s",
			(window->flags & SDL_WINDOW_FULLSCREEN_DESKTOP) ? "true" : "false");

	SDL_RendererInfo rinfo;
	SDL_GetRendererInfo(window->renderer, &rinfo);
	DUMP_WINDOW("renderer", "%s",
			(rinfo.flags & SDL_RENDERER_ACCELERATED) ? "hardware" : "software");

	if (window->config) {
		DUMP_WINDOW("wallpaper-path", "%s", window->config->wallpaper_path);
	} else {
		DUMP_WINDOW("wallpaper-path", "%s", "default");
	}
	DUMP_WINDOW("wallpaper-mode", "%s",
			window->wallpaper.mode == WALLPAPER_DONT_SHOW ? "none"     :
			window->wallpaper.mode == WALLPAPER_TILED     ? "tiled"    :
			window->wallpaper.mode == WALLPAPER_CENTERED  ? "centered" :
			window->wallpaper.mode == WALLPAPER_SCALED    ? "scaled"   :
			"ERROR");
	DUMP_WINDOW("status-bar-font", "%d:%s",
			window->dialog_font->size, window->dialog_font->name);

	DUMP_WINDOW("graphics-id", "%d", window->graphics.id);
	DUMP_WINDOW("tile-scale", "width:%d", tile_width);
	DUMP_WINDOW("tile-scale", "height:%d", tile_height);
#undef DUMP_WINDOW
	file_put(config, "\n");

	for (size_t i = 0; i < N_ELEMENTS(window->subwindows); i++) {
		struct subwindow *subwindow = window->subwindows[i];
		if (subwindow != NULL) {
			dump_subwindow(subwindow, config);
		}
	}
}

static void detach_subwindow_from_window(struct sdlpui_window *window,
		struct subwindow *subwindow)
{
	assert(subwindow->window == window);

	size_t i = 0;
	while (i < N_ELEMENTS(window->subwindows) && window->subwindows[i] != subwindow) {
		i++;
	}
	assert(i < N_ELEMENTS(window->subwindows));

	window->subwindows[i] = NULL;
	/* Also update the state of the menu bar. */
	if (window->status_bar) {
		int cidx = subwindow->index
			+ ((window->index == MAIN_WINDOW) ? 1 : 0);
		struct sdlpui_simple_menu *sm;
		struct sdlpui_menu_button *mb;

		assert(window->status_bar->type_code
			== SDLPUI_DIALOG_SIMPLE_MENU);
		sm = (struct sdlpui_simple_menu*)window->status_bar->priv;
		assert(cidx < sm->number);
		assert(sm->controls[cidx].type_code
			== SDLPUI_CTRL_MENU_BUTTON);
		mb = (struct sdlpui_menu_button*)sm->controls[cidx].priv;
		assert(mb->subtype_code == SDLPUI_MB_TOGGLE);
		mb->v.toggled = false;
		window->status_bar->dirty = true;
		window->dirty = true;
	}
}

static void attach_subwindow_to_window(struct sdlpui_window *window,
		struct subwindow *subwindow)
{
	assert(subwindow->window == window);

	size_t i = 0;
	while (i < N_ELEMENTS(window->subwindows) && window->subwindows[i] != NULL) {
		i++;
	}
	assert(i < N_ELEMENTS(window->subwindows));

	window->subwindows[i] = subwindow;
}

static struct subwindow *make_subwindow(struct sdlpui_window *window,
		unsigned index)
{
	struct subwindow *subwindow = get_new_subwindow(window->app, index);
	assert(subwindow != NULL);

	load_subwindow(window, subwindow);
	attach_subwindow_to_window(window, subwindow);
	load_term(subwindow);

	return subwindow;
}

static struct subwindow *transfer_subwindow(struct sdlpui_window *window,
		unsigned index)
{
	struct subwindow *subwindow = get_subwindow_direct(window->app, index);

	assert(subwindow != NULL);
	assert(subwindow->inited);
	assert(subwindow->loaded);

	detach_subwindow_from_window(subwindow->window, subwindow);
	if (subwindow->window != window) {
		int minw, minh;

		/* Removing a subwindow can change the minimum window size. */
		get_minimum_window_size(subwindow->window, &minw, &minh);
		SDL_SetWindowMinimumSize(subwindow->window->window, minw, minh);
	}
	subwindow->window = window;
	attach_subwindow_to_window(window, subwindow);

	SDL_DestroyTexture(subwindow->texture);
	subwindow->texture = make_subwindow_texture(window,
			subwindow->full_rect.w, subwindow->full_rect.h);
	assert(subwindow->texture != NULL);

	SDL_DestroyTexture(subwindow->aux_texture);
	subwindow->aux_texture = make_subwindow_texture(window, 1, 1);
	assert(subwindow->aux_texture != NULL);

	struct font *new_font = make_font(subwindow->window,
			subwindow->font->name, subwindow->font->size);
	assert(new_font != NULL);
	free_font(subwindow->font);
	subwindow->font = new_font;

	render_clear(window, subwindow->texture, &subwindow->color);

	subwindow->borders.error = false;
	render_borders(subwindow);

	fit_subwindow_in_window(window, subwindow);

	return subwindow;
}

static void load_subwindow(struct sdlpui_window *window,
		struct subwindow *subwindow)
{
	assert(window->loaded);
	assert(!subwindow->loaded);

	if (subwindow->font == NULL) {
		const char *try_names[3];
		int try_sizes[3];
		int n_tries = 0, i = 0;

		if (subwindow->config && subwindow->config->font_name) {
			try_names[n_tries] = subwindow->config->font_name;
			try_sizes[n_tries] = subwindow->config->font_size;
			++n_tries;
		}
		try_names[n_tries] = DEFAULT_FONT;
		try_sizes[n_tries] = (suffix_i(DEFAULT_FONT, ".fon")) ?
			0 : DEFAULT_VECTOR_FONT_SIZE;
		++n_tries;
		if (window->app->font_count > 0 && window->app->fonts[0].name) {
			try_names[n_tries] = window->app->fonts[0].name;
			try_sizes[n_tries] = window->app->fonts[0].size;
			++n_tries;
		}

		while (1) {
			if (i >= n_tries) {
				quit_fmt("No usable font for subwindow %u;"
					" configured to use '%s'",
					subwindow->index, try_names[0]);
			}
			subwindow->font = make_font(window, try_names[i],
				try_sizes[i]);
			if (subwindow->font) {
				if (i > 0) {
					plog_fmt("Font '%s' unusable "
						"substituting '%s'",
						try_names[0], try_names[i]);
					if (subwindow->config) {
						string_free(subwindow->config->font_name);
						subwindow->config->font_name =
							string_make(try_names[i]);
					}
				}
				break;
			}
			++i;
		}
	}

	/*
	 * For newer configuration files, stored_rect will have the desired
	 * size for fullscreen.  Older configuration files only save the
	 * size of whatever mode (fullscreen, not fullscreen) the game was in
	 * so those will already have the right size in full_rect.
	 */
	if (subwindow->config && window->config && (window->config->window_flags
			& SDL_WINDOW_FULLSCREEN_DESKTOP)
			&& subwindow->stored_rect.w
			&& subwindow->stored_rect.h) {
		SDL_Rect tmp_rect = subwindow->full_rect;

		subwindow->full_rect = subwindow->stored_rect;
		subwindow->stored_rect = tmp_rect;
	}

	if (!adjust_subwindow_geometry(window, subwindow)) {
		quit_fmt("cannot adjust geometry of subwindow %u in window %u",
				subwindow->index, window->index);
	}
	subwindow->texture = make_subwindow_texture(window,
			subwindow->full_rect.w, subwindow->full_rect.h);
	assert(subwindow->texture != NULL);

	/* just a pixel for sizing rect */
	subwindow->aux_texture = make_subwindow_texture(window, 1, 1);
	assert(subwindow->aux_texture != NULL);

	/* same testing sequence as for status bar */
	if (SDL_SetRenderDrawColor(window->renderer,
				subwindow->color.r, subwindow->color.g,
				subwindow->color.b, subwindow->color.a) != 0)
	{
		quit_fmt("cannot set draw color for subwindow %u window %u: %s",
				subwindow->index, window->index, SDL_GetError());
	}
	if (SDL_SetRenderTarget(window->renderer, subwindow->texture) != 0) {
		quit_fmt("cannot set subwindow %u as render target in window %u: %s",
				subwindow->index, window->index, SDL_GetError());
	}
	if (SDL_RenderClear(window->renderer) != 0) {
		quit_fmt("cannot clear texture in subwindow %u window %u: %s",
				subwindow->index, window->index, SDL_GetError());
	}

	subwindow->window = window;
	subwindow->loaded = true;

	render_borders(subwindow);
}

static void clear_pw_flag(struct subwindow *subwindow)
{
	assert(subwindow->index < N_ELEMENTS(angband_term));

	uint32_t new_flags[N_ELEMENTS(window_flag)];
	assert(sizeof(new_flags) == sizeof(window_flag));
	memcpy(new_flags, window_flag, sizeof(new_flags));

	assert(subwindow->index < N_ELEMENTS(new_flags));
	new_flags[subwindow->index] = 0;

	subwindows_set_flags(new_flags, N_ELEMENTS(new_flags));
}

static void unload_term(struct subwindow *subwindow)
{
	if (!subwindow->linked) {
		return;
	}

	term_nuke(subwindow->term);
	mem_free(subwindow->term);
	angband_term[subwindow->index] = NULL;

	subwindow->term = NULL;
	subwindow->linked = false;
}

static void link_term(struct subwindow *subwindow)
{
	assert(subwindow->term != NULL);
	assert(subwindow->index < N_ELEMENTS(angband_term));
	assert(angband_term[subwindow->index] == NULL);

	subwindow->term->higher_pict = true;
	subwindow->term->soft_cursor = true;
	subwindow->term->complex_input = true;
	subwindow->term->never_frosh = true;

	subwindow->term->xtra_hook = term_xtra_hook;
	subwindow->term->curs_hook = term_curs_hook;
	subwindow->term->bigcurs_hook = term_bigcurs_hook;
	subwindow->term->wipe_hook = term_wipe_hook;
	subwindow->term->text_hook = term_text_hook;
	subwindow->term->pict_hook = term_pict_hook;
	subwindow->term->view_map_hook = term_view_map_hook;
	if (subwindow->window->graphics.overdraw_row) {
		subwindow->term->dblh_hook = is_dh_tile;
	} else {
		subwindow->term->dblh_hook = NULL;
	}

	subwindow->term->data = subwindow;
	angband_term[subwindow->index] = subwindow->term;
}

static void load_term(struct subwindow *subwindow)
{
	assert(!subwindow->linked);

	subwindow->term = mem_zalloc(sizeof(*subwindow->term));

	term_init(subwindow->term,
			subwindow->cols, subwindow->rows,
			SUBWINDOW_KEYQ_SIZE(subwindow));

	link_term(subwindow);

	term *old = Term;
	Term_activate(subwindow->term);
	Term_redraw();
	Term_activate(old);

	subwindow->linked = true;
}

/* initialize subwindow with suitable hardcoded defaults */
static bool wipe_subwindow(struct subwindow *subwindow)
{
	struct my_app *app = subwindow->app;
	unsigned index = subwindow->index;
	memset(subwindow, 0, sizeof(*subwindow));
	subwindow->app = app;
	subwindow->index = index;

	/* XXX 80x24 is essential for main */
	get_minimum_subwindow_size(true,
		subwindow->app->def_font_w, subwindow->app->def_font_h,
		&subwindow->full_rect.w, &subwindow->full_rect.h);
	if (subwindow->index != MAIN_SUBWINDOW) {
		subwindow->full_rect.w /= 2;
		subwindow->full_rect.h /= 2;
	}
	subwindow->cols = MIN_COLS_MAIN;
	subwindow->rows = MIN_ROWS_MAIN;

	subwindow->color = subwindow->app->colors[DEFAULT_SUBWINDOW_BG_COLOR];
	subwindow->borders.color = subwindow->app->colors[DEFAULT_SUBWINDOW_BORDER_COLOR];
	subwindow->borders.visible = true;

	subwindow->texture = NULL;
	subwindow->aux_texture = NULL;
	subwindow->window = NULL;
	subwindow->font = NULL;

	subwindow->term = NULL;

	subwindow->config = NULL;
	subwindow->inited = true;
	subwindow->visible = true;

	return true;
}

static int sort_cb_font_info(const void *infoa, const void *infob)
{
	int typea = ((struct font_info *) infoa)->type;
	int typeb = ((struct font_info *) infob)->type;
	
	const char *namea = ((struct font_info *) infoa)->name;
	const char *nameb = ((struct font_info *) infob)->name;

	if (typea != typeb) {
		/* raster (angband's .fon) fonts go last */
		return typea == FONT_TYPE_RASTER ? 1 : -1;
	} else if (typea == FONT_TYPE_VECTOR && typeb == FONT_TYPE_VECTOR) {
		/* vector (.ttf, etc) fonts go in alphabetical order */
		return strcmp(namea, nameb);
	} else {
		/* otherwise, we'll sort them numbers-wise (6x12x.fon before 6x13x.fon) */
		int wa = 0, ha = 0, wb = 0, hb = 0;
		char *facea = NULL, *exta = NULL, *faceb = NULL, *extb = NULL;
		char *ew;
		long lv;

		lv = strtol(namea, &ew, 10);
		if (ew != namea && *ew == 'x' && lv > INT_MIN && lv < INT_MAX) {
			wa = (int)lv;
			lv = strtol(ew + 1, &facea, 10);
			if (facea != ew + 1 && lv > INT_MIN && lv < INT_MAX) {
				ha = (int)lv;
				exta = strchr(facea, '.');
				if (exta == facea) {
					exta = NULL;
				}
			}
		}
		lv = strtol(nameb, &ew, 10);
		if (ew != nameb && *ew == 'x' && lv > INT_MIN && lv < INT_MAX) {
			wb = (int)lv;
			lv = strtol(ew + 1, &faceb, 10);
			if (faceb != ew + 1 && lv > INT_MIN && lv < INT_MAX) {
				hb = (int)lv;
				extb = strchr(faceb, '.');
				if (extb == faceb) {
					extb = NULL;
				}
			}
		}

		if (!exta) {
			if (!extb) {
				/*
				 * Neither match the expected pattern.  Sort
				 * alphabetically.
				 */
				return strcmp(namea, nameb);
			}
			/* Put b last since it matches the expected pattern. */
			return -1;
		}
		if (!extb) {
			/* Put a last since it matches the expected pattern. */
			return 1;
		}
		if (wa < wb) {
			return -1;
		}
		if (wa > wb) {
			return 1;
		}
		if (ha < hb) {
			return -1;
		}
		if (ha > hb) {
			return 1;
		}
		return strncmp(facea, faceb, MAX(exta - facea, extb - faceb));
	}
}

static bool is_font_file(const char *path)
{
	bool is_font = false;

	TTF_Font *font = TTF_OpenFont(path, 1);

	if (font != NULL) {
		if (TTF_FontFaceIsFixedWidth(font)) {
			is_font = true;
		}
		TTF_CloseFont(font);
	}

	return is_font;
}

static void free_font_info(struct font_info *font_info)
{
	if (font_info->name != NULL) {
		mem_free(font_info->name);
		font_info->name = NULL;
	}
	if (font_info->path != NULL) {
		mem_free(font_info->path);
		font_info->path = NULL;
	}
}

static void free_window_config(struct window_config *config)
{
	if (config->wallpaper_path != NULL) {
		mem_free(config->wallpaper_path);
	}
	if (config->font_name != NULL) {
		mem_free(config->font_name);
	}
	mem_free(config);
}

static void free_graphics(struct graphics *graphics)
{
	if (graphics->texture != NULL) {
		SDL_DestroyTexture(graphics->texture);
		graphics->texture = NULL;
	}
}

static void free_subwindow_config(struct subwindow_config *config)
{
	if (config->font_name != NULL) {
		mem_free(config->font_name);
	}
	mem_free(config);
}

static void free_subwindow(struct subwindow *subwindow)
{
	assert(subwindow->loaded);

	free_font(subwindow->font);
	subwindow->font = NULL;
	if (subwindow->texture != NULL) {
		SDL_DestroyTexture(subwindow->texture);
		subwindow->texture = NULL;
	}
	if (subwindow->aux_texture != NULL) {
		SDL_DestroyTexture(subwindow->aux_texture);
		subwindow->aux_texture = NULL;
	}
	if (subwindow->term != NULL) {
		unload_term(subwindow);
	}
	if (subwindow->config != NULL) {
		free_subwindow_config(subwindow->config);
		subwindow->config = NULL;
	}

	subwindow->window = NULL;
	subwindow->loaded = false;
	subwindow->inited = false;
}

static void free_window(struct sdlpui_window *window)
{
	assert(window->loaded);

	while (window->d_head) {
		sdlpui_popdown_dialog(window->d_head, window, false);
	}
	window->d_tail = NULL;
	window->d_mouse = NULL;
	window->d_key = NULL;
	window->status_bar = NULL;
	window->move_button = NULL;
	window->size_button = NULL;
	window->infod = NULL;
	window->shorte = NULL;
	window->detaild = NULL;

	for (size_t i = 0; i < N_ELEMENTS(window->subwindows); i++) {
		struct subwindow *subwindow = window->subwindows[i];
		if (subwindow != NULL) {
			free_subwindow(subwindow);
			window->subwindows[i] = NULL;
		}
	}

	if (window->wallpaper.texture != NULL) {
		SDL_DestroyTexture(window->wallpaper.texture);
		window->wallpaper.texture = NULL;
	}

	if (window->stipple.texture != NULL) {
		SDL_DestroyTexture(window->stipple.texture);
		window->stipple.texture = NULL;
	}

	free_font(window->dialog_font);
	window->dialog_font = NULL;

	free_graphics(&window->graphics);

	if (window->renderer != NULL) {
		SDL_DestroyRenderer(window->renderer);
		window->renderer = NULL;
	}

	if (window->window != NULL) {
		SDL_DestroyWindow(window->window);
		window->window = NULL;
	}

	if (window->config != NULL) {
		free_window_config(window->config);
		window->config = NULL;
	}

	window->loaded = false;
	window->inited = false;
}

static void init_colors(struct my_app *a)
{
	assert(N_ELEMENTS(a->colors) == N_ELEMENTS(angband_color_table));
	size_t i;

	for (i = 0; i < N_ELEMENTS(a->colors); i++) {
		a->colors[i].r = angband_color_table[i][1];
		a->colors[i].g = angband_color_table[i][2];
		a->colors[i].b = angband_color_table[i][3];
		a->colors[i].a = DEFAULT_ALPHA_FULL;
	}
	for (i = 0; i < N_ELEMENTS(a->windows); i++) {
		a->windows[i].color = a->colors[DEFAULT_WINDOW_BG_COLOR];
	}
	for (i = 0; i < N_ELEMENTS(a->subwindows); i++) {
		/* Retain whatever customized alpha the subwindow has. */
		a->subwindows[i].color.r =
			a->colors[DEFAULT_SUBWINDOW_BG_COLOR].r;
		a->subwindows[i].color.g =
			a->colors[DEFAULT_SUBWINDOW_BG_COLOR].g;
		a->subwindows[i].color.b =
			a->colors[DEFAULT_SUBWINDOW_BG_COLOR].b;
	}
}

static void init_font_info(struct my_app *a, const char *directory)
{
	char name[1024], path[4096];
	ang_dir *dir;
	TTF_Font *hfnt;

	a->font_count = 0;
	a->font_alloc = 0;
	a->fonts = NULL;

	dir = my_dopen(directory);
	if (!dir) {
		quit_fmt("Font directory '%s' is unreadable", directory);
	}

	while (my_dread(dir, name, sizeof(name))) {
		path_build(path, sizeof(path), directory, name);

		if (is_font_file(path)) {
			if (a->font_count >= a->font_alloc) {
				if (a->font_count > 1024) {
					break;
				}
				a->font_alloc = (a->font_alloc)
					? 2 * a->font_alloc : 32;
				a->fonts = mem_realloc(a->fonts,
					a->font_alloc * sizeof(*a->fonts));
			}
			a->fonts[a->font_count].name = string_make(name);
			a->fonts[a->font_count].path = string_make(path);
			if (suffix_i(path, ".fon")) {
				a->fonts[a->font_count].type = FONT_TYPE_RASTER;
				a->fonts[a->font_count].size = 0;
			} else {
				a->fonts[a->font_count].type = FONT_TYPE_VECTOR;
				a->fonts[a->font_count].size =
					DEFAULT_VECTOR_FONT_SIZE;
			}
			++a->font_count;
		}
	}
	if (!a->font_count) {
		quit_fmt("No usable fonts found in '%s'", directory);
	}

	sort(a->fonts, a->font_count, sizeof(a->fonts[0]), sort_cb_font_info);

	for (int i = 0; i < a->font_count; ++i) {
		a->fonts[i].index = i;
	}

	my_dclose(dir);

	path_build(path, sizeof(path), directory, DEFAULT_FONT);
	hfnt = TTF_OpenFont(path, suffix_i(path, ".fon") ?
		0 : DEFAULT_VECTOR_FONT_SIZE);
	if (!hfnt) {
		hfnt = TTF_OpenFont(a->fonts[0].path,
			(a->fonts[0].type == FONT_TYPE_VECTOR) ?
			DEFAULT_VECTOR_FONT_SIZE : 0);
	}
	if (hfnt) {
		if (TTF_GlyphMetrics(hfnt, GLYPH_FOR_ADVANCE, NULL, NULL, NULL,
				NULL, &a->def_font_w) != 0) {
			quit_fmt("cannot query glyph metrics for "
				"default font: %s", TTF_GetError());
		}
		a->def_font_h = TTF_FontHeight(hfnt);
		TTF_CloseFont(hfnt);
	} else {
		/* There's no available default; substitute something useful. */
		a->def_font_w = 10;
		a->def_font_h = 20;
	}
}

static void create_defaults(struct my_app *a)
{
	struct sdlpui_window *window = get_new_window(a, MAIN_WINDOW);
	assert(window != NULL);

	struct subwindow *subwindow = get_new_subwindow(a, MAIN_SUBWINDOW);
	assert(subwindow != NULL);

	assert(MAIN_SUBWINDOW < N_ELEMENTS(window->subwindows));
	window->subwindows[MAIN_SUBWINDOW] = subwindow;
}

static void quit_systems(void)
{
	SDL_StopTextInput();
	sdlpui_quit();
	TTF_Quit();
	IMG_Quit();
	SDL_Quit();
}

static void quit_hook(const char *s)
{
	if (s) {
		SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Fatal Error",
			s, NULL);
	}

	/*
	 * If at least the main window was successfully set up, remember the
	 * configuration.
	 */
	if (g_app.windows[0].loaded) {
		dump_config_file(&g_app);
	}

	free_globals(&g_app);
	quit_systems();
}

static void init_systems(void)
{
#if defined(SDLPUI_TRACE_EVENTS) || defined(SDLPUI_TRACE_RENDER)
	SDL_LogSetAllPriority(SDL_LOG_PRIORITY_VERBOSE);
#endif
	if (SDL_Init(INIT_SDL_FLAGS) != 0) {
		quit_fmt("SDL_Init: %s", SDL_GetError());
	}
	if (IMG_Init(INIT_IMG_FLAGS) != INIT_IMG_FLAGS) {
		quit_fmt("IMG_Init: %s", IMG_GetError());
		SDL_Quit();
	}
	if (TTF_Init() != 0) {
		quit_fmt("TTF_Init: %s", TTF_GetError());
	}
	if (sdlpui_init()) {
		quit("sdlpui_init() failed");
	}
	SHORTCUT_EDITOR_CODE = sdlpui_register_code("ANGBAND_SHORTCUT_EDITOR");
	SDL_assert(SHORTCUT_EDITOR_CODE);

	/* On (some?) Macs the touchpad sends both mouse events and touch events;
	 * the latter interfere badly with the working of the status bar */
	SDL_EventState(SDL_FINGERMOTION, SDL_DISABLE);
	SDL_EventState(SDL_FINGERDOWN, SDL_DISABLE);
	SDL_EventState(SDL_FINGERUP, SDL_DISABLE);
	/* Ignore Keymap changed events since they are not handled */
	SDL_EventState(SDL_KEYMAPCHANGED, SDL_DISABLE);

	SDL_StartTextInput();
	SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
#ifdef SDL_HINT_POLL_SENTINEL
	SDL_SetHint(SDL_HINT_POLL_SENTINEL, "0");
#endif
}

errr init_sdl2(int argc, char **argv)
{
	int i;

	g_app.print_sdl_details = false;
	for (i = 1; i < argc; ++i) {
		if (streq(argv[i], "-v")) {
			g_app.print_sdl_details = true;
			continue;
		}
		SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Ignoring option: %s",
			argv[i]);
	}

	quit_aux = quit_hook;

	/* Dump details about SDL that do not require SDL_Init(). */
	if (g_app.print_sdl_details) {
		SDL_version vr, vc;

		SDL_GetVersion(&vr);
		SDL_VERSION(&vc);
		SDL_Log("SDL library version: %u.%u.%u (runtime) "
			"%u.%u.%u (compiled; %s)", vr.major, vr.minor, vr.patch,
			vc.major, vc.minor, vc.patch, SDL_REVISION);
	}

	init_systems();

	/*
	 * Dump details about SDL that may require that SDL_Init() has been
	 * called.
	 */
	if (g_app.print_sdl_details) {
		const char *driver_name;
		const SDL_version *pv;
		SDL_version lv;
		int num_displays;

		SDL_Log("Runtime SDL library revision: %s", SDL_GetRevision());
		pv = IMG_Linked_Version();
		SDL_IMAGE_VERSION(&lv);
		SDL_Log("SDL_image library version: %u.%u.%u (runtime) "
			"%u.%u.%u (compiled)", pv->major, pv->minor, pv->patch,
			lv.major, lv.minor, lv.patch);
		pv = TTF_Linked_Version();
		SDL_TTF_VERSION(&lv);
		SDL_Log("SDL_ttf library version: %u.%u.%u (runtime) "
			"%u.%u.%u (compiled)", pv->major, pv->minor, pv->patch,
			lv.major, lv.minor, lv.patch);
		driver_name = SDL_GetCurrentVideoDriver();
		SDL_Log("Platform and video driver: \"%s\" \"%s\"",
			SDL_GetPlatform(), (driver_name) ? driver_name :
			"Not initialized");
		num_displays = SDL_GetNumVideoDisplays();
		if (num_displays < 0) {
			SDL_Log("No available displays: %s",
				SDL_GetError());
		}
		for (i = 0; i < num_displays; ++i) {
			const char *name = SDL_GetDisplayName(i);

			if (name) {
				SDL_DisplayMode mode;

				SDL_Log("Display %d: %s:", i, name);
				if (SDL_GetCurrentDisplayMode(i, &mode)) {
					SDL_Log("    Mode unavailable: %s",
						SDL_GetError());
				} else {
					SDL_Log("    Size: %d x %d",
						mode.w, mode.h);
					SDL_Log("    Refresh rate and "
						"pixel format: %d %lu",
						mode.refresh_rate,
						(unsigned long)mode.format);
				}
			} else {
				SDL_Log("Display %d: no name: %s", i,
					SDL_GetError());
			}
		}
	}

	if (!init_graphics_modes()) {
		quit("Graphics list load failed");
	}

	init_globals(&g_app);
	if (!read_config_file(&g_app)) {
		create_defaults(&g_app);
	}

	start_windows(&g_app);
	load_terms(&g_app);

#ifdef MSYS2_ENCODING_WORKAROUND
	/*
	 * Under MSYS2, mbcstowcs() converts UTF-8 by outputting a wchar_t
	 * (a 16-bit quantity) for every byte in the UTF-8 sequence.  For most
	 * bytes in the UTF-8 sequence, the corresponding wchar_t has the same
	 * value as the byte but some are altered.  For instance the UTF-8
	 * sequence 0x24, 0xC2, 0xA2, 0xE2, 0x82, 0xAC, 0xF0, 0x90, 0x8D, 0x88,
	 * 0xF0, 0x90, 0x91, and 0x99 which represents U+0024, U+00A2, U+20AC,
	 * U+10348, and U+10459 becomes 0x0024, 0x00C2, 0x00A2, 0x00E2, 0x201A,
	 * 0x00AC, 0x00F0, 0x0090, 0x008D, 0x02C6, 0x00F0, 0x0090, 0x2018,
	 * and 0x2122.  Override that to use a UTF-16 encoding for the
	 * wchar_t's where any codepoint that would require a surrogate pair
	 * is lossily converted to U+FFFD since ui-term.c only allows storage
	 * for one wchar_t per grid location.
	 */
	text_mbcs_hook = term_mbcs_sdl2_msys2;
	text_wctomb_hook = term_wctomb_sdl2_msys2;
	text_wcsz_hook = term_wcsz_sdl2_msys2;
	text_iswprint_hook = term_iswprint_sdl2_msys2;
#endif /* MSYS2_ENCODING_WORKAROUND */

	return 0;
}

static void init_globals(struct my_app *a)
{
	int num_joysticks;

	path_build(a->config_file, sizeof(a->config_file),
			DEFAULT_CONFIG_FILE_DIR, DEFAULT_CONFIG_FILE);

	for (size_t i = 0; i < N_ELEMENTS(a->subwindows); i++) {
		a->subwindows[i].index = i;
		a->subwindows[i].app = a;
	}
	for (size_t i = 0; i < N_ELEMENTS(a->windows); i++) {
		a->windows[i].index = i;
		a->windows[i].app = a;
		a->menu_shortcuts[i] = KEYPRESS_NULL;
	}

	init_font_info(a, ANGBAND_DIR_FONTS);
	init_colors(a);

	a->w_mouse = NULL;
	a->w_key = NULL;
	a->kp_as_mod = true;
	a->controller = NULL;
	num_joysticks = SDL_NumJoysticks();
	for (int i = 0; i < num_joysticks; i++) {
		if (!SDL_IsGameController(i)) continue;
		a->controller = SDL_GameControllerOpen(i);
		/* We support one controller only */
		if (a->controller) break;
	}
}

static bool is_subwindow_loaded(struct my_app *a, unsigned index)
{
	const struct subwindow *subwindow = get_subwindow_direct(a, index);
	assert(subwindow != NULL);

	return subwindow->loaded;
}

static struct subwindow *get_subwindow_direct(struct my_app *a, unsigned index)
{
	size_t i;
	if (index < N_ELEMENTS(a->subwindows)
			&& a->subwindows[index].index == index) {
		i = index;
	} else {
		for (i = 0; i < N_ELEMENTS(a->subwindows); i++) {
			if (a->subwindows[i].index == index) {
				break;
			}
		}
		if (i == N_ELEMENTS(a->subwindows)) {
			return NULL;
		}
	}

	return &a->subwindows[i];
}

static struct subwindow *get_new_subwindow(struct my_app *a, unsigned index)
{
	struct subwindow *subwindow = get_subwindow_direct(a, index);
	assert(subwindow != NULL);
	assert(!subwindow->inited);
	assert(!subwindow->loaded);
	assert(!subwindow->linked);

	if (!wipe_subwindow(subwindow)) {
		return NULL;
	}

	return subwindow;
}

static struct sdlpui_window *get_new_window(struct my_app *a, unsigned index)
{
	assert(index < N_ELEMENTS(a->windows));
	assert(a->windows[index].index == index);

	struct sdlpui_window *window = &a->windows[index];
	assert(!window->inited);
	assert(!window->loaded);

	wipe_window(window, DEFAULT_DISPLAY);

	return window;
}

static struct sdlpui_window *get_window_direct(struct my_app *a, unsigned index)
{
	assert(index < N_ELEMENTS(a->windows));

	if (a->windows[index].loaded) {
		assert(a->windows[index].index == index);
		return &a->windows[index];
	}

	return NULL;
}

static struct sdlpui_window *get_window_by_id(struct my_app *a, Uint32 id)
{
	for (size_t i = 0; i < N_ELEMENTS(a->windows); i++) {
		if (a->windows[i].loaded && a->windows[i].id == id) {
			return &a->windows[i];
		}
	}
	
	return NULL;
}

static void free_globals(struct my_app *a)
{
	for (int i = 0; i < a->font_count; i++) {
		free_font_info(&a->fonts[i]);
	}
	mem_free(a->fonts);
	a->fonts = NULL;
	a->font_count = 0;
	a->font_alloc = 0;
	for (size_t i = 0; i < N_ELEMENTS(a->windows); i++) {
		if (a->windows[i].loaded) {
			free_window(&a->windows[i]);
		}
	}
	for (size_t i = 0; i < N_ELEMENTS(a->subwindows); i++) {
		assert(!a->subwindows[i].inited);
		assert(!a->subwindows[i].loaded);
		assert(!a->subwindows[i].linked);
	}
	if (a->controller) {
		SDL_GameControllerClose(a->controller);
		a->controller = NULL;
	}
}

static void start_windows(struct my_app *a)
{
	int i;

	for (i = (int)N_ELEMENTS(a->windows); i > 0; i--) {
		if (a->windows[i - 1].inited) {
			start_window(&a->windows[i - 1]);
		}
	}
	if (a->print_sdl_details) {
		for (i = 0; i < (int)N_ELEMENTS(a->windows); ++i) {
			if (!a->windows[i].window) continue;
			log_window_details(i, a->windows[i].window,
				a->windows[i].renderer);
		}
	}
}

static void load_terms(struct my_app *a)
{
	for (size_t i = 0; i < N_ELEMENTS(a->subwindows); i++) {
		if (a->subwindows[i].loaded) {
			load_term(&a->subwindows[i]);
		}
	}

	Term_activate(term_screen);
}

/* Config file stuff */

static void dump_config_file(const struct my_app *a)
{
	ang_file *config = file_open(a->config_file, MODE_WRITE, FTYPE_TEXT);

	assert(config != NULL);

	for (size_t i = 0; i < N_ELEMENTS(a->windows); i++) {
		if (a->windows[i].loaded) {
			dump_window(&a->windows[i], config);
		}
	}
	for (size_t i = 0; i < N_ELEMENTS(a->menu_shortcuts); i++) {
		char keypress[1024];

		if (a->menu_shortcuts[i].type == EVT_KBRD
				&& a->menu_shortcuts[i].code) {
			keypress_to_text(keypress, sizeof(keypress),
				a->menu_shortcuts + i, false);
		} else {
			my_strcpy(keypress, "None", sizeof(keypress));
		}
		file_putf(config, "menu-shortcut:%u:%s\n", (unsigned int)i,
			keypress);
	}
	file_putf(config, "kp-as-modifier:%d\n", (a->kp_as_mod) ? 1 : 0);

	file_close(config);
}

static struct sdlpui_window *get_window_from_parser(struct parser *parser)
{
	struct my_app *a = parser_priv(parser);
	unsigned int ind = parser_getuint(parser, "index");

	return (ind >= N_ELEMENTS(a->windows)) ? NULL : &a->windows[ind];
}

static struct subwindow *get_subwindow_from_parser(struct parser *parser)
{
	struct my_app *a = parser_priv(parser);
	unsigned int ind = parser_getuint(parser, "index");

	return (ind >= N_ELEMENTS(a->subwindows)) ? NULL : &a->subwindows[ind];
}

static enum parser_error config_window_display(struct parser *parser)
{
	struct sdlpui_window *window = get_window_from_parser(parser);
	int display = parser_getint(parser, "display");

	if (!window) {
		return PARSE_ERROR_OUT_OF_BOUNDS;
	}

	if (display < 0 || display > SDL_GetNumVideoDisplays()) {
		return PARSE_ERROR_OUT_OF_BOUNDS;
	}

	wipe_window(window, display);

	window->config = mem_zalloc(sizeof(*window->config));

	window->config->window_flags = SDL_WINDOW_RESIZABLE;

	return PARSE_ERROR_NONE;
}

static enum parser_error config_window_fullscreen(struct parser *parser)
{
	struct sdlpui_window *window = get_window_from_parser(parser);

	if (!window) {
		return PARSE_ERROR_OUT_OF_BOUNDS;
	}
	if (!window->inited) {
		return PARSE_ERROR_MISSING_RECORD_HEADER;
	}

	const char *mode = parser_getsym(parser, "fullscreen");
	if (streq(mode, "true")) {
		window->config->window_flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
	} else if (streq(mode, "false")) {
		;
	} else {
		return PARSE_ERROR_INVALID_VALUE;
	}
	return PARSE_ERROR_NONE;
}

static enum parser_error config_window_rect(struct parser *parser)
{
	struct sdlpui_window *window = get_window_from_parser(parser);

	if (!window) {
		return PARSE_ERROR_OUT_OF_BOUNDS;
	}
	if (!window->inited) {
		return PARSE_ERROR_MISSING_RECORD_HEADER;
	}
	/*
	 * Assume not in fullscreen mode for now.  If necessary, swap
	 * full_rect and stored_rect later in start_window().
	 */
	window->full_rect.x = parser_getint(parser, "x");
	window->full_rect.y = parser_getint(parser, "y");
	window->full_rect.w = parser_getint(parser, "w");
	window->full_rect.h = parser_getint(parser, "h");

	return PARSE_ERROR_NONE;
}

static enum parser_error config_window_rect_fs(struct parser *parser)
{
	struct sdlpui_window *window = get_window_from_parser(parser);

	if (!window) {
		return PARSE_ERROR_OUT_OF_BOUNDS;
	}
	if (!window->inited) {
		return PARSE_ERROR_MISSING_RECORD_HEADER;
	}
	/*
	 * Assume not in fullscreen mode for now.  If necessary, swap
	 * full_rect and stored_rect later in start_window().
	 */
	window->stored_rect.x = parser_getint(parser, "x");
	window->stored_rect.y = parser_getint(parser, "y");
	window->stored_rect.w = parser_getint(parser, "w");
	window->stored_rect.h = parser_getint(parser, "h");

	return PARSE_ERROR_NONE;
}

static enum parser_error config_window_renderer(struct parser *parser)
{
	struct sdlpui_window *window = get_window_from_parser(parser);
	const char *type = parser_getsym(parser, "type");

	if (!window) {
		return PARSE_ERROR_OUT_OF_BOUNDS;
	}
	if (!window->inited) {
		return PARSE_ERROR_MISSING_RECORD_HEADER;
	}

	if (streq(type, "hardware")) {
		window->config->renderer_flags = SDL_RENDERER_ACCELERATED;
	} else if (streq(type, "software")) {
		window->config->renderer_flags = SDL_RENDERER_SOFTWARE;
	} else {
		return PARSE_ERROR_INVALID_VALUE;
	}
	
	return PARSE_ERROR_NONE;
}

static enum parser_error config_window_wallpaper_path(struct parser *parser)
{
	struct sdlpui_window *window = get_window_from_parser(parser);
	const char *path = parser_getstr(parser, "path");

	if (!window) {
		return PARSE_ERROR_OUT_OF_BOUNDS;
	}
	if (!window->inited) {
		return PARSE_ERROR_MISSING_RECORD_HEADER;
	}
	if (streq(path, "default")) {
		char buf[4096];
		path_build(buf, sizeof(buf), DEFAULT_WALLPAPER_DIR, DEFAULT_WALLPAPER);

		window->config->wallpaper_path = string_make(buf);
	} else {
		window->config->wallpaper_path = string_make(path);
	}

	return PARSE_ERROR_NONE;
}

static enum parser_error config_window_wallpaper_mode(struct parser *parser)
{
	struct sdlpui_window *window = get_window_from_parser(parser);
	const char *mode = parser_getstr(parser, "mode");

	if (!window) {
		return PARSE_ERROR_OUT_OF_BOUNDS;
	}
	if (!window->inited) {
		return PARSE_ERROR_MISSING_RECORD_HEADER;
	}

	if (streq(mode, "none")) {
		window->wallpaper.mode = WALLPAPER_DONT_SHOW;
	} else if (streq(mode, "tiled")) {
		window->wallpaper.mode = WALLPAPER_TILED;
	} else if (streq(mode, "centered")) {
		window->wallpaper.mode = WALLPAPER_CENTERED;
	} else if (streq(mode, "scaled")) {
		window->wallpaper.mode = WALLPAPER_SCALED;
	} else {
		return PARSE_ERROR_INVALID_VALUE;
	}

	return PARSE_ERROR_NONE;
}

static enum parser_error config_window_font(struct parser *parser)
{
	struct sdlpui_window *window = get_window_from_parser(parser);
	const char *name = parser_getstr(parser, "name");
	int size = parser_getint(parser, "size");

	if (!window) {
		return PARSE_ERROR_OUT_OF_BOUNDS;
	}
	if (!window->inited) {
		return PARSE_ERROR_MISSING_RECORD_HEADER;
	}

	/* Checking whether the font is usable will be done in load_window(). */
	window->config->font_name = string_make(name);
	window->config->font_size = size;

	return PARSE_ERROR_NONE;
}

static enum parser_error config_window_graphics(struct parser *parser)
{
	struct sdlpui_window *window = get_window_from_parser(parser);

	if (!window) {
		return PARSE_ERROR_OUT_OF_BOUNDS;
	}
	if (!window->inited) {
		return PARSE_ERROR_MISSING_RECORD_HEADER;
	}

	int id = parser_getint(parser, "id");

	if (get_graphics_mode(id) == NULL) {
		return PARSE_ERROR_INVALID_VALUE;
	}

	window->graphics.id = id;

	return PARSE_ERROR_NONE;
}

static enum parser_error config_window_tile_scale(struct parser *parser)
{
	struct sdlpui_window *window = get_window_from_parser(parser);

	if (!window) {
		return PARSE_ERROR_OUT_OF_BOUNDS;
	}
	if (!window->inited) {
		return PARSE_ERROR_MISSING_RECORD_HEADER;
	}

	if (window->graphics.id != GRAPHICS_NONE) {
		if (streq(parser_getsym(parser, "which"), "height")) {
			tile_height = parser_getint(parser, "scale");
		} else if (streq(parser_getsym(parser, "which"), "width")) {
			tile_width = parser_getint(parser, "scale");
		} else {
			return PARSE_ERROR_INVALID_OPTION;
		}
	}

	return PARSE_ERROR_NONE;
}

static enum parser_error config_subwindow_window(struct parser *parser)
{
	struct subwindow *subwindow = get_subwindow_from_parser(parser);
	struct my_app *a = parser_priv(parser);
	struct sdlpui_window *window;
	unsigned windex = parser_getuint(parser, "windex");

	if (!subwindow) {
		return PARSE_ERROR_OUT_OF_BOUNDS;
	}

	if (windex >= N_ELEMENTS(a->windows)) {
		return PARSE_ERROR_OUT_OF_BOUNDS;
	}
	window = &a->windows[windex];
	if (!window->inited) {
		return PARSE_ERROR_NON_SEQUENTIAL_RECORDS;
	}
	if (!wipe_subwindow(subwindow)) {
		return PARSE_ERROR_GENERIC;
	}
	subwindow->config = mem_zalloc(sizeof(*subwindow->config));
	/*
	 * Old versions only wrote visible subwindows to the configuration
	 * file and did not append the visibility status to the subwindow-window
	 * directive.
	 */
	if (parser_hasval(parser, "vis")) {
		subwindow->visible = (parser_getint(parser, "vis") != 0);
	}
	subwindow->window = window;
	attach_subwindow_to_window(window, subwindow);

	return PARSE_ERROR_NONE;
}

static enum parser_error config_subwindow_rect(struct parser *parser)
{
	struct subwindow *subwindow = get_subwindow_from_parser(parser);

	if (!subwindow) {
		return PARSE_ERROR_OUT_OF_BOUNDS;
	}
	if (!subwindow->inited) {
		return PARSE_ERROR_MISSING_RECORD_HEADER;
	}

	/*
	 * Assume not in fullscreen mode for now.  If necessary, swap
	 * full_rect and stored_rect later in load_subwindow().
	 */
	subwindow->full_rect.x = parser_getint(parser, "x");
	subwindow->full_rect.y = parser_getint(parser, "y");
	subwindow->full_rect.w = parser_getint(parser, "w");
	subwindow->full_rect.h = parser_getint(parser, "h");

	return PARSE_ERROR_NONE;
}

static enum parser_error config_subwindow_rect_fs(struct parser *parser)
{
	struct subwindow *subwindow = get_subwindow_from_parser(parser);

	if (!subwindow) {
		return PARSE_ERROR_OUT_OF_BOUNDS;
	}
	if (!subwindow->inited) {
		return PARSE_ERROR_MISSING_RECORD_HEADER;
	}

	/*
	 * Assume not in fullscreen mode for now.  If necessary, swap
	 * full_rect and stored_rect later in load_subwindow().
	 */
	subwindow->stored_rect.x = parser_getint(parser, "x");
	subwindow->stored_rect.y = parser_getint(parser, "y");
	subwindow->stored_rect.w = parser_getint(parser, "w");
	subwindow->stored_rect.h = parser_getint(parser, "h");

	return PARSE_ERROR_NONE;
}

static enum parser_error config_subwindow_font(struct parser *parser)
{
	struct subwindow *subwindow = get_subwindow_from_parser(parser);
	const char *name = parser_getstr(parser, "name");
	int size = parser_getint(parser, "size");

	if (!subwindow) {
		return PARSE_ERROR_OUT_OF_BOUNDS;
	}
	if (!subwindow->inited) {
		return PARSE_ERROR_MISSING_RECORD_HEADER;
	}

	/*
	 * Checking whether the font is usable will be done in load_subwindow().
	 */
	subwindow->config->font_name = string_make(name);
	subwindow->config->font_size = size;

	return PARSE_ERROR_NONE;
}

static enum parser_error config_subwindow_borders(struct parser *parser)
{
	struct subwindow *subwindow = get_subwindow_from_parser(parser);
	const char *borders = parser_getsym(parser, "borders");

	if (!subwindow) {
		return PARSE_ERROR_OUT_OF_BOUNDS;
	}
	if (!subwindow->inited) {
		return PARSE_ERROR_MISSING_RECORD_HEADER;
	}

	if (streq(borders, "true")) {
		subwindow->borders.visible = true;
	} else if (streq(borders, "false")) {
		subwindow->borders.visible = false;
	} else {
		return PARSE_ERROR_INVALID_VALUE;
	}

	return PARSE_ERROR_NONE;
}

static enum parser_error config_subwindow_top(struct parser *parser)
{
	struct subwindow *subwindow = get_subwindow_from_parser(parser);
	const char *top = parser_getsym(parser, "top");
	const char *always = parser_getsym(parser, "always");

	if (!subwindow) {
		return PARSE_ERROR_OUT_OF_BOUNDS;
	}
	if (!subwindow->inited) {
		return PARSE_ERROR_MISSING_RECORD_HEADER;
	}

	if (streq(top, "true")) {
		subwindow->top = true;
	} else if (streq(top, "false")) {
		subwindow->top = false;
	} else {
		return PARSE_ERROR_INVALID_VALUE;
	}

	if (streq(always, "true")) {
		subwindow->always_top = true;
	} else if (streq(always, "false")) {
		subwindow->always_top = false;
	} else {
		return PARSE_ERROR_INVALID_VALUE;
	}

	return PARSE_ERROR_NONE;
}

static enum parser_error config_subwindow_alpha(struct parser *parser)
{
	struct subwindow *subwindow = get_subwindow_from_parser(parser);
	int alpha = parser_getint(parser, "alpha");

	if (!subwindow) {
		return PARSE_ERROR_OUT_OF_BOUNDS;
	}
	if (!subwindow->inited) {
		return PARSE_ERROR_MISSING_RECORD_HEADER;
	}

	if (alpha < 0 || alpha > DEFAULT_ALPHA_FULL) {
		return PARSE_ERROR_INVALID_VALUE;
	}

	subwindow->color.a = alpha;

	return PARSE_ERROR_NONE;
}

static enum parser_error config_menu_shortcut(struct parser *parser)
{
	struct my_app *a = parser_priv(parser);
	unsigned int ind = parser_getuint(parser, "index");
	const char *keypress_str = parser_getstr(parser, "keypress");

	if (ind >= MAX_WINDOWS) {
		return PARSE_ERROR_OUT_OF_BOUNDS;
	}
	if (!*keypress_str || streq(keypress_str, "None")) {
		a->menu_shortcuts[ind] = KEYPRESS_NULL;
	} else {
		struct keypress buf[2];

		buf[0].type = EVT_NONE;
		buf[1].type = EVT_NONE;
		keypress_from_text(buf, N_ELEMENTS(buf), keypress_str);
		if (buf[0].type == EVT_KBRD && buf[1].type == EVT_NONE) {
			/* Got exactly one keystroke. */
			a->menu_shortcuts[ind] = buf[0];
		} else {
			/* Treat anything else as no shortcut. */
			a->menu_shortcuts[ind] = KEYPRESS_NULL;
		}
	}
	return PARSE_ERROR_NONE;
}

static enum parser_error config_kp_as_mod(struct parser *parser)
{
	struct my_app *a = parser_priv(parser);

	a->kp_as_mod = (parser_getint(parser, "enabled") != 0);
	return PARSE_ERROR_NONE;
}

static struct parser *init_parse_config(struct my_app *a)
{
	struct parser *parser = parser_new();
	
	parser_reg(parser, "window-display uint index int display",
			config_window_display);
	parser_reg(parser, "window-fullscreen uint index sym fullscreen",
			config_window_fullscreen);
	parser_reg(parser, "window-full-rect uint index int x int y int w "
			"int h", config_window_rect);
	parser_reg(parser, "window-full-rect-fs uint index int x int y int w "
			"int h", config_window_rect_fs);
	parser_reg(parser, "window-renderer uint index sym type",
			config_window_renderer);
	parser_reg(parser, "window-wallpaper-path uint index str path",
			config_window_wallpaper_path);
	parser_reg(parser, "window-wallpaper-mode uint index str mode",
			config_window_wallpaper_mode);
	parser_reg(parser, "window-status-bar-font uint index int size str name",
			config_window_font);
	parser_reg(parser, "window-graphics-id uint index int id",
			config_window_graphics);
	parser_reg(parser, "window-tile-scale uint index sym which int scale",
			config_window_tile_scale);

	parser_reg(parser, "subwindow-window uint index uint windex ?int vis",
			config_subwindow_window);
	parser_reg(parser, "subwindow-full-rect uint index int x int y "
			"int w int h", config_subwindow_rect);
	parser_reg(parser, "subwindow-full-rect-fs uint index int x int y "
			"int w int h", config_subwindow_rect_fs);
	parser_reg(parser, "subwindow-font uint index int size str name",
			config_subwindow_font);
	parser_reg(parser, "subwindow-borders uint index sym borders",
			config_subwindow_borders);
	parser_reg(parser, "subwindow-top uint index sym top sym always",
			config_subwindow_top);
	parser_reg(parser, "subwindow-alpha uint index int alpha",
			config_subwindow_alpha);

	parser_reg(parser, "menu-shortcut uint index str keypress",
			config_menu_shortcut);
	parser_reg(parser, "kp-as-modifier int enabled", config_kp_as_mod);

	parser_setpriv(parser, a);

	return parser;
}

static void print_error(const char *name, struct parser *parser)
{
	struct parser_state state;
	parser_getstate(parser, &state);

	fprintf(stderr, "parse error in %s line %d column %d: %s: %s\n",
			name,
			state.line,
			state.col,
			state.msg,
			parser_error_str[state.error]);
}

static bool read_config_file(struct my_app *a)
{
	char line[1024];
	ang_file *config = file_open(a->config_file, MODE_READ, FTYPE_TEXT);
	struct parser *parser;
	errr error = PARSE_ERROR_NONE;

	if (config == NULL) {
		/* not an error, its ok for a config file to not exist */
		return false;
	}

	parser = init_parse_config(a);

	while (file_getl(config, line, sizeof(line))) {
		error = parser_parse(parser, line);
		if (error != PARSE_ERROR_NONE) {
			print_error(a->config_file, parser);
			break;
		}
	}

	parser_destroy(parser);
	file_close(config);

	return error == PARSE_ERROR_NONE;
}

#endif /* USE_SDL2 */
