Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_wrap.c

n_wrap regression: where the lines break, and the properties a height cache depends on (measure == draw, always >= 1 line, no infinite loop on an over-wide item).

n_wrap regression: where the lines break, and the properties a height cache depends on (measure == draw, always >= 1 line, no infinite loop on an over-wide item).

Author
Castagnier Mickael
Version
1.0
/*
* 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_log.h"
#include "nilorea/n_wrap.h"
static int failures = 0;
#define CHECK(cond, msg) \
do { \
if (!(cond)) { \
n_log(LOG_ERR, "FAIL: %s", (msg)); \
failures++; \
} \
} while (0)
/* collector: records every emitted line so a case can assert the ranges */
#define MAX_LINES 16
typedef struct {
size_t first[MAX_LINES], last[MAX_LINES];
double width[MAX_LINES];
size_t n;
static void collect(size_t first, size_t last, size_t line, double width, void* user) {
collector* c = (collector*)user;
if (c->n != line) {
n_log(LOG_ERR, "FAIL: line index %zu out of order (expected %zu)", line,
c->n);
}
if (c->n < MAX_LINES) {
c->first[c->n] = first;
c->last[c->n] = last;
c->width[c->n] = width;
}
c->n++;
}
/* rectangle collector for n_wrap_span_rects */
typedef struct {
size_t line[MAX_LINES];
double x0[MAX_LINES], x1[MAX_LINES];
size_t n;
static void rect_collect(size_t line, double x0, double x1, void* user) {
rects* r = (rects*)user;
if (r->n < MAX_LINES) {
r->line[r->n] = line;
r->x0[r->n] = x0;
r->x1[r->n] = x1;
}
r->n++;
}
/* run a case, and assert the count-only pass agrees with the emitting one --
* the property that keeps a cached height and a later draw from drifting */
static size_t run(const n_wrap_item* items, size_t count, double max_width, collector* c) {
size_t lines, counted;
c->n = 0;
lines = n_wrap_layout(items, count, max_width, collect, c);
counted = n_wrap_layout(items, count, max_width, NULL, NULL);
CHECK(lines == counted, "measure pass and emit pass agree on line count");
return lines;
}
static int eq(double a, double b) {
double d = a - b;
return d < 0.0001 && d > -0.0001;
}
int main(void) {
/* everything fits: one line, whatever the item count */
{
n_wrap_item it[3] = {{1.0, 0}, {1.0, 0}, {1.0, 0}};
CHECK(run(it, 3, 10.0, &c) == 1, "fits -> 1 line");
CHECK(c.n == 1 && c.first[0] == 0 && c.last[0] == 3, "fits -> [0,3)");
CHECK(eq(c.width[0], 3.0), "fits -> width 3");
}
/* breaks at the last space that fits, and that space is DROPPED */
{
n_wrap_item it[5] = {{2.0, 0},
{1.0, N_WRAP_SPACE},
{2.0, 0},
{1.0, N_WRAP_SPACE},
{2.0, 0}};
CHECK(run(it, 5, 5.0, &c) == 2, "space wrap -> 2 lines");
CHECK(c.n == 2, "space wrap -> 2 emitted");
CHECK(c.first[0] == 0 && c.last[0] == 3, "line 0 = items 0..2");
CHECK(eq(c.width[0], 5.0), "line 0 width excludes the break space");
CHECK(c.first[1] == 4 && c.last[1] == 5,
"line 1 starts AFTER the dropped space");
}
/* no break point anywhere: the run breaks mid-word rather than overflowing */
{
n_wrap_item it[3] = {{3.0, 0}, {3.0, 0}, {3.0, 0}};
CHECK(run(it, 3, 5.0, &c) == 3, "unbroken run -> one item per line");
CHECK(c.first[1] == 1 && c.last[1] == 2, "mid-run break keeps order");
}
/* an item wider than the line is PLACED, not dropped, and does not loop */
{
n_wrap_item it[1] = {{10.0, 0}};
CHECK(run(it, 1, 5.0, &c) == 1, "over-wide item -> 1 line");
CHECK(c.n == 1 && c.first[0] == 0 && c.last[0] == 1,
"over-wide item is placed alone");
CHECK(eq(c.width[0], 10.0), "over-wide line reports its real width");
}
/* the same, with something before it: the wide item still terminates */
{
n_wrap_item it[2] = {{1.0, 0}, {99.0, 0}};
CHECK(run(it, 2, 5.0, &c) == 2, "narrow + over-wide -> 2 lines");
CHECK(c.first[1] == 1 && c.last[1] == 2, "over-wide item on its own");
}
/* a hard break ends the line and occupies no cell */
{
n_wrap_item it[3] = {{1.0, 0}, {0.0, N_WRAP_NEWLINE}, {1.0, 0}};
CHECK(run(it, 3, 100.0, &c) == 2, "newline -> 2 lines");
CHECK(c.first[0] == 0 && c.last[0] == 1, "newline ends line 0 before it");
CHECK(c.first[1] == 2 && c.last[1] == 3, "line 1 starts after it");
}
/* a TRAILING hard break counts a final empty line but does not emit one */
{
n_wrap_item it[2] = {{1.0, 0}, {0.0, N_WRAP_NEWLINE}};
CHECK(run(it, 2, 100.0, &c) == 2, "trailing newline -> 2 lines counted");
CHECK(c.n == 1, "trailing newline emits only the non-empty line");
}
/* two hard breaks in a row leave a real empty line, and it IS emitted */
{
n_wrap_item it[3] = {{0.0, N_WRAP_NEWLINE},
{1.0, 0}};
CHECK(run(it, 3, 100.0, &c) == 3, "blank line -> 3 lines");
CHECK(c.n == 3 && c.first[1] == c.last[1], "the blank line is empty");
}
/* a line that STARTS with a space has no usable break point: the space
* stays rather than vanishing, so no text silently loses its indent */
{
n_wrap_item it[2] = {{1.0, N_WRAP_SPACE}, {9.0, 0}};
CHECK(run(it, 2, 5.0, &c) == 2, "leading space -> 2 lines");
CHECK(c.first[0] == 0 && c.last[0] == 1, "the leading space is kept");
}
/* empty input still occupies one line, so an empty message has a height */
{
n_wrap_item it[1] = {{1.0, 0}};
CHECK(run(it, 0, 5.0, &c) == 1, "empty -> 1 line");
CHECK(c.n == 0, "empty emits nothing");
(void)it;
}
/* defensive: a NULL array is an empty one, not a crash */
{
c.n = 0;
CHECK(n_wrap_layout(NULL, 7, 5.0, collect, &c) == 1,
"NULL items -> 1 line");
CHECK(c.n == 0, "NULL items emit nothing");
}
/* a non-positive width cannot fit anything, but must still terminate */
{
n_wrap_item it[3] = {{1.0, 0}, {1.0, N_WRAP_SPACE}, {1.0, 0}};
size_t lines = run(it, 3, 0.0, &c);
CHECK(lines >= 1 && lines <= 3, "zero width terminates");
}
/* hit-testing, n_wrap_index_at: "ab cd" (2,2,1,2,2) wrapped to 5 lays out
* as "ab" | "cd" with the space dropped; lines are 10 tall */
{
n_wrap_item it[5] = {{2.0, 0},
{2.0, 0},
{1.0, N_WRAP_SPACE},
{2.0, 0},
{2.0, 0}};
CHECK(n_wrap_index_at(it, 5, 5.0, 10.0, 0.0, 5.0) == 0,
"index_at: line start -> 0");
CHECK(n_wrap_index_at(it, 5, 5.0, 10.0, 0.9, 5.0) == 0,
"index_at: left half of 'a' -> before it");
CHECK(n_wrap_index_at(it, 5, 5.0, 10.0, 1.5, 5.0) == 1,
"index_at: right half of 'a' -> after it");
CHECK(n_wrap_index_at(it, 5, 5.0, 10.0, 3.1, 5.0) == 2,
"index_at: right half of 'b' -> line end");
CHECK(n_wrap_index_at(it, 5, 5.0, 10.0, 100.0, 5.0) == 2,
"index_at: past line 0's end -> its end, not the next line");
CHECK(n_wrap_index_at(it, 5, 5.0, 10.0, 0.0, 15.0) == 3,
"index_at: line 1 starts after the dropped space");
CHECK(n_wrap_index_at(it, 5, 5.0, 10.0, 2.9, 15.0) == 4,
"index_at: left half of 'd'");
CHECK(n_wrap_index_at(it, 5, 5.0, 10.0, 3.0, 15.0) == 5,
"index_at: right half of 'd' -> count");
CHECK(n_wrap_index_at(it, 5, 5.0, 10.0, 1.0, -1.0) == 0,
"index_at: above the text -> 0");
CHECK(n_wrap_index_at(it, 5, 5.0, 10.0, 1.0, 25.0) == 5,
"index_at: below the text -> count");
CHECK(n_wrap_index_at(NULL, 5, 5.0, 10.0, 1.0, 1.0) == 0,
"index_at: NULL items -> 0");
}
/* an atomic item (an emoji cell) is selected whole or not at all */
{
n_wrap_item it[3] = {{1.0, 0}, {10.0, 0}, {1.0, 0}};
CHECK(n_wrap_index_at(it, 3, 50.0, 10.0, 5.9, 5.0) == 1,
"index_at: left half of a cell -> before it");
CHECK(n_wrap_index_at(it, 3, 50.0, 10.0, 6.1, 5.0) == 2,
"index_at: right half of a cell -> after it");
}
/* a trailing hard break: the empty last line holds only the end */
{
n_wrap_item it[2] = {{2.0, 0}, {0.0, N_WRAP_NEWLINE}};
CHECK(n_wrap_index_at(it, 2, 50.0, 10.0, 0.0, 15.0) == 2,
"index_at: empty line after a trailing break -> count");
}
/* highlight rectangles, n_wrap_span_rects, on the same "ab" | "cd" */
{
n_wrap_item it[5] = {{2.0, 0},
{2.0, 0},
{1.0, N_WRAP_SPACE},
{2.0, 0},
{2.0, 0}};
rects r;
r.n = 0;
CHECK(n_wrap_span_rects(it, 5, 5.0, 1, 4, rect_collect, &r) == 2,
"span_rects: 'b cd'-ish span crosses the break -> 2 rects");
CHECK(r.n == 2 && r.line[0] == 0 && eq(r.x0[0], 2.0) &&
eq(r.x1[0], 4.0),
"span_rects: line 0 covers 'b' only");
CHECK(r.n == 2 && r.line[1] == 1 && eq(r.x0[1], 0.0) &&
eq(r.x1[1], 2.0),
"span_rects: line 1 covers 'c' only");
r.n = 0;
CHECK(n_wrap_span_rects(it, 5, 5.0, 4, 1, rect_collect, &r) == 2,
"span_rects: reversed bounds are the same span");
CHECK(n_wrap_span_rects(it, 5, 5.0, 2, 3, NULL, NULL) == 0,
"span_rects: only the dropped space -> nothing to paint");
CHECK(n_wrap_span_rects(it, 5, 5.0, 3, 3, NULL, NULL) == 0,
"span_rects: empty span -> 0");
CHECK(n_wrap_span_rects(it, 5, 5.0, 0, 99, NULL, NULL) == 2,
"span_rects: hi clamps to count");
}
if (failures) {
n_log(LOG_ERR, "ex_wrap: %d failure(s)", failures);
return 1;
}
n_log(LOG_NOTICE, "ex_wrap: all checks passed");
return 0;
}
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