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

examples/ex_gui_suggest.c

Functions

Name
void vocabulary(const char * prefix, N_GUI_SUGGEST_SINK * sink, void * user_data)
A vocabulary to complete against, the way a catalog would answer.
void type(N_GUI_CTX * gui, const char * s)
Type a whole string into whatever has focus.
void nothing(const char * prefix, N_GUI_SUGGEST_SINK * sink, void * user_data)
A provider that answers nothing, whatever it is asked.
int main(void )
ALLEGRO_EVENT key_event(int keycode)
Build a key event for keycode.
void expect_true(const char * label, int cond)
Assert a condition, logging which check failed.
void expect_text(const char * label, const char * got, const char * want)
Assert two strings are equal.
ALLEGRO_EVENT char_event(char c)
Build a key event that types c.

Attributes

Name
char[64] last_prefix
The last prefix the provider was given.
int failures
Count of failed assertions.
int asks
How many times the provider was asked.

Defines

Name
ALLEGRO_UNSTABLE

Functions Documentation

function vocabulary

static void vocabulary(
    const char * prefix,
    N_GUI_SUGGEST_SINK * sink,
    void * user_data
)

A vocabulary to complete against, the way a catalog would answer.

Parameters:

  • prefix What has been typed of the word under the cursor.
  • sink Where the answers go.
  • user_data Unused.

Case folded for the match and not for the answer, which is the rule a store of tags has to follow: offering a spelling that cannot be added is worse than offering nothing.

function type

static void type(
    N_GUI_CTX * gui,
    const char * s
)

Type a whole string into whatever has focus.

Parameters:

  • gui The context.
  • s The text.

function nothing

static void nothing(
    const char * prefix,
    N_GUI_SUGGEST_SINK * sink,
    void * user_data
)

A provider that answers nothing, whatever it is asked.

Parameters:

  • prefix Unused.
  • sink Unused.
  • user_data Unused.

function main

int main(
    void 
)

function key_event

static ALLEGRO_EVENT key_event(
    int keycode
)

Build a key event for keycode.

Parameters:

  • keycode An ALLEGRO_KEY_* code.

Return: The event, ready for n_gui_process_event.

function expect_true

static void expect_true(
    const char * label,
    int cond
)

Assert a condition, logging which check failed.

Parameters:

  • label What is being checked.
  • cond Non-zero to pass.

function expect_text

static void expect_text(
    const char * label,
    const char * got,
    const char * want
)

Assert two strings are equal.

Parameters:

  • label What is being checked.
  • got The value under test.
  • want The expected value.

function char_event

static ALLEGRO_EVENT char_event(
    char c
)

Build a key event that types c.

Parameters:

  • c The character.

Return: The event.

Attributes Documentation

variable last_prefix

static char[64] last_prefix = "";

The last prefix the provider was given.

variable failures

static int failures = 0;

Count of failed assertions.

variable asks

static int asks = 0;

How many times the provider was asked.

Macros Documentation

define ALLEGRO_UNSTABLE

#define ALLEGRO_UNSTABLE 1

Source code

/*
 * Nilorea Library
 * Copyright (C) 2005-2026 Castagnier Mickael
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 */


#define ALLEGRO_UNSTABLE 1

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

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

static int failures = 0;

static int asks = 0;

static char last_prefix[64] = "";

static void expect_true(const char* label, int cond) {
    if (!cond) {
        n_log(LOG_ERR, "%s: FAILED", label);
        failures++;
    } else {
        n_log(LOG_INFO, "%s: OK", label);
    }
}

static void expect_text(const char* label, const char* got, const char* want) {
    if (!got || strcmp(got, want) != 0) {
        n_log(LOG_ERR, "%s: FAILED, got \"%s\" wanted \"%s\"", label, got ? got : "(null)", want);
        failures++;
    } else {
        n_log(LOG_INFO, "%s: OK", label);
    }
}

static void vocabulary(const char* prefix, N_GUI_SUGGEST_SINK* sink, void* user_data) {
    static const char* words[] = {"sunset", "sunsets", "sunrise", "sunflower",
                                  "beach", "berlin", "bergen", NULL};
    size_t n = strlen(prefix);
    int i = 0;

    (void)user_data;
    asks++;
    snprintf(last_prefix, sizeof(last_prefix), "%s", prefix);
    for (i = 0; words[i]; i++) {
        if (strncasecmp(words[i], prefix, n) == 0) n_gui_suggest_add(sink, words[i]);
    }
}

static void nothing(const char* prefix, N_GUI_SUGGEST_SINK* sink, void* user_data) {
    (void)prefix;
    (void)sink;
    (void)user_data;
}

static ALLEGRO_EVENT key_event(int keycode) {
    ALLEGRO_EVENT ev;
    memset(&ev, 0, sizeof(ev));
    ev.type = ALLEGRO_EVENT_KEY_CHAR;
    ev.keyboard.keycode = keycode;
    return ev;
}

static ALLEGRO_EVENT char_event(char c) {
    ALLEGRO_EVENT ev = key_event(0);
    ev.keyboard.unichar = (int)(unsigned char)c;
    return ev;
}

static void type(N_GUI_CTX* gui, const char* s) {
    while (*s) {
        n_gui_process_event(gui, char_event(*s));
        s++;
    }
}

int main(void) {
    ALLEGRO_FONT* font;
    N_GUI_CTX* gui;
    int win, field, plain;

    set_log_level(LOG_ERR);
    if (!al_init() || !al_init_font_addon()) {
        n_log(LOG_ERR, "allegro init failed");
        return 1;
    }
    font = al_create_builtin_font(); /* no display needed */
    if (!font) {
        n_log(LOG_ERR, "al_create_builtin_font failed");
        return 1;
    }
    gui = n_gui_new_ctx(font);
    if (!gui) {
        n_log(LOG_ERR, "n_gui_new_ctx failed");
        al_destroy_font(font);
        return 1;
    }
    n_gui_set_display_size(gui, 800.0f, 600.0f);
    win = n_gui_add_window(gui, "W", 0.0f, 0.0f, 400.0f, 300.0f);
    n_gui_window_set_flags(gui, win, N_GUI_WIN_FRAMELESS);

    field = n_gui_add_textarea(gui, win, 0.0f, 0.0f, 300.0f, 20.0f, 0, 200, NULL, NULL);
    plain = n_gui_add_textarea(gui, win, 0.0f, 40.0f, 300.0f, 20.0f, 0, 200, NULL, NULL);
    n_gui_open_window(gui, win);

    /* A field with no provider is a field that behaves exactly as it did. */
    n_gui_set_focus(gui, plain);
    type(gui, "sun");
    n_gui_process_event(gui, key_event(ALLEGRO_KEY_DOWN));
    expect_true("a field with no provider offers nothing",
                !n_gui_textarea_suggest_is_open(gui, plain));

    n_gui_textarea_set_suggest(gui, field, vocabulary, ",", NULL);
    n_gui_set_focus(gui, field);

    /* Typing opens it as soon as something matches. */
    type(gui, "sun");
    expect_true("typing opens the panel", n_gui_textarea_suggest_is_open(gui, field));
    expect_text("and the provider was given what was typed", last_prefix, "sun");

    /* Asked when the word under the cursor changes, and not when a key leaves
       it as it was. The cursor is at the end, so RIGHT moves nothing and asks
       nothing; LEFT shortens the word to "su" and asks once. */
    {
        int before = asks;

        n_gui_process_event(gui, key_event(ALLEGRO_KEY_RIGHT));
        expect_true("a key that does not change the word does not ask again",
                    asks == before);
        n_gui_process_event(gui, key_event(ALLEGRO_KEY_LEFT));
        expect_true("and one that does, asks once", asks == before + 1);
        expect_text("with the shorter word", last_prefix, "su");
        n_gui_process_event(gui, key_event(ALLEGRO_KEY_END));
    }

    /* ENTER takes the highlighted one, which starts on the first. */
    n_gui_process_event(gui, key_event(ALLEGRO_KEY_ENTER));
    expect_text("ENTER puts the first one in", n_gui_textarea_get_text(gui, field), "sunset");
    expect_true("and closes the panel", !n_gui_textarea_suggest_is_open(gui, field));

    /* Down moves the highlight, so the second entry is reachable. */
    n_gui_textarea_set_text(gui, field, "");
    type(gui, "sun");
    n_gui_process_event(gui, key_event(ALLEGRO_KEY_DOWN));
    n_gui_process_event(gui, key_event(ALLEGRO_KEY_ENTER));
    expect_text("Down then ENTER takes the second", n_gui_textarea_get_text(gui, field),
                "sunsets");

    /* TAB does what ENTER does, because a completion is what TAB means in a
       field that has one. */
    n_gui_textarea_set_text(gui, field, "");
    type(gui, "sunf");
    n_gui_process_event(gui, key_event(ALLEGRO_KEY_TAB));
    expect_text("TAB takes it too", n_gui_textarea_get_text(gui, field), "sunflower");

    /* ESCAPE dismisses without changing the text, and Down brings it back
       without the word having to be retyped. */
    n_gui_textarea_set_text(gui, field, "");
    type(gui, "sun");
    n_gui_process_event(gui, key_event(ALLEGRO_KEY_ESCAPE));
    expect_true("ESCAPE puts it away", !n_gui_textarea_suggest_is_open(gui, field));
    expect_text("and leaves the text alone", n_gui_textarea_get_text(gui, field), "sun");
    n_gui_process_event(gui, key_event(ALLEGRO_KEY_DOWN));
    expect_true("Down asks for it again", n_gui_textarea_suggest_is_open(gui, field));

    /* A word nothing matches closes it rather than showing an empty panel. */
    n_gui_textarea_set_text(gui, field, "");
    type(gui, "zzz");
    expect_true("a word nothing matches shows nothing",
                !n_gui_textarea_suggest_is_open(gui, field));

    /* The separator is the whole point of the feature in a list field: the
       word under the cursor is completed and the rest of the line stays. */
    n_gui_textarea_set_text(gui, field, "");
    type(gui, "beach, sunr");
    expect_text("the provider sees only the word under the cursor", last_prefix, "sunr");
    n_gui_process_event(gui, key_event(ALLEGRO_KEY_ENTER));
    expect_text("and only that word is replaced", n_gui_textarea_get_text(gui, field),
                "beach, sunrise");

    /* Nothing is added after it. A field that appended its own separator
       would be guessing that another entry is coming. */
    expect_true("and nothing was added after it",
                n_gui_textarea_get_text_length(gui, field) == strlen("beach, sunrise"));

    /* Completing in the middle of a line replaces that word and not the tail. */
    n_gui_textarea_set_text(gui, field, "");
    type(gui, "berg, beach");
    n_gui_process_event(gui, key_event(ALLEGRO_KEY_HOME));
    n_gui_process_event(gui, key_event(ALLEGRO_KEY_RIGHT));
    n_gui_process_event(gui, key_event(ALLEGRO_KEY_RIGHT));
    n_gui_process_event(gui, key_event(ALLEGRO_KEY_RIGHT));
    n_gui_process_event(gui, key_event(ALLEGRO_KEY_RIGHT));
    expect_true("the panel followed the cursor back into the first word",
                n_gui_textarea_suggest_is_open(gui, field));
    n_gui_process_event(gui, key_event(ALLEGRO_KEY_ENTER));
    expect_text("the first word is completed and the tail is kept",
                n_gui_textarea_get_text(gui, field), "bergen, beach");

    /* A provider that answers nothing is not an error, it is a field with no
       completions today. */
    n_gui_textarea_set_suggest(gui, field, nothing, ",", NULL);
    n_gui_textarea_set_text(gui, field, "");
    type(gui, "sun");
    expect_true("a provider that answers nothing shows nothing",
                !n_gui_textarea_suggest_is_open(gui, field));

    /* And setting NULL takes the feature off the field again. */
    n_gui_textarea_set_suggest(gui, field, vocabulary, ",", NULL);
    n_gui_textarea_set_text(gui, field, "");
    type(gui, "sun");
    expect_true("it is on again", n_gui_textarea_suggest_is_open(gui, field));
    n_gui_textarea_set_suggest(gui, field, NULL, NULL, NULL);
    expect_true("setting NULL closes what was up",
                !n_gui_textarea_suggest_is_open(gui, field));
    type(gui, "s");
    expect_true("and stops offering", !n_gui_textarea_suggest_is_open(gui, field));

    /* Duplicates are refused, so a provider that walks two sources does not
       show the same word twice. */
    n_gui_textarea_set_suggest(gui, field, vocabulary, ",", NULL);
    n_gui_textarea_set_text(gui, field, "");
    type(gui, "beach");
    expect_true("an exact match still offers itself",
                n_gui_textarea_suggest_is_open(gui, field));
    n_gui_process_event(gui, key_event(ALLEGRO_KEY_ENTER));
    expect_text("and picking it changes nothing", n_gui_textarea_get_text(gui, field), "beach");

    /* Asking a widget that is not a textarea is answered rather than obeyed. */
    {
        int label = n_gui_add_label(gui, win, "x", 0.0f, 80.0f, 50.0f, 20.0f,
                                    N_GUI_ALIGN_LEFT);

        expect_true("a non-textarea has no panel", !n_gui_textarea_suggest_is_open(gui, label));
        n_gui_textarea_set_suggest(gui, label, vocabulary, ",", NULL);
        expect_true("and setting one on it does nothing",
                    !n_gui_textarea_suggest_is_open(gui, label));
    }
    expect_true("an unknown widget has no panel", !n_gui_textarea_suggest_is_open(gui, 9999));

    n_gui_destroy_ctx(&gui);
    al_destroy_font(font);
    if (failures) {
        n_log(LOG_ERR, "ex_gui_suggest: %d failure(s)", failures);
        return 1;
    }
    n_log(LOG_NOTICE, "ex_gui_suggest: all checks passed");
    return 0;
}