Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_gui_md.c
Go to the documentation of this file.
1/*
2 * Nilorea Library
3 * Copyright (C) 2005-2026 Castagnier Mickael
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 * implied. See the License for the specific language governing
15 * permissions and limitations under the License.
16 *
17 * SPDX-License-Identifier: Apache-2.0
18 */
19
36
37#include "nilorea/n_gui.h"
38#include "nilorea/n_gui_md.h"
39#include "nilorea/n_log.h"
40
41#include <allegro5/allegro_font.h>
42#include <allegro5/allegro_image.h>
43#include <allegro5/allegro_primitives.h>
44#include <allegro5/allegro_ttf.h>
45
46#include <stdio.h>
47#include <string.h>
48
49static int failures = 0;
50
51#define CHECK(cond, msg) \
52 do { \
53 if (!(cond)) { \
54 n_log(LOG_ERR, "FAIL: %s", (msg)); \
55 failures++; \
56 } \
57 } while (0)
58
60static const char* SAMPLE =
61 "# A heading\n"
62 "\n"
63 "A paragraph with **bold**, *italic*, `code` and a\n"
64 "[link](https://example.com/page) in it, hard wrapped by whoever wrote it\n"
65 "and re-wrapped by the renderer.\n"
66 "\n"
67 "## A smaller heading\n"
68 "\n"
69 "> a quoted line\n"
70 "\n"
71 "```\n"
72 "a fenced code block\n"
73 "keeping its own line breaks\n"
74 "```\n"
75 "\n"
76 "- a list item\n"
77 "- one that carries on\n"
78 " onto a second line\n"
79 "\n"
80 "| Key | Effect |\n"
81 "|:----|-------:|\n"
82 "| `G` | back to the grid |\n"
83 "| Escape | close a dialog, otherwise go back to the grid |\n";
84
87static ALLEGRO_FONT* face(const char* name, int px) {
88 static const char* const dirs[] = {
89 "/usr/share/fonts/truetype/dejavu/",
90 "/usr/share/fonts/TTF/",
91 "/usr/share/fonts/dejavu/",
92 NULL};
93 char path[512];
94 int i;
95 for (i = 0; dirs[i]; i++) {
96 ALLEGRO_FONT* f;
97 (void)snprintf(path, sizeof(path), "%s%s", dirs[i], name);
98 f = al_load_ttf_font(path, px, 0);
99 if (f)
100 return f;
101 }
102 return NULL;
103}
104
105int main(int argc, char** argv) {
106 ALLEGRO_DISPLAY* display;
107 N_GUI_MD_FONTS fonts;
108 N_GUI_MD_STYLE style;
109 ALLEGRO_COLOR bg = al_map_rgb(33, 37, 46);
110 ALLEGRO_COLOR fg = al_map_rgb(220, 223, 228);
111 const char* out_path = NULL;
112 float h_wide, h_narrow, drawn;
113 int i;
114
116 for (i = 1; i < argc; i++)
117 if (strcmp(argv[i], "-o") == 0 && i + 1 < argc)
118 out_path = argv[++i];
119
120 if (!al_init()) {
121 n_log(LOG_ERR, "cannot initialise Allegro");
122 return 1;
123 }
124 al_init_font_addon();
125 al_init_ttf_addon();
126 (void)al_init_image_addon();
127 if (!al_init_primitives_addon()) {
128 n_log(LOG_ERR, "cannot initialise the primitives addon");
129 return 1;
130 }
131 display = al_create_display(900, 600);
132 if (!display) {
133 n_log(LOG_ERR, "cannot create a display; run this under Xvfb");
134 return 1;
135 }
136
137 memset(&fonts, 0, sizeof(fonts));
138 fonts.regular = face("DejaVuSans.ttf", 14);
139 fonts.bold = face("DejaVuSans-Bold.ttf", 14);
140 fonts.italic = face("DejaVuSans-Oblique.ttf", 14);
141 fonts.bolditalic = face("DejaVuSans-BoldOblique.ttf", 14);
142 fonts.mono = face("DejaVuSansMono.ttf", 14);
143 fonts.h1 = face("DejaVuSans-Bold.ttf", 21);
144 fonts.h2 = face("DejaVuSans-Bold.ttf", 17);
145 fonts.h3 = face("DejaVuSans-Bold.ttf", 15);
146 if (!fonts.regular) {
147 /* No TTF anywhere: the builtin bitmap font still proves the layout,
148 and every other face staying NULL exercises the fallbacks. */
149 fonts.regular = al_create_builtin_font();
150 n_log(LOG_NOTICE, "no TTF found, using the builtin font");
151 }
152 CHECK(fonts.regular != NULL, "a regular face");
153 if (!fonts.regular)
154 return 1;
155
156 n_gui_md_style_default(&style, fg, bg, (float)al_get_font_line_height(fonts.regular));
157 CHECK(style.parse_flags == N_MARKDOWN_ALL, "the default style asks for every block kind");
158 CHECK(style.hard_breaks == 0, "the default style reflows paragraphs");
159
160 /* the whole point of the module's shape: one walk, two entry points */
161 h_wide = n_gui_md_height(&fonts, &style, 760.0f, SAMPLE);
162 CHECK(h_wide > 0.0f, "a height for the sample");
163 al_clear_to_color(bg);
164 drawn = n_gui_md_draw(&fonts, &style, 20.0f, 20.0f, 760.0f, SAMPLE);
165 CHECK(drawn == h_wide, "draw returns exactly what measure said");
166
167 /* a narrower column wraps more, so it is taller; a measure is independent
168 of where it would be drawn */
169 h_narrow = n_gui_md_height(&fonts, &style, 300.0f, SAMPLE);
170 CHECK(h_narrow > h_wide, "a narrower column is taller");
171
172 /* the link is found where it was drawn, and nowhere else */
173 {
174 char url[256];
175 int found = 0, y;
176 /* walk down the left half of the laid-out page looking for it: the
177 exact line it lands on depends on the font, and the point of the
178 check is that the hit test agrees with the draw at all */
179 for (y = 20; y < 20 + (int)h_wide && !found; y++) {
180 int x;
181 for (x = 20; x < 780 && !found; x += 4)
182 found = n_gui_md_link_at(&fonts, &style, 20.0f, 20.0f, 760.0f, SAMPLE,
183 (float)x, (float)y, url, sizeof(url));
184 }
185 CHECK(found, "the link is somewhere in the laid-out page");
186 if (found)
187 CHECK(strcmp(url, "https://example.com/page") == 0, "and it is the right url");
188 url[0] = 'x';
189 CHECK(!n_gui_md_link_at(&fonts, &style, 20.0f, 20.0f, 760.0f, SAMPLE,
190 -100.0f, -100.0f, url, sizeof(url)),
191 "no link outside the page");
192 CHECK(url[0] == '\0', "a miss clears the url rather than leaving it stale");
193 }
194
195 /* the awkward inputs */
196 {
197 CHECK(n_gui_md_height(&fonts, &style, 300.0f, NULL) == 0.0f, "NULL text has no height");
198 CHECK(n_gui_md_height(&fonts, &style, 300.0f, "") == 0.0f, "empty text has no height");
199 CHECK(n_gui_md_height(NULL, &style, 300.0f, SAMPLE) == 0.0f, "no fonts, no height");
200 CHECK(n_gui_md_height(&fonts, NULL, 300.0f, SAMPLE) == 0.0f, "no style, no height");
201 /* a width of zero or less must terminate, not spin: every flow clamps
202 to one pixel, so a glyph always fits somewhere */
203 CHECK(n_gui_md_height(&fonts, &style, 0.0f, SAMPLE) > 0.0f, "a zero width still lays out");
204 CHECK(n_gui_md_height(&fonts, &style, -10.0f, SAMPLE) > 0.0f, "a negative width too");
205 }
206
207 /* hard_breaks is what a chat message wants, and it makes a difference */
208 {
209 N_GUI_MD_STYLE chat = style;
210 float soft, hard;
211 chat.parse_flags = 0;
212 soft = n_gui_md_height(&fonts, &chat, 760.0f, "one\ntwo\nthree\n");
213 chat.hard_breaks = 1;
214 hard = n_gui_md_height(&fonts, &chat, 760.0f, "one\ntwo\nthree\n");
215 CHECK(hard > soft, "hard breaks keep the three lines the source has");
216 }
217
218 /* ---- the widget over it ---------------------------------------------- */
219 {
221 CHECK(gui != NULL, "a gui context");
222 if (gui) {
223 int win = n_gui_add_window(gui, "Doc", 0, 0, 400, 300);
224 int mv = n_gui_add_markdownview(gui, win, 10, 10, 360, 240, &fonts);
225 N_GUI_MD_STYLE* live;
226 CHECK(mv >= 0, "a markdownview");
228 "the text starts empty rather than NULL");
230 CHECK(strcmp(n_gui_markdownview_get_text(gui, mv), SAMPLE) == 0,
231 "set_text round-trips");
232 /* the height is not known until it has been laid out once, which
233 is the draw: that is the contract, and it is why the bar cannot
234 disagree with the page */
236 "no content height before the first draw");
237 al_clear_to_color(bg);
240 "the sample overflows a 240px pane, so there is a bar");
241 /* the scroll clamps to what there is, in both directions */
242 n_gui_markdownview_scroll_to(gui, mv, 1000000.0f);
246 "a scroll past the end is clamped");
248 CHECK(n_gui_markdownview_get_scroll(gui, mv) == 0.0f, "and so is one before it");
249 /* asking for the style hands over the colours for good */
250 live = n_gui_markdownview_style(gui, mv);
251 CHECK(live != NULL, "the live style");
252 if (live) {
253 live->link = al_map_rgb(255, 0, 0);
255 CHECK(live->link.r > 0.9f, "a colour set by the caller survives a draw");
256 }
258 CHECK(strcmp(n_gui_markdownview_get_text(gui, mv), "") == 0,
259 "NULL text clears rather than crashes");
260 /* the awkward ids. A window id is not tested against: window and
261 widget ids are separate spaces, so one can legitimately name a
262 widget. A widget of another type is the real guard. */
263 CHECK(n_gui_markdownview_style(gui, 12345) == NULL, "no such widget, no style");
264 {
265 int other = n_gui_add_label(gui, win, "not a view", 10, 260, 100, 20,
267 CHECK(other >= 0, "a label to test the type guard with");
268 CHECK(n_gui_markdownview_style(gui, other) == NULL,
269 "another widget type is not a markdownview");
270 CHECK(n_gui_markdownview_get_text(gui, other) == NULL,
271 "and has no markdown text");
272 }
274 CHECK(gui == NULL, "the context is nulled on destroy");
275 }
276 }
277
278 if (out_path) {
279 ALLEGRO_BITMAP* page = al_create_bitmap(800, (int)h_wide + 40);
280 if (page) {
281 al_set_target_bitmap(page);
282 al_clear_to_color(bg);
283 (void)n_gui_md_draw(&fonts, &style, 20.0f, 20.0f, 760.0f, SAMPLE);
284 if (al_save_bitmap(out_path, page))
285 n_log(LOG_NOTICE, "wrote %s", out_path);
286 al_set_target_backbuffer(display);
287 al_destroy_bitmap(page);
288 }
289 }
290
291 /* The caller owns the faces: n_gui_md never took them. */
292 al_destroy_font(fonts.regular);
293 al_destroy_font(fonts.bold);
294 al_destroy_font(fonts.italic);
295 al_destroy_font(fonts.bolditalic);
296 al_destroy_font(fonts.mono);
297 al_destroy_font(fonts.h1);
298 al_destroy_font(fonts.h2);
299 al_destroy_font(fonts.h3);
300 al_destroy_display(display);
301
302 if (failures) {
303 n_log(LOG_ERR, "ex_gui_md: %d failure(s)", failures);
304 return 1;
305 }
306 n_log(LOG_NOTICE, "ex_gui_md: all checks passed");
307 return 0;
308}
static int failures
Definition ex_atlas.c:51
int main(void)
Definition ex_atlas.c:117
#define CHECK(cond, msg)
ALLEGRO_DISPLAY * display
Definition ex_fluid.c:54
static N_GUI_CTX * gui
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 su...
Definition ex_gui_md.c:87
N_GUI_MD_STYLE * n_gui_markdownview_style(N_GUI_CTX *ctx, int widget_id)
the view's style, to read or to change in place, or NULL if there is no such widget.
Definition n_gui.c:5881
#define N_GUI_ALIGN_LEFT
left aligned text
Definition n_gui.h:212
int n_gui_add_markdownview(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h, const N_GUI_MD_FONTS *fonts)
add a read-only scrolling Markdown view.
Definition n_gui.c:5814
int n_gui_add_label(N_GUI_CTX *ctx, int window_id, const char *text, float x, float y, float w, float h, int align)
Add a static text label.
Definition n_gui.c:3639
void n_gui_markdownview_scroll_to(N_GUI_CTX *ctx, int widget_id, float y)
scroll to y pixels down the content, clamped to what there is
Definition n_gui.c:5893
void n_gui_draw(N_GUI_CTX *ctx)
draw all visible GUI windows and widgets
Definition n_gui.c:11735
void n_gui_markdownview_set_text(N_GUI_CTX *ctx, int widget_id, const char *text)
set the Markdown source (copied; NULL clears it).
Definition n_gui.c:5852
float n_gui_markdownview_get_scroll(N_GUI_CTX *ctx, int widget_id)
how far down the content the view is, in pixels
Definition n_gui.c:5903
int n_gui_add_window(N_GUI_CTX *ctx, const char *title, float x, float y, float w, float h)
Add a new pseudo-window to the context.
Definition n_gui.c:1277
N_GUI_CTX * n_gui_new_ctx(ALLEGRO_FONT *default_font)
Create a new GUI context.
Definition n_gui.c:1103
void n_gui_destroy_ctx(N_GUI_CTX **ctx)
Destroy a GUI context and all its windows/widgets.
Definition n_gui.c:1209
const char * n_gui_markdownview_get_text(N_GUI_CTX *ctx, int widget_id)
the Markdown source, owned by the widget, or NULL
Definition n_gui.c:5867
float n_gui_markdownview_content_height(N_GUI_CTX *ctx, int widget_id)
the height of the whole document at the width it was last drawn at, or 0 before the first draw
Definition n_gui.c:5908
The top-level GUI context that holds all windows.
Definition n_gui.h:1669
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:113
#define LOG_ERR
error conditions
Definition n_log.h:100
void set_log_level(const int log_level)
Set the global log level value ( static int LOG_LEVEL ).
Definition n_log.c:123
#define LOG_NOTICE
normal but significant condition
Definition n_log.h:104
a sound sample
GUI system: buttons, sliders, text areas, checkboxes, scrollbars, dropdown menus, windows.
int n_gui_md_link_at(const N_GUI_MD_FONTS *fonts, const N_GUI_MD_STYLE *style, float x, float y, float width, const char *text, float px, float py, char *url, size_t url_cap)
the url of the link under (px, py), if there is one
Definition n_gui_md.c:663
float n_gui_md_draw(const N_GUI_MD_FONTS *fonts, const N_GUI_MD_STYLE *style, float x, float y, float width, const char *text)
draw text as markdown at (x, y), wrapped to width
Definition n_gui_md.c:657
void n_gui_md_style_default(N_GUI_MD_STYLE *out, ALLEGRO_COLOR text, ALLEGRO_COLOR background, float line_height)
fill a style from two colours and a line height
Definition n_gui_md.c:622
float n_gui_md_height(const N_GUI_MD_FONTS *fonts, const N_GUI_MD_STYLE *style, float width, const char *text)
height of text laid out as markdown at width, in pixels
Definition n_gui_md.c:651
Lay out and draw n_markdown's block model with Allegro fonts.
int hard_breaks
Non-zero to break a paragraph where its source breaks, which is what a chat message wants: somebody p...
Definition n_gui_md.h:122
ALLEGRO_FONT * bold
bold
Definition n_gui_md.h:69
ALLEGRO_COLOR link
link text
Definition n_gui_md.h:93
ALLEGRO_FONT * regular
body text, and the fallback for every other face.
Definition n_gui_md.h:67
ALLEGRO_FONT * h1
"# " headings, ideally the bold face at a larger size
Definition n_gui_md.h:77
ALLEGRO_FONT * bolditalic
both at once
Definition n_gui_md.h:73
ALLEGRO_FONT * h3
"### " and deeper
Definition n_gui_md.h:81
unsigned parse_flags
n_markdown parse flags.
Definition n_gui_md.h:113
ALLEGRO_FONT * mono
code and fenced code blocks
Definition n_gui_md.h:75
ALLEGRO_FONT * h2
"## " headings
Definition n_gui_md.h:79
ALLEGRO_FONT * italic
italic
Definition n_gui_md.h:71
The faces a render uses.
Definition n_gui_md.h:65
Colours and metrics.
Definition n_gui_md.h:87
Generic log system.
#define N_MARKDOWN_ALL
every block kind this parser knows.
Definition n_markdown.h:90