Files
FnixLib/tests/test_window_x11.c
T

331 lines
11 KiB
C

/*
* 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");
}