Add initial FnixLib development implementation
This commit is contained in:
commit
84742ae38e
33 files changed
+3340
No files matched your search
@@ -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