From cd0f70f86d1cedffcd1bacf57d9250fdff1f7af3 Mon Sep 17 00:00:00 2001
From: Paul Duncan <pabs@pablotron.org>
Date: Sun, 13 Jan 2019 08:29:12 -0500
Subject: refactor sok-sdl, add zoom and warp_buf support

---
 meson.build        |   3 +
 src/sdl/action.c   |  37 +++++-
 src/sdl/action.h   |   5 +-
 src/sdl/color.c    |  25 ++++
 src/sdl/color.h    |  20 ++++
 src/sdl/draw.c     | 169 +++++++++++++++++++++++++++
 src/sdl/draw.h     |  21 ++++
 src/sdl/main.c     | 335 ++++++++++++++++++-----------------------------------
 src/sdl/util.h     |  13 +++
 src/sdl/warp-buf.c |  52 +++++++++
 src/sdl/warp-buf.h |  17 +++
 11 files changed, 473 insertions(+), 224 deletions(-)
 create mode 100644 src/sdl/color.c
 create mode 100644 src/sdl/color.h
 create mode 100644 src/sdl/draw.c
 create mode 100644 src/sdl/draw.h
 create mode 100644 src/sdl/util.h
 create mode 100644 src/sdl/warp-buf.c
 create mode 100644 src/sdl/warp-buf.h

diff --git a/meson.build b/meson.build
index d8c3b35..b3c7cbd 100644
--- a/meson.build
+++ b/meson.build
@@ -19,7 +19,10 @@ executable('sok-text', sources + [
 # sdl exe
 executable('sok-sdl', sources + [
   'src/text/levels.c',
+  'src/sdl/warp-buf.c',
+  'src/sdl/color.c',
   'src/sdl/action.c',
+  'src/sdl/draw.c',
   'src/sdl/main.c',
 ], dependencies: [
   dependency('SDL2'),
diff --git a/src/sdl/action.c b/src/sdl/action.c
index a4dcb79..8307f19 100644
--- a/src/sdl/action.c
+++ b/src/sdl/action.c
@@ -24,8 +24,8 @@ keycode_to_dir(const SDL_Keycode code) {
   }
 }
 
-action_t
-get_action(
+static action_t
+get_key_action(
   const SDL_Keycode code
 ) {
   switch (code) {
@@ -61,3 +61,36 @@ get_action(
     return (action_t) { .type = ACTION_NONE };
   }
 }
+
+static action_t
+get_wheel_action(
+  const Sint32 y
+) {
+  if (y > 0) {
+    return (action_t) { .type = ACTION_ZOOM_IN, .data = y };
+  } else if (y < 0) {
+    return (action_t) { .type = ACTION_ZOOM_OUT, .data = -y };
+  } else {
+    return (action_t) { .type = ACTION_NONE };
+  }
+}
+
+action_t
+get_action(
+  const SDL_Event * const ev
+) {
+  switch (ev->type) {
+  case SDL_QUIT:
+    return (action_t) { .type = ACTION_QUIT };
+    break;
+  case SDL_KEYUP:
+    return get_key_action(ev->key.keysym.sym);
+    break;
+  case SDL_MOUSEWHEEL:
+    return get_wheel_action(ev->wheel.y);
+    break;
+  default:
+    // ignore event
+    return (action_t) { .type = ACTION_NONE };
+  }
+}
diff --git a/src/sdl/action.h b/src/sdl/action.h
index d6d9870..4713f62 100644
--- a/src/sdl/action.h
+++ b/src/sdl/action.h
@@ -10,6 +10,9 @@ typedef enum {
   ACTION_WARP,
   ACTION_WARP_BUF_PUSH,
   ACTION_WARP_BUF_POP,
+  ACTION_ZOOM_IN,
+  ACTION_ZOOM_OUT,
+  ACTION_ZOOM_RESET,
   ACTION_UNDO,
   ACTION_NEXT,
   ACTION_SOLVE,
@@ -22,6 +25,6 @@ typedef struct {
   uint64_t data;
 } action_t;
 
-action_t get_action(const SDL_Keycode);
+action_t get_action(const SDL_Event * const);
 
 #endif /* ACTION_H */
diff --git a/src/sdl/color.c b/src/sdl/color.c
new file mode 100644
index 0000000..1e75cb6
--- /dev/null
+++ b/src/sdl/color.c
@@ -0,0 +1,25 @@
+#include <stdbool.h>
+#include "color.h"
+
+static const SDL_Color
+PALETTE[] = {
+  { .r = 0x00, .g = 0x00, .b = 0x00, .a = 0xFF }, // COLOR_BG
+  { .r = 0x00, .g = 0xFF, .b = 0x00, .a = 0xFF }, // COLOR_BG_WON
+  { .r = 0x66, .g = 0x66, .b = 0x66, .a = 0xFF }, // COLOR_WALL
+  { .r = 0x00, .g = 0xFF, .b = 0x00, .a = 0xFF }, // COLOR_GOAL
+  { .r = 0xFF, .g = 0x00, .b = 0x00, .a = 0xFF }, // COLOR_HOME
+  { .r = 0xFF, .g = 0xFF, .b = 0x00, .a = 0xFF }, // COLOR_HOME_GOAL
+  { .r = 0x00, .g = 0x00, .b = 0xFF, .a = 0xFF }, // COLOR_BOX
+  { .r = 0x00, .g = 0xFF, .b = 0xFF, .a = 0xFF }, // COLOR_BOX_GOAL
+  { 0, 0, 0, 0 },                                 // COLOR_LAST
+};
+
+bool
+set_color(
+  SDL_Renderer * const renderer,
+  const color_t ofs
+) {
+  // FIXME: check for error?
+  const SDL_Color c = PALETTE[ofs];
+  return SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a) < 0;
+}
diff --git a/src/sdl/color.h b/src/sdl/color.h
new file mode 100644
index 0000000..5cff762
--- /dev/null
+++ b/src/sdl/color.h
@@ -0,0 +1,20 @@
+#ifndef COLOR_H
+#define COLOR_H
+
+#include <SDL.h>
+
+typedef enum {
+  COLOR_BG,
+  COLOR_BG_WON,
+  COLOR_WALL,
+  COLOR_GOAL,
+  COLOR_HOME,
+  COLOR_HOME_GOAL,
+  COLOR_BOX,
+  COLOR_BOX_GOAL,
+  COLOR_LAST,
+} color_t;
+
+_Bool set_color(SDL_Renderer * const, const color_t);
+
+#endif /* COLOR_H */
diff --git a/src/sdl/draw.c b/src/sdl/draw.c
new file mode 100644
index 0000000..f6e350b
--- /dev/null
+++ b/src/sdl/draw.c
@@ -0,0 +1,169 @@
+#include <stdbool.h> // bool
+#include "util.h" // warn()/die()
+#include "color.h" // set_color()
+#include "draw.h"
+
+static size_t
+get_cell_size(
+  const draw_ctx_t * const draw_ctx
+) {
+  return 32 + 8 * *draw_ctx->zoom;
+}
+
+static SDL_Rect
+get_cell_rect(
+  const draw_ctx_t * const draw_ctx,
+  const sok_pos_t pos
+) {
+  const size_t cell_size = get_cell_size(draw_ctx);
+
+  return (SDL_Rect) {
+    draw_ctx->render_ofs.x + pos.x * cell_size,
+    draw_ctx->render_ofs.y + pos.y * cell_size,
+    cell_size,
+    cell_size
+  };
+}
+
+static bool
+draw_on_size(
+  const sok_ctx_t * const ctx,
+  const sok_pos_t level_size,
+  void * const data
+) {
+  draw_ctx_t * const draw_ctx = data;
+  const size_t cell_size = get_cell_size(draw_ctx);
+
+  // get renderer size
+  int renderer_x, renderer_y;
+  if (SDL_GetRendererOutputSize(draw_ctx->renderer, &renderer_x, &renderer_y)) {
+    die("SDL_GetRendererOutputSize(): %s", SDL_GetError());
+  }
+
+  // calculate renderer offset
+  draw_ctx->render_ofs.x = (renderer_x - level_size.x * cell_size) / 2;
+  draw_ctx->render_ofs.y = (renderer_y - level_size.y * cell_size) / 2;
+
+  return true;
+}
+
+static bool
+draw_on_walls_start(
+  const sok_ctx_t * const ctx,
+  void * const data
+) {
+  draw_ctx_t * const draw_ctx = data;
+  set_color(draw_ctx->renderer, COLOR_WALL);
+
+  return true;
+}
+
+static bool
+draw_on_wall(
+  const sok_ctx_t * const ctx,
+  const sok_pos_t pos,
+  void * const data
+) {
+  draw_ctx_t * const draw_ctx = data;
+  const SDL_Rect rect = get_cell_rect(draw_ctx, pos);
+
+  if (SDL_RenderFillRect(draw_ctx->renderer, &rect)) {
+    die("SDL_RenderFillRect(): %s", SDL_GetError());
+  }
+
+  return true;
+}
+
+static bool
+draw_on_goals_start(
+  const sok_ctx_t * const ctx,
+  void * const data
+) {
+  draw_ctx_t * const draw_ctx = data;
+  set_color(draw_ctx->renderer, COLOR_GOAL);
+
+  return true;
+}
+
+static bool
+draw_on_goal(
+  const sok_ctx_t * const ctx,
+  const sok_pos_t pos,
+  const bool has_player,
+  const bool has_box,
+  void * const data
+) {
+  draw_ctx_t * const draw_ctx = data;
+  const SDL_Rect rect = get_cell_rect(draw_ctx, pos);
+
+  if (SDL_RenderFillRect(draw_ctx->renderer, &rect)) {
+    die("SDL_RenderFillRect(): %s", SDL_GetError());
+  }
+
+  return true;
+}
+
+static bool
+draw_on_home(
+  const sok_ctx_t * const ctx,
+  const sok_pos_t pos,
+  const bool has_goal,
+  void * const data
+) {
+  draw_ctx_t * const draw_ctx = data;
+  const SDL_Rect rect = get_cell_rect(draw_ctx, pos);
+
+  set_color(draw_ctx->renderer, has_goal ? COLOR_HOME_GOAL : COLOR_HOME);
+
+  if (SDL_RenderFillRect(draw_ctx->renderer, &rect)) {
+    die("SDL_RenderFillRect(): %s", SDL_GetError());
+  }
+
+  return true;
+}
+
+static bool
+draw_on_box(
+  const sok_ctx_t * const ctx,
+  const sok_pos_t pos,
+  const bool has_goal,
+  void * const data
+) {
+  draw_ctx_t * const draw_ctx = data;
+  const SDL_Rect rect = get_cell_rect(draw_ctx, pos);
+
+  set_color(draw_ctx->renderer, has_goal ? COLOR_BOX_GOAL : COLOR_BOX);
+
+  if (SDL_RenderFillRect(draw_ctx->renderer, &rect)) {
+    die("SDL_RenderFillRect(): %s", SDL_GetError());
+  }
+
+  return true;
+}
+
+static const sok_ctx_walk_cbs_t
+DRAW_CBS = {
+  .on_size        = draw_on_size,
+  .on_walls_start = draw_on_walls_start,
+  .on_wall        = draw_on_wall,
+  .on_goals_start = draw_on_goals_start,
+  .on_goal        = draw_on_goal,
+  .on_home        = draw_on_home,
+  .on_box         = draw_on_box,
+};
+
+void
+draw(
+  draw_ctx_t * const draw_ctx
+) {
+  // clear background
+  set_color(draw_ctx->renderer, sok_ctx_is_done(draw_ctx->ctx) ? COLOR_BG_WON : COLOR_BG);
+  SDL_RenderClear(draw_ctx->renderer);
+
+  // render
+  sok_ctx_walk(draw_ctx->ctx, &DRAW_CBS, draw_ctx);
+
+  // flip
+  SDL_RenderPresent(draw_ctx->renderer);
+}
+
diff --git a/src/sdl/draw.h b/src/sdl/draw.h
new file mode 100644
index 0000000..304d972
--- /dev/null
+++ b/src/sdl/draw.h
@@ -0,0 +1,21 @@
+#ifndef DRAW_H
+#define DRAW_H
+
+#include <stddef.h> // size_t
+#include <SDL.h>
+#include "../libsok/sok.h" 
+#include "../text/levels.h"
+
+typedef struct {
+  SDL_Renderer * const renderer;
+  const sok_ctx_t * const ctx;
+  const size_t * const level_num;
+  const level_t *level;
+  const size_t * const zoom;
+
+  sok_pos_t render_ofs;
+} draw_ctx_t;
+
+void draw(draw_ctx_t * const);
+
+#endif /* DRAW_H */
diff --git a/src/sdl/main.c b/src/sdl/main.c
index 831d3f7..882eefb 100644
--- a/src/sdl/main.c
+++ b/src/sdl/main.c
@@ -6,171 +6,47 @@
 #include <SDL_image.h>
 #include "../text/levels.h"
 #include "../libsok/sok.h"
+#include "util.h"
 #include "action.h"
-
-#define warn(...) do { \
-  SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__); \
-} while (0)
-
-#define die(...) do { \
-  SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__); \
-  exit(EXIT_FAILURE); \
-} while (0)
-
-static void
-solve_on_error(
-  const char * const err
-) {
-  die("Error solving level: %s", err);
-}
+#include "warp-buf.h"
+#include "draw.h"
 
 static void
 draw_moves(
   const sok_ctx_t * const ctx,
   const size_t skip_moves
 ) {
-  printf("Solution (%zu moves): ", ctx->num_moves - skip_moves);
+  char buf[1024] = { 0 };
+  size_t ofs = 0;
+
   for (size_t i = skip_moves; i < ctx->num_moves; i++) {
     if (ctx->moves[i].dir >= SOK_DIR_LAST) {
       die("invalid move: %u", ctx->moves[i].dir);
     }
 
-    fputs(SOK_DIR_TO_STR(ctx->moves[i].dir), stdout);
-  }
-  printf("\n");
-}
-
-static const SDL_Color
-RED = { .r = 0x00, .g = 0xFF, .b = 0x00, .a = 0xFF },
-BLACK = { .r = 0x00, .g = 0x00, .b = 0x00, .a = 0xFF };
-
-static SDL_Color
-get_bg(
-  const sok_ctx_t * const ctx
-) {
-  return (sok_ctx_is_done(ctx)) ? RED : BLACK;
-}
-
-bool draw_on_wall(
-  const sok_ctx_t * const ctx,
-  const sok_pos_t pos,
-  void * const data
-) {
-  SDL_Renderer *renderer = data;
-  SDL_Rect rect = { 32 * pos.x, 32 * pos.y, 32, 32 };
-
-  SDL_SetRenderDrawColor(renderer, 0x66, 0x66, 0x66, 0xFF);
-
-  if (SDL_RenderFillRect(renderer, &rect)) {
-    die("SDL_RenderFillRect(): %s", SDL_GetError());
-  }
-
-  return true;
-}
-
-bool draw_on_goal(
-  const sok_ctx_t * const ctx,
-  const sok_pos_t pos,
-  const bool has_player,
-  const bool has_box,
-  void * const data
-) {
-  SDL_Renderer *renderer = data;
-  SDL_Rect rect = { 32 * pos.x, 32 * pos.y, 32, 32 };
-
-  SDL_SetRenderDrawColor(renderer, 0x00, 0xFF, 0x00, 0xFF);
-
-  if (SDL_RenderFillRect(renderer, &rect)) {
-    die("SDL_RenderFillRect(): %s", SDL_GetError());
-  }
-
-  return true;
-}
-
-bool draw_on_home(
-  const sok_ctx_t * const ctx,
-  const sok_pos_t pos,
-  const bool has_goal,
-  void * const data
-) {
-  SDL_Renderer *renderer = data;
-  SDL_Rect rect = { 32 * pos.x, 32 * pos.y, 32, 32 };
-
-  SDL_SetRenderDrawColor(renderer, 0xFF, has_goal ? 0xFF : 0x00, 0x00, 0xFF);
-
-  if (SDL_RenderFillRect(renderer, &rect)) {
-    die("SDL_RenderFillRect(): %s", SDL_GetError());
-  }
-
-  return true;
-}
-
-bool draw_on_box(
-  const sok_ctx_t * const ctx,
-  const sok_pos_t pos,
-  const bool has_goal,
-  void * const data
-) {
-  SDL_Renderer *renderer = data;
-  SDL_Rect rect = { 32 * pos.x, 32 * pos.y, 32, 32 };
-
-  SDL_SetRenderDrawColor(renderer, 0x00, has_goal ? 0xFF : 0x00, 0xFF, 0xFF);
-
-  if (SDL_RenderFillRect(renderer, &rect)) {
-    die("SDL_RenderFillRect(): %s", SDL_GetError());
+    buf[ofs++] = SOK_DIR_TO_CHAR(ctx->moves[i].dir);
   }
 
-  return true;
+  SDL_Log("Solution (%zu moves): %s", ctx->num_moves - skip_moves, buf);
 }
 
-static sok_ctx_walk_cbs_t
-DRAW_CBS = {
-  .on_wall = draw_on_wall,
-  .on_goal = draw_on_goal,
-  .on_home = draw_on_home,
-  .on_box = draw_on_box,
-};
-/* 
- * typedef struct {
- *   sok_ctx_walk_pos_cb_t on_size,
- *                         on_wall;
- *   sok_ctx_walk_tile_cb_t on_home,
- *                          on_box;
- *   sok_ctx_walk_goal_cb_t on_goal;
- *   sok_ctx_walk_move_cb_t on_move;
- * } sok_ctx_walk_cbs_t;
- * 
- * _Bool sok_ctx_walk(
- *   const sok_ctx_t * const,
- *   const sok_ctx_walk_cbs_t * const,
- *   void * const
- * );
- */ 
-
 static void
-draw(
-  SDL_Renderer * const renderer,
-  const sok_ctx_t * const ctx,
-  const size_t level_num,
-  const level_t * const level
+solve_on_error(
+  const char * const err
 ) {
-  // clear
-  const SDL_Color c = get_bg(ctx);
-  SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a);
-  SDL_RenderClear(renderer);
-
-  sok_ctx_walk(ctx, &DRAW_CBS, renderer);
-
-  // flip
-  SDL_RenderPresent(renderer);
+  die("Error solving level: %s", err);
 }
 
 int main(int argc, char *argv[]) {
-  size_t warp_buf = 0;
-  size_t level_num = (argc > 1) ? atoi(argv[1]) : 0;
+  size_t level_num = (argc > 1) ? atoi(argv[1]) : 0,
+         zoom = 0;
   const level_t *level = levels_get_level(level_num);
 
-  // init context
+  // init warp buffer
+  warp_buf_t warp_buf;
+  warp_buf_clear(&warp_buf);
+
+  // init sok context
   sok_ctx_t ctx;
   sok_ctx_init(&ctx, NULL);
 
@@ -195,94 +71,111 @@ int main(int argc, char *argv[]) {
     die("SDL_CreateWindowAndRenderer(): %s", SDL_GetError());
   }
 
+  // init draw context
+  draw_ctx_t draw_ctx = {
+    .level_num  = &level_num,
+    .level      = level,
+    .ctx        = &ctx,
+    .renderer   = renderer,
+    .zoom       = &zoom,
+  };
+
   bool done = false;
   SDL_Event ev;
   while (!done) {
     while (SDL_PollEvent(&ev)) {
-      switch (ev.type) {
-      case SDL_QUIT:
+      const action_t action = get_action(&ev);
+
+      switch (action.type) {
+      case ACTION_NONE:
+        // do nothing
+        break;
+      case ACTION_QUIT:
         done = true;
         break;
-      case SDL_KEYUP:
+      case ACTION_MOVE:
+        if (!sok_ctx_move(&ctx, (sok_dir_t) action.data)) {
+          warn("move %s failed", SOK_DIR_TO_STR((sok_dir_t) action.data));
+        }
+
+        break;
+      case ACTION_UNDO:
+        if (!sok_ctx_undo(&ctx)) {
+          warn("undo failed");
+        }
+
+        break;
+      case ACTION_NEXT:
+        if (sok_ctx_is_done(&ctx)) {
+          // advance level
+          level_num++;
+          level = levels_get_level(level_num);
+          draw_ctx.level = level;
+
+          // load next level
+          if (!sok_ctx_set_level(&ctx, level->data)) {
+            die("Couldn't load level %zu", level_num);
+          }
+        } else {
+          warn("cannot advance to next level");
+        }
+
+        break;
+      case ACTION_RESET:
+        // reset level
+        if (!sok_ctx_set_level(&ctx, level->data)) {
+          die("Couldn't load level %zu", level_num);
+        }
+
+        break;
+      case ACTION_WARP:
+        if (warp_buf_get(&warp_buf, &level_num)) {
+          level = levels_get_level(level_num);
+          draw_ctx.level = level;
+
+          // load level
+          if (!sok_ctx_set_level(&ctx, level->data)) {
+            die("Couldn't load level %zu", level_num);
+          }
+
+          // clear warp buf
+          warp_buf_clear(&warp_buf);
+        }
+
+        break;
+      case ACTION_WARP_BUF_PUSH:
+        warp_buf_push_num(&warp_buf, action.data);
+
+        break;
+      case ACTION_WARP_BUF_POP:
+        warp_buf_pop_num(&warp_buf);
+
+        break;
+      case ACTION_SOLVE:
         {
-          const action_t action = get_action(ev.key.keysym.sym);
-
-          switch (action.type) {
-          case ACTION_QUIT:
-            done = true;
-            break;
-          case ACTION_MOVE:
-            if (!sok_ctx_move(&ctx, (sok_dir_t) action.data)) {
-              warn("move %s failed", SOK_DIR_TO_STR((sok_dir_t) action.data));
-            }
-
-            break;
-          case ACTION_UNDO:
-            if (!sok_ctx_undo(&ctx)) {
-              warn("undo failed");
-            }
-
-            break;
-          case ACTION_NEXT:
-            if (sok_ctx_is_done(&ctx)) {
-              // advance level
-              level_num++;
-              level = levels_get_level(level_num);
-
-              // load next level
-              if (!sok_ctx_set_level(&ctx, level->data)) {
-                die("Couldn't load level %zu", level_num);
-              }
-            } else {
-              warn("cannot advance to next level");
-            }
-
-            break;
-          case ACTION_RESET:
-            // reset level
-            if (!sok_ctx_set_level(&ctx, level->data)) {
-              die("Couldn't load level %zu", level_num);
-            }
-
-            break;
-          case ACTION_WARP:
-            level_num = warp_buf;
-            level = levels_get_level(level_num);
-
-            // load level
-            if (!sok_ctx_set_level(&ctx, level->data)) {
-              die("Couldn't load level %zu", level_num);
-            }
-
-            warp_buf = 0;
-
-            break;
-          case ACTION_WARP_BUF_PUSH:
-            warp_buf = 10 * warp_buf + action.data;
-
-            break;
-          case ACTION_WARP_BUF_POP:
-            warp_buf /= 10;
-
-            break;
-          case ACTION_SOLVE:
-            {
-              // get current number of moves
-              const size_t old_num_moves = ctx.num_moves;
-
-              if (sok_solve(&ctx, solve_on_error)) {
-                // found solution, print it
-                draw_moves(&ctx, old_num_moves);
-              } else {
-                warn("Couldn't solve level");
-              }
-            }
-          default:
-            // ignore
-            break;
+          // get current number of moves
+          const size_t old_num_moves = ctx.num_moves;
+
+          if (sok_solve(&ctx, solve_on_error)) {
+            // found solution, print it
+            draw_moves(&ctx, old_num_moves);
+          } else {
+            warn("Couldn't solve level");
           }
         }
 
+        break;
+      case ACTION_ZOOM_IN:
+        if (zoom < 10) {
+          zoom++;
+        }
+
+        break;
+      case ACTION_ZOOM_OUT:
+        if (zoom > 0) {
+          zoom--;
+        }
+
         break;
       default:
         // ignore
@@ -290,7 +183,7 @@ int main(int argc, char *argv[]) {
       }
     }
 
-    draw(renderer, &ctx, level_num, level);
+    draw(&draw_ctx);
   }
 
   // fini renderer, window
diff --git a/src/sdl/util.h b/src/sdl/util.h
new file mode 100644
index 0000000..8d7e130
--- /dev/null
+++ b/src/sdl/util.h
@@ -0,0 +1,13 @@
+#ifndef UTIL_H
+#define UTIL_H
+
+#define warn(...) do { \
+  SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__); \
+} while (0)
+
+#define die(...) do { \
+  SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__); \
+  exit(EXIT_FAILURE); \
+} while (0)
+
+#endif /* UTIL_H */
diff --git a/src/sdl/warp-buf.c b/src/sdl/warp-buf.c
new file mode 100644
index 0000000..5168c7f
--- /dev/null
+++ b/src/sdl/warp-buf.c
@@ -0,0 +1,52 @@
+#include <stdbool.h>
+#include "warp-buf.h"
+
+void
+warp_buf_clear(
+  warp_buf_t * const buf
+) {
+  buf->len = 0;
+  buf->dst = 0;
+}
+
+bool
+warp_buf_has_num(
+  const warp_buf_t * const buf
+) {
+  return buf->len > 0;
+}
+
+void
+warp_buf_push_num(
+  warp_buf_t * const buf,
+  const size_t num
+) {
+  buf->dst = buf->dst * 10 + num;
+  buf->len++;
+}
+
+void
+warp_buf_pop_num(
+  warp_buf_t * const buf
+) {
+  if (buf->len > 0) {
+    buf->len--;
+    buf->dst /= 10;
+  }
+}
+
+bool
+warp_buf_get(
+  const warp_buf_t * const buf,
+  size_t * const r
+) {
+  if (!buf->len) {
+    return false;
+  }
+
+  if (r) {
+    *r = buf->dst;
+  }
+
+  return true;
+}
diff --git a/src/sdl/warp-buf.h b/src/sdl/warp-buf.h
new file mode 100644
index 0000000..5e00e5d
--- /dev/null
+++ b/src/sdl/warp-buf.h
@@ -0,0 +1,17 @@
+#ifndef WARP_BUF_H
+#define WARP_BUF_H
+
+#include <stddef.h> // size_t
+
+typedef struct {
+  size_t len,
+         dst;
+} warp_buf_t;
+
+void warp_buf_clear(warp_buf_t * const);
+_Bool warp_buf_has_num(const warp_buf_t * const);
+void warp_buf_push_num(warp_buf_t * const, const size_t);
+void warp_buf_pop_num(warp_buf_t * const);
+_Bool warp_buf_get(const warp_buf_t * const, size_t * const);
+
+#endif /* WARP_BUF_H */
-- 
cgit v1.2.3