core/src/menu.c

#include <stdlib.h>
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include "panic.h"
#include "font.h"
#include "main.h"
#include "menu.h"

#define PADDING 4

static int option_at(menu *m, int x, int y) {
	float cur_y = PADDING;
	for (int i = 0; i < m->count; i++) {
		float tw, th;
		must(SDL_GetTextureSize(m->textures[i], &tw, &th));
		cur_y += th;
		if (y < cur_y) return i;
	}
	return -1;
}

menu *create_menu(menu_entry *entries, int count, int x, int y) {
	menu *m = calloc(1, sizeof(menu));
	if (!m) panic("out of memory");

	x += 2; y -= 8;
	m->window = must(SDL_CreatePopupWindow(window,
		x, y, 0, 0, SDL_WINDOW_POPUP_MENU | SDL_WINDOW_BORDERLESS
	));
	m->renderer = must(SDL_CreateRenderer(m->window, NULL));

	m->entries = entries;
	m->count = count;
	m->textures = calloc(count, sizeof(SDL_Texture *));
	if (!m->textures) panic("out of memory");

	int w = PADDING, h = PADDING;
	for (int i = 0; i < count; i++) {
		SDL_Color color = {0xff, 0xff, 0xff, 0xff};
		SDL_Surface *text =
			must(TTF_RenderText_Blended(font, m->entries[i].name, 0, color));
		m->textures[i] = must(SDL_CreateTextureFromSurface(m->renderer, text));
		SDL_DestroySurface(text);
		float tw, th;
		must(SDL_GetTextureSize(m->textures[i], &tw, &th));
		w = SDL_max(w, tw);
		h += th;
	}
	w += PADDING; h += PADDING;
	m->height = h;

	int px, py;
	must(SDL_GetWindowPosition(window, &px, &py));
	SDL_Rect disp;
	must(
		SDL_GetDisplayUsableBounds(SDL_GetDisplayForWindow(m->window), &disp)
	);
	if (py + y + h > disp.h) {
		y = disp.h - h - py;
		y = SDL_max(y, disp.y - py);
	}
	if (px + x + w > disp.w) {
		x = disp.w - w - px;
		x = SDL_max(x, disp.x - px);
	}
	h = SDL_min(h, disp.h);
	SDL_WarpMouseInWindow(window, x - 2, y + 8);
	SDL_SetWindowPosition(m->window, x, y);
	SDL_SetWindowSize(m->window, w, h);
	SDL_RaiseWindow(m->window);
	return m;
}

bool menu_handle_event(menu *m, SDL_Event e) {
	switch (e.type) {
	case SDL_EVENT_MOUSE_WHEEL:
		if (e.wheel.windowID != SDL_GetWindowID(m->window)) {
			break;
		}
		int w, h;
		SDL_GetWindowSize(m->window, &w, &h);
		m->scroll += -e.wheel.y * 15;
		m->scroll = SDL_max(m->scroll, 0);
		m->scroll = SDL_min(m->scroll, m->height - h);
		break;
	case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
		if (e.window.windowID == SDL_GetWindowID(m->window))
			return false;
		break;
	case SDL_EVENT_MOUSE_BUTTON_DOWN:
		if (e.button.windowID != SDL_GetWindowID(m->window)) {
			return false;
		}
	case SDL_EVENT_MOUSE_BUTTON_UP:
		if (e.button.windowID != SDL_GetWindowID(m->window)) {
			break;
		}
		int option = option_at(m, e.button.x, e.button.y + m->scroll);
		if (option != -1) {
			m->entries[option].action(m->entries[option].data);
			return false;
		}
		break;
	case SDL_EVENT_KEY_DOWN:
		if (e.key.key == SDLK_ESCAPE) return false;
		break;
	case SDL_EVENT_QUIT:
		return false;
	default:
	}
	return true;
}

void menu_render(menu *m) {
	int ww, wh;
	SDL_GetWindowSize(m->window, &ww, &wh);
	float mx, my;
	SDL_GetMouseState(&mx, &my);
	int hovered = -1;
	if (SDL_GetWindowFlags(m->window) & SDL_WINDOW_MOUSE_FOCUS)
		hovered = option_at(m, mx, my + m->scroll);

	SDL_SetRenderDrawColor(m->renderer, 0x00, 0x00, 0x00, 0xff);
	SDL_RenderClear(m->renderer);
	float x = PADDING, y = PADDING - SDL_floor(m->scroll);
	for (int i = 0; i < m->count; i++) {
		SDL_FRect dst;
		dst.x = x;
		dst.y = y;
		must(SDL_GetTextureSize(m->textures[i], &dst.w, &dst.h));
		if (i == hovered) {
			SDL_FRect highlight_rect = {0, y, ww, dst.h};
			SDL_SetRenderDrawColor(m->renderer, 0x50, 0x50, 0x50, 0xff);
			SDL_RenderFillRect(m->renderer, &highlight_rect);
		}
		SDL_SetRenderDrawColor(m->renderer, 0x00, 0x00, 0x00, 0xff);
		SDL_RenderTexture(m->renderer, m->textures[i], NULL, &dst);
		y += dst.h;
	}
	SDL_FRect border_rect = {0, 0, ww, wh};
	SDL_SetRenderDrawColor(m->renderer, 0x50, 0x50, 0x50, 0xff);
	SDL_RenderRect(m->renderer, &border_rect);
	SDL_RenderPresent(m->renderer);
}

void close_menu(menu *m) {
	for (int i = 0; i < m->count; i++) {
		SDL_DestroyTexture(m->textures[i]);
	}
	free(m->textures);
	SDL_DestroyRenderer(m->renderer);
	SDL_DestroyWindow(m->window);
	free(m);
}