examples

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

sdl-gl.cpp (15984B)


      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::vector<point> vertices;
    210 	std::vector<unsigned char> wireframe;
    211 	GLuint mode;
    212 };
    213 
    214 std::vector<shp> shapes;
    215 
    216 struct object
    217 {
    218 	circle pos;
    219 	rgb col;
    220 	float spin;
    221 	float rotation;
    222 	shp& shape;
    223 };
    224 std::vector<std::shared_ptr<object>> objs = {};
    225 
    226 constexpr auto deref = std::views::transform([](auto const& p) -> auto& { return *p; });
    227 
    228 void presentScene(void)
    229 {
    230 	glClearColor(0, 0, 0, 1);
    231 	glClear(GL_COLOR_BUFFER_BIT);
    232 	for (auto& o : objs | deref) {
    233 		o.rotation += o.spin / 60;
    234 		rotor r = rotor::angle(o.rotation);
    235 		if (o.pos.colliding(mouse_pos))
    236 			glColor(o.col + rgb{.25f, .25f, .25f,});
    237 		else
    238 			glColor(o.col);
    239 
    240 		if (not wireframe)
    241 		{
    242 			glBegin(o.shape.mode);
    243 			for (auto p : o.shape.vertices)
    244 			{
    245 				p *= o.pos.radius;
    246 				auto pp = (p * r + pan + o.pos.pos) * scale;
    247 				glVertex2f(pp.x / aspect_ratio, pp.y);
    248 			}
    249 		}
    250 		else
    251 		{
    252 			glBegin(GL_LINE_LOOP);
    253 			for (auto idx : o.shape.wireframe)
    254 			{
    255 				auto p = o.shape.vertices[idx];
    256 				p *= o.pos.radius;
    257 				auto pp = (p * r + pan + o.pos.pos) * scale;
    258 				glVertex2f(pp.x / aspect_ratio, pp.y);
    259 			}
    260 		}
    261 		glEnd();
    262 	}
    263 	glFinish();
    264 }
    265 
    266 struct panel
    267 {
    268 	screen_coord pos, size;
    269 	rgb colour;
    270 	void draw() const;
    271 	bool colliding(screen_coord c)
    272 	{
    273 		auto br = pos + size;
    274 		return c.x >= pos.x && c.y >= pos.y && c.x < br.x && c.y < br.y;
    275 	}
    276 };
    277 
    278 panel menu;
    279 bool details;
    280 
    281 void panel::draw() const
    282 {
    283 	auto tl = window_to_gl(pos);
    284 	auto tr = window_to_gl({pos.x + size.x, pos.y});
    285 	auto bl = window_to_gl({pos.x, pos.y + size.y});
    286 	auto br = window_to_gl(pos + size);
    287 
    288 	glBegin(GL_TRIANGLE_STRIP);
    289 	glColor(colour);
    290 	glVertex(tl);
    291 	glVertex(tr);
    292 	glVertex(bl);
    293 	glVertex(br);
    294 	glEnd();
    295 	point half{1 / (2.f * screen.x), 1 / (2.f * screen.y)};
    296 
    297 	tl.y -= half.y;
    298 	tr.y -= half.y;
    299 	bl.y += half.y;
    300 	br.y += half.y;
    301 	tr.x -= half.x;
    302 	br.x -= half.x;
    303 	tl.x += half.x;
    304 	bl.x += half.x;
    305 
    306 	glBegin(GL_LINE_STRIP);
    307 	glColor(colour / 2.f);
    308 	glVertex(bl);
    309 	glVertex(br);
    310 	glVertex(tr);
    311 	glEnd();
    312 
    313 	glBegin(GL_LINE_STRIP);
    314 	glColor(colour * 1.5f);
    315 	glVertex(bl);
    316 	glVertex(tl);
    317 	glVertex(tr);
    318 	glEnd();
    319 }
    320 
    321 void key(SDL_KeyboardEvent& e)
    322 {
    323 	static std::uniform_real_distribution<float> size_dis{.5f, 1.5f};
    324 	if (e.repeat)
    325 		return;
    326 	static uint8_t ctrls;
    327 	switch (e.keysym.sym)
    328 	{
    329 	case SDLK_LCTRL:
    330 	case SDLK_RCTRL:
    331 		ctrls += e.state ? 1 : -1;
    332 		break;
    333 
    334 	case SDLK_c:
    335 		if (e.state && ctrls)
    336 			exit(0);
    337 		break;
    338 
    339 	case SDLK_a:
    340 		if (e.state)
    341 		{
    342 			static int i = 0;
    343 			objs.push_back(std::make_shared<object>(circle{mouse_pos, size_dis(gen)}, rgb::random(), norm_dis(gen), 0, shapes[i++ % shapes.size()]));
    344 		}
    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 		SDL_MouseButtonEvent const& e = co_await click_event;
    410 		point drag_pos = pan - window_to_local({e.x, e.y});
    411 		if (menu.colliding(mouse_coord))
    412 		{
    413 			if (e.button == SDL_BUTTON_LEFT)
    414 				details = !details;
    415 			continue;
    416 		}
    417 		switch (e.button)
    418 		{
    419 		case SDL_BUTTON_RIGHT:
    420 		{
    421 			for (;;)
    422 			{
    423 				SDL_Event const& e2 = co_await motion_or_release_event;
    424 				if (e2.type == SDL_MOUSEMOTION)
    425 					pan = drag_pos + window_to_local({e2.motion.x, e2.motion.y});
    426 				else if (auto& b = e2.button; b.button == SDL_BUTTON_RIGHT)
    427 					break;
    428 			}
    429 			break;
    430 		}
    431 		case SDL_BUTTON_LEFT:
    432 		{
    433 			auto it = std::ranges::find_if(objs, [](auto const& o){return o->pos.colliding(mouse_pos);});
    434 			if (it == std::end(objs))
    435 				break;
    436 
    437 			drag_pos = (*it)->pos.pos - window_to_local({e.x, e.y});
    438 			std::weak_ptr<object> wp = *it;
    439 
    440 			for (;;)
    441 			{
    442 				SDL_Event const& e2 = co_await motion_or_release_event;
    443 				if (wp.expired())
    444 					break;
    445 				else if (e2.type == SDL_MOUSEMOTION)
    446 					wp.lock()->pos.pos = drag_pos + window_to_local({e2.motion.x, e2.motion.y});
    447 				else if (auto& b = e2.button; b.button == SDL_BUTTON_LEFT)
    448 					break;
    449 			}
    450 			break;
    451 		}
    452 		}
    453 	}
    454 }
    455 
    456 void wheel(SDL_MouseWheelEvent* e)
    457 {
    458 	float factor = exp(0.1f * e->y);
    459 	pan += window_to_local({e->mouseX, e->mouseY}) * (1/factor-1);
    460 	scale *= factor;
    461 }
    462 
    463 void window_event(SDL_WindowEvent const* e)
    464 {
    465 	switch (e->event)
    466 	{
    467 	case SDL_WINDOWEVENT_SIZE_CHANGED:
    468 		screen.x = e->data1;
    469 		screen.y = e->data2;
    470 		aspect_ratio = (float) screen.x / screen.y;
    471 		glViewport(0, 0, screen.x, screen.y);
    472 		return;
    473 	}
    474 }
    475 
    476 void doInput()
    477 {
    478 	SDL_Event event;
    479 	while (SDL_PollEvent(&event))
    480 	{
    481 		switch (event.type)
    482 		{
    483 		case SDL_QUIT:
    484 			exit(0);
    485 		case SDL_KEYDOWN:
    486 		case SDL_KEYUP:
    487 			key(event.key);
    488 			break;
    489 		case SDL_MOUSEMOTION:
    490 			mouse_motion(event.motion);
    491 			motion_or_release_event(event);
    492 			break;
    493 		case SDL_MOUSEBUTTONDOWN:
    494 			click_event.ready(event.button);
    495 			break;
    496 		case SDL_MOUSEBUTTONUP:
    497 			motion_or_release_event(event);
    498 			break;
    499 		case SDL_MOUSEWHEEL:
    500 			wheel(&event.wheel);
    501 			break;
    502 
    503 		case SDL_WINDOWEVENT:
    504 			window_event(&event.window);
    505 			break;
    506 
    507 		default:
    508 			break;
    509 		}
    510 	}
    511 }
    512 
    513 void init_shape()
    514 {
    515 	{
    516 		constexpr int n = 10;
    517 		shapes.emplace_back();
    518 		auto& shape = shapes.back();
    519 		shape.mode = GL_TRIANGLE_FAN;
    520 		shape.vertices.emplace_back(0,0);
    521 		for (int i = 0; i <= n; ++i)
    522 			shape.vertices.push_back(point{0, (.65f + i % 2) / -1.65f} * rotor::angle((float)i / n));
    523 		for (int i = 0; i < n; ++i)
    524 			shape.wireframe.push_back(i+1);
    525 	}
    526 	{
    527 		constexpr int n = 3;
    528 		shapes.emplace_back();
    529 		auto& shape = shapes.back();
    530 		shape.mode = GL_TRIANGLES;
    531 		for (int i = 0; i < n; ++i)
    532 			shape.vertices.push_back(point{0, 1} * rotor::angle((float)i / n));
    533 		for (int i = 0; i < n; ++i)
    534 			shape.wireframe.push_back(i);
    535 	}
    536 	{
    537 		constexpr int n = 4;
    538 		shapes.emplace_back();
    539 		auto& shape = shapes.back();
    540 		shape.mode = GL_TRIANGLE_FAN;
    541 		for (int i = 0; i < n; ++i)
    542 			shape.vertices.push_back(point{0, 1} * rotor::angle((float)i / n));
    543 		for (int i = 0; i < n; ++i)
    544 			shape.wireframe.push_back(i);
    545 	}
    546 	{
    547 		constexpr int n = 64;
    548 		shapes.emplace_back();
    549 		auto& shape = shapes.back();
    550 		shape.mode = GL_TRIANGLE_STRIP;
    551 		for (int i = 0; i <= n; ++i)
    552 		{
    553 			float turns = i * .5f / n;
    554 			point p{0, 1};
    555 			p *= rotor::angle(turns);
    556 			p.x *= 1 - i % 2 * .45f;
    557 			shape.vertices.push_back(p);
    558 		}
    559 		for (int i = 0; i <= n / 2; ++i)
    560 			shape.wireframe.push_back(i * 2);
    561 		for (int i = n / 2; i > 0; --i)
    562 			shape.wireframe.push_back(i * 2 - 1);
    563 	}
    564 }
    565 
    566 void init_scene()
    567 {
    568 	objs.push_back(std::make_shared<object>(circle{{-1.5f, -1}, 1.f}, rgb{.7,.2,.2}, .2, 0, shapes[0]));
    569 	objs.push_back(std::make_shared<object>(circle{{0, 1}, .5f}, rgb{.2,.7,.2}, 0, 0, shapes[0]));
    570 	objs.push_back(std::make_shared<object>(circle{{1.5f, -1}, 1.f}, rgb{.2,.2,.7}, -.2, 0, shapes[0]));
    571 }
    572 
    573 struct psf2
    574 {
    575 	unsigned int magic, version, hdr_size, flags, length, g_size, height, width;
    576 };
    577 struct font
    578 {
    579 	psf2 hdr;
    580 	GLuint tex;
    581 	font(char const* filename)
    582 	{
    583 		int fds[2];
    584 		if (pipe(fds))
    585 			err(1, "pipe");
    586 		pid_t child = fork();
    587 		if (child < 0)
    588 			err(1, "fork");
    589 		if (!child)
    590 		{
    591 			if (dup2(fds[1], 1) == -1)
    592 				err(1, "dup2");
    593 			if (close(fds[0]))
    594 				err(1, "close");
    595 			if (close(fds[1]))
    596 				err(1, "close");
    597 			execlp("gunzip", "gunzip", "--stdout", filename, nullptr);
    598 			err(1, "execlp");
    599 		}
    600 		close(fds[1]);
    601 
    602 		if (read(fds[0], &hdr, sizeof(hdr)) != sizeof(hdr))
    603 			err(1, "read");
    604 		if (hdr.magic != 0x864AB572)
    605 			errx(1, "Not a PSF2 file");
    606 
    607 		std::vector<unsigned char> storage(hdr.length * hdr.g_size);
    608 
    609 		for (ssize_t read = 0; read < (ssize_t)storage.size(); )
    610 		{
    611 			int n = ::read(fds[0], storage.data() + read, storage.size() - read);
    612 			if (n < 0)
    613 				err(1, "read");
    614 			if (n == 0)
    615 				errx(1, "not enough data from font");
    616 			read += n;
    617 		}
    618 		close(fds[0]);
    619 		assert(wait(nullptr) == child);
    620 		std::vector<unsigned char> pix(hdr.length * hdr.height * hdr.width);
    621 
    622 		for (size_t i = 0; i < pix.size(); ++i)
    623 			pix[i] = ((storage[i / 8] >> (7 - i % 8)) & 1) * 255;
    624 
    625 		glGenTextures(1, &tex);
    626 		glBindTexture(GL_TEXTURE_2D, tex);
    627 		glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, hdr.width, hdr.height * hdr.length, 0, GL_ALPHA, GL_UNSIGNED_BYTE, pix.data());
    628 		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    629 	}
    630 	screen_coord draw(unsigned char c, screen_coord pos)
    631 	{
    632 		screen_coord br_screen{pos.x + (int)hdr.width, pos.y + (int)hdr.height};
    633 		float text = (float)c / hdr.length;
    634 		float texb = (float)(c + 1u) / hdr.length;
    635 		auto tl = window_to_gl(pos);
    636 		auto tr = window_to_gl({br_screen.x, pos.y});
    637 		auto bl = window_to_gl({pos.x, br_screen.y});
    638 		auto br = window_to_gl(br_screen);
    639 
    640 		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    641 		glEnable(GL_BLEND);
    642 		glEnable(GL_TEXTURE_2D);
    643 		glBegin(GL_TRIANGLE_STRIP);
    644 		glTexCoord2f(0,text);
    645 		glVertex(tl);
    646 		glTexCoord2f(1,text);
    647 		glVertex(tr);
    648 		glTexCoord2f(0,texb);
    649 		glVertex(bl);
    650 		glTexCoord2f(1,texb);
    651 		glVertex(br);
    652 		glEnd();
    653 		glDisable(GL_TEXTURE_2D);
    654 		glDisable(GL_BLEND);
    655 
    656 		return br_screen;
    657 	}
    658 	screen_coord draw(std::string_view str, screen_coord pos)
    659 	{
    660 		for (auto c : str)
    661 			pos.x = draw(c, pos).x;
    662 		return pos;
    663 	}
    664 };
    665 
    666 font* fnt;
    667 
    668 void draw_UI()
    669 {
    670 	std::vector<std::string> strings;
    671 	strings.push_back(std::format("scale: {:.2}", scale));
    672 	strings.push_back(std::format("  pan: ({:.1}, {:.1})", pan.x, pan.y));
    673 	strings.push_back(std::format("{} shapes", shapes.size()));
    674 	strings.push_back(std::format("{} objects", objs.size()));
    675 	int width = (int)(2 + fnt->hdr.width * std::ranges::max(strings | std::views::transform([](auto const& s){ return s.size(); })));
    676 	int height = (int)(2 + fnt->hdr.height * strings.size());
    677 	menu.size = {width, height};
    678 	menu.colour = menu.colliding(mouse_coord) ? rgb{.6, .6, .6} : rgb{.4, .4, .4};
    679 	menu.draw();
    680 	glColor3f(0, 0, 0);
    681 	int y = 1;
    682 	for (auto const& s : strings)
    683 	{
    684 		fnt->draw(s, {1, y});
    685 		y += fnt->hdr.height;
    686 	}
    687 
    688 	if (not details)
    689 		return;
    690 
    691 	auto x = (int)width;
    692 	strings.clear();
    693 	for (auto const& [i, o] : objs | deref | std::views::enumerate)
    694 		strings.push_back(std::format("{}: size {}", i, o.pos.radius));
    695 
    696 	if (strings.empty())
    697 		return;
    698 
    699 	width = (int)(2 + fnt->hdr.width * std::ranges::max(strings | std::views::transform([](auto const& s){ return s.size(); })));
    700 	height = (int)(2 + fnt->hdr.height * strings.size());
    701 	panel{{x, 0}, {width, height}, rgb{.4, .4, .4}}.draw();
    702 	glColor3f(0, 0, 0);
    703 	y = 1;
    704 	for (auto const& s : strings)
    705 	{
    706 		fnt->draw(s, {x + 1, y});
    707 		y += fnt->hdr.height;
    708 	}
    709 }
    710 
    711 int main()
    712 {
    713 	init_shape();
    714 	init_scene();
    715 	mouse_task();
    716 	initSDL();
    717 	atexit(SDL_Quit);
    718 
    719 	fnt = new font{"/usr/share/kbd/consolefonts/lat9-16.psf.gz"};
    720 
    721 	for (;;)
    722 	{
    723 		doInput();
    724 		presentScene();
    725 		draw_UI();
    726 		SDL_GL_SwapWindow(window);
    727 	}
    728 }