examples

Toy examples in single C files.
git clone git://henryandlizzy.uk/examples
Log | Files | Refs

sdl-gl.cpp (16758B)


      1 #include <GL/gl.h>
      2 #include <SDL2/SDL.h>
      3 #include <SDL2/SDL_events.h>
      4 #include <SDL2/SDL_keycode.h>
      5 #include <SDL2/SDL_mouse.h>
      6 #include <SDL2/SDL_video.h>
      7 #include <err.h>
      8 
      9 #include "coroutine_owner.hpp"
     10 
     11 #include <algorithm>
     12 #include <cassert>
     13 #include <exception>
     14 #include <format>
     15 #include <memory>
     16 #include <numbers>
     17 #include <print>
     18 #include <random>
     19 #include <ranges>
     20 #include <vector>
     21 
     22 #include <unistd.h>
     23 #include <sys/wait.h>
     24 #include <err.h>
     25 
     26 extern "C" char const* __asan_default_options() { return "detect_leaks=0"; }
     27 
     28 struct coroutine_task
     29 {
     30 	struct promise_type
     31 	{
     32 		coroutine_task get_return_object(void) noexcept { return {}; }
     33 		std::suspend_never initial_suspend(void) const noexcept { return {}; }
     34 		std::suspend_never final_suspend() const noexcept { return {}; }
     35 		void unhandled_exception() const noexcept { std::terminate(); }
     36 		void return_void(void) const noexcept {}
     37 	};
     38 };
     39 
     40 template <typename T>
     41 struct awaitable_value
     42 {
     43 	bool ready(T& e)
     44 	{
     45 		assert(not event);
     46 		if (not waiting)
     47 			return false;
     48 
     49 		event = &e;
     50 		waiting.release().resume();
     51 		return true;
     52 	}
     53 	bool operator()(T& e)
     54 	{
     55 		return ready(e);
     56 	}
     57 
     58 	bool await_ready() const
     59 	{
     60 		return event;
     61 	}
     62 
     63 	void await_suspend(std::coroutine_handle<> h)
     64 	{
     65 		assert(not waiting);
     66 		waiting.reset(h);
     67 	};
     68 
     69 	T& await_resume()
     70 	{
     71 		assert(event);
     72 		return *std::exchange(event, nullptr);
     73 	}
     74 
     75 private:
     76 	T* event = nullptr;
     77 	coroutine_owner<> waiting = {};
     78 };
     79 
     80 template <typename T>
     81 struct vec2
     82 {
     83 	T x = 0, y = 0;
     84 	vec2 operator -() const { return {-x, -y}; };
     85 	vec2 operator *(T rhs) const { return {x * rhs, y * rhs}; }
     86 	vec2 operator /(T rhs) const { return {x / rhs, y / rhs}; }
     87 	vec2 operator +(vec2 rhs) const { return {x + rhs.x, y + rhs.y}; }
     88 	vec2 operator -(vec2 rhs) const { return *this + -rhs; }
     89 	vec2& operator *=(T rhs) { return *this = *this * rhs; }
     90 	vec2& operator /=(T rhs) { return *this = *this / rhs; }
     91 	vec2& operator +=(vec2 rhs) { return *this = *this + rhs; }
     92 	vec2& operator -=(vec2 rhs) { return *this = *this - rhs; }
     93 	T magnitude_squared() const { return x*x + y*y; }
     94 };
     95 
     96 using point = vec2<float>;
     97 using screen_coord = vec2<int>;
     98 
     99 screen_coord screen = {800, 450};
    100 float aspect_ratio = (float) screen.x / screen.y;
    101 
    102 bool wireframe;
    103 
    104 SDL_Window* window;
    105 
    106 void initSDL(void)
    107 {
    108 	int windowFlags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE;
    109 
    110 	if (SDL_Init(SDL_INIT_VIDEO) < 0)
    111 		errx(1, "Couldn't initialize SDL: %s\n", SDL_GetError());
    112 
    113 	window = SDL_CreateWindow("SDL OpenGL example", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, screen.x, screen.y, windowFlags);
    114 
    115 	if (!window)
    116 		errx(1, "Failed to open %d x %d window: %s\n", screen.x, screen.y, SDL_GetError());
    117 
    118 	SDL_GLContext context = SDL_GL_CreateContext(window);
    119 	if (not context)
    120 		errx(1, "Failed to create OpenGL context: %s", SDL_GetError());
    121 	if (SDL_GL_MakeCurrent(window, context))
    122 		errx(1, "Failed to make OpenGL context current: %s", SDL_GetError());
    123 }
    124 
    125 std::mt19937 gen;
    126 std::lognormal_distribution<float> dis(-1, .75);
    127 std::normal_distribution<float> norm_dis{0.f, .5f};
    128 
    129 struct rgb
    130 {
    131 	float r,g,b;
    132 
    133 	rgb operator + (rgb const& rhs) const
    134 	{
    135 		return {r + rhs.r, g + rhs.g, b + rhs.b};
    136 	}
    137 
    138 	rgb& operator *=(float rhs) { r *= rhs; g *= rhs; b *= rhs; return *this; }
    139 	rgb& operator /=(float rhs) { r /= rhs; g /= rhs; b /= rhs; return *this; }
    140 	rgb operator *(float rhs) const { return rgb{*this} *= rhs; }
    141 	rgb operator /(float rhs) const { return rgb{*this} /= rhs; }
    142 
    143 	static rgb random()
    144 	{
    145 		return {dis(gen), dis(gen), dis(gen)};
    146 	}
    147 };
    148 
    149 void glColor(rgb const& c)
    150 {
    151 	glColor3f(c.r, c.g, c.b);
    152 }
    153 
    154 
    155 void glVertex(point p)
    156 {
    157 	glVertex2f(p.x, p.y);
    158 }
    159 
    160 point window_to_gl(screen_coord c)
    161 {
    162 	float screen_x = (2.f * c.x / screen.x - 1.f);
    163 	float screen_y = -2.f * c.y / screen.y + 1.f;
    164 	return {screen_x, screen_y};
    165 }
    166 
    167 float scale = .5f;
    168 
    169 point window_to_local(screen_coord c)
    170 {
    171 	auto p = window_to_gl(c);
    172 	p.x *= aspect_ratio;
    173 	return p * (1/scale);
    174 }
    175 
    176 struct rotor
    177 {
    178 	float s, xy;
    179 	static rotor angle(float turns) { float radians = turns * 2 * std::numbers::pi; return {cos(radians), sin(radians)}; }
    180 };
    181 point operator *(point lhs, rotor rhs)
    182 {
    183 	return {lhs.x * rhs.s - lhs.y * rhs.xy, lhs.x * rhs.xy + lhs.y * rhs.s};
    184 }
    185 
    186 point& operator *=(point& lhs, rotor rhs)
    187 {
    188 	return lhs = lhs * rhs;
    189 }
    190 
    191 struct circle
    192 {
    193 	point pos;
    194 	float radius;
    195 
    196 	bool colliding(point p) const
    197 	{
    198 		return (pos - p).magnitude_squared() <= radius * radius;
    199 	}
    200 };
    201 
    202 point pan = {0, 0};
    203 
    204 screen_coord mouse_coord;
    205 point mouse_pos;
    206 
    207 struct shp
    208 {
    209 	std::string name;
    210 	std::vector<point> vertices;
    211 	std::vector<unsigned char> wireframe;
    212 	GLuint mode;
    213 };
    214 
    215 std::vector<shp> shapes;
    216 unsigned current_shape = 0;
    217 
    218 struct object
    219 {
    220 	circle pos;
    221 	rgb col;
    222 	float spin;
    223 	float rotation;
    224 	shp& shape;
    225 };
    226 std::vector<std::shared_ptr<object>> objs = {};
    227 
    228 constexpr auto deref = std::views::transform([](auto const& p) -> auto& { return *p; });
    229 
    230 void presentScene(void)
    231 {
    232 	glClearColor(0, 0, 0, 1);
    233 	glClear(GL_COLOR_BUFFER_BIT);
    234 	for (auto& o : objs | deref) {
    235 		o.rotation += o.spin / 60;
    236 		rotor r = rotor::angle(o.rotation);
    237 		if (o.pos.colliding(mouse_pos))
    238 			glColor(o.col + rgb{.25f, .25f, .25f,});
    239 		else
    240 			glColor(o.col);
    241 
    242 		if (not wireframe)
    243 		{
    244 			glBegin(o.shape.mode);
    245 			for (auto p : o.shape.vertices)
    246 			{
    247 				p *= o.pos.radius;
    248 				auto pp = (p * r + pan + o.pos.pos) * scale;
    249 				glVertex2f(pp.x / aspect_ratio, pp.y);
    250 			}
    251 		}
    252 		else
    253 		{
    254 			glBegin(GL_LINE_LOOP);
    255 			for (auto idx : o.shape.wireframe)
    256 			{
    257 				auto p = o.shape.vertices[idx];
    258 				p *= o.pos.radius;
    259 				auto pp = (p * r + pan + o.pos.pos) * scale;
    260 				glVertex2f(pp.x / aspect_ratio, pp.y);
    261 			}
    262 		}
    263 		glEnd();
    264 	}
    265 	glFinish();
    266 }
    267 
    268 struct panel
    269 {
    270 	screen_coord pos, size;
    271 	rgb colour;
    272 	void draw() const;
    273 	bool colliding(screen_coord c)
    274 	{
    275 		auto br = pos + size;
    276 		return c.x >= pos.x && c.y >= pos.y && c.x < br.x && c.y < br.y;
    277 	}
    278 };
    279 
    280 panel menu;
    281 bool details;
    282 std::vector<panel> panels;
    283 
    284 void panel::draw() const
    285 {
    286 	auto tl = window_to_gl(pos);
    287 	auto tr = window_to_gl({pos.x + size.x, pos.y});
    288 	auto bl = window_to_gl({pos.x, pos.y + size.y});
    289 	auto br = window_to_gl(pos + size);
    290 
    291 	glBegin(GL_TRIANGLE_STRIP);
    292 	glColor(colour);
    293 	glVertex(tl);
    294 	glVertex(tr);
    295 	glVertex(bl);
    296 	glVertex(br);
    297 	glEnd();
    298 	point half{1 / (2.f * screen.x), 1 / (2.f * screen.y)};
    299 
    300 	tl.y -= half.y;
    301 	tr.y -= half.y;
    302 	bl.y += half.y;
    303 	br.y += half.y;
    304 	tr.x -= half.x;
    305 	br.x -= half.x;
    306 	tl.x += half.x;
    307 	bl.x += half.x;
    308 
    309 	glBegin(GL_LINE_STRIP);
    310 	glColor(colour / 2.f);
    311 	glVertex(bl);
    312 	glVertex(br);
    313 	glVertex(tr);
    314 	glEnd();
    315 
    316 	glBegin(GL_LINE_STRIP);
    317 	glColor(colour * 1.5f);
    318 	glVertex(bl);
    319 	glVertex(tl);
    320 	glVertex(tr);
    321 	glEnd();
    322 }
    323 
    324 void key(SDL_KeyboardEvent& e)
    325 {
    326 	static std::uniform_real_distribution<float> size_dis{.5f, 1.5f};
    327 	if (e.repeat)
    328 		return;
    329 	static uint8_t ctrls;
    330 	switch (e.keysym.sym)
    331 	{
    332 	case SDLK_LCTRL:
    333 	case SDLK_RCTRL:
    334 		ctrls += e.state ? 1 : -1;
    335 		break;
    336 
    337 	case SDLK_c:
    338 		if (e.state && ctrls)
    339 			exit(0);
    340 		break;
    341 
    342 	case SDLK_a:
    343 		if (e.state)
    344 			objs.push_back(std::make_shared<object>(circle{mouse_pos, size_dis(gen)}, rgb::random(), norm_dis(gen), 0, shapes[current_shape]));
    345 		break;
    346 
    347 	case SDLK_d:
    348 		if (e.state)
    349 			std::erase_if(objs, [](auto& o) { return o->pos.colliding(mouse_pos); });
    350 		break;
    351 
    352 	case SDLK_r:
    353 		if (e.state)
    354 			for (auto& obj : objs | deref)
    355 				if (obj.pos.colliding(mouse_pos))
    356 				{
    357 					std::swap(obj.col.r, obj.col.g);
    358 					std::swap(obj.col.g, obj.col.b);
    359 				}
    360 		break;
    361 
    362 	case SDLK_EQUALS:
    363 		if (e.state)
    364 			for (auto& obj : objs | deref)
    365 				if (obj.pos.colliding(mouse_pos))
    366 					obj.pos.radius *= 1.2;
    367 		break;
    368 
    369 	case SDLK_MINUS:
    370 		if (e.state)
    371 			for (auto& obj : objs | deref)
    372 				if (obj.pos.colliding(mouse_pos))
    373 					obj.pos.radius /= 1.2;
    374 		break;
    375 
    376 	case SDLK_s:
    377 		if (e.state)
    378 		{
    379 			for (auto& obj : objs | deref)
    380 				if (obj.pos.colliding(mouse_pos))
    381 					obj.spin *= -1;
    382 
    383 		}
    384 		break;
    385 
    386 	case SDLK_w:
    387 		if (e.state)
    388 			wireframe = !wireframe;
    389 		break;
    390 
    391 	case SDLK_ESCAPE:
    392 		exit(0);
    393 	}
    394 }
    395 
    396 void mouse_motion(SDL_MouseMotionEvent& e)
    397 {
    398 	mouse_coord = {e.x, e.y};
    399 	mouse_pos = window_to_local(mouse_coord) - pan;
    400 }
    401 
    402 awaitable_value<SDL_MouseButtonEvent const> click_event;
    403 awaitable_value<SDL_Event const> motion_or_release_event;
    404 
    405 coroutine_task mouse_task()
    406 {
    407 	for (;;)
    408 	{
    409 	loop:
    410 		SDL_MouseButtonEvent const& e = co_await click_event;
    411 		point drag_pos = pan - window_to_local({e.x, e.y});
    412 		if (menu.colliding(mouse_coord))
    413 		{
    414 			if (e.button == SDL_BUTTON_LEFT)
    415 				details = !details;
    416 			continue;
    417 		}
    418 
    419 		for (auto const& [i, p] : std::views::enumerate(panels))
    420 			if (p.colliding(mouse_coord))
    421 			{
    422 				if (e.button == SDL_BUTTON_LEFT)
    423 					current_shape = i;
    424 				goto loop;
    425 			}
    426 
    427 		switch (e.button)
    428 		{
    429 		case SDL_BUTTON_RIGHT:
    430 		{
    431 			for (;;)
    432 			{
    433 				SDL_Event const& e2 = co_await motion_or_release_event;
    434 				if (e2.type == SDL_MOUSEMOTION)
    435 					pan = drag_pos + window_to_local({e2.motion.x, e2.motion.y});
    436 				else if (auto& b = e2.button; b.button == SDL_BUTTON_RIGHT)
    437 					break;
    438 			}
    439 			break;
    440 		}
    441 		case SDL_BUTTON_LEFT:
    442 		{
    443 			auto it = std::ranges::find_if(objs, [](auto const& o){return o->pos.colliding(mouse_pos);});
    444 			if (it == std::end(objs))
    445 				break;
    446 
    447 			drag_pos = (*it)->pos.pos - window_to_local({e.x, e.y});
    448 			std::weak_ptr<object> wp = *it;
    449 
    450 			for (;;)
    451 			{
    452 				SDL_Event const& e2 = co_await motion_or_release_event;
    453 				if (wp.expired())
    454 					break;
    455 				else if (e2.type == SDL_MOUSEMOTION)
    456 					wp.lock()->pos.pos = drag_pos + window_to_local({e2.motion.x, e2.motion.y});
    457 				else if (auto& b = e2.button; b.button == SDL_BUTTON_LEFT)
    458 					break;
    459 			}
    460 			break;
    461 		}
    462 		}
    463 	}
    464 }
    465 
    466 void wheel(SDL_MouseWheelEvent* e)
    467 {
    468 	float factor = exp(0.1f * e->y);
    469 	pan += window_to_local({e->mouseX, e->mouseY}) * (1/factor-1);
    470 	scale *= factor;
    471 }
    472 
    473 void window_event(SDL_WindowEvent const* e)
    474 {
    475 	switch (e->event)
    476 	{
    477 	case SDL_WINDOWEVENT_SIZE_CHANGED:
    478 		screen.x = e->data1;
    479 		screen.y = e->data2;
    480 		aspect_ratio = (float) screen.x / screen.y;
    481 		glViewport(0, 0, screen.x, screen.y);
    482 		return;
    483 	}
    484 }
    485 
    486 void doInput()
    487 {
    488 	SDL_Event event;
    489 	while (SDL_PollEvent(&event))
    490 	{
    491 		switch (event.type)
    492 		{
    493 		case SDL_QUIT:
    494 			exit(0);
    495 		case SDL_KEYDOWN:
    496 		case SDL_KEYUP:
    497 			key(event.key);
    498 			break;
    499 		case SDL_MOUSEMOTION:
    500 			mouse_motion(event.motion);
    501 			motion_or_release_event(event);
    502 			break;
    503 		case SDL_MOUSEBUTTONDOWN:
    504 			click_event.ready(event.button);
    505 			break;
    506 		case SDL_MOUSEBUTTONUP:
    507 			motion_or_release_event(event);
    508 			break;
    509 		case SDL_MOUSEWHEEL:
    510 			wheel(&event.wheel);
    511 			break;
    512 
    513 		case SDL_WINDOWEVENT:
    514 			window_event(&event.window);
    515 			break;
    516 
    517 		default:
    518 			break;
    519 		}
    520 	}
    521 }
    522 
    523 void init_shape()
    524 {
    525 	{
    526 		constexpr int n = 10;
    527 		shapes.emplace_back();
    528 		auto& shape = shapes.back();
    529 		shape.name = "star";
    530 		shape.mode = GL_TRIANGLE_FAN;
    531 		shape.vertices.emplace_back(0,0);
    532 		for (int i = 0; i <= n; ++i)
    533 			shape.vertices.push_back(point{0, (.65f + i % 2) / -1.65f} * rotor::angle((float)i / n));
    534 		for (int i = 0; i < n; ++i)
    535 			shape.wireframe.push_back(i+1);
    536 	}
    537 	{
    538 		constexpr int n = 3;
    539 		shapes.emplace_back();
    540 		auto& shape = shapes.back();
    541 		shape.name = "triangle";
    542 		shape.mode = GL_TRIANGLES;
    543 		for (int i = 0; i < n; ++i)
    544 			shape.vertices.push_back(point{0, 1} * rotor::angle((float)i / n));
    545 		for (int i = 0; i < n; ++i)
    546 			shape.wireframe.push_back(i);
    547 	}
    548 	{
    549 		constexpr int n = 4;
    550 		shapes.emplace_back();
    551 		auto& shape = shapes.back();
    552 		shape.name = "square";
    553 		shape.mode = GL_TRIANGLE_FAN;
    554 		for (int i = 0; i < n; ++i)
    555 			shape.vertices.push_back(point{0, 1} * rotor::angle((float)i / n));
    556 		for (int i = 0; i < n; ++i)
    557 			shape.wireframe.push_back(i);
    558 	}
    559 	{
    560 		constexpr int n = 64;
    561 		shapes.emplace_back();
    562 		auto& shape = shapes.back();
    563 		shape.name = "crescent";
    564 		shape.mode = GL_TRIANGLE_STRIP;
    565 		for (int i = 0; i <= n; ++i)
    566 		{
    567 			float turns = i * .5f / n;
    568 			point p{0, 1};
    569 			p *= rotor::angle(turns);
    570 			p.x *= 1 - i % 2 * .45f;
    571 			shape.vertices.push_back(p);
    572 		}
    573 		for (int i = 0; i <= n / 2; ++i)
    574 			shape.wireframe.push_back(i * 2);
    575 		for (int i = n / 2; i > 0; --i)
    576 			shape.wireframe.push_back(i * 2 - 1);
    577 	}
    578 }
    579 
    580 void init_scene()
    581 {
    582 	objs.push_back(std::make_shared<object>(circle{{-1.5f, -1}, 1.f}, rgb{.7,.2,.2}, .2, 0, shapes[0]));
    583 	objs.push_back(std::make_shared<object>(circle{{0, 1}, .5f}, rgb{.2,.7,.2}, 0, 0, shapes[0]));
    584 	objs.push_back(std::make_shared<object>(circle{{1.5f, -1}, 1.f}, rgb{.2,.2,.7}, -.2, 0, shapes[0]));
    585 }
    586 
    587 struct psf2
    588 {
    589 	unsigned int magic, version, hdr_size, flags, length, g_size, height, width;
    590 };
    591 struct font
    592 {
    593 	psf2 hdr;
    594 	GLuint tex;
    595 	font(char const* filename)
    596 	{
    597 		int fds[2];
    598 		if (pipe(fds))
    599 			err(1, "pipe");
    600 		pid_t child = fork();
    601 		if (child < 0)
    602 			err(1, "fork");
    603 		if (!child)
    604 		{
    605 			if (dup2(fds[1], 1) == -1)
    606 				err(1, "dup2");
    607 			if (close(fds[0]))
    608 				err(1, "close");
    609 			if (close(fds[1]))
    610 				err(1, "close");
    611 			execlp("gunzip", "gunzip", "--stdout", filename, nullptr);
    612 			err(1, "execlp");
    613 		}
    614 		close(fds[1]);
    615 
    616 		if (read(fds[0], &hdr, sizeof(hdr)) != sizeof(hdr))
    617 			err(1, "read");
    618 		if (hdr.magic != 0x864AB572)
    619 			errx(1, "Not a PSF2 file");
    620 
    621 		std::vector<unsigned char> storage(hdr.length * hdr.g_size);
    622 
    623 		for (ssize_t read = 0; read < (ssize_t)storage.size(); )
    624 		{
    625 			int n = ::read(fds[0], storage.data() + read, storage.size() - read);
    626 			if (n < 0)
    627 				err(1, "read");
    628 			if (n == 0)
    629 				errx(1, "not enough data from font");
    630 			read += n;
    631 		}
    632 		close(fds[0]);
    633 		assert(wait(nullptr) == child);
    634 		std::vector<unsigned char> pix(hdr.length * hdr.height * hdr.width);
    635 
    636 		for (size_t i = 0; i < pix.size(); ++i)
    637 			pix[i] = ((storage[i / 8] >> (7 - i % 8)) & 1) * 255;
    638 
    639 		glGenTextures(1, &tex);
    640 		glBindTexture(GL_TEXTURE_2D, tex);
    641 		glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, hdr.width, hdr.height * hdr.length, 0, GL_ALPHA, GL_UNSIGNED_BYTE, pix.data());
    642 		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    643 	}
    644 	screen_coord draw(unsigned char c, screen_coord pos)
    645 	{
    646 		screen_coord br_screen{pos.x + (int)hdr.width, pos.y + (int)hdr.height};
    647 		float text = (float)c / hdr.length;
    648 		float texb = (float)(c + 1u) / hdr.length;
    649 		auto tl = window_to_gl(pos);
    650 		auto tr = window_to_gl({br_screen.x, pos.y});
    651 		auto bl = window_to_gl({pos.x, br_screen.y});
    652 		auto br = window_to_gl(br_screen);
    653 
    654 		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    655 		glEnable(GL_BLEND);
    656 		glEnable(GL_TEXTURE_2D);
    657 		glBegin(GL_TRIANGLE_STRIP);
    658 		glTexCoord2f(0,text);
    659 		glVertex(tl);
    660 		glTexCoord2f(1,text);
    661 		glVertex(tr);
    662 		glTexCoord2f(0,texb);
    663 		glVertex(bl);
    664 		glTexCoord2f(1,texb);
    665 		glVertex(br);
    666 		glEnd();
    667 		glDisable(GL_TEXTURE_2D);
    668 		glDisable(GL_BLEND);
    669 
    670 		return br_screen;
    671 	}
    672 	screen_coord draw(std::string_view str, screen_coord pos)
    673 	{
    674 		for (auto c : str)
    675 			pos.x = draw(c, pos).x;
    676 		return pos;
    677 	}
    678 };
    679 
    680 std::unique_ptr<font> fnt;
    681 
    682 void draw_UI()
    683 {
    684 	std::vector<std::string> strings;
    685 	strings.push_back(std::format("scale: {:.2}", scale));
    686 	strings.push_back(std::format("  pan: ({:.1}, {:.1})", pan.x, pan.y));
    687 	strings.push_back(std::format("{} shapes", shapes.size()));
    688 	strings.push_back(std::format("{} objects", objs.size()));
    689 	int width = (int)(2 + fnt->hdr.width * std::ranges::max(strings | std::views::transform([](auto const& s){ return s.size(); })));
    690 	int height = (int)(2 + fnt->hdr.height * strings.size());
    691 	menu.size = {width, height};
    692 	menu.colour = menu.colliding(mouse_coord) ? rgb{.6, .6, .6} : rgb{.4, .4, .4};
    693 	menu.draw();
    694 	glColor3f(0, 0, 0);
    695 	int y = 1;
    696 	for (auto const& s : strings)
    697 	{
    698 		fnt->draw(s, {1, y});
    699 		y += fnt->hdr.height;
    700 	}
    701 
    702 	y += 5;
    703 
    704 	panels.clear();
    705 
    706 	for (auto const& [i, s] : std::views::enumerate(shapes))
    707 	{
    708 		auto& p = panels.emplace_back(
    709 			screen_coord{0, y},
    710 			screen_coord{width, (int)fnt->hdr.height},
    711 			rgb{.4, .4, .4}
    712 		);
    713 		if (p.colliding(mouse_coord))
    714 			p.colour = rgb{.3, .6, .3};
    715 		p.draw();
    716 		if (current_shape == i)
    717 			glColor3f(1, 1, 1);
    718 		else
    719 			glColor3f(0, 0, 0);
    720 		fnt->draw(s.name, {1, y});
    721 		y += fnt->hdr.height;
    722 	}
    723 	if (not details)
    724 		return;
    725 
    726 	auto x = (int)width;
    727 	strings.clear();
    728 	for (auto const& [i, o] : objs | deref | std::views::enumerate)
    729 		strings.push_back(std::format("{}: size {}", i, o.pos.radius));
    730 
    731 	if (strings.empty())
    732 		return;
    733 
    734 	width = (int)(2 + fnt->hdr.width * std::ranges::max(strings | std::views::transform([](auto const& s){ return s.size(); })));
    735 	height = (int)(2 + fnt->hdr.height * strings.size());
    736 	panel{{x, 0}, {width, height}, rgb{.4, .4, .4}}.draw();
    737 	glColor3f(0, 0, 0);
    738 	y = 1;
    739 	for (auto const& s : strings)
    740 	{
    741 		fnt->draw(s, {x + 1, y});
    742 		y += fnt->hdr.height;
    743 	}
    744 }
    745 
    746 int main()
    747 {
    748 	init_shape();
    749 	init_scene();
    750 	mouse_task();
    751 	initSDL();
    752 	atexit(SDL_Quit);
    753 
    754 	fnt.reset(new font{"/usr/share/kbd/consolefonts/lat9-16.psf.gz"});
    755 
    756 	for (;;)
    757 	{
    758 		doInput();
    759 		presentScene();
    760 		draw_UI();
    761 		SDL_GL_SwapWindow(window);
    762 	}
    763 }