commit 84742ae38ea49e33ccd482c805a9f9ee50734005 Author: TEC-GO Date: Tue Oct 13 20:36:51 2026 -0300 Add initial FnixLib development implementation diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8da988 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +build/ +build-*/ +cmake-build-*/ +.cache/ +compile_commands.json +*.o +*.a +*.so diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..55908b0 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,80 @@ +cmake_minimum_required(VERSION 3.16) + +project(FnixLib + VERSION 0.1.0 + DESCRIPTION "Biblioteca multimedia propia para juegos y aplicaciones 2D/3D" + LANGUAGES C) + +# La versión 0.1.0 solo tiene implementación para Linux (X11). +if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") + message(FATAL_ERROR + "FnixLib ${PROJECT_VERSION} solo está implementada para Linux (X11). " + "El soporte de Windows todavía no existe.") +endif() + +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Release CACHE STRING "Tipo de compilación" FORCE) +endif() + +set(CMAKE_C_STANDARD 17) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +option(FNIX_BUILD_EXAMPLES "Compilar los ejemplos" ON) +option(FNIX_BUILD_TESTS "Compilar las pruebas (ctest)" ON) +option(FNIX_WARNINGS_AS_ERRORS "Tratar las advertencias como errores" OFF) +option(FNIX_SANITIZE "Compilar con AddressSanitizer y UBSan" OFF) + +if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") + add_compile_options(-Wall -Wextra -Wpedantic -Wshadow) + if(FNIX_WARNINGS_AS_ERRORS) + add_compile_options(-Werror) + endif() + if(FNIX_SANITIZE) + add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer + -fno-sanitize-recover=undefined) + add_link_options(-fsanitize=address,undefined) + endif() +endif() + +# Única dependencia del sistema: Xlib (libX11). +find_package(X11 REQUIRED) + +add_library(fnix + src/core/errors.c + src/core/window.c + src/graphics/canvas.c + src/graphics/draw2d.c + src/input/input.c + src/time/time.c + src/platform/linux/x11_keys.c + src/platform/linux/x11_window.c) +add_library(FnixLib::fnix ALIAS fnix) + +target_include_directories(fnix + PUBLIC $ + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) +target_link_libraries(fnix PRIVATE X11::X11) + +if(FNIX_BUILD_EXAMPLES) + add_executable(basic examples/basic/main.c) + target_link_libraries(basic PRIVATE fnix) +endif() + +if(FNIX_BUILD_TESTS) + enable_testing() + + foreach(test_name core draw2d input) + add_executable(test_${test_name} tests/test_${test_name}.c) + target_link_libraries(test_${test_name} PRIVATE fnix) + add_test(NAME ${test_name} COMMAND test_${test_name}) + endforeach() + # test_input comprueba la lógica interna, así que usa cabeceras privadas. + target_include_directories(test_input PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + + # Necesita un servidor X11; sin DISPLAY termina con el código 77 (omitida). + add_executable(test_window_x11 tests/test_window_x11.c) + target_link_libraries(test_window_x11 PRIVATE fnix X11::X11) + add_test(NAME window_x11 COMMAND test_window_x11) + set_tests_properties(window_x11 PROPERTIES SKIP_RETURN_CODE 77) +endif() diff --git a/README.md b/README.md new file mode 100644 index 0000000..05853f3 --- /dev/null +++ b/README.md @@ -0,0 +1,133 @@ +# FnixLib + +Biblioteca multimedia propia, escrita en C, para juegos y aplicaciones. Sin +bibliotecas multimedia de terceros (nada de SDL, GLFW, raylib, GLAD ni GLEW): solo +usa la biblioteca estándar de C y las interfaces nativas del sistema. + +**Versión 0.1.0 — primera versión funcional, solo Linux (X11), solo 2D por software.** + +## Qué hay hoy + +| Módulo | Estado | +|---|---| +| Errores (`errors.h`) | Códigos y mensajes por hilo | +| Ventana (`window.h`) | Linux/X11, tamaño fijo, límite de FPS, delta time | +| Dibujo 2D por software (`draw2d.h`, `canvas.h`) | Píxel, línea, rectángulo, círculo (lleno y contorno), alfa | +| Teclado y ratón (`input.h`, `events.h`) | Estado por fotograma y cola de eventos | +| Tiempo (`time.h`) | Reloj monótono y espera | + +**No existe todavía:** imágenes/texturas, fuentes y texto, audio, matemáticas, +colisiones, animaciones, 3D, mandos, Windows, instalación (`cmake --install`) ni +`find_package`. Los encabezados correspondientes no se han creado a propósito. + +## Requisitos + +- Compilador C con soporte de C17: GCC (verificado) o Clang (no verificado). +- CMake 3.16 o superior y Make. +- Xlib: paquete `libx11` en Arch/CachyOS, `libx11-dev` en Debian/Ubuntu. +- Para ejecutar programas: un servidor X11 (o XWayland en una sesión Wayland, no + verificado). + +En CachyOS / Arch: + +```sh +sudo pacman -S --needed gcc cmake make libx11 +``` + +## Compilar y probar + +```sh +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build +ctest --test-dir build --output-on-failure +./build/basic +``` + +`ctest` ejecuta cuatro pruebas. La de `window_x11` necesita un servidor X11; sin +`DISPLAY` se omite. Para ejecutarla sin pantalla: instalar `xorg-server-xvfb` y usar +`xvfb-run -a ctest --test-dir build --output-on-failure`. + +Opciones de CMake: `-DFNIX_BUILD_EXAMPLES=OFF`, `-DFNIX_BUILD_TESTS=OFF`, +`-DFNIX_WARNINGS_AS_ERRORS=ON`, `-DFNIX_SANITIZE=ON` (AddressSanitizer + UBSan). + +## Ejemplo mínimo + +```c +#include +#include + +int main(void) +{ + FnixWindow *window = fnix_window_create("Mi juego", 800, 600); + if (window == NULL) { + fprintf(stderr, "Error: %s\n", fnix_last_error()); + return 1; + } + + while (!fnix_window_should_close(window)) { + fnix_begin_frame(window); /* eventos + destino de dibujo */ + if (fnix_key_pressed(window, FNIX_KEY_ESCAPE)) { + fnix_window_request_close(window); + } + + fnix_clear(FNIX_BLACK); + fnix_draw_circle(400, 300, 50, FNIX_RED); + fnix_end_frame(window); /* muestra y limita a 60 FPS */ + } + + fnix_window_destroy(window); + return 0; +} +``` + +Un programa más completo (movimiento con teclado, ratón, alfa) está en +`examples/basic/main.c`. Para usar la biblioteca desde otro proyecto CMake mientras +no exista la instalación, añade este repositorio con `add_subdirectory(FnixLib)` y +enlaza `FnixLib::fnix`. + +## Estructura + +``` +include/fnix/ API pública (fnix.h incluye todo) +src/core/ errores y ventana (lógica independiente del sistema) +src/graphics/ lienzo y renderizador 2D por software +src/input/ estado de teclado/ratón y cola de eventos +src/time/ reloj y espera (POSIX) +src/platform/ interfaz con el sistema; linux/ contiene la parte de X11 +tests/ pruebas (cuatro ejecutables) +examples/basic/ ejemplo interactivo +docs/ DECISIONS.md: decisiones de diseño y límites conocidos +``` + +## Estado de verificación (13/10/2026) + +Verificado en: Ubuntu 24.04 (x86_64), GCC 13.3, Xlib, servidor Xvfb de 24 bits. + +- Compilación con `-std=c17 -Wall -Wextra -Wpedantic -Wshadow -Werror`: sin avisos. +- Las cuatro pruebas pasan, también compiladas con AddressSanitizer + UBSan (sin + errores ni fugas). +- La prueba de integración comprueba que lo dibujado llega a la ventana (leyendo + píxeles del servidor), el teclado, el ratón, la rueda, el cierre de ventana, la + destrucción y el límite de FPS. +- El ejemplo `basic` se ejecutó bajo Xvfb a ~60 FPS y se inspeccionó una captura. + +**No verificado:** la configuración con CMake (el entorno de pruebas no tenía +`cmake`; se compiló con `gcc` directamente usando las mismas opciones, así que el +`CMakeLists.txt` debe probarse), Clang, una pantalla real o Wayland/XWayland, +bibliotecas compartidas (`BUILD_SHARED_LIBS=ON`) y Windows. + +## Limitaciones conocidas + +- Solo Linux/X11. La ventana tiene tamaño fijo y no se puede redimensionar. +- Exige una pantalla TrueColor de 24/32 bits con orden de bytes igual al de la CPU; + en otro caso la creación de la ventana falla con un mensaje claro. +- Los errores de protocolo de X11 los gestiona el manejador por defecto de Xlib, que + termina el proceso; FnixLib no lo sustituye para no interferir con el programa. +- Las teclas dependen de la distribución de teclado activa (ver `docs/DECISIONS.md`). +- Sin sincronización vertical ni aceleración por GPU. +- Coordenadas, tamaños y radios de dibujo limitados a ±2^28. +- Un `FnixCanvas` de destino es un valor por hilo; el dibujo no es seguro desde + varios hilos sobre el mismo lienzo. + +Las decisiones de diseño y el motivo de cada una están en `docs/DECISIONS.md`. El +repositorio no incluye licencia: debe elegirla el autor. diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md new file mode 100644 index 0000000..3931fcb --- /dev/null +++ b/docs/DECISIONS.md @@ -0,0 +1,101 @@ +# Decisiones de diseño de FnixLib + +Registro de las decisiones técnicas tomadas en la versión 0.1.0, con su motivo y +lo que costaría revertirlas. Es un documento nuevo; no sustituye a ningún otro. + +## Convenciones + +- **Lenguaje:** C17 estricto (`-std=c17`, sin extensiones de GNU). +- **Nombres:** funciones `fnix_modulo_accion`, tipos `FnixTipo`, constantes y + macros `FNIX_NOMBRE`. Los identificadores van en inglés; los comentarios y los + mensajes de error, en español. +- **Errores:** las funciones que crean objetos devuelven `NULL` si fallan; las de + consulta no fallan (devuelven 0/false ante argumentos inválidos). El motivo se + guarda por hilo y se lee con `fnix_last_error_code()` / `fnix_last_error()`. + Un éxito no borra el último error (como `errno`). +- **Objetos opacos:** `FnixWindow` y `FnixCanvas` solo se manejan por puntero, de + modo que su contenido puede cambiar sin romper la API. + +## Decisiones + +### D1. Xlib (libX11) como interfaz con el sistema gráfico de Linux +Se trata como interfaz nativa del sistema, igual que Win32 en Windows. Alternativas +descartadas por ahora: hablar el protocolo X11 a mano sobre un socket (mucho trabajo +sin beneficio en 0.1.0) y Wayland nativo (exige además implementar el protocolo y +buscar una forma de dibujar). Los ejecutables se verifican bajo X11; en sesiones +Wayland deberían funcionar mediante XWayland, pero **no está verificado**. +*Reversible:* todo lo específico de X11 está en `src/platform/linux/` detrás de +`src/platform/platform.h`. + +### D2. Renderizador 2D por software y presentación con `XPutImage` +Es la forma más sencilla de validar la arquitectura sin depender de la GPU. Coste: +no hay aceleración ni sincronización vertical. El backend de GPU (OpenGL) queda para +la fase 4 y se añadirá detrás de la misma API de dibujo. + +### D3. Formato de píxel `0x00RRGGBB` y requisitos de pantalla +El lienzo guarda `uint32_t 0x00RRGGBB`. Es lo que acepta X11 sin conversión en el +caso habitual. Si el servidor no ofrece un visual TrueColor de 24/32 bits con esas +máscaras y el mismo orden de bytes que la CPU, `fnix_window_create` falla con +`FNIX_ERROR_UNSUPPORTED` y un mensaje explicativo (no se intenta convertir). + +### D4. Destino de dibujo implícito, por hilo +`fnix_draw_*` dibuja sobre el lienzo fijado con `fnix_set_target()`; +`fnix_begin_frame()` fija el de la ventana. Mantiene la API corta +(`fnix_draw_circle(x, y, r, color)`) y permite pruebas sin ventana usando +`fnix_canvas_create()`. Coste: estado oculto por hilo. + +### D5. Ventana de tamaño fijo +Se indica con los *hints* de tamaño mínimo = máximo. Redimensionar exigiría decidir +qué ocurre con el lienzo y no aporta a la primera versión. + +### D6. Entrada: estado por fotograma + cola de eventos +`fnix_begin_frame()` borra los *pressed/released* y la rueda, procesa los eventos del +sistema y los aplica a una máquina de estados (`input.c`), que es lo que consultan +`fnix_key_down`, `fnix_key_pressed`, etc. Además cada evento se guarda en una cola +de 256 entradas (se descarta lo más antiguo) para quien prefiera +`fnix_window_poll_event`. Detalles: +- El autorepeat no vuelve a activar *pressed* (`XkbSetDetectableAutoRepeat`). +- Al perder el foco, lo pulsado pasa a soltado: evita teclas pegadas. +- Las teclas se identifican por el KeySym sin modificadores (índice 0) de la + distribución activa; con distribuciones no QWERTY la tecla física puede cambiar de + identificador. Alternativa (códigos físicos/scancodes) pospuesta. + +### D7. Tiempo entre fotogramas y límite de FPS +El *delta time* se mide de `begin_frame` a `begin_frame` (0 en el primero). El límite +de FPS (60 por defecto) duerme en `end_frame` hasta una fecha límite que avanza un +periodo por fotograma, de modo que el promedio a largo plazo es exacto. Si se va más +de un fotograma tarde, se reajusta en lugar de recuperar con una ráfaga. + +### D8. Geometría y recorte +- Entradas limitadas a `[-2^28, 2^28]`; fuera de ese rango no se dibuja y se + registra un error. Con ese límite los cálculos en `int64_t` no desbordan. +- Todo se recorta antes de recorrer píxeles: el coste depende del lienzo, no del + tamaño de la figura (una línea de millones de píxeles fuera de pantalla es barata). +- Cada primitiva escribe cada píxel como máximo una vez, para que la mezcla con alfa + no se aplique dos veces. +- Líneas: Bresenham con aritmética entera; los empates se redondean hacia arriba, por + lo que una línea y su inversa pueden diferir en algún píxel intermedio. +- Círculos: un píxel pertenece al disco si `dx² + dy² <= r² + r`; el contorno es el + disco de radio `r` menos el de `r - 1`. + +### D9. Mezcla alfa +"Source-over" con alfa recto y redondeo al entero más cercano; alfa 255 reemplaza y +alfa 0 no dibuja. `fnix_clear` ignora el alfa. + +### D10. CMake +Biblioteca estática por defecto (`BUILD_SHARED_LIBS` la cambia, **sin verificar**), +tipo *Release* si no se indica otro, opciones `FNIX_BUILD_EXAMPLES`, +`FNIX_BUILD_TESTS`, `FNIX_WARNINGS_AS_ERRORS` y `FNIX_SANITIZE`. En sistemas que no +son Linux la configuración falla con un mensaje claro en vez de compilar a medias. + +### D11. Pruebas +Tres ejecutables sin pantalla (núcleo, dibujo, entrada) y una prueba de integración +que usa un servidor X real: una segunda conexión inyecta eventos con `XSendEvent` y +lee los píxeles de la ventana con `XGetImage`. Sin `DISPLAY` termina con código 77 y +`ctest` la marca como omitida. + +## Pendiente (no implementado) +- `cmake --install`, `find_package(FnixLib CONFIG)` y empaquetado. +- Windows, y compilación cruzada desde Linux. +- Imágenes y texturas, fuentes y texto, audio, matemáticas, colisiones, 3D. +- Licencia: el repositorio no incluye `LICENSE`; debe elegirla el autor. diff --git a/examples/basic/main.c b/examples/basic/main.c new file mode 100644 index 0000000..b633526 --- /dev/null +++ b/examples/basic/main.c @@ -0,0 +1,121 @@ +/* + * Ejemplo básico de FnixLib: ventana, formas, teclado, ratón y delta time. + * + * Controles: + * Flechas o WASD mover el cuadrado + * Ratón un anillo sigue al cursor; mantener el botón izquierdo dibuja + * Escape salir + * + * Opción para pruebas automáticas: + * ./basic --frames N termina solo tras N fotogramas + */ +#include + +#include +#include +#include + +int main(int argc, char **argv) +{ + int max_frames = 0; /* 0 = sin límite */ + if (argc == 3 && strcmp(argv[1], "--frames") == 0) { + max_frames = atoi(argv[2]); + } + + FnixWindow *window = fnix_window_create("FnixLib - ejemplo básico", 800, 600); + if (window == NULL) { + fprintf(stderr, "Error al crear la ventana: %s\n", fnix_last_error()); + return 1; + } + + const int radius = 24; + float ball_x = 400.0f, ball_y = 300.0f; + float ball_vx = 260.0f, ball_vy = 190.0f; + float box_x = 100.0f, box_y = 100.0f; + const float box_size = 48.0f; + const float box_speed = 300.0f; + + int frames = 0; + double fps_timer = 0.0; + int fps_frames = 0; + + while (!fnix_window_should_close(window)) { + fnix_begin_frame(window); + const float dt = (float)fnix_window_delta_time(window); + + if (fnix_key_pressed(window, FNIX_KEY_ESCAPE)) { + fnix_window_request_close(window); + } + + /* Cuadrado controlado por el teclado. */ + if (fnix_key_down(window, FNIX_KEY_LEFT) || fnix_key_down(window, FNIX_KEY_A)) { + box_x -= box_speed * dt; + } + if (fnix_key_down(window, FNIX_KEY_RIGHT) || fnix_key_down(window, FNIX_KEY_D)) { + box_x += box_speed * dt; + } + if (fnix_key_down(window, FNIX_KEY_UP) || fnix_key_down(window, FNIX_KEY_W)) { + box_y -= box_speed * dt; + } + if (fnix_key_down(window, FNIX_KEY_DOWN) || fnix_key_down(window, FNIX_KEY_S)) { + box_y += box_speed * dt; + } + if (box_x < 0.0f) box_x = 0.0f; + if (box_y < 0.0f) box_y = 0.0f; + if (box_x > 800.0f - box_size) box_x = 800.0f - box_size; + if (box_y > 600.0f - box_size) box_y = 600.0f - box_size; + + /* Pelota que rebota. */ + ball_x += ball_vx * dt; + ball_y += ball_vy * dt; + if (ball_x < (float)radius) { ball_x = (float)radius; ball_vx = -ball_vx; } + if (ball_x > 800.0f - (float)radius) { ball_x = 800.0f - (float)radius; ball_vx = -ball_vx; } + if (ball_y < (float)radius) { ball_y = (float)radius; ball_vy = -ball_vy; } + if (ball_y > 600.0f - (float)radius) { ball_y = 600.0f - (float)radius; ball_vy = -ball_vy; } + + /* Dibujo. */ + fnix_clear(FNIX_RGB(24, 24, 32)); + + for (int x = 0; x <= 800; x += 50) { + fnix_draw_line(x, 0, x, 599, FNIX_RGB(40, 40, 56)); + } + for (int y = 0; y <= 600; y += 50) { + fnix_draw_line(0, y, 799, y, FNIX_RGB(40, 40, 56)); + } + + fnix_draw_rect_outline(0, 0, 800, 600, FNIX_GRAY); + fnix_draw_circle((int)ball_x, (int)ball_y, radius, FNIX_YELLOW); + fnix_draw_circle_outline((int)ball_x, (int)ball_y, radius + 6, FNIX_RGBA(255, 255, 0, 110)); + fnix_draw_rect((int)box_x, (int)box_y, (int)box_size, (int)box_size, + FNIX_RGBA(80, 160, 255, 200)); + fnix_draw_rect_outline((int)box_x, (int)box_y, (int)box_size, (int)box_size, FNIX_WHITE); + + const int mx = fnix_mouse_x(window); + const int my = fnix_mouse_y(window); + if (fnix_mouse_down(window, FNIX_MOUSE_LEFT)) { + fnix_draw_circle(mx, my, 10, FNIX_RGBA(255, 80, 80, 160)); + } + fnix_draw_circle_outline(mx, my, 14, FNIX_WHITE); + fnix_draw_line(mx - 20, my, mx + 20, my, FNIX_WHITE); + fnix_draw_line(mx, my - 20, mx, my + 20, FNIX_WHITE); + + fnix_end_frame(window); + + /* FPS por la salida estándar, una vez por segundo. */ + fps_timer += (double)dt; + fps_frames++; + if (fps_timer >= 1.0) { + printf("FPS: %.1f\n", (double)fps_frames / fps_timer); + fflush(stdout); + fps_timer = 0.0; + fps_frames = 0; + } + + if (max_frames > 0 && ++frames >= max_frames) { + break; + } + } + + fnix_window_destroy(window); + return 0; +} diff --git a/include/fnix/canvas.h b/include/fnix/canvas.h new file mode 100644 index 0000000..8f5c544 --- /dev/null +++ b/include/fnix/canvas.h @@ -0,0 +1,45 @@ +/* + * FnixLib - lienzo (búfer de píxeles en memoria). + * + * Un FnixCanvas es la superficie sobre la que dibuja el renderizador 2D por + * software. Cada ventana tiene uno (fnix_window_canvas), pero también se + * pueden crear lienzos independientes, por ejemplo para pruebas sin pantalla. + * + * Formato de píxel: uint32_t 0x00RRGGBB, filas consecutivas sin relleno, + * origen (0,0) arriba a la izquierda. + */ +#ifndef FNIX_CANVAS_H +#define FNIX_CANVAS_H + +#include "fnix/types.h" + +FNIX_BEGIN_DECLS + +/* Lado máximo de un lienzo, en píxeles. */ +#define FNIX_CANVAS_MAX_SIZE 16384 + +/* Crea un lienzo negro de width x height. Devuelve NULL si el tamaño no está + * entre 1 y FNIX_CANVAS_MAX_SIZE o si falta memoria (ver fnix_last_error). */ +FnixCanvas *fnix_canvas_create(int width, int height); + +/* Libera el lienzo. Acepta NULL. Si era el destino de dibujo del hilo actual, + * el destino pasa a ser NULL. */ +void fnix_canvas_destroy(FnixCanvas *canvas); + +int fnix_canvas_width(const FnixCanvas *canvas); +int fnix_canvas_height(const FnixCanvas *canvas); + +/* Acceso de solo lectura a los píxeles (0x00RRGGBB). NULL si canvas es NULL. */ +const uint32_t *fnix_canvas_pixels(const FnixCanvas *canvas); + +/* Color del píxel (x, y) con alfa 255. Fuera del lienzo devuelve FNIX_TRANSPARENT. */ +FnixColor fnix_canvas_get_pixel(const FnixCanvas *canvas, int x, int y); + +/* Lienzo sobre el que dibujan las funciones fnix_draw_*. Es un valor por hilo. + * fnix_begin_frame() lo establece al lienzo de la ventana. */ +void fnix_set_target(FnixCanvas *canvas); +FnixCanvas *fnix_get_target(void); + +FNIX_END_DECLS + +#endif /* FNIX_CANVAS_H */ diff --git a/include/fnix/draw2d.h b/include/fnix/draw2d.h new file mode 100644 index 0000000..de0e27d --- /dev/null +++ b/include/fnix/draw2d.h @@ -0,0 +1,47 @@ +/* + * FnixLib - dibujo 2D por software. + * + * Todas las funciones dibujan sobre el lienzo de destino (fnix_get_target()). + * Sin destino no hacen nada y dejan un error en fnix_last_error(). + * + * Reglas comunes: + * - Se recorta al lienzo: dibujar parcial o totalmente fuera es seguro. + * - Color con alfa 255 reemplaza el píxel; alfa 0 no dibuja; los valores + * intermedios se mezclan con el fondo (alfa "source-over"). + * - Cada píxel se modifica como máximo una vez por llamada, así que la mezcla + * con alfa nunca se aplica dos veces sobre el mismo píxel. + * - Coordenadas, anchos, altos y radios deben estar en [-2^28, 2^28]; fuera + * de ese rango no se dibuja nada y se registra un error. + */ +#ifndef FNIX_DRAW2D_H +#define FNIX_DRAW2D_H + +#include "fnix/types.h" + +FNIX_BEGIN_DECLS + +/* Rellena todo el lienzo. Ignora el alfa del color (escribe color opaco). */ +void fnix_clear(FnixColor color); + +void fnix_draw_pixel(int x, int y, FnixColor color); + +/* Línea de 1 píxel de grosor; incluye ambos extremos. */ +void fnix_draw_line(int x_start, int y_start, int x_end, int y_end, + FnixColor color); + +/* Rectángulo con esquina superior izquierda (x, y). width o height <= 0 + * no dibujan nada. */ +void fnix_draw_rect(int x, int y, int width, int height, FnixColor color); +void fnix_draw_rect_outline(int x, int y, int width, int height, + FnixColor color); + +/* Círculo de centro (cx, cy). Un píxel (dx, dy) pertenece al disco si + * dx*dx + dy*dy <= radius*radius + radius. radius 0 dibuja un píxel; + * radius < 0 no dibuja nada. El contorno es el disco de radio `radius` + * menos el de radio `radius - 1` (1 píxel de grosor). */ +void fnix_draw_circle(int cx, int cy, int radius, FnixColor color); +void fnix_draw_circle_outline(int cx, int cy, int radius, FnixColor color); + +FNIX_END_DECLS + +#endif /* FNIX_DRAW2D_H */ diff --git a/include/fnix/errors.h b/include/fnix/errors.h new file mode 100644 index 0000000..73f48ab --- /dev/null +++ b/include/fnix/errors.h @@ -0,0 +1,42 @@ +/* + * FnixLib - manejo de errores. + * + * Convención general de la API: + * - Las funciones que crean un objeto devuelven un puntero; NULL indica fallo. + * - Las funciones de consulta no fallan: con un puntero NULL o un argumento + * fuera de rango devuelven 0 / false. + * - Cuando algo falla, FnixLib guarda un código y un mensaje legibles + * (por hilo), que se consultan con fnix_last_error_code() y + * fnix_last_error(). Un éxito NO borra el último error (como errno). + */ +#ifndef FNIX_ERRORS_H +#define FNIX_ERRORS_H + +#include "fnix/types.h" + +FNIX_BEGIN_DECLS + +typedef enum FnixError { + FNIX_OK = 0, + FNIX_ERROR_INVALID_ARGUMENT, /* argumento nulo o fuera de rango */ + FNIX_ERROR_OUT_OF_MEMORY, /* no se pudo reservar memoria */ + FNIX_ERROR_PLATFORM, /* fallo del sistema (pantalla, ventana) */ + FNIX_ERROR_UNSUPPORTED /* el sistema no ofrece lo que se necesita */ +} FnixError; + +/* Descripción corta y fija de un código. Nunca devuelve NULL. */ +const char *fnix_error_string(FnixError error); + +/* Código del último error ocurrido en este hilo (FNIX_OK si no hubo ninguno). */ +FnixError fnix_last_error_code(void); + +/* Mensaje detallado del último error. Nunca devuelve NULL; es "" si no hubo + * errores. El puntero es válido hasta el siguiente error en este hilo. */ +const char *fnix_last_error(void); + +/* Olvida el último error de este hilo. */ +void fnix_clear_error(void); + +FNIX_END_DECLS + +#endif /* FNIX_ERRORS_H */ diff --git a/include/fnix/events.h b/include/fnix/events.h new file mode 100644 index 0000000..15a61ec --- /dev/null +++ b/include/fnix/events.h @@ -0,0 +1,45 @@ +/* + * FnixLib - cola de eventos. + * + * Además de las consultas de estado de input.h, cada ventana guarda los + * eventos recibidos en una cola que el programa puede vaciar con + * fnix_window_poll_event(). La cola conserva como máximo los 256 eventos más + * recientes: si el programa no la consulta, los más antiguos se descartan. + */ +#ifndef FNIX_EVENTS_H +#define FNIX_EVENTS_H + +#include "fnix/input.h" +#include "fnix/types.h" + +FNIX_BEGIN_DECLS + +typedef enum FnixEventType { + FNIX_EVENT_NONE = 0, + FNIX_EVENT_WINDOW_CLOSE, /* el usuario pidió cerrar la ventana */ + FNIX_EVENT_FOCUS_GAINED, + FNIX_EVENT_FOCUS_LOST, + FNIX_EVENT_KEY_DOWN, /* usa key y repeat */ + FNIX_EVENT_KEY_UP, /* usa key */ + FNIX_EVENT_MOUSE_BUTTON_DOWN, /* usa button, x, y */ + FNIX_EVENT_MOUSE_BUTTON_UP, /* usa button, x, y */ + FNIX_EVENT_MOUSE_MOVE, /* usa x, y */ + FNIX_EVENT_MOUSE_WHEEL /* usa wheel, x, y */ +} FnixEventType; + +/* Solo son significativos los campos indicados para cada tipo; el resto vale 0. */ +typedef struct FnixEvent { + FnixEventType type; + FnixKey key; + bool repeat; /* KEY_DOWN generado por autorepeat */ + FnixMouseButton button; + int x, y; + int wheel; /* +1 arriba, -1 abajo */ +} FnixEvent; + +/* Saca el evento más antiguo de la cola. Devuelve false si está vacía. */ +bool fnix_window_poll_event(FnixWindow *window, FnixEvent *event); + +FNIX_END_DECLS + +#endif /* FNIX_EVENTS_H */ diff --git a/include/fnix/fnix.h b/include/fnix/fnix.h new file mode 100644 index 0000000..8423ab6 --- /dev/null +++ b/include/fnix/fnix.h @@ -0,0 +1,21 @@ +/* + * FnixLib - cabecera general: incluye toda la API pública. + */ +#ifndef FNIX_FNIX_H +#define FNIX_FNIX_H + +#define FNIX_VERSION_MAJOR 0 +#define FNIX_VERSION_MINOR 1 +#define FNIX_VERSION_PATCH 0 +#define FNIX_VERSION_STRING "0.1.0" + +#include "fnix/canvas.h" +#include "fnix/draw2d.h" +#include "fnix/errors.h" +#include "fnix/events.h" +#include "fnix/input.h" +#include "fnix/time.h" +#include "fnix/types.h" +#include "fnix/window.h" + +#endif /* FNIX_FNIX_H */ diff --git a/include/fnix/input.h b/include/fnix/input.h new file mode 100644 index 0000000..39dc052 --- /dev/null +++ b/include/fnix/input.h @@ -0,0 +1,72 @@ +/* + * FnixLib - estado del teclado y del ratón. + * + * Las consultas se refieren al estado tras el último fnix_begin_frame(): + * - down: la tecla/botón está mantenida ahora mismo. + * - pressed: pasó de suelta a pulsada en este fotograma (el autorepeat del + * teclado NO la vuelve a activar). + * - released: pasó de pulsada a suelta en este fotograma. También se activa + * para todo lo que estaba pulsado cuando la ventana pierde el foco. + * + * Las teclas se identifican por el símbolo que produce cada una SIN modificadores + * en la distribución de teclado activa: con una distribución no QWERTY, la tecla + * física que antes era "A" puede cambiar de identificador. + * + * Todas las funciones aceptan window == NULL o valores inválidos y devuelven 0/false. + */ +#ifndef FNIX_INPUT_H +#define FNIX_INPUT_H + +#include "fnix/types.h" + +FNIX_BEGIN_DECLS + +typedef enum FnixKey { + FNIX_KEY_UNKNOWN = 0, + /* Las letras, los dígitos y F1..F12 son contiguos: no reordenar. */ + FNIX_KEY_A, FNIX_KEY_B, FNIX_KEY_C, FNIX_KEY_D, FNIX_KEY_E, FNIX_KEY_F, + FNIX_KEY_G, FNIX_KEY_H, FNIX_KEY_I, FNIX_KEY_J, FNIX_KEY_K, FNIX_KEY_L, + FNIX_KEY_M, FNIX_KEY_N, FNIX_KEY_O, FNIX_KEY_P, FNIX_KEY_Q, FNIX_KEY_R, + FNIX_KEY_S, FNIX_KEY_T, FNIX_KEY_U, FNIX_KEY_V, FNIX_KEY_W, FNIX_KEY_X, + FNIX_KEY_Y, FNIX_KEY_Z, + FNIX_KEY_0, FNIX_KEY_1, FNIX_KEY_2, FNIX_KEY_3, FNIX_KEY_4, + FNIX_KEY_5, FNIX_KEY_6, FNIX_KEY_7, FNIX_KEY_8, FNIX_KEY_9, + FNIX_KEY_F1, FNIX_KEY_F2, FNIX_KEY_F3, FNIX_KEY_F4, FNIX_KEY_F5, + FNIX_KEY_F6, FNIX_KEY_F7, FNIX_KEY_F8, FNIX_KEY_F9, FNIX_KEY_F10, + FNIX_KEY_F11, FNIX_KEY_F12, + FNIX_KEY_SPACE, FNIX_KEY_ENTER, FNIX_KEY_ESCAPE, FNIX_KEY_TAB, + FNIX_KEY_BACKSPACE, + FNIX_KEY_LEFT, FNIX_KEY_RIGHT, FNIX_KEY_UP, FNIX_KEY_DOWN, + FNIX_KEY_LEFT_SHIFT, FNIX_KEY_RIGHT_SHIFT, + FNIX_KEY_LEFT_CTRL, FNIX_KEY_RIGHT_CTRL, + FNIX_KEY_LEFT_ALT, FNIX_KEY_RIGHT_ALT, + FNIX_KEY_COUNT +} FnixKey; + +typedef enum FnixMouseButton { + FNIX_MOUSE_LEFT = 0, + FNIX_MOUSE_MIDDLE, + FNIX_MOUSE_RIGHT, + FNIX_MOUSE_BUTTON_COUNT +} FnixMouseButton; + +bool fnix_key_down(const FnixWindow *window, FnixKey key); +bool fnix_key_pressed(const FnixWindow *window, FnixKey key); +bool fnix_key_released(const FnixWindow *window, FnixKey key); + +bool fnix_mouse_down(const FnixWindow *window, FnixMouseButton button); +bool fnix_mouse_pressed(const FnixWindow *window, FnixMouseButton button); +bool fnix_mouse_released(const FnixWindow *window, FnixMouseButton button); + +/* Posición del cursor en píxeles de la ventana (origen arriba a la izquierda). + * Puede quedar fuera de la ventana mientras se arrastra con un botón pulsado. */ +int fnix_mouse_x(const FnixWindow *window); +int fnix_mouse_y(const FnixWindow *window); + +/* Desplazamiento de la rueda acumulado en este fotograma: positivo hacia + * arriba (alejándose del usuario), negativo hacia abajo. */ +int fnix_mouse_wheel(const FnixWindow *window); + +FNIX_END_DECLS + +#endif /* FNIX_INPUT_H */ diff --git a/include/fnix/time.h b/include/fnix/time.h new file mode 100644 index 0000000..dc35b58 --- /dev/null +++ b/include/fnix/time.h @@ -0,0 +1,20 @@ +/* + * FnixLib - tiempo. + */ +#ifndef FNIX_TIME_H +#define FNIX_TIME_H + +#include "fnix/types.h" + +FNIX_BEGIN_DECLS + +/* Segundos desde un origen arbitrario, con un reloj monótono (nunca retrocede). + * Sirve para medir intervalos, no para conocer la hora del día. */ +double fnix_time_now(void); + +/* Duerme el hilo actual al menos `seconds` segundos. Valores <= 0 no hacen nada. */ +void fnix_time_sleep(double seconds); + +FNIX_END_DECLS + +#endif /* FNIX_TIME_H */ diff --git a/include/fnix/types.h b/include/fnix/types.h new file mode 100644 index 0000000..9a0d974 --- /dev/null +++ b/include/fnix/types.h @@ -0,0 +1,47 @@ +/* + * FnixLib - tipos públicos comunes. + */ +#ifndef FNIX_TYPES_H +#define FNIX_TYPES_H + +#include +#include +#include + +#ifdef __cplusplus +#define FNIX_BEGIN_DECLS extern "C" { +#define FNIX_END_DECLS } +#else +#define FNIX_BEGIN_DECLS +#define FNIX_END_DECLS +#endif + +FNIX_BEGIN_DECLS + +/* Tipos opacos: su contenido es privado y puede cambiar entre versiones. */ +typedef struct FnixWindow FnixWindow; +typedef struct FnixCanvas FnixCanvas; + +/* Color RGBA con 8 bits por canal. a = 255 es opaco, a = 0 es invisible. */ +typedef struct FnixColor { + uint8_t r, g, b, a; +} FnixColor; + +#define FNIX_RGBA(red, green, blue, alpha) \ + ((FnixColor){(red), (green), (blue), (alpha)}) +#define FNIX_RGB(red, green, blue) FNIX_RGBA((red), (green), (blue), 255) + +#define FNIX_BLACK FNIX_RGB(0, 0, 0) +#define FNIX_WHITE FNIX_RGB(255, 255, 255) +#define FNIX_RED FNIX_RGB(255, 0, 0) +#define FNIX_GREEN FNIX_RGB(0, 255, 0) +#define FNIX_BLUE FNIX_RGB(0, 0, 255) +#define FNIX_YELLOW FNIX_RGB(255, 255, 0) +#define FNIX_CYAN FNIX_RGB(0, 255, 255) +#define FNIX_MAGENTA FNIX_RGB(255, 0, 255) +#define FNIX_GRAY FNIX_RGB(128, 128, 128) +#define FNIX_TRANSPARENT FNIX_RGBA(0, 0, 0, 0) + +FNIX_END_DECLS + +#endif /* FNIX_TYPES_H */ diff --git a/include/fnix/window.h b/include/fnix/window.h new file mode 100644 index 0000000..b29352f --- /dev/null +++ b/include/fnix/window.h @@ -0,0 +1,65 @@ +/* + * FnixLib - ventanas. + * + * Uso típico: + * + * FnixWindow *window = fnix_window_create("Mi juego", 800, 600); + * while (!fnix_window_should_close(window)) { + * fnix_begin_frame(window); // lee eventos, fija el destino + * fnix_clear(FNIX_BLACK); + * fnix_draw_circle(400, 300, 50, FNIX_RED); + * fnix_end_frame(window); // muestra y limita los FPS + * } + * fnix_window_destroy(window); + * + * Limitaciones de la versión 0.1.0: la ventana tiene tamaño fijo y solo existe + * la implementación para Linux/X11 (ver README.md). + * + * Todas las funciones aceptan window == NULL (no hacen nada o devuelven 0). + */ +#ifndef FNIX_WINDOW_H +#define FNIX_WINDOW_H + +#include "fnix/types.h" + +FNIX_BEGIN_DECLS + +/* Crea y muestra una ventana de width x height píxeles (cada lado entre 1 y + * FNIX_CANVAS_MAX_SIZE) y su lienzo. Devuelve NULL si falla; el motivo está + * en fnix_last_error(). Límite inicial de FPS: 60. */ +FnixWindow *fnix_window_create(const char *title, int width, int height); + +/* Cierra la ventana y libera todos sus recursos. Acepta NULL. */ +void fnix_window_destroy(FnixWindow *window); + +/* true cuando el usuario cerró la ventana o se llamó a fnix_window_request_close. */ +bool fnix_window_should_close(const FnixWindow *window); +void fnix_window_request_close(FnixWindow *window); + +int fnix_window_width(const FnixWindow *window); +int fnix_window_height(const FnixWindow *window); + +/* Lienzo de la ventana; lo que se dibuje en él se muestra en fnix_end_frame(). + * Pertenece a la ventana: no se debe destruir por separado. */ +FnixCanvas *fnix_window_canvas(FnixWindow *window); + +/* Limita los fotogramas por segundo durmiendo en fnix_end_frame(). + * 0 desactiva el límite (el bucle usará toda la CPU). Valores negativos = 0. */ +void fnix_window_set_target_fps(FnixWindow *window, int fps); + +/* Segundos entre el inicio del fotograma anterior y el del actual (medidos en + * fnix_begin_frame). Vale 0 en el primer fotograma. */ +double fnix_window_delta_time(const FnixWindow *window); + +/* Inicio de fotograma: borra los "pressed/released" y la rueda del fotograma + * anterior, procesa los eventos pendientes del sistema, calcula el delta time + * y fija el lienzo de la ventana como destino de dibujo. NO borra el lienzo. */ +void fnix_begin_frame(FnixWindow *window); + +/* Fin de fotograma: muestra el lienzo en la ventana y espera lo necesario para + * respetar el límite de FPS. */ +void fnix_end_frame(FnixWindow *window); + +FNIX_END_DECLS + +#endif /* FNIX_WINDOW_H */ diff --git a/src/core/errors.c b/src/core/errors.c new file mode 100644 index 0000000..afa581b --- /dev/null +++ b/src/core/errors.c @@ -0,0 +1,51 @@ +#include "fnix/errors.h" + +#include "core/internal.h" + +#include +#include + +static _Thread_local FnixError g_last_code = FNIX_OK; +static _Thread_local char g_last_message[256] = ""; + +void fnix_set_error(FnixError code, const char *format, ...) +{ + va_list args; + va_start(args, format); + vsnprintf(g_last_message, sizeof g_last_message, format, args); + va_end(args); + g_last_code = code; +} + +const char *fnix_error_string(FnixError error) +{ + switch (error) { + case FNIX_OK: + return "sin error"; + case FNIX_ERROR_INVALID_ARGUMENT: + return "argumento inválido"; + case FNIX_ERROR_OUT_OF_MEMORY: + return "memoria insuficiente"; + case FNIX_ERROR_PLATFORM: + return "error del sistema"; + case FNIX_ERROR_UNSUPPORTED: + return "no soportado por el sistema"; + } + return "código de error desconocido"; +} + +FnixError fnix_last_error_code(void) +{ + return g_last_code; +} + +const char *fnix_last_error(void) +{ + return g_last_message; +} + +void fnix_clear_error(void) +{ + g_last_code = FNIX_OK; + g_last_message[0] = '\0'; +} diff --git a/src/core/internal.h b/src/core/internal.h new file mode 100644 index 0000000..f56ed7b --- /dev/null +++ b/src/core/internal.h @@ -0,0 +1,18 @@ +/* Uso interno de FnixLib. No forma parte de la API pública. */ +#ifndef FNIX_CORE_INTERNAL_H +#define FNIX_CORE_INTERNAL_H + +#include "fnix/errors.h" + +#if defined(__GNUC__) +#define FNIX_PRINTF_LIKE(fmt_arg, first_arg) \ + __attribute__((format(printf, fmt_arg, first_arg))) +#else +#define FNIX_PRINTF_LIKE(fmt_arg, first_arg) +#endif + +/* Registra el último error del hilo actual (código + mensaje con formato printf). */ +void fnix_set_error(FnixError code, const char *format, ...) + FNIX_PRINTF_LIKE(2, 3); + +#endif /* FNIX_CORE_INTERNAL_H */ diff --git a/src/core/window.c b/src/core/window.c new file mode 100644 index 0000000..9b0f637 --- /dev/null +++ b/src/core/window.c @@ -0,0 +1,150 @@ +#include "fnix/window.h" + +#include "core/internal.h" +#include "core/window_internal.h" +#include "fnix/canvas.h" +#include "fnix/time.h" +#include "graphics/canvas_internal.h" + +#include + +#define FNIX_DEFAULT_FPS 60 + +FnixWindow *fnix_window_create(const char *title, int width, int height) +{ + if (title == NULL) { + fnix_set_error(FNIX_ERROR_INVALID_ARGUMENT, "el título de la ventana es NULL"); + return NULL; + } + if (width < 1 || height < 1 || width > FNIX_CANVAS_MAX_SIZE || + height > FNIX_CANVAS_MAX_SIZE) { + fnix_set_error(FNIX_ERROR_INVALID_ARGUMENT, + "tamaño de ventana inválido: %dx%d (cada lado debe estar entre 1 y %d)", + width, height, FNIX_CANVAS_MAX_SIZE); + return NULL; + } + + FnixWindow *window = calloc(1, sizeof *window); + if (window == NULL) { + fnix_set_error(FNIX_ERROR_OUT_OF_MEMORY, "sin memoria para la ventana"); + return NULL; + } + window->width = width; + window->height = height; + + window->canvas = fnix_canvas_create(width, height); + if (window->canvas == NULL) { + free(window); + return NULL; /* el error ya quedó registrado */ + } + + window->platform = + fnix_platform_window_create(title, width, height, window->canvas->pixels); + if (window->platform == NULL) { + fnix_canvas_destroy(window->canvas); + free(window); + return NULL; /* el error ya quedó registrado */ + } + + window->target_fps = FNIX_DEFAULT_FPS; + window->deadline = fnix_time_now(); + return window; +} + +void fnix_window_destroy(FnixWindow *window) +{ + if (window == NULL) { + return; + } + fnix_platform_window_destroy(window->platform); + fnix_canvas_destroy(window->canvas); + free(window); +} + +bool fnix_window_should_close(const FnixWindow *window) +{ + return window ? window->should_close : true; +} + +void fnix_window_request_close(FnixWindow *window) +{ + if (window != NULL) { + window->should_close = true; + } +} + +int fnix_window_width(const FnixWindow *window) +{ + return window ? window->width : 0; +} + +int fnix_window_height(const FnixWindow *window) +{ + return window ? window->height : 0; +} + +FnixCanvas *fnix_window_canvas(FnixWindow *window) +{ + return window ? window->canvas : NULL; +} + +void fnix_window_set_target_fps(FnixWindow *window, int fps) +{ + if (window == NULL) { + return; + } + window->target_fps = fps > 0 ? fps : 0; + window->deadline = fnix_time_now(); +} + +double fnix_window_delta_time(const FnixWindow *window) +{ + return window ? window->delta_time : 0.0; +} + +void fnix_window_dispatch_event(FnixWindow *window, FnixEvent *event) +{ + fnix_input_apply_event(&window->input, event); + if (event->type == FNIX_EVENT_WINDOW_CLOSE) { + window->should_close = true; + } + fnix_event_queue_push(&window->events, event); +} + +void fnix_begin_frame(FnixWindow *window) +{ + if (window == NULL) { + return; + } + + const double now = fnix_time_now(); + window->delta_time = window->has_prev_begin ? now - window->prev_begin : 0.0; + window->prev_begin = now; + window->has_prev_begin = true; + + fnix_input_begin_frame(&window->input); + fnix_platform_pump_events(window->platform, window); + fnix_set_target(window->canvas); +} + +void fnix_end_frame(FnixWindow *window) +{ + if (window == NULL) { + return; + } + + fnix_platform_window_present(window->platform); + + if (window->target_fps > 0) { + const double period = 1.0 / (double)window->target_fps; + window->deadline += period; + const double now = fnix_time_now(); + if (now < window->deadline) { + fnix_time_sleep(window->deadline - now); + } else if (now - window->deadline > period) { + /* Vamos más de un fotograma tarde: se reajusta en vez de intentar + * recuperar con una ráfaga de fotogramas sin espera. */ + window->deadline = now; + } + } +} diff --git a/src/core/window_internal.h b/src/core/window_internal.h new file mode 100644 index 0000000..530f3ed --- /dev/null +++ b/src/core/window_internal.h @@ -0,0 +1,32 @@ +/* Uso interno de FnixLib. No forma parte de la API pública. */ +#ifndef FNIX_CORE_WINDOW_INTERNAL_H +#define FNIX_CORE_WINDOW_INTERNAL_H + +#include "fnix/canvas.h" +#include "fnix/events.h" +#include "fnix/window.h" +#include "input/input_internal.h" +#include "platform/platform.h" + +struct FnixWindow { + FnixPlatformWindow *platform; + FnixCanvas *canvas; + int width; + int height; + bool should_close; + + FnixInputState input; + FnixEventQueue events; + + int target_fps; /* 0 = sin límite */ + double deadline; /* instante en que debe terminar el fotograma actual */ + double prev_begin; /* instante del último fnix_begin_frame */ + bool has_prev_begin; + double delta_time; +}; + +/* Lo llaman las plataformas por cada evento recibido: actualiza el estado de + * entrada, marca el cierre si procede y encola el evento. */ +void fnix_window_dispatch_event(FnixWindow *window, FnixEvent *event); + +#endif /* FNIX_CORE_WINDOW_INTERNAL_H */ diff --git a/src/graphics/canvas.c b/src/graphics/canvas.c new file mode 100644 index 0000000..128f572 --- /dev/null +++ b/src/graphics/canvas.c @@ -0,0 +1,85 @@ +#include "fnix/canvas.h" + +#include "core/internal.h" +#include "graphics/canvas_internal.h" + +#include + +static _Thread_local FnixCanvas *g_target = NULL; + +FnixCanvas *fnix_canvas_create(int width, int height) +{ + if (width < 1 || height < 1 || width > FNIX_CANVAS_MAX_SIZE || + height > FNIX_CANVAS_MAX_SIZE) { + fnix_set_error(FNIX_ERROR_INVALID_ARGUMENT, + "tamaño de lienzo inválido: %dx%d (cada lado debe estar entre 1 y %d)", + width, height, FNIX_CANVAS_MAX_SIZE); + return NULL; + } + + FnixCanvas *canvas = calloc(1, sizeof *canvas); + if (canvas == NULL) { + fnix_set_error(FNIX_ERROR_OUT_OF_MEMORY, "sin memoria para el lienzo"); + return NULL; + } + + canvas->pixels = calloc((size_t)width * (size_t)height, sizeof(uint32_t)); + if (canvas->pixels == NULL) { + fnix_set_error(FNIX_ERROR_OUT_OF_MEMORY, + "sin memoria para los píxeles de un lienzo de %dx%d", width, + height); + free(canvas); + return NULL; + } + + canvas->width = width; + canvas->height = height; + return canvas; +} + +void fnix_canvas_destroy(FnixCanvas *canvas) +{ + if (canvas == NULL) { + return; + } + if (g_target == canvas) { + g_target = NULL; + } + free(canvas->pixels); + free(canvas); +} + +int fnix_canvas_width(const FnixCanvas *canvas) +{ + return canvas ? canvas->width : 0; +} + +int fnix_canvas_height(const FnixCanvas *canvas) +{ + return canvas ? canvas->height : 0; +} + +const uint32_t *fnix_canvas_pixels(const FnixCanvas *canvas) +{ + return canvas ? canvas->pixels : NULL; +} + +FnixColor fnix_canvas_get_pixel(const FnixCanvas *canvas, int x, int y) +{ + if (canvas == NULL || x < 0 || y < 0 || x >= canvas->width || + y >= canvas->height) { + return FNIX_TRANSPARENT; + } + const uint32_t p = canvas->pixels[(size_t)y * (size_t)canvas->width + (size_t)x]; + return FNIX_RGB((p >> 16) & 0xFFu, (p >> 8) & 0xFFu, p & 0xFFu); +} + +void fnix_set_target(FnixCanvas *canvas) +{ + g_target = canvas; +} + +FnixCanvas *fnix_get_target(void) +{ + return g_target; +} diff --git a/src/graphics/canvas_internal.h b/src/graphics/canvas_internal.h new file mode 100644 index 0000000..78ab7b2 --- /dev/null +++ b/src/graphics/canvas_internal.h @@ -0,0 +1,13 @@ +/* Uso interno de FnixLib. No forma parte de la API pública. */ +#ifndef FNIX_GRAPHICS_CANVAS_INTERNAL_H +#define FNIX_GRAPHICS_CANVAS_INTERNAL_H + +#include "fnix/canvas.h" + +struct FnixCanvas { + int width; + int height; + uint32_t *pixels; /* width * height, formato 0x00RRGGBB */ +}; + +#endif /* FNIX_GRAPHICS_CANVAS_INTERNAL_H */ diff --git a/src/graphics/draw2d.c b/src/graphics/draw2d.c new file mode 100644 index 0000000..7c2d34c --- /dev/null +++ b/src/graphics/draw2d.c @@ -0,0 +1,371 @@ +/* + * Renderizador 2D por software. + * + * Detalles de implementación: + * - Los cálculos geométricos usan int64_t. Como las entradas están limitadas a + * [-2^28, 2^28], ningún producto intermedio desborda. + * - Los recortes se hacen antes de recorrer píxeles, así que el coste depende + * del tamaño del lienzo y no del tamaño de lo dibujado. + * - Cada primitiva escribe cada píxel como máximo una vez (importante para el + * alfa: mezclar dos veces el mismo píxel daría otro color). + */ +#include "fnix/draw2d.h" + +#include "core/internal.h" +#include "fnix/canvas.h" +#include "graphics/canvas_internal.h" + +#define FNIX_COORD_LIMIT (1 << 28) + +typedef struct Pen { + FnixCanvas *canvas; + FnixColor color; + uint32_t packed; /* color opaco empaquetado 0x00RRGGBB */ +} Pen; + +static inline uint32_t pack_color(FnixColor c) +{ + return ((uint32_t)c.r << 16) | ((uint32_t)c.g << 8) | (uint32_t)c.b; +} + +/* Mezcla "source-over" con alfa recto; redondea al entero más cercano. */ +static inline uint32_t blend_over(uint32_t dst, FnixColor src) +{ + const uint32_t a = src.a; + const uint32_t inv = 255u - a; + const uint32_t r = ((uint32_t)src.r * a + ((dst >> 16) & 0xFFu) * inv + 127u) / 255u; + const uint32_t g = ((uint32_t)src.g * a + ((dst >> 8) & 0xFFu) * inv + 127u) / 255u; + const uint32_t b = ((uint32_t)src.b * a + (dst & 0xFFu) * inv + 127u) / 255u; + return (r << 16) | (g << 8) | b; +} + +static void error_no_target(void) +{ + fnix_set_error(FNIX_ERROR_INVALID_ARGUMENT, + "no hay lienzo de destino: llama a fnix_begin_frame() o " + "fnix_set_target() antes de dibujar"); +} + +static void error_out_of_range(void) +{ + fnix_set_error(FNIX_ERROR_INVALID_ARGUMENT, + "valor fuera de rango: coordenadas, tamaños y radios deben estar " + "entre -%d y %d", + FNIX_COORD_LIMIT, FNIX_COORD_LIMIT); +} + +/* Prepara el pincel. Devuelve false si no hay nada que dibujar. */ +static bool pen_begin(Pen *pen, FnixColor color) +{ + pen->canvas = fnix_get_target(); + if (pen->canvas == NULL) { + error_no_target(); + return false; + } + if (color.a == 0) { + return false; + } + pen->color = color; + pen->packed = pack_color(color); + return true; +} + +static bool in_range(int v) +{ + return v >= -FNIX_COORD_LIMIT && v <= FNIX_COORD_LIMIT; +} + +static inline void plot(const Pen *pen, int64_t x, int64_t y) +{ + FnixCanvas *c = pen->canvas; + if (x < 0 || y < 0 || x >= c->width || y >= c->height) { + return; + } + uint32_t *p = &c->pixels[(size_t)y * (size_t)c->width + (size_t)x]; + *p = (pen->color.a == 255) ? pen->packed : blend_over(*p, pen->color); +} + +/* Fila horizontal y, de x_first a x_last inclusive, recortada al lienzo. */ +static void fill_span(const Pen *pen, int64_t x_first, int64_t x_last, int64_t y) +{ + FnixCanvas *c = pen->canvas; + if (y < 0 || y >= c->height || x_first > x_last) { + return; + } + if (x_first < 0) { + x_first = 0; + } + if (x_last >= c->width) { + x_last = (int64_t)c->width - 1; + } + if (x_first > x_last) { + return; + } + + uint32_t *row = c->pixels + (size_t)y * (size_t)c->width; + if (pen->color.a == 255) { + for (int64_t x = x_first; x <= x_last; x++) { + row[x] = pen->packed; + } + } else { + for (int64_t x = x_first; x <= x_last; x++) { + row[x] = blend_over(row[x], pen->color); + } + } +} + +static int64_t max64(int64_t a, int64_t b) { return a > b ? a : b; } +static int64_t min64(int64_t a, int64_t b) { return a < b ? a : b; } + +void fnix_clear(FnixColor color) +{ + FnixCanvas *c = fnix_get_target(); + if (c == NULL) { + error_no_target(); + return; + } + const uint32_t packed = pack_color(color); + const size_t count = (size_t)c->width * (size_t)c->height; + for (size_t i = 0; i < count; i++) { + c->pixels[i] = packed; + } +} + +void fnix_draw_pixel(int x, int y, FnixColor color) +{ + Pen pen; + if (!pen_begin(&pen, color)) { + return; + } + plot(&pen, x, y); +} + +/* + * Línea de Bresenham a lo largo del eje "mayor". En el paso i (0..a_len) el + * desplazamiento en el eje menor es floor((2*m_len*i + a_len) / (2*a_len)), + * que se actualiza de forma incremental. Solo se recorren los pasos cuya + * coordenada mayor cae dentro del lienzo. + */ +static void line_major(const Pen *pen, int64_t a_start, int64_t a_step, + int64_t a_len, int64_t m_start, int64_t m_step, + int64_t m_len, int64_t a_size, bool swap_axes) +{ + int64_t first, last; /* pasos i cuya coordenada mayor está en [0, a_size-1] */ + if (a_step > 0) { + first = -a_start; + last = a_size - 1 - a_start; + } else { + first = a_start - (a_size - 1); + last = a_start; + } + first = max64(first, 0); + last = min64(last, a_len); + if (first > last) { + return; + } + + const int64_t twice = 2 * a_len; + const int64_t numerator = 2 * m_len * first + a_len; + int64_t m_offset = numerator / twice; + int64_t remainder = numerator % twice; + + for (int64_t i = first; i <= last; i++) { + const int64_t a = a_start + a_step * i; + const int64_t m = m_start + m_step * m_offset; + if (swap_axes) { + plot(pen, m, a); + } else { + plot(pen, a, m); + } + remainder += 2 * m_len; + if (remainder >= twice) { + remainder -= twice; + m_offset++; + } + } +} + +void fnix_draw_line(int x_start, int y_start, int x_end, int y_end, FnixColor color) +{ + Pen pen; + if (!pen_begin(&pen, color)) { + return; + } + if (!in_range(x_start) || !in_range(y_start) || !in_range(x_end) || + !in_range(y_end)) { + error_out_of_range(); + return; + } + + const int64_t dx = (int64_t)x_end - x_start; + const int64_t dy = (int64_t)y_end - y_start; + const int64_t adx = dx < 0 ? -dx : dx; + const int64_t ady = dy < 0 ? -dy : dy; + const int64_t sx = dx < 0 ? -1 : 1; + const int64_t sy = dy < 0 ? -1 : 1; + + if (adx == 0 && ady == 0) { + plot(&pen, x_start, y_start); + } else if (adx >= ady) { + line_major(&pen, x_start, sx, adx, y_start, sy, ady, pen.canvas->width, false); + } else { + line_major(&pen, y_start, sy, ady, x_start, sx, adx, pen.canvas->height, true); + } +} + +static void rect_fill(const Pen *pen, int x, int y, int width, int height) +{ + const int64_t x_last = (int64_t)x + width - 1; + const int64_t y_last = (int64_t)y + height - 1; + const int64_t row_first = max64(y, 0); + const int64_t row_stop = min64(y_last, (int64_t)pen->canvas->height - 1); + for (int64_t row = row_first; row <= row_stop; row++) { + fill_span(pen, x, x_last, row); + } +} + +void fnix_draw_rect(int x, int y, int width, int height, FnixColor color) +{ + Pen pen; + if (!pen_begin(&pen, color) || width <= 0 || height <= 0) { + return; + } + if (!in_range(x) || !in_range(y) || !in_range(width) || !in_range(height)) { + error_out_of_range(); + return; + } + rect_fill(&pen, x, y, width, height); +} + +void fnix_draw_rect_outline(int x, int y, int width, int height, FnixColor color) +{ + Pen pen; + if (!pen_begin(&pen, color) || width <= 0 || height <= 0) { + return; + } + if (!in_range(x) || !in_range(y) || !in_range(width) || !in_range(height)) { + error_out_of_range(); + return; + } + if (width <= 2 || height <= 2) { + rect_fill(&pen, x, y, width, height); /* no queda interior */ + return; + } + + const int64_t x_last = (int64_t)x + width - 1; + const int64_t y_last = (int64_t)y + height - 1; + fill_span(&pen, x, x_last, y); + fill_span(&pen, x, x_last, y_last); + + const int64_t row_first = max64((int64_t)y + 1, 0); + const int64_t row_stop = min64(y_last - 1, (int64_t)pen.canvas->height - 1); + for (int64_t row = row_first; row <= row_stop; row++) { + fill_span(&pen, x, x, row); + fill_span(&pen, x_last, x_last, row); + } +} + +/* Raíz cuadrada entera por defecto (n >= 0), método de Newton. */ +static int64_t isqrt64(int64_t n) +{ + if (n < 2) { + return n; + } + int64_t x = n; + int64_t y = (x + 1) / 2; + while (y < x) { + x = y; + y = (x + n / x) / 2; + } + return x; +} + +/* Semiancho del disco de radio r en la fila dy, seguido de forma incremental: + * al aumentar |dy| el semiancho nunca crece. Requiere 0 <= dy <= r. */ +typedef struct DiscRow { + int64_t limit; /* r*r + r */ + int64_t x; +} DiscRow; + +static void disc_start(DiscRow *d, int64_t r, int64_t dy) +{ + d->limit = r * r + r; + d->x = isqrt64(d->limit - dy * dy); +} + +static int64_t disc_half_width(DiscRow *d, int64_t dy) +{ + while (d->x * d->x + dy * dy > d->limit) { + d->x--; + } + return d->x; +} + +/* Disco (ring = false) o anillo de 1 píxel (ring = true). Se recorre la mitad + * inferior (dy >= 0) y la superior (dy >= 1) por separado, solo en las filas + * visibles; cada fila se dibuja una vez. */ +static void circle_draw(const Pen *pen, int64_t cx, int64_t cy, int64_t r, bool ring) +{ + const int64_t height = pen->canvas->height; + + for (int half = 0; half < 2; half++) { + int64_t lo, hi; /* rango de dy visible */ + if (half == 0) { + lo = max64(0, -cy); + hi = min64(r, height - 1 - cy); + } else { + lo = max64(1, cy - (height - 1)); + hi = min64(r, cy); + } + if (lo > hi) { + continue; + } + + DiscRow outer, inner; + bool inner_started = false; + disc_start(&outer, r, lo); + + for (int64_t dy = lo; dy <= hi; dy++) { + const int64_t y = (half == 0) ? cy + dy : cy - dy; + const int64_t xo = disc_half_width(&outer, dy); + + if (ring && dy < r) { + if (!inner_started) { + disc_start(&inner, r - 1, dy); + inner_started = true; + } + const int64_t xi = disc_half_width(&inner, dy); + fill_span(pen, cx - xo, cx - xi - 1, y); + fill_span(pen, cx + xi + 1, cx + xo, y); + } else { + fill_span(pen, cx - xo, cx + xo, y); + } + } + } +} + +void fnix_draw_circle(int cx, int cy, int radius, FnixColor color) +{ + Pen pen; + if (!pen_begin(&pen, color) || radius < 0) { + return; + } + if (!in_range(cx) || !in_range(cy) || !in_range(radius)) { + error_out_of_range(); + return; + } + circle_draw(&pen, cx, cy, radius, false); +} + +void fnix_draw_circle_outline(int cx, int cy, int radius, FnixColor color) +{ + Pen pen; + if (!pen_begin(&pen, color) || radius < 0) { + return; + } + if (!in_range(cx) || !in_range(cy) || !in_range(radius)) { + error_out_of_range(); + return; + } + circle_draw(&pen, cx, cy, radius, true); +} diff --git a/src/input/input.c b/src/input/input.c new file mode 100644 index 0000000..dd13665 --- /dev/null +++ b/src/input/input.c @@ -0,0 +1,177 @@ +#include "fnix/input.h" + +#include "core/window_internal.h" +#include "fnix/events.h" +#include "input/input_internal.h" + +#include + +static bool valid_key(FnixKey key) +{ + return key > FNIX_KEY_UNKNOWN && key < FNIX_KEY_COUNT; +} + +static bool valid_button(FnixMouseButton button) +{ + return button >= 0 && button < FNIX_MOUSE_BUTTON_COUNT; +} + +void fnix_input_begin_frame(FnixInputState *state) +{ + memset(state->key_pressed, 0, sizeof state->key_pressed); + memset(state->key_released, 0, sizeof state->key_released); + memset(state->mouse_pressed, 0, sizeof state->mouse_pressed); + memset(state->mouse_released, 0, sizeof state->mouse_released); + state->wheel = 0; +} + +void fnix_input_apply_event(FnixInputState *state, FnixEvent *event) +{ + switch (event->type) { + case FNIX_EVENT_KEY_DOWN: + if (valid_key(event->key)) { + if (state->key_down[event->key]) { + event->repeat = true; + } else { + event->repeat = false; + state->key_down[event->key] = true; + state->key_pressed[event->key] = true; + } + } + break; + + case FNIX_EVENT_KEY_UP: + if (valid_key(event->key) && state->key_down[event->key]) { + state->key_down[event->key] = false; + state->key_released[event->key] = true; + } + break; + + case FNIX_EVENT_MOUSE_BUTTON_DOWN: + state->mouse_x = event->x; + state->mouse_y = event->y; + if (valid_button(event->button) && !state->mouse_down[event->button]) { + state->mouse_down[event->button] = true; + state->mouse_pressed[event->button] = true; + } + break; + + case FNIX_EVENT_MOUSE_BUTTON_UP: + state->mouse_x = event->x; + state->mouse_y = event->y; + if (valid_button(event->button) && state->mouse_down[event->button]) { + state->mouse_down[event->button] = false; + state->mouse_released[event->button] = true; + } + break; + + case FNIX_EVENT_MOUSE_MOVE: + state->mouse_x = event->x; + state->mouse_y = event->y; + break; + + case FNIX_EVENT_MOUSE_WHEEL: + state->mouse_x = event->x; + state->mouse_y = event->y; + state->wheel += event->wheel; + break; + + case FNIX_EVENT_FOCUS_LOST: + /* Evita teclas "pegadas": lo que estaba pulsado pasa a soltado. */ + for (int k = 0; k < FNIX_KEY_COUNT; k++) { + if (state->key_down[k]) { + state->key_down[k] = false; + state->key_released[k] = true; + } + } + for (int b = 0; b < FNIX_MOUSE_BUTTON_COUNT; b++) { + if (state->mouse_down[b]) { + state->mouse_down[b] = false; + state->mouse_released[b] = true; + } + } + break; + + case FNIX_EVENT_NONE: + case FNIX_EVENT_WINDOW_CLOSE: + case FNIX_EVENT_FOCUS_GAINED: + break; + } +} + +void fnix_event_queue_push(FnixEventQueue *queue, const FnixEvent *event) +{ + if (queue->count == FNIX_EVENT_QUEUE_CAPACITY) { + queue->head = (queue->head + 1) % FNIX_EVENT_QUEUE_CAPACITY; + queue->count--; + } + const int tail = (queue->head + queue->count) % FNIX_EVENT_QUEUE_CAPACITY; + queue->items[tail] = *event; + queue->count++; +} + +bool fnix_event_queue_pop(FnixEventQueue *queue, FnixEvent *out) +{ + if (queue->count == 0) { + return false; + } + *out = queue->items[queue->head]; + queue->head = (queue->head + 1) % FNIX_EVENT_QUEUE_CAPACITY; + queue->count--; + return true; +} + +/* ---- API pública ------------------------------------------------------ */ + +bool fnix_key_down(const FnixWindow *window, FnixKey key) +{ + return window && valid_key(key) && window->input.key_down[key]; +} + +bool fnix_key_pressed(const FnixWindow *window, FnixKey key) +{ + return window && valid_key(key) && window->input.key_pressed[key]; +} + +bool fnix_key_released(const FnixWindow *window, FnixKey key) +{ + return window && valid_key(key) && window->input.key_released[key]; +} + +bool fnix_mouse_down(const FnixWindow *window, FnixMouseButton button) +{ + return window && valid_button(button) && window->input.mouse_down[button]; +} + +bool fnix_mouse_pressed(const FnixWindow *window, FnixMouseButton button) +{ + return window && valid_button(button) && window->input.mouse_pressed[button]; +} + +bool fnix_mouse_released(const FnixWindow *window, FnixMouseButton button) +{ + return window && valid_button(button) && window->input.mouse_released[button]; +} + +int fnix_mouse_x(const FnixWindow *window) +{ + return window ? window->input.mouse_x : 0; +} + +int fnix_mouse_y(const FnixWindow *window) +{ + return window ? window->input.mouse_y : 0; +} + +int fnix_mouse_wheel(const FnixWindow *window) +{ + return window ? window->input.wheel : 0; +} + +bool fnix_window_poll_event(FnixWindow *window, FnixEvent *event) +{ + if (window == NULL || event == NULL) { + return false; + } + return fnix_event_queue_pop(&window->events, event); +} diff --git a/src/input/input_internal.h b/src/input/input_internal.h new file mode 100644 index 0000000..cbf91b1 --- /dev/null +++ b/src/input/input_internal.h @@ -0,0 +1,41 @@ +/* Uso interno de FnixLib. No forma parte de la API pública. */ +#ifndef FNIX_INPUT_INTERNAL_H +#define FNIX_INPUT_INTERNAL_H + +#include "fnix/events.h" +#include "fnix/input.h" + +#define FNIX_EVENT_QUEUE_CAPACITY 256 + +typedef struct FnixInputState { + bool key_down[FNIX_KEY_COUNT]; + bool key_pressed[FNIX_KEY_COUNT]; + bool key_released[FNIX_KEY_COUNT]; + bool mouse_down[FNIX_MOUSE_BUTTON_COUNT]; + bool mouse_pressed[FNIX_MOUSE_BUTTON_COUNT]; + bool mouse_released[FNIX_MOUSE_BUTTON_COUNT]; + int mouse_x; + int mouse_y; + int wheel; +} FnixInputState; + +typedef struct FnixEventQueue { + FnixEvent items[FNIX_EVENT_QUEUE_CAPACITY]; + int head; /* índice del evento más antiguo */ + int count; /* eventos almacenados */ +} FnixEventQueue; + +/* Borra lo que solo dura un fotograma: pressed, released y rueda. */ +void fnix_input_begin_frame(FnixInputState *state); + +/* Actualiza el estado con un evento. Si es un KEY_DOWN de una tecla que ya + * estaba pulsada, lo marca como repetición (event->repeat = true). */ +void fnix_input_apply_event(FnixInputState *state, FnixEvent *event); + +/* Añade un evento; si la cola está llena descarta el más antiguo. */ +void fnix_event_queue_push(FnixEventQueue *queue, const FnixEvent *event); + +/* Extrae el evento más antiguo. Devuelve false si la cola está vacía. */ +bool fnix_event_queue_pop(FnixEventQueue *queue, FnixEvent *out); + +#endif /* FNIX_INPUT_INTERNAL_H */ diff --git a/src/platform/linux/x11_keys.c b/src/platform/linux/x11_keys.c new file mode 100644 index 0000000..ee1b21d --- /dev/null +++ b/src/platform/linux/x11_keys.c @@ -0,0 +1,44 @@ +#include "platform/linux/x11_keys.h" + +#include + +/* La traducción de rangos depende de que estos grupos sean contiguos. */ +_Static_assert(FNIX_KEY_Z - FNIX_KEY_A == 25, "las letras deben ser contiguas"); +_Static_assert(FNIX_KEY_9 - FNIX_KEY_0 == 9, "los dígitos deben ser contiguos"); +_Static_assert(FNIX_KEY_F12 - FNIX_KEY_F1 == 11, "F1..F12 deben ser contiguas"); + +FnixKey fnix_x11_translate_key(KeySym keysym) +{ + if (keysym >= XK_a && keysym <= XK_z) { + return (FnixKey)(FNIX_KEY_A + (int)(keysym - XK_a)); + } + if (keysym >= XK_A && keysym <= XK_Z) { + return (FnixKey)(FNIX_KEY_A + (int)(keysym - XK_A)); + } + if (keysym >= XK_0 && keysym <= XK_9) { + return (FnixKey)(FNIX_KEY_0 + (int)(keysym - XK_0)); + } + if (keysym >= XK_F1 && keysym <= XK_F12) { + return (FnixKey)(FNIX_KEY_F1 + (int)(keysym - XK_F1)); + } + + switch (keysym) { + case XK_space: return FNIX_KEY_SPACE; + case XK_Return: return FNIX_KEY_ENTER; + case XK_KP_Enter: return FNIX_KEY_ENTER; + case XK_Escape: return FNIX_KEY_ESCAPE; + case XK_Tab: return FNIX_KEY_TAB; + case XK_BackSpace: return FNIX_KEY_BACKSPACE; + case XK_Left: return FNIX_KEY_LEFT; + case XK_Right: return FNIX_KEY_RIGHT; + case XK_Up: return FNIX_KEY_UP; + case XK_Down: return FNIX_KEY_DOWN; + case XK_Shift_L: return FNIX_KEY_LEFT_SHIFT; + case XK_Shift_R: return FNIX_KEY_RIGHT_SHIFT; + case XK_Control_L: return FNIX_KEY_LEFT_CTRL; + case XK_Control_R: return FNIX_KEY_RIGHT_CTRL; + case XK_Alt_L: return FNIX_KEY_LEFT_ALT; + case XK_Alt_R: return FNIX_KEY_RIGHT_ALT; + default: return FNIX_KEY_UNKNOWN; + } +} diff --git a/src/platform/linux/x11_keys.h b/src/platform/linux/x11_keys.h new file mode 100644 index 0000000..fd62f80 --- /dev/null +++ b/src/platform/linux/x11_keys.h @@ -0,0 +1,13 @@ +/* Uso interno de FnixLib (plataforma Linux/X11). */ +#ifndef FNIX_PLATFORM_LINUX_X11_KEYS_H +#define FNIX_PLATFORM_LINUX_X11_KEYS_H + +#include "fnix/input.h" + +#include + +/* Traduce un KeySym de X11 a FnixKey. Devuelve FNIX_KEY_UNKNOWN si no hay + * equivalente. */ +FnixKey fnix_x11_translate_key(KeySym keysym); + +#endif /* FNIX_PLATFORM_LINUX_X11_KEYS_H */ diff --git a/src/platform/linux/x11_window.c b/src/platform/linux/x11_window.c new file mode 100644 index 0000000..7d033a1 --- /dev/null +++ b/src/platform/linux/x11_window.c @@ -0,0 +1,263 @@ +/* + * Ventana para Linux sobre Xlib (libX11). + * + * Dependencia del sistema: libX11 (paquete `libx11` en Arch/CachyOS, + * `libx11-dev` en Debian/Ubuntu). Se considera una interfaz nativa del sistema + * gráfico, igual que Win32 en Windows; no es una biblioteca multimedia. + * + * Presentación: el lienzo se copia a la ventana con XPutImage (sin aceleración + * ni sincronización vertical). Solo se admite la configuración de pantalla más + * común: visual TrueColor con máscaras 0xFF0000/0x00FF00/0x0000FF, 32 bits por + * píxel y el mismo orden de bytes que la CPU. Si no se cumple, la creación de + * la ventana falla con FNIX_ERROR_UNSUPPORTED y un mensaje explicativo. + */ +#include "platform/platform.h" + +#include "core/internal.h" +#include "core/window_internal.h" +#include "platform/linux/x11_keys.h" + +#include +#include +#include + +#include +#include +#include + +struct FnixPlatformWindow { + Display *display; + Window window; + GC gc; + XImage *image; + Atom wm_delete; + int width; + int height; + uint32_t *surface; +}; + +static bool host_is_little_endian(void) +{ + const uint16_t one = 1; + return *(const uint8_t *)&one == 1; +} + +FnixPlatformWindow *fnix_platform_window_create(const char *title, int width, + int height, uint32_t *surface) +{ + FnixPlatformWindow *pw = calloc(1, sizeof *pw); + if (pw == NULL) { + fnix_set_error(FNIX_ERROR_OUT_OF_MEMORY, "sin memoria para la ventana X11"); + return NULL; + } + pw->width = width; + pw->height = height; + pw->surface = surface; + + pw->display = XOpenDisplay(NULL); + if (pw->display == NULL) { + const char *name = getenv("DISPLAY"); + fnix_set_error(FNIX_ERROR_PLATFORM, + "no se pudo abrir la pantalla X11 (DISPLAY=%s): ¿hay un servidor " + "X11 o XWayland en ejecución?", + name ? name : "sin definir"); + goto fail; + } + + Display *dpy = pw->display; + const int screen = DefaultScreen(dpy); + Visual *visual = DefaultVisual(dpy, screen); + const int depth = DefaultDepth(dpy, screen); + + if ((depth != 24 && depth != 32) || visual->class != TrueColor || + visual->red_mask != 0xFF0000UL || visual->green_mask != 0x00FF00UL || + visual->blue_mask != 0x0000FFUL) { + fnix_set_error(FNIX_ERROR_UNSUPPORTED, + "formato de pantalla no soportado (profundidad %d, máscaras " + "R=%#lx G=%#lx B=%#lx); se necesita TrueColor 24/32 bits RGB", + depth, visual->red_mask, visual->green_mask, visual->blue_mask); + goto fail; + } + if ((ImageByteOrder(dpy) == LSBFirst) != host_is_little_endian()) { + fnix_set_error(FNIX_ERROR_UNSUPPORTED, + "el servidor X11 usa un orden de bytes distinto al de la CPU"); + goto fail; + } + + /* Imagen que describe el formato del lienzo; sus datos se asignan en cada + * presentación. */ + pw->image = XCreateImage(dpy, visual, (unsigned)depth, ZPixmap, 0, NULL, + (unsigned)width, (unsigned)height, 32, width * 4); + if (pw->image == NULL || pw->image->bits_per_pixel != 32) { + fnix_set_error(FNIX_ERROR_UNSUPPORTED, + "el servidor X11 no ofrece imágenes de 32 bits por píxel"); + goto fail; + } + + XSetWindowAttributes attributes; + memset(&attributes, 0, sizeof attributes); + attributes.background_pixmap = None; /* sin fondo: evita parpadeos */ + attributes.event_mask = KeyPressMask | KeyReleaseMask | ButtonPressMask | + ButtonReleaseMask | PointerMotionMask | ExposureMask | + FocusChangeMask; + + pw->window = XCreateWindow(dpy, RootWindow(dpy, screen), 0, 0, (unsigned)width, + (unsigned)height, 0, depth, InputOutput, visual, + CWBackPixmap | CWEventMask, &attributes); + if (pw->window == 0) { + fnix_set_error(FNIX_ERROR_PLATFORM, "XCreateWindow falló"); + goto fail; + } + + pw->gc = XCreateGC(dpy, pw->window, 0, NULL); + if (pw->gc == NULL) { + fnix_set_error(FNIX_ERROR_PLATFORM, "XCreateGC falló"); + goto fail; + } + + /* Título (ICCCM y EWMH, este último admite UTF-8). */ + XStoreName(dpy, pw->window, title); + XChangeProperty(dpy, pw->window, XInternAtom(dpy, "_NET_WM_NAME", False), + XInternAtom(dpy, "UTF8_STRING", False), 8, PropModeReplace, + (const unsigned char *)title, (int)strlen(title)); + + /* Tamaño fijo: mínimo = máximo. */ + XSizeHints *hints = XAllocSizeHints(); + if (hints != NULL) { + hints->flags = PMinSize | PMaxSize; + hints->min_width = hints->max_width = width; + hints->min_height = hints->max_height = height; + XSetWMNormalHints(dpy, pw->window, hints); + XFree(hints); + } + + /* Recibir el cierre de ventana como mensaje en lugar de perder la conexión. */ + pw->wm_delete = XInternAtom(dpy, "WM_DELETE_WINDOW", False); + XSetWMProtocols(dpy, pw->window, &pw->wm_delete, 1); + + /* Sin esto, mantener una tecla genera pares KeyRelease/KeyPress falsos. */ + XkbSetDetectableAutoRepeat(dpy, True, NULL); + + XMapWindow(dpy, pw->window); + XFlush(dpy); + return pw; + +fail: + fnix_platform_window_destroy(pw); + return NULL; +} + +void fnix_platform_window_destroy(FnixPlatformWindow *pw) +{ + if (pw == NULL) { + return; + } + if (pw->image != NULL) { + pw->image->data = NULL; /* el búfer pertenece al lienzo, no a Xlib */ + XDestroyImage(pw->image); + } + if (pw->gc != NULL) { + XFreeGC(pw->display, pw->gc); + } + if (pw->window != 0) { + XDestroyWindow(pw->display, pw->window); + } + if (pw->display != NULL) { + XCloseDisplay(pw->display); + } + free(pw); +} + +void fnix_platform_window_present(FnixPlatformWindow *pw) +{ + if (pw == NULL) { + return; + } + pw->image->data = (char *)pw->surface; + XPutImage(pw->display, pw->window, pw->gc, pw->image, 0, 0, 0, 0, + (unsigned)pw->width, (unsigned)pw->height); + XFlush(pw->display); +} + +void fnix_platform_pump_events(FnixPlatformWindow *pw, FnixWindow *owner) +{ + if (pw == NULL) { + return; + } + + while (XPending(pw->display) > 0) { + XEvent xe; + XNextEvent(pw->display, &xe); + + FnixEvent event; + memset(&event, 0, sizeof event); + + switch (xe.type) { + case ClientMessage: + if (xe.xclient.format == 32 && (Atom)xe.xclient.data.l[0] == pw->wm_delete) { + event.type = FNIX_EVENT_WINDOW_CLOSE; + fnix_window_dispatch_event(owner, &event); + } + break; + + case KeyPress: + case KeyRelease: { + const FnixKey key = fnix_x11_translate_key(XLookupKeysym(&xe.xkey, 0)); + if (key != FNIX_KEY_UNKNOWN) { + event.type = (xe.type == KeyPress) ? FNIX_EVENT_KEY_DOWN : FNIX_EVENT_KEY_UP; + event.key = key; + fnix_window_dispatch_event(owner, &event); + } + break; + } + + case ButtonPress: + case ButtonRelease: + event.x = xe.xbutton.x; + event.y = xe.xbutton.y; + if (xe.xbutton.button == Button4 || xe.xbutton.button == Button5) { + if (xe.type == ButtonPress) { /* la rueda solo genera pulsación */ + event.type = FNIX_EVENT_MOUSE_WHEEL; + event.wheel = (xe.xbutton.button == Button4) ? 1 : -1; + fnix_window_dispatch_event(owner, &event); + } + } else if (xe.xbutton.button == Button1 || xe.xbutton.button == Button2 || + xe.xbutton.button == Button3) { + event.type = (xe.type == ButtonPress) ? FNIX_EVENT_MOUSE_BUTTON_DOWN + : FNIX_EVENT_MOUSE_BUTTON_UP; + event.button = (xe.xbutton.button == Button1) ? FNIX_MOUSE_LEFT + : (xe.xbutton.button == Button2) ? FNIX_MOUSE_MIDDLE + : FNIX_MOUSE_RIGHT; + fnix_window_dispatch_event(owner, &event); + } + break; + + case MotionNotify: + event.type = FNIX_EVENT_MOUSE_MOVE; + event.x = xe.xmotion.x; + event.y = xe.xmotion.y; + fnix_window_dispatch_event(owner, &event); + break; + + case FocusIn: + case FocusOut: + if (xe.xfocus.detail != NotifyPointer) { + event.type = (xe.type == FocusIn) ? FNIX_EVENT_FOCUS_GAINED + : FNIX_EVENT_FOCUS_LOST; + fnix_window_dispatch_event(owner, &event); + } + break; + + case Expose: + /* Sin almacenamiento de respaldo, el servidor pide repintar al + * descubrirse la ventana; se hace una vez por ráfaga de Expose. */ + if (xe.xexpose.count == 0) { + fnix_platform_window_present(pw); + } + break; + + default: + break; + } + } +} diff --git a/src/platform/platform.h b/src/platform/platform.h new file mode 100644 index 0000000..58dac49 --- /dev/null +++ b/src/platform/platform.h @@ -0,0 +1,31 @@ +/* + * Interfaz entre el núcleo de FnixLib y cada sistema operativo. + * + * Cada plataforma (src/platform/linux, y en el futuro src/platform/windows) + * implementa estas funciones. El núcleo no incluye cabeceras del sistema. + * + * Uso interno de FnixLib. No forma parte de la API pública. + */ +#ifndef FNIX_PLATFORM_H +#define FNIX_PLATFORM_H + +#include "fnix/types.h" + +typedef struct FnixPlatformWindow FnixPlatformWindow; + +/* Crea y muestra una ventana de tamaño fijo. `surface` apunta al búfer de + * width*height píxeles 0x00RRGGBB que se mostrará; debe seguir válido hasta + * fnix_platform_window_destroy(). Devuelve NULL y registra el error si falla. */ +FnixPlatformWindow *fnix_platform_window_create(const char *title, int width, + int height, uint32_t *surface); + +void fnix_platform_window_destroy(FnixPlatformWindow *window); + +/* Copia la superficie a la ventana. */ +void fnix_platform_window_present(FnixPlatformWindow *window); + +/* Lee los eventos pendientes del sistema y los entrega a `owner` con + * fnix_window_dispatch_event(). No bloquea. */ +void fnix_platform_pump_events(FnixPlatformWindow *window, FnixWindow *owner); + +#endif /* FNIX_PLATFORM_H */ diff --git a/src/time/time.c b/src/time/time.c new file mode 100644 index 0000000..ca21a32 --- /dev/null +++ b/src/time/time.c @@ -0,0 +1,35 @@ +/* + * Tiempo para sistemas POSIX (Linux). La versión de Windows aún no existe. + */ +#define _POSIX_C_SOURCE 200809L + +#include "fnix/time.h" + +#include +#include + +double fnix_time_now(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} + +void fnix_time_sleep(double seconds) +{ + if (!(seconds > 0.0)) { + return; /* también descarta NaN */ + } + if (seconds > 1e9) { + seconds = 1e9; + } + + struct timespec request; + request.tv_sec = (time_t)seconds; + request.tv_nsec = (long)((seconds - (double)request.tv_sec) * 1e9); + + /* clock_nanosleep devuelve el código de error directamente. Si una señal la + * interrumpe, `request` queda con el tiempo restante y se continúa. */ + while (clock_nanosleep(CLOCK_MONOTONIC, 0, &request, &request) == EINTR) { + } +} diff --git a/tests/check.h b/tests/check.h new file mode 100644 index 0000000..f9f948b --- /dev/null +++ b/tests/check.h @@ -0,0 +1,26 @@ +/* Mini-arnés de pruebas: sin dependencias externas. */ +#ifndef FNIX_TEST_CHECK_H +#define FNIX_TEST_CHECK_H + +#include + +static int g_check_failures = 0; +static int g_check_total = 0; + +#define CHECK(cond) \ + do { \ + g_check_total++; \ + if (!(cond)) { \ + g_check_failures++; \ + fprintf(stderr, "%s:%d: CHECK fallido: %s\n", __FILE__, __LINE__, \ + #cond); \ + } \ + } while (0) + +/* Imprime el resumen y devuelve el código de salida del programa. */ +#define CHECK_REPORT(name) \ + (printf("%s: %d comprobaciones, %d fallos\n", (name), g_check_total, \ + g_check_failures), \ + g_check_failures == 0 ? 0 : 1) + +#endif /* FNIX_TEST_CHECK_H */ diff --git a/tests/test_core.c b/tests/test_core.c new file mode 100644 index 0000000..a866417 --- /dev/null +++ b/tests/test_core.c @@ -0,0 +1,134 @@ +/* Pruebas sin pantalla: errores, tiempo y validación de argumentos. */ +#include "check.h" + +#include + +#include + +static void test_error_strings(void) +{ + CHECK(strlen(fnix_error_string(FNIX_OK)) > 0); + CHECK(strlen(fnix_error_string(FNIX_ERROR_INVALID_ARGUMENT)) > 0); + CHECK(strlen(fnix_error_string(FNIX_ERROR_OUT_OF_MEMORY)) > 0); + CHECK(strlen(fnix_error_string(FNIX_ERROR_PLATFORM)) > 0); + CHECK(strlen(fnix_error_string(FNIX_ERROR_UNSUPPORTED)) > 0); + CHECK(strlen(fnix_error_string((FnixError)12345)) > 0); +} + +static void test_last_error(void) +{ + fnix_clear_error(); + CHECK(fnix_last_error_code() == FNIX_OK); + CHECK(fnix_last_error() != NULL); + CHECK(strcmp(fnix_last_error(), "") == 0); + + FnixCanvas *canvas = fnix_canvas_create(0, 10); + CHECK(canvas == NULL); + CHECK(fnix_last_error_code() == FNIX_ERROR_INVALID_ARGUMENT); + CHECK(strlen(fnix_last_error()) > 0); + + /* Un éxito no borra el último error (comportamiento tipo errno). */ + canvas = fnix_canvas_create(4, 4); + CHECK(canvas != NULL); + CHECK(fnix_last_error_code() == FNIX_ERROR_INVALID_ARGUMENT); + fnix_canvas_destroy(canvas); + + fnix_clear_error(); + CHECK(fnix_last_error_code() == FNIX_OK); + CHECK(strcmp(fnix_last_error(), "") == 0); +} + +static void test_canvas_validation(void) +{ + CHECK(fnix_canvas_create(-1, 5) == NULL); + CHECK(fnix_canvas_create(5, -1) == NULL); + CHECK(fnix_canvas_create(FNIX_CANVAS_MAX_SIZE + 1, 5) == NULL); + CHECK(fnix_canvas_create(5, FNIX_CANVAS_MAX_SIZE + 1) == NULL); + + FnixCanvas *canvas = fnix_canvas_create(7, 3); + CHECK(canvas != NULL); + CHECK(fnix_canvas_width(canvas) == 7); + CHECK(fnix_canvas_height(canvas) == 3); + CHECK(fnix_canvas_pixels(canvas) != NULL); + + /* Lienzo nuevo: todo negro. */ + for (int y = 0; y < 3; y++) { + for (int x = 0; x < 7; x++) { + const FnixColor c = fnix_canvas_get_pixel(canvas, x, y); + CHECK(c.r == 0 && c.g == 0 && c.b == 0 && c.a == 255); + } + } + + /* Fuera de rango o NULL: transparente / 0. */ + CHECK(fnix_canvas_get_pixel(canvas, -1, 0).a == 0); + CHECK(fnix_canvas_get_pixel(canvas, 7, 0).a == 0); + CHECK(fnix_canvas_get_pixel(canvas, 0, 3).a == 0); + CHECK(fnix_canvas_get_pixel(NULL, 0, 0).a == 0); + CHECK(fnix_canvas_width(NULL) == 0); + CHECK(fnix_canvas_height(NULL) == 0); + CHECK(fnix_canvas_pixels(NULL) == NULL); + + /* Destruir el lienzo de destino limpia el destino. */ + fnix_set_target(canvas); + CHECK(fnix_get_target() == canvas); + fnix_canvas_destroy(canvas); + CHECK(fnix_get_target() == NULL); + fnix_canvas_destroy(NULL); /* no debe fallar */ +} + +static void test_window_validation(void) +{ + /* Estas comprobaciones ocurren antes de tocar el sistema gráfico. */ + fnix_clear_error(); + CHECK(fnix_window_create(NULL, 100, 100) == NULL); + CHECK(fnix_last_error_code() == FNIX_ERROR_INVALID_ARGUMENT); + + fnix_clear_error(); + CHECK(fnix_window_create("x", 0, 100) == NULL); + CHECK(fnix_last_error_code() == FNIX_ERROR_INVALID_ARGUMENT); + CHECK(fnix_window_create("x", 100, -5) == NULL); + CHECK(fnix_window_create("x", FNIX_CANVAS_MAX_SIZE + 1, 100) == NULL); + + /* Funciones con ventana NULL: seguras. */ + fnix_window_destroy(NULL); + fnix_begin_frame(NULL); + fnix_end_frame(NULL); + fnix_window_request_close(NULL); + fnix_window_set_target_fps(NULL, 30); + CHECK(fnix_window_should_close(NULL)); + CHECK(fnix_window_width(NULL) == 0); + CHECK(fnix_window_height(NULL) == 0); + CHECK(fnix_window_canvas(NULL) == NULL); + CHECK(fnix_window_delta_time(NULL) == 0.0); +} + +static void test_time(void) +{ + double previous = fnix_time_now(); + for (int i = 0; i < 1000; i++) { + const double now = fnix_time_now(); + CHECK(now >= previous); + previous = now; + } + + const double before = fnix_time_now(); + fnix_time_sleep(0.05); + const double elapsed = fnix_time_now() - before; + CHECK(elapsed >= 0.05); + CHECK(elapsed < 0.5); + + const double before_zero = fnix_time_now(); + fnix_time_sleep(0.0); + fnix_time_sleep(-3.0); + CHECK(fnix_time_now() - before_zero < 0.01); +} + +int main(void) +{ + test_error_strings(); + test_last_error(); + test_canvas_validation(); + test_window_validation(); + test_time(); + return CHECK_REPORT("test_core"); +} diff --git a/tests/test_draw2d.c b/tests/test_draw2d.c new file mode 100644 index 0000000..604d196 --- /dev/null +++ b/tests/test_draw2d.c @@ -0,0 +1,441 @@ +/* + * Pruebas sin pantalla del renderizador 2D por software. + * Los círculos se comparan píxel a píxel con una referencia calculada por + * fuerza bruta a partir de la definición documentada en draw2d.h. + */ +#include "check.h" + +#include + +#include + +static FnixCanvas *new_canvas(int width, int height) +{ + FnixCanvas *canvas = fnix_canvas_create(width, height); + CHECK(canvas != NULL); + fnix_set_target(canvas); + return canvas; +} + +static bool is_black(const FnixCanvas *c, int x, int y) +{ + const FnixColor p = fnix_canvas_get_pixel(c, x, y); + return p.r == 0 && p.g == 0 && p.b == 0; +} + +static bool pixel_is(const FnixCanvas *c, int x, int y, FnixColor expected) +{ + const FnixColor p = fnix_canvas_get_pixel(c, x, y); + return p.r == expected.r && p.g == expected.g && p.b == expected.b; +} + +static int count_drawn(const FnixCanvas *c) +{ + int n = 0; + for (int y = 0; y < fnix_canvas_height(c); y++) { + for (int x = 0; x < fnix_canvas_width(c); x++) { + if (!is_black(c, x, y)) { + n++; + } + } + } + return n; +} + +static void test_clear(void) +{ + FnixCanvas *c = new_canvas(8, 6); + fnix_clear(FNIX_BLUE); + bool all_blue = true; + for (int y = 0; y < 6; y++) { + for (int x = 0; x < 8; x++) { + all_blue = all_blue && pixel_is(c, x, y, FNIX_BLUE); + } + } + CHECK(all_blue); + + /* clear ignora el alfa y deja el píxel opaco. */ + fnix_clear(FNIX_RGBA(10, 20, 30, 5)); + CHECK(pixel_is(c, 3, 3, FNIX_RGB(10, 20, 30))); + CHECK(fnix_canvas_get_pixel(c, 3, 3).a == 255); + fnix_canvas_destroy(c); +} + +static void test_pixel(void) +{ + FnixCanvas *c = new_canvas(10, 10); + fnix_draw_pixel(3, 4, FNIX_RED); + CHECK(pixel_is(c, 3, 4, FNIX_RED)); + CHECK(count_drawn(c) == 1); + + /* Fuera del lienzo: se ignora. */ + fnix_draw_pixel(-1, 0, FNIX_RED); + fnix_draw_pixel(0, -1, FNIX_RED); + fnix_draw_pixel(10, 0, FNIX_RED); + fnix_draw_pixel(0, 10, FNIX_RED); + fnix_draw_pixel(2000000000, -2000000000, FNIX_RED); + CHECK(count_drawn(c) == 1); + fnix_canvas_destroy(c); +} + +static void test_rect(void) +{ + FnixCanvas *c = new_canvas(8, 8); + + fnix_draw_rect(2, 3, 4, 2, FNIX_GREEN); + CHECK(count_drawn(c) == 8); + CHECK(pixel_is(c, 2, 3, FNIX_GREEN)); + CHECK(pixel_is(c, 5, 4, FNIX_GREEN)); + CHECK(is_black(c, 1, 3) && is_black(c, 6, 3) && is_black(c, 2, 2) && is_black(c, 2, 5)); + + /* Recortado por la esquina superior izquierda: solo 2x2 visibles. */ + fnix_clear(FNIX_BLACK); + fnix_draw_rect(-2, -2, 4, 4, FNIX_RED); + CHECK(count_drawn(c) == 4); + + /* Totalmente fuera, o tamaño nulo/negativo: no dibuja. */ + fnix_clear(FNIX_BLACK); + fnix_draw_rect(20, 20, 5, 5, FNIX_RED); + fnix_draw_rect(-20, 0, 5, 5, FNIX_RED); + fnix_draw_rect(1, 1, 0, 5, FNIX_RED); + fnix_draw_rect(1, 1, 5, -3, FNIX_RED); + CHECK(count_drawn(c) == 0); + + /* Rectángulo mucho mayor que el lienzo: lo cubre entero. */ + fnix_draw_rect(-100, -100, 1000, 1000, FNIX_WHITE); + CHECK(count_drawn(c) == 64); + + /* Valores extremos dentro del rango permitido. */ + fnix_clear(FNIX_BLACK); + fnix_draw_rect(-(1 << 27), -(1 << 27), 1 << 28, 1 << 28, FNIX_WHITE); + CHECK(count_drawn(c) == 64); + fnix_canvas_destroy(c); +} + +static void test_rect_outline(void) +{ + FnixCanvas *c = new_canvas(12, 12); + + fnix_draw_rect_outline(1, 1, 6, 5, FNIX_RED); + CHECK(count_drawn(c) == 2 * 6 + 2 * (5 - 2)); /* 18 */ + CHECK(pixel_is(c, 1, 1, FNIX_RED) && pixel_is(c, 6, 5, FNIX_RED)); + CHECK(is_black(c, 3, 3)); /* interior vacío */ + + /* Sin interior: equivale a un rectángulo lleno. */ + fnix_clear(FNIX_BLACK); + fnix_draw_rect_outline(2, 2, 2, 5, FNIX_RED); + CHECK(count_drawn(c) == 10); + + /* Parcialmente fuera: solo se ven los lados dentro del lienzo. */ + fnix_clear(FNIX_BLACK); + fnix_draw_rect_outline(-3, 4, 8, 5, FNIX_RED); /* x de -3 a 4 */ + CHECK(pixel_is(c, 4, 6, FNIX_RED)); /* lado derecho */ + CHECK(is_black(c, 0, 6)); /* el lado izquierdo quedó fuera */ + CHECK(pixel_is(c, 0, 4, FNIX_RED) && pixel_is(c, 4, 8, FNIX_RED)); + fnix_canvas_destroy(c); +} + +/* Referencia por fuerza bruta: ¿pertenece (dx, dy) al disco de radio r? */ +static bool in_disc(long dx, long dy, long r) +{ + return r >= 0 && dx * dx + dy * dy <= r * r + r; +} + +static void test_circles_match_reference(void) +{ + enum { W = 48, H = 36 }; + FnixCanvas *c = new_canvas(W, H); + const int centers[][2] = {{24, 18}, {2, 3}, {46, 34}, {-5, -5}, {60, 40}, {24, -3}}; + + for (size_t k = 0; k < sizeof centers / sizeof centers[0]; k++) { + for (int r = 0; r <= 20; r++) { + for (int ring = 0; ring < 2; ring++) { + fnix_clear(FNIX_BLACK); + if (ring) { + fnix_draw_circle_outline(centers[k][0], centers[k][1], r, FNIX_WHITE); + } else { + fnix_draw_circle(centers[k][0], centers[k][1], r, FNIX_WHITE); + } + + int mismatches = 0; + for (int y = 0; y < H; y++) { + for (int x = 0; x < W; x++) { + const long dx = x - centers[k][0]; + const long dy = y - centers[k][1]; + bool expected = in_disc(dx, dy, r); + if (ring && r >= 1) { + expected = expected && !in_disc(dx, dy, r - 1); + } + if (expected == is_black(c, x, y)) { + mismatches++; + } + } + } + CHECK(mismatches == 0); + } + } + } + + /* Radio negativo: no dibuja. */ + fnix_clear(FNIX_BLACK); + fnix_draw_circle(10, 10, -1, FNIX_WHITE); + fnix_draw_circle_outline(10, 10, -7, FNIX_WHITE); + CHECK(count_drawn(c) == 0); + + /* Radio 0: un píxel. */ + fnix_draw_circle(10, 10, 0, FNIX_WHITE); + CHECK(count_drawn(c) == 1 && pixel_is(c, 10, 10, FNIX_WHITE)); + fnix_canvas_destroy(c); +} + +static void test_huge_circles(void) +{ + FnixCanvas *c = new_canvas(40, 30); + + /* Disco gigante que cubre el lienzo: termina rápido y lo llena. */ + fnix_draw_circle(0, 0, 1 << 28, FNIX_WHITE); + CHECK(count_drawn(c) == 40 * 30); + + /* Anillo gigante lejos del lienzo: no se ve nada. */ + fnix_clear(FNIX_BLACK); + fnix_draw_circle_outline(0, 0, 1 << 28, FNIX_WHITE); + CHECK(count_drawn(c) == 0); + + /* Borde izquierdo de un disco enorme que roza la columna 0: sin desbordes + * en ninguna de las dos mitades (filas por encima y por debajo del centro). */ + fnix_clear(FNIX_BLACK); + fnix_draw_circle_outline(1 << 28, 10, 1 << 28, FNIX_WHITE); + CHECK(count_drawn(c) == 30); + bool column_zero = true; + for (int y = 0; y < 30; y++) { + column_zero = column_zero && pixel_is(c, 0, y, FNIX_WHITE); + } + CHECK(column_zero); + fnix_canvas_destroy(c); +} + +static void test_alpha_blends_once(void) +{ + FnixCanvas *c = new_canvas(40, 40); + const FnixColor half = FNIX_RGBA(255, 0, 0, 128); + + /* Sobre negro: (255*128 + 127) / 255 = 128. Una doble mezcla daría 192. */ + fnix_draw_rect(2, 2, 5, 5, half); + CHECK(pixel_is(c, 3, 3, FNIX_RGB(128, 0, 0))); + + fnix_clear(FNIX_BLACK); + fnix_draw_circle(20, 20, 15, half); + fnix_draw_circle_outline(20, 20, 8, half); + fnix_draw_line(0, 0, 39, 17, half); + fnix_draw_rect_outline(1, 1, 38, 38, half); + bool single_blend = true; + for (int y = 0; y < 40; y++) { + for (int x = 0; x < 40; x++) { + const FnixColor p = fnix_canvas_get_pixel(c, x, y); + /* Cada píxel pudo recibir 1 o 2 figuras distintas; los que recibieron + * una sola valen exactamente 128. Ninguno puede valer 192 por la + * misma figura: lo comprobamos de forma aislada abajo. */ + single_blend = single_blend && (p.r == 0 || p.r >= 128); + } + } + CHECK(single_blend); + + /* Aislado: cada primitiva por separado mezcla cada píxel una sola vez. */ + for (int shape = 0; shape < 5; shape++) { + fnix_clear(FNIX_BLACK); + switch (shape) { + case 0: fnix_draw_circle(20, 20, 15, half); break; + case 1: fnix_draw_circle_outline(20, 20, 15, half); break; + case 2: fnix_draw_line(0, 0, 39, 17, half); break; + case 3: fnix_draw_rect_outline(1, 1, 38, 38, half); break; + default: fnix_draw_circle_outline(-3, 20, 12, half); break; + } + bool all_128 = true; + for (int y = 0; y < 40; y++) { + for (int x = 0; x < 40; x++) { + const FnixColor p = fnix_canvas_get_pixel(c, x, y); + all_128 = all_128 && (p.r == 0 || p.r == 128); + } + } + CHECK(all_128); + CHECK(count_drawn(c) > 0); + } + + /* Mezcla sobre blanco: R = (255*128 + 255*127 + 127)/255 = 255, + * G = B = (0 + 255*127 + 127)/255 = 127. */ + fnix_clear(FNIX_WHITE); + fnix_draw_rect(0, 0, 4, 4, half); + CHECK(pixel_is(c, 1, 1, FNIX_RGB(255, 127, 127))); + + /* Redondeo al entero más cercano (el truncado daría otros valores): + * 10*200/255 = 7,84 -> 8 y 100*254/255 = 99,6 -> 100. */ + fnix_clear(FNIX_BLACK); + fnix_draw_rect(0, 0, 2, 2, FNIX_RGBA(10, 10, 10, 200)); + CHECK(pixel_is(c, 0, 0, FNIX_RGB(8, 8, 8))); + fnix_clear(FNIX_RGB(100, 100, 100)); + fnix_draw_rect(0, 0, 2, 2, FNIX_RGBA(0, 0, 0, 1)); + CHECK(pixel_is(c, 0, 0, FNIX_RGB(100, 100, 100))); + + /* Alfa 0 no dibuja; alfa 255 reemplaza. */ + fnix_clear(FNIX_BLACK); + fnix_draw_rect(0, 0, 10, 10, FNIX_RGBA(255, 255, 255, 0)); + fnix_draw_circle(10, 10, 5, FNIX_TRANSPARENT); + fnix_draw_line(0, 0, 30, 30, FNIX_TRANSPARENT); + CHECK(count_drawn(c) == 0); + fnix_canvas_destroy(c); +} + +static void test_lines(void) +{ + FnixCanvas *c = new_canvas(40, 30); + + /* Horizontal, vertical y diagonal exactas. */ + fnix_draw_line(2, 3, 9, 3, FNIX_WHITE); + CHECK(count_drawn(c) == 8 && pixel_is(c, 2, 3, FNIX_WHITE) && pixel_is(c, 9, 3, FNIX_WHITE)); + + fnix_clear(FNIX_BLACK); + fnix_draw_line(5, 20, 5, 4, FNIX_WHITE); /* hacia arriba */ + CHECK(count_drawn(c) == 17 && pixel_is(c, 5, 20, FNIX_WHITE) && pixel_is(c, 5, 4, FNIX_WHITE)); + + fnix_clear(FNIX_BLACK); + fnix_draw_line(0, 0, 7, 7, FNIX_WHITE); + CHECK(count_drawn(c) == 8); + bool diagonal = true; + for (int i = 0; i < 8; i++) { + diagonal = diagonal && pixel_is(c, i, i, FNIX_WHITE); + } + CHECK(diagonal); + + /* Posición exacta de cada píxel: y = redondeo(3*x/10), empate hacia arriba. + * Valores calculados con aritmética exacta: 0 0 1 1 1 2 2 2 2 3 3. */ + { + const int expected_minor[11] = {0, 0, 1, 1, 1, 2, 2, 2, 2, 3, 3}; + fnix_clear(FNIX_BLACK); + fnix_draw_line(0, 0, 10, 3, FNIX_WHITE); + CHECK(count_drawn(c) == 11); + bool exact = true; + for (int i = 0; i <= 10; i++) { + exact = exact && pixel_is(c, i, expected_minor[i], FNIX_WHITE); + } + CHECK(exact); + + /* Lo mismo con los ejes intercambiados (línea pronunciada). */ + fnix_clear(FNIX_BLACK); + fnix_draw_line(0, 0, 3, 10, FNIX_WHITE); + CHECK(count_drawn(c) == 11); + exact = true; + for (int i = 0; i <= 10; i++) { + exact = exact && pixel_is(c, expected_minor[i], i, FNIX_WHITE); + } + CHECK(exact); + } + + /* Punto. */ + fnix_clear(FNIX_BLACK); + fnix_draw_line(11, 12, 11, 12, FNIX_WHITE); + CHECK(count_drawn(c) == 1 && pixel_is(c, 11, 12, FNIX_WHITE)); + + /* Pendientes suaves y pronunciadas: un píxel por paso del eje mayor, con + * ambos extremos, y la línea inversa también (otro sentido, mismos pasos). */ + const int segments[][4] = {{2, 2, 30, 9}, {5, 1, 9, 28}, {38, 28, 1, 2}, {3, 25, 35, 3}}; + for (size_t k = 0; k < sizeof segments / sizeof segments[0]; k++) { + const int *s = segments[k]; + const int adx = s[2] > s[0] ? s[2] - s[0] : s[0] - s[2]; + const int ady = s[3] > s[1] ? s[3] - s[1] : s[1] - s[3]; + const int expected = (adx > ady ? adx : ady) + 1; + + fnix_clear(FNIX_BLACK); + fnix_draw_line(s[0], s[1], s[2], s[3], FNIX_WHITE); + CHECK(count_drawn(c) == expected); + CHECK(pixel_is(c, s[0], s[1], FNIX_WHITE) && pixel_is(c, s[2], s[3], FNIX_WHITE)); + + fnix_clear(FNIX_BLACK); + fnix_draw_line(s[2], s[3], s[0], s[1], FNIX_WHITE); + CHECK(count_drawn(c) == expected); + CHECK(pixel_is(c, s[0], s[1], FNIX_WHITE) && pixel_is(c, s[2], s[3], FNIX_WHITE)); + } + fnix_canvas_destroy(c); + + /* Líneas que atraviesan el lienzo desde muy lejos: se recortan sin recorrer + * los millones de pasos exteriores. */ + c = new_canvas(16, 16); + fnix_draw_line(-100000, 4, 100000, 4, FNIX_WHITE); + CHECK(count_drawn(c) == 16); + + fnix_clear(FNIX_BLACK); + fnix_draw_line(-1000, -1000, 1000, 1000, FNIX_WHITE); + CHECK(count_drawn(c) == 16); + bool diagonal16 = true; + for (int i = 0; i < 16; i++) { + diagonal16 = diagonal16 && pixel_is(c, i, i, FNIX_WHITE); + } + CHECK(diagonal16); + + fnix_clear(FNIX_BLACK); + fnix_draw_line(3, -50, 3, 50, FNIX_WHITE); + CHECK(count_drawn(c) == 16); + + fnix_clear(FNIX_BLACK); + fnix_draw_line(-100000, -50000, 100000, 50000, FNIX_WHITE); /* pendiente 1/2 */ + CHECK(count_drawn(c) == 16); + CHECK(pixel_is(c, 0, 0, FNIX_WHITE) && pixel_is(c, 10, 5, FNIX_WHITE)); + + /* Totalmente fuera del lienzo. */ + fnix_clear(FNIX_BLACK); + fnix_draw_line(-50, -50, -10, 90, FNIX_WHITE); + fnix_draw_line(30, 0, 90, 20, FNIX_WHITE); + CHECK(count_drawn(c) == 0); + fnix_canvas_destroy(c); +} + +static void test_range_and_no_target(void) +{ + FnixCanvas *c = new_canvas(16, 16); + + /* Valores fuera de [-2^28, 2^28]: no dibuja y registra el error. */ + fnix_clear_error(); + fnix_draw_line(1 << 29, 0, 0, 0, FNIX_WHITE); + CHECK(fnix_last_error_code() == FNIX_ERROR_INVALID_ARGUMENT); + fnix_clear_error(); + fnix_draw_rect(0, 0, 1 << 29, 5, FNIX_WHITE); + CHECK(fnix_last_error_code() == FNIX_ERROR_INVALID_ARGUMENT); + fnix_clear_error(); + fnix_draw_circle(0, 0, 2000000000, FNIX_WHITE); + CHECK(fnix_last_error_code() == FNIX_ERROR_INVALID_ARGUMENT); + fnix_clear_error(); + fnix_draw_circle_outline(-2000000000, 0, 5, FNIX_WHITE); + CHECK(fnix_last_error_code() == FNIX_ERROR_INVALID_ARGUMENT); + fnix_clear_error(); + fnix_draw_rect_outline(0, 0, 5, -(1 << 29), FNIX_WHITE); /* alto <= 0: no es error */ + CHECK(count_drawn(c) == 0); + + /* Sin lienzo de destino: no hace nada y lo informa. */ + fnix_set_target(NULL); + fnix_clear_error(); + fnix_clear(FNIX_RED); + CHECK(fnix_last_error_code() == FNIX_ERROR_INVALID_ARGUMENT); + fnix_clear_error(); + fnix_draw_pixel(1, 1, FNIX_RED); + CHECK(fnix_last_error_code() == FNIX_ERROR_INVALID_ARGUMENT); + fnix_draw_line(0, 0, 5, 5, FNIX_RED); + fnix_draw_rect(0, 0, 5, 5, FNIX_RED); + fnix_draw_rect_outline(0, 0, 5, 5, FNIX_RED); + fnix_draw_circle(5, 5, 3, FNIX_RED); + fnix_draw_circle_outline(5, 5, 3, FNIX_RED); + CHECK(count_drawn(c) == 0); + fnix_canvas_destroy(c); +} + +int main(void) +{ + test_clear(); + test_pixel(); + test_rect(); + test_rect_outline(); + test_circles_match_reference(); + test_huge_circles(); + test_alpha_blends_once(); + test_lines(); + test_range_and_no_target(); + return CHECK_REPORT("test_draw2d"); +} diff --git a/tests/test_input.c b/tests/test_input.c new file mode 100644 index 0000000..fc8d38b --- /dev/null +++ b/tests/test_input.c @@ -0,0 +1,238 @@ +/* + * Pruebas sin pantalla de la lógica de entrada: máquina de estados de + * teclado/ratón, cola de eventos y consultas públicas. Usa cabeceras internas. + */ +#include "check.h" + +#include + +#include "core/window_internal.h" +#include "input/input_internal.h" + +#include + +static FnixEvent key_event(FnixEventType type, FnixKey key) +{ + FnixEvent e; + memset(&e, 0, sizeof e); + e.type = type; + e.key = key; + return e; +} + +static FnixEvent mouse_event(FnixEventType type, FnixMouseButton button, int x, int y) +{ + FnixEvent e; + memset(&e, 0, sizeof e); + e.type = type; + e.button = button; + e.x = x; + e.y = y; + return e; +} + +static void test_key_lifecycle(void) +{ + FnixInputState s; + memset(&s, 0, sizeof s); + + FnixEvent down = key_event(FNIX_EVENT_KEY_DOWN, FNIX_KEY_A); + fnix_input_apply_event(&s, &down); + CHECK(s.key_down[FNIX_KEY_A] && s.key_pressed[FNIX_KEY_A]); + CHECK(!down.repeat); + + /* Autorepeat: el sistema repite KEY_DOWN sin KEY_UP. */ + FnixEvent again = key_event(FNIX_EVENT_KEY_DOWN, FNIX_KEY_A); + fnix_input_apply_event(&s, &again); + CHECK(again.repeat); + CHECK(s.key_down[FNIX_KEY_A]); + + fnix_input_begin_frame(&s); + CHECK(s.key_down[FNIX_KEY_A]); + CHECK(!s.key_pressed[FNIX_KEY_A]); + + /* Una repetición en un fotograma posterior tampoco vuelve a "pressed". */ + FnixEvent repeat2 = key_event(FNIX_EVENT_KEY_DOWN, FNIX_KEY_A); + fnix_input_apply_event(&s, &repeat2); + CHECK(!s.key_pressed[FNIX_KEY_A]); + + FnixEvent up = key_event(FNIX_EVENT_KEY_UP, FNIX_KEY_A); + fnix_input_apply_event(&s, &up); + CHECK(!s.key_down[FNIX_KEY_A] && s.key_released[FNIX_KEY_A]); + + fnix_input_begin_frame(&s); + CHECK(!s.key_released[FNIX_KEY_A]); + + /* Pulsar y soltar dentro del mismo fotograma no se pierde. */ + FnixEvent quick_down = key_event(FNIX_EVENT_KEY_DOWN, FNIX_KEY_B); + FnixEvent quick_up = key_event(FNIX_EVENT_KEY_UP, FNIX_KEY_B); + fnix_input_apply_event(&s, &quick_down); + fnix_input_apply_event(&s, &quick_up); + CHECK(s.key_pressed[FNIX_KEY_B] && s.key_released[FNIX_KEY_B]); + CHECK(!s.key_down[FNIX_KEY_B]); + + /* Soltar una tecla que no estaba pulsada no genera "released". */ + FnixEvent stray_up = key_event(FNIX_EVENT_KEY_UP, FNIX_KEY_C); + fnix_input_apply_event(&s, &stray_up); + CHECK(!s.key_released[FNIX_KEY_C]); + + /* Teclas inválidas se ignoran sin escribir fuera del arreglo. */ + FnixEvent bad1 = key_event(FNIX_EVENT_KEY_DOWN, FNIX_KEY_UNKNOWN); + FnixEvent bad2 = key_event(FNIX_EVENT_KEY_DOWN, FNIX_KEY_COUNT); + FnixEvent bad3 = key_event(FNIX_EVENT_KEY_DOWN, (FnixKey)-5); + fnix_input_apply_event(&s, &bad1); + fnix_input_apply_event(&s, &bad2); + fnix_input_apply_event(&s, &bad3); +} + +static void test_mouse(void) +{ + FnixInputState s; + memset(&s, 0, sizeof s); + + FnixEvent move = mouse_event(FNIX_EVENT_MOUSE_MOVE, FNIX_MOUSE_LEFT, 12, 34); + fnix_input_apply_event(&s, &move); + CHECK(s.mouse_x == 12 && s.mouse_y == 34); + + FnixEvent down = mouse_event(FNIX_EVENT_MOUSE_BUTTON_DOWN, FNIX_MOUSE_RIGHT, 50, 60); + fnix_input_apply_event(&s, &down); + CHECK(s.mouse_down[FNIX_MOUSE_RIGHT] && s.mouse_pressed[FNIX_MOUSE_RIGHT]); + CHECK(!s.mouse_down[FNIX_MOUSE_LEFT]); + CHECK(s.mouse_x == 50 && s.mouse_y == 60); + + fnix_input_begin_frame(&s); + CHECK(s.mouse_down[FNIX_MOUSE_RIGHT] && !s.mouse_pressed[FNIX_MOUSE_RIGHT]); + + FnixEvent up = mouse_event(FNIX_EVENT_MOUSE_BUTTON_UP, FNIX_MOUSE_RIGHT, 51, 61); + fnix_input_apply_event(&s, &up); + CHECK(!s.mouse_down[FNIX_MOUSE_RIGHT] && s.mouse_released[FNIX_MOUSE_RIGHT]); + + /* Rueda: se acumula dentro del fotograma y se borra al empezar el siguiente. */ + fnix_input_begin_frame(&s); + FnixEvent w1 = mouse_event(FNIX_EVENT_MOUSE_WHEEL, FNIX_MOUSE_LEFT, 0, 0); + FnixEvent w2 = w1; + FnixEvent w3 = w1; + w1.wheel = 1; + w2.wheel = 1; + w3.wheel = -1; + fnix_input_apply_event(&s, &w1); + fnix_input_apply_event(&s, &w2); + fnix_input_apply_event(&s, &w3); + CHECK(s.wheel == 1); + fnix_input_begin_frame(&s); + CHECK(s.wheel == 0); + + /* Botón inválido: no escribe fuera del arreglo. */ + FnixEvent bad = mouse_event(FNIX_EVENT_MOUSE_BUTTON_DOWN, (FnixMouseButton)99, 0, 0); + fnix_input_apply_event(&s, &bad); +} + +static void test_focus_lost(void) +{ + FnixInputState s; + memset(&s, 0, sizeof s); + + FnixEvent k = key_event(FNIX_EVENT_KEY_DOWN, FNIX_KEY_W); + FnixEvent m = mouse_event(FNIX_EVENT_MOUSE_BUTTON_DOWN, FNIX_MOUSE_LEFT, 1, 1); + fnix_input_apply_event(&s, &k); + fnix_input_apply_event(&s, &m); + fnix_input_begin_frame(&s); + CHECK(s.key_down[FNIX_KEY_W] && s.mouse_down[FNIX_MOUSE_LEFT]); + + FnixEvent lost; + memset(&lost, 0, sizeof lost); + lost.type = FNIX_EVENT_FOCUS_LOST; + fnix_input_apply_event(&s, &lost); + CHECK(!s.key_down[FNIX_KEY_W] && s.key_released[FNIX_KEY_W]); + CHECK(!s.mouse_down[FNIX_MOUSE_LEFT] && s.mouse_released[FNIX_MOUSE_LEFT]); +} + +static void test_event_queue(void) +{ + static FnixEventQueue q; /* es grande: fuera de la pila */ + memset(&q, 0, sizeof q); + FnixEvent out; + + CHECK(!fnix_event_queue_pop(&q, &out)); + + for (int i = 0; i < 5; i++) { + FnixEvent e = mouse_event(FNIX_EVENT_MOUSE_MOVE, FNIX_MOUSE_LEFT, i, 0); + fnix_event_queue_push(&q, &e); + } + for (int i = 0; i < 5; i++) { + CHECK(fnix_event_queue_pop(&q, &out) && out.x == i); + } + CHECK(!fnix_event_queue_pop(&q, &out)); + + /* Desbordamiento: se conservan los FNIX_EVENT_QUEUE_CAPACITY más recientes. */ + const int total = FNIX_EVENT_QUEUE_CAPACITY + 44; + for (int i = 0; i < total; i++) { + FnixEvent e = mouse_event(FNIX_EVENT_MOUSE_MOVE, FNIX_MOUSE_LEFT, i, 0); + fnix_event_queue_push(&q, &e); + } + CHECK(q.count == FNIX_EVENT_QUEUE_CAPACITY); + for (int i = total - FNIX_EVENT_QUEUE_CAPACITY; i < total; i++) { + CHECK(fnix_event_queue_pop(&q, &out) && out.x == i); + } + CHECK(!fnix_event_queue_pop(&q, &out)); +} + +static void test_public_queries(void) +{ + /* Una ventana "falsa" (sin plataforma) basta para probar la API pública. */ + static FnixWindow w; + memset(&w, 0, sizeof w); + + FnixEvent down = key_event(FNIX_EVENT_KEY_DOWN, FNIX_KEY_SPACE); + fnix_window_dispatch_event(&w, &down); + CHECK(fnix_key_down(&w, FNIX_KEY_SPACE)); + CHECK(fnix_key_pressed(&w, FNIX_KEY_SPACE)); + CHECK(!fnix_key_released(&w, FNIX_KEY_SPACE)); + CHECK(!fnix_key_down(&w, FNIX_KEY_A)); + + FnixEvent click = mouse_event(FNIX_EVENT_MOUSE_BUTTON_DOWN, FNIX_MOUSE_MIDDLE, 7, 9); + fnix_window_dispatch_event(&w, &click); + CHECK(fnix_mouse_down(&w, FNIX_MOUSE_MIDDLE)); + CHECK(fnix_mouse_pressed(&w, FNIX_MOUSE_MIDDLE)); + CHECK(fnix_mouse_x(&w) == 7 && fnix_mouse_y(&w) == 9); + + /* Los eventos quedaron en la cola, en orden. */ + FnixEvent e; + CHECK(fnix_window_poll_event(&w, &e) && e.type == FNIX_EVENT_KEY_DOWN && + e.key == FNIX_KEY_SPACE); + CHECK(fnix_window_poll_event(&w, &e) && e.type == FNIX_EVENT_MOUSE_BUTTON_DOWN); + CHECK(!fnix_window_poll_event(&w, &e)); + CHECK(!fnix_window_poll_event(&w, NULL)); + + /* El cierre marca la ventana. */ + CHECK(!fnix_window_should_close(&w)); + FnixEvent close_event; + memset(&close_event, 0, sizeof close_event); + close_event.type = FNIX_EVENT_WINDOW_CLOSE; + fnix_window_dispatch_event(&w, &close_event); + CHECK(fnix_window_should_close(&w)); + + /* Argumentos inválidos: nunca fallan, devuelven falso/0. */ + CHECK(!fnix_key_down(NULL, FNIX_KEY_A)); + CHECK(!fnix_key_pressed(NULL, FNIX_KEY_A)); + CHECK(!fnix_key_released(NULL, FNIX_KEY_A)); + CHECK(!fnix_key_down(&w, FNIX_KEY_UNKNOWN)); + CHECK(!fnix_key_down(&w, FNIX_KEY_COUNT)); + CHECK(!fnix_key_down(&w, (FnixKey)-1)); + CHECK(!fnix_mouse_down(NULL, FNIX_MOUSE_LEFT)); + CHECK(!fnix_mouse_down(&w, FNIX_MOUSE_BUTTON_COUNT)); + CHECK(!fnix_mouse_pressed(&w, (FnixMouseButton)-1)); + CHECK(!fnix_mouse_released(NULL, FNIX_MOUSE_LEFT)); + CHECK(fnix_mouse_x(NULL) == 0 && fnix_mouse_y(NULL) == 0); + CHECK(fnix_mouse_wheel(NULL) == 0); +} + +int main(void) +{ + test_key_lifecycle(); + test_mouse(); + test_focus_lost(); + test_event_queue(); + test_public_queries(); + return CHECK_REPORT("test_input"); +} diff --git a/tests/test_window_x11.c b/tests/test_window_x11.c new file mode 100644 index 0000000..33b640f --- /dev/null +++ b/tests/test_window_x11.c @@ -0,0 +1,330 @@ +/* + * Prueba de integración contra un servidor X11 real (por ejemplo Xvfb). + * + * Una segunda conexión al servidor actúa de "observador": localiza la ventana + * de FnixLib, le inyecta eventos con XSendEvent y lee de vuelta con XGetImage + * los píxeles que realmente llegaron a la ventana. + * + * Si DISPLAY no está definido, la prueba se omite (código de salida 77). + */ +#include "check.h" + +#include + +#include +#include +#include + +#include +#include + +#define TITLE "FnixLib test window" +#define WIDTH 320 +#define HEIGHT 240 + +/* Procesa eventos hasta que `cond` sea verdadera (máximo ~1 s). */ +#define PUMP_UNTIL(window, cond, ok) \ + do { \ + (ok) = false; \ + for (int attempt_ = 0; attempt_ < 200 && !(ok); attempt_++) { \ + fnix_begin_frame(window); \ + if (cond) { \ + (ok) = true; \ + } else { \ + fnix_time_sleep(0.005); \ + } \ + } \ + } while (0) + +static Window find_window(Display *dpy, Window parent, const char *name) +{ + char *fetched = NULL; + if (XFetchName(dpy, parent, &fetched) && fetched != NULL) { + const bool match = strcmp(fetched, name) == 0; + XFree(fetched); + if (match) { + return parent; + } + } + + Window root_return, parent_return, *children = NULL; + unsigned count = 0; + if (!XQueryTree(dpy, parent, &root_return, &parent_return, &children, &count)) { + return 0; + } + Window found = 0; + for (unsigned i = 0; i < count && found == 0; i++) { + found = find_window(dpy, children[i], name); + } + if (children != NULL) { + XFree(children); + } + return found; +} + +static Window wait_for_window(Display *dpy, const char *name) +{ + for (int i = 0; i < 200; i++) { + const Window w = find_window(dpy, DefaultRootWindow(dpy), name); + if (w != 0) { + return w; + } + fnix_time_sleep(0.005); + } + return 0; +} + +static bool wait_viewable(Display *dpy, Window w) +{ + for (int i = 0; i < 200; i++) { + XWindowAttributes attrs; + if (XGetWindowAttributes(dpy, w, &attrs) && attrs.map_state == IsViewable) { + return true; + } + fnix_time_sleep(0.005); + } + return false; +} + +/* Lee un píxel de la ventana tal como lo tiene el servidor. */ +static unsigned long server_pixel(Display *dpy, Window w, int x, int y) +{ + XImage *image = XGetImage(dpy, w, x, y, 1, 1, AllPlanes, ZPixmap); + if (image == NULL) { + return ~0UL; + } + const unsigned long p = XGetPixel(image, 0, 0) & 0xFFFFFFUL; + XDestroyImage(image); + return p; +} + +/* Espera (hasta ~1 s) a que el píxel del servidor tenga el valor esperado. */ +static bool server_pixel_becomes(Display *dpy, Window w, int x, int y, unsigned long expected) +{ + for (int i = 0; i < 200; i++) { + if (server_pixel(dpy, w, x, y) == expected) { + return true; + } + fnix_time_sleep(0.005); + } + return false; +} + +static void send_key(Display *obs, Window win, KeySym keysym, bool press) +{ + XEvent ev; + memset(&ev, 0, sizeof ev); + ev.xkey.type = press ? KeyPress : KeyRelease; + ev.xkey.display = obs; + ev.xkey.window = win; + ev.xkey.root = DefaultRootWindow(obs); + ev.xkey.same_screen = True; + ev.xkey.keycode = XKeysymToKeycode(obs, keysym); + XSendEvent(obs, win, False, press ? KeyPressMask : KeyReleaseMask, &ev); + XFlush(obs); +} + +static void send_motion(Display *obs, Window win, int x, int y) +{ + XEvent ev; + memset(&ev, 0, sizeof ev); + ev.xmotion.type = MotionNotify; + ev.xmotion.display = obs; + ev.xmotion.window = win; + ev.xmotion.root = DefaultRootWindow(obs); + ev.xmotion.same_screen = True; + ev.xmotion.x = x; + ev.xmotion.y = y; + ev.xmotion.is_hint = NotifyNormal; + XSendEvent(obs, win, False, PointerMotionMask, &ev); + XFlush(obs); +} + +static void send_button(Display *obs, Window win, unsigned button, bool press, int x, int y) +{ + XEvent ev; + memset(&ev, 0, sizeof ev); + ev.xbutton.type = press ? ButtonPress : ButtonRelease; + ev.xbutton.display = obs; + ev.xbutton.window = win; + ev.xbutton.root = DefaultRootWindow(obs); + ev.xbutton.same_screen = True; + ev.xbutton.button = button; + ev.xbutton.x = x; + ev.xbutton.y = y; + XSendEvent(obs, win, False, press ? ButtonPressMask : ButtonReleaseMask, &ev); + XFlush(obs); +} + +static void send_close_request(Display *obs, Window win) +{ + XEvent ev; + memset(&ev, 0, sizeof ev); + ev.xclient.type = ClientMessage; + ev.xclient.display = obs; + ev.xclient.window = win; + ev.xclient.message_type = XInternAtom(obs, "WM_PROTOCOLS", False); + ev.xclient.format = 32; + ev.xclient.data.l[0] = (long)XInternAtom(obs, "WM_DELETE_WINDOW", False); + ev.xclient.data.l[1] = CurrentTime; + XSendEvent(obs, win, False, NoEventMask, &ev); + XFlush(obs); +} + +int main(void) +{ + if (getenv("DISPLAY") == NULL) { + fprintf(stderr, "OMITIDA: DISPLAY no está definido (usa Xvfb o una sesión X11)\n"); + return 77; + } + + FnixWindow *window = fnix_window_create(TITLE, WIDTH, HEIGHT); + if (window == NULL) { + fprintf(stderr, "no se pudo crear la ventana: %s\n", fnix_last_error()); + return 1; + } + CHECK(fnix_window_width(window) == WIDTH && fnix_window_height(window) == HEIGHT); + CHECK(fnix_canvas_width(fnix_window_canvas(window)) == WIDTH); + CHECK(!fnix_window_should_close(window)); + + Display *obs = XOpenDisplay(NULL); + CHECK(obs != NULL); + if (obs == NULL) { + fnix_window_destroy(window); + return CHECK_REPORT("test_window_x11"); + } + Window win = wait_for_window(obs, TITLE); + CHECK(win != 0); + if (win == 0) { + XCloseDisplay(obs); + fnix_window_destroy(window); + return CHECK_REPORT("test_window_x11"); + } + CHECK(wait_viewable(obs, win)); + + /* --- Fotogramas, delta time y límite de FPS ------------------------- */ + fnix_window_set_target_fps(window, 50); /* periodo ideal: 20 ms */ + double dt_sum = 0.0; + int dt_count = 0; + for (int frame = 0; frame < 16; frame++) { + fnix_begin_frame(window); + if (frame == 0) { + CHECK(fnix_window_delta_time(window) == 0.0); + } else { + dt_sum += fnix_window_delta_time(window); + dt_count++; + } + CHECK(fnix_get_target() == fnix_window_canvas(window)); + + fnix_clear(FNIX_BLACK); + fnix_draw_circle(160, 120, 50, FNIX_RED); + fnix_draw_rect(10, 10, 40, 30, FNIX_BLUE); + fnix_draw_circle_outline(250, 180, 30, FNIX_GREEN); + fnix_end_frame(window); + } + const double average_dt = dt_sum / dt_count; + printf("delta time medio con límite de 50 FPS: %.4f s (ideal 0.0200)\n", average_dt); + CHECK(average_dt > 0.017 && average_dt < 0.023); + + /* --- Lo dibujado llegó de verdad a la ventana ----------------------- */ + CHECK(server_pixel_becomes(obs, win, 160, 120, 0xFF0000UL)); /* centro del círculo */ + CHECK(server_pixel_becomes(obs, win, 20, 20, 0x0000FFUL)); /* rectángulo azul */ + CHECK(server_pixel_becomes(obs, win, 5, 230, 0x000000UL)); /* fondo */ + CHECK(server_pixel_becomes(obs, win, 160, 180, 0x000000UL)); /* fuera del círculo */ + CHECK(server_pixel_becomes(obs, win, 250 + 30, 180, 0x00FF00UL)); /* contorno verde */ + CHECK(server_pixel_becomes(obs, win, 250, 180, 0x000000UL)); /* interior del anillo */ + + /* --- Teclado --------------------------------------------------------- */ + bool ok; + CHECK(XKeysymToKeycode(obs, XK_a) != 0); + send_key(obs, win, XK_a, true); + PUMP_UNTIL(window, fnix_key_pressed(window, FNIX_KEY_A), ok); + CHECK(ok); + CHECK(fnix_key_down(window, FNIX_KEY_A)); + fnix_begin_frame(window); + CHECK(fnix_key_down(window, FNIX_KEY_A) && !fnix_key_pressed(window, FNIX_KEY_A)); + + send_key(obs, win, XK_a, false); + PUMP_UNTIL(window, fnix_key_released(window, FNIX_KEY_A), ok); + CHECK(ok); + CHECK(!fnix_key_down(window, FNIX_KEY_A)); + + send_key(obs, win, XK_Escape, true); + PUMP_UNTIL(window, fnix_key_pressed(window, FNIX_KEY_ESCAPE), ok); + CHECK(ok); + send_key(obs, win, XK_Escape, false); + PUMP_UNTIL(window, fnix_key_released(window, FNIX_KEY_ESCAPE), ok); + CHECK(ok); + + /* --- Ratón ----------------------------------------------------------- */ + send_motion(obs, win, 100, 50); + PUMP_UNTIL(window, fnix_mouse_x(window) == 100 && fnix_mouse_y(window) == 50, ok); + CHECK(ok); + + send_button(obs, win, Button1, true, 101, 51); + PUMP_UNTIL(window, fnix_mouse_pressed(window, FNIX_MOUSE_LEFT), ok); + CHECK(ok); + CHECK(fnix_mouse_down(window, FNIX_MOUSE_LEFT)); + CHECK(fnix_mouse_x(window) == 101 && fnix_mouse_y(window) == 51); + send_button(obs, win, Button1, false, 101, 51); + PUMP_UNTIL(window, fnix_mouse_released(window, FNIX_MOUSE_LEFT), ok); + CHECK(ok); + CHECK(!fnix_mouse_down(window, FNIX_MOUSE_LEFT)); + + send_button(obs, win, Button3, true, 5, 5); + PUMP_UNTIL(window, fnix_mouse_pressed(window, FNIX_MOUSE_RIGHT), ok); + CHECK(ok); + send_button(obs, win, Button3, false, 5, 5); + PUMP_UNTIL(window, fnix_mouse_released(window, FNIX_MOUSE_RIGHT), ok); + CHECK(ok); + + send_button(obs, win, Button4, true, 9, 9); /* rueda arriba */ + PUMP_UNTIL(window, fnix_mouse_wheel(window) == 1, ok); + CHECK(ok); + send_button(obs, win, Button5, true, 9, 9); /* rueda abajo */ + PUMP_UNTIL(window, fnix_mouse_wheel(window) == -1, ok); + CHECK(ok); + + /* --- Cola de eventos --------------------------------------------------- */ + int counts[FNIX_EVENT_MOUSE_WHEEL + 1]; + memset(counts, 0, sizeof counts); + bool key_a_down_seen = false; + FnixEvent event; + while (fnix_window_poll_event(window, &event)) { + counts[event.type]++; + if (event.type == FNIX_EVENT_KEY_DOWN && event.key == FNIX_KEY_A && !event.repeat) { + key_a_down_seen = true; + } + } + CHECK(key_a_down_seen); + CHECK(counts[FNIX_EVENT_KEY_DOWN] == 2); /* A y Escape */ + CHECK(counts[FNIX_EVENT_KEY_UP] == 2); + CHECK(counts[FNIX_EVENT_MOUSE_BUTTON_DOWN] == 2); /* izquierdo y derecho */ + CHECK(counts[FNIX_EVENT_MOUSE_BUTTON_UP] == 2); + CHECK(counts[FNIX_EVENT_MOUSE_MOVE] >= 1); + CHECK(counts[FNIX_EVENT_MOUSE_WHEEL] == 2); + CHECK(counts[FNIX_EVENT_WINDOW_CLOSE] == 0); + + /* --- Cierre de ventana pedido por el usuario / gestor de ventanas ---- */ + CHECK(!fnix_window_should_close(window)); + send_close_request(obs, win); + PUMP_UNTIL(window, fnix_window_should_close(window), ok); + CHECK(ok); + CHECK(fnix_window_poll_event(window, &event) && event.type == FNIX_EVENT_WINDOW_CLOSE); + + /* --- Destrucción: la ventana desaparece del servidor ------------------ */ + fnix_window_destroy(window); + XSync(obs, False); + bool gone = false; + for (int i = 0; i < 200 && !gone; i++) { + gone = find_window(obs, DefaultRootWindow(obs), TITLE) == 0; + if (!gone) { + fnix_time_sleep(0.005); + } + } + CHECK(gone); + CHECK(fnix_get_target() == NULL); + + XCloseDisplay(obs); + return CHECK_REPORT("test_window_x11"); +}