Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 78 additions & 5 deletions demo/common/overview.c
Original file line number Diff line number Diff line change
Expand Up @@ -839,12 +839,85 @@ overview(struct nk_context *ctx)
} else popup_active = nk_false;
}

/* tooltip */
nk_layout_row_static(ctx, 30, 150, 1);
/* tooltips */
nk_layout_row_static(ctx, 30, 400, 1);
bounds = nk_widget_bounds(ctx);
nk_label(ctx, "Hover me for tooltip", NK_TEXT_LEFT);
if (nk_input_is_mouse_hovering_rect(in, bounds))
Copy link
Copy Markdown
Contributor Author

@sleeptightAnsiC sleeptightAnsiC Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering, how do we fix the bug, where hovering a rect always triggers the tooltip to appear, even though there is another nk_window above it? [screenshot below]

I did not attempt to fix it as I thought maybe you guys know a more reliable way to trigger these? My idea was to simply check, if this branch runs on currently active nk_window, but I'm not so sure about it.

Image

EDIT: for comparison, "chart" does not have this issue because it activates tooltip based on active chart index:

image image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is similar to the bug where a scrollbar will activate (when you have AUTO_HIDE on) even if your mouse is in a popup on in front of the window.

Copy link
Copy Markdown
Contributor Author

@sleeptightAnsiC sleeptightAnsiC Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a scrollbar will activate (when you have AUTO_HIDE on) even if your mouse is in a popup on in front of the window

Hmn... Tried to reproduce this with nk_window that comes with demo/overview but I couldn't. Though the scrollbar does not seem to be hiding as it should, so maybe I'm doing something wrong.

However, this does feel like the same bug. I've checked the code and it seems to be doing a similar thing:

if (nk_input_is_mouse_hovering_rect(in, *scroll))
*state = NK_WIDGET_STATE_HOVERED;

If you could provide me with a repro I would just move it into a different task. This really feels like a bigger problem.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tested and I get the bug at least with the sdl2 and sd3_renderer backends with the standard demo. Here's a video of the sdl2 renderer, sorry it doesn't record my cursor but I swear I keep it in the menu popup and the second window for the test.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screencast.from.2026-02-21.14-30-52.mp4

Forgot the video, oops.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few notes:

There is nk_widget_is_hovered but from what I see it needs active layout (?) and grepping for this shows no use inside of the repo. Strange...

NK_API nk_bool
nk_widget_is_hovered(const struct nk_context *ctx)
{
struct nk_rect c, v;
struct nk_rect bounds;
NK_ASSERT(ctx);
NK_ASSERT(ctx->current);
NK_ASSERT(ctx->current->layout);
if (!ctx || !ctx->current || !ctx->current->layout || (ctx->active != ctx->current && !((int)ctx->current->layout->type & (int)NK_PANEL_SET_POPUP)))
return 0;
c = ctx->current->layout->clip;
c.x = (float)((int)c.x);
c.y = (float)((int)c.y);
c.w = (float)((int)c.w);
c.h = (float)((int)c.h);
nk_layout_peek(&bounds, ctx);
nk_unify(&v, &c, bounds.x, bounds.y, bounds.x + bounds.w, bounds.y + bounds.h);
if (!NK_INTERSECT(c.x, c.y, c.w, c.h, bounds.x, bounds.y, bounds.w, bounds.h))
return 0;
return nk_input_is_mouse_hovering_rect(&ctx->input, bounds);
}

On the other hand, the chart is storing its own hover state based on NK_WINDOW_ROM (which few other widgets also seem to be doing) and given how this flag was documented, this seems to be a hack, like it expects that active windows will always have this flag NOT set (which is correct but hacky)

NK_WINDOW_ROM = NK_FLAG(12), /**< sets window widgets into a read only mode and does not allow input changes */

Nuklear/src/nuklear_chart.c

Lines 173 to 181 in 7295164

/* user selection of current data point */
if (!(layout->flags & NK_WINDOW_ROM)) {
if (nk_input_is_mouse_hovering_rect(i, bounds)) {
ret = NK_CHART_HOVERING;
ret |= (!i->mouse.buttons[NK_BUTTON_LEFT].down &&
i->mouse.buttons[NK_BUTTON_LEFT].clicked) ? NK_CHART_CLICKED: 0;
color = g->slots[slot].highlight;
}
}

I think, this is a refactor angle... Right now, every widget is trying to reimplement the wheel...

nk_tooltip(ctx, "This is a tooltip");
nk_label(ctx, "Hover for default tooltip", NK_TEXT_LEFT);
if (nk_input_is_mouse_hovering_rect(in, bounds)) {
nk_tooltip(ctx, "This is very boring default tooltip...");
}
bounds = nk_widget_bounds(ctx);
nk_label(ctx, "Hover for Gnome-like tooltip", NK_TEXT_LEFT);
if (nk_input_is_mouse_hovering_rect(in, bounds)) {
struct nk_vec2 offset = { 0, 15 };
nk_tooltip_offset(ctx, "Gnome centers above cursor with greater y offset", NK_TOOLTIP_ABOVE, offset);
}
bounds = nk_widget_bounds(ctx);
nk_label(ctx, "Hover for above-on-left tooltip", NK_TEXT_LEFT);
if (nk_input_is_mouse_hovering_rect(in, bounds)) {
struct nk_vec2 offset = { 0, 0 };
nk_tooltip_offset(ctx, "above-on-left from cursor with 0:0 offset", NK_TOOLTIP_ABOVE|NK_TOOLTIP_ON_LEFT, offset);
}
bounds = nk_widget_bounds(ctx);
nk_label(ctx, "Hover for MAGIC!", NK_TEXT_LEFT);
if (nk_input_is_mouse_hovering_rect(in, bounds)) {
static double accum_time_seconds = 0.0;
const double speed = 3.0, radius = 50.0;
struct nk_vec2 offset;
offset.x = radius * cos(accum_time_seconds * speed);
offset.y = radius * sin(accum_time_seconds * speed);
nk_tooltip_offset(ctx, "WOW!", NK_TOOLTIP_ABS_OFFSET, offset);
accum_time_seconds += (double)(ctx->delta_time_seconds);
}

/* editor for custom tooltip */
{
static char text_buf[64] = {0};
static int text_len = 0;
static int text_initialized = 0;
static nk_flags tooltip = 0;
static struct nk_vec2 offset = {0};
if (!text_initialized) {
const char text_default[] = "you can customize this!";
NK_ASSERT(sizeof(text_default) < sizeof(text_buf));
memcpy(text_buf, text_default, sizeof(text_default));
text_len = sizeof(text_default) - 1;
text_initialized = 1;
}
bounds = nk_widget_bounds(ctx);
nk_label(ctx, "Hover for custom tooltip (you can customize it below)", NK_TEXT_LEFT);
if (nk_input_is_mouse_hovering_rect(in, bounds)) {
nk_tooltip_offset(ctx, text_buf, tooltip, offset);
}
nk_layout_row_dynamic(ctx, 1, 1);
nk_rule_horizontal(ctx, nk_white, nk_true);
nk_layout_row_dynamic(ctx, 30, 2);
nk_label(ctx, "custom tooltip text:", NK_TEXT_LEFT);
nk_edit_string(ctx, NK_EDIT_FIELD, text_buf, &text_len, sizeof(text_buf), nk_filter_default);
NK_ASSERT(text_len < (int)sizeof(text_buf));
text_buf[text_len] = '\0'; /* TODO: why nk_edit_string is NOT setting this on its own? */
Comment on lines +896 to +898
Copy link
Copy Markdown
Contributor Author

@sleeptightAnsiC sleeptightAnsiC Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(from bottom of #900 (comment))

PS, I did run into some weird behavior. When I clicked on a property, I got focus and a cursor on your edit string as well and have to click the property again to actually have it accept input. Very strange.

I think I've also seen this. I'm not entirely sure where this issue comes from (maybe two widgets get the same hash?) but given the fact that I was already confused by nk_edit_string behavior (the todo note), maybe I did something wrong here.

nk_layout_row_dynamic(ctx, 30, 1);
#define TOOLTIP_FOREACH_FLAG(BODY) \
BODY(ABOVE) \
BODY(BELOW) \
BODY(ON_LEFT) \
BODY(ON_RIGHT) \
BODY(ABS_OFFSET)
#define TOOLTIP_CHECKBOX_FLAG(FLAG) \
{ \
nk_bool checked = !!(tooltip & NK_TOOLTIP_##FLAG); \
nk_checkbox_label_align(ctx, "custom tooltip flag: " #FLAG, &checked, NK_WIDGET_RIGHT, NK_TEXT_LEFT);\
tooltip = checked ? (tooltip | NK_TOOLTIP_##FLAG) : (tooltip & ~NK_TOOLTIP_##FLAG); \
}
TOOLTIP_FOREACH_FLAG(TOOLTIP_CHECKBOX_FLAG)
#undef TOOLTIP_FOREACH_FLAG
#undef TOOLTIP_CHECKBOX_FLAG
Comment on lines +899 to +914
Copy link
Copy Markdown
Contributor Author

@sleeptightAnsiC sleeptightAnsiC Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this XMACRO is a bit overcomplicated. I wanted to save on space. Not sure, if it was worth it. (note: this can be changed to array and for-loop, maybe? but it would be a ton of code)

nk_layout_row_dynamic(ctx, 30, 2);
nk_label(ctx, "custom tooltip offset", NK_TEXT_LEFT);
nk_property_float(ctx, "x", -100.0f, &offset.x, 100.0f, 5.0f, 0.5f);
nk_label(ctx, "custom tooltip offset", NK_TEXT_LEFT);
nk_property_float(ctx, "y", -100.0f, &offset.y, 100.0f, 5.0f, 0.5f);
}

nk_tree_pop(ctx);
}
Expand Down
108 changes: 100 additions & 8 deletions nuklear.h
Original file line number Diff line number Diff line change
Expand Up @@ -3826,12 +3826,27 @@ NK_API void nk_contextual_end(struct nk_context*);
* TOOLTIP
*
* ============================================================================= */
enum nk_tooltip_flags {
/**!< tells where tooltip should appear relatively to the cursor, can be combined (e.g. BELOW|ON_RIGHT) */
NK_TOOLTIP_ABOVE = NK_FLAG(0),
NK_TOOLTIP_BELOW = NK_FLAG(1),
NK_TOOLTIP_ON_LEFT = NK_FLAG(2),
NK_TOOLTIP_ON_RIGHT = NK_FLAG(3),
/**!< if set, the offset will be absolute, instead of relative */
NK_TOOLTIP_ABS_OFFSET = NK_FLAG(4)
/* FIXME: https://github.com/Immediate-Mode-UI/Nuklear/issues/899 */
/*NK_TOOLTIP_CLAMP_IN_SCREEN = NK_FLAG(5),*/
};
NK_API void nk_tooltip(struct nk_context*, const char*);
NK_API void nk_tooltip_offset(struct nk_context *ctx, const char *text, nk_flags tooltip, struct nk_vec2);
#ifdef NK_INCLUDE_STANDARD_VARARGS
NK_API void nk_tooltipf(struct nk_context*, NK_PRINTF_FORMAT_STRING const char*, ...) NK_PRINTF_VARARG_FUNC(2);
NK_API void nk_tooltipfv(struct nk_context*, NK_PRINTF_FORMAT_STRING const char*, va_list) NK_PRINTF_VALIST_FUNC(2);
NK_API void nk_tooltipf_offset(struct nk_context*, nk_flags tooltip, struct nk_vec2, NK_PRINTF_FORMAT_STRING const char*, ...) NK_PRINTF_VARARG_FUNC(4);
NK_API void nk_tooltipfv_offset(struct nk_context*, nk_flags tooltip, struct nk_vec2, NK_PRINTF_FORMAT_STRING const char*, va_list) NK_PRINTF_VALIST_FUNC(4);
#endif
NK_API nk_bool nk_tooltip_begin(struct nk_context*, float width);
NK_API nk_bool nk_tooltip_begin_offset(struct nk_context*, float width, nk_flags tooltip, struct nk_vec2);
NK_API void nk_tooltip_end(struct nk_context*);
/* =============================================================================
*
Expand Down Expand Up @@ -30603,10 +30618,40 @@ nk_combobox_callback(struct nk_context *ctx,
* TOOLTIP
*
* ===============================================================*/
NK_LIB struct nk_vec2
nk_tooltip_get_default_offset(const struct nk_context *ctx)
{
struct nk_vec2 offset = {0};
NK_ASSERT(ctx);
if (!ctx) return offset;
if (ctx->style.cursor_active) {
/* nuklear is drawing its own cursor so we can reuse its size (best case!) */
offset.x = ctx->style.window.padding.x + ctx->style.cursor_active->size.x;
offset.x = ctx->style.window.padding.y + ctx->style.cursor_active->size.y;
} else if (ctx->style.font) {
/* assume that cursor size is similar to font height (flawed but reasonable)*/
offset.y = ctx->style.window.padding.x + ctx->style.font->height;
offset.x = ctx->style.window.padding.y + ctx->style.font->height;
}
return offset;
}
NK_LIB nk_flags
nk_tooltip_get_default_flags(const struct nk_context *ctx)
{
NK_UNUSED(ctx);
return NK_TOOLTIP_BELOW|NK_TOOLTIP_ON_RIGHT;
}
NK_API nk_bool
nk_tooltip_begin(struct nk_context *ctx, float width)
{
int x,y,w,h;
return nk_tooltip_begin_offset(ctx, width,
nk_tooltip_get_default_flags(ctx),
nk_tooltip_get_default_offset(ctx));
}
NK_API nk_bool
nk_tooltip_begin_offset(struct nk_context *ctx, float width, nk_flags tooltip, struct nk_vec2 offset)
{
int x,y,w,h,mul_x,mul_y;
struct nk_window *win;
const struct nk_input *in;
struct nk_rect bounds;
Expand All @@ -30625,14 +30670,38 @@ nk_tooltip_begin(struct nk_context *ctx, float width)
return 0;

w = nk_iceilf(width);
h = nk_iceilf(nk_null_rect.h);
x = nk_ifloorf(in->mouse.pos.x + 1) - (int)win->layout->clip.x;
y = nk_ifloorf(in->mouse.pos.y + 1) - (int)win->layout->clip.y;
h = ctx->current->layout->row.min_height;

/* find axis multipliers based on bitmask state */
mul_x = 0;
mul_x -= !!(tooltip & NK_TOOLTIP_ON_LEFT );
mul_x += !!(tooltip & NK_TOOLTIP_ON_RIGHT);
NK_ASSERT(mul_x == -1 || mul_x == 0 || mul_x == 1);
mul_y = 0;
mul_y -= !!(tooltip & NK_TOOLTIP_ABOVE);
mul_y += !!(tooltip & NK_TOOLTIP_BELOW);
NK_ASSERT(mul_y == -1 || mul_y == 0 || mul_y == 1);

/* turn relative offset into absolute, unless it's already absolute
* notice that offset axis is ignored in cases where mul==0
* (if you don't like this behavior, make sure to use ABS_OFFSET flag)*/
if (!(tooltip & NK_TOOLTIP_ABS_OFFSET)) {
offset.x *= mul_x;
offset.y *= mul_y;
}

/* find origin */
x = -w/2 + (mul_x * w/2);
x += nk_ifloorf(in->mouse.pos.x + 1) - (int)win->layout->clip.x;
x += offset.x;
y = -h/2 + (mul_y * h/2);
y += nk_ifloorf(in->mouse.pos.y + 1) - (int)win->layout->clip.y;
y += offset.y;

bounds.x = (float)x;
bounds.y = (float)y;
bounds.w = (float)w;
bounds.h = (float)h;
bounds.h = (float)nk_iceilf(nk_null_rect.h);

ret = nk_popup_begin(ctx, NK_POPUP_DYNAMIC,
"__##Tooltip##__", NK_WINDOW_NO_SCROLLBAR|NK_WINDOW_BORDER, bounds);
Expand All @@ -30641,7 +30710,6 @@ nk_tooltip_begin(struct nk_context *ctx, float width)
ctx->current->layout->type = NK_PANEL_TOOLTIP;
return ret;
}

NK_API void
nk_tooltip_end(struct nk_context *ctx)
{
Expand All @@ -30653,7 +30721,7 @@ nk_tooltip_end(struct nk_context *ctx)
nk_popup_end(ctx);
}
NK_API void
nk_tooltip(struct nk_context *ctx, const char *text)
nk_tooltip_offset(struct nk_context *ctx, const char *text, nk_flags tooltip, struct nk_vec2 offset)
{
const struct nk_style *style;
struct nk_vec2 padding;
Expand Down Expand Up @@ -30681,14 +30749,29 @@ nk_tooltip(struct nk_context *ctx, const char *text)
text_height = (style->font->height + 2 * padding.y);

/* execute tooltip and fill with text */
if (nk_tooltip_begin(ctx, (float)text_width)) {
if (nk_tooltip_begin_offset(ctx, (float)text_width, tooltip, offset)) {
nk_layout_row_dynamic(ctx, (float)text_height, 1);
nk_text(ctx, text, text_len, NK_TEXT_LEFT);
nk_tooltip_end(ctx);
}
}
NK_API void
nk_tooltip(struct nk_context *ctx, const char *text)
{
nk_tooltip_offset(ctx, text,
nk_tooltip_get_default_flags(ctx),
nk_tooltip_get_default_offset(ctx));
}
#ifdef NK_INCLUDE_STANDARD_VARARGS
NK_API void
nk_tooltipf_offset(struct nk_context *ctx, nk_flags tooltip, struct nk_vec2 offset, const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
nk_tooltipfv_offset(ctx, tooltip, offset, fmt, args);
va_end(args);
}
NK_API void
nk_tooltipf(struct nk_context *ctx, const char *fmt, ...)
{
va_list args;
Expand All @@ -30697,6 +30780,13 @@ nk_tooltipf(struct nk_context *ctx, const char *fmt, ...)
va_end(args);
}
NK_API void
nk_tooltipfv_offset(struct nk_context *ctx, nk_flags tooltip, struct nk_vec2 offset, const char *fmt, va_list args)
{
char buf[256];
nk_strfmt(buf, NK_LEN(buf), fmt, args);
nk_tooltip_offset(ctx, buf, tooltip, offset);
}
NK_API void
nk_tooltipfv(struct nk_context *ctx, const char *fmt, va_list args)
{
char buf[256];
Expand Down Expand Up @@ -30762,6 +30852,8 @@ nk_tooltipfv(struct nk_context *ctx, const char *fmt, va_list args)
/// - [y]: Minor version with non-breaking API and library changes
/// - [z]: Patch version with no direct changes to the API
///
/// - 2026/02/17 (4.13.3) - Fixed default tooltip from being covered by the cursor,
/// added new tooltip variation that allows to set the offset manually
/// - 2026/01/31 (4.13.2) - Fix: replace incorrect static asserts for size(nk_bool)
/// - 2026/01/26 (4.13.1) - Fix: nk_do_property now uses NK_STRTOD via macro
/// - Fix: failure to build from source, due to
Expand Down
2 changes: 2 additions & 0 deletions src/CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
/// - [y]: Minor version with non-breaking API and library changes
/// - [z]: Patch version with no direct changes to the API
///
/// - 2026/02/17 (4.13.3) - Fixed default tooltip from being covered by the cursor,
/// added new tooltip variation that allows to set the offset manually
/// - 2026/01/31 (4.13.2) - Fix: replace incorrect static asserts for size(nk_bool)
/// - 2026/01/26 (4.13.1) - Fix: nk_do_property now uses NK_STRTOD via macro
/// - Fix: failure to build from source, due to
Expand Down
15 changes: 15 additions & 0 deletions src/nuklear.h
Original file line number Diff line number Diff line change
Expand Up @@ -3603,12 +3603,27 @@ NK_API void nk_contextual_end(struct nk_context*);
* TOOLTIP
*
* ============================================================================= */
enum nk_tooltip_flags {
/**!< tells where tooltip should appear relatively to the cursor, can be combined (e.g. BELOW|ON_RIGHT) */
NK_TOOLTIP_ABOVE = NK_FLAG(0),
NK_TOOLTIP_BELOW = NK_FLAG(1),
NK_TOOLTIP_ON_LEFT = NK_FLAG(2),
NK_TOOLTIP_ON_RIGHT = NK_FLAG(3),
/**!< if set, the offset will be absolute, instead of relative */
NK_TOOLTIP_ABS_OFFSET = NK_FLAG(4)
/* FIXME: https://github.com/Immediate-Mode-UI/Nuklear/issues/899 */
/*NK_TOOLTIP_CLAMP_IN_SCREEN = NK_FLAG(5),*/
};
NK_API void nk_tooltip(struct nk_context*, const char*);
NK_API void nk_tooltip_offset(struct nk_context *ctx, const char *text, nk_flags tooltip, struct nk_vec2);
#ifdef NK_INCLUDE_STANDARD_VARARGS
NK_API void nk_tooltipf(struct nk_context*, NK_PRINTF_FORMAT_STRING const char*, ...) NK_PRINTF_VARARG_FUNC(2);
NK_API void nk_tooltipfv(struct nk_context*, NK_PRINTF_FORMAT_STRING const char*, va_list) NK_PRINTF_VALIST_FUNC(2);
NK_API void nk_tooltipf_offset(struct nk_context*, nk_flags tooltip, struct nk_vec2, NK_PRINTF_FORMAT_STRING const char*, ...) NK_PRINTF_VARARG_FUNC(4);
NK_API void nk_tooltipfv_offset(struct nk_context*, nk_flags tooltip, struct nk_vec2, NK_PRINTF_FORMAT_STRING const char*, va_list) NK_PRINTF_VALIST_FUNC(4);
#endif
NK_API nk_bool nk_tooltip_begin(struct nk_context*, float width);
NK_API nk_bool nk_tooltip_begin_offset(struct nk_context*, float width, nk_flags tooltip, struct nk_vec2);
NK_API void nk_tooltip_end(struct nk_context*);
/* =============================================================================
*
Expand Down
Loading