7 n__gui__grid_8c
gullradriel edited this page 2026-10-12 07:59:13 +02:00

src/n_gui_grid.c

A virtualised grid of cells for N_GUI, scrolled vertically. More...

Structures

Name
struct _N_GUI_GRID
a grid's state, owned by its custom widget

Functions

Name
void _grid_sync_bar(_N_GUI_GRID * g)
helper: tell the scrollbar where the view is
float _grid_step(const _N_GUI_GRID * g)
helper: one row and the gap below it
size_t _grid_rows_for(const _N_GUI_GRID * g, int columns)
helper: rows the cells make at a column count
void _grid_place(_N_GUI_GRID * g)
helper: decide whether the bar shows, and place the grid and the bar
void _grid_on_bar(int widget_id, double pos, void * user_data)
the scrollbar's callback: the view follows a drag of the bar
_N_GUI_GRID * _grid_of(N_GUI_CTX * ctx, int widget_id)
helper: find a grid's state behind a widget id, or NULL
int _grid_mouse(int widget_id, const N_GUI_MOUSE_EVENT * ev, void * user_data)
custom mouse callback
int _grid_index_local(const _N_GUI_GRID * g, float w, float lx, float ly, size_t * index)
helper: the cell at a widget-local point, the rules of n_gui_grid_index_at
void _grid_draw(N_GUI_WIDGET * widget, float x, float y, ALLEGRO_FONT * font, void * user_data)
custom draw callback
void _grid_destroy(void * user_data)
the widget's destroy callback.
float _grid_content_h_for(const _N_GUI_GRID * g, float w)
helper: content height at a width: the gap above the first row, then each row with its gap
int _grid_columns_for(const _N_GUI_GRID * g, float w)
helper: cells across a width
void _grid_clamp(_N_GUI_GRID * g)
helper: keep the view inside the content

Detailed Description

A virtualised grid of cells for N_GUI, scrolled vertically.

Author: Castagnier Mickael

Version: 1.0

Date: 24/09/2026

Functions Documentation

function _grid_sync_bar

static void _grid_sync_bar(
    _N_GUI_GRID * g
)

helper: tell the scrollbar where the view is

function _grid_step

static float _grid_step(
    const _N_GUI_GRID * g
)

helper: one row and the gap below it

function _grid_rows_for

static size_t _grid_rows_for(
    const _N_GUI_GRID * g,
    int columns
)

helper: rows the cells make at a column count

function _grid_place

static void _grid_place(
    _N_GUI_GRID * g
)

helper: decide whether the bar shows, and place the grid and the bar

function _grid_on_bar

static void _grid_on_bar(
    int widget_id,
    double pos,
    void * user_data
)

the scrollbar's callback: the view follows a drag of the bar

function _grid_of

static _N_GUI_GRID * _grid_of(
    N_GUI_CTX * ctx,
    int widget_id
)

helper: find a grid's state behind a widget id, or NULL

function _grid_mouse

static int _grid_mouse(
    int widget_id,
    const N_GUI_MOUSE_EVENT * ev,
    void * user_data
)

custom mouse callback

function _grid_index_local

static int _grid_index_local(
    const _N_GUI_GRID * g,
    float w,
    float lx,
    float ly,
    size_t * index
)

helper: the cell at a widget-local point, the rules of n_gui_grid_index_at

function _grid_draw

static void _grid_draw(
    N_GUI_WIDGET * widget,
    float x,
    float y,
    ALLEGRO_FONT * font,
    void * user_data
)

custom draw callback

function _grid_destroy

static void _grid_destroy(
    void * user_data
)

the widget's destroy callback.

The scrollbar is a widget of the same window and is only ever destroyed with it, by n_gui_destroy_ctx(), which calls no scrollbar callback, so nothing reaches the state once it is freed.

function _grid_content_h_for

static float _grid_content_h_for(
    const _N_GUI_GRID * g,
    float w
)

helper: content height at a width: the gap above the first row, then each row with its gap

function _grid_columns_for

static int _grid_columns_for(
    const _N_GUI_GRID * g,
    float w
)

helper: cells across a width

function _grid_clamp

static void _grid_clamp(
    _N_GUI_GRID * g
)

helper: keep the view inside the content

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
 */


#ifdef HAVE_ALLEGRO

#include <math.h>
#include <stdlib.h>
#include <string.h>

#include "nilorea/n_common.h"
#include "nilorea/n_log.h"
#include "nilorea/n_gui_grid.h"

typedef struct {
    N_GUI_CTX* ctx;
    int id;
    int bar;
    N_GUI_GRID_CALLBACKS cb;
    void* user_data;
    size_t count;
    float cell_w;
    float cell_h;
    float gap;
    float scroll;
    float outer_x, outer_y, outer_w, outer_h;
    float bar_w;
    float inner_w;
    int visible;
    int bar_showing;
    int placing;
} _N_GUI_GRID;

static int _grid_columns_for(const _N_GUI_GRID* g, float w) {
    int columns = (int)((w + g->gap) / (g->cell_w + g->gap));

    return columns > 0 ? columns : 1;
}

static size_t _grid_rows_for(const _N_GUI_GRID* g, int columns) {
    return (g->count + (size_t)columns - 1) / (size_t)columns;
}

static float _grid_step(const _N_GUI_GRID* g) {
    return g->cell_h + g->gap;
}

static float _grid_content_h_for(const _N_GUI_GRID* g, float w) {
    return g->gap + (float)_grid_rows_for(g, _grid_columns_for(g, w)) * _grid_step(g);
}

static void _grid_clamp(_N_GUI_GRID* g) {
    float most = _grid_content_h_for(g, g->inner_w) - g->outer_h;

    if (most < 0.0f) most = 0.0f;
    if (g->scroll > most) g->scroll = most;
    if (g->scroll < 0.0f) g->scroll = 0.0f;
}

static void _grid_sync_bar(_N_GUI_GRID* g) {
    if (!g->bar_showing) return;
    g->placing = 1;
    n_gui_scrollbar_set_sizes(g->ctx, g->bar, _grid_content_h_for(g, g->inner_w), g->outer_h);
    n_gui_scrollbar_set_pos(g->ctx, g->bar, g->scroll);
    g->placing = 0;
}

static void _grid_place(_N_GUI_GRID* g) {
    /* Asked of the whole width first. A grid that needs a bar still needs
     * one once the bar has its strip, and one that does not cannot be made
     * to by narrowing it: narrowing a grid of fixed cells only adds rows.
     * So one pass settles it. */
    int want = g->count > 0 && _grid_content_h_for(g, g->outer_w) > g->outer_h + 0.5f;

    g->inner_w = g->outer_w;
    if (want) {
        g->inner_w = g->outer_w - g->bar_w;
        if (g->inner_w < 1.0f) g->inner_w = 1.0f;
    }
    n_gui_widget_set_rect(g->ctx, g->id, g->outer_x, g->outer_y, g->inner_w, g->outer_h);
    n_gui_widget_set_rect(g->ctx, g->bar, g->outer_x + g->inner_w, g->outer_y, g->bar_w > 0.0f ? g->bar_w : 1.0f, g->outer_h);
    /* hidden with the grid too: a bar left over a hidden grid is drawn on
     * whatever took its place */
    n_gui_set_widget_visible(g->ctx, g->bar, want && g->visible);
    g->bar_showing = want;
    if (!want) g->scroll = 0.0f;
    _grid_clamp(g);
    _grid_sync_bar(g);
    n_gui_mark_dirty(g->ctx);
}

// cppcheck-suppress constParameterCallback ; callback signature must match N_GUI_CUSTOM_DRAW
// cppcheck-suppress constParameter ; the same, under the id cppcheck 2.10 uses
static void _grid_draw(N_GUI_WIDGET* widget, float x, float y, ALLEGRO_FONT* font, void* user_data) {
    const _N_GUI_GRID* g = (const _N_GUI_GRID*)user_data;
    int cx = 0, cy = 0, cw = 0, ch = 0;
    int columns = 0;
    size_t rows = 0, first_row = 0, last_row = 0, row = 0;
    float step = 0.0f;

    (void)font;
    if (!widget || !g) return;
    if (g->cb.draw_background) g->cb.draw_background(g->id, x, y, widget->w, widget->h, g->user_data);
    if (!g->count || !g->cb.draw_cell) return;
    columns = _grid_columns_for(g, widget->w);
    rows = _grid_rows_for(g, columns);
    step = _grid_step(g);
    /* The rows the view touches, one either side of the arithmetic: the
     * clipping rectangle cuts the overhang, and an off by one here would
     * show as a strip of empty grid. */
    first_row = g->scroll > g->gap ? (size_t)((g->scroll - g->gap) / step) : 0;
    last_row = first_row + (size_t)(widget->h / step) + 1;
    if (last_row >= rows) last_row = rows - 1;

    /* the cells are cut at the grid, inside whatever the window cuts at */
    al_get_clipping_rectangle(&cx, &cy, &cw, &ch);
    {
        int x0 = (int)x > cx ? (int)x : cx;
        int y0 = (int)y > cy ? (int)y : cy;
        int x1 = (int)(x + widget->w) < cx + cw ? (int)(x + widget->w) : cx + cw;
        int y1 = (int)(y + widget->h) < cy + ch ? (int)(y + widget->h) : cy + ch;

        if (x1 <= x0 || y1 <= y0) return;
        al_set_clipping_rectangle(x0, y0, x1 - x0, y1 - y0);
    }
    for (row = first_row; row <= last_row; row++) {
        int column = 0;

        for (column = 0; column < columns; column++) {
            size_t index = row * (size_t)columns + (size_t)column;

            if (index >= g->count) break;
            g->cb.draw_cell(g->id, index, x + g->gap + (float)column * (g->cell_w + g->gap), y + g->gap + (float)row * step - g->scroll, g->cell_w, g->cell_h, g->user_data);
        }
    }
    al_set_clipping_rectangle(cx, cy, cw, ch);
    if (g->cb.visible) {
        /* one row beyond the bottom, so a steady scroll does not outrun
         * whatever prepares the cells */
        size_t first = first_row * (size_t)columns;
        size_t last = (last_row + 2) * (size_t)columns;

        if (last > g->count) last = g->count;
        g->cb.visible(g->id, first, last, g->user_data);
    }
}

static int _grid_index_local(const _N_GUI_GRID* g, float w, float lx, float ly, size_t* index) {
    int columns = _grid_columns_for(g, w);
    long column = (long)floorf((lx - g->gap) / (g->cell_w + g->gap));
    float content_y = ly - g->gap + g->scroll;
    long row = content_y < 0.0f ? 0 : (long)floorf(content_y / _grid_step(g));
    size_t at = 0;

    if (column < 0) column = 0;
    if (column >= columns) column = columns - 1;
    at = (size_t)row * (size_t)columns + (size_t)column;
    if (at >= g->count) return FALSE;
    *index = at;
    return TRUE;
}

// cppcheck-suppress constParameterCallback ; callback signature must match N_GUI_CUSTOM_MOUSE
// cppcheck-suppress constParameter ; the same, under the id cppcheck 2.10 uses
static int _grid_mouse(int widget_id, const N_GUI_MOUSE_EVENT* ev, void* user_data) {
    _N_GUI_GRID* g = (_N_GUI_GRID*)user_data;
    const N_GUI_WIDGET* wgt = g ? n_gui_get_widget(g->ctx, widget_id) : NULL;

    if (!g || !wgt) return 0;
    if (ev->type == N_GUI_MOUSE_WHEEL) {
        /* the caller first: a turn it takes is not a scroll */
        if (g->cb.wheel && g->cb.wheel(widget_id, ev->dz, ev->modifiers, g->user_data)) {
            n_gui_mark_dirty(g->ctx);
            return 1;
        }
        /* one notch, one row: the unit the grid is read in */
        g->scroll -= (float)ev->dz * _grid_step(g);
        _grid_clamp(g);
        _grid_sync_bar(g);
        n_gui_mark_dirty(g->ctx);
        return 1;
    }
    if (ev->type == N_GUI_MOUSE_DOWN) {
        size_t index = 0;
        float sx = 0.0f, sy = 0.0f;

        if (!_grid_index_local(g, wgt->w, ev->x, ev->y, &index)) return 0;
        if (!g->cb.press) return 1;
        n_gui_widget_screen_rect(g->ctx, widget_id, &sx, &sy, NULL, NULL);
        g->cb.press(widget_id, index, ev->button, sx + ev->x, sy + ev->y, g->user_data);
        return 1;
    }
    return 0;
}

static void _grid_on_bar(int widget_id, double pos, void* user_data) {
    _N_GUI_GRID* g = (_N_GUI_GRID*)user_data;

    (void)widget_id;
    /* a bar being told where it is reports it as a move; following that
     * would place the bar again, and the grid would walk under the hand */
    if (!g || g->placing) return;
    g->scroll = (float)pos;
    _grid_clamp(g);
    n_gui_mark_dirty(g->ctx);
}

static void _grid_destroy(void* user_data) {
    free(user_data);
}

static _N_GUI_GRID* _grid_of(N_GUI_CTX* ctx, int widget_id) {
    const N_GUI_WIDGET* wgt = ctx ? n_gui_get_widget(ctx, widget_id) : NULL;
    const N_GUI_CUSTOM_DATA* cd = NULL;

    if (!wgt || wgt->type != N_GUI_TYPE_CUSTOM || !wgt->data) return NULL;
    cd = (const N_GUI_CUSTOM_DATA*)wgt->data;
    /* the paint callback says what the widget is */
    if (cd->draw != _grid_draw) return NULL;
    return (_N_GUI_GRID*)cd->user_data;
}

int n_gui_grid_new(N_GUI_CTX* ctx, int window_id, const N_GUI_GRID_CALLBACKS* callbacks, void* user_data) {
    _N_GUI_GRID* g = NULL;

    __n_assert(ctx, return -1);
    __n_assert(callbacks, return -1);
    if (!callbacks->draw_cell) {
        n_log(LOG_ERR, "grid: draw_cell is required");
        return -1;
    }
    g = (_N_GUI_GRID*)calloc(1, sizeof(*g));
    if (!g) return -1;
    g->ctx = ctx;
    g->cb = *callbacks;
    g->user_data = user_data;
    g->cell_w = 64.0f;
    g->cell_h = 64.0f;
    g->gap = 4.0f;
    g->outer_w = 16.0f;
    g->outer_h = 16.0f;
    g->inner_w = 16.0f;
    g->bar_w = 12.0f;
    g->visible = 1;
    g->id = n_gui_add_custom(ctx, window_id, 0.0f, 0.0f, 16.0f, 16.0f, _grid_draw, g);
    if (g->id < 0) {
        free(g);
        return -1;
    }
    n_gui_custom_set_destroy(ctx, g->id, _grid_destroy);
    n_gui_custom_set_mouse(ctx, g->id, _grid_mouse, g);
    g->bar = n_gui_add_scrollbar(ctx, window_id, 0.0f, 0.0f, 16.0f, 16.0f, N_GUI_SCROLLBAR_V, N_GUI_SHAPE_ROUNDED, 1.0, 1.0, _grid_on_bar, g);
    if (g->bar < 0) {
        n_log(LOG_ERR, "grid: cannot create its scrollbar");
        /* the widget owns the state now; it goes when the window does */
        n_gui_set_widget_visible(ctx, g->id, 0);
        return -1;
    }
    n_gui_set_widget_visible(ctx, g->bar, 0);
    return g->id;
}

void n_gui_grid_set_rect(N_GUI_CTX* ctx, int widget_id, float x, float y, float w, float h, float bar_w) {
    _N_GUI_GRID* g = _grid_of(ctx, widget_id);

    if (!g) return;
    g->outer_x = x;
    g->outer_y = y;
    g->outer_w = w > 1.0f ? w : 1.0f;
    g->outer_h = h > 1.0f ? h : 1.0f;
    g->bar_w = bar_w > 0.0f ? bar_w : 0.0f;
    _grid_place(g);
}

void n_gui_grid_set_cell_size(N_GUI_CTX* ctx, int widget_id, float cell_w, float cell_h, float gap) {
    _N_GUI_GRID* g = _grid_of(ctx, widget_id);

    if (!g || cell_w <= 0.0f || cell_h <= 0.0f) return;
    g->cell_w = cell_w;
    g->cell_h = cell_h;
    g->gap = gap > 0.0f ? gap : 0.0f;
    _grid_place(g);
}

void n_gui_grid_set_count(N_GUI_CTX* ctx, int widget_id, size_t count) {
    _N_GUI_GRID* g = _grid_of(ctx, widget_id);

    if (!g) return;
    g->count = count;
    _grid_place(g);
}

void n_gui_grid_set_visible(N_GUI_CTX* ctx, int widget_id, int visible) {
    _N_GUI_GRID* g = _grid_of(ctx, widget_id);

    if (!g) return;
    g->visible = visible ? 1 : 0;
    n_gui_set_widget_visible(ctx, g->id, g->visible);
    n_gui_set_widget_visible(ctx, g->bar, g->visible && g->bar_showing);
    n_gui_mark_dirty(ctx);
}

int n_gui_grid_columns(N_GUI_CTX* ctx, int widget_id) {
    const _N_GUI_GRID* g = _grid_of(ctx, widget_id);

    return g ? _grid_columns_for(g, g->inner_w) : 0;
}

float n_gui_grid_get_scroll(N_GUI_CTX* ctx, int widget_id) {
    const _N_GUI_GRID* g = _grid_of(ctx, widget_id);

    return g ? g->scroll : 0.0f;
}

void n_gui_grid_set_scroll(N_GUI_CTX* ctx, int widget_id, float scroll) {
    _N_GUI_GRID* g = _grid_of(ctx, widget_id);

    if (!g) return;
    g->scroll = scroll;
    _grid_clamp(g);
    _grid_sync_bar(g);
    n_gui_mark_dirty(ctx);
}

void n_gui_grid_scroll_to(N_GUI_CTX* ctx, int widget_id, size_t index) {
    _N_GUI_GRID* g = _grid_of(ctx, widget_id);
    float top = 0.0f, bottom = 0.0f;
    size_t row = 0;

    if (!g || index >= g->count) return;
    row = index / (size_t)_grid_columns_for(g, g->inner_w);
    /* the first row counts from the very top, so the gap above it shows */
    top = row ? g->gap + (float)row * _grid_step(g) : 0.0f;
    bottom = g->gap + (float)(row + 1) * _grid_step(g);
    if (top < g->scroll) g->scroll = top;
    if (bottom > g->scroll + g->outer_h) g->scroll = bottom - g->outer_h;
    _grid_clamp(g);
    _grid_sync_bar(g);
    n_gui_mark_dirty(ctx);
}

int n_gui_grid_index_at(N_GUI_CTX* ctx, int widget_id, float x, float y, size_t* index) {
    const _N_GUI_GRID* g = _grid_of(ctx, widget_id);
    float gx = 0.0f, gy = 0.0f, gw = 0.0f, gh = 0.0f;

    __n_assert(index, return FALSE);
    if (!g || !g->visible || !g->count) return FALSE;
    if (!n_gui_widget_screen_rect(ctx, widget_id, &gx, &gy, &gw, &gh)) return FALSE;
    if (x < gx || x >= gx + gw || y < gy || y >= gy + gh) return FALSE;
    return _grid_index_local(g, gw, x - gx, y - gy, index);
}

int n_gui_grid_cell_rect(N_GUI_CTX* ctx, int widget_id, size_t index, float* x, float* y, float* w, float* h) {
    const _N_GUI_GRID* g = _grid_of(ctx, widget_id);
    float gx = 0.0f, gy = 0.0f, gw = 0.0f, gh = 0.0f, cx = 0.0f, cy = 0.0f;
    int columns = 0;

    if (!g || index >= g->count) return FALSE;
    n_gui_widget_screen_rect(ctx, widget_id, &gx, &gy, &gw, &gh);
    columns = _grid_columns_for(g, g->inner_w);
    cx = gx + g->gap + (float)(index % (size_t)columns) * (g->cell_w + g->gap);
    cy = gy + g->gap + (float)(index / (size_t)columns) * _grid_step(g) - g->scroll;
    if (x) *x = cx;
    if (y) *y = cy;
    if (w) *w = g->cell_w;
    if (h) *h = g->cell_h;
    return (cy + g->cell_h > gy && cy < gy + gh) ? TRUE : FALSE;
}

int n_gui_grid_scrollbar(N_GUI_CTX* ctx, int widget_id) {
    const _N_GUI_GRID* g = _grid_of(ctx, widget_id);

    return g ? g->bar : -1;
}

int n_gui_grid_scrollbar_showing(N_GUI_CTX* ctx, int widget_id) {
    const _N_GUI_GRID* g = _grid_of(ctx, widget_id);

    return g ? g->bar_showing : 0;
}

#endif /* HAVE_ALLEGRO */