7 ex__gui__md_8c
gullradriel edited this page 2026-10-29 20:54:40 +01:00

examples/ex_gui_md.c

Functions

Name
int main(int argc, char ** argv)
ALLEGRO_FONT * face(const char * name, int px)
Load a face, trying the usual places; NULL when there is none, which the renderer is documented to survive.

Attributes

Name
int failures

Defines

Name
CHECK(cond, msg)

Functions Documentation

function main

int main(
    int argc,
    char ** argv
)

function face

static ALLEGRO_FONT * face(
    const char * name,
    int px
)

Load a face, trying the usual places; NULL when there is none, which the renderer is documented to survive.

Attributes Documentation

variable failures

static int failures = 0;

Macros Documentation

define CHECK

#define CHECK(
    cond,
    msg
)
do {                                       \
    if (!(cond)) {                         \
        n_log(LOG_ERR, "FAIL: %s", (msg)); \
        failures++;                        \
    }                                      \
} while (0)

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
 */


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

#include <allegro5/allegro_font.h>
#include <allegro5/allegro_image.h>
#include <allegro5/allegro_primitives.h>
#include <allegro5/allegro_ttf.h>

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

static int failures = 0;

#define CHECK(cond, msg)                       \
    do {                                       \
        if (!(cond)) {                         \
            n_log(LOG_ERR, "FAIL: %s", (msg)); \
            failures++;                        \
        }                                      \
    } while (0)

static const char* SAMPLE =
    "# A heading\n"
    "\n"
    "A paragraph with **bold**, *italic*, `code` and a\n"
    "[link](https://example.com/page) in it, hard wrapped by whoever wrote it\n"
    "and re-wrapped by the renderer.\n"
    "\n"
    "## A smaller heading\n"
    "\n"
    "> a quoted line\n"
    "\n"
    "```\n"
    "a fenced code block\n"
    "keeping its own line breaks\n"
    "```\n"
    "\n"
    "- a list item\n"
    "- one that carries on\n"
    "  onto a second line\n"
    "\n"
    "| Key | Effect |\n"
    "|:----|-------:|\n"
    "| `G` | back to the grid |\n"
    "| Escape | close a dialog, otherwise go back to the grid |\n";

static ALLEGRO_FONT* face(const char* name, int px) {
    static const char* const dirs[] = {
        "/usr/share/fonts/truetype/dejavu/",
        "/usr/share/fonts/TTF/",
        "/usr/share/fonts/dejavu/",
        NULL};
    char path[512];
    int i;
    for (i = 0; dirs[i]; i++) {
        ALLEGRO_FONT* f;
        (void)snprintf(path, sizeof(path), "%s%s", dirs[i], name);
        f = al_load_ttf_font(path, px, 0);
        if (f)
            return f;
    }
    return NULL;
}

int main(int argc, char** argv) {
    ALLEGRO_DISPLAY* display;
    N_GUI_MD_FONTS fonts;
    N_GUI_MD_STYLE style;
    ALLEGRO_COLOR bg = al_map_rgb(33, 37, 46);
    ALLEGRO_COLOR fg = al_map_rgb(220, 223, 228);
    const char* out_path = NULL;
    float h_wide, h_narrow, drawn;
    int i;

    set_log_level(LOG_NOTICE);
    for (i = 1; i < argc; i++)
        if (strcmp(argv[i], "-o") == 0 && i + 1 < argc)
            out_path = argv[++i];

    if (!al_init()) {
        n_log(LOG_ERR, "cannot initialise Allegro");
        return 1;
    }
    al_init_font_addon();
    al_init_ttf_addon();
    (void)al_init_image_addon();
    if (!al_init_primitives_addon()) {
        n_log(LOG_ERR, "cannot initialise the primitives addon");
        return 1;
    }
    display = al_create_display(900, 600);
    if (!display) {
        n_log(LOG_ERR, "cannot create a display; run this under Xvfb");
        return 1;
    }

    memset(&fonts, 0, sizeof(fonts));
    fonts.regular = face("DejaVuSans.ttf", 14);
    fonts.bold = face("DejaVuSans-Bold.ttf", 14);
    fonts.italic = face("DejaVuSans-Oblique.ttf", 14);
    fonts.bolditalic = face("DejaVuSans-BoldOblique.ttf", 14);
    fonts.mono = face("DejaVuSansMono.ttf", 14);
    fonts.h1 = face("DejaVuSans-Bold.ttf", 21);
    fonts.h2 = face("DejaVuSans-Bold.ttf", 17);
    fonts.h3 = face("DejaVuSans-Bold.ttf", 15);
    if (!fonts.regular) {
        /* No TTF anywhere: the builtin bitmap font still proves the layout,
           and every other face staying NULL exercises the fallbacks. */
        fonts.regular = al_create_builtin_font();
        n_log(LOG_NOTICE, "no TTF found, using the builtin font");
    }
    CHECK(fonts.regular != NULL, "a regular face");
    if (!fonts.regular)
        return 1;

    n_gui_md_style_default(&style, fg, bg, (float)al_get_font_line_height(fonts.regular));
    CHECK(style.parse_flags == N_MARKDOWN_ALL, "the default style asks for every block kind");
    CHECK(style.hard_breaks == 0, "the default style reflows paragraphs");

    /* the whole point of the module's shape: one walk, two entry points */
    h_wide = n_gui_md_height(&fonts, &style, 760.0f, SAMPLE);
    CHECK(h_wide > 0.0f, "a height for the sample");
    al_clear_to_color(bg);
    drawn = n_gui_md_draw(&fonts, &style, 20.0f, 20.0f, 760.0f, SAMPLE);
    CHECK(drawn == h_wide, "draw returns exactly what measure said");

    /* a narrower column wraps more, so it is taller; a measure is independent
       of where it would be drawn */
    h_narrow = n_gui_md_height(&fonts, &style, 300.0f, SAMPLE);
    CHECK(h_narrow > h_wide, "a narrower column is taller");

    /* the link is found where it was drawn, and nowhere else */
    {
        char url[256];
        int found = 0, y;
        /* walk down the left half of the laid-out page looking for it: the
           exact line it lands on depends on the font, and the point of the
           check is that the hit test agrees with the draw at all */
        for (y = 20; y < 20 + (int)h_wide && !found; y++) {
            int x;
            for (x = 20; x < 780 && !found; x += 4)
                found = n_gui_md_link_at(&fonts, &style, 20.0f, 20.0f, 760.0f, SAMPLE,
                                         (float)x, (float)y, url, sizeof(url));
        }
        CHECK(found, "the link is somewhere in the laid-out page");
        if (found)
            CHECK(strcmp(url, "https://example.com/page") == 0, "and it is the right url");
        url[0] = 'x';
        CHECK(!n_gui_md_link_at(&fonts, &style, 20.0f, 20.0f, 760.0f, SAMPLE,
                                -100.0f, -100.0f, url, sizeof(url)),
              "no link outside the page");
        CHECK(url[0] == '\0', "a miss clears the url rather than leaving it stale");
    }

    /* the awkward inputs */
    {
        CHECK(n_gui_md_height(&fonts, &style, 300.0f, NULL) == 0.0f, "NULL text has no height");
        CHECK(n_gui_md_height(&fonts, &style, 300.0f, "") == 0.0f, "empty text has no height");
        CHECK(n_gui_md_height(NULL, &style, 300.0f, SAMPLE) == 0.0f, "no fonts, no height");
        CHECK(n_gui_md_height(&fonts, NULL, 300.0f, SAMPLE) == 0.0f, "no style, no height");
        /* a width of zero or less must terminate, not spin: every flow clamps
           to one pixel, so a glyph always fits somewhere */
        CHECK(n_gui_md_height(&fonts, &style, 0.0f, SAMPLE) > 0.0f, "a zero width still lays out");
        CHECK(n_gui_md_height(&fonts, &style, -10.0f, SAMPLE) > 0.0f, "a negative width too");
    }

    /* hard_breaks is what a chat message wants, and it makes a difference */
    {
        N_GUI_MD_STYLE chat = style;
        float soft, hard;
        chat.parse_flags = 0;
        soft = n_gui_md_height(&fonts, &chat, 760.0f, "one\ntwo\nthree\n");
        chat.hard_breaks = 1;
        hard = n_gui_md_height(&fonts, &chat, 760.0f, "one\ntwo\nthree\n");
        CHECK(hard > soft, "hard breaks keep the three lines the source has");
    }

    /* ---- the widget over it ---------------------------------------------- */
    {
        N_GUI_CTX* gui = n_gui_new_ctx(fonts.regular);
        CHECK(gui != NULL, "a gui context");
        if (gui) {
            int win = n_gui_add_window(gui, "Doc", 0, 0, 400, 300);
            int mv = n_gui_add_markdownview(gui, win, 10, 10, 360, 240, &fonts);
            N_GUI_MD_STYLE* live;
            CHECK(mv >= 0, "a markdownview");
            CHECK(n_gui_markdownview_get_text(gui, mv) != NULL,
                  "the text starts empty rather than NULL");
            n_gui_markdownview_set_text(gui, mv, SAMPLE);
            CHECK(strcmp(n_gui_markdownview_get_text(gui, mv), SAMPLE) == 0,
                  "set_text round-trips");
            /* the height is not known until it has been laid out once, which
               is the draw: that is the contract, and it is why the bar cannot
               disagree with the page */
            CHECK(n_gui_markdownview_content_height(gui, mv) == 0.0f,
                  "no content height before the first draw");
            al_clear_to_color(bg);
            n_gui_draw(gui);
            CHECK(n_gui_markdownview_content_height(gui, mv) > 240.0f,
                  "the sample overflows a 240px pane, so there is a bar");
            /* the scroll clamps to what there is, in both directions */
            n_gui_markdownview_scroll_to(gui, mv, 1000000.0f);
            n_gui_draw(gui);
            CHECK(n_gui_markdownview_get_scroll(gui, mv) <=
                      n_gui_markdownview_content_height(gui, mv),
                  "a scroll past the end is clamped");
            n_gui_markdownview_scroll_to(gui, mv, -50.0f);
            CHECK(n_gui_markdownview_get_scroll(gui, mv) == 0.0f, "and so is one before it");
            /* asking for the style hands over the colours for good */
            live = n_gui_markdownview_style(gui, mv);
            CHECK(live != NULL, "the live style");
            if (live) {
                live->link = al_map_rgb(255, 0, 0);
                n_gui_draw(gui);
                CHECK(live->link.r > 0.9f, "a colour set by the caller survives a draw");
            }
            n_gui_markdownview_set_text(gui, mv, NULL);
            CHECK(strcmp(n_gui_markdownview_get_text(gui, mv), "") == 0,
                  "NULL text clears rather than crashes");
            /* the awkward ids. A window id is not tested against: window and
               widget ids are separate spaces, so one can legitimately name a
               widget. A widget of another type is the real guard. */
            CHECK(n_gui_markdownview_style(gui, 12345) == NULL, "no such widget, no style");
            {
                int other = n_gui_add_label(gui, win, "not a view", 10, 260, 100, 20,
                                            N_GUI_ALIGN_LEFT);
                CHECK(other >= 0, "a label to test the type guard with");
                CHECK(n_gui_markdownview_style(gui, other) == NULL,
                      "another widget type is not a markdownview");
                CHECK(n_gui_markdownview_get_text(gui, other) == NULL,
                      "and has no markdown text");
            }
            n_gui_destroy_ctx(&gui);
            CHECK(gui == NULL, "the context is nulled on destroy");
        }
    }

    if (out_path) {
        ALLEGRO_BITMAP* page = al_create_bitmap(800, (int)h_wide + 40);
        if (page) {
            al_set_target_bitmap(page);
            al_clear_to_color(bg);
            (void)n_gui_md_draw(&fonts, &style, 20.0f, 20.0f, 760.0f, SAMPLE);
            if (al_save_bitmap(out_path, page))
                n_log(LOG_NOTICE, "wrote %s", out_path);
            al_set_target_backbuffer(display);
            al_destroy_bitmap(page);
        }
    }

    /* The caller owns the faces: n_gui_md never took them. */
    al_destroy_font(fonts.regular);
    al_destroy_font(fonts.bold);
    al_destroy_font(fonts.italic);
    al_destroy_font(fonts.bolditalic);
    al_destroy_font(fonts.mono);
    al_destroy_font(fonts.h1);
    al_destroy_font(fonts.h2);
    al_destroy_font(fonts.h3);
    al_destroy_display(display);

    if (failures) {
        n_log(LOG_ERR, "ex_gui_md: %d failure(s)", failures);
        return 1;
    }
    n_log(LOG_NOTICE, "ex_gui_md: all checks passed");
    return 0;
}