juegos

Juegos de terminal de Pancho: Catan (TUI, GUI, web, servidor, PicoCalc), ajedrez, calculadora y minijuegos
git clone https://git.lu3dhn.xyz/juegos.git
Log | Files | Refs

guikit.c (11775B)


      1 /* guikit.c - ventana SDL3: el renderer de pixeles (gfx) dibuja en un framebuffer
      2  * RGB565 que se sube como textura. Tamano fijo (escala entera, con bordes) o
      3  * adaptable (el framebuffer sigue la forma de la ventana y se escala sin bordes). */
      4 #include <SDL3/SDL.h>
      5 #include <stdlib.h>
      6 #include <string.h>
      7 #ifdef __EMSCRIPTEN__
      8 #include <emscripten.h>
      9 #endif
     10 #include "guikit.h"
     11 
     12 static struct {
     13     SDL_Window   *win;
     14     SDL_Renderer *ren;
     15     SDL_Texture  *tex;
     16     uint16_t     *fb;
     17     int           w, h;
     18     bool          fluid;
     19     int           minw, minh;
     20     int           k, ox, oy;    /* adaptable: escala entera y margen (pixeles de pantalla) */
     21     Surface       surf;
     22     bool          drag;         /* boton apretado (o dedo): arrastrar sobre algo = rueda */
     23     int           drag_x, drag_y, drag_last, drag_acc;
     24 } G;
     25 
     26 /* posicion de un evento de mouse en pixeles del framebuffer */
     27 static void fb_pos(SDL_Event *e, float px, float py, int *x, int *y)
     28 {
     29     SDL_Event tmp = *e;
     30     tmp.type = SDL_EVENT_MOUSE_MOTION;
     31     tmp.motion.x = px; tmp.motion.y = py;
     32     SDL_ConvertEventToRenderCoordinates(G.ren, &tmp);
     33     *x = (int)tmp.motion.x;
     34     *y = (int)tmp.motion.y;
     35     if (G.fluid && G.k > 0) { *x = (*x - G.ox) / G.k; *y = (*y - G.oy) / G.k; }
     36 }
     37 
     38 static void span_fb(Surface *s, int x, int y, int n, Color c)
     39 {
     40     (void)s;
     41     uint16_t *p = G.fb + y * G.w + x;
     42     while (n--) *p++ = c;
     43 }
     44 
     45 void gk_present(void)
     46 {
     47     const char *shot = SDL_getenv("APP_SHOT");
     48     if (shot) {
     49         SDL_IOStream *io = SDL_IOFromFile(shot, "wb");
     50         if (io) {
     51             SDL_IOprintf(io, "P6 %d %d 255\n", G.w, G.h);
     52             for (int i = 0; i < G.w * G.h; i++) {
     53                 uint16_t c = G.fb[i];
     54                 uint8_t rgb[3] = { (uint8_t)((c >> 11) << 3), (uint8_t)(((c >> 5) & 63) << 2), (uint8_t)((c & 31) << 3) };
     55                 SDL_WriteIO(io, rgb, 3);
     56             }
     57             SDL_CloseIO(io);
     58         }
     59     }
     60     SDL_UpdateTexture(G.tex, NULL, G.fb, G.w * 2);
     61     SDL_SetRenderDrawColor(G.ren, 10, 12, 16, 255);
     62     SDL_RenderClear(G.ren);
     63     if (G.fluid && G.k > 0) {
     64         SDL_FRect d = { (float)G.ox, (float)G.oy, (float)(G.w * G.k), (float)(G.h * G.k) };
     65         SDL_RenderTexture(G.ren, G.tex, NULL, &d);
     66     } else {
     67         SDL_RenderTexture(G.ren, G.tex, NULL, NULL);
     68     }
     69     SDL_RenderPresent(G.ren);
     70 }
     71 
     72 static bool fb_resize(int w, int h)
     73 {
     74     if (w < 1) w = 1;
     75     if (h < 1) h = 1;
     76     if (G.fb && w == G.w && h == G.h) return true;
     77     uint16_t *fb = SDL_calloc((size_t)w * (size_t)h, 2);
     78     SDL_Texture *tex = SDL_CreateTexture(G.ren, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STREAMING, w, h);
     79     if (!fb || !tex) { SDL_free(fb); if (tex) SDL_DestroyTexture(tex); return G.fb != NULL; }
     80     SDL_free(G.fb);
     81     if (G.tex) SDL_DestroyTexture(G.tex);
     82     G.fb = fb;
     83     G.tex = tex;
     84     G.w = w;
     85     G.h = h;
     86     SDL_SetTextureScaleMode(G.tex, SDL_SCALEMODE_NEAREST);
     87     surf_init(&G.surf, w, h, span_fb, 0);
     88     return true;
     89 }
     90 
     91 /* adaptable: escala entera k (la mayor que deja al menos el minimo), el framebuffer
     92  * llena la ventana y sobran menos de k pixeles de borde. Escalar por un factor no
     93  * entero deja los pixeles desparejos (unos de 1, otros de 2) y la letra se lee mal. */
     94 static void fluid_fit(void)
     95 {
     96     int pw, ph;
     97     if (!SDL_GetCurrentRenderOutputSize(G.ren, &pw, &ph) || pw < 1 || ph < 1) return;
     98     /* el minimo es "lado largo x lado corto": sirve igual con la pantalla apaisada o
     99      * vertical (telefono); el que dibuja decide el layout segun la forma que le queda */
    100     int k = SDL_min(SDL_max(pw, ph) / SDL_max(G.minw, G.minh), SDL_min(pw, ph) / SDL_min(G.minw, G.minh));
    101     if (k < 1) k = 1;
    102     int w = pw / k, h = ph / k;
    103     if (w < 1) w = 1;
    104     if (h < 1) h = 1;
    105     fb_resize(w, h);
    106     G.k = k;
    107     G.ox = (pw - G.w * k) / 2;
    108     G.oy = (ph - G.h * k) / 2;
    109 }
    110 
    111 static int map_key(SDL_Keycode k)
    112 {
    113     switch (k) {
    114     case SDLK_UP: return K_UP;
    115     case SDLK_DOWN: return K_DOWN;
    116     case SDLK_LEFT: return K_LEFT;
    117     case SDLK_RIGHT: return K_RIGHT;
    118     case SDLK_RETURN: case SDLK_KP_ENTER: return K_OK;
    119     case SDLK_ESCAPE: return K_BACK;
    120     case SDLK_TAB: return K_TAB;
    121     case SDLK_BACKSPACE: return K_BKSP;
    122     case SDLK_KP_PLUS: return K_PLUS;
    123     case SDLK_KP_MINUS: return K_MINUS;
    124     case SDLK_DELETE: return K_DEL;
    125     case SDLK_HOME: return K_HOME;
    126     case SDLK_END: return K_END;
    127     case SDLK_PAGEUP: return K_PGUP;
    128     case SDLK_PAGEDOWN: return K_PGDN;
    129     }
    130     if (k >= SDLK_F1 && k <= SDLK_F10) return K_F1 + (int)(k - SDLK_F1);
    131     return 0;
    132 }
    133 
    134 static int translate(SDL_Event *e, InEvent *ev)
    135 {
    136     switch (e->type) {
    137     case SDL_EVENT_QUIT:
    138         ev->type = EV_QUIT;
    139         return 1;
    140     case SDL_EVENT_KEY_DOWN: {
    141         bool alt_enter = e->key.key == SDLK_RETURN && (e->key.mod & SDL_KMOD_ALT);
    142         if (e->key.key == SDLK_F11 || alt_enter) {
    143             SDL_SetWindowFullscreen(G.win, !(SDL_GetWindowFlags(G.win) & SDL_WINDOW_FULLSCREEN));
    144             return 0;
    145         }
    146         int k = map_key(e->key.key);
    147         if (!k) return 0;
    148         ev->type = EV_KEY;
    149         ev->key = k;
    150         return 1;
    151     }
    152     case SDL_EVENT_TEXT_INPUT: {
    153         const char *t = e->text.text;
    154         int ch = utf8_next(&t);
    155         if (!ch) return 0;
    156         ev->type = EV_KEY;
    157         ev->key = ch;
    158         return 1;
    159     }
    160     case SDL_EVENT_MOUSE_BUTTON_DOWN:
    161         if (e->button.button != SDL_BUTTON_LEFT) return 0;
    162         ev->type = EV_CLICK;
    163         fb_pos(e, e->button.x, e->button.y, &ev->x, &ev->y);
    164         G.drag = true;                      /* si despues se mueve, es un arrastre */
    165         G.drag_x = ev->x; G.drag_y = ev->y; G.drag_last = ev->y; G.drag_acc = 0;
    166         return 1;
    167     case SDL_EVENT_MOUSE_BUTTON_UP:
    168         if (e->button.button == SDL_BUTTON_LEFT) G.drag = false;
    169         return 0;
    170     case SDL_EVENT_MOUSE_MOTION: {
    171         /* arrastrar con el boton apretado (o el dedo, en pantallas tactiles) vale como
    172          * rueda sobre el punto donde empezo: cada 33 px, una muesca (3 filas del registro) */
    173         if (!G.drag) return 0;
    174         if (!(e->motion.state & SDL_BUTTON_LMASK)) { G.drag = false; return 0; }   /* se solto fuera, o pointercancel */
    175         int x, y;
    176         fb_pos(e, e->motion.x, e->motion.y, &x, &y);
    177         G.drag_acc += y - G.drag_last;
    178         G.drag_last = y;
    179         int steps = G.drag_acc / 33;
    180         if (!steps) return 0;
    181         G.drag_acc -= steps * 33;
    182         ev->type = EV_WHEEL;
    183         ev->key = steps;
    184         ev->x = G.drag_x;
    185         ev->y = G.drag_y;
    186         return 1;
    187     }
    188     case SDL_EVENT_MOUSE_WHEEL: {
    189         if (e->wheel.y == 0) return 0;
    190         ev->type = EV_WHEEL;
    191         ev->key = e->wheel.y > 0 ? 1 : -1;
    192         if (e->wheel.direction == SDL_MOUSEWHEEL_FLIPPED) ev->key = -ev->key;
    193         fb_pos(e, e->wheel.mouse_x, e->wheel.mouse_y, &ev->x, &ev->y);
    194         return 1;
    195     }
    196     case SDL_EVENT_WINDOW_RESIZED: case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: case SDL_EVENT_WINDOW_EXPOSED:
    197         if (G.fluid) fluid_fit();
    198         ev->type = EV_RESIZE;
    199         return 1;
    200     }
    201     return 0;
    202 }
    203 
    204 static int scripted(InEvent *ev)
    205 {
    206     static const char *keys;
    207     static bool init;
    208     if (!init) { keys = SDL_getenv("APP_KEYS"); init = true; }
    209     if (!keys) return -1;
    210     if (!*keys) { SDL_Delay(50); return 0; }
    211     int k = key_script(&keys);
    212     ev->type = EV_KEY;
    213     if (k == KS_PAUSE) { SDL_Delay(300); ev->type = EV_NONE; return 0; }
    214     if (k == KS_QUIT) ev->type = EV_QUIT;
    215     else ev->key = k;
    216     return 1;
    217 }
    218 
    219 int gk_wait(int timeout_ms, int nfds, InEvent *ev)
    220 {
    221     ev->type = EV_NONE;
    222     int sk = scripted(ev);
    223     if (sk >= 0) {
    224         SDL_Event q;
    225         while (SDL_PollEvent(&q)) if (q.type == SDL_EVENT_QUIT) { ev->type = EV_QUIT; return 1; }
    226         return sk;
    227     }
    228     SDL_Event e;
    229     /* con sockets abiertos no se puede bloquear en SDL: se vuelve seguido a mirarlos */
    230     int wait = nfds > 0 && timeout_ms > 25 ? 25 : timeout_ms;
    231     if (wait < 0) wait = 0;
    232 #ifdef __EMSCRIPTEN__
    233     if (wait == 0) emscripten_sleep(0);     /* con timeout 0 SDL no cede: que el navegador respire igual */
    234 #endif
    235     if (!SDL_WaitEventTimeout(&e, wait)) return 0;
    236     do {
    237         if (translate(&e, ev)) return 1;
    238     } while (SDL_PollEvent(&e));
    239     return 0;
    240 }
    241 
    242 void gk_close(void)
    243 {
    244     SDL_StopTextInput(G.win);
    245     SDL_DestroyTexture(G.tex);
    246     SDL_DestroyRenderer(G.ren);
    247     SDL_DestroyWindow(G.win);
    248     SDL_free(G.fb);
    249     SDL_Quit();
    250 }
    251 
    252 Surface *gk_open(const char *title, int w, int h, int scale)
    253 {
    254     if (!SDL_Init(SDL_INIT_VIDEO)) return NULL;
    255     memset(&G, 0, sizeof G);
    256     if (scale < 1) scale = 1;
    257     if (!SDL_CreateWindowAndRenderer(title, w * scale, h * scale, SDL_WINDOW_RESIZABLE, &G.win, &G.ren)) return NULL;
    258     SDL_SetRenderLogicalPresentation(G.ren, w, h, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
    259     if (!fb_resize(w, h)) return NULL;
    260     SDL_StartTextInput(G.win);
    261     return &G.surf;
    262 }
    263 
    264 /* tamano inicial de la ventana adaptable: APP_WIN=AxB, o el 90% del area util de
    265  * la pantalla (sin barra de tareas) menos el marco y la barra de titulo */
    266 static void fluid_initial(int *w, int *h)
    267 {
    268     const char *env = SDL_getenv("APP_WIN");
    269     if (env && SDL_sscanf(env, "%dx%d", w, h) == 2 && *w > 0 && *h > 0) return;
    270     *w = G.minw * 2;
    271     *h = G.minh * 2;
    272 #ifdef __EMSCRIPTEN__
    273     /* el canvas llena la ventana por CSS, pero SDL recien lo ajusta en el proximo
    274      * "resize": arrancar ya con el tamano del viewport (en px CSS) */
    275     *w = EM_ASM_INT({ return window.innerWidth; });
    276     *h = EM_ASM_INT({ return window.innerHeight; });
    277     if (*w < 1 || *h < 1) { *w = G.minw; *h = G.minh; }
    278     return;
    279 #endif
    280     SDL_Rect r;
    281     if (!SDL_GetDisplayUsableBounds(SDL_GetPrimaryDisplay(), &r)) return;
    282     *w = (r.w - 16) * 9 / 10;
    283     *h = (r.h - 40) * 9 / 10;
    284 }
    285 
    286 Surface *gk_open_fluid(const char *title, int min_w, int min_h)
    287 {
    288     if (!SDL_Init(SDL_INIT_VIDEO)) return NULL;
    289     memset(&G, 0, sizeof G);
    290     G.fluid = true;
    291     G.minw = min_w;
    292     G.minh = min_h;
    293     int w, h;
    294     fluid_initial(&w, &h);
    295     /* en la web el canvas lo dimensiona el CSS (100vw x 100dvh) y SDL lo sigue en cada
    296      * resize del navegador, con devicePixelRatio por HIGH_PIXEL_DENSITY */
    297     SDL_WindowFlags flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY;
    298     if (!SDL_CreateWindowAndRenderer(title, w, h, flags, &G.win, &G.ren)) return NULL;
    299     SDL_SetWindowMinimumSize(G.win, min_w / 2, min_h / 2);
    300     if (!fb_resize(min_w, min_h)) return NULL;
    301     fluid_fit();
    302     SDL_StartTextInput(G.win);
    303     return &G.surf;
    304 }
    305 
    306 char *gk_clipboard(void)
    307 {
    308     if (!SDL_HasClipboardText()) return NULL;
    309     char *t = SDL_GetClipboardText();
    310     if (!t) return NULL;
    311     char *copy = malloc(strlen(t) + 1);
    312     if (copy) strcpy(copy, t);
    313     SDL_free(t);
    314     return copy;
    315 }
    316 
    317 #ifdef __EMSCRIPTEN__
    318 /* Un campo de texto HTML encima del canvas (web/shell.html lo define: #gk-input con
    319  * un <input>, OK y Cancelar; ver gkTextOpen/gkTextDone). Se espera cediendo al
    320  * navegador, como hace SDL con sus cuadros de dialogo en emscripten. */
    321 int gk_text_input(const char *title, char *buf, int n)
    322 {
    323     EM_ASM({ gkTextOpen(UTF8ToString($0), UTF8ToString($1), $2); }, title, buf, n - 1);
    324     int r;
    325     while ((r = EM_ASM_INT({ return gkTextDone(); })) == 0) emscripten_sleep(16);
    326     /* lo que se toco en el canvas mientras el cuadro estaba abierto no vale */
    327     SDL_FlushEvents(SDL_EVENT_MOUSE_MOTION, SDL_EVENT_MOUSE_WHEEL);
    328     SDL_FlushEvents(SDL_EVENT_FINGER_DOWN, SDL_EVENT_FINGER_CANCELED);
    329     G.drag = false;
    330     if (r < 0) return 0;
    331     EM_ASM({ stringToUTF8(gkTextValue(), $0, $1); }, buf, n);
    332     return 1;
    333 }
    334 #else
    335 int gk_text_input(const char *title, char *buf, int n)
    336 {
    337     (void)title; (void)buf; (void)n;
    338     return -1;
    339 }
    340 #endif