// various UI elements used by ted

#include "ted-internal.h"

#if __unix__
#include <fcntl.h>
#include <unistd.h>
#endif

struct Selector {
	SelectorEntry *entries;
	char *search_term;
	Rect bounds;
	u32 cursor;
	float scroll;
	bool enable_cursor;
};

struct FileSelector {
	char title[32];
	Selector sel;
	Rect bounds;
	char cwd[TED_PATH_MAX];
	/// indicates that this is for creating files, not opening files
	bool create_menu;
};

static Status file_selector_cd_(Ted *ted, FileSelector *fs, const char *path, int symlink_depth);

static void selector_init(Selector *s) {
	s->enable_cursor = true;
}

Selector *selector_new(void) {
	Selector *s = calloc(1, sizeof *s);
	selector_init(s);
	return s;
}

void selector_free(Selector *s) {
	selector_clear(s);
	free(s);
}

void selector_clear_entries(Selector *s) {
	arr_foreach_ptr(s->entries, SelectorEntry, e) {
		free((void *)e->name);
		free((void *)e->detail);
	}
	arr_clear(s->entries);
}

void selector_clear(Selector *s) {
	selector_clear_entries(s);
	free(s->search_term);
	s->search_term = NULL;
	s->scroll = 0;
	s->cursor = 0;
}

void selector_set_cursor(Selector *s, u32 pos) {
	s->cursor = pos;
}

u32 selector_get_cursor(Selector *s) {
	return s->cursor;
}

Status selector_get_entry(Selector *s, u32 index, SelectorEntry *entry) {
	if (index >= arr_len(s->entries))
		return false;
	*entry = s->entries[index];
	return true;
}

void selector_set_bounds(Selector *s, Rect bounds) {
	s->bounds = bounds;
}

Status selector_get_cursor_entry(Selector *s, SelectorEntry *entry) {
	return selector_get_entry(s, s->cursor, entry);
}

FileSelector *file_selector_new(void) {
	FileSelector *s = calloc(1, sizeof *s);
	selector_init(&s->sel);
	return s;
}

void file_selector_set_create(FileSelector *s, bool create) {
	s->create_menu = create;
}

void file_selector_free(FileSelector *s) {
	file_selector_clear(s);
	free(s);
}

void file_selector_set_bounds(FileSelector *s, Rect bounds) {
	s->bounds = bounds;
}

void file_selector_set_title(FileSelector *s, const char *title) {
	strbuf_cpy(s->title, title);
}

static float selector_entries_start_y(Ted *ted, const Selector *s) {
	float padding = ted_active_settings(ted)->padding;

	return s->bounds.pos.y
		+ ted_line_buffer_height(ted) + padding; // make room for line buffer
}

// number of entries that can be displayed on the screen
static u32 selector_max_displayable_entries(Ted *ted, const Selector *s) {
	float char_height = text_font_char_height(ted->font);
	float entries_h = rect_y2(s->bounds) - selector_entries_start_y(ted, s);
	return (u32)(entries_h / char_height);
}

static bool selector_show_entry(Selector *s, const SelectorEntry *e) {
	return !s->search_term || strstr_case_insensitive(e->name, s->search_term);
}

static u32 selector_filtered_entry_count(Selector *s) {
	u32 count = 0;
	arr_foreach_ptr(s->entries, const SelectorEntry, e) {
		count += selector_show_entry(s, e);
	}
	return count;
}

static void selector_clamp_scroll(Ted *ted, Selector *s) {
	float max_scroll = (float)selector_filtered_entry_count(s) - (float)selector_max_displayable_entries(ted, s);
	if (max_scroll < 0) max_scroll = 0;
	s->scroll = clampf(s->scroll, 0, max_scroll);
}

static void selector_scroll_to_cursor(Ted *ted, Selector *s) {
	u32 max_entries = selector_max_displayable_entries(ted, s);
	float scrolloff = ted_active_settings(ted)->scrolloff;
	float min_scroll = (float)s->cursor - ((float)max_entries - scrolloff);
	float max_scroll = (float)s->cursor - scrolloff;
	s->scroll = clampf(s->scroll, min_scroll, max_scroll);
	selector_clamp_scroll(ted, s);
}

static void selector_move(Ted *ted, Selector *s, i32 direction) {
	assert(direction == -1 || direction == 1);
	if (!s->enable_cursor || selector_filtered_entry_count(s) == 0)
		return;
	
	do
		s->cursor = (u32)mod_i64((i64)s->cursor + direction, arr_len(s->entries));
	while (!selector_show_entry(s, &s->entries[s->cursor]));
	selector_scroll_to_cursor(ted, s);
}

void selector_up(Ted *ted, Selector *s) {
	selector_move(ted, s, -1);
}

void selector_down(Ted *ted, Selector *s) {
	selector_move(ted, s, 1);
}

void selector_home(Ted *ted, Selector *s) {
	if (!s->enable_cursor || selector_filtered_entry_count(s) == 0)
		return;
	
	s->cursor = 0;
	while (!selector_show_entry(s, &s->entries[s->cursor]))
		++s->cursor;
	selector_scroll_to_cursor(ted, s);
}

void selector_end(Ted *ted, Selector *s) {
	if (!s->enable_cursor || selector_filtered_entry_count(s) == 0)
		return;
	
	s->cursor = arr_len(s->entries) - 1;
	while (!selector_show_entry(s, &s->entries[s->cursor]))
		--s->cursor;
	selector_scroll_to_cursor(ted, s);
}

static int selectory_entry_cmp_name(void *context, const void *av, const void *bv) {
	const Selector *s = context;
	const SelectorEntry *a = av, *b = bv;
	
	// put exact match of search term first
	if (s->search_term && streq(a->name, s->search_term))
		return -1;
	if (s->search_term && streq(b->name, s->search_term))
		return 1;

	return strcmp_case_insensitive(a->name, b->name);
}

void selector_sort_entries_by_name(Selector *s) {
	qsort_with_context(s->entries, arr_len(s->entries), sizeof *s->entries, selectory_entry_cmp_name, s);
}

static Rect selector_entry_rect_unclipped(Ted *ted, Selector *s, u32 i_display) {
	float char_height = text_font_char_height(ted->font);
	Rect r = {
		.pos = {
			s->bounds.pos.x,
			selector_entries_start_y(ted, s) + char_height * (
				(float)i_display - s->scroll
			)
		},
		.size = {
			s->bounds.size.x,
			char_height,
		}
	};
	return r;
}

static Rect selector_entry_rect_clipped(Ted *ted, Selector *s, u32 i_display) {
	Rect r = selector_entry_rect_unclipped(ted, s, i_display);
	Rect entry_bounds = s->bounds;
	entry_bounds.pos.y = selector_entries_start_y(ted, s);
	entry_bounds.size.y -= entry_bounds.pos.y - s->bounds.pos.y;
	rect_clip_to_rect(&r, entry_bounds);
	return r;
}

char *selector_update(Ted *ted, Selector *s) {
	char *ret = NULL;
	TextBuffer *line_buffer = ted->line_buffer;
	{
		char *prev_search_term = s->search_term;
		s->search_term = buffer_get_line_utf8(line_buffer, 0);
		if (prev_search_term && !streq(prev_search_term, s->search_term)) {
			// reset cursor because not doing it looks weird
			selector_home(ted, s);
		}
		free(prev_search_term);
	}
	
	ted->selector_open = s;
	u32 i_display = 0;
	arr_foreach_ptr(s->entries, const SelectorEntry, e) {
		if (!selector_show_entry(s, e)) continue;
		Rect entry_rect = selector_entry_rect_clipped(ted, s, i_display++);
		
		// check if this entry was clicked on
		if (ted_clicked_in_rect(ted, entry_rect)) {
			// this option was selected
			s->cursor = (u32)(e - s->entries); // indicate the index of the selected entry using s->cursor
			ret = str_dup(e->name);
			break;
		}
	}

	if (line_buffer_is_submitted(line_buffer)) {
		line_buffer_clear_submitted(line_buffer);
		if (!ret) {
			if (s->enable_cursor) {
				// select this option
				if (s->cursor < arr_len(s->entries))
					ret = str_dup(s->entries[s->cursor].name);

			} else {
				// user typed in submission
				ret = buffer_get_line_utf8(line_buffer, 0);
			}
		}
	}

	// apply scroll
	float scroll_speed = 2.5f;
	s->scroll += scroll_speed * (float)ted->scroll_total_y;
	selector_clamp_scroll(ted, s);
	return ret;
}

void selector_render(Ted *ted, Selector *s) {
	const Settings *settings = ted_active_settings(ted);
	Font *font = ted->font;
	float padding = settings->padding;

	Rect bounds = s->bounds;
	
	if (arr_len(s->entries)) {
		// clamp cursor
		s->cursor = clamp_u32(s->cursor, 0, arr_len(s->entries) - 1);
		
		// make sure cursor points to an entry in the filtered list
		u32 prev = U32_MAX;
		for (u32 i = 0; i < arr_len(s->entries); ++i) {
			if (selector_show_entry(s, &s->entries[i]))
				prev = i;
			if (i >= s->cursor && prev != U32_MAX) {
				s->cursor = prev;
				break;
			}
		}
		if (!selector_show_entry(s, &s->entries[s->cursor])) {
			s->cursor = prev;
		}
	}
	
	
	float x1, y1, x2, y2;
	rect_coords(bounds, &x1, &y1, &x2, &y2);

	{
		float line_buffer_height = ted_line_buffer_height(ted);
		buffer_render(ted->line_buffer, rect4(x1, y1, x2, y1 + line_buffer_height));
		y1 += line_buffer_height;
	}
	
	TextRenderState text_state = text_render_state_default;
	text_state.min_x = x1;
	text_state.max_x = x2;
	text_state.min_y = selector_entries_start_y(ted, s);
	text_state.max_y = y2;

	// render entries themselves
	u32 i_display = 0;
	for (u32 i = 0; i < arr_len(s->entries); ++i) {
		const SelectorEntry *entry = &s->entries[i];
		if (!selector_show_entry(s, entry)) continue;
		Rect r_unclipped = selector_entry_rect_unclipped(ted, s, i_display);
		Rect r_clipped = selector_entry_rect_clipped(ted, s, i_display);
		++i_display;
		if (r_clipped.size.x * r_clipped.size.y <= 0) continue;
		float x = r_unclipped.pos.x, y = r_unclipped.pos.y;
		text_state.x = x; text_state.y = y;
		
		if (ted_mouse_in_rect(ted, r_clipped) || (s->enable_cursor && s->cursor == i)) {
			// highlight it
			gl_geometry_rect(r_clipped, settings_color(settings, COLOR_MENU_HL));
		}
		
		// draw name
		settings_color_floats(settings, entry->color ? entry->color : COLOR_TEXT, text_state.color);
		text_state_break_kerning(&text_state);
		text_utf8_with_state(font, &text_state, entry->name);
		
		if (entry->detail) {
			// draw detail
			const float detail_size = text_get_size_vec2(font, entry->detail).x;
			TextRenderState detail_state = text_state;
			detail_state.x = floor(maxd(text_state.x + 2 * padding, detail_state.max_x - detail_size));
			
			settings_color_floats(settings, COLOR_COMMENT, detail_state.color);
			text_state_break_kerning(&detail_state);
			text_utf8_with_state(font, &detail_state, entry->detail);
		}
	}
	gl_geometry_draw();
	text_render(font);
}

void file_selector_clear(FileSelector *fs) {
	selector_clear(&fs->sel);
	memset(fs, 0, sizeof *fs);
}

static int file_selector_entry_cmp(void *context, const SelectorEntry *a, const SelectorEntry *b) {
	const Selector *s = context;
	FsType a_type = (FsType)a->userdata, b_type = (FsType)b->userdata;


	// put exact match of search term first
	if (s->search_term && streq(a->name, s->search_term))
		return -1;
	if (s->search_term && streq(b->name, s->search_term))
		return 1;

	// put directories first
	if (a_type == FS_DIRECTORY && b_type != FS_DIRECTORY) {
		return -1;
	}
	if (a_type != FS_DIRECTORY && b_type == FS_DIRECTORY) {
		return +1;
	}
	return strcmp_case_insensitive(a->name, b->name);
}

// cd to the directory `name`. `name` cannot include any path separators.
static Status file_selector_cd1(Ted *ted, FileSelector *fs, const char *name, size_t name_len, int symlink_depth) {
	char *const cwd = fs->cwd;

	if (name_len == 0 || (name_len == 1 && name[0] == '.')) {
		// no name, or .
		return true;
	}

	if (name_len == 1 && name[0] == '~') {
		// just in case the user's HOME happens to be accidentally set to, e.g. '/foo/~', make
		// sure we don't recurse infinitely
		if (symlink_depth < 32) {
			return file_selector_cd_(ted, fs, ted->home, symlink_depth + 1);
		} else {
			return false;
		}
	}

	if (name_len == 2 && name[0] == '.' && name[1] == '.') {
		// ..
		char *last_sep = strrchr(cwd, PATH_SEPARATOR);
		if (last_sep) {
			if (last_sep == cwd // this is the starting "/" of a path
			#if _WIN32
				|| (last_sep == cwd + 2 && cwd[1] == ':') // this is the \ of C:\  .
			#endif
				) {
				last_sep[1] = '\0'; // include the last separator
			} else {
				last_sep[0] = '\0';
			}
		}
	} else {
		char path[TED_PATH_MAX];
		strbuf_cpy(path, cwd);
		if (path[strlen(path) - 1] != PATH_SEPARATOR)
			strbuf_catf(path, "%c", PATH_SEPARATOR);
		strbuf_catf(path, "%*s", (int)name_len, name);
		
		if (fs_path_type(path) != FS_DIRECTORY) {
			// trying to cd to something that's not a directory!
			return false;
		}

		#if __unix__
		if (symlink_depth < 32) { // on my system, MAXSYMLINKS is 20, so this should be plenty
			char link_to[TED_PATH_MAX];
			ssize_t bytes = readlink(path, link_to, sizeof link_to);
			if (bytes != -1) {
				// this is a symlink
				link_to[bytes] = '\0';
				return file_selector_cd_(ted, fs, link_to, symlink_depth + 1);
			}
		} else {
			return false;
		}
		#else
		(void)symlink_depth;
		#endif

		// add path separator to end if not already there (which could happen in the case of /)
		if (cwd[strlen(cwd) - 1] != PATH_SEPARATOR)
			str_catf(cwd, sizeof fs->cwd, "%c", PATH_SEPARATOR);
		// add name itself
		strn_cat(cwd, sizeof fs->cwd, name, name_len);
	}
	return true;
	
}

static Status file_selector_cd_(Ted *ted, FileSelector *fs, const char *path, int symlink_depth) {
	char *const cwd = fs->cwd;
	if (path[0] == '\0') return true;

	if (path_is_absolute(path)) {
		// absolute path (e.g. /foo, c:\foo)
		// start out by replacing cwd with the start of the absolute path
		if (path[0] == PATH_SEPARATOR) {
			char root[TED_PATH_MAX];
			// necessary because the full path of \ on windows isn't just \, it's c:\ or something
			char pathsep[] = {PATH_SEPARATOR, '\0'};
			ted_path_full(ted, pathsep, root, sizeof root);
			strcpy(cwd, root);
			path += 1;
		}
		#if _WIN32
		else {
			cwd[0] = '\0';
			strn_cat(cwd, sizeof fs->cwd, path, 3);
			path += 3;
		}
		#endif
	}

	const char *p = path;

	while (*p) {
		size_t len = strcspn(p, ALL_PATH_SEPARATORS);
		if (!file_selector_cd1(ted, fs, p, len, symlink_depth))
			return false;
		p += len;
		p += strspn(p, ALL_PATH_SEPARATORS);
	}
	
	return true;
}

// go to the directory `path`. make sure `path` only contains path separators like PATH_SEPARATOR, not any
// other members of ALL_PATH_SEPARATORS
// returns false if this path doesn't exist or isn't a directory
static bool file_selector_cd(Ted *ted, FileSelector *fs, const char *path) {
	fs->sel.cursor = 0;
	fs->sel.scroll = 0;
	return file_selector_cd_(ted, fs, path, 0);
}

static ColorSetting color_setting_for_file_type(FsType type) {
	switch (type) {
	case FS_FILE: return COLOR_TEXT;
	case FS_DIRECTORY: return COLOR_TEXT_FOLDER;
	default: return COLOR_TEXT_OTHER;
	}
}

void selector_sort_entries(Selector *s, int (*compar)(void *context, const SelectorEntry *e1, const SelectorEntry *e2), void *context) {
	qsort_with_context(s->entries, arr_len(s->entries), sizeof *s->entries, (int (*) (void *, const void *, const void *))compar, context);
}

char *file_selector_update(Ted *ted, FileSelector *fs) {
	TextBuffer *line_buffer = ted->line_buffer;
	String32 search_term32 = buffer_get_line(line_buffer, 0);
	fs->sel.enable_cursor = !fs->create_menu || search_term32.len == 0;
	char *const cwd = fs->cwd;

	if (cwd[0] == '\0') {
		// set the file selector's directory to our current directory.
		str_cpy(cwd, sizeof fs->cwd, ted->cwd);
	}
	

	// check if the search term contains a path separator. if so, cd to the dirname.
	u32 first_path_sep = U32_MAX, last_path_sep = U32_MAX;
	for (u32 i = 0; i < search_term32.len; ++i) {
		char32_t c = search_term32.str[i];
		if (c < CHAR_MAX && is_path_separator((char)c)) {
			last_path_sep = i;
			if (first_path_sep == U32_MAX)
				first_path_sep = i;
		}
	}

	if (last_path_sep != U32_MAX) {
		bool include_last_path_sep = last_path_sep == 0;
		String32 dir_name32 = str32_substr(search_term32, 0, last_path_sep + include_last_path_sep);
		char *dir_name = str32_to_utf8_cstr(dir_name32);
		if (dir_name) {
			// replace all members of ALL_PATH_SEPARATORS with PATH_SEPARATOR in dir_name (i.e. change / to \ on windows)
			for (char *p = dir_name; *p; ++p)
				if (is_path_separator(*p))
					*p = PATH_SEPARATOR;

			if (file_selector_cd(ted, fs, dir_name)) {
				buffer_delete_chars_at_pos(line_buffer, buffer_pos_start_of_file(line_buffer), last_path_sep + 1); // delete up to and including the last path separator
				buffer_clear_undo_redo(line_buffer);			
			} else {
				// delete up to first path separator in line buffer
				BufferPos pos = {.line = 0, .index = first_path_sep};
				size_t nchars = search_term32.len - first_path_sep;
				buffer_delete_chars_at_pos(line_buffer, pos, (i64)nchars);
			}
			free(dir_name);
		}
	}

	char *option_chosen = selector_update(ted, &fs->sel);

	if (option_chosen) {
		char path[TED_PATH_MAX];
		path_full(cwd, option_chosen, path, sizeof path);
		char *ret = NULL;

		switch (fs_path_type(path)) {
		case FS_NON_EXISTENT:
		case FS_OTHER:
			if (fs->create_menu)
				ret = str_dup(path); // you can only select non-existent things if this is a create menu
			break;
		case FS_FILE:
			// selected a file!
			ret = str_dup(path);
			break;
		case FS_DIRECTORY:
			// cd there
			file_selector_cd(ted, fs, option_chosen);
			buffer_clear(line_buffer);
			break;
		}
		
		free(option_chosen);
		if (ret) {
			return ret;
		}
	}
	
	// free previous entries
	selector_clear_entries(&fs->sel);
	
	// get new entries
	FsDirectoryEntry **files;
	// if the directory we're in gets deleted, go back a directory.
	for (u32 i = 0; i < 100; ++i) {
		files = fs_list_directory(cwd);
		if (files) break;
		else if (i == 0) {
			if (fs_path_type(cwd) == FS_NON_EXISTENT)
				ted_error(ted, "%s is not a directory.", cwd);
			else
				ted_error(ted, "Can't list directory %s.", cwd);
		}
		file_selector_cd(ted, fs, "..");
	}

	if (files) {
		for (u32 i = 0; files[i]; ++i) {
			char *name = files[i]->name;
			if (streq(name, ".")) {
				continue;
			}
			SelectorEntry entry = {
				.color = color_setting_for_file_type(files[i]->type),
				.name = name,
				.userdata = files[i]->type,
			};
			selector_add_entry(&fs->sel, &entry);
		}
		selector_sort_entries(&fs->sel, file_selector_entry_cmp, &fs->sel);

		for (u32 i = 0; files[i]; ++i)
			free(files[i]);
		free(files);
		// set cwd to this (if no buffers are open, the "open" menu should use the last file selector's cwd)
		strbuf_cpy(ted->cwd, cwd);
	} else {
		ted_error(ted, "Couldn't list directory '%s'.", cwd);
	}
	
	return NULL;
}

void file_selector_render(Ted *ted, FileSelector *fs) {
	const Settings *settings = ted_active_settings(ted);
	Rect bounds = fs->bounds;
	Font *font = ted->font, *font_bold = ted->font_bold;
	float padding = settings->padding;
	float char_height = text_font_char_height(font);
	
	if (*fs->title) {
		text_utf8(font_bold, fs->title, bounds.pos.x, bounds.pos.y, settings_color(settings, COLOR_TEXT));
		rect_shrink_top(&bounds, text_font_char_height(font_bold) * 0.75f + padding);
	}

	// current working directory
	{
		const char *cwd = fs->cwd;
		const float text_width = text_get_size_vec2(font, cwd).x;
		TextRenderState	state = text_render_state_default;
		state.x = bounds.pos.x;
		if (text_width > bounds.size.x) {
			// very long cwd
			// make sure the end of the cwd is shown
			state.x = rect_x2(bounds) - text_width - padding;
		}
		state.y = bounds.pos.y;
		settings_color_floats(settings, COLOR_TEXT, state.color);
		state.min_x = bounds.pos.x;
		state.max_x = rect_x2(bounds);
		
		text_utf8_with_state(font, &state, fs->cwd);
		rect_shrink_top(&bounds, char_height + padding);
	}

	// render selector
	Selector *sel = &fs->sel;
	sel->bounds = bounds;
	selector_render(ted, sel);
	text_render(font_bold);
}

vec2 button_get_size(Ted *ted, const char *text) {
	float border_thickness = ted_active_settings(ted)->border_thickness;
	return vec2_add_const(text_get_size_vec2(ted->font, text), 2 * border_thickness);
}

void button_render(Ted *ted, Rect button, const char *text, u32 color) {	
	if (ted_mouse_in_rect(ted, button)) {
		// highlight button when hovering over it
		u32 new_color = (color & 0xffffff00) | ((color & 0xff) / 3);
		gl_geometry_rect(button, new_color);
	}
	
	gl_geometry_rect_border(button, ted_active_settings(ted)->border_thickness, ted_active_color(ted, COLOR_BORDER));
	gl_geometry_draw();

	vec2 pos = rect_center(button);
	text_utf8_anchored(ted->font, text, pos.x, pos.y, color, ANCHOR_MIDDLE);
	text_render(ted->font);
}

bool button_update(Ted *ted, Rect button) {
	return ted_clicked_in_rect(ted, button);
}

static void popup_get_rects(Ted const *ted, u32 options, Rect *popup, Rect *button_yes, Rect *button_no, Rect *button_cancel) {
	float window_width = ted->window_width, window_height = ted->window_height;
	
	*popup = rect_centered((vec2){window_width * 0.5f, window_height * 0.5f}, (vec2){300, 200});
	float button_height = 30;
	u16 nbuttons = util_popcount(options);
	float button_width = popup->size.x / nbuttons;
	popup->size = vec2_clamp(popup->size, (vec2){0, 0}, (vec2){window_width, window_height});
	Rect r = rect_xywh(popup->pos.x, rect_y2(*popup) - button_height, button_width, button_height);
	if (options & POPUP_YES) {
		*button_yes = r;
		r.pos.x += button_width;
	}
	if (options & POPUP_NO) {
		*button_no = r;
		r.pos.x += button_width;
	}
	if (options & POPUP_CANCEL) {
		*button_cancel = r;
		r.pos.x += button_width;
	}	
}

PopupOption popup_update(Ted *ted, u32 options) {
	Rect r = {0}, button_yes = {0}, button_no = {0}, button_cancel = {0};
	popup_get_rects(ted, options, &r, &button_yes, &button_no, &button_cancel);
	if (button_update(ted, button_yes))
		return POPUP_YES;
	if (button_update(ted, button_no))
		return POPUP_NO;
	if (button_update(ted, button_cancel))
		return POPUP_CANCEL;
	return POPUP_NONE;
}

void popup_render(Ted *ted, u32 options, const char *title, const char *body) {
	float window_width = ted->window_width;
	Font *font = ted->font;
	Font *font_bold = ted->font_bold;
	Rect r, button_yes, button_no, button_cancel;
	const Settings *settings = ted_active_settings(ted);
	const float char_height_bold = text_font_char_height(font_bold);
	const float padding = settings->padding;
	const float border_thickness = settings->border_thickness;
	
	popup_get_rects(ted, options, &r, &button_yes, &button_no, &button_cancel);
	
	
	float y = r.pos.y;
	
	// popup rectangle
	gl_geometry_rect(r, settings_color(settings, COLOR_MENU_BG));
	gl_geometry_rect_border(r, border_thickness, settings_color(settings, COLOR_BORDER));
	// line separating text from body
	gl_geometry_rect(rect_xywh(r.pos.x, y + char_height_bold, r.size.x, border_thickness),
		settings_color(settings, COLOR_BORDER));
	
	if (options & POPUP_YES) button_render(ted, button_yes, "Yes", settings_color(settings, COLOR_YES));
	if (options & POPUP_NO) button_render(ted, button_no, "No", settings_color(settings, COLOR_NO));
	if (options & POPUP_CANCEL) button_render(ted, button_cancel, "Cancel", settings_color(settings, COLOR_CANCEL));

	// title text
	vec2 title_size = {0};
	text_get_size(font_bold, title, &title_size.x, &title_size.y);
	vec2 title_pos = {(window_width - title_size.x) * 0.5f, y};
	text_utf8(font_bold, title, title_pos.x, title_pos.y, settings_color(settings, COLOR_TEXT));
	text_render(font_bold);

	// body text
	float text_x1 = rect_x1(r) + padding;
	float text_x2 = rect_x2(r) - padding;

	TextRenderState state = text_render_state_default;
	state.min_x = text_x1;
	state.max_x = text_x2;
	state.wrap = true;
	state.x = text_x1;
	state.y = y + char_height_bold + padding;
	settings_color_floats(settings, COLOR_TEXT, state.color);
	text_utf8_with_state(font, &state, body);

	text_render(font);
}

// returns the size of the checkbox, including the label
vec2 checkbox_frame(Ted *ted, bool *value, const char *label, vec2 pos) {
	Font *font = ted->font;
	float char_height = text_font_char_height(font);
	float checkbox_size = char_height;
	const Settings *settings = ted_active_settings(ted);
	const float padding = settings->padding;
	const float border_thickness = settings->border_thickness;
	const u32 color_text = settings_color(settings, COLOR_TEXT);
	
	Rect checkbox_rect = rect(pos, (vec2){checkbox_size, checkbox_size});
	
	if (ted_clicked_in_rect(ted, checkbox_rect)) {
		*value = !*value;
	}
	
	checkbox_rect.pos = vec2_add(checkbox_rect.pos, (vec2){0.5f, 0.5f});
	gl_geometry_rect_border(checkbox_rect, border_thickness, color_text);
	if (*value) {
		Rect r = checkbox_rect;
		rect_shrink(&r, border_thickness + 2);
		gl_geometry_rect(r, color_text);
	}
	
	vec2 text_pos = vec2_add(pos, (vec2){checkbox_size + padding * 0.5f, 0});
	vec2 size = text_get_size_vec2(font, label);
	text_utf8(font, label, text_pos.x, text_pos.y, color_text);
	
	gl_geometry_draw();
	text_render(font);
	return vec2_add(size, (vec2){checkbox_size + padding * 0.5f, 0});
}

void selector_add_entry(Selector *s, const SelectorEntry *entry) {
	SelectorEntry s_entry = {
		.color = entry->color,
		.name = str_dup(entry->name),
		.detail = str_dup(entry->detail),
		.userdata = entry->userdata,
	};
	arr_add(s->entries, s_entry);
}