Add initial FnixLib development implementation
This commit is contained in:
commit
84742ae38e
33 files changed
+3340
No files matched your search
@@ -0,0 +1,8 @@
|
||||
build/
|
||||
build-*/
|
||||
cmake-build-*/
|
||||
.cache/
|
||||
compile_commands.json
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
@@ -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 $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
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()
|
||||
@@ -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 <fnix/fnix.h>
|
||||
#include <stdio.h>
|
||||
|
||||
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.
|
||||
@@ -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.
|
||||
@@ -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 <fnix/fnix.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* FnixLib - tipos públicos comunes.
|
||||
*/
|
||||
#ifndef FNIX_TYPES_H
|
||||
#define FNIX_TYPES_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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 */
|
||||
@@ -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 */
|
||||
@@ -0,0 +1,51 @@
|
||||
#include "fnix/errors.h"
|
||||
|
||||
#include "core/internal.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
|
||||
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';
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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 <stdlib.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -0,0 +1,85 @@
|
||||
#include "fnix/canvas.h"
|
||||
|
||||
#include "core/internal.h"
|
||||
#include "graphics/canvas_internal.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
#include "fnix/input.h"
|
||||
|
||||
#include "core/window_internal.h"
|
||||
#include "fnix/events.h"
|
||||
#include "input/input_internal.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -0,0 +1,44 @@
|
||||
#include "platform/linux/x11_keys.h"
|
||||
|
||||
#include <X11/keysym.h>
|
||||
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
@@ -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 <X11/Xlib.h>
|
||||
|
||||
/* 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 */
|
||||
@@ -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 <X11/XKBlib.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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 <errno.h>
|
||||
#include <time.h>
|
||||
|
||||
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) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/* Mini-arnés de pruebas: sin dependencias externas. */
|
||||
#ifndef FNIX_TEST_CHECK_H
|
||||
#define FNIX_TEST_CHECK_H
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
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 */
|
||||
@@ -0,0 +1,134 @@
|
||||
/* Pruebas sin pantalla: errores, tiempo y validación de argumentos. */
|
||||
#include "check.h"
|
||||
|
||||
#include <fnix/fnix.h>
|
||||
|
||||
#include <string.h>
|
||||
|
||||
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");
|
||||
}
|
||||
@@ -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 <fnix/fnix.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
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");
|
||||
}
|
||||
@@ -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 <fnix/fnix.h>
|
||||
|
||||
#include "core/window_internal.h"
|
||||
#include "input/input_internal.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
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");
|
||||
}
|
||||
@@ -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 <fnix/fnix.h>
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/keysym.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#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");
|
||||
}
|
||||
Reference in new issue
Block a user