5 ex_gui_md_8c example
gullradriel edited this page 2026-10-09 00:51:57 +02:00

ex_gui_md.c

n_gui_md regression: measure equals draw, every block kind lays out, a link can be found, a narrower column is a taller one, and the N_GUI_TYPE_MARKDOWNVIEW widget over it scrolls and follows its theme. Needs a display, because it measures real glyphs, so it is not in run_tests.sh with the headless examples; run it under Xvfb.

xvfb-run -a ./examples/ex_gui_md

With -o FILE it also writes the rendered page out, which is how the layout gets looked at rather than only counted. ```c

/*

  • 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 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;

}


_Filename: ex_gui_md.c_