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

src/n_gui_popup.c

A popup menu with submenus for N_GUI, the kind a right click opens. More...

Structures

Name
struct _N_GUI_POPUP_LEVEL
what a level's widget is handed: stable while the entries array moves
struct _N_GUI_POPUP_ENTRY
one entry.
struct N_GUI_POPUP
the popup

Functions

Name
int _popup_valid(const N_GUI_POPUP * popup, int id)
helper: is this id an entry
int _popup_show_level(N_GUI_POPUP * popup, int level, float x, float y)
helper: lay a level out, place it with its corner at a point pulled back inside the display, and open it on top
int _popup_row_shows(const N_GUI_POPUP * popup, int id)
helper: is an entry a row that shows, separators aside
float _popup_row_h(const N_GUI_POPUP * popup)
helper: height of an item row
int _popup_row_at(const N_GUI_POPUP * popup, int level, float ly)
helper: the row index at a level-local y, or -1
void _popup_open_child(N_GUI_POPUP * popup, int level, int row)
helper: open a submenu beside its row.
int _popup_mouse(int widget_id, const N_GUI_MOUSE_EVENT * ev, void * user_data)
custom mouse callback: hover opens submenus, a press acts
int _popup_make_window(N_GUI_POPUP * popup, int level)
helper: make a level's window the first time it opens
int _popup_layout(N_GUI_POPUP * popup, int level)
helper: the rows of a level, and its size.
int _popup_is_level(const N_GUI_POPUP * popup, int id)
helper: is this id a level (the root or a submenu)
int _popup_has_rows(const N_GUI_POPUP * popup, int level)
helper: does a level have at least one row that would be shown.
ALLEGRO_FONT * _popup_font(const N_GUI_POPUP * popup)
helper: the font rows are measured with
void _popup_draw(N_GUI_WIDGET * widget, float x, float y, ALLEGRO_FONT * font, void * user_data)
custom draw callback: the rows of one level
void _popup_close_level(N_GUI_POPUP * popup, int level)
helper: close a level and everything open beside it
int _popup_append(N_GUI_POPUP * popup, int kind, int parent, const char * text, const char * tip, int tag)
helper: append an entry, returning its id or -1
int _popup_activate(N_GUI_POPUP * popup, int level, int row)
helper: act on a row: pick an item, open a submenu.

Defines

Name
_POPUP_KIND_SUB
a submenu, which is also a level
_POPUP_KIND_SEP
a separator
_POPUP_KIND_ROOT
the top level
_POPUP_KIND_ITEM
an item, picked with a tag
N_GUI_POPUP_SEP_H
height of a separator row, at scale 1
N_GUI_POPUP_PAD_Y
padding above and below a row's text, at scale 1
N_GUI_POPUP_PAD_X
padding left and right of a row's text, at scale 1
N_GUI_POPUP_OVERLAP
how far a submenu overlaps its parent, at scale 1, so the pointer does not cross a gap on its way in
N_GUI_POPUP_MIN_W
narrowest a level is, at scale 1
N_GUI_POPUP_ARROW_W
room kept at the right of a level for the submenu arrow, at scale 1

Detailed Description

A popup menu with submenus for N_GUI, the kind a right click opens.

Author: Castagnier Mickael

Version: 1.0

Date: 29/09/2026

Functions Documentation

function _popup_valid

static int _popup_valid(
    const N_GUI_POPUP * popup,
    int id
)

helper: is this id an entry

function _popup_show_level

static int _popup_show_level(
    N_GUI_POPUP * popup,
    int level,
    float x,
    float y
)

helper: lay a level out, place it with its corner at a point pulled back inside the display, and open it on top

function _popup_row_shows

static int _popup_row_shows(
    const N_GUI_POPUP * popup,
    int id
)

helper: is an entry a row that shows, separators aside

function _popup_row_h

static float _popup_row_h(
    const N_GUI_POPUP * popup
)

helper: height of an item row

function _popup_row_at

static int _popup_row_at(
    const N_GUI_POPUP * popup,
    int level,
    float ly
)

helper: the row index at a level-local y, or -1

function _popup_open_child

static void _popup_open_child(
    N_GUI_POPUP * popup,
    int level,
    int row
)

helper: open a submenu beside its row.

To the right of the parent, unless it would leave the display there, then to the left: a submenu pulled back over its parent would hide the row it came from.

function _popup_mouse

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

custom mouse callback: hover opens submenus, a press acts

function _popup_make_window

static int _popup_make_window(
    N_GUI_POPUP * popup,
    int level
)

helper: make a level's window the first time it opens

function _popup_layout

static int _popup_layout(
    N_GUI_POPUP * popup,
    int level
)

helper: the rows of a level, and its size.

Separators are kept only between two shown rows, once, so that hiding entries never leaves a line at an end or two lines together.

function _popup_is_level

static int _popup_is_level(
    const N_GUI_POPUP * popup,
    int id
)

helper: is this id a level (the root or a submenu)

function _popup_has_rows

static int _popup_has_rows(
    const N_GUI_POPUP * popup,
    int level
)

helper: does a level have at least one row that would be shown.

A submenu without one is not shown either: an arrow to an empty panel is a row that does nothing.

function _popup_font

static ALLEGRO_FONT * _popup_font(
    const N_GUI_POPUP * popup
)

helper: the font rows are measured with

function _popup_draw

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

custom draw callback: the rows of one level

function _popup_close_level

static void _popup_close_level(
    N_GUI_POPUP * popup,
    int level
)

helper: close a level and everything open beside it

function _popup_append

static int _popup_append(
    N_GUI_POPUP * popup,
    int kind,
    int parent,
    const char * text,
    const char * tip,
    int tag
)

helper: append an entry, returning its id or -1

function _popup_activate

static int _popup_activate(
    N_GUI_POPUP * popup,
    int level,
    int row
)

helper: act on a row: pick an item, open a submenu.

1 when it did.

Macros Documentation

define _POPUP_KIND_SUB

#define _POPUP_KIND_SUB 2

a submenu, which is also a level

define _POPUP_KIND_SEP

#define _POPUP_KIND_SEP 3

a separator

define _POPUP_KIND_ROOT

#define _POPUP_KIND_ROOT 0

the top level

define _POPUP_KIND_ITEM

#define _POPUP_KIND_ITEM 1

an item, picked with a tag

define N_GUI_POPUP_SEP_H

#define N_GUI_POPUP_SEP_H 7.0f

height of a separator row, at scale 1

define N_GUI_POPUP_PAD_Y

#define N_GUI_POPUP_PAD_Y 4.0f

padding above and below a row's text, at scale 1

define N_GUI_POPUP_PAD_X

#define N_GUI_POPUP_PAD_X 10.0f

padding left and right of a row's text, at scale 1

define N_GUI_POPUP_OVERLAP

#define N_GUI_POPUP_OVERLAP 2.0f

how far a submenu overlaps its parent, at scale 1, so the pointer does not cross a gap on its way in

define N_GUI_POPUP_MIN_W

#define N_GUI_POPUP_MIN_W 120.0f

narrowest a level is, at scale 1

define N_GUI_POPUP_ARROW_W

#define N_GUI_POPUP_ARROW_W 16.0f

room kept at the right of a level for the submenu arrow, at scale 1

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 <stdlib.h>
#include <string.h>

#include <allegro5/allegro_primitives.h>

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

#define N_GUI_POPUP_PAD_X 10.0f
#define N_GUI_POPUP_PAD_Y 4.0f
#define N_GUI_POPUP_SEP_H 7.0f
#define N_GUI_POPUP_ARROW_W 16.0f
#define N_GUI_POPUP_MIN_W 120.0f
#define N_GUI_POPUP_OVERLAP 2.0f

#define _POPUP_KIND_ROOT 0
#define _POPUP_KIND_ITEM 1
#define _POPUP_KIND_SUB 2
#define _POPUP_KIND_SEP 3

typedef struct {
    N_GUI_POPUP* popup;
    int entry;
} _N_GUI_POPUP_LEVEL;

typedef struct {
    int kind;
    int parent;
    char* text;
    char* tip;
    int tag;
    int shown;
    int enabled;
    int window;
    int widget;
    _N_GUI_POPUP_LEVEL* hook;
    int hover;
    int child;
    int* rows;
    int nb_rows;
    float* row_y;
    float w, h;
    float x, y;
} _N_GUI_POPUP_ENTRY;

struct N_GUI_POPUP {
    N_GUI_CTX* ctx;
    float scale;
    N_GUI_POPUP_FN on_pick;
    void* user_data;
    _N_GUI_POPUP_ENTRY* entries;
    int nb_entries;
    int capacity;
};

static int _popup_valid(const N_GUI_POPUP* popup, int id) {
    return popup && id >= 0 && id < popup->nb_entries;
}

static int _popup_is_level(const N_GUI_POPUP* popup, int id) {
    return _popup_valid(popup, id) && (popup->entries[id].kind == _POPUP_KIND_ROOT || popup->entries[id].kind == _POPUP_KIND_SUB);
}

static ALLEGRO_FONT* _popup_font(const N_GUI_POPUP* popup) {
    return popup->ctx ? popup->ctx->default_font : NULL;
}

static float _popup_row_h(const N_GUI_POPUP* popup) {
    ALLEGRO_FONT* font = _popup_font(popup);
    float line = font ? (float)al_get_font_line_height(font) : 12.0f;

    return line + 2.0f * N_GUI_POPUP_PAD_Y * popup->scale;
}

static int _popup_append(N_GUI_POPUP* popup, int kind, int parent, const char* text, const char* tip, int tag) {
    _N_GUI_POPUP_ENTRY* e = NULL;

    if (popup->nb_entries == popup->capacity) {
        int wanted = popup->capacity ? popup->capacity * 2 : 16;
        _N_GUI_POPUP_ENTRY* grown = (_N_GUI_POPUP_ENTRY*)realloc(popup->entries, (size_t)wanted * sizeof(*grown));

        if (!grown) {
            n_log(LOG_ERR, "popup: out of memory for an entry");
            return -1;
        }
        popup->entries = grown;
        popup->capacity = wanted;
    }
    e = &popup->entries[popup->nb_entries];
    memset(e, 0, sizeof(*e));
    e->kind = kind;
    e->parent = parent;
    e->tag = tag;
    e->shown = 1;
    e->enabled = 1;
    e->window = -1;
    e->widget = -1;
    e->hover = -1;
    e->child = -1;
    if (text && text[0]) {
        e->text = strdup(text);
        if (!e->text) return -1;
    }
    if (tip && tip[0]) {
        e->tip = strdup(tip);
        if (!e->tip) {
            FreeNoLog(e->text);
            return -1;
        }
    }
    return popup->nb_entries++;
}

static int _popup_has_rows(const N_GUI_POPUP* popup, int level) {
    int i = 0;

    for (i = 1; i < popup->nb_entries; i++) {
        const _N_GUI_POPUP_ENTRY* e = &popup->entries[i];

        if (e->parent != level || !e->shown || e->kind == _POPUP_KIND_SEP) continue;
        if (e->kind == _POPUP_KIND_SUB && !_popup_has_rows(popup, i)) continue;
        return 1;
    }
    return 0;
}

static int _popup_row_shows(const N_GUI_POPUP* popup, int id) {
    const _N_GUI_POPUP_ENTRY* e = &popup->entries[id];

    if (!e->shown) return 0;
    if (e->kind == _POPUP_KIND_SUB) return _popup_has_rows(popup, id);
    return 1;
}

static int _popup_layout(N_GUI_POPUP* popup, int level) {
    _N_GUI_POPUP_ENTRY* lv = &popup->entries[level];
    ALLEGRO_FONT* font = _popup_font(popup);
    float s = popup->scale, row_h = _popup_row_h(popup), y = 0.0f, widest = 0.0f;
    int i = 0, n = 0, pending_sep = -1, has_sub = 0;

    FreeNoLog(lv->rows);
    FreeNoLog(lv->row_y);
    lv->nb_rows = 0;
    lv->rows = (int*)calloc((size_t)popup->nb_entries + 1, sizeof(int));
    lv->row_y = (float*)calloc((size_t)popup->nb_entries + 1, sizeof(float));
    if (!lv->rows || !lv->row_y) {
        FreeNoLog(lv->rows);
        FreeNoLog(lv->row_y);
        return -1;
    }
    /* the heading, on the top level only, takes a row and a separator */
    if (level == N_GUI_POPUP_ROOT && lv->text) {
        y += row_h + N_GUI_POPUP_SEP_H * s;
        if (font) widest = (float)al_get_text_width(font, lv->text);
    }
    for (i = 1; i < popup->nb_entries; i++) {
        const _N_GUI_POPUP_ENTRY* e = &popup->entries[i];

        if (e->parent != level) continue;
        if (e->kind == _POPUP_KIND_SEP) {
            if (e->shown && n > 0) pending_sep = i;
            continue;
        }
        if (!_popup_row_shows(popup, i)) continue;
        if (pending_sep >= 0) {
            lv->rows[n] = pending_sep;
            lv->row_y[n] = y;
            y += N_GUI_POPUP_SEP_H * s;
            n++;
            pending_sep = -1;
        }
        lv->rows[n] = i;
        lv->row_y[n] = y;
        y += row_h;
        n++;
        if (e->kind == _POPUP_KIND_SUB) has_sub = 1;
        if (font && e->text) {
            float w = (float)al_get_text_width(font, e->text);

            if (w > widest) widest = w;
        }
    }
    lv->nb_rows = n;
    lv->w = widest + 2.0f * N_GUI_POPUP_PAD_X * s + (has_sub ? N_GUI_POPUP_ARROW_W * s : 0.0f);
    if (lv->w < N_GUI_POPUP_MIN_W * s) lv->w = N_GUI_POPUP_MIN_W * s;
    /* a little room under the last row */
    lv->h = y + N_GUI_POPUP_PAD_Y * s;
    return n > 0 ? 0 : -1;
}

static int _popup_row_at(const N_GUI_POPUP* popup, int level, float ly) {
    const _N_GUI_POPUP_ENTRY* lv = &popup->entries[level];
    float row_h = _popup_row_h(popup), sep_h = N_GUI_POPUP_SEP_H * popup->scale;
    int r = 0;

    for (r = 0; r < lv->nb_rows; r++) {
        float h = popup->entries[lv->rows[r]].kind == _POPUP_KIND_SEP ? sep_h : row_h;

        if (ly >= lv->row_y[r] && ly < lv->row_y[r] + h) return r;
    }
    return -1;
}

static void _popup_close_level(N_GUI_POPUP* popup, int level) {
    _N_GUI_POPUP_ENTRY* lv = &popup->entries[level];

    if (lv->child >= 0) _popup_close_level(popup, lv->child);
    lv->child = -1;
    lv->hover = -1;
    if (lv->window >= 0) n_gui_close_window(popup->ctx, lv->window);
}

static void _popup_draw(N_GUI_WIDGET* widget, float x, float y, ALLEGRO_FONT* font, void* user_data);
static int _popup_mouse(int widget_id, const N_GUI_MOUSE_EVENT* ev, void* user_data);

static int _popup_make_window(N_GUI_POPUP* popup, int level) {
    _N_GUI_POPUP_ENTRY* lv = &popup->entries[level];

    if (lv->window >= 0) return 0;
    if (!lv->hook) {
        lv->hook = (_N_GUI_POPUP_LEVEL*)calloc(1, sizeof(*lv->hook));
        if (!lv->hook) return -1;
        lv->hook->popup = popup;
        lv->hook->entry = level;
    }
    lv->window = n_gui_add_window(popup->ctx, "", 0.0f, 0.0f, lv->w, lv->h);
    if (lv->window < 0) return -1;
    /* Frameless and fixed: a menu with a title bar is a dialog, and one that
     * can be dragged away from what it is about is worse than one that
     * cannot. */
    n_gui_window_set_flags(popup->ctx, lv->window, N_GUI_WIN_FRAMELESS | N_GUI_WIN_FIXED_POSITION);
    n_gui_window_set_zorder(popup->ctx, lv->window, N_GUI_ZORDER_POPUP, 0);
    lv->widget = n_gui_add_custom(popup->ctx, lv->window, 0.0f, 0.0f, lv->w, lv->h, _popup_draw, lv->hook);
    if (lv->widget < 0) {
        n_gui_close_window(popup->ctx, lv->window);
        return -1;
    }
    n_gui_custom_set_mouse(popup->ctx, lv->widget, _popup_mouse, lv->hook);
    n_gui_close_window(popup->ctx, lv->window);
    return 0;
}

static int _popup_show_level(N_GUI_POPUP* popup, int level, float x, float y) {
    _N_GUI_POPUP_ENTRY* lv = &popup->entries[level];
    float dw = popup->ctx->display_w, dh = popup->ctx->display_h;

    if (_popup_layout(popup, level) != 0) return -1;
    if (_popup_make_window(popup, level) != 0) return -1;
    if (dw > 0.0f && x + lv->w > dw) x = dw - lv->w;
    if (dh > 0.0f && y + lv->h > dh) y = dh - lv->h;
    if (x < 0.0f) x = 0.0f;
    if (y < 0.0f) y = 0.0f;
    lv->x = x;
    lv->y = y;
    lv->hover = -1;
    lv->child = -1;
    n_gui_window_set_rect(popup->ctx, lv->window, x, y, lv->w, lv->h);
    n_gui_widget_set_rect(popup->ctx, lv->widget, 0.0f, 0.0f, lv->w, lv->h);
    n_gui_set_widget_tooltip(popup->ctx, lv->widget, NULL);
    n_gui_open_window(popup->ctx, lv->window);
    n_gui_raise_window(popup->ctx, lv->window);
    n_gui_mark_dirty(popup->ctx);
    return 0;
}

static void _popup_open_child(N_GUI_POPUP* popup, int level, int row) {
    _N_GUI_POPUP_ENTRY* lv = &popup->entries[level];
    int sub = lv->rows[row];
    float s = popup->scale, x = 0.0f, y = 0.0f, dw = popup->ctx->display_w;

    if (lv->child == sub) return;
    if (lv->child >= 0) _popup_close_level(popup, lv->child);
    lv->child = -1;
    if (_popup_layout(popup, sub) != 0) return;
    x = lv->x + lv->w - N_GUI_POPUP_OVERLAP * s;
    if (dw > 0.0f && x + popup->entries[sub].w > dw) x = lv->x - popup->entries[sub].w + N_GUI_POPUP_OVERLAP * s;
    /* its first row level with the row that opened it */
    y = lv->y + lv->row_y[row];
    if (_popup_show_level(popup, sub, x, y) == 0) lv->child = sub;
}

static int _popup_activate(N_GUI_POPUP* popup, int level, int row) {
    const _N_GUI_POPUP_ENTRY* lv = &popup->entries[level];
    const _N_GUI_POPUP_ENTRY* e = NULL;

    if (row < 0 || row >= lv->nb_rows) return 0;
    e = &popup->entries[lv->rows[row]];
    if (!e->enabled) return 0;
    if (e->kind == _POPUP_KIND_SUB) {
        _popup_open_child(popup, level, row);
        return 1;
    }
    if (e->kind == _POPUP_KIND_ITEM) {
        int tag = e->tag;

        /* closed first, so a dialog the callback opens is not under it */
        n_gui_popup_close(popup);
        if (popup->on_pick) popup->on_pick(popup, tag, popup->user_data);
        return 1;
    }
    return 0;
}

// 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 _popup_draw(N_GUI_WIDGET* widget, float x, float y, ALLEGRO_FONT* font, void* user_data) {
    const _N_GUI_POPUP_LEVEL* hook = (const _N_GUI_POPUP_LEVEL*)user_data;
    const N_GUI_POPUP* popup = NULL;
    const _N_GUI_POPUP_ENTRY* lv = NULL;
    const N_GUI_THEME* th = NULL;
    float s = 1.0f, row_h = 0.0f, pad_x = 0.0f, pad_y = 0.0f;
    ALLEGRO_COLOR dim;
    int r = 0;

    if (!widget || !hook || !hook->popup || !font) return;
    popup = hook->popup;
    lv = &popup->entries[hook->entry];
    th = &widget->theme;
    s = popup->scale;
    row_h = _popup_row_h(popup);
    pad_x = N_GUI_POPUP_PAD_X * s;
    pad_y = N_GUI_POPUP_PAD_Y * s;
    /* greyed text: the text colour halfway to the background */
    {
        float tr = 0, tg = 0, tb = 0, ta = 0, br = 0, bg = 0, bb = 0, ba = 0;

        al_unmap_rgba_f(th->text_normal, &tr, &tg, &tb, &ta);
        al_unmap_rgba_f(th->bg_normal, &br, &bg, &bb, &ba);
        dim = al_map_rgba_f((tr + br) * 0.5f, (tg + bg) * 0.5f, (tb + bb) * 0.5f, ta);
    }
    al_draw_filled_rectangle(x, y, x + widget->w, y + widget->h, th->bg_normal);
    al_draw_rectangle(x + 0.5f, y + 0.5f, x + widget->w - 0.5f, y + widget->h - 0.5f, th->border_normal, 1.0f);
    if (hook->entry == N_GUI_POPUP_ROOT && lv->text) {
        float my = y + row_h + N_GUI_POPUP_SEP_H * s * 0.5f;

        al_draw_text(font, dim, x + pad_x, y + pad_y, 0, lv->text);
        al_draw_line(x + pad_x * 0.5f, my, x + widget->w - pad_x * 0.5f, my, th->border_normal, 1.0f);
    }
    for (r = 0; r < lv->nb_rows; r++) {
        const _N_GUI_POPUP_ENTRY* e = &popup->entries[lv->rows[r]];
        float ry = y + lv->row_y[r];
        int lit = 0;
        ALLEGRO_COLOR ink;

        if (e->kind == _POPUP_KIND_SEP) {
            float my = ry + N_GUI_POPUP_SEP_H * s * 0.5f;

            al_draw_line(x + pad_x * 0.5f, my, x + widget->w - pad_x * 0.5f, my, th->border_normal, 1.0f);
            continue;
        }
        /* lit under the pointer, and while its submenu is open, so the way
         * back to it shows */
        lit = e->enabled && (r == lv->hover || lv->rows[r] == lv->child);
        if (lit) al_draw_filled_rectangle(x + 1.0f, ry, x + widget->w - 1.0f, ry + row_h, th->bg_hover);
        ink = !e->enabled ? dim : (lit ? th->text_hover : th->text_normal);
        if (e->text) al_draw_text(font, ink, x + pad_x, ry + pad_y, 0, e->text);
        if (e->kind == _POPUP_KIND_SUB) {
            /* a small triangle pointing right, drawn rather than typed */
            float ax = x + widget->w - pad_x - 4.0f * s, ay = ry + row_h * 0.5f, a = 3.5f * s;

            al_draw_filled_triangle(ax - a, ay - a, ax - a, ay + a, ax + a * 0.5f, ay, ink);
        }
    }
}

// cppcheck-suppress constParameterCallback ; callback signature must match N_GUI_CUSTOM_MOUSE
static int _popup_mouse(int widget_id, const N_GUI_MOUSE_EVENT* ev, void* user_data) {
    const _N_GUI_POPUP_LEVEL* hook = (const _N_GUI_POPUP_LEVEL*)user_data;
    N_GUI_POPUP* popup = NULL;
    _N_GUI_POPUP_ENTRY* lv = NULL;
    int level = 0, row = -1;

    if (!hook || !hook->popup || !ev) return 0;
    popup = hook->popup;
    level = hook->entry;
    lv = &popup->entries[level];
    row = _popup_row_at(popup, level, ev->y);
    if (ev->type == N_GUI_MOUSE_MOVE) {
        if (row == lv->hover) return 1;
        lv->hover = row;
        n_gui_set_widget_tooltip(popup->ctx, widget_id, row >= 0 ? popup->entries[lv->rows[row]].tip : NULL);
        if (row >= 0) {
            const _N_GUI_POPUP_ENTRY* e = &popup->entries[lv->rows[row]];

            if (e->kind == _POPUP_KIND_SUB && e->enabled) {
                _popup_open_child(popup, level, row);
            } else if (e->kind != _POPUP_KIND_SEP && lv->child >= 0) {
                /* another row of this level: whatever was open beside it
                 * goes. A separator keeps it, so crossing one on the way to
                 * the submenu does not shut it. */
                _popup_close_level(popup, lv->child);
                lv->child = -1;
            }
        }
        n_gui_mark_dirty(popup->ctx);
        return 1;
    }
    if (ev->type == N_GUI_MOUSE_DOWN) {
        /* Either button: a menu opened with the right one is often picked
         * with it too. A press anywhere on a level is the popup's, even on a
         * separator, so it never falls through to what is under it. */
        _popup_activate(popup, level, row);
        return 1;
    }
    return ev->type == N_GUI_MOUSE_UP;
}

N_GUI_POPUP* n_gui_popup_new(N_GUI_CTX* ctx, float scale, N_GUI_POPUP_FN on_pick, void* user_data) {
    N_GUI_POPUP* popup = NULL;

    __n_assert(ctx, return NULL);
    __n_assert(on_pick, return NULL);
    popup = (N_GUI_POPUP*)calloc(1, sizeof(*popup));
    __n_assert(popup, return NULL);
    popup->ctx = ctx;
    popup->scale = scale > 0.0f ? scale : 1.0f;
    popup->on_pick = on_pick;
    popup->user_data = user_data;
    if (_popup_append(popup, _POPUP_KIND_ROOT, -1, NULL, NULL, 0) != N_GUI_POPUP_ROOT) {
        free(popup->entries);
        free(popup);
        return NULL;
    }
    return popup;
}

int n_gui_popup_add_item(N_GUI_POPUP* popup, int parent, const char* text, int tag, const char* tip) {
    if (!_popup_is_level(popup, parent)) return -1;
    return _popup_append(popup, _POPUP_KIND_ITEM, parent, text, tip, tag);
}

int n_gui_popup_add_submenu(N_GUI_POPUP* popup, int parent, const char* text, const char* tip) {
    if (!_popup_is_level(popup, parent)) return -1;
    return _popup_append(popup, _POPUP_KIND_SUB, parent, text, tip, 0);
}

int n_gui_popup_add_separator(N_GUI_POPUP* popup, int parent) {
    if (!_popup_is_level(popup, parent)) return -1;
    return _popup_append(popup, _POPUP_KIND_SEP, parent, NULL, NULL, 0);
}

void n_gui_popup_set_title(N_GUI_POPUP* popup, const char* text) {
    _N_GUI_POPUP_ENTRY* root = NULL;

    if (!popup || !popup->entries) return;
    root = &popup->entries[N_GUI_POPUP_ROOT];
    FreeNoLog(root->text);
    /* a failed copy leaves no heading, which is the only harm it can do */
    if (text && text[0]) root->text = strdup(text);
}

void n_gui_popup_set_shown(N_GUI_POPUP* popup, int entry, int shown) {
    if (!_popup_valid(popup, entry) || entry == N_GUI_POPUP_ROOT) return;
    popup->entries[entry].shown = shown ? 1 : 0;
}

void n_gui_popup_set_enabled(N_GUI_POPUP* popup, int entry, int enabled) {
    if (!_popup_valid(popup, entry) || entry == N_GUI_POPUP_ROOT) return;
    popup->entries[entry].enabled = enabled ? 1 : 0;
}

int n_gui_popup_open(N_GUI_POPUP* popup, float x, float y) {
    if (!popup) return -1;
    n_gui_popup_close(popup);
    return _popup_show_level(popup, N_GUI_POPUP_ROOT, x, y);
}

void n_gui_popup_close(N_GUI_POPUP* popup) {
    if (!popup || !popup->entries) return;
    _popup_close_level(popup, N_GUI_POPUP_ROOT);
    n_gui_mark_dirty(popup->ctx);
}

int n_gui_popup_is_open(const N_GUI_POPUP* popup) {
    if (!popup || !popup->entries || popup->entries[N_GUI_POPUP_ROOT].window < 0) return 0;
    return n_gui_window_is_open(popup->ctx, popup->entries[N_GUI_POPUP_ROOT].window);
}

int n_gui_popup_hit(const N_GUI_POPUP* popup, float x, float y) {
    int level = N_GUI_POPUP_ROOT;

    if (!n_gui_popup_is_open(popup)) return 0;
    /* the open levels are a chain from the root, each one's child */
    while (level >= 0) {
        const _N_GUI_POPUP_ENTRY* lv = &popup->entries[level];

        if (x >= lv->x && x < lv->x + lv->w && y >= lv->y && y < lv->y + lv->h) return 1;
        level = lv->child;
    }
    return 0;
}

int n_gui_popup_window(const N_GUI_POPUP* popup, int level) {
    if (!_popup_is_level(popup, level)) return -1;
    return popup->entries[level].window;
}

int n_gui_popup_press(N_GUI_POPUP* popup, int entry) {
    const _N_GUI_POPUP_ENTRY* lv = NULL;
    int level = 0, r = 0;

    if (!_popup_valid(popup, entry) || entry == N_GUI_POPUP_ROOT) return 0;
    level = popup->entries[entry].parent;
    lv = &popup->entries[level];
    if (lv->window < 0 || !n_gui_window_is_open(popup->ctx, lv->window)) return 0;
    for (r = 0; r < lv->nb_rows; r++) {
        if (lv->rows[r] == entry) return _popup_activate(popup, level, r);
    }
    return 0;
}

void n_gui_popup_free(N_GUI_POPUP** popup) {
    int i = 0;

    if (!popup || !*popup) return;
    n_gui_popup_close(*popup);
    for (i = 0; i < (*popup)->nb_entries; i++) {
        _N_GUI_POPUP_ENTRY* e = &(*popup)->entries[i];

        /* the widgets outlive the handle: hidden, and deaf, so nothing in
         * them reaches the freed state */
        if (e->widget >= 0) {
            n_gui_custom_set_mouse((*popup)->ctx, e->widget, NULL, NULL);
            n_gui_set_widget_visible((*popup)->ctx, e->widget, 0);
        }
        FreeNoLog(e->text);
        FreeNoLog(e->tip);
        FreeNoLog(e->rows);
        FreeNoLog(e->row_y);
        FreeNoLog(e->hook);
    }
    free((*popup)->entries);
    free(*popup);
    *popup = NULL;
}

#endif /* HAVE_ALLEGRO */