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