6 ex_gui_button_keys_8c example
gullradriel edited this page 2026-10-09 00:55:18 +02:00

ex_gui_button_keys.c

n_gui buttons from the keyboard A dialog has to be answerable without the mouse. Tab and Shift+Tab reach the buttons as well as the fields; on a focused button Enter or Space presses it and the arrows move to the next or previous button of its window in reading order, going round. Another button bound to Enter does not fire while a button has the keyboard, a mouse press elsewhere takes the keyboard back from a button, and n_gui_focus_takes_key() says which keys a focused button keeps, so an application's own shortcuts can leave them alone.

Whatever has the keyboard, a button, a slider or a list, and whether Tab or a click gave it, is drawn with an outline in the theme's active border colour; the last check draws the window into a memory bitmap and reads it.

Headless: the builtin font needs no display, the keys are events handed to n_gui_process_event(), and the drawing goes to a memory bitmap. ```c

#define ALLEGRO_UNSTABLE 1

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h>

#include "nilorea/n_common.h" #include "nilorea/n_log.h" #include "nilorea/n_gui.h"

#include <allegro5/allegro_primitives.h>

static int log_level = LOG_ERR; static int failures = 0; static int pressed[64];

static void expect_true(const char* what, int ok) { if (!ok) { n_log(LOG_ERR, "FAIL: %s", what); failures++; } else { n_log(LOG_INFO, "ok: %s", what); } }

static int process_args(int argc, char** argv) { int opt = 0;

while ((opt = getopt(argc, argv, "V:")) != -1) {
    if (opt == 'V') {
        log_level = atoi(optarg);
    } else {
        return -1;
    }
}
return 0;

}

static void on_click(int widget_id, void* user_data) { (void)user_data; if (widget_id >= 0 && widget_id < 64) pressed[widget_id]++; }

static void key(N_GUI_CTX* gui, int keycode, int modifiers, int repeat) { ALLEGRO_EVENT ev;

memset(&ev, 0, sizeof(ev));
ev.keyboard.keycode = keycode;
ev.keyboard.modifiers = (unsigned)modifiers;
if (!repeat) {
    ev.type = ALLEGRO_EVENT_KEY_DOWN;
    n_gui_process_event(gui, ev);
}
ev.type = ALLEGRO_EVENT_KEY_CHAR;
ev.keyboard.repeat = repeat ? true : false;
n_gui_process_event(gui, ev);

}

int main(int argc, char** argv) { set_log_level(LOG_ERR); if (process_args(argc, argv) != 0) { fprintf(stderr, "Usage: %s [-V LOG_LEVEL]\n", argv[0]); return 2; } set_log_level(log_level);

if (!al_init() || !al_init_font_addon() || !al_init_primitives_addon()) {
    n_log(LOG_ERR, "allegro init failed");
    return 1;
}
ALLEGRO_FONT* font = al_create_builtin_font();
if (!font) return 1;
N_GUI_CTX* gui = n_gui_new_ctx(font);
if (!gui) {
    al_destroy_font(font);
    return 1;
}
n_gui_set_display_size(gui, 800.0f, 600.0f);
int win = n_gui_add_window(gui, "Question", 10, 10, 400, 200);
int field = n_gui_add_textarea(gui, win, 10, 10, 300, 24, 0, 64, NULL, NULL);
/* added out of order on purpose: the arrows follow the screen, not the list */
int go = n_gui_add_button(gui, win, "Delete", 250, 50, 80, 24, N_GUI_SHAPE_RECT, on_click, NULL);
int other = n_gui_add_button(gui, win, "Other", 10, 50, 80, 24, N_GUI_SHAPE_RECT, on_click, NULL);
int cancel = n_gui_add_button(gui, win, "Cancel", 130, 51, 80, 24, N_GUI_SHAPE_RECT, on_click, NULL);
int below = n_gui_add_button(gui, win, "Help", 10, 90, 80, 24, N_GUI_SHAPE_RECT, on_click, NULL);
int hidden = n_gui_add_button(gui, win, "Hidden", 10, 130, 80, 24, N_GUI_SHAPE_RECT, on_click, NULL);

if (field < 0 || go < 0 || other < 0 || cancel < 0 || below < 0 || hidden < 0 || below >= 64) {
    n_log(LOG_ERR, "cannot build the window");
    return 1;
}
n_gui_set_widget_visible(gui, hidden, 0);
/* Help answers Enter globally, as a default button would. */
n_gui_button_set_keycode(gui, below, ALLEGRO_KEY_ENTER, 0);

/* Tab reaches the field, then the buttons, in the window's order. */
key(gui, ALLEGRO_KEY_TAB, 0, 0);
expect_true("Tab focuses the field first", n_gui_get_focus(gui) == field);
key(gui, ALLEGRO_KEY_TAB, 0, 0);
expect_true("Tab goes on to a button", n_gui_get_focus(gui) == go);
key(gui, ALLEGRO_KEY_TAB, ALLEGRO_KEYMOD_SHIFT, 0);
expect_true("Shift+Tab comes back to the field", n_gui_get_focus(gui) == field);

/* The arrows, in reading order: Other, Cancel (a pixel lower, same row),
 * Delete, then Help on the next row, then round; the hidden one never. */
n_gui_set_focus(gui, other);
key(gui, ALLEGRO_KEY_RIGHT, 0, 0);
expect_true("Right: Other to Cancel", n_gui_get_focus(gui) == cancel);
key(gui, ALLEGRO_KEY_RIGHT, 0, 0);
expect_true("Right: Cancel to Delete", n_gui_get_focus(gui) == go);
key(gui, ALLEGRO_KEY_DOWN, 0, 0);
expect_true("Down: Delete to Help, on the next row", n_gui_get_focus(gui) == below);
key(gui, ALLEGRO_KEY_RIGHT, 0, 0);
expect_true("Right from the last goes round to the first", n_gui_get_focus(gui) == other);
key(gui, ALLEGRO_KEY_LEFT, 0, 0);
expect_true("Left from the first goes round to the last", n_gui_get_focus(gui) == below);
key(gui, ALLEGRO_KEY_UP, 0, 0);
expect_true("Up: Help back to Delete", n_gui_get_focus(gui) == go);

/* Enter and Space press the focused button, and only it. */
key(gui, ALLEGRO_KEY_ENTER, 0, 0);
expect_true("Enter presses the focused button", pressed[go] == 1);
expect_true("and not the one bound to Enter", pressed[below] == 0);
key(gui, ALLEGRO_KEY_SPACE, 0, 0);
expect_true("Space presses it too", pressed[go] == 2);
key(gui, ALLEGRO_KEY_ENTER, 0, 1);
expect_true("a held key's repeats are not more presses", pressed[go] == 2);
key(gui, ALLEGRO_KEY_PAD_ENTER, 0, 0);
expect_true("the keypad's Enter presses it", pressed[go] == 3);

expect_true("a focused button keeps Enter", n_gui_focus_takes_key(gui, ALLEGRO_KEY_ENTER));
expect_true("and the arrows", n_gui_focus_takes_key(gui, ALLEGRO_KEY_LEFT));
expect_true("but not a letter", !n_gui_focus_takes_key(gui, ALLEGRO_KEY_A));

/* With nothing focused the binding answers Enter again. */
n_gui_set_focus(gui, -1);
key(gui, ALLEGRO_KEY_ENTER, 0, 0);
expect_true("with no focus, Enter is the bound button's", pressed[below] == 1 && pressed[go] == 3);

/* A mouse press on another button takes the keyboard back, as a press
 * anywhere else does: a button takes no focus from a press, and the
 * outline must not stay on a button nobody is using. */
{
    float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
    ALLEGRO_EVENT ev;
    int before = pressed[other];

    n_gui_set_focus(gui, go);
    if (n_gui_widget_screen_rect(gui, other, &x, &y, &w, &h)) {
        memset(&ev, 0, sizeof(ev));
        ev.type = ALLEGRO_EVENT_MOUSE_AXES;
        ev.mouse.x = (int)(x + w * 0.5f);
        ev.mouse.y = (int)(y + h * 0.5f);
        n_gui_process_event(gui, ev);
        ev.type = ALLEGRO_EVENT_MOUSE_BUTTON_DOWN;
        ev.mouse.button = 1;
        n_gui_process_event(gui, ev);
        ev.type = ALLEGRO_EVENT_MOUSE_BUTTON_UP;
        n_gui_process_event(gui, ev);
    }
    expect_true("the clicked button is pressed", pressed[other] == before + 1);
    expect_true("and the keyboard leaves the button it was on", n_gui_get_focus(gui) == -1);
}

/* The focus drawn: a slider reached by Tab, and a button, show an
 * outline in the active border colour just inside their edge; without
 * the focus that pixel is something else. */
{
    ALLEGRO_COLOR mark = al_map_rgb(255, 0, 255);
    N_GUI_THEME theme = n_gui_make_theme(al_map_rgb(40, 40, 40), al_map_rgb(50, 50, 50), al_map_rgb(60, 60, 60),
                                         al_map_rgb(90, 90, 90), al_map_rgb(100, 100, 100), mark,
                                         al_map_rgb(220, 220, 220), al_map_rgb(230, 230, 230),
                                         al_map_rgb(240, 240, 240), 1.0f, 0.0f, 0.0f);
    int win2 = n_gui_add_window(gui, "Focus", 420, 10, 300, 160);
    int slider = 0, button = 0;
    ALLEGRO_BITMAP* target = NULL;
    int old = al_get_new_bitmap_flags();
    float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
    unsigned char r = 0, g = 0, b = 0;

    n_gui_set_default_theme(gui, theme);
    slider = n_gui_add_slider(gui, win2, 10, 10, 200, 24, 0.0, 100.0, 50.0, N_GUI_SLIDER_VALUE, NULL, NULL);
    button = n_gui_add_button(gui, win2, "Go", 10, 50, 80, 24, N_GUI_SHAPE_RECT, on_click, NULL);
    al_set_new_bitmap_flags(ALLEGRO_MEMORY_BITMAP);
    target = al_create_bitmap(800, 600);
    al_set_new_bitmap_flags(old);
    if (!target || slider < 0 || button < 0) {
        expect_true("a bitmap to draw into", 0);
    } else {
        int s_on = 0, s_off = 0, b_on = 0;

        n_gui_raise_window(gui, win2);
        al_set_target_bitmap(target);
        /* not focused */
        n_gui_set_focus(gui, -1);
        al_clear_to_color(al_map_rgb(0, 0, 0));
        n_gui_draw(gui);
        n_gui_widget_screen_rect(gui, slider, &x, &y, &w, &h);
        al_unmap_rgb(al_get_pixel(target, (int)x + 1, (int)(y + h * 0.5f)), &r, &g, &b);
        s_off = (r == 255 && g == 0 && b == 255);
        /* the slider, by Tab from the window's start */
        n_gui_set_focus(gui, slider);
        al_clear_to_color(al_map_rgb(0, 0, 0));
        n_gui_draw(gui);
        al_unmap_rgb(al_get_pixel(target, (int)x + 1, (int)(y + h * 0.5f)), &r, &g, &b);
        s_on = (r == 255 && g == 0 && b == 255);
        n_gui_set_focus(gui, button);
        al_clear_to_color(al_map_rgb(0, 0, 0));
        n_gui_draw(gui);
        n_gui_widget_screen_rect(gui, button, &x, &y, &w, &h);
        al_unmap_rgb(al_get_pixel(target, (int)x + 1, (int)(y + h * 0.5f)), &r, &g, &b);
        b_on = (r == 255 && g == 0 && b == 255);
        al_set_target_bitmap(NULL);
        expect_true("a slider without the focus has no outline", !s_off);
        expect_true("a focused slider is outlined in the active border colour", s_on);
        expect_true("and so is a focused button", b_on);
    }
    if (target) al_destroy_bitmap(target);
}

n_gui_destroy_ctx(&gui);
al_destroy_font(font);
n_log(LOG_NOTICE, "ex_gui_button_keys: %s", failures ? "FAILED" : "ok");
return failures ? 1 : 0;

}


_Filename: ex_gui_button_keys.c_