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

examples/ex_wrap.c

Structures

Name
struct rects
struct collector

Functions

Name
size_t run(const n_wrap_item * items, size_t count, double max_width, collector * c)
void rect_collect(size_t line, double x0, double x1, void * user)
int main(void )
int eq(double a, double b)
void collect(size_t first, size_t last, size_t line, double width, void * user)

Attributes

Name
int failures

Defines

Name
MAX_LINES
CHECK(cond, msg)

Functions Documentation

function run

static size_t run(
    const n_wrap_item * items,
    size_t count,
    double max_width,
    collector * c
)

function rect_collect

static void rect_collect(
    size_t line,
    double x0,
    double x1,
    void * user
)

function main

int main(
    void 
)

function eq

static int eq(
    double a,
    double b
)

function collect

static void collect(
    size_t first,
    size_t last,
    size_t line,
    double width,
    void * user
)

Attributes Documentation

variable failures

static int failures = 0;

Macros Documentation

define MAX_LINES

#define MAX_LINES 16

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_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;
} collector;

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);
        failures++;
    }
    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;
} rects;

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) {
    collector c;
    set_log_level(LOG_NOTICE);

    /* 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},
                             {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;
}