Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_wrap.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
28
29#include "nilorea/n_log.h"
30#include "nilorea/n_wrap.h"
31
32static int failures = 0;
33
34#define CHECK(cond, msg) \
35 do { \
36 if (!(cond)) { \
37 n_log(LOG_ERR, "FAIL: %s", (msg)); \
38 failures++; \
39 } \
40 } while (0)
41
42/* collector: records every emitted line so a case can assert the ranges */
43#define MAX_LINES 16
44typedef struct {
47 size_t n;
48} collector;
49
50static void collect(size_t first, size_t last, size_t line, double width, void* user) {
51 collector* c = (collector*)user;
52 if (c->n != line) {
53 n_log(LOG_ERR, "FAIL: line index %zu out of order (expected %zu)", line,
54 c->n);
55 failures++;
56 }
57 if (c->n < MAX_LINES) {
58 c->first[c->n] = first;
59 c->last[c->n] = last;
60 c->width[c->n] = width;
61 }
62 c->n++;
63}
64
65/* rectangle collector for n_wrap_span_rects */
66typedef struct {
67 size_t line[MAX_LINES];
69 size_t n;
70} rects;
71
72static void rect_collect(size_t line, double x0, double x1, void* user) {
73 rects* r = (rects*)user;
74 if (r->n < MAX_LINES) {
75 r->line[r->n] = line;
76 r->x0[r->n] = x0;
77 r->x1[r->n] = x1;
78 }
79 r->n++;
80}
81
82/* run a case, and assert the count-only pass agrees with the emitting one --
83 * the property that keeps a cached height and a later draw from drifting */
84static size_t run(const n_wrap_item* items, size_t count, double max_width, collector* c) {
85 size_t lines, counted;
86 c->n = 0;
87 lines = n_wrap_layout(items, count, max_width, collect, c);
88 counted = n_wrap_layout(items, count, max_width, NULL, NULL);
89 CHECK(lines == counted, "measure pass and emit pass agree on line count");
90 return lines;
91}
92
93static int eq(double a, double b) {
94 double d = a - b;
95 return d < 0.0001 && d > -0.0001;
96}
97
98int main(void) {
99 collector c;
101
102 /* everything fits: one line, whatever the item count */
103 {
104 n_wrap_item it[3] = {{1.0, 0}, {1.0, 0}, {1.0, 0}};
105 CHECK(run(it, 3, 10.0, &c) == 1, "fits -> 1 line");
106 CHECK(c.n == 1 && c.first[0] == 0 && c.last[0] == 3, "fits -> [0,3)");
107 CHECK(eq(c.width[0], 3.0), "fits -> width 3");
108 }
109
110 /* breaks at the last space that fits, and that space is DROPPED */
111 {
112 n_wrap_item it[5] = {{2.0, 0},
113 {1.0, N_WRAP_SPACE},
114 {2.0, 0},
115 {1.0, N_WRAP_SPACE},
116 {2.0, 0}};
117 CHECK(run(it, 5, 5.0, &c) == 2, "space wrap -> 2 lines");
118 CHECK(c.n == 2, "space wrap -> 2 emitted");
119 CHECK(c.first[0] == 0 && c.last[0] == 3, "line 0 = items 0..2");
120 CHECK(eq(c.width[0], 5.0), "line 0 width excludes the break space");
121 CHECK(c.first[1] == 4 && c.last[1] == 5,
122 "line 1 starts AFTER the dropped space");
123 }
124
125 /* no break point anywhere: the run breaks mid-word rather than overflowing */
126 {
127 n_wrap_item it[3] = {{3.0, 0}, {3.0, 0}, {3.0, 0}};
128 CHECK(run(it, 3, 5.0, &c) == 3, "unbroken run -> one item per line");
129 CHECK(c.first[1] == 1 && c.last[1] == 2, "mid-run break keeps order");
130 }
131
132 /* an item wider than the line is PLACED, not dropped, and does not loop */
133 {
134 n_wrap_item it[1] = {{10.0, 0}};
135 CHECK(run(it, 1, 5.0, &c) == 1, "over-wide item -> 1 line");
136 CHECK(c.n == 1 && c.first[0] == 0 && c.last[0] == 1,
137 "over-wide item is placed alone");
138 CHECK(eq(c.width[0], 10.0), "over-wide line reports its real width");
139 }
140
141 /* the same, with something before it: the wide item still terminates */
142 {
143 n_wrap_item it[2] = {{1.0, 0}, {99.0, 0}};
144 CHECK(run(it, 2, 5.0, &c) == 2, "narrow + over-wide -> 2 lines");
145 CHECK(c.first[1] == 1 && c.last[1] == 2, "over-wide item on its own");
146 }
147
148 /* a hard break ends the line and occupies no cell */
149 {
150 n_wrap_item it[3] = {{1.0, 0}, {0.0, N_WRAP_NEWLINE}, {1.0, 0}};
151 CHECK(run(it, 3, 100.0, &c) == 2, "newline -> 2 lines");
152 CHECK(c.first[0] == 0 && c.last[0] == 1, "newline ends line 0 before it");
153 CHECK(c.first[1] == 2 && c.last[1] == 3, "line 1 starts after it");
154 }
155
156 /* a TRAILING hard break counts a final empty line but does not emit one */
157 {
158 n_wrap_item it[2] = {{1.0, 0}, {0.0, N_WRAP_NEWLINE}};
159 CHECK(run(it, 2, 100.0, &c) == 2, "trailing newline -> 2 lines counted");
160 CHECK(c.n == 1, "trailing newline emits only the non-empty line");
161 }
162
163 /* two hard breaks in a row leave a real empty line, and it IS emitted */
164 {
165 n_wrap_item it[3] = {{0.0, N_WRAP_NEWLINE},
166 {0.0, N_WRAP_NEWLINE},
167 {1.0, 0}};
168 CHECK(run(it, 3, 100.0, &c) == 3, "blank line -> 3 lines");
169 CHECK(c.n == 3 && c.first[1] == c.last[1], "the blank line is empty");
170 }
171
172 /* a line that STARTS with a space has no usable break point: the space
173 * stays rather than vanishing, so no text silently loses its indent */
174 {
175 n_wrap_item it[2] = {{1.0, N_WRAP_SPACE}, {9.0, 0}};
176 CHECK(run(it, 2, 5.0, &c) == 2, "leading space -> 2 lines");
177 CHECK(c.first[0] == 0 && c.last[0] == 1, "the leading space is kept");
178 }
179
180 /* empty input still occupies one line, so an empty message has a height */
181 {
182 n_wrap_item it[1] = {{1.0, 0}};
183 CHECK(run(it, 0, 5.0, &c) == 1, "empty -> 1 line");
184 CHECK(c.n == 0, "empty emits nothing");
185 (void)it;
186 }
187
188 /* defensive: a NULL array is an empty one, not a crash */
189 {
190 c.n = 0;
191 CHECK(n_wrap_layout(NULL, 7, 5.0, collect, &c) == 1,
192 "NULL items -> 1 line");
193 CHECK(c.n == 0, "NULL items emit nothing");
194 }
195
196 /* a non-positive width cannot fit anything, but must still terminate */
197 {
198 n_wrap_item it[3] = {{1.0, 0}, {1.0, N_WRAP_SPACE}, {1.0, 0}};
199 size_t lines = run(it, 3, 0.0, &c);
200 CHECK(lines >= 1 && lines <= 3, "zero width terminates");
201 }
202
203 /* hit-testing, n_wrap_index_at: "ab cd" (2,2,1,2,2) wrapped to 5 lays out
204 * as "ab" | "cd" with the space dropped; lines are 10 tall */
205 {
206 n_wrap_item it[5] = {{2.0, 0},
207 {2.0, 0},
208 {1.0, N_WRAP_SPACE},
209 {2.0, 0},
210 {2.0, 0}};
211 CHECK(n_wrap_index_at(it, 5, 5.0, 10.0, 0.0, 5.0) == 0,
212 "index_at: line start -> 0");
213 CHECK(n_wrap_index_at(it, 5, 5.0, 10.0, 0.9, 5.0) == 0,
214 "index_at: left half of 'a' -> before it");
215 CHECK(n_wrap_index_at(it, 5, 5.0, 10.0, 1.5, 5.0) == 1,
216 "index_at: right half of 'a' -> after it");
217 CHECK(n_wrap_index_at(it, 5, 5.0, 10.0, 3.1, 5.0) == 2,
218 "index_at: right half of 'b' -> line end");
219 CHECK(n_wrap_index_at(it, 5, 5.0, 10.0, 100.0, 5.0) == 2,
220 "index_at: past line 0's end -> its end, not the next line");
221 CHECK(n_wrap_index_at(it, 5, 5.0, 10.0, 0.0, 15.0) == 3,
222 "index_at: line 1 starts after the dropped space");
223 CHECK(n_wrap_index_at(it, 5, 5.0, 10.0, 2.9, 15.0) == 4,
224 "index_at: left half of 'd'");
225 CHECK(n_wrap_index_at(it, 5, 5.0, 10.0, 3.0, 15.0) == 5,
226 "index_at: right half of 'd' -> count");
227 CHECK(n_wrap_index_at(it, 5, 5.0, 10.0, 1.0, -1.0) == 0,
228 "index_at: above the text -> 0");
229 CHECK(n_wrap_index_at(it, 5, 5.0, 10.0, 1.0, 25.0) == 5,
230 "index_at: below the text -> count");
231 CHECK(n_wrap_index_at(NULL, 5, 5.0, 10.0, 1.0, 1.0) == 0,
232 "index_at: NULL items -> 0");
233 }
234
235 /* an atomic item (an emoji cell) is selected whole or not at all */
236 {
237 n_wrap_item it[3] = {{1.0, 0}, {10.0, 0}, {1.0, 0}};
238 CHECK(n_wrap_index_at(it, 3, 50.0, 10.0, 5.9, 5.0) == 1,
239 "index_at: left half of a cell -> before it");
240 CHECK(n_wrap_index_at(it, 3, 50.0, 10.0, 6.1, 5.0) == 2,
241 "index_at: right half of a cell -> after it");
242 }
243
244 /* a trailing hard break: the empty last line holds only the end */
245 {
246 n_wrap_item it[2] = {{2.0, 0}, {0.0, N_WRAP_NEWLINE}};
247 CHECK(n_wrap_index_at(it, 2, 50.0, 10.0, 0.0, 15.0) == 2,
248 "index_at: empty line after a trailing break -> count");
249 }
250
251 /* highlight rectangles, n_wrap_span_rects, on the same "ab" | "cd" */
252 {
253 n_wrap_item it[5] = {{2.0, 0},
254 {2.0, 0},
255 {1.0, N_WRAP_SPACE},
256 {2.0, 0},
257 {2.0, 0}};
258 rects r;
259 r.n = 0;
260 CHECK(n_wrap_span_rects(it, 5, 5.0, 1, 4, rect_collect, &r) == 2,
261 "span_rects: 'b cd'-ish span crosses the break -> 2 rects");
262 CHECK(r.n == 2 && r.line[0] == 0 && eq(r.x0[0], 2.0) &&
263 eq(r.x1[0], 4.0),
264 "span_rects: line 0 covers 'b' only");
265 CHECK(r.n == 2 && r.line[1] == 1 && eq(r.x0[1], 0.0) &&
266 eq(r.x1[1], 2.0),
267 "span_rects: line 1 covers 'c' only");
268 r.n = 0;
269 CHECK(n_wrap_span_rects(it, 5, 5.0, 4, 1, rect_collect, &r) == 2,
270 "span_rects: reversed bounds are the same span");
271 CHECK(n_wrap_span_rects(it, 5, 5.0, 2, 3, NULL, NULL) == 0,
272 "span_rects: only the dropped space -> nothing to paint");
273 CHECK(n_wrap_span_rects(it, 5, 5.0, 3, 3, NULL, NULL) == 0,
274 "span_rects: empty span -> 0");
275 CHECK(n_wrap_span_rects(it, 5, 5.0, 0, 99, NULL, NULL) == 2,
276 "span_rects: hi clamps to count");
277 }
278
279 if (failures) {
280 n_log(LOG_ERR, "ex_wrap: %d failure(s)", failures);
281 return 1;
282 }
283 n_log(LOG_NOTICE, "ex_wrap: all checks passed");
284 return 0;
285}
static int failures
Definition ex_atlas.c:51
int main(void)
Definition ex_atlas.c:117
#define CHECK(cond, msg)
static N_GUI_MOUSE_EVENT last
the last event the widget was told of, and how many
int run
Definition ex_kafka.c:56
#define MAX_LINES
Definition ex_wrap.c:43
static void rect_collect(size_t line, double x0, double x1, void *user)
Definition ex_wrap.c:72
size_t last[16]
Definition ex_wrap.c:45
size_t first[16]
Definition ex_wrap.c:45
double x0[16]
Definition ex_wrap.c:68
size_t n
Definition ex_wrap.c:69
size_t line[16]
Definition ex_wrap.c:67
double width[16]
Definition ex_wrap.c:46
static int eq(double a, double b)
Definition ex_wrap.c:93
double x1[16]
Definition ex_wrap.c:68
size_t n
Definition ex_wrap.c:47
void line(BITMAP *bmp, int x1, int y1, int x2, int y2, int color)
draw a line
#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
Generic log system.
static int collect(const char *path, const char *dir, const PK_OBJ *o, const char *prefix, N_PTH *pth, int depth)
collect the tensors under a dictionary, depth first
Definition n_pth.c:707
size_t n_wrap_layout(const n_wrap_item *items, size_t count, double max_width, n_wrap_emit emit, void *user)
Break items into lines no wider than max_width.
Definition n_wrap.c:50
size_t n_wrap_span_rects(const n_wrap_item *items, size_t count, double max_width, size_t lo, size_t hi, n_wrap_rect_emit emit, void *user)
The rectangles that cover the items [lo, hi): one per line the range touches, in order,...
Definition n_wrap.c:249
size_t n_wrap_index_at(const n_wrap_item *items, size_t count, double max_width, double line_height, double x, double y)
The item boundary nearest a point: the inverse of n_wrap_layout(), for turning a click or a drag into...
Definition n_wrap.c:176
Greedy word-wrap over pre-measured, styled items.
#define N_WRAP_SPACE
the item is a legal break point (typically a space); it is DROPPED when a line breaks there,...
Definition n_wrap.h:70
#define N_WRAP_NEWLINE
the item is a hard line break; it ends the line and occupies no cell
Definition n_wrap.h:72
One pre-measured layout item: a glyph, an emoji cell, an inline image, or a whole word,...
Definition n_wrap.h:76