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

Tooltip bubble regression (headless): an in-app window does not narrow the bubble, style.tooltip_max_w caps how wide it runs, and a window opened over the pointer hides it instead of being drawn over.

Tooltip bubble regression (headless): an in-app window does not narrow the bubble, style.tooltip_max_w caps how wide it runs, and a window opened over the pointer hides it instead of being drawn over.

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
*/
#define ALLEGRO_UNSTABLE 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "nilorea/n_log.h"
#include "nilorea/n_gui.h"
/* wide enough that an uncapped bubble can outrun style.tooltip_max_w */
#define TT_W 1200
#define TT_H 400
/* the panel is deliberately tiny: it is the "70 px HUD gauge" shape that used to
squeeze the bubble down to a few characters per line */
#define PANEL_X 10.0f
#define PANEL_W 80.0f
/* window holding the split pane whose divider band is the only tooltip target */
#define SPLIT_X 400.0f
#define SPLIT_Y 100.0f
#define SPLIT_W 200.0f
#define SPLIT_H 120.0f
static int failures = 0;
static void expect_true(const char* label, int cond) {
if (!cond) {
n_log(LOG_ERR, "%s: condition false", label);
}
}
static void usage(void) {
fprintf(stderr,
" -v version\n"
" -V log level: LOG_NULL, LOG_NOTICE, LOG_INFO, LOG_ERR, LOG_DEBUG\n"
" -h help\n");
}
/* the example is quiet by default (LOG_ERR); -V raises it, which is what
run_tests.sh passes. */
static void process_args(int argc, char** argv) {
int getoptret = 0;
while ((getoptret = getopt(argc, argv, "hvV:")) != EOF) {
switch (getoptret) {
case 'v':
fprintf(stderr, "build date: %s %s\n", __DATE__, __TIME__);
exit(0);
case 'V':
if (!strcmp("LOG_NULL", optarg))
else if (!strcmp("LOG_NOTICE", optarg))
else if (!strcmp("LOG_INFO", optarg))
else if (!strcmp("LOG_ERR", optarg))
else if (!strcmp("LOG_DEBUG", optarg))
else {
fprintf(stderr, "%s is not a valid log level\n", optarg);
exit(1);
}
break;
case 'h':
usage();
exit(0);
default:
case '?':
if (optopt == 'V') fprintf(stderr, "\n Missing log level\n");
usage();
exit(1);
}
}
}
/* Render the whole GUI into target, with the tooltip either armed or not. The
bubble is found by differencing the two renders, so nothing here depends on
the bubble's colours or on how its corners are rasterized. */
static void render(N_GUI_CTX* gui, ALLEGRO_BITMAP* target, int armed, int widget_id) {
al_set_target_bitmap(target);
al_clear_to_color(al_map_rgb(0, 0, 0));
gui->tooltip_widget_id = armed ? widget_id : -1;
if (armed) {
/* pretend the hover happened long enough ago that the delay has elapsed */
gui->tooltip_armed_at = al_get_time() - (double)gui->style.tooltip_delay - 1.0;
}
}
/* Width in pixels of the region that appears only when the tooltip is armed,
i.e. the bubble. Returns 0 when the two renders are identical. */
static int bubble_width(ALLEGRO_BITMAP* a, ALLEGRO_BITMAP* b) {
ALLEGRO_LOCKED_REGION* la = al_lock_bitmap(a, ALLEGRO_PIXEL_FORMAT_ABGR_8888_LE, ALLEGRO_LOCK_READONLY);
ALLEGRO_LOCKED_REGION* lb = al_lock_bitmap(b, ALLEGRO_PIXEL_FORMAT_ABGR_8888_LE, ALLEGRO_LOCK_READONLY);
int min_x = TT_W, max_x = -1;
if (la && lb) {
for (int y = 0; y < TT_H; y++) {
const unsigned char* ra = (const unsigned char*)la->data + (ptrdiff_t)y * la->pitch;
const unsigned char* rb = (const unsigned char*)lb->data + (ptrdiff_t)y * lb->pitch;
for (int x = 0; x < TT_W; x++) {
if (memcmp(ra + (ptrdiff_t)x * 4, rb + (ptrdiff_t)x * 4, 4) != 0) {
if (x < min_x) min_x = x;
if (x > max_x) max_x = x;
}
}
}
}
if (la) al_unlock_bitmap(a);
if (lb) al_unlock_bitmap(b);
return (max_x < 0) ? 0 : (max_x - min_x + 1);
}
/* Move the pointer to (x,y) through a real mouse event and report which widget
the arming scan picked, so the test exercises n_gui_process_event rather than
the tooltip_widget_id field it maintains. */
static int hover_tooltip_at(N_GUI_CTX* gui, int x, int y) {
ALLEGRO_EVENT ev;
memset(&ev, 0, sizeof(ev));
ev.type = ALLEGRO_EVENT_MOUSE_AXES;
ev.mouse.x = x;
ev.mouse.y = y;
return gui->tooltip_widget_id;
}
int main(int argc, char** argv) {
process_args(argc, argv);
if (!al_init() || !al_init_font_addon()) {
n_log(LOG_ERR, "allegro init failed");
return 1;
}
al_init_primitives_addon();
/* memory bitmaps: the whole test runs without a display, like ex_gui_kvtable */
al_set_new_bitmap_flags(ALLEGRO_MEMORY_BITMAP);
ALLEGRO_BITMAP* plain = al_create_bitmap(TT_W, TT_H);
ALLEGRO_BITMAP* tipped = al_create_bitmap(TT_W, TT_H);
ALLEGRO_FONT* font = al_create_builtin_font();
if (!plain || !tipped || !font) {
n_log(LOG_ERR, "bitmap or font creation failed");
return 1;
}
if (!gui) {
n_log(LOG_ERR, "n_gui_new_ctx failed");
return 1;
}
n_gui_set_display_size(gui, (float)TT_W, (float)TT_H);
int win = n_gui_add_window(gui, "HUD", PANEL_X, 10.0f, PANEL_W, 60.0f);
int btn = n_gui_add_button(gui, win, "?", 5.0f, 5.0f, 30.0f, 20.0f, N_GUI_SHAPE_RECT, NULL, NULL);
expect_true("window created", win >= 0);
expect_true("button created", btn >= 0);
/* long enough that its natural width outruns both the panel and the cap */
"Promote this panel to a real OS window, and back again, "
"which is the sort of sentence a tooltip has to hold when it "
"explains something instead of restating the label beside it.");
/* The middle of that button, in display coordinates: the client area of a
titled window starts below its title bar, so 30,40 is the title bar and
not the button. The blocks that drive the arming scan through real mouse
motion need the real thing. */
const int btn_x = (int)(PANEL_X + 5.0f + 15.0f);
const int btn_y = (int)(10.0f + gui->style.titlebar_h + 5.0f + 10.0f);
/* cursor inside the tiny panel, with room to the right for the bubble */
gui->mouse_x = 30;
gui->mouse_y = 40;
/* An in-app window must not narrow the bubble. Before the fix the bubble was
clamped to the widget's window rect, so it came out no wider than the 80px
panel and the sentence wrapped to a few characters per line. */
{
render(gui, plain, 0, btn);
render(gui, tipped, 1, btn);
int w = bubble_width(plain, tipped);
expect_true("tooltip is drawn at all", w > 0);
expect_true("bubble is not narrowed to the in-app window", (float)w > PANEL_W);
n_log(LOG_INFO, "in-app bubble width: %d px (panel is %.0f px)", w, (double)PANEL_W);
}
/* style.tooltip_max_w caps it: the bubble stays near the cap, not the display */
{
gui->style.tooltip_max_w = 480.0f;
render(gui, plain, 0, btn);
render(gui, tipped, 1, btn);
int capped = bubble_width(plain, tipped);
/* text width + the bubble's 8px horizontal padding each side, plus a pixel
of slack for the rounded border */
expect_true("bubble honours tooltip_max_w", capped <= 480 + 16 + 2);
expect_true("bubble still uses the width it is allowed", capped > 480 / 2);
/* 0 disables the cap, and the same tooltip then runs far wider */
gui->style.tooltip_max_w = 0.0f;
render(gui, plain, 0, btn);
render(gui, tipped, 1, btn);
int uncapped = bubble_width(plain, tipped);
expect_true("tooltip_max_w = 0 restores wrap-at-the-clamp-edge", uncapped > capped);
n_log(LOG_INFO, "capped %d px, uncapped %d px", capped, uncapped);
gui->style.tooltip_max_w = 480.0f;
}
/* a widget with no tooltip draws nothing extra */
{
int bare = n_gui_add_button(gui, win, "x", 40.0f, 5.0f, 20.0f, 20.0f, N_GUI_SHAPE_RECT, NULL, NULL);
render(gui, plain, 0, bare);
render(gui, tipped, 1, bare);
expect_true("no tooltip text means no bubble", bubble_width(plain, tipped) == 0);
}
/* A split pane spans the whole region it divides, so only its divider band
arms its tooltip: taking the whole rect would answer for every
widget-free pixel of both panes. This drives the arming scan through
real mouse motion rather than poking tooltip_widget_id. */
{
int spwin = n_gui_add_window(gui, "Split", SPLIT_X, SPLIT_Y, SPLIT_W, SPLIT_H);
int sp = n_gui_add_splitpane(gui, spwin, 0.0f, 0.0f, SPLIT_W, SPLIT_H,
N_GUI_SPLIT_VERTICAL, 0.5f, NULL, NULL);
expect_true("split pane created", sp >= 0);
n_gui_set_widget_tooltip(gui, sp, "Drag to resize");
/* client-area origin of a titled window: y is below the title bar */
float ox = SPLIT_X;
float oy = SPLIT_Y + gui->style.titlebar_h;
int divider_x = (int)(ox + SPLIT_W * 0.5f);
expect_true("pointer on the divider arms the split pane",
hover_tooltip_at(gui, divider_x, (int)(oy + SPLIT_H * 0.5f)) == sp);
expect_true("pointer well left of the divider arms nothing",
hover_tooltip_at(gui, (int)(ox + 10.0f), (int)(oy + SPLIT_H * 0.5f)) == -1);
expect_true("pointer well right of the divider arms nothing",
hover_tooltip_at(gui, (int)(ox + SPLIT_W - 10.0f), (int)(oy + SPLIT_H * 0.5f)) == -1);
/* a widget sitting over a pane still gets its own tooltip */
int over = n_gui_add_button(gui, spwin, "b", 10.0f, 10.0f, 30.0f, 20.0f, N_GUI_SHAPE_RECT, NULL, NULL);
n_gui_set_widget_tooltip(gui, over, "on top of the pane");
expect_true("a widget above the pane still arms its own tooltip",
hover_tooltip_at(gui, (int)(ox + 20.0f), (int)(oy + 20.0f)) == over);
}
/* A window opened over the armed widget hides the bubble. The bubble is
drawn last and over everything, and nothing re-runs the arming scan when
a window opens without the pointer moving, so it used to be painted over
the window that had just appeared - a context menu on a right click, with
its first entries covered until the pointer moved. */
{
int menu = 0;
gui->mouse_x = btn_x;
gui->mouse_y = btn_y;
render(gui, plain, 0, btn);
render(gui, tipped, 1, btn);
expect_true("the bubble shows with nothing over it", bubble_width(plain, tipped) > 0);
/* over the pointer, and above the panel: a pop-up sorts in front */
menu = n_gui_add_window(gui, "Menu", 20.0f, 30.0f, 120.0f, 90.0f);
expect_true("menu window created", menu >= 0);
expect_true("nothing is pending while a window covers the pointer",
render(gui, plain, 0, btn);
render(gui, tipped, 1, btn);
expect_true("and no bubble is drawn over it", bubble_width(plain, tipped) == 0);
/* the same window somewhere else leaves the bubble alone */
n_gui_window_set_rect(gui, menu, 600.0f, 200.0f, 120.0f, 90.0f);
render(gui, plain, 0, btn);
render(gui, tipped, 1, btn);
expect_true("a window that does not cover the pointer changes nothing",
bubble_width(plain, tipped) > 0);
/* and closing it brings the bubble back where it was */
n_gui_window_set_rect(gui, menu, 20.0f, 30.0f, 120.0f, 90.0f);
render(gui, plain, 0, btn);
render(gui, tipped, 1, btn);
expect_true("a closed window covers nothing", bubble_width(plain, tipped) > 0);
}
/* Any button dismisses an armed tooltip, not only the first. A right click
is what opens a context menu, and it used to leave the bubble armed. */
{
ALLEGRO_EVENT ev;
int button = 0;
for (button = 1; button <= 3; button++) {
expect_true("the pointer arms the button's tooltip",
hover_tooltip_at(gui, btn_x, btn_y) == btn);
memset(&ev, 0, sizeof(ev));
ev.type = ALLEGRO_EVENT_MOUSE_BUTTON_DOWN;
ev.mouse.button = (unsigned)button;
ev.mouse.x = btn_x;
ev.mouse.y = btn_y;
expect_true("and a press on any button dismisses it", gui->tooltip_widget_id == -1);
memset(&ev, 0, sizeof(ev));
ev.type = ALLEGRO_EVENT_MOUSE_BUTTON_UP;
ev.mouse.button = (unsigned)button;
ev.mouse.x = btn_x;
ev.mouse.y = btn_y;
}
}
al_destroy_font(font);
al_destroy_bitmap(plain);
al_destroy_bitmap(tipped);
if (failures) {
n_log(LOG_ERR, "ex_gui_tooltip: %d failure(s)", failures);
return 1;
}
n_log(LOG_NOTICE, "ex_gui_tooltip: all checks passed");
return 0;
}
static void usage(void)
void process_args(int argc, char **argv)
static int failures
Definition ex_atlas.c:51
int main(void)
Definition ex_atlas.c:117
static void expect_true(const char *label, int cond)
Definition ex_atlas.c:53
static int log_level
int getoptret
Definition ex_fluid.c:59
static N_GUI_CTX * gui
#define SPLIT_H
#define SPLIT_X
#define TT_H
#define TT_W
#define SPLIT_W
#define PANEL_W
#define PANEL_X
#define SPLIT_Y
static int bubble_width(ALLEGRO_BITMAP *a, ALLEGRO_BITMAP *b)
static int hover_tooltip_at(N_GUI_CTX *gui, int x, int y)
static void render(N_GUI_CTX *gui, ALLEGRO_BITMAP *target, int armed, int widget_id)
FONT * font
the default 8x8 font, valid after allegro_init()
void n_gui_set_display_size(N_GUI_CTX *ctx, float w, float h)
set the display (viewport) size so global scrollbars can be computed.
Definition n_gui.c:12348
void n_gui_set_widget_tooltip(N_GUI_CTX *ctx, int widget_id, const char *text)
Set (or clear) the tooltip text of any widget.
Definition n_gui.c:2880
void n_gui_window_set_rect(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h)
Move and resize a window at runtime, reflowing its widgets.
Definition n_gui.c:12196
int n_gui_process_event(N_GUI_CTX *ctx, ALLEGRO_EVENT event)
Process an allegro event through the GUI system.
Definition n_gui.c:13611
void n_gui_close_window(N_GUI_CTX *ctx, int window_id)
Close (hide) a window.
Definition n_gui.c:1406
int n_gui_tooltip_pending(N_GUI_CTX *ctx)
Report the tooltip phase.
Definition n_gui.c:2970
void n_gui_window_set_zorder(N_GUI_CTX *ctx, int window_id, int z_mode, int z_value)
Set window z-order mode and value.
Definition n_gui.c:2403
int n_gui_add_splitpane(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h, int orientation, float ratio, void(*on_change)(int, float, void *), void *user_data)
Add a split pane widget: a draggable divider between two regions.
Definition n_gui.c:3331
#define N_GUI_WIN_FRAMELESS
frameless window: no title bar drawn, drag via window body unless N_GUI_WIN_FIXED_POSITION is also se...
Definition n_gui.h:276
void n_gui_draw(N_GUI_CTX *ctx)
draw all visible GUI windows and widgets
Definition n_gui.c:11735
int n_gui_add_window(N_GUI_CTX *ctx, const char *title, float x, float y, float w, float h)
Add a new pseudo-window to the context.
Definition n_gui.c:1277
N_GUI_CTX * n_gui_new_ctx(ALLEGRO_FONT *default_font)
Create a new GUI context.
Definition n_gui.c:1103
#define N_GUI_SPLIT_VERTICAL
split pane: vertical divider, regions are left and right
Definition n_gui.h:146
void n_gui_destroy_ctx(N_GUI_CTX **ctx)
Destroy a GUI context and all its windows/widgets.
Definition n_gui.c:1209
#define N_GUI_ZORDER_POPUP
popup/modal dialogs: drawn above EVERYTHING including ALWAYS_ON_TOP windows.
Definition n_gui.h:369
void n_gui_window_set_flags(N_GUI_CTX *ctx, int window_id, int flags)
Set feature flags on a window.
Definition n_gui.c:2515
#define N_GUI_SHAPE_RECT
rectangle shape (default)
Definition n_gui.h:170
int n_gui_add_button(N_GUI_CTX *ctx, int window_id, const char *label, float x, float y, float w, float h, int shape, void(*on_click)(int, void *), void *user_data)
Add a button widget to a window.
Definition n_gui.c:2720
The top-level GUI context that holds all windows.
Definition n_gui.h:1669
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:113
#define LOG_DEBUG
debug-level messages
Definition n_log.h:108
#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
#define LOG_NULL
no log output
Definition n_log.h:70
#define LOG_INFO
informational
Definition n_log.h:106
Common headers and low-level functions & define.
GUI system: buttons, sliders, text areas, checkboxes, scrollbars, dropdown menus, windows.
Generic log system.