Chira Engine
A customizable MIT-licensed game engine.
DeviceGL.cpp
1 #include "DeviceGL.h"
2 
3 #include <vector>
4 
5 #include <backends/imgui_impl_sdl2.h>
6 #include <glad/gl.h>
7 #include <glad/glversion.h>
8 #include <imgui.h>
9 #define SDL_MAIN_HANDLED
10 #include <SDL.h>
11 
12 #include <config/Config.h>
13 #include <config/ConEntry.h>
14 #include <i18n/TranslationManager.h>
15 #include <input/InputManager.h>
16 #include <loader/image/Image.h>
17 #include <resource/provider/FilesystemResourceProvider.h>
18 #include <render/material/MaterialFrameBuffer.h>
19 #include <render/material/MaterialTextured.h>
20 #include <render/mesh/MeshDataBuilder.h>
21 #include <ui/Font.h>
22 #include <ui/IPanel.h>
23 
24 using namespace chira;
25 
26 CHIRA_CREATE_LOG(WINDOW);
27 
28 static void setVSync(bool enable) {
29  if (!enable) {
30  SDL_GL_SetSwapInterval(0);
31  } else if (SDL_GL_SetSwapInterval(-1) == -1) {
32  // Fall back to regular vsync if we don't have adaptive vsync
33  SDL_GL_SetSwapInterval(1);
34  }
35 }
36 
37 ConVar win_vsync{"win_vsync", true, "Limit the FPS to your monitor's resolution.", CON_FLAG_CACHE, [](ConVar::CallbackArg newValue) {
38  setVSync(static_cast<bool>(std::stoi(newValue.data())));
39 }};
40 
41 [[maybe_unused]]
42 ConVar input_raw_mouse_motion{"input_raw_mouse_motion", true, "Get more accurate mouse motion.", CON_FLAG_CACHE};
43 
44 static void setImGuiConfigPath() {
45  static std::string configPath = Config::getConfigFile("imgui.ini");
46  ImGui::GetIO().IniFilename = configPath.c_str();
47 }
48 
49 Renderer::FrameBufferHandle g_WindowFramebufferHandle{};
50 SDL_Window* g_Splashscreen = nullptr;
51 void* g_GLContext = nullptr;
52 
53 bool Device::initBackendAndCreateSplashscreen(bool splashScreenVisible) {
54  static bool alreadyRan = false;
55  if (alreadyRan)
56  return false;
57  alreadyRan = true;
58 
59  SDL_SetMainReady();
60 
61 #ifdef CHIRA_PLATFORM_WINDOWS
62  // Force enable DPI awareness because the manifest method didn't work
63  SDL_SetHintWithPriority(SDL_HINT_WINDOWS_DPI_SCALING, "0", SDL_HINT_OVERRIDE);
64  SDL_SetHintWithPriority(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2", SDL_HINT_OVERRIDE);
65 #endif
66 
67  if (SDL_Init(SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER)) {
68  LOG_WINDOW.error("SDL2 failed to initialize: {}", SDL_GetError());
69  return false;
70  }
71 
72  int windowFlags =
73  SDL_WINDOW_OPENGL |
74  SDL_WINDOW_ALLOW_HIGHDPI |
75  SDL_WINDOW_ALWAYS_ON_TOP |
76  SDL_WINDOW_BORDERLESS |
77  (splashScreenVisible ? SDL_WINDOW_SHOWN : SDL_WINDOW_HIDDEN);
78  int width = 640, height = 480;
79 
80  SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
81  SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
82  SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
83  SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
84  SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
85  SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
86  SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
87 
88  g_Splashscreen = SDL_CreateWindow("Loading...", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, windowFlags);
89  if (!g_Splashscreen) {
90  LOG_WINDOW.error("Splashscreen window creation failed! Error: {}", SDL_GetError());
91  return false;
92  }
93 
94  SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
95  SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, GL_VERSION_MAJOR);
96  SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, GL_VERSION_MINOR);
97 
98  int glContextFlags;
99  SDL_GL_GetAttribute(SDL_GL_CONTEXT_FLAGS, &glContextFlags);
100  glContextFlags |= SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG;
101 #ifdef DEBUG
102  glContextFlags |= SDL_GL_CONTEXT_DEBUG_FLAG;
103 #endif
104  SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, glContextFlags);
105 
106  g_GLContext = SDL_GL_CreateContext(g_Splashscreen);
107  if (!g_GLContext) {
108  LOG_WINDOW.error("Splashscreen window context creation failed! Error: {}", SDL_GetError());
109  return false;
110  }
111  if (!gladLoadGL((GLADloadfunc) SDL_GL_GetProcAddress)) {
112  LOG_WINDOW.error("{} must be available to run this program!", GL_VERSION_STRING_PRETTY);
113  return false;
114  }
115  if (SDL_GL_MakeCurrent(g_Splashscreen, g_GLContext)) {
116  LOG_WINDOW.error("Splashscreen window context failed to be made current! Error: {}", SDL_GetError());
117  return false;
118  }
119  setVSync(win_vsync.getValue<bool>());
120 
121  Renderer::pushFrameBuffer(g_WindowFramebufferHandle);
122  Renderer::recreateFrameBuffer(&g_WindowFramebufferHandle, width, height, WrapMode::REPEAT, WrapMode::REPEAT, FilterMode::LINEAR, true);
123  glViewport(0, 0, width, height);
124 
125  MeshDataBuilder plane;
126  plane.addSquare({}, {2, -2}, SignedAxis::ZN, 0);
127  plane.setMaterial(Resource::getResource<MaterialTextured>("file://materials/splashscreen.json").cast<IMaterial>());
128  plane.render(glm::identity<glm::mat4>());
129 
130  glDisable(GL_DEPTH_TEST);
131  Renderer::popFrameBuffer();
132 
133  MeshDataBuilder windowSurface;
134  windowSurface.addSquare({}, {2, -2}, SignedAxis::ZN, 0);
135  windowSurface.setMaterial(Resource::getResource<MaterialFrameBuffer>("file://materials/window.json", &g_WindowFramebufferHandle).cast<IMaterial>());
136  windowSurface.render(glm::identity<glm::mat4>());
137 
138  glEnable(GL_DEPTH_TEST);
139  SDL_GL_SwapWindow(g_Splashscreen);
140 
141  return true;
142 }
143 
144 void Device::destroySplashscreen() {
145  if (g_Splashscreen) {
146  SDL_DestroyWindow(g_Splashscreen);
147  g_Splashscreen = nullptr;
148  }
149 }
150 
151 void Device::destroyBackend() {
152  Renderer::destroyImGui();
153  Device::destroyAllWindows();
154  SDL_GL_DeleteContext(g_GLContext);
155  SDL_Quit();
156 }
157 
158 std::uint64_t Device::getTicks() {
159  return SDL_GetTicks64();
160 }
161 
162 std::array<Device::WindowHandle, 256> g_Windows{};
163 
164 [[nodiscard]] static int findFreeWindow() {
165  for (unsigned int i = 0; i < g_Windows.size(); i++) {
166  if (!g_Windows.at(i))
167  return static_cast<int>(i);
168  }
169  return -1;
170 }
171 
172 Device::WindowHandle* Device::createWindow(int width, int height, std::string_view title, Viewport* viewport) {
173  int freeWindow = findFreeWindow();
174  if (freeWindow == -1)
175  return nullptr;
176 
177  g_Windows[freeWindow] = WindowHandle{};
178  WindowHandle& handle = g_Windows.at(freeWindow);
179 
180  handle.width = width;
181  handle.height = height;
182 
183  int windowFlags =
184  SDL_WINDOW_OPENGL |
185  SDL_WINDOW_ALLOW_HIGHDPI |
186  SDL_WINDOW_RESIZABLE |
187  SDL_WINDOW_SHOWN;
188  handle.window = SDL_CreateWindow(title.data(), SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, windowFlags);
189  if (!handle.window) {
190  LOG_WINDOW.error("Window creation failed! Error: {}", SDL_GetError());
191  return nullptr;
192  }
193  SDL_SetWindowData(handle.window, "handle", &handle);
194  SDL_GL_MakeCurrent(handle.window, g_GLContext);
195 
196  if (viewport) {
197  handle.viewport = viewport;
198  handle.viewportIsSelfOwned = false;
199  } else {
200  handle.viewport = new Viewport{{width, height}};
201  handle.viewportIsSelfOwned = true;
202  }
203 
204  int iconWidth, iconHeight, bitsPerPixel;
205  auto* icon = Image::getUncompressedImage(FilesystemResourceProvider::getResourceAbsolutePath("file://textures/ui/icon.png"), &iconWidth, &iconHeight, &bitsPerPixel, 4, false);
206  if (icon) {
207  auto* sdlIcon = SDL_CreateRGBSurfaceWithFormatFrom(icon, iconWidth, iconHeight, bitsPerPixel, 8, SDL_PIXELFORMAT_RGBA8888);
208  SDL_SetWindowIcon(handle.window, sdlIcon);
209  SDL_FreeSurface(sdlIcon);
210  Image::deleteUncompressedImage(icon);
211  }
212 
213  handle.imguiContext = ImGui::CreateContext();
214  ImGui::SetCurrentContext(handle.imguiContext);
215  auto& io = ImGui::GetIO();
216  io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard | ImGuiConfigFlags_NavEnableGamepad | ImGuiConfigFlags_DockingEnable;
217  setImGuiConfigPath();
218 
219  Renderer::initImGui(handle.window, g_GLContext);
220 
221  static bool bakedFonts = false;
222  if (!bakedFonts) {
223  bakedFonts = true;
224 
225  auto defaultFont = Resource::getUniqueUncachedResource<Font>(TR("resource.font.default"));
226  io.FontDefault = defaultFont->getFont();
227  io.Fonts->Build();
228  }
229 
230  return &handle;
231 }
232 
233 void Device::refreshWindows() {
234  // Render each window
235  for (auto& handle : g_Windows) {
236  if (!handle)
237  continue;
238 
239  if (Device::isWindowAboutToBeDestroyed(&handle)) {
240  Device::destroyWindow(&handle);
241  continue;
242  }
243 
244  if (!Device::isWindowVisible(&handle)) {
245  handle.viewport->update();
246  continue;
247  }
248 
249  SDL_GL_MakeCurrent(handle.window, g_GLContext);
250  glViewport(0, 0, handle.width, handle.height);
251 
252  ImGui::SetCurrentContext(handle.imguiContext);
253  setImGuiConfigPath();
254 
255  Renderer::pushFrameBuffer(*handle.viewport->getRawHandle());
256  Renderer::startImGuiFrame();
257 
258  handle.viewport->update();
259  handle.viewport->render();
260 
261  for (auto& [uuid, panel] : handle.panels) {
262  panel->render();
263  }
264 
265  glDisable(GL_DEPTH_TEST);
266 
267  Renderer::endImGuiFrame();
268  Renderer::popFrameBuffer();
269 
270  MeshDataBuilder surface;
271  surface.addSquare({}, {2, -2}, SignedAxis::ZN, 0);
272  surface.setMaterial(Resource::getUniqueUncachedResource<MaterialFrameBuffer>("file://materials/window.json", handle.viewport->getRawHandle()).cast<IMaterial>());
273  surface.render(glm::identity<glm::mat4>());
274 
275  glEnable(GL_DEPTH_TEST);
276 
277  SDL_GL_SwapWindow(handle.window);
278  }
279 
280  // Process input
281  SDL_Event event;
282  while (SDL_PollEvent(&event)) {
283  auto* handle = reinterpret_cast<Device::WindowHandle*>(SDL_GetWindowData(SDL_GetWindowFromID(event.window.windowID), "handle"));
284  if (!handle)
285  continue;
286 
287  ImGui::SetCurrentContext(handle->imguiContext);
288  ImGui_ImplSDL2_ProcessEvent(&event);
289 
290  switch (event.type) {
291  case SDL_QUIT:
292  for (auto& windowHandle : g_Windows) {
293  if (windowHandle) {
294  Device::queueDestroyWindow(&windowHandle, true);
295  }
296  }
297  break;
298  case SDL_WINDOWEVENT:
299  switch (event.window.event) {
300  case SDL_WINDOWEVENT_CLOSE:
301  Device::queueDestroyWindow(handle, true);
302  break;
303  case SDL_WINDOWEVENT_RESIZED:
304  case SDL_WINDOWEVENT_SIZE_CHANGED:
305  case SDL_WINDOWEVENT_MAXIMIZED:
306  SDL_GetWindowSizeInPixels(handle->window, &handle->width, &handle->height);
307  handle->viewport->setSize({handle->width, handle->height});
308  break;
309  default:
310  // There's quite a few events we don't care about
311  break;
312  }
313  break;
314  case SDL_KEYDOWN:
315  for (const auto& keyEvent : Input::KeyEvent::getEvents()) {
316  if (keyEvent.getEvent() == event.key.keysym.sym && keyEvent.getEventType() == Input::KeyEventType::PRESSED) {
317  keyEvent();
318  }
319  }
320  break;
321  case SDL_KEYUP:
322  for (const auto& keyEvent : Input::KeyEvent::getEvents()) {
323  if (keyEvent.getEvent() == event.key.keysym.sym && keyEvent.getEventType() == Input::KeyEventType::RELEASED) {
324  keyEvent();
325  }
326  }
327  break;
328  case SDL_MOUSEBUTTONDOWN:
329  for (const auto& mouseEvent : Input::MouseEvent::getEvents()) {
330  if (static_cast<uint8_t>(mouseEvent.getEvent()) == event.button.button && mouseEvent.getEventType() == Input::MouseEventType::CLICKED) {
331  mouseEvent(event.button.x, event.button.y, event.button.clicks);
332  }
333  }
334  break;
335  case SDL_MOUSEBUTTONUP:
336  for (const auto& mouseEvent : Input::MouseEvent::getEvents()) {
337  if (static_cast<uint8_t>(mouseEvent.getEvent()) == event.button.button && mouseEvent.getEventType() == Input::MouseEventType::RELEASED) {
338  mouseEvent(event.button.x, event.button.y, event.button.clicks);
339  }
340  }
341  break;
342  case SDL_MOUSEMOTION:
343  for (const auto& mouseMotionEvent : Input::MouseMotionEvent::getEvents()) {
344  if (mouseMotionEvent.getEvent() == Input::MouseMotion::MOVEMENT) {
345  mouseMotionEvent(event.motion.x, event.motion.y, event.motion.xrel, event.motion.yrel);
346  }
347  }
348  break;
349  case SDL_MOUSEWHEEL:
350  for (const auto& mouseMotionEvent : Input::MouseMotionEvent::getEvents()) {
351  if (mouseMotionEvent.getEvent() == Input::MouseMotion::SCROLL) {
352  mouseMotionEvent(event.wheel.x, event.wheel.y, event.wheel.x, event.wheel.y);
353  }
354  }
355  break;
356  default:
357  // todo(input): handle joystick / game controller inputs!
358  break;
359  }
360  }
361 
362  // Handle repeating events
363  // This is a pointer to a static variable in SDL so this is safe
364  static const auto* keyStates = SDL_GetKeyboardState(nullptr);
365  for (const auto& keyEvent : Input::KeyEvent::getEvents()) {
366  if (keyStates[SDL_GetScancodeFromKey(keyEvent.getEvent())] && keyEvent.getEventType() == Input::KeyEventType::REPEATED) {
367  keyEvent();
368  }
369  }
370 }
371 
372 int Device::getWindowCount() {
373  int count = 0;
374  for (const auto& handle : g_Windows) {
375  count += static_cast<bool>(handle);
376  }
377  return count;
378 }
379 
380 Viewport* Device::getWindowViewport(WindowHandle* handle) {
381  return handle->viewport;
382 }
383 
384 void Device::setWindowTitle(WindowHandle* handle, std::string_view title) {
385  SDL_SetWindowTitle(handle->window, title.data());
386 }
387 
388 std::string_view Device::getWindowTitle(WindowHandle* handle) {
389  return SDL_GetWindowTitle(handle->window);
390 }
391 
392 void Device::setWindowMaximized(WindowHandle* handle, bool maximize) {
393  if (Device::isWindowFullscreen(handle))
394  return;
395  if (maximize) {
396  SDL_MaximizeWindow(handle->window);
397  } else {
398  SDL_RestoreWindow(handle->window);
399  }
400  SDL_GetWindowSize(handle->window, &handle->width, &handle->height);
401  handle->viewport->setSize({handle->width, handle->height});
402 }
403 
404 bool Device::isWindowMaximized(WindowHandle* handle) {
405  return SDL_GetWindowFlags(handle->window) & SDL_WINDOW_MAXIMIZED;
406 }
407 
408 void Device::minimizeWindow(WindowHandle* handle, bool minimize) {
409  if (minimize) {
410  SDL_MinimizeWindow(handle->window);
411  } else {
412  SDL_RestoreWindow(handle->window);
413  }
414 }
415 
416 bool Device::isWindowMinimized(WindowHandle* handle) {
417  return SDL_GetWindowFlags(handle->window) & SDL_WINDOW_MINIMIZED;
418 }
419 
420 void Device::setWindowFullscreen(WindowHandle* handle, bool fullscreen) {
421  SDL_SetWindowFullscreen(handle->window, fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
422 }
423 
424 bool Device::isWindowFullscreen(WindowHandle* handle) {
425  return SDL_GetWindowFlags(handle->window) & SDL_WINDOW_FULLSCREEN_DESKTOP;
426 }
427 
428 void Device::setWindowVisibility(WindowHandle* handle, bool visible) {
429  if (visible) {
430  SDL_ShowWindow(handle->window);
431  } else {
432  SDL_HideWindow(handle->window);
433  }
434  handle->hidden = visible;
435 }
436 
437 bool Device::isWindowVisible(WindowHandle* handle) {
438  return !handle->hidden;
439 }
440 
441 void Device::setWindowSize(WindowHandle* handle, int width, int height) {
442  handle->width = width;
443  handle->height = height;
444  handle->viewport->setSize({width, height});
445  SDL_SetWindowSize(handle->window, width, height);
446 }
447 
448 glm::vec2i Device::getWindowSize(WindowHandle* handle) {
449  glm::vec2i dims;
450  SDL_GetWindowSize(handle->window, &dims.x, &dims.y);
451  return dims;
452 }
453 
454 void Device::setWindowPosition(WindowHandle* handle, int width, int height) {
455  if (Device::isWindowFullscreen(handle))
456  return;
457  SDL_SetWindowPosition(handle->window, width, height);
458 }
459 
460 void Device::setWindowPositionFromCenter(WindowHandle* handle, int width, int height) {
461  if (Device::isWindowFullscreen(handle))
462  return;
463  SDL_Rect rect;
464  SDL_GetDisplayBounds(0, &rect);
465  SDL_SetWindowPosition(handle->window, (rect.w / 2) + width, (rect.h / 2) + height);
466 }
467 
468 glm::vec2i Device::getWindowPosition(WindowHandle* handle) {
469  glm::vec2i pos;
470  SDL_GetWindowPosition(handle->window, &pos.x, &pos.y);
471  return pos;
472 }
473 
474 void Device::setMousePositionGlobal(int x, int y) {
475  SDL_WarpMouseGlobal(x, y);
476 }
477 
478 void Device::setMousePositionInWindow(WindowHandle* handle, int x, int y) {
479  SDL_WarpMouseInWindow(handle->window, x, y);
480 }
481 
482 glm::vec2i Device::getMousePositionGlobal() {
483  glm::vec2i pos{-1, -1};
484  SDL_GetGlobalMouseState(&pos.x, &pos.y);
485  return pos;
486 }
487 
488 glm::vec2i Device::getMousePositionInFocusedWindow() {
489  glm::vec2i pos{-1, -1};
490  SDL_GetMouseState(&pos.x, &pos.y);
491  return pos;
492 }
493 
494 void Device::setMouseCapturedWindow(WindowHandle* handle, bool captured) {
495  SDL_RaiseWindow(handle->window);
496  ImGui::SetCurrentContext(handle->imguiContext);
497  setImGuiConfigPath();
498 
499  if (captured) {
500  SDL_SetRelativeMouseMode(SDL_TRUE);
501  ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouse;
502  } else {
503  SDL_SetRelativeMouseMode(SDL_FALSE);
504  ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouse;
505  }
506  handle->mouseCaptured = captured;
507 }
508 
509 bool Device::isMouseCapturedWindow(WindowHandle* handle) {
510  return handle->mouseCaptured;
511 }
512 
514 void Device::queueDestroyWindow(WindowHandle* handle, bool free) {
515  handle->shouldClose = free;
516 }
517 
518 bool Device::isWindowAboutToBeDestroyed(WindowHandle* handle) {
519  return handle->shouldClose;
520 }
521 
522 void Device::destroyWindow(WindowHandle* handle) {
523  if (handle->viewportIsSelfOwned) {
524  delete handle->viewport;
525  }
526  Device::removeAllPanelsFromWindow(handle);
527  ImGui::DestroyContext(handle->imguiContext);
528  handle->imguiContext = nullptr;
529  SDL_DestroyWindow(handle->window);
530  handle->window = nullptr;
531 }
532 
533 void Device::destroyAllWindows() {
534  for (auto& handle : g_Windows) {
535  if (handle) {
536  Device::destroyWindow(&handle);
537  }
538  }
539 }
540 
541 uuids::uuid Device::addPanelToWindow(WindowHandle* handle, IPanel* panel) {
542  const auto uuid = UUIDGenerator::getNewUUID();
543  handle->panels[uuid] = panel;
544  return uuid;
545 }
546 
547 [[nodiscard]] IPanel* Device::getPanelOnWindow(WindowHandle* handle, const uuids::uuid& panelID) {
548  if (handle->panels.contains(panelID))
549  return handle->panels[panelID];
550  return nullptr;
551 }
552 
553 void Device::removePanelFromWindow(WindowHandle* handle, const uuids::uuid& panelID) {
554  if (handle->panels.contains(panelID)) {
555  delete handle->panels[panelID];
556  handle->panels.erase(panelID);
557  }
558 }
559 
560 void Device::removeAllPanelsFromWindow(WindowHandle* handle) {
561  for (const auto& [panelID, panel] : handle->panels) {
562  delete panel;
563  }
564  handle->panels.clear();
565 }
566 
567 void Device::popup(std::string_view message, std::string_view title, unsigned int popupFlags, std::string_view ok) {
568  SDL_MessageBoxButtonData buttons[] {
569  {
570  .flags = SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT,
571  .buttonid = 0,
572  .text = ok.data(),
573  },
574  };
575  SDL_MessageBoxData data{
576  .flags = popupFlags,
577  .window = nullptr,
578  .title = title.data(),
579  .message = message.data(),
580  .numbuttons = 1,
581  .buttons = buttons,
582  .colorScheme = nullptr,
583  };
584  int buttonID;
585  SDL_ShowMessageBox(&data, &buttonID);
586 }
587 
588 void Device::popupInfo(std::string_view message, std::string_view title) {
589  Device::popup(message, title, POPUP_INFO);
590 }
591 
592 void Device::popupWarning(std::string_view message, std::string_view title) {
593  Device::popup(message, title, POPUP_WARNING);
594 }
595 
596 void Device::popupError(std::string_view message, std::string_view title) {
597  Device::popup(message, title, POPUP_ERROR);
598 }
599 
600 bool Device::popupChoice(std::string_view message, std::string_view title, unsigned int popupFlags, std::string_view ok, std::string_view cancel) {
601  SDL_MessageBoxButtonData buttons[] {
602  {
603  .flags = SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT,
604  .buttonid = 1,
605  .text = ok.data(),
606  },
607  {
608  .flags = SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT,
609  .buttonid = 0,
610  .text = cancel.data(),
611  }
612  };
613  SDL_MessageBoxData data{
614  .flags = popupFlags,
615  .window = nullptr,
616  .title = title.data(),
617  .message = message.data(),
618  .numbuttons = 2,
619  .buttons = buttons,
620  .colorScheme = nullptr,
621  };
622  int buttonID;
623  SDL_ShowMessageBox(&data, &buttonID);
624  return buttonID;
625 }
626 
627 bool Device::popupInfoChoice(std::string_view message, std::string_view title) {
628  return Device::popupChoice(message, title, POPUP_INFO);
629 }
630 
631 bool Device::popupWarningChoice(std::string_view message, std::string_view title) {
632  return Device::popupChoice(message, title, POPUP_WARNING);
633 }
634 
635 bool Device::popupErrorChoice(std::string_view message, std::string_view title) {
636  return Device::popupChoice(message, title, POPUP_ERROR);
637 }
static std::string getResourceAbsolutePath(const std::string &identifier)
Takes a resource identifier and returns the full absolute path, if it exists.
void addSquare(Vertex v1, Vertex v2, Vertex v3, Vertex v4, bool addDuplicate=false)
Vertex v4 forms a face with vertex v1 and v3.