commit b44497a0e20b249923f01284f85163b61b7e7609
parent 210aa3d51d8dcc1e36c73d3404e032937271e0d6
Author: lumidify <nobody@lumidify.org>
Date:   Thu,  2 May 2024 16:10:04 +0200
Move theme config to main config file
This is also still really ugly.
Diffstat:
26 files changed, 584 insertions(+), 1097 deletions(-)
diff --git a/LICENSE b/LICENSE
@@ -1,4 +1,4 @@
-See src/ltkd/khash.h, src/ltk/ini.*, src/ltk/stb_truetype.*, src/ltk/strtonum.c,
+See src/ltkd/khash.h, src/ltk/stb_truetype.*, src/ltk/num.c,
 src/ltk/ctrlsel.*, and src/ltk/macros.h for third-party licenses.
 
 ISC License
diff --git a/Makefile b/Makefile
@@ -56,9 +56,7 @@ OBJ_LTK = \
 	src/ltk/rect.o \
 	src/ltk/widget.o \
 	src/ltk/ltk.o \
-	src/ltk/ini.o \
 	src/ltk/button.o \
-	src/ltk/theme.o \
 	src/ltk/graphics_xlib.o \
 	src/ltk/surface_cache.o \
 	src/ltk/event_xlib.o \
@@ -97,7 +95,6 @@ OBJ_TEST = examples/ltk/test.o
 HDR_LTK = \
 	src/ltk/button.h \
 	src/ltk/color.h \
-	src/ltk/ini.h \
 	src/ltk/label.h \
 	src/ltk/rect.h \
 	src/ltk/widget.h \
@@ -107,7 +104,7 @@ HDR_LTK = \
 	src/ltk/stb_truetype.h \
 	src/ltk/text.h \
 	src/ltk/util.h \
-	src/ltk/theme.h \
+	src/ltk/widget_internal.h \
 	src/ltk/graphics.h \
 	src/ltk/surface_cache.h \
 	src/ltk/macros.h \
diff --git a/config.example/ltk.cfg b/config.example/ltk.cfg
@@ -8,6 +8,38 @@ dpi-scale = 1.0
 # In future:
 # text-editor = ...
 
+[theme:window]
+font-size = 12pt
+bg = "#000000"
+fg = "#FFFFFF"
+font = "Liberation Mono"
+
+[theme:button]
+border-width = 0.5mm
+text-color = "#FFFFFF"
+pad = 1mm
+border = "#339999"
+fill = "#113355"
+border-pressed = "#FFFFFF"
+fill-pressed = "#113355"
+border-active = "#FFFFFF"
+fill-active = "#738194"
+border-disabled = "#FFFFFF"
+fill-disabled = "#292929"
+
+[theme:label]
+text-color = "#FFFFFF"
+pad = 1mm
+
+[theme:scrollbar]
+size = 3.5mm
+bg = "#000000"
+bg-disabled = "#555555"
+fg = "#113355"
+fg-pressed = "#113355"
+fg-active = "#738194"
+fg-disabled = "#292929"
+
 [key-binding:window]
 # In future:
 # bind edit-text-external ...
diff --git a/config.example/theme.ini b/config.example/theme.ini
@@ -1,31 +0,0 @@
-[window]
-font-size = 12pt
-bg = #000000
-fg = #FFFFFF
-font = Liberation Mono
-
-[button]
-border-width = 0.5mm
-text-color = #FFFFFF
-pad = 1mm
-border = #339999
-fill = #113355
-border-pressed = #FFFFFF
-fill-pressed = #113355
-border-active = #FFFFFF
-fill-active = #738194
-border-disabled = #FFFFFF
-fill-disabled = #292929
-
-[label]
-text-color = #FFFFFF
-pad = 1mm
-
-[scrollbar]
-size = 3.5mm
-bg = #000000
-bg-disabled = #555555
-fg = #113355
-fg-pressed = #113355
-fg-active = #738194
-fg-disabled = #292929
diff --git a/src/ltk/button.c b/src/ltk/button.c
@@ -24,7 +24,6 @@
 #include "memory.h"
 #include "rect.h"
 #include "text.h"
-#include "theme.h"
 #include "util.h"
 #include "widget.h"
 
@@ -97,21 +96,11 @@ static ltk_theme_parseinfo parseinfo[] = {
 	{"pad", THEME_SIZE, {.size = &theme.pad}, {.size = {.val = 100, .unit = LTK_UNIT_MM}}, 0, MAX_BUTTON_PADDING, 0},
 	{"text-color", THEME_COLOR, {.color = &theme.text_color}, {.color = "#FFFFFF"}, 0, 0, 0},
 };
-static int parseinfo_sorted = 0;
-
-int
-ltk_button_ini_handler(ltk_renderdata *data, const char *prop, const char *value) {
-	return ltk_theme_handle_value(data, "button", prop, value, parseinfo, LENGTH(parseinfo), &parseinfo_sorted);
-}
-
-int
-ltk_button_fill_theme_defaults(ltk_renderdata *data) {
-	return ltk_theme_fill_defaults(data, "button", parseinfo, LENGTH(parseinfo));
-}
 
 void
-ltk_button_uninitialize_theme(ltk_renderdata *data) {
-	ltk_theme_uninitialize(data, parseinfo, LENGTH(parseinfo));
+ltk_button_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len) {
+	*p = parseinfo;
+	*len = LENGTH(parseinfo);
 }
 
 static void
diff --git a/src/ltk/button.h b/src/ltk/button.h
@@ -30,10 +30,6 @@ typedef struct {
 	ltk_text_line *tl;
 } ltk_button;
 
-int ltk_button_ini_handler(ltk_renderdata *data, const char *prop, const char *value);
-int ltk_button_fill_theme_defaults(ltk_renderdata *data);
-void ltk_button_uninitialize_theme(ltk_renderdata *data);
-
 ltk_button *ltk_button_create(ltk_window *window, char *text);
 
 #endif /* LTK_BUTTON_H */
diff --git a/src/ltk/config.c b/src/ltk/config.c
@@ -24,15 +24,38 @@
 #include "memory.h"
 #include "config.h"
 #include "sort_search.h"
-
-#include "entry.h"
-#include "window.h"
+#include "widget_internal.h"
 
 GEN_SORT_SEARCH_HELPERS(keybinding, ltk_keybinding_cb, text)
-//GEN_SORT_SEARCH_HELPERS(theme, ltk_theme_parseinfo, key)
+GEN_SORT_SEARCH_HELPERS(theme, ltk_theme_parseinfo, key)
 LTK_ARRAY_INIT_IMPL(keypress, ltk_keypress_cfg)
 LTK_ARRAY_INIT_IMPL(keyrelease, ltk_keyrelease_cfg)
 
+static ltk_general_config general_config;
+static ltk_language_mapping *mappings = NULL;
+static size_t mappings_alloc = 0, mappings_len = 0;
+
+static ltk_theme_parseinfo general_parseinfo[] = {
+	{"line-editor", THEME_STRING, {.str = &general_config.line_editor}, {.str = NULL}, 0, 0, 0},
+	{"dpi-scale", THEME_DOUBLE, {.d = &general_config.dpi_scale}, {.d = 1.0}, 10, 10000, 0},
+	{"explicit-focus", THEME_BOOL, {.b = &general_config.explicit_focus}, {.b = 0}, 0, 0, 0},
+	{"all-activatable", THEME_BOOL, {.b = &general_config.all_activatable}, {.b = 0}, 0, 0, 0},
+	{"fixed-dpi", THEME_DOUBLE, {.d = &general_config.fixed_dpi}, {.d = 96.0}, 100, 400000, 0},
+	/* FIXME: warning if set to true but xrandr not enabled */
+#if USE_XRANDR
+	{"mixed-dpi", THEME_BOOL, {.b = &general_config.mixed_dpi}, {.b = 1}, 0, 0, 0},
+#else
+	{"mixed-dpi", THEME_BOOL, {.b = &general_config.mixed_dpi}, {.b = 0}, 0, 0, 0},
+#endif
+};
+
+/* just to use the same interface for all theme sections */
+static void
+ltk_general_get_theme_parseinfo(ltk_theme_parseinfo **parseinfo, size_t *len) {
+	*parseinfo = general_parseinfo;
+	*len = LENGTH(general_parseinfo);
+}
+
 static struct {
 	const char *name;
 	void (*get_parseinfo)(
@@ -45,6 +68,232 @@ static struct {
 	{"window", <k_window_get_keybinding_parseinfo},
 };
 
+static struct theme_handlerinfo {
+	const char *name;
+	void (*get_parseinfo)(ltk_theme_parseinfo **parseinfo, size_t *len);
+	const char *parent;
+} theme_handlers[] = {
+	{"general", <k_general_get_theme_parseinfo, NULL},
+	{"theme:window", <k_window_get_theme_parseinfo, NULL},
+	{"theme:button", <k_button_get_theme_parseinfo, "window"},
+	{"theme:entry", <k_entry_get_theme_parseinfo, "window"},
+	{"theme:label", <k_label_get_theme_parseinfo, "window"},
+	{"theme:scrollbar", <k_scrollbar_get_theme_parseinfo, "window"},
+	{"theme:menu", <k_menu_get_theme_parseinfo, "window"},
+	{"theme:menuentry", <k_menuentry_get_theme_parseinfo, "window"},
+	{"theme:submenu", <k_submenu_get_theme_parseinfo, "window"},
+	{"theme:submenuentry", <k_submenuentry_get_theme_parseinfo, "window"},
+};
+
+GEN_SORT_SEARCH_HELPERS(themehandler, struct theme_handlerinfo, name)
+
+static void
+sort_themehandlers(void) {
+	ltk_theme_parseinfo *parseinfo;
+	size_t len;
+	themehandler_sort(theme_handlers, LENGTH(theme_handlers));
+	for (size_t i = 0; i < LENGTH(theme_handlers); i++) {
+		theme_handlers[i].get_parseinfo(&parseinfo, &len);
+		theme_sort(parseinfo, len);
+	}
+}
+
+/* FIXME: handle '#' or no '#' in color specification */
+static int
+handle_theme_setting(ltk_renderdata *renderdata, ltk_theme_parseinfo *entry, const char *value) {
+	const char *errstr = NULL;
+	char *endptr = NULL;
+	/* FIXME: better warnings */
+	long long ll;
+	switch (entry->type) {
+	case THEME_INT:
+		if (entry->max > INT_MAX)
+			entry->max = INT_MAX;
+		if (entry->min < INT_MIN)
+			entry->min = INT_MIN;
+		*(entry->ptr.i) = ltk_strtonum(value, entry->min, entry->max, &errstr);
+		if (errstr)
+			return 1;
+		entry->initialized = 1;
+		break;
+	case THEME_DOUBLE:
+		/* FIXME: maybe overflow prevention here as well */
+		ll = ltk_strtoscalednum(value, entry->min, entry->max, &endptr, &errstr);
+		if (errstr || *endptr != '\0')
+			return 1;
+		*(entry->ptr.d) = ll / 100.0;
+		entry->initialized = 1;
+		break;
+	case THEME_UINT:
+		if (entry->max > INT_MAX)
+			entry->max = INT_MAX;
+		if (entry->min < 0)
+			entry->min = 0;
+		*(entry->ptr.u) = ltk_strtonum(value, entry->min, entry->max, &errstr);
+		if (errstr)
+			return 1;
+		entry->initialized = 1;
+		break;
+	case THEME_SIZE:
+		if (entry->max > INT_MAX)
+			entry->max = INT_MAX;
+		if (entry->min < INT_MIN)
+			entry->min = INT_MIN;
+		entry->ptr.size->unit = LTK_UNIT_PX;
+		entry->ptr.size->val = ltk_strtoscalednum(value, entry->min, entry->max, &endptr, &errstr);
+		if (errstr)
+			return 1;
+		if (*endptr == '\0') {
+			/* NOP */
+		} else if (!strcmp(endptr, "px")) {
+			entry->ptr.size->unit = LTK_UNIT_PX;
+		} else if (!strcmp(endptr, "pt")) {
+			entry->ptr.size->unit = LTK_UNIT_PT;
+		} else if (!strcmp(endptr, "mm")) {
+			entry->ptr.size->unit = LTK_UNIT_MM;
+		} else {
+			return 1;
+		}
+		entry->initialized = 1;
+		break;
+	case THEME_STRING:
+		*(entry->ptr.str) = ltk_strdup(value);
+		entry->initialized = 1;
+		break;
+	case THEME_COLOR:
+		/* FIXME: warning message possibly misleading because this can fail for reasons
+		   other than an invalid color specification */
+		if (!(*(entry->ptr.color) = ltk_color_create(renderdata, value)))
+			return 1;
+		entry->initialized = 1;
+		break;
+	case THEME_BOOL:
+		if (strcmp(value, "true") == 0) {
+			*(entry->ptr.b) = 1;
+		} else if (strcmp(value, "false") == 0) {
+			*(entry->ptr.b) = 0;
+		} else {
+			return 1;
+		}
+		entry->initialized = 1;
+		break;
+	case THEME_BORDERSIDES:
+		*(entry->ptr.border) = LTK_BORDER_NONE;
+		for (const char *c = value; *c != '\0'; c++) {
+			switch (*c) {
+			case 't':
+				*(entry->ptr.border) |= LTK_BORDER_TOP;
+				break;
+			case 'b':
+				*(entry->ptr.border) |= LTK_BORDER_BOTTOM;
+				break;
+			case 'l':
+				*(entry->ptr.border) |= LTK_BORDER_LEFT;
+				break;
+			case 'r':
+				*(entry->ptr.border) |= LTK_BORDER_RIGHT;
+				break;
+			default:
+				return 1;
+			}
+		}
+		entry->initialized = 1;
+		break;
+	default:
+		ltk_fatal("Invalid theme setting type. This should not happen.\n");
+	}
+	return 0;
+}
+
+static int
+fill_theme_defaults(ltk_renderdata *renderdata) {
+	ltk_theme_parseinfo *parseinfo;
+	size_t len;
+	for (size_t j = 0; j < LENGTH(theme_handlers); j++) {
+		theme_handlers[j].get_parseinfo(&parseinfo, &len);
+		for (size_t i = 0; i < len; i++) {
+			ltk_theme_parseinfo *e = &parseinfo[i];
+			if (e->initialized)
+				continue;
+			switch (e->type) {
+			case THEME_INT:
+				*(e->ptr.i) = e->defaultval.i;
+				e->initialized = 1;
+				break;
+			case THEME_UINT:
+				*(e->ptr.u) = e->defaultval.u;
+				e->initialized = 1;
+				break;
+			case THEME_DOUBLE:
+				*(e->ptr.d) = e->defaultval.d;
+				e->initialized = 1;
+				break;
+			case THEME_SIZE:
+				*(e->ptr.size) = e->defaultval.size;
+				e->initialized = 1;
+				break;
+			case THEME_STRING:
+				if (e->defaultval.str)
+					*(e->ptr.str) = ltk_strdup(e->defaultval.str);
+				else
+					*(e->ptr.str) = NULL;
+				e->initialized = 1;
+				break;
+			case THEME_COLOR:
+				if (!(*(e->ptr.color) = ltk_color_create(renderdata, e->defaultval.color)))
+					return 1;
+				e->initialized = 1;
+				break;
+			case THEME_BOOL:
+				*(e->ptr.b) = e->defaultval.b;
+				e->initialized = 1;
+				break;
+			case THEME_BORDERSIDES:
+				*(e->ptr.border) = e->defaultval.border;
+				e->initialized = 1;
+				break;
+			default:
+				ltk_fatal("Invalid theme setting type. This should not happen.\n");
+			}
+		}
+	}
+	return 0;
+}
+
+static void
+uninitialize_theme(ltk_renderdata *renderdata) {
+	ltk_theme_parseinfo *parseinfo;
+	size_t len;
+	for (size_t j = 0; j < LENGTH(theme_handlers); j++) {
+		theme_handlers[j].get_parseinfo(&parseinfo, &len);
+		for (size_t i = 0; i < len; i++) {
+			ltk_theme_parseinfo *e = &parseinfo[i];
+			if (!e->initialized)
+				continue;
+			switch (e->type) {
+			case THEME_STRING:
+				ltk_free(*(e->ptr.str));
+				e->initialized = 0;
+				break;
+			case THEME_COLOR:
+				ltk_color_destroy(renderdata, *(e->ptr.color));
+				e->initialized = 0;
+				break;
+			case THEME_SIZE:
+			case THEME_INT:
+			case THEME_UINT:
+			case THEME_BOOL:
+			case THEME_BORDERSIDES:
+			case THEME_DOUBLE:
+				e->initialized = 0;
+				break;
+			default:
+				ltk_fatal("Invalid theme setting type. This should not happen.\n");
+			}
+		}
+	}
+}
+
 static int
 register_keypress(ltk_array(keypress) *bindings, ltk_keybinding_cb *arr, size_t arrlen, const char *func_name, size_t func_len, ltk_keypress_binding b) {
 	ltk_keybinding_cb *cb = keybinding_get_entry(arr, arrlen, func_name, func_len);
@@ -139,8 +388,6 @@ ltk_keyrelease_bindings_destroy(ltk_array(keyrelease) *arr) {
 static void sort_keysyms(void);
 static int parse_keysym(char *text, size_t len, ltk_keysym *sym_ret);
 
-ltk_config *global_config = NULL;
-
 enum toktype {
 	STRING,
 	SECTION,
@@ -524,23 +771,23 @@ error:
 }
 
 static void
-push_lang_mapping(ltk_config *c) {
-	if (c->mappings_alloc == c->mappings_len) {
-		c->mappings_alloc = ideal_array_size(c->mappings_alloc, c->mappings_len + 1);
-		c->mappings = ltk_reallocarray(c->mappings, c->mappings_alloc, sizeof(ltk_language_mapping));
+push_lang_mapping(void) {
+	if (mappings_alloc == mappings_len) {
+		mappings_alloc = ideal_array_size(mappings_alloc, mappings_len + 1);
+		mappings = ltk_reallocarray(mappings, mappings_alloc, sizeof(ltk_language_mapping));
 	}
-	c->mappings[c->mappings_len].lang = NULL;
-	c->mappings[c->mappings_len].mappings = NULL;
-	c->mappings[c->mappings_len].mappings_alloc = 0;
-	c->mappings[c->mappings_len].mappings_len = 0;
-	c->mappings_len++;
+	mappings[mappings_len].lang = NULL;
+	mappings[mappings_len].mappings = NULL;
+	mappings[mappings_len].mappings_alloc = 0;
+	mappings[mappings_len].mappings_len = 0;
+	mappings_len++;
 }
 
 static void
-push_text_mapping(ltk_config *c, char *text1, size_t len1, char *text2, size_t len2) {
-	if (c->mappings_len == 0)
+push_text_mapping(char *text1, size_t len1, char *text2, size_t len2) {
+	if (mappings_len == 0)
 		return; /* I guess just fail silently... */
-	ltk_language_mapping *m = &c->mappings[c->mappings_len - 1];
+	ltk_language_mapping *m = &mappings[mappings_len - 1];
 	if (m->mappings_alloc == m->mappings_len) {
 		m->mappings_alloc = ideal_array_size(m->mappings_alloc, m->mappings_len + 1);
 		m->mappings = ltk_reallocarray(m->mappings, m->mappings_alloc, sizeof(ltk_keytext_mapping));
@@ -550,39 +797,36 @@ push_text_mapping(ltk_config *c, char *text1, size_t len1, char *text2, size_t l
 	m->mappings_len++;
 }
 
-static void
-destroy_config(ltk_config *c) {
-	for (size_t i = 0; i < c->mappings_len; i++) {
-		ltk_free(c->mappings[i].lang);
-		for (size_t j = 0; j < c->mappings[i].mappings_len; j++) {
-			ltk_free(c->mappings[i].mappings[j].from);
-			ltk_free(c->mappings[i].mappings[j].to);
+void
+ltk_config_cleanup(ltk_renderdata *renderdata) {
+	if (mappings) {
+		for (size_t i = 0; i < mappings_len; i++) {
+			ltk_free(mappings[i].lang);
+			for (size_t j = 0; j < mappings[i].mappings_len; j++) {
+				ltk_free(mappings[i].mappings[j].from);
+				ltk_free(mappings[i].mappings[j].to);
+			}
+			ltk_free(mappings[i].mappings);
 		}
-		ltk_free(c->mappings[i].mappings);
+		ltk_free(mappings);
+		mappings = NULL;
+		mappings_len = mappings_alloc = 0;
 	}
-	ltk_free(c->general.line_editor);
-	ltk_free(c->mappings);
-	ltk_free(c);
+	uninitialize_theme(renderdata);
 }
 
-void
-ltk_config_cleanup(void) {
-	if (global_config)
-		destroy_config(global_config);
-	global_config = NULL;
-}
-
-ltk_config *
-ltk_config_get(void) {
-	return global_config;
+/* FIXME: error if not initialized */
+ltk_general_config *
+ltk_config_get_general(void) {
+	return &general_config;
 }
 
 int
 ltk_config_get_language_index(char *lang, size_t *idx_ret) {
-	if (!global_config)
+	if (!mappings)
 		return 1;
-	for (size_t i = 0; i < global_config->mappings_len; i++) {
-		if (!strcmp(lang, global_config->mappings[i].lang)) {
+	for (size_t i = 0; i < mappings_len; i++) {
+		if (!strcmp(lang, mappings[i].lang)) {
 			*idx_ret = i;
 			return 0;
 		}
@@ -592,9 +836,9 @@ ltk_config_get_language_index(char *lang, size_t *idx_ret) {
 
 ltk_language_mapping *
 ltk_config_get_language_mapping(size_t idx) {
-	if (!global_config || idx >= global_config->mappings_len)
+	if (idx >= mappings_len)
 		return NULL;
-	return &global_config->mappings[idx];
+	return &mappings[idx];
 }
 
 int
@@ -605,33 +849,32 @@ str_array_prefix(const char *str, const char *ar, size_t len) {
 	return !strncmp(str, ar, slen);
 }
 
+/* FIXME: The current model is kind of weird because most parts of the config
+   are stored in the other object files. This makes it difficult to support
+   reloading of the config since the old config needs to be kept until the
+   new config has been successfully loaded, but the parseinfos include direct
+   pointers to the config. It might be better to just have one huge config
+   struct that includes everything. That would also make it a bit clearer when
+   something hasn't been initialized yet. */
 /* WARNING: errstr must be freed! */
 /* FIXME: make ltk_load_file give size_t; handle errors there (copy from ledit) */
 static int
 load_from_text(
+    ltk_renderdata *renderdata,
     const char *filename,
     char *file_contents,
     size_t len,
     char **errstr) {
-	ltk_config *config = ltk_malloc(sizeof(ltk_config));
-	config->mappings = NULL;
-	config->mappings_alloc = config->mappings_len = 0;
-	config->general.explicit_focus = 0;
-	config->general.all_activatable = 0;
-	config->general.line_editor = NULL;
-	config->general.dpi_scale = 1.0;
-	config->general.fixed_dpi = 480; /* 5 * 96 */
-#if USE_XRANDR
-	config->general.mixed_dpi = 1;
-#else
-	config->general.mixed_dpi = 0;
-#endif
+	sort_keysyms();
+	sort_keybindings();
+	sort_themehandlers();
 
 	struct lexstate s = {filename, file_contents, len, 0, 1, 0};
 	struct token tok = next_token(&s);
 	int start_of_line = 1;
 	char *msg = NULL;
 	struct token secttok;
+	txtbuf *themeval = txtbuf_new();
 	while (tok.type != END) {
 		switch (tok.type) {
 		case SECTION:
@@ -645,94 +888,7 @@ load_from_text(
 				msg = "Section must be alone on line";
 				goto error;
 			}
-			/* FIXME: generalize (at least once more options are added) */
-			if (str_array_equal("general", secttok.text, secttok.len)) {
-				struct token prev1tok, prev2tok;
-				while (1) {
-					tok = next_token(&s);
-					if (tok.type == SECTION || tok.type == END)
-						break;
-					else if (tok.type == NEWLINE)
-						continue;
-					prev2tok = tok;
-					tok = next_token(&s);
-					prev1tok = tok;
-					tok = next_token(&s);
-					if (prev2tok.type != STRING || prev1tok.type != EQUALS || tok.type != STRING) {
-						msg = "Invalid assignment statement";
-						goto error;
-					}
-					if (str_array_equal("explicit-focus", prev2tok.text, prev2tok.len)) {
-						if (str_array_equal("true", tok.text, tok.len)) {
-							config->general.explicit_focus = 1;
-						} else if (str_array_equal("false", tok.text, tok.len)) {
-							config->general.explicit_focus = 0;
-						} else {
-							msg = "Invalid boolean setting";
-							goto error;
-						}
-					} else if (str_array_equal("all-activatable", prev2tok.text, prev2tok.len)) {
-						if (str_array_equal("true", tok.text, tok.len)) {
-							config->general.all_activatable = 1;
-						} else if (str_array_equal("false", tok.text, tok.len)) {
-							config->general.all_activatable = 0;
-						} else {
-							msg = "Invalid boolean setting";
-							goto error;
-						}
-					/* FIXME: warning if set to true but xrandr not enabled */
-					} else if (str_array_equal("mixed-dpi", prev2tok.text, prev2tok.len)) {
-						if (str_array_equal("true", tok.text, tok.len)) {
-							config->general.mixed_dpi = 1;
-						} else if (str_array_equal("false", tok.text, tok.len)) {
-							config->general.mixed_dpi = 0;
-						} else {
-							msg = "Invalid boolean setting";
-							goto error;
-						}
-					} else if (str_array_equal("line-editor", prev2tok.text, prev2tok.len)) {
-						config->general.line_editor = ltk_strndup(tok.text, tok.len);
-					} else if (str_array_equal("fixed-dpi", prev2tok.text, prev2tok.len)) {
-						/* FIXME: remove this allocation! */
-						char *tmp = ltk_strndup(tok.text, tok.len);
-						/* FIXME: proper min/max values for dpi */
-						const char *tmp_err = NULL;
-						config->general.fixed_dpi = ltk_strtonum(tmp, 10, 4000, &tmp_err);
-						ltk_free(tmp);
-						if (tmp_err) {
-							msg = "Invalid DPI setting";
-							goto error;
-						}
-						/* because of weird scaling that is currently used, see event_xlib.c */
-						config->general.fixed_dpi *= 5;
-					} else if (str_array_equal("dpi-scale", prev2tok.text, prev2tok.len)) {
-						/* FIXME: remove this allocation! */
-						char *tmp = ltk_strndup(tok.text, tok.len);
-						const char *tmp_err = NULL;
-						char *ep = NULL;
-						/* FIXME: proper min/max values for scale */
-						config->general.dpi_scale = ltk_strtoscalednum(tmp, 10, 10000, &ep, &tmp_err);
-						char c = *ep;
-						ltk_free(tmp);
-						if (tmp_err || c != '\0') {
-							msg = "Invalid DPI scale setting";
-							goto error;
-						}
-						config->general.dpi_scale /= 100;
-					} else {
-						msg = "Invalid setting";
-						goto error;
-					}
-					tok = next_token(&s);
-					if (tok.type == END) {
-						break;
-					} else if (tok.type != NEWLINE) {
-						msg = "Invalid assignment statement";
-						goto error;
-					}
-					start_of_line = 1;
-				}
-			} else if (str_array_prefix("key-binding:", secttok.text, secttok.len)) {
+			if (str_array_prefix("key-binding:", secttok.text, secttok.len)) {
 				int ret = 0;
 				char *widget = secttok.text + strlen("key-binding:");
 				size_t len = secttok.len - strlen("key-binding:");
@@ -746,7 +902,7 @@ load_from_text(
 				}
 			} else if (str_array_equal("key-mapping", secttok.text, secttok.len)) {
 				int lang_init = 0;
-				push_lang_mapping(config);
+				push_lang_mapping();
 				struct token prev1tok, prev2tok;
 				while (1) {
 					tok = next_token(&s);
@@ -770,14 +926,14 @@ load_from_text(
 							msg = "Language already set";
 							goto error;
 						}
-						config->mappings[config->mappings_len - 1].lang = ltk_strndup(tok.text, tok.len);
+						mappings[mappings_len - 1].lang = ltk_strndup(tok.text, tok.len);
 						lang_init = 1;
 					} else if (str_array_equal("map", prev2tok.text, prev2tok.len)) {
 						if (prev1tok.type != STRING || tok.type != STRING) {
 							msg = "Invalid map statement";
 							goto error;
 						}
-						push_text_mapping(config, prev1tok.text, prev1tok.len, tok.text, tok.len);
+						push_text_mapping(prev1tok.text, prev1tok.len, tok.text, tok.len);
 					} else {
 						msg = "Invalid statement in language mapping";
 						goto error;
@@ -796,8 +952,58 @@ load_from_text(
 					goto error;
 				}
 			} else {
-				msg = "Invalid section";
-				goto error;
+				struct token prev1tok, prev2tok;
+				struct theme_handlerinfo *handler = themehandler_get_entry(
+					theme_handlers, LENGTH(theme_handlers), secttok.text, secttok.len
+				);
+				if (!handler) {
+					msg = "Invalid section";
+					goto error;
+				}
+				ltk_theme_parseinfo *parseinfo;
+				size_t parseinfo_len;
+				handler->get_parseinfo(&parseinfo, &parseinfo_len);
+				while (1) {
+					tok = next_token(&s);
+					if (tok.type == SECTION || tok.type == END)
+						break;
+					else if (tok.type == NEWLINE)
+						continue;
+					prev2tok = tok;
+					tok = next_token(&s);
+					prev1tok = tok;
+					tok = next_token(&s);
+					if (prev2tok.type != STRING || prev1tok.type != EQUALS || tok.type != STRING) {
+						msg = "Syntax error in assignment statement";
+						goto error;
+					}
+					ltk_theme_parseinfo *parse_entry = theme_get_entry(
+						parseinfo, parseinfo_len, prev2tok.text, prev2tok.len
+					);
+					if (!parse_entry) {
+						msg = "Invalid left-hand side in assignment statement";
+						goto error;
+					} else if (parse_entry->initialized) {
+						msg = "Duplicate assignment";
+						goto error;
+					}
+					/* temporarly copy to txtbuf so it is NUL-terminated (the alternative
+					   would be to use replacements for ltk_strtonum, etc. that accept
+					   a length parameter) */
+					txtbuf_set_textn(themeval, tok.text, tok.len);
+					if (handle_theme_setting(renderdata, parse_entry, themeval->text)) {
+						msg = "Invalid right-hand side in assignment";
+						goto error;
+					}
+					tok = next_token(&s);
+					if (tok.type == END) {
+						break;
+					} else if (tok.type != NEWLINE) {
+						msg = "Syntax error in assignment statement";
+						goto error;
+					}
+					start_of_line = 1;
+				}
 			}
 			break;
 		case NEWLINE:
@@ -809,7 +1015,12 @@ load_from_text(
 			break;
 		}
 	}
-	global_config = config;
+	/* FIXME: better error reporting */
+	if (fill_theme_defaults(renderdata)) {
+		*errstr = ltk_strdup("Unable to load theme defaults");
+		goto errornomsg;
+	}
+	txtbuf_destroy(themeval);
 	return 0;
 error:
 	if (msg) {
@@ -818,22 +1029,21 @@ error:
 		);
 	}
 errornomsg:
-	destroy_config(config);
+	ltk_config_cleanup(renderdata);
+	txtbuf_destroy(themeval);
 	return 1;
 }
 
 int
-ltk_config_parsefile(const char *filename, char **errstr) {
+ltk_config_parsefile(ltk_renderdata *renderdata, const char *filename, char **errstr) {
 	unsigned long len = 0;
 	char *ferrstr = NULL;
-	sort_keysyms();
-	sort_keybindings();
 	char *file_contents = ltk_read_file(filename, &len, &ferrstr);
 	if (!file_contents) {
 		*errstr = ltk_print_fmt("Unable to open file \"%s\": %s", filename, ferrstr);
 		return 1;
 	}
-	int ret = load_from_text(filename, file_contents, len, errstr);
+	int ret = load_from_text(renderdata, filename, file_contents, len, errstr);
 	ltk_free(file_contents);
 	return ret;
 }
@@ -856,11 +1066,9 @@ const char *default_config = "[general]\n"
 
 /* FIXME: improve this configuration */
 int
-ltk_config_load_default(char **errstr) {
-	sort_keysyms();
-	sort_keybindings();
+ltk_config_load_default(ltk_renderdata *renderdata, char **errstr) {
 	char *config_copied = ltk_strdup(default_config);
-	int ret = load_from_text("<default config>", config_copied, strlen(config_copied), errstr);
+	int ret = load_from_text(renderdata, "<default config>", config_copied, strlen(config_copied), errstr);
 	ltk_free(config_copied);
 	return ret;
 }
diff --git a/src/ltk/config.h b/src/ltk/config.h
@@ -20,7 +20,9 @@
 #include <stddef.h>
 
 #include "array.h"
+#include "color.h"
 #include "widget.h"
+#include "graphics.h"
 #include "eventdefs.h"
 
 typedef enum{
@@ -56,18 +58,12 @@ typedef struct {
 typedef struct {
 	char *line_editor;
 	double dpi_scale;
-	unsigned int fixed_dpi;
-	char mixed_dpi;
-	char explicit_focus;
-	char all_activatable;
+	double fixed_dpi;
+	int mixed_dpi;
+	int explicit_focus;
+	int all_activatable;
 } ltk_general_config;
 
-typedef struct {
-	ltk_language_mapping *mappings;
-	size_t mappings_alloc, mappings_len;
-	ltk_general_config general;
-} ltk_config;
-
 typedef int (*ltk_keybinding_func)(ltk_widget *, ltk_key_event *);
 
 typedef struct {
@@ -85,15 +81,68 @@ typedef struct {
 	ltk_keybinding_cb cb;
 } ltk_keyrelease_cfg;
 
+typedef enum {
+	THEME_STRING,
+	THEME_COLOR,
+	THEME_INT,
+	THEME_UINT,
+	THEME_BOOL,
+	THEME_BORDERSIDES,
+	THEME_SIZE,
+	THEME_DOUBLE,
+} ltk_theme_datatype;
+
+typedef struct {
+	char *key;
+	ltk_theme_datatype type;
+	/* Note: Bool and int are both integers, but they are
+	   separate just to make it a bit clearer */
+	union {
+		char **str;
+		ltk_color **color;
+		int *i;
+		unsigned int *u;
+		int *b;
+		ltk_border_sides *border;
+		ltk_size *size;
+		double *d;
+	} ptr;
+	/* Note: The default color is also given as a string
+	   because it has to be allocated first (it is only a
+	   different entry in the union in order to make it
+	   a bit clearer) */
+	union {
+		char *str;
+		char *color;
+		int i;
+		unsigned int u;
+		int b;
+		ltk_border_sides border;
+		ltk_size size;
+		double d;
+	} defaultval;
+	/* FIXME: min/max doesn't make too much sense for sizes since they
+	   can use different units, but that shouldn't matter for now because
+	   min/max is only used as a sanity check to avoid extreme sizes or
+	   negative sizes where that isn't allowed */
+	/* only for integers, doubles, or sizes */
+	/* doubles are weird at the moment since only two decimal places are
+	   allowed. The min/max for doubles is the min/max given here, divided
+	   by 100, i.e. to allow the range 1.0-10.0, min must be 100 and max
+	   must be 1000. */
+	long long min, max;
+	int initialized;
+} ltk_theme_parseinfo;
+
 LTK_ARRAY_INIT_DECL(keypress, ltk_keypress_cfg)
 LTK_ARRAY_INIT_DECL(keyrelease, ltk_keyrelease_cfg)
 
-void ltk_config_cleanup(void);
-ltk_config *ltk_config_get(void);
+void ltk_config_cleanup(ltk_renderdata *renderdata);
+ltk_general_config *ltk_config_get_general(void);
 int ltk_config_get_language_index(char *lang, size_t *idx_ret);
 ltk_language_mapping *ltk_config_get_language_mapping(size_t idx);
-int ltk_config_parsefile(const char *filename, char **errstr);
-int ltk_config_load_default(char **errstr);
+int ltk_config_parsefile(ltk_renderdata *renderdata, const char *filename, char **errstr);
+int ltk_config_load_default(ltk_renderdata *renderdata, char **errstr);
 
 void ltk_keypress_bindings_destroy(ltk_array(keypress) *arr);
 void ltk_keyrelease_bindings_destroy(ltk_array(keyrelease) *arr);
diff --git a/src/ltk/entry.c b/src/ltk/entry.c
@@ -35,7 +35,6 @@
 #include "memory.h"
 #include "rect.h"
 #include "text.h"
-#include "theme.h"
 #include "txtbuf.h"
 #include "util.h"
 #include "widget.h"
@@ -193,21 +192,11 @@ static ltk_theme_parseinfo parseinfo[] = {
 	{"text-color", THEME_COLOR, {.color = &theme.text_color}, {.color = "#FFFFFF"}, 0, 0, 0},
 	{"selection-color", THEME_COLOR, {.color = &theme.selection_color}, {.color = "#000000"}, 0, 0, 0},
 };
-static int parseinfo_sorted = 0;
-
-int
-ltk_entry_ini_handler(ltk_renderdata *data, const char *prop, const char *value) {
-	return ltk_theme_handle_value(data, "entry", prop, value, parseinfo, LENGTH(parseinfo), &parseinfo_sorted);
-}
-
-int
-ltk_entry_fill_theme_defaults(ltk_renderdata *data) {
-	return ltk_theme_fill_defaults(data, "entry", parseinfo, LENGTH(parseinfo));
-}
 
 void
-ltk_entry_uninitialize_theme(ltk_renderdata *data) {
-	ltk_theme_uninitialize(data, parseinfo, LENGTH(parseinfo));
+ltk_entry_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len) {
+	*p = parseinfo;
+	*len = LENGTH(parseinfo);
 }
 
 /* FIXME: draw cursor in different color on selection side that will be expanded */
@@ -576,15 +565,15 @@ static int
 edit_external(ltk_widget *self, ltk_key_event *event) {
 	(void)event;
 	ltk_entry *entry = LTK_CAST_ENTRY(self);
-	ltk_config *config = ltk_config_get();
+	ltk_general_config *config = ltk_config_get_general();
 	/* FIXME: allow arguments to key mappings - this would allow to have different key mappings
 	   for different editors instead of just one command */
-	if (!config->general.line_editor) {
+	if (!config->line_editor) {
 		ltk_warn("Unable to run external editing command: line editor not configured\n");
 	} else {
 		/* FIXME: somehow show that there was an error if this returns 1? */
 		/* FIXME: change interface to not require length of cmd */
-		ltk_call_cmd(LTK_CAST_WIDGET(entry), config->general.line_editor, strlen(config->general.line_editor), entry->text, entry->len);
+		ltk_call_cmd(LTK_CAST_WIDGET(entry), config->line_editor, strlen(config->line_editor), entry->text, entry->len);
 	}
 	return 0;
 }
diff --git a/src/ltk/entry.h b/src/ltk/entry.h
@@ -40,15 +40,4 @@ typedef struct {
 
 ltk_entry *ltk_entry_create(ltk_window *window, char *text);
 
-/* FIXME: these should be private to ltk */
-void ltk_entry_cleanup(void);
-void ltk_entry_get_keybinding_parseinfo(
-        ltk_keybinding_cb **press_cbs_ret, size_t *press_len_ret,
-        ltk_keybinding_cb **release_cbs_ret, size_t *release_len_ret,
-        ltk_array(keypress) **presses_ret, ltk_array(keyrelease) **releases_ret
-);
-int ltk_entry_ini_handler(ltk_renderdata *data, const char *prop, const char *value);
-int ltk_entry_fill_theme_defaults(ltk_renderdata *data);
-void ltk_entry_uninitialize_theme(ltk_renderdata *data);
-
 #endif /* LTK_ENTRY_H */
diff --git a/src/ltk/event_xlib.c b/src/ltk/event_xlib.c
@@ -235,8 +235,8 @@ ltk_recalc_renderwindow_dpi(ltk_renderwindow *window) {
 static void
 update_monitor_config(ltk_renderdata *renderdata, ltk_renderwindow **windows, size_t num_windows) {
 	int nmon;
-	ltk_config *config = ltk_config_get();
-	if (!config->general.mixed_dpi)
+	ltk_general_config *config = ltk_config_get_general();
+	if (!config->mixed_dpi)
 		return;
 	XRRMonitorInfo *mi = XRRGetMonitors(renderdata->dpy, renderdata->root_window, 1, &nmon);
 	if (nmon > 0 && !renderdata->monitors)
@@ -246,7 +246,7 @@ update_monitor_config(ltk_renderdata *renderdata, ltk_renderwindow **windows, si
 		/* FIXME: This only uses the width for the calculation. It should be the same if using
 		   the height, but is that guaranteed? */
 		/* FIXME: can width or mwidth ever by negative? */
-		info.dpi = (unsigned int)round(config->general.dpi_scale * (info.width / (info.mwidth / 127.0)));
+		info.dpi = (unsigned int)round(config->dpi_scale * (info.width / (info.mwidth / 127.0)));
 		/* FIXME: need to adjust default dpi and document */
 		/* -> config file dpi should still be regular dpi */
 		/* FIXME: check for overflows in the later pixel computation */
diff --git a/src/ltk/ini.c b/src/ltk/ini.c
@@ -1,201 +0,0 @@
-/* inih -- simple .INI file parser
-
-inih is released under the New BSD license (see LICENSE.txt). Go to the project
-home page for more info:
-
-https://github.com/benhoyt/inih
-
-*/
-
-#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
-#define _CRT_SECURE_NO_WARNINGS
-#endif
-
-#include <stdio.h>
-#include <ctype.h>
-#include <string.h>
-
-#include "ini.h"
-
-#if !INI_USE_STACK
-#include <stdlib.h>
-#endif
-
-#define MAX_SECTION 50
-#define MAX_NAME 50
-
-/* Strip whitespace chars off end of given string, in place. Return s. */
-static char* rstrip(char* s)
-{
-    char* p = s + strlen(s);
-    while (p > s && isspace((unsigned char)(*--p)))
-        *p = '\0';
-    return s;
-}
-
-/* Return pointer to first non-whitespace char in given string. */
-static char* lskip(const char* s)
-{
-    while (*s && isspace((unsigned char)(*s)))
-        s++;
-    return (char*)s;
-}
-
-/* Return pointer to first char (of chars) or inline comment in given string,
-   or pointer to null at end of string if neither found. Inline comment must
-   be prefixed by a whitespace character to register as a comment. */
-static char* find_chars_or_comment(const char* s, const char* chars)
-{
-#if INI_ALLOW_INLINE_COMMENTS
-    int was_space = 0;
-    while (*s && (!chars || !strchr(chars, *s)) &&
-           !(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) {
-        was_space = isspace((unsigned char)(*s));
-        s++;
-    }
-#else
-    while (*s && (!chars || !strchr(chars, *s))) {
-        s++;
-    }
-#endif
-    return (char*)s;
-}
-
-/* Version of strncpy that ensures dest (size bytes) is null-terminated. */
-static char* strncpy0(char* dest, const char* src, size_t size)
-{
-    strncpy(dest, src, size);
-    dest[size - 1] = '\0';
-    return dest;
-}
-
-/* See documentation in header file. */
-int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
-                     void* user)
-{
-    /* Uses a fair bit of stack (use heap instead if you need to) */
-#if INI_USE_STACK
-    char line[INI_MAX_LINE];
-#else
-    char* line;
-#endif
-    char section[MAX_SECTION] = "";
-    char prev_name[MAX_NAME] = "";
-
-    char* start;
-    char* end;
-    char* name;
-    char* value;
-    int lineno = 0;
-    int error = 0;
-
-#if !INI_USE_STACK
-    line = (char*)malloc(INI_MAX_LINE);
-    if (!line) {
-        return -2;
-    }
-#endif
-
-#if INI_HANDLER_LINENO
-#define HANDLER(u, s, n, v) handler(u, s, n, v, lineno)
-#else
-#define HANDLER(u, s, n, v) handler(u, s, n, v)
-#endif
-
-    /* Scan through stream line by line */
-    while (reader(line, INI_MAX_LINE, stream) != NULL) {
-        lineno++;
-
-        start = line;
-#if INI_ALLOW_BOM
-        if (lineno == 1 && (unsigned char)start[0] == 0xEF &&
-                           (unsigned char)start[1] == 0xBB &&
-                           (unsigned char)start[2] == 0xBF) {
-            start += 3;
-        }
-#endif
-        start = lskip(rstrip(start));
-
-        if (*start == ';' || *start == '#') {
-            /* Per Python configparser, allow both ; and # comments at the
-               start of a line */
-        }
-#if INI_ALLOW_MULTILINE
-        else if (*prev_name && *start && start > line) {
-            /* Non-blank line with leading whitespace, treat as continuation
-               of previous name's value (as per Python configparser). */
-            if (!HANDLER(user, section, prev_name, start) && !error)
-                error = lineno;
-        }
-#endif
-        else if (*start == '[') {
-            /* A "[section]" line */
-            end = find_chars_or_comment(start + 1, "]");
-            if (*end == ']') {
-                *end = '\0';
-                strncpy0(section, start + 1, sizeof(section));
-                *prev_name = '\0';
-            }
-            else if (!error) {
-                /* No ']' found on section line */
-                error = lineno;
-            }
-        }
-        else if (*start) {
-            /* Not a comment, must be a name[=:]value pair */
-            end = find_chars_or_comment(start, "=:");
-            if (*end == '=' || *end == ':') {
-                *end = '\0';
-                name = rstrip(start);
-                value = end + 1;
-#if INI_ALLOW_INLINE_COMMENTS
-                end = find_chars_or_comment(value, NULL);
-                if (*end)
-                    *end = '\0';
-#endif
-                value = lskip(value);
-                rstrip(value);
-
-                /* Valid name[=:]value pair found, call handler */
-                strncpy0(prev_name, name, sizeof(prev_name));
-                if (!HANDLER(user, section, name, value) && !error)
-                    error = lineno;
-            }
-            else if (!error) {
-                /* No '=' or ':' found on name[=:]value line */
-                error = lineno;
-            }
-        }
-
-#if INI_STOP_ON_FIRST_ERROR
-        if (error)
-            break;
-#endif
-    }
-
-#if !INI_USE_STACK
-    free(line);
-#endif
-
-    return error;
-}
-
-/* See documentation in header file. */
-int ini_parse_file(FILE* file, ini_handler handler, void* user)
-{
-    return ini_parse_stream((ini_reader)fgets, file, handler, user);
-}
-
-/* See documentation in header file. */
-int ini_parse(const char* filename, ini_handler handler, void* user)
-{
-    FILE* file;
-    int error;
-
-    file = fopen(filename, "r");
-    if (!file)
-        return -1;
-    error = ini_parse_file(file, handler, user);
-    fclose(file);
-    return error;
-}
diff --git a/src/ltk/ini.h b/src/ltk/ini.h
@@ -1,104 +0,0 @@
-/* inih -- simple .INI file parser
-
-inih is released under the New BSD license (see LICENSE.txt). Go to the project
-home page for more info:
-
-https://github.com/benhoyt/inih
-
-*/
-
-#ifndef __INI_H__
-#define __INI_H__
-
-/* Make this header file easier to include in C++ code */
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#include <stdio.h>
-
-/* Nonzero if ini_handler callback should accept lineno parameter. */
-#ifndef INI_HANDLER_LINENO
-#define INI_HANDLER_LINENO 0
-#endif
-
-/* Typedef for prototype of handler function. */
-#if INI_HANDLER_LINENO
-typedef int (*ini_handler)(void* user, const char* section,
-                           const char* name, const char* value,
-                           int lineno);
-#else
-typedef int (*ini_handler)(void* user, const char* section,
-                           const char* name, const char* value);
-#endif
-
-/* Typedef for prototype of fgets-style reader function. */
-typedef char* (*ini_reader)(char* str, int num, void* stream);
-
-/* Parse given INI-style file. May have [section]s, name=value pairs
-   (whitespace stripped), and comments starting with ';' (semicolon). Section
-   is "" if name=value pair parsed before any section heading. name:value
-   pairs are also supported as a concession to Python's configparser.
-
-   For each name=value pair parsed, call handler function with given user
-   pointer as well as section, name, and value (data only valid for duration
-   of handler call). Handler should return nonzero on success, zero on error.
-
-   Returns 0 on success, line number of first error on parse error (doesn't
-   stop on first error), -1 on file open error, or -2 on memory allocation
-   error (only when INI_USE_STACK is zero).
-*/
-int ini_parse(const char* filename, ini_handler handler, void* user);
-
-/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't
-   close the file when it's finished -- the caller must do that. */
-int ini_parse_file(FILE* file, ini_handler handler, void* user);
-
-/* Same as ini_parse(), but takes an ini_reader function pointer instead of
-   filename. Used for implementing custom or string-based I/O. */
-int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
-                     void* user);
-
-/* Nonzero to allow multi-line value parsing, in the style of Python's
-   configparser. If allowed, ini_parse() will call the handler with the same
-   name for each subsequent line parsed. */
-#ifndef INI_ALLOW_MULTILINE
-#define INI_ALLOW_MULTILINE 1
-#endif
-
-/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of
-   the file. See http://code.google.com/p/inih/issues/detail?id=21 */
-#ifndef INI_ALLOW_BOM
-#define INI_ALLOW_BOM 1
-#endif
-
-/* Nonzero to allow inline comments (with valid inline comment characters
-   specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match
-   Python 3.2+ configparser behaviour. */
-#ifndef INI_ALLOW_INLINE_COMMENTS
-#define INI_ALLOW_INLINE_COMMENTS 1
-#endif
-#ifndef INI_INLINE_COMMENT_PREFIXES
-#define INI_INLINE_COMMENT_PREFIXES ";"
-#endif
-
-/* Nonzero to use stack, zero to use heap (malloc/free). */
-#ifndef INI_USE_STACK
-#define INI_USE_STACK 1
-#endif
-
-/* Stop parsing on first error (default is to keep parsing). */
-#ifndef INI_STOP_ON_FIRST_ERROR
-#define INI_STOP_ON_FIRST_ERROR 0
-#endif
-
-/* Maximum line length for any line in INI file. */
-#ifndef INI_MAX_LINE
-#define INI_MAX_LINE 200
-#endif
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /* __INI_H__ */
diff --git a/src/ltk/label.c b/src/ltk/label.c
@@ -26,7 +26,6 @@
 #include "text.h"
 #include "label.h"
 #include "graphics.h"
-#include "theme.h"
 
 #define MAX_LABEL_PADDING 50000
 
@@ -63,8 +62,6 @@ static struct {
 	ltk_size pad;
 } theme;
 
-int parseinfo_sorted = 0;
-
 static ltk_theme_parseinfo parseinfo[] = {
 	{"bg-color", THEME_COLOR, {.color = &theme.bg_color}, {.color = "#000000"}, 0, 0, 0},
 	{"bg-color-active", THEME_COLOR, {.color = &theme.bg_color_active}, {.color = "#222222"}, 0, 0, 0},
@@ -72,19 +69,10 @@ static ltk_theme_parseinfo parseinfo[] = {
 	{"text-color", THEME_COLOR, {.color = &theme.text_color}, {.color = "#FFFFFF"}, 0, 0, 0},
 };
 
-int
-ltk_label_ini_handler(ltk_renderdata *data, const char *prop, const char *value) {
-	return ltk_theme_handle_value(data, "label", prop, value, parseinfo, LENGTH(parseinfo), &parseinfo_sorted);
-}
-
-int
-ltk_label_fill_theme_defaults(ltk_renderdata *data) {
-	return ltk_theme_fill_defaults(data, "label", parseinfo, LENGTH(parseinfo));
-}
-
 void
-ltk_label_uninitialize_theme(ltk_renderdata *data) {
-	ltk_theme_uninitialize(data, parseinfo, LENGTH(parseinfo));
+ltk_label_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len) {
+	*p = parseinfo;
+	*len = LENGTH(parseinfo);
 }
 
 static void
diff --git a/src/ltk/label.h b/src/ltk/label.h
@@ -29,10 +29,6 @@ typedef struct {
 	ltk_text_line *tl;
 } ltk_label;
 
-int ltk_label_ini_handler(ltk_renderdata *data, const char *prop, const char *value);
-int ltk_label_fill_theme_defaults(ltk_renderdata *data);
-void ltk_label_uninitialize_theme(ltk_renderdata *data);
-
 ltk_label *ltk_label_create(ltk_window *window, char *text);
 
 #endif /* LTK_LABEL_H */
diff --git a/src/ltk/ltk.c b/src/ltk/ltk.c
@@ -33,7 +33,6 @@
 #include "eventdefs.h"
 #include "graphics.h"
 #include "image.h"
-#include "ini.h"
 #include "label.h"
 #include "macros.h"
 #include "memory.h"
@@ -43,8 +42,7 @@
 #include "text.h"
 #include "util.h"
 #include "widget.h"
-
-#define MAX_WINDOW_FONT_SIZE 200
+#include "widget_internal.h"
 
 typedef struct {
 	char *tmpfile;
@@ -87,91 +85,56 @@ static size_t timers_num = 0;
 static size_t timers_alloc = 0;
 
 static void ltk_handle_event(ltk_event *event);
-static void ltk_load_theme(const char *path);
-static void ltk_uninitialize_theme(void);
-static int ltk_ini_handler(void *renderdata, const char *widget, const char *prop, const char *value);
 
 static short running = 1;
 
 typedef struct {
 	char *name;
-	int (*ini_handler)(ltk_renderdata *, const char *, const char *);
-	int (*fill_theme_defaults)(ltk_renderdata *);
-	void (*uninitialize_theme)(ltk_renderdata *);
 	void (*cleanup)(void);
 } ltk_widget_funcs;
 
-/* FIXME: use binary search when searching for the widget */
+/* FIXME: I guess the names aren't needed anymore here, but who
+   knows if I'll need them again sometime... */
 static ltk_widget_funcs widget_funcs[] = {
 	{
 		.name = "box",
-		.ini_handler = NULL,
-		.fill_theme_defaults = NULL,
-		.uninitialize_theme = NULL,
 		.cleanup = NULL,
 	},
 	{
 		.name = "button",
-		.ini_handler = <k_button_ini_handler,
-		.fill_theme_defaults = <k_button_fill_theme_defaults,
-		.uninitialize_theme = <k_button_uninitialize_theme,
 		.cleanup = NULL,
 	},
 	{
 		.name = "entry",
-		.ini_handler = <k_entry_ini_handler,
-		.fill_theme_defaults = <k_entry_fill_theme_defaults,
-		.uninitialize_theme = <k_entry_uninitialize_theme,
 		.cleanup = <k_entry_cleanup,
 	},
 	{
 		.name = "grid",
-		.ini_handler = NULL,
-		.fill_theme_defaults = NULL,
-		.uninitialize_theme = NULL,
 		.cleanup = NULL,
 	},
 	{
 		.name = "label",
-		.ini_handler = <k_label_ini_handler,
-		.fill_theme_defaults = <k_label_fill_theme_defaults,
-		.uninitialize_theme = <k_label_uninitialize_theme,
 		.cleanup = NULL,
 	},
 	{
 		/* FIXME: this is actually image_widget */
 		.name = "image",
-		.ini_handler = NULL,
-		.fill_theme_defaults = NULL,
-		.uninitialize_theme = NULL,
 		.cleanup = NULL,
 	},
 	{
 		.name = "menu",
-		.ini_handler = <k_menu_ini_handler,
-		.fill_theme_defaults = <k_menu_fill_theme_defaults,
-		.uninitialize_theme = <k_menu_uninitialize_theme,
 		.cleanup = NULL,
 	},
 	{
 		.name = "menuentry",
-		.ini_handler = <k_menuentry_ini_handler,
-		.fill_theme_defaults = <k_menuentry_fill_theme_defaults,
-		.uninitialize_theme = <k_menuentry_uninitialize_theme,
 		.cleanup = NULL,
 	},
 	{
 		.name = "submenu",
-		.ini_handler = <k_submenu_ini_handler,
-		.fill_theme_defaults = <k_submenu_fill_theme_defaults,
-		.uninitialize_theme = <k_submenu_uninitialize_theme,
 		.cleanup = NULL,
 	},
 	{
 		.name = "submenuentry",
-		.ini_handler = <k_submenuentry_ini_handler,
-		.fill_theme_defaults = <k_submenuentry_fill_theme_defaults,
-		.uninitialize_theme = <k_submenuentry_uninitialize_theme,
 		.cleanup = NULL,
 		 /*
 		 This "widget" is only needed to have separate styles for regular
@@ -186,17 +149,11 @@ static ltk_widget_funcs widget_funcs[] = {
 	},
 	{
 		.name = "scrollbar",
-		.ini_handler = <k_scrollbar_ini_handler,
-		.fill_theme_defaults = <k_scrollbar_fill_theme_defaults,
-		.uninitialize_theme = <k_scrollbar_uninitialize_theme,
 		.cleanup = NULL,
 	},
 	{
 		/* Handler for window theme. */
 		.name = "window",
-		.ini_handler = <k_window_ini_handler,
-		.fill_theme_defaults = <k_window_fill_theme_defaults,
-		.uninitialize_theme = <k_window_uninitialize_theme,
 		.cleanup = <k_window_cleanup,
 	}
 };
@@ -216,32 +173,29 @@ ltk_init(void) {
 		ltk_fatal_errno("Unable to setup ltk directory.\n");
 	shared_data.cur_kbd = 0;
 
+	shared_data.renderdata = ltk_renderer_create();
+	if (!shared_data.renderdata)
+		return 1; /* FIXME: clean up */
+
 	/* FIXME: search different directories for config */
-	/* FIXME: don't print error if config or theme file doesn't exist */
+	/* FIXME: don't print error if config file doesn't exist */
 	char *config_path = ltk_strcat_useful(ltk_dir, "/ltk.cfg");
-	char *theme_path;
+	ltk_free0(ltk_dir);
 	char *errstr = NULL;
-	if (ltk_config_parsefile(config_path, &errstr)) {
+	if (ltk_config_parsefile(shared_data.renderdata, config_path, &errstr)) {
 		if (errstr) {
 			ltk_warn("Unable to load config: %s\n", errstr);
 			ltk_free0(errstr);
 		}
-		if (ltk_config_load_default(&errstr)) {
+		if (ltk_config_load_default(shared_data.renderdata, &errstr)) {
 			/* FIXME: I guess errstr isn't freed here, but whatever */
 			/* FIXME: return error instead of dying */
 			ltk_fatal("Unable to load default config: %s\n", errstr);
 		}
 	}
 	ltk_free0(config_path);
-	theme_path = ltk_strcat_useful(ltk_dir, "/theme.ini");
-	ltk_free0(ltk_dir);
-	shared_data.renderdata = ltk_renderer_create();
-	if (!shared_data.renderdata)
-		return 1; /* FIXME: clean up */
+
 	ltk_events_init(shared_data.renderdata);
-	ltk_load_theme(theme_path);
-	ltk_free0(theme_path);
-	/* FIXME: maybe "general" theme instead of window theme? */
 	ltk_window_theme *window_theme = ltk_window_get_theme();
 	shared_data.text_context = ltk_text_context_create(shared_data.renderdata, window_theme->font);
 	shared_data.clipboard = ltk_clipboard_create(shared_data.renderdata);
@@ -382,7 +336,8 @@ ltk_mainloop(void) {
 
 void
 ltk_deinit(void) {
-	if (running)
+	/* if renderdata is NULL, the other initialization can't have happened either */
+	if (running || !shared_data.renderdata)
 		return;
 	if (shared_data.cmds) {
 		for (size_t i = 0; i < ltk_array_len(shared_data.cmds); i++) {
@@ -403,11 +358,11 @@ ltk_deinit(void) {
 	if (shared_data.rwindows)
 		ltk_array_destroy(rwindow, shared_data.rwindows);
 	shared_data.rwindows = NULL;
-	ltk_config_cleanup();
 	for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
 		if (widget_funcs[i].cleanup)
 			widget_funcs[i].cleanup();
 	}
+	ltk_config_cleanup(shared_data.renderdata);
 	if (shared_data.text_context)
 		ltk_text_context_destroy(shared_data.text_context);
 	shared_data.text_context = NULL;
@@ -415,10 +370,7 @@ ltk_deinit(void) {
 		ltk_clipboard_destroy(shared_data.clipboard);
 	shared_data.clipboard = NULL;
 	ltk_events_cleanup();
-	if (shared_data.renderdata) {
-		ltk_uninitialize_theme();
-		ltk_renderer_destroy(shared_data.renderdata);
-	}
+	ltk_renderer_destroy(shared_data.renderdata);
 	shared_data.renderdata = NULL;
 }
 
@@ -511,45 +463,6 @@ ltk_register_timer(long first, long repeat, void (*callback)(ltk_callback_arg da
 	return id;
 }
 
-/* FIXME: standardize return codes - usually, 0 is returned on success, but ini.h
-   uses 1 on success, so this is all a bit confusing */
-/* FIXME: switch away from ini.h */
-static int
-ltk_ini_handler(void *renderdata, const char *widget, const char *prop, const char *value) {
-	for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
-		if (widget_funcs[i].ini_handler && !strcmp(widget, widget_funcs[i].name)) {
-			widget_funcs[i].ini_handler(renderdata, prop, value);
-			return 1;
-		}
-	}
-	return 0;
-}
-
-/* FIXME: don't call ltk_fatal, instead return error from ltk_init */
-static void
-ltk_load_theme(const char *path) {
-	/* FIXME: give line number in error message */
-	if (ini_parse(path, ltk_ini_handler, shared_data.renderdata) != 0) {
-		ltk_warn("Unable to load theme.\n");
-	}
-	for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
-		if (widget_funcs[i].fill_theme_defaults) {
-			if (widget_funcs[i].fill_theme_defaults(shared_data.renderdata)) {
-				ltk_uninitialize_theme();
-				ltk_fatal("Unable to load theme defaults.\n");
-			}
-		}
-	}
-}
-
-static void
-ltk_uninitialize_theme(void) {
-	for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
-		if (widget_funcs[i].uninitialize_theme)
-			widget_funcs[i].uninitialize_theme(shared_data.renderdata);
-	}
-}
-
 int
 ltk_call_cmd(ltk_widget *caller, const char *cmd, size_t cmdlen, const char *text, size_t textlen) {
 	/* FIXME: support environment variable $TMPDIR */
diff --git a/src/ltk/menu.c b/src/ltk/menu.c
@@ -35,7 +35,6 @@
 #include "text.h"
 #include "menu.h"
 #include "graphics.h"
-#include "theme.h"
 
 #define MAX_MENU_BORDER_WIDTH 10000
 #define MAX_MENU_PAD 50000
@@ -208,22 +207,6 @@ static ltk_theme_parseinfo menu_parseinfo[] = {
 	{"scroll-background", THEME_COLOR, {.color = &menu_theme.scroll_background}, {.color = "#333333"}, 0, 0, 0},
 	{"scroll-arrow-color", THEME_COLOR, {.color = &menu_theme.scroll_arrow_color}, {.color = "#000000"}, 0, 0, 0},
 };
-static int menu_parseinfo_sorted = 0;
-
-int
-ltk_menu_ini_handler(ltk_renderdata *data, const char *prop, const char *value) {
-	return ltk_theme_handle_value(data, "menu", prop, value, menu_parseinfo, LENGTH(menu_parseinfo), &menu_parseinfo_sorted);
-}
-
-int
-ltk_menu_fill_theme_defaults(ltk_renderdata *data) {
-	return ltk_theme_fill_defaults(data, "menu", menu_parseinfo, LENGTH(menu_parseinfo));
-}
-
-void
-ltk_menu_uninitialize_theme(ltk_renderdata *data) {
-	ltk_theme_uninitialize(data, menu_parseinfo, LENGTH(menu_parseinfo));
-}
 
 static ltk_theme_parseinfo menu_entry_parseinfo[] = {
 	{"text-pad", THEME_SIZE, {.size = &menu_entry_theme.text_pad}, {.size = {.val = 100, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_PAD, 0},
@@ -245,22 +228,6 @@ static ltk_theme_parseinfo menu_entry_parseinfo[] = {
 	{"border-disabled", THEME_COLOR, {.color = &menu_entry_theme.border_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
 	{"fill-disabled", THEME_COLOR, {.color = &menu_entry_theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0},
 };
-static int menu_entry_parseinfo_sorted = 0;
-
-int
-ltk_menuentry_ini_handler(ltk_renderdata *data, const char *prop, const char *value) {
-	return ltk_theme_handle_value(data, "menu-entry", prop, value, menu_entry_parseinfo, LENGTH(menu_entry_parseinfo), &menu_entry_parseinfo_sorted);
-}
-
-int
-ltk_menuentry_fill_theme_defaults(ltk_renderdata *data) {
-	return ltk_theme_fill_defaults(data, "menu-entry", menu_entry_parseinfo, LENGTH(menu_entry_parseinfo));
-}
-
-void
-ltk_menuentry_uninitialize_theme(ltk_renderdata *data) {
-	ltk_theme_uninitialize(data, menu_entry_parseinfo, LENGTH(menu_entry_parseinfo));
-}
 
 static ltk_theme_parseinfo submenu_parseinfo[] = {
 	{"pad", THEME_SIZE, {.size = &submenu_theme.pad}, {.size = {.val = 0, .unit = LTK_UNIT_PX}}, 0, MAX_MENU_PAD, 0},
@@ -273,22 +240,6 @@ static ltk_theme_parseinfo submenu_parseinfo[] = {
 	{"scroll-background", THEME_COLOR, {.color = &submenu_theme.scroll_background}, {.color = "#333333"}, 0, 0, 0},
 	{"scroll-arrow-color", THEME_COLOR, {.color = &submenu_theme.scroll_arrow_color}, {.color = "#000000"}, 0, 0, 0},
 };
-static int submenu_parseinfo_sorted = 0;
-
-int
-ltk_submenu_ini_handler(ltk_renderdata *data, const char *prop, const char *value) {
-	return ltk_theme_handle_value(data, "submenu", prop, value, submenu_parseinfo, LENGTH(submenu_parseinfo), &submenu_parseinfo_sorted);
-}
-
-int
-ltk_submenu_fill_theme_defaults(ltk_renderdata *data) {
-	return ltk_theme_fill_defaults(data, "submenu", submenu_parseinfo, LENGTH(submenu_parseinfo));
-}
-
-void
-ltk_submenu_uninitialize_theme(ltk_renderdata *data) {
-	ltk_theme_uninitialize(data, submenu_parseinfo, LENGTH(submenu_parseinfo));
-}
 
 static ltk_theme_parseinfo submenu_entry_parseinfo[] = {
 	{"text-pad", THEME_SIZE, {.size = &submenu_entry_theme.text_pad}, {.size = {.val = 100, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_PAD, 0},
@@ -310,21 +261,28 @@ static ltk_theme_parseinfo submenu_entry_parseinfo[] = {
 	{"border-disabled", THEME_COLOR, {.color = &submenu_entry_theme.border_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
 	{"fill-disabled", THEME_COLOR, {.color = &submenu_entry_theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0},
 };
-static int submenu_entry_parseinfo_sorted = 0;
+void
+ltk_menu_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len) {
+	*p = menu_parseinfo;
+	*len = LENGTH(menu_parseinfo);
+}
 
-int
-ltk_submenuentry_ini_handler(ltk_renderdata *data, const char *prop, const char *value) {
-	return ltk_theme_handle_value(data, "submenu-entry", prop, value, submenu_entry_parseinfo, LENGTH(submenu_entry_parseinfo), &submenu_entry_parseinfo_sorted);
+void
+ltk_submenu_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len) {
+	*p = submenu_parseinfo;
+	*len = LENGTH(submenu_parseinfo);
 }
 
-int
-ltk_submenuentry_fill_theme_defaults(ltk_renderdata *data) {
-	return ltk_theme_fill_defaults(data, "submenu-entry", submenu_entry_parseinfo, LENGTH(submenu_entry_parseinfo));
+void
+ltk_menuentry_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len) {
+	*p = menu_entry_parseinfo;
+	*len = LENGTH(menu_entry_parseinfo);
 }
 
 void
-ltk_submenuentry_uninitialize_theme(ltk_renderdata *data) {
-	ltk_theme_uninitialize(data, submenu_entry_parseinfo, LENGTH(submenu_entry_parseinfo));
+ltk_submenuentry_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len) {
+	*p = submenu_entry_parseinfo;
+	*len = LENGTH(submenu_entry_parseinfo);
 }
 
 static void
diff --git a/src/ltk/menu.h b/src/ltk/menu.h
@@ -67,20 +67,6 @@ struct ltk_menuentry {
 should submenus also allow setting orientation?
 -> would maybe look weird in some cases */
 
-int ltk_menu_ini_handler(ltk_renderdata *data, const char *prop, const char *value);
-int ltk_menu_fill_theme_defaults(ltk_renderdata *data);
-void ltk_menu_uninitialize_theme(ltk_renderdata *data);
-int ltk_submenu_ini_handler(ltk_renderdata *data, const char *prop, const char *value);
-int ltk_submenu_fill_theme_defaults(ltk_renderdata *data);
-void ltk_submenu_uninitialize_theme(ltk_renderdata *data);
-
-int ltk_menuentry_ini_handler(ltk_renderdata *data, const char *prop, const char *value);
-int ltk_menuentry_fill_theme_defaults(ltk_renderdata *data);
-void ltk_menuentry_uninitialize_theme(ltk_renderdata *data);
-int ltk_submenuentry_ini_handler(ltk_renderdata *data, const char *prop, const char *value);
-int ltk_submenuentry_fill_theme_defaults(ltk_renderdata *data);
-void ltk_submenuentry_uninitialize_theme(ltk_renderdata *data);
-
 /* FIXME: allow orientation */
 ltk_menu *ltk_menu_create(ltk_window *window);
 ltk_menu *ltk_submenu_create(ltk_window *window);
diff --git a/src/ltk/num.c b/src/ltk/num.c
@@ -249,7 +249,7 @@ ltk_strtoscalednum(
 		/* FIXME: warn if there are any more digits */
 	}
 
-	/* FIXME: decrease code duplication */
+	/* FIXME: decrease code duplication and check that this actually works */
 	long long cutoff = ll < 0 ? LLONG_MIN : LLONG_MAX;
 	long long cutlim = cutoff % 100;
 	cutoff /= 100;
diff --git a/src/ltk/scrollbar.c b/src/ltk/scrollbar.c
@@ -23,7 +23,6 @@
 #include "widget.h"
 #include "util.h"
 #include "scrollbar.h"
-#include "theme.h"
 #include "eventdefs.h"
 
 #define MAX_SCROLLBAR_WIDTH 10000 /* completely arbitrary */
@@ -74,21 +73,11 @@ static ltk_theme_parseinfo parseinfo[] = {
 	{"fg-pressed", THEME_COLOR, {.color = &theme.fg_pressed}, {.color = "#113355"}, 0, 0, 0},
 	{"fg-disabled", THEME_COLOR, {.color = &theme.fg_disabled}, {.color = "#292929"}, 0, 0, 0},
 };
-static int parseinfo_sorted = 0;
-
-int
-ltk_scrollbar_ini_handler(ltk_renderdata *data, const char *prop, const char *value) {
-	return ltk_theme_handle_value(data, "scrollbar", prop, value, parseinfo, LENGTH(parseinfo), &parseinfo_sorted);
-}
-
-int
-ltk_scrollbar_fill_theme_defaults(ltk_renderdata *data) {
-	return ltk_theme_fill_defaults(data, "scrollbar", parseinfo, LENGTH(parseinfo));
-}
 
 void
-ltk_scrollbar_uninitialize_theme(ltk_renderdata *data) {
-	ltk_theme_uninitialize(data, parseinfo, LENGTH(parseinfo));
+ltk_scrollbar_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len) {
+	*p = parseinfo;
+	*len = LENGTH(parseinfo);
 }
 
 void
diff --git a/src/ltk/scrollbar.h b/src/ltk/scrollbar.h
@@ -37,8 +37,4 @@ void ltk_scrollbar_set_virtual_size(ltk_scrollbar *scrollbar, int virtual_size);
 ltk_scrollbar *ltk_scrollbar_create(ltk_window *window, ltk_orientation orient);
 void ltk_scrollbar_scroll(ltk_widget *self, int delta, int scaled);
 
-int ltk_scrollbar_ini_handler(ltk_renderdata *data, const char *prop, const char *value);
-int ltk_scrollbar_fill_theme_defaults(ltk_renderdata *data);
-void ltk_scrollbar_uninitialize_theme(ltk_renderdata *data);
-
 #endif /* LTK_SCROLLBAR_H */
diff --git a/src/ltk/theme.c b/src/ltk/theme.c
@@ -1,211 +0,0 @@
-/*
- * Copyright (c) 2022-2024 lumidify <nobody@lumidify.org>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <stdlib.h>
-#include <string.h>
-
-#include "graphics.h"
-#include "util.h"
-#include "theme.h"
-#include "memory.h"
-
-/* FIXME: handle '#' or no '#' in color specification */
-static int
-search_helper(const void *keyv, const void *entryv) {
-	char *key = (char *)keyv;
-	ltk_theme_parseinfo *entry = (ltk_theme_parseinfo *)entryv;
-	return strcmp(key, entry->key);
-}
-
-static int
-sort_helper(const void *entry1v, const void *entry2v) {
-	ltk_theme_parseinfo *entry1 = (ltk_theme_parseinfo *)entry1v;
-	ltk_theme_parseinfo *entry2 = (ltk_theme_parseinfo *)entry2v;
-	return strcmp(entry1->key, entry2->key);
-}
-
-/* FIXME: more information for errors */
-int
-ltk_theme_handle_value(ltk_renderdata *renderdata, char *debug_name, const char *prop, const char *value, ltk_theme_parseinfo *parseinfo, size_t len, int *sorted) {
-	if (!*sorted) {
-		qsort(parseinfo, len, sizeof(ltk_theme_parseinfo), &sort_helper);
-		*sorted = 1;
-	}
-	ltk_theme_parseinfo *entry = bsearch(prop, parseinfo, len, sizeof(ltk_theme_parseinfo), &search_helper);
-	if (!entry) {
-		ltk_warn("Invalid property '%s:%s'.\n", debug_name, prop);
-		return 1;
-	} else if (entry->initialized) {
-		ltk_warn("Duplicate setting for property '%s:%s'.\n", debug_name, prop);
-		return 1;
-	}
-	const char *errstr = NULL;
-	switch (entry->type) {
-	case THEME_INT:
-		*(entry->ptr.i) = ltk_strtonum(value, entry->min, entry->max, &errstr);
-		if (errstr) {
-			ltk_warn("Invalid value '%s' for property '%s:%s'.\n", value, debug_name, prop);
-			return 1;
-		} else {
-			entry->initialized = 1;
-		}
-		break;
-	case THEME_SIZE:
-		entry->ptr.size->unit = LTK_UNIT_PX;
-		char *endptr = NULL;
-		/* this already takes care of overflow prevention because entry->min and entry->max are int */
-		entry->ptr.size->val = ltk_strtoscalednum(value, entry->min, entry->max, &endptr, &errstr);
-		if (errstr) {
-			ltk_warn("Invalid value '%s' for property '%s:%s': %s\n", value, debug_name, prop, errstr);
-			return 1;
-		}
-		if (*endptr == '\0') {
-			/* NOP */
-		} else if (!strcmp(endptr, "px")) {
-			entry->ptr.size->unit = LTK_UNIT_PX;
-		} else if (!strcmp(endptr, "pt")) {
-			entry->ptr.size->unit = LTK_UNIT_PT;
-		} else if (!strcmp(endptr, "mm")) {
-			entry->ptr.size->unit = LTK_UNIT_MM;
-		} else {
-			ltk_warn("Invalid value '%s' for property '%s:%s'\n", value, debug_name, prop);
-			return 1;
-		}
-		entry->initialized = 1;
-		break;
-	case THEME_STRING:
-		/* FIXME: check if already set? */
-		*(entry->ptr.str) = ltk_strdup(value);
-		entry->initialized = 1;
-		break;
-	case THEME_COLOR:
-		if (!(*(entry->ptr.color) = ltk_color_create(renderdata, value))) {
-			ltk_warn("Unable to create color '%s' for property '%s:%s'.\n", value, debug_name, prop);
-			return 1;
-		} else {
-			entry->initialized = 1;
-		}
-		break;
-	case THEME_BOOL:
-		if (strcmp(value, "true") == 0) {
-			*(entry->ptr.b) = 1;
-		} else if (strcmp(value, "false") == 0) {
-			*(entry->ptr.b) = 0;
-		} else {
-			ltk_warn("Invalid value '%s' for property '%s:%s'.\n", value, debug_name, prop);
-			return 1;
-		}
-		entry->initialized = 1;
-		break;
-	case THEME_BORDERSIDES:
-		*(entry->ptr.border) = LTK_BORDER_NONE;
-		for (const char *c = value; *c != '\0'; c++) {
-			switch (*c) {
-			case 't':
-				*(entry->ptr.border) |= LTK_BORDER_TOP;
-				break;
-			case 'b':
-				*(entry->ptr.border) |= LTK_BORDER_BOTTOM;
-				break;
-			case 'l':
-				*(entry->ptr.border) |= LTK_BORDER_LEFT;
-				break;
-			case 'r':
-				*(entry->ptr.border) |= LTK_BORDER_RIGHT;
-				break;
-			default:
-				ltk_warn("Invalid value '%s' for property '%s:%s'.\n", value, debug_name, prop);
-				return 1;
-			}
-		}
-		entry->initialized = 1;
-		break;
-	default:
-		ltk_fatal("Invalid theme setting type. This should not happen.\n");
-		/* TODO: ltk_assert(0); */
-	}
-	return 0;
-}
-
-int
-ltk_theme_fill_defaults(ltk_renderdata *renderdata, char *debug_name, ltk_theme_parseinfo *parseinfo, size_t len) {
-	for (size_t i = 0; i < len; i++) {
-		ltk_theme_parseinfo *e = &parseinfo[i];
-		if (e->initialized)
-			continue;
-		switch (e->type) {
-		case THEME_INT:
-			*(e->ptr.i) = e->defaultval.i;
-			e->initialized = 1;
-			break;
-		case THEME_SIZE:
-			*(e->ptr.size) = e->defaultval.size;
-			e->initialized = 1;
-			break;
-		case THEME_STRING:
-			*(e->ptr.str) = ltk_strdup(e->defaultval.str);
-			e->initialized = 1;
-			break;
-		case THEME_COLOR:
-			if (!(*(e->ptr.color) = ltk_color_create(renderdata, e->defaultval.color))) {
-				ltk_warn("Unable to create default color '%s' for property '%s:%s'.\n", e->defaultval.color, debug_name, e->key);
-				return 1;
-			} else {
-				e->initialized = 1;
-			}
-			break;
-		case THEME_BOOL:
-			*(e->ptr.b) = e->defaultval.b;
-			e->initialized = 1;
-			break;
-		case THEME_BORDERSIDES:
-			*(e->ptr.border) = e->defaultval.border;
-			e->initialized = 1;
-			break;
-		default:
-			ltk_fatal("Invalid theme setting type. This should not happen.\n");
-			/* TODO: ltk_assert(0); */
-		}
-	}
-	return 0;
-}
-
-void
-ltk_theme_uninitialize(ltk_renderdata *renderdata, ltk_theme_parseinfo *parseinfo, size_t len) {
-	for (size_t i = 0; i < len; i++) {
-		ltk_theme_parseinfo *e = &parseinfo[i];
-		if (!e->initialized)
-			continue;
-		switch (e->type) {
-		case THEME_STRING:
-			ltk_free(*(e->ptr.str));
-			e->initialized = 0;
-			break;
-		case THEME_COLOR:
-			ltk_color_destroy(renderdata, *(e->ptr.color));
-			e->initialized = 0;
-			break;
-		case THEME_SIZE:
-		case THEME_INT:
-		case THEME_BOOL:
-		case THEME_BORDERSIDES:
-			e->initialized = 0;
-			break;
-		default:
-			ltk_fatal("Invalid theme setting type. This should not happen.\n");
-		}
-	}
-}
diff --git a/src/ltk/theme.h b/src/ltk/theme.h
@@ -1,71 +0,0 @@
-/*
- * Copyright (c) 2022-2024 lumidify <nobody@lumidify.org>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef LTK_THEME_H
-#define LTK_THEME_H
-
-#include <stddef.h>
-#include "color.h"
-#include "graphics.h"
-
-typedef enum {
-	THEME_STRING,
-	THEME_COLOR,
-	THEME_INT,
-	THEME_BOOL,
-	THEME_BORDERSIDES,
-	THEME_SIZE,
-} ltk_theme_datatype;
-
-typedef struct {
-	char *key;
-	ltk_theme_datatype type;
-	/* Note: Bool and int are both integers, but they are
-	   separate just to make it a bit clearer */
-	union {
-		char **str;
-		ltk_color **color;
-		int *i;
-		int *b;
-		ltk_border_sides *border;
-		ltk_size *size;
-	} ptr;
-	/* Note: The default color is also given as a string
-	   because it has to be allocated first (it is only a
-	   different entry in the union in order to make it
-	   a bit clearer) */
-	union {
-		char *str;
-		char *color;
-		int i;
-		int b;
-		ltk_border_sides border;
-		ltk_size size;
-	} defaultval;
-	/* FIXME: min/max doesn't make too much sense for sizes since they
-	   can use different units, but that shouldn't matter for now because
-	   min/max is only used as a sanity check to avoid extreme sizes or
-	   negative sizes where that isn't allowed */
-	int min, max; /* only for integers or sizes */
-	int initialized;
-} ltk_theme_parseinfo;
-
-/* Both return 1 on error, 0 on success */
-int ltk_theme_handle_value(ltk_renderdata *renderdata, char *debug_name, const char *prop, const char *value, ltk_theme_parseinfo *parseinfo, size_t len, int *sorted);
-int ltk_theme_fill_defaults(ltk_renderdata *renderdata, char *debug_name, ltk_theme_parseinfo *parseinfo, size_t len);
-void ltk_theme_uninitialize(ltk_renderdata *renderdata, ltk_theme_parseinfo *parseinfo, size_t len);
-
-#endif /* LTK_THEME_H */
diff --git a/src/ltk/widget_internal.h b/src/ltk/widget_internal.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef LTK_WIDGET_INTERNAL_H
+#define LTK_WIDGET_INTERNAL_H
+
+#include <stddef.h>
+
+#include "array.h"
+#include "config.h"
+#include "window.h"
+#include "graphics.h"
+
+void ltk_window_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);
+void ltk_button_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);
+void ltk_label_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);
+void ltk_menu_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);
+void ltk_menuentry_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);
+void ltk_submenu_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);
+void ltk_submenuentry_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);
+void ltk_scrollbar_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);
+
+void ltk_entry_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);
+void ltk_entry_cleanup(void);
+void ltk_entry_get_keybinding_parseinfo(
+	ltk_keybinding_cb **press_cbs_ret, size_t *press_len_ret,
+	ltk_keybinding_cb **release_cbs_ret, size_t *release_len_ret,
+	ltk_array(keypress) **presses_ret, ltk_array(keyrelease) **releases_ret
+);
+
+void ltk_window_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);
+ltk_window *ltk_window_create_intern(ltk_renderdata *data, const char *title, int x, int y, unsigned int w, unsigned int h);
+void ltk_window_destroy_intern(ltk_window *window);
+void ltk_window_cleanup(void);
+void ltk_window_get_keybinding_parseinfo(
+        ltk_keybinding_cb **press_cbs_ret, size_t *press_len_ret,
+        ltk_keybinding_cb **release_cbs_ret, size_t *release_len_ret,
+        ltk_array(keypress) **presses_ret, ltk_array(keyrelease) **releases_ret
+);
+
+#endif /* LTK_WIDGET_INTERNAL_H */
diff --git a/src/ltk/window.c b/src/ltk/window.c
@@ -22,7 +22,6 @@
 #include "ltk.h"
 #include "util.h"
 #include "array.h"
-#include "theme.h"
 #include "widget.h"
 #include "window.h"
 #include "memory.h"
@@ -126,21 +125,11 @@ static ltk_theme_parseinfo theme_parseinfo[] = {
 	{"font", THEME_STRING, {.str = &theme.font}, {.str = "Monospace"}, 0, 0, 0},
 	{"font-size", THEME_SIZE, {.size = &theme.font_size}, {.size = {.val = 1200, .unit = LTK_UNIT_PT}}, 0, MAX_WINDOW_FONT_SIZE, 0},
 };
-static int theme_parseinfo_sorted = 0;
-
-int
-ltk_window_fill_theme_defaults(ltk_renderdata *data) {
-	return ltk_theme_fill_defaults(data, "window", theme_parseinfo, LENGTH(theme_parseinfo));
-}
-
-int
-ltk_window_ini_handler(ltk_renderdata *data, const char *prop, const char *value) {
-	return ltk_theme_handle_value(data, "window", prop, value, theme_parseinfo, LENGTH(theme_parseinfo), &theme_parseinfo_sorted);
-}
 
 void
-ltk_window_uninitialize_theme(ltk_renderdata *data) {
-	ltk_theme_uninitialize(data, theme_parseinfo, LENGTH(theme_parseinfo));
+ltk_window_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len) {
+	*p = theme_parseinfo;
+	*len = LENGTH(theme_parseinfo);
 }
 
 /* FIXME: maybe ltk_fatal if ltk not initialized? */
@@ -631,8 +620,8 @@ ltk_window_create_intern(ltk_renderdata *data, const char *title, int x, int y, 
 	window->popups_num = window->popups_alloc = 0;
 	window->popups_locked = 0;
 
-	ltk_config *config = ltk_config_get();
-	unsigned int dpi = (unsigned int)round(config->general.dpi_scale * config->general.fixed_dpi);
+	ltk_general_config *config = ltk_config_get_general();
+	unsigned int dpi = (unsigned int)round(config->dpi_scale * config->fixed_dpi * 5);
 	window->renderwindow = ltk_renderer_create_window(data, title, x, y, w, h, dpi);
 	ltk_renderer_set_window_properties(window->renderwindow, theme.bg);
 	window->theme = &theme;
@@ -894,8 +883,8 @@ static int
 prev_child(ltk_window *window) {
 	if (!window->root_widget)
 		return 0;
-	ltk_config *config = ltk_config_get();
-	ltk_widget_flags act_flags = config->general.all_activatable ? LTK_ACTIVATABLE_ALWAYS : LTK_ACTIVATABLE_NORMAL;
+	ltk_general_config *config = ltk_config_get_general();
+	ltk_widget_flags act_flags = config->all_activatable ? LTK_ACTIVATABLE_ALWAYS : LTK_ACTIVATABLE_NORMAL;
 	ltk_widget *new, *cur = window->active_widget;
 	int changed = 0;
 	ltk_widget *prevcur = cur;
@@ -955,8 +944,8 @@ static int
 next_child(ltk_window *window) {
 	if (!window->root_widget)
 		return 0;
-	ltk_config *config = ltk_config_get();
-	ltk_widget_flags act_flags = config->general.all_activatable ? LTK_ACTIVATABLE_ALWAYS : LTK_ACTIVATABLE_NORMAL;
+	ltk_general_config *config = ltk_config_get_general();
+	ltk_widget_flags act_flags = config->all_activatable ? LTK_ACTIVATABLE_ALWAYS : LTK_ACTIVATABLE_NORMAL;
 	ltk_widget *new, *cur = window->active_widget;
 	int changed = 0;
 	ltk_widget *prevcur = cur;
@@ -1040,8 +1029,8 @@ static int
 left_top_child(ltk_window *window, int left) {
 	if (!window->root_widget)
 		return 0;
-	ltk_config *config = ltk_config_get();
-	ltk_widget_flags act_flags = config->general.all_activatable ? LTK_ACTIVATABLE_ALWAYS : LTK_ACTIVATABLE_NORMAL;
+	ltk_general_config *config = ltk_config_get_general();
+	ltk_widget_flags act_flags = config->all_activatable ? LTK_ACTIVATABLE_ALWAYS : LTK_ACTIVATABLE_NORMAL;
 	ltk_widget *new, *cur = window->active_widget;
 	ltk_rect old_rect = {0, 0, 0, 0};
 	ltk_widget *last_activatable = NULL;
@@ -1103,8 +1092,8 @@ static int
 right_bottom_child(ltk_window *window, int right) {
 	if (!window->root_widget)
 		return 0;
-	ltk_config *config = ltk_config_get();
-	ltk_widget_flags act_flags = config->general.all_activatable ? LTK_ACTIVATABLE_ALWAYS : LTK_ACTIVATABLE_NORMAL;
+	ltk_general_config *config = ltk_config_get_general();
+	ltk_widget_flags act_flags = config->all_activatable ? LTK_ACTIVATABLE_ALWAYS : LTK_ACTIVATABLE_NORMAL;
 	ltk_widget *new, *cur = window->active_widget;
 	int changed = 0;
 	ltk_rect old_rect = {0, 0, 0, 0};
diff --git a/src/ltk/window.h b/src/ltk/window.h
@@ -61,10 +61,7 @@ typedef struct ltk_window {
 	char popups_locked;
 } ltk_window;
 
-/* FIXME: should be private to ltk */
-ltk_window *ltk_window_create_intern(ltk_renderdata *data, const char *title, int x, int y, unsigned int w, unsigned int h);
-void ltk_window_destroy_intern(ltk_window *window);
-
+/* FIXME: which of these should be internal to LTK? */
 void ltk_window_handle_event(ltk_window *window, ltk_event *event);
 void ltk_window_fake_motion_event(ltk_window *window, int x, int y);
 
@@ -84,16 +81,6 @@ void ltk_window_register_popup(ltk_window *window, ltk_widget *popup);
 void ltk_window_unregister_popup(ltk_window *window, ltk_widget *popup);
 void ltk_window_unregister_all_popups(ltk_window *window);
 
-/* FIXME: these should be private to ltk */
-void ltk_window_cleanup(void);
-void ltk_window_get_keybinding_parseinfo(
-	ltk_keybinding_cb **press_cbs_ret, size_t *press_len_ret,
-	ltk_keybinding_cb **release_cbs_ret, size_t *release_len_ret,
-	ltk_array(keypress) **presses_ret, ltk_array(keyrelease) **releases_ret
-);
-int ltk_window_fill_theme_defaults(ltk_renderdata *data);
-int ltk_window_ini_handler(ltk_renderdata *data, const char *prop, const char *value);
-void ltk_window_uninitialize_theme(ltk_renderdata *data);
 ltk_window_theme *ltk_window_get_theme(void);
 
 #endif  /* LTK_WINDOW_H */