From ea1d969fbefee63c55f994b80cfcce3fa412c760 Mon Sep 17 00:00:00 2001 From: Robert Winkler Date: Wed, 19 Aug 2026 02:16:04 -0700 Subject: [PATCH 1/6] First pass at smart popup positioning when it's clipped by window --- clib.json | 2 +- demo/glfw_opengl4/nuklear_glfw_gl4.h | 4 + demo/rawfb/nuklear_rawfb.h | 2 + demo/sdl3_renderer/nuklear_sdl3_renderer.h | 7 + nuklear.h | 233 ++++++++++++++++++++- src/HEADER.h | 2 +- src/nuklear.h | 47 +++++ src/nuklear_combo.c | 2 + src/nuklear_context.c | 21 ++ src/nuklear_contextual.c | 9 +- src/nuklear_internal.h | 6 + src/nuklear_menu.c | 10 +- src/nuklear_panel.c | 11 + src/nuklear_popup.c | 95 +++++++++ src/nuklear_tooltip.c | 30 ++- 15 files changed, 464 insertions(+), 17 deletions(-) diff --git a/clib.json b/clib.json index 0b843fabc..d7236b639 100644 --- a/clib.json +++ b/clib.json @@ -1,6 +1,6 @@ { "name": "nuklear", - "version": "4.13.3", + "version": "4.13.4", "repo": "Immediate-Mode-UI/Nuklear", "description": "A small ANSI C gui toolkit", "keywords": ["gl", "ui", "toolkit"], diff --git a/demo/glfw_opengl4/nuklear_glfw_gl4.h b/demo/glfw_opengl4/nuklear_glfw_gl4.h index b6d31c18d..63940daec 100644 --- a/demo/glfw_opengl4/nuklear_glfw_gl4.h +++ b/demo/glfw_opengl4/nuklear_glfw_gl4.h @@ -556,6 +556,8 @@ nk_glfw3_init(GLFWwindow *win, enum nk_glfw_init_state init_state, glfw.ctx.clip.copy = nk_glfw3_clipboard_copy; glfw.ctx.clip.paste = nk_glfw3_clipboard_paste; glfw.ctx.clip.userdata = nk_handle_ptr(0); + glfwGetWindowSize(win, &glfw.width, &glfw.height); + nk_set_display_size(&glfw.ctx, (float)glfw.width, (float)glfw.height); glfw.last_button_click = 0; {struct nk_glfw_device *dev = &glfw.ogl; @@ -608,6 +610,8 @@ nk_glfw3_new_frame(void) glfwGetFramebufferSize(win, &glfw.display_width, &glfw.display_height); glfw.fb_scale.x = (float)glfw.display_width/(float)glfw.width; glfw.fb_scale.y = (float)glfw.display_height/(float)glfw.height; + /* logical window size, not framebuffer size */ + nk_set_display_size(ctx, (float)glfw.width, (float)glfw.height); nk_input_begin(ctx); for (i = 0; i < glfw.text_len; ++i) diff --git a/demo/rawfb/nuklear_rawfb.h b/demo/rawfb/nuklear_rawfb.h index 15f890dbc..e3a4b40f7 100644 --- a/demo/rawfb/nuklear_rawfb.h +++ b/demo/rawfb/nuklear_rawfb.h @@ -844,6 +844,7 @@ nk_rawfb_init(void *fb, void *tex_mem, const unsigned int w, const unsigned int free(rawfb); return NULL; } + nk_set_display_size(&rawfb->ctx, (float)w, (float)h); nk_font_atlas_init_default(&rawfb->atlas); nk_font_atlas_begin(&rawfb->atlas); @@ -1025,6 +1026,7 @@ nk_rawfb_resize_fb(struct rawfb_context *rawfb, rawfb->fb.pixels = fb; rawfb->fb.pitch = pitch; rawfb->fb.pl = pl; + nk_set_display_size(&rawfb->ctx, (float)w, (float)h); } NK_API void diff --git a/demo/sdl3_renderer/nuklear_sdl3_renderer.h b/demo/sdl3_renderer/nuklear_sdl3_renderer.h index 826dadfe7..efbe4dfac 100644 --- a/demo/sdl3_renderer/nuklear_sdl3_renderer.h +++ b/demo/sdl3_renderer/nuklear_sdl3_renderer.h @@ -386,6 +386,9 @@ nk_sdl_init(SDL_Window *win, SDL_Renderer *renderer, struct nk_allocator allocat sdl->win = win; sdl->renderer = renderer; nk_init(&sdl->ctx, &sdl->allocator, 0); + {int dw = 0, dh = 0; + if (SDL_GetWindowSize(win, &dw, &dh)) + nk_set_display_size(&sdl->ctx, (float)dw, (float)dh);} sdl->ctx.userdata = nk_handle_ptr((void*)sdl); sdl->ctx.clip.copy = nk_sdl_clipboard_copy; sdl->ctx.clip.paste = nk_sdl_clipboard_paste; @@ -564,6 +567,10 @@ nk_sdl_handle_event(struct nk_context* ctx, SDL_Event *evt) case SDL_EVENT_MOUSE_WHEEL: nk_input_scroll(ctx, nk_vec2(evt->wheel.x, evt->wheel.y)); return 1; + + case SDL_EVENT_WINDOW_RESIZED: + nk_set_display_size(ctx, (float)evt->window.data1, (float)evt->window.data2); + return 1; } return 0; } diff --git a/nuklear.h b/nuklear.h index 3f77eb7d1..9b5fe3822 100644 --- a/nuklear.h +++ b/nuklear.h @@ -3,7 +3,7 @@ * Single-header ANSI C immediate mode cross-platform GUI library. * * VERSION: - * v4.13.3 + * v4.13.4 * * HOMEPAGE: * https://github.com/Immediate-Mode-UI/Nuklear/ @@ -571,6 +571,8 @@ enum nk_symbol_type { * \ref nk_clear | Called at the end of the frame to reset and prepare the context for the next frame * \ref nk_free | Shutdown and free all memory allocated inside the context * \ref nk_set_user_data| Utility function to pass user data to draw command + * \ref nk_set_display_size | Sets the backend surface size used to keep popups on-screen + * \ref nk_set_display_bounds | Sets a display rectangle (non-zero origin) used to keep popups on-screen */ #ifdef NK_INCLUDE_DEFAULT_ALLOCATOR @@ -699,6 +701,48 @@ NK_API void nk_free(struct nk_context*); */ NK_API void nk_set_user_data(struct nk_context*, nk_handle handle); #endif + +/** + * \brief Sets the size of the surface nuklear is drawn into. + * + * \details + * Comboboxes, menus, contextuals and tooltips use this to stay fully visible + * instead of being clipped by the OS window / framebuffer. Explicit + * `nk_popup_begin` rects are not moved. + * + * Coordinates must match widget and mouse space (logical window pixels, not + * framebuffer pixels on HiDPI). A width or height of 0 disables fitting and is + * the default until this is called. + * + * The value is retained. Call once after `nk_init*` and again whenever the + * surface size changes. It does not need to be set every frame. + * + * ```c + * void nk_set_display_size(struct nk_context *ctx, float width, float height); + * ``` + * + * \param[in] ctx Must point to a previously initialized `nk_context` struct + * \param[in] width Surface width in the same space as `nk_input_motion` + * \param[in] height Surface height in the same space as `nk_input_motion` + */ +NK_API void nk_set_display_size(struct nk_context *ctx, float width, float height); + +/** + * \brief Sets the rectangle of the surface nuklear is drawn into. + * + * \details + * Same as `nk_set_display_size` but allows a non-zero origin, for example when + * the UI is drawn into a sub-rectangle of a window. An empty rectangle + * (`w == 0` or `h == 0`) disables fitting. + * + * ```c + * void nk_set_display_bounds(struct nk_context *ctx, struct nk_rect bounds); + * ``` + * + * \param[in] ctx Must point to a previously initialized `nk_context` struct + * \param[in] bounds Surface rectangle in the same space as `nk_input_motion` + */ +NK_API void nk_set_display_bounds(struct nk_context *ctx, struct nk_rect bounds); /* ============================================================================= * * INPUT @@ -5958,6 +6002,9 @@ struct nk_context { enum nk_button_behavior button_behavior; struct nk_configuration_stacks stacks; float delta_time_seconds; + /** surface nuklear is drawn into, in widget/mouse space. + * `w == 0` or `h == 0` means unset (popup fitting disabled). */ + struct nk_rect display_bounds; /* private: should only be accessed if you @@ -6320,6 +6367,12 @@ NK_LIB void nk_panel_alloc_space(struct nk_rect *bounds, const struct nk_context NK_LIB void nk_layout_peek(struct nk_rect *bounds, const struct nk_context *ctx); /* popup */ +enum nk_popup_fit { + NK_POPUP_FIT_FLIP, /* combo, menu: flip around the trigger */ + NK_POPUP_FIT_SLIDE, /* contextual: slide to stay on-screen */ + NK_POPUP_FIT_TOOLTIP /* tooltip: flip around cursor, then slide */ +}; +NK_LIB struct nk_rect nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, struct nk_rect anchor, enum nk_popup_fit fit, float known_h); NK_LIB nk_bool nk_nonblock_begin(struct nk_context *ctx, nk_flags flags, struct nk_rect body, struct nk_rect header, enum nk_panel_type panel_type); /* text */ @@ -19619,6 +19672,27 @@ nk_set_user_data(struct nk_context *ctx, nk_handle handle) } #endif NK_API void +nk_set_display_size(struct nk_context *ctx, float width, float height) +{ + NK_ASSERT(ctx); + if (!ctx) return; + if (width < 0) width = 0; + if (height < 0) height = 0; + ctx->display_bounds.x = 0; + ctx->display_bounds.y = 0; + ctx->display_bounds.w = width; + ctx->display_bounds.h = height; +} +NK_API void +nk_set_display_bounds(struct nk_context *ctx, struct nk_rect bounds) +{ + NK_ASSERT(ctx); + if (!ctx) return; + if (bounds.w < 0) bounds.w = 0; + if (bounds.h < 0) bounds.h = 0; + ctx->display_bounds = bounds; +} +NK_API void nk_free(struct nk_context *ctx) { NK_ASSERT(ctx); @@ -20684,6 +20758,17 @@ nk_panel_end(struct nk_context *ctx) } window->flags = layout->flags; + /* persist actual popup height so the next frame can flip/slide without + * using the caller-supplied maximum (DYNAMIC shrinks from the top) */ + if (((int)layout->type & (int)NK_PANEL_SET_POPUP) && + (layout->flags & NK_WINDOW_DYNAMIC) && + !(layout->flags & NK_WINDOW_MINIMIZED)) + { + float bottom = layout->bounds.y + layout->bounds.h + layout->footer_height; + if (bottom > window->bounds.y) + window->bounds.h = bottom - window->bounds.y; + } + /* property garbage collector */ if (window->property.active && window->property.old != window->property.seq && window->property.active == window->property.prev) { @@ -21406,6 +21491,101 @@ nk_rule_horizontal(struct nk_context *ctx, struct nk_color color, nk_bool roundi * POPUP * * ===============================================================*/ +NK_LIB struct nk_rect +nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, + struct nk_rect anchor, enum nk_popup_fit fit, float known_h) +{ + struct nk_rect d; + float h; + float overlap; + float space_below; + float space_above; + float d_right; + float d_bottom; + + if (!ctx) return body; + d = ctx->display_bounds; + if (d.w <= 0 || d.h <= 0) return body; + + d_right = d.x + d.w; + d_bottom = d.y + d.h; + h = (known_h > 0.0f) ? known_h : body.h; + overlap = (anchor.y + anchor.h) - body.y; + + /* horizontal: flip/align first, then slide, then shrink */ + if (fit == NK_POPUP_FIT_FLIP) { + if (body.x + body.w > d_right) + body.x = anchor.x + anchor.w - body.w; + } else if (fit == NK_POPUP_FIT_TOOLTIP) { + if (body.x + body.w > d_right) { + float flipped = anchor.x - body.w; + if (flipped >= d.x) + body.x = flipped; + } + } + if (body.x + body.w > d_right) + body.x = d_right - body.w; + if (body.x < d.x) + body.x = d.x; + if (body.x + body.w > d_right) { + body.w = d_right - body.x; + if (body.w < 1.0f) body.w = 1.0f; + } + + /* vertical: flip only when actual height is known so DYNAMIC + * shrink-from-top does not leave a gap above the trigger */ + space_below = d_bottom - body.y; + space_above = anchor.y - d.y; + + if (fit == NK_POPUP_FIT_FLIP) { + if (h > space_below) { + if (known_h > 0.0f && known_h <= space_above) { + body.y = anchor.y - known_h; + if (overlap > 0.0f) + body.y += overlap; + } else if (known_h <= 0.0f) { + if (space_below > 0.0f) + body.h = space_below; + } else if (space_above > space_below) { + body.y = d.y; + body.h = space_above; + if (body.h < 1.0f) body.h = 1.0f; + } else if (space_below > 0.0f) { + body.h = space_below; + } + } + } else if (fit == NK_POPUP_FIT_SLIDE) { + if (known_h > 0.0f) { + if (body.y + known_h > d_bottom) + body.y = d_bottom - known_h; + if (body.y < d.y) + body.y = d.y; + if (body.y + known_h > d_bottom) { + body.h = d_bottom - body.y; + if (body.h < 1.0f) body.h = 1.0f; + } + } else if (body.h > space_below && space_below > 0.0f) { + body.h = space_below; + } + } else { + if (known_h > 0.0f && body.y + known_h > d_bottom) { + float flipped = anchor.y - known_h; + if (flipped >= d.y) + body.y = flipped; + } + if (known_h > 0.0f) { + if (body.y + known_h > d_bottom) + body.y = d_bottom - known_h; + if (body.y < d.y) + body.y = d.y; + if (body.y + known_h > d_bottom) { + body.h = d_bottom - body.y; + if (body.h < 1.0f) body.h = 1.0f; + } + } + } + return body; +} NK_API nk_bool nk_popup_begin(struct nk_context *ctx, enum nk_popup_type type, const char *title, nk_flags flags, struct nk_rect rect) @@ -21722,8 +21902,15 @@ nk_contextual_begin(struct nk_context *ctx, nk_flags flags, struct nk_vec2 size, body.w = size.x; body.h = size.y; + {float known_h = (!is_clicked && popup) ? popup->bounds.h : 0; + float req_h = body.h; + body = nk_fit_popup_rect(ctx, body, body, NK_POPUP_FIT_SLIDE, known_h); + flags |= NK_WINDOW_NO_SCROLLBAR; + if (body.h + 0.5f < req_h) + flags &= ~(nk_flags)NK_WINDOW_NO_SCROLLBAR;} + /* start nonblocking contextual popup */ - ret = nk_nonblock_begin(ctx, flags | NK_WINDOW_NO_SCROLLBAR, body, + ret = nk_nonblock_begin(ctx, flags, body, null_rect, NK_PANEL_CONTEXTUAL); if (ret) win->popup.type = NK_PANEL_CONTEXTUAL; else { @@ -21994,8 +22181,14 @@ nk_menu_begin(struct nk_context *ctx, struct nk_window *win, is_active = (popup && (win->popup.name == hash) && win->popup.type == NK_PANEL_MENU); if ((is_clicked && is_open && !is_active) || (is_open && !is_active) || (!is_open && !is_active && !is_clicked)) return 0; - if (!nk_nonblock_begin(ctx, NK_WINDOW_NO_SCROLLBAR, body, header, NK_PANEL_MENU)) - return 0; + {nk_flags flags = NK_WINDOW_NO_SCROLLBAR; + float known_h = (is_active && popup) ? popup->bounds.h : 0; + float req_h = body.h; + body = nk_fit_popup_rect(ctx, body, header, NK_POPUP_FIT_FLIP, known_h); + if (body.h + 0.5f < req_h) + flags = 0; + if (!nk_nonblock_begin(ctx, flags, body, header, NK_PANEL_MENU)) + return 0;} win->popup.type = NK_PANEL_MENU; win->popup.name = hash; @@ -30040,6 +30233,8 @@ nk_combo_begin(struct nk_context *ctx, struct nk_window *win, is_active = (popup && (win->popup.name == hash) && win->popup.type == NK_PANEL_COMBO); if ((is_clicked && is_open && !is_active) || (is_open && !is_active) || (!is_open && !is_active && !is_clicked)) return 0; + {float known_h = (is_active && popup) ? popup->bounds.h : 0; + body = nk_fit_popup_rect(ctx, body, header, NK_POPUP_FIT_FLIP, known_h);} if (!nk_nonblock_begin(ctx, 0, body, (is_clicked && is_open)?nk_rect(0,0,0,0):header, NK_PANEL_COMBO)) return 0; @@ -30943,10 +31138,32 @@ nk_tooltip_begin_offset(struct nk_context *ctx, float width, enum nk_tooltip_pos NK_ASSERT(0 && "Invalid tooltip position"); } - bounds.x = (float)x; - bounds.y = (float)y; - bounds.w = (float)w; - bounds.h = (float)nk_iceilf(nk_null_rect.h); + {struct nk_rect screen; + struct nk_rect anchor; + float known_h = (float)h; + float clip_x = win->layout->clip.x; + float clip_y = win->layout->clip.y; + + if (win->popup.win && win->popup.type == NK_PANEL_TOOLTIP && + win->popup.win->bounds.h > known_h) + known_h = win->popup.win->bounds.h; + + screen.x = (float)x + clip_x; + screen.y = (float)y + clip_y; + screen.w = (float)w; + screen.h = known_h; + anchor.x = in->mouse.pos.x; + anchor.y = in->mouse.pos.y; + anchor.w = 1; + anchor.h = 1; + screen = nk_fit_popup_rect(ctx, screen, anchor, NK_POPUP_FIT_TOOLTIP, known_h); + + bounds.x = screen.x - clip_x; + bounds.y = screen.y - clip_y; + bounds.w = screen.w; + /* keep a large max so NK_POPUP_DYNAMIC can grow; the fitter only + * shrinks h when the tooltip cannot fit on the surface at all */ + bounds.h = (screen.h + 0.5f < known_h) ? screen.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); diff --git a/src/HEADER.h b/src/HEADER.h index ef59d6a62..b3bd96c50 100644 --- a/src/HEADER.h +++ b/src/HEADER.h @@ -3,7 +3,7 @@ * Single-header ANSI C immediate mode cross-platform GUI library. * * VERSION: - * v4.13.3 + * v4.13.4 * * HOMEPAGE: * https://github.com/Immediate-Mode-UI/Nuklear/ diff --git a/src/nuklear.h b/src/nuklear.h index 0a115a6cd..c0a24bd7d 100644 --- a/src/nuklear.h +++ b/src/nuklear.h @@ -381,6 +381,8 @@ enum nk_symbol_type { * \ref nk_clear | Called at the end of the frame to reset and prepare the context for the next frame * \ref nk_free | Shutdown and free all memory allocated inside the context * \ref nk_set_user_data| Utility function to pass user data to draw command + * \ref nk_set_display_size | Sets the backend surface size used to keep popups on-screen + * \ref nk_set_display_bounds | Sets a display rectangle (non-zero origin) used to keep popups on-screen */ #ifdef NK_INCLUDE_DEFAULT_ALLOCATOR @@ -509,6 +511,48 @@ NK_API void nk_free(struct nk_context*); */ NK_API void nk_set_user_data(struct nk_context*, nk_handle handle); #endif + +/** + * \brief Sets the size of the surface nuklear is drawn into. + * + * \details + * Comboboxes, menus, contextuals and tooltips use this to stay fully visible + * instead of being clipped by the OS window / framebuffer. Explicit + * `nk_popup_begin` rects are not moved. + * + * Coordinates must match widget and mouse space (logical window pixels, not + * framebuffer pixels on HiDPI). A width or height of 0 disables fitting and is + * the default until this is called. + * + * The value is retained. Call once after `nk_init*` and again whenever the + * surface size changes. It does not need to be set every frame. + * + * ```c + * void nk_set_display_size(struct nk_context *ctx, float width, float height); + * ``` + * + * \param[in] ctx Must point to a previously initialized `nk_context` struct + * \param[in] width Surface width in the same space as `nk_input_motion` + * \param[in] height Surface height in the same space as `nk_input_motion` + */ +NK_API void nk_set_display_size(struct nk_context *ctx, float width, float height); + +/** + * \brief Sets the rectangle of the surface nuklear is drawn into. + * + * \details + * Same as `nk_set_display_size` but allows a non-zero origin, for example when + * the UI is drawn into a sub-rectangle of a window. An empty rectangle + * (`w == 0` or `h == 0`) disables fitting. + * + * ```c + * void nk_set_display_bounds(struct nk_context *ctx, struct nk_rect bounds); + * ``` + * + * \param[in] ctx Must point to a previously initialized `nk_context` struct + * \param[in] bounds Surface rectangle in the same space as `nk_input_motion` + */ +NK_API void nk_set_display_bounds(struct nk_context *ctx, struct nk_rect bounds); /* ============================================================================= * * INPUT @@ -5768,6 +5812,9 @@ struct nk_context { enum nk_button_behavior button_behavior; struct nk_configuration_stacks stacks; float delta_time_seconds; + /** surface nuklear is drawn into, in widget/mouse space. + * `w == 0` or `h == 0` means unset (popup fitting disabled). */ + struct nk_rect display_bounds; /* private: should only be accessed if you diff --git a/src/nuklear_combo.c b/src/nuklear_combo.c index b0bada143..ca3b4fac2 100644 --- a/src/nuklear_combo.c +++ b/src/nuklear_combo.c @@ -33,6 +33,8 @@ nk_combo_begin(struct nk_context *ctx, struct nk_window *win, is_active = (popup && (win->popup.name == hash) && win->popup.type == NK_PANEL_COMBO); if ((is_clicked && is_open && !is_active) || (is_open && !is_active) || (!is_open && !is_active && !is_clicked)) return 0; + {float known_h = (is_active && popup) ? popup->bounds.h : 0; + body = nk_fit_popup_rect(ctx, body, header, NK_POPUP_FIT_FLIP, known_h);} if (!nk_nonblock_begin(ctx, 0, body, (is_clicked && is_open)?nk_rect(0,0,0,0):header, NK_PANEL_COMBO)) return 0; diff --git a/src/nuklear_context.c b/src/nuklear_context.c index 771449689..4af9b6cb8 100644 --- a/src/nuklear_context.c +++ b/src/nuklear_context.c @@ -85,6 +85,27 @@ nk_set_user_data(struct nk_context *ctx, nk_handle handle) } #endif NK_API void +nk_set_display_size(struct nk_context *ctx, float width, float height) +{ + NK_ASSERT(ctx); + if (!ctx) return; + if (width < 0) width = 0; + if (height < 0) height = 0; + ctx->display_bounds.x = 0; + ctx->display_bounds.y = 0; + ctx->display_bounds.w = width; + ctx->display_bounds.h = height; +} +NK_API void +nk_set_display_bounds(struct nk_context *ctx, struct nk_rect bounds) +{ + NK_ASSERT(ctx); + if (!ctx) return; + if (bounds.w < 0) bounds.w = 0; + if (bounds.h < 0) bounds.h = 0; + ctx->display_bounds = bounds; +} +NK_API void nk_free(struct nk_context *ctx) { NK_ASSERT(ctx); diff --git a/src/nuklear_contextual.c b/src/nuklear_contextual.c index 006a0b403..3009d9abe 100644 --- a/src/nuklear_contextual.c +++ b/src/nuklear_contextual.c @@ -57,8 +57,15 @@ nk_contextual_begin(struct nk_context *ctx, nk_flags flags, struct nk_vec2 size, body.w = size.x; body.h = size.y; + {float known_h = (!is_clicked && popup) ? popup->bounds.h : 0; + float req_h = body.h; + body = nk_fit_popup_rect(ctx, body, body, NK_POPUP_FIT_SLIDE, known_h); + flags |= NK_WINDOW_NO_SCROLLBAR; + if (body.h + 0.5f < req_h) + flags &= ~(nk_flags)NK_WINDOW_NO_SCROLLBAR;} + /* start nonblocking contextual popup */ - ret = nk_nonblock_begin(ctx, flags | NK_WINDOW_NO_SCROLLBAR, body, + ret = nk_nonblock_begin(ctx, flags, body, null_rect, NK_PANEL_CONTEXTUAL); if (ret) win->popup.type = NK_PANEL_CONTEXTUAL; else { diff --git a/src/nuklear_internal.h b/src/nuklear_internal.h index b380b7644..4f5cf644e 100644 --- a/src/nuklear_internal.h +++ b/src/nuklear_internal.h @@ -246,6 +246,12 @@ NK_LIB void nk_panel_alloc_space(struct nk_rect *bounds, const struct nk_context NK_LIB void nk_layout_peek(struct nk_rect *bounds, const struct nk_context *ctx); /* popup */ +enum nk_popup_fit { + NK_POPUP_FIT_FLIP, /* combo, menu: flip around the trigger */ + NK_POPUP_FIT_SLIDE, /* contextual: slide to stay on-screen */ + NK_POPUP_FIT_TOOLTIP /* tooltip: flip around cursor, then slide */ +}; +NK_LIB struct nk_rect nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, struct nk_rect anchor, enum nk_popup_fit fit, float known_h); NK_LIB nk_bool nk_nonblock_begin(struct nk_context *ctx, nk_flags flags, struct nk_rect body, struct nk_rect header, enum nk_panel_type panel_type); /* text */ diff --git a/src/nuklear_menu.c b/src/nuklear_menu.c index 00ec5e11f..33be6ca5c 100644 --- a/src/nuklear_menu.c +++ b/src/nuklear_menu.c @@ -102,8 +102,14 @@ nk_menu_begin(struct nk_context *ctx, struct nk_window *win, is_active = (popup && (win->popup.name == hash) && win->popup.type == NK_PANEL_MENU); if ((is_clicked && is_open && !is_active) || (is_open && !is_active) || (!is_open && !is_active && !is_clicked)) return 0; - if (!nk_nonblock_begin(ctx, NK_WINDOW_NO_SCROLLBAR, body, header, NK_PANEL_MENU)) - return 0; + {nk_flags flags = NK_WINDOW_NO_SCROLLBAR; + float known_h = (is_active && popup) ? popup->bounds.h : 0; + float req_h = body.h; + body = nk_fit_popup_rect(ctx, body, header, NK_POPUP_FIT_FLIP, known_h); + if (body.h + 0.5f < req_h) + flags = 0; + if (!nk_nonblock_begin(ctx, flags, body, header, NK_PANEL_MENU)) + return 0;} win->popup.type = NK_PANEL_MENU; win->popup.name = hash; diff --git a/src/nuklear_panel.c b/src/nuklear_panel.c index 4c7972108..9dc75a3ca 100644 --- a/src/nuklear_panel.c +++ b/src/nuklear_panel.c @@ -586,6 +586,17 @@ nk_panel_end(struct nk_context *ctx) } window->flags = layout->flags; + /* persist actual popup height so the next frame can flip/slide without + * using the caller-supplied maximum (DYNAMIC shrinks from the top) */ + if (((int)layout->type & (int)NK_PANEL_SET_POPUP) && + (layout->flags & NK_WINDOW_DYNAMIC) && + !(layout->flags & NK_WINDOW_MINIMIZED)) + { + float bottom = layout->bounds.y + layout->bounds.h + layout->footer_height; + if (bottom > window->bounds.y) + window->bounds.h = bottom - window->bounds.y; + } + /* property garbage collector */ if (window->property.active && window->property.old != window->property.seq && window->property.active == window->property.prev) { diff --git a/src/nuklear_popup.c b/src/nuklear_popup.c index 8b2339e5c..0a05e42d4 100644 --- a/src/nuklear_popup.c +++ b/src/nuklear_popup.c @@ -6,6 +6,101 @@ * POPUP * * ===============================================================*/ +NK_LIB struct nk_rect +nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, + struct nk_rect anchor, enum nk_popup_fit fit, float known_h) +{ + struct nk_rect d; + float h; + float overlap; + float space_below; + float space_above; + float d_right; + float d_bottom; + + if (!ctx) return body; + d = ctx->display_bounds; + if (d.w <= 0 || d.h <= 0) return body; + + d_right = d.x + d.w; + d_bottom = d.y + d.h; + h = (known_h > 0.0f) ? known_h : body.h; + overlap = (anchor.y + anchor.h) - body.y; + + /* horizontal: flip/align first, then slide, then shrink */ + if (fit == NK_POPUP_FIT_FLIP) { + if (body.x + body.w > d_right) + body.x = anchor.x + anchor.w - body.w; + } else if (fit == NK_POPUP_FIT_TOOLTIP) { + if (body.x + body.w > d_right) { + float flipped = anchor.x - body.w; + if (flipped >= d.x) + body.x = flipped; + } + } + if (body.x + body.w > d_right) + body.x = d_right - body.w; + if (body.x < d.x) + body.x = d.x; + if (body.x + body.w > d_right) { + body.w = d_right - body.x; + if (body.w < 1.0f) body.w = 1.0f; + } + + /* vertical: flip only when actual height is known so DYNAMIC + * shrink-from-top does not leave a gap above the trigger */ + space_below = d_bottom - body.y; + space_above = anchor.y - d.y; + + if (fit == NK_POPUP_FIT_FLIP) { + if (h > space_below) { + if (known_h > 0.0f && known_h <= space_above) { + body.y = anchor.y - known_h; + if (overlap > 0.0f) + body.y += overlap; + } else if (known_h <= 0.0f) { + if (space_below > 0.0f) + body.h = space_below; + } else if (space_above > space_below) { + body.y = d.y; + body.h = space_above; + if (body.h < 1.0f) body.h = 1.0f; + } else if (space_below > 0.0f) { + body.h = space_below; + } + } + } else if (fit == NK_POPUP_FIT_SLIDE) { + if (known_h > 0.0f) { + if (body.y + known_h > d_bottom) + body.y = d_bottom - known_h; + if (body.y < d.y) + body.y = d.y; + if (body.y + known_h > d_bottom) { + body.h = d_bottom - body.y; + if (body.h < 1.0f) body.h = 1.0f; + } + } else if (body.h > space_below && space_below > 0.0f) { + body.h = space_below; + } + } else { + if (known_h > 0.0f && body.y + known_h > d_bottom) { + float flipped = anchor.y - known_h; + if (flipped >= d.y) + body.y = flipped; + } + if (known_h > 0.0f) { + if (body.y + known_h > d_bottom) + body.y = d_bottom - known_h; + if (body.y < d.y) + body.y = d.y; + if (body.y + known_h > d_bottom) { + body.h = d_bottom - body.y; + if (body.h < 1.0f) body.h = 1.0f; + } + } + } + return body; +} NK_API nk_bool nk_popup_begin(struct nk_context *ctx, enum nk_popup_type type, const char *title, nk_flags flags, struct nk_rect rect) diff --git a/src/nuklear_tooltip.c b/src/nuklear_tooltip.c index 08a00a440..49ac2ca9f 100644 --- a/src/nuklear_tooltip.c +++ b/src/nuklear_tooltip.c @@ -80,10 +80,32 @@ nk_tooltip_begin_offset(struct nk_context *ctx, float width, enum nk_tooltip_pos NK_ASSERT(0 && "Invalid tooltip position"); } - bounds.x = (float)x; - bounds.y = (float)y; - bounds.w = (float)w; - bounds.h = (float)nk_iceilf(nk_null_rect.h); + {struct nk_rect screen; + struct nk_rect anchor; + float known_h = (float)h; + float clip_x = win->layout->clip.x; + float clip_y = win->layout->clip.y; + + if (win->popup.win && win->popup.type == NK_PANEL_TOOLTIP && + win->popup.win->bounds.h > known_h) + known_h = win->popup.win->bounds.h; + + screen.x = (float)x + clip_x; + screen.y = (float)y + clip_y; + screen.w = (float)w; + screen.h = known_h; + anchor.x = in->mouse.pos.x; + anchor.y = in->mouse.pos.y; + anchor.w = 1; + anchor.h = 1; + screen = nk_fit_popup_rect(ctx, screen, anchor, NK_POPUP_FIT_TOOLTIP, known_h); + + bounds.x = screen.x - clip_x; + bounds.y = screen.y - clip_y; + bounds.w = screen.w; + /* keep a large max so NK_POPUP_DYNAMIC can grow; the fitter only + * shrinks h when the tooltip cannot fit on the surface at all */ + bounds.h = (screen.h + 0.5f < known_h) ? screen.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); From 8542d8c6581af1b28ad252e8ac966e10d3625fa8 Mon Sep 17 00:00:00 2001 From: Robert Winkler Date: Wed, 19 Aug 2026 02:42:29 -0700 Subject: [PATCH 2/6] Fix some bugs in menus/combos (flickering) and context menu insufficient width handling --- nuklear.h | 115 +++++++++++++++++++-------------------- src/nuklear.h | 1 + src/nuklear_combo.c | 2 +- src/nuklear_contextual.c | 2 +- src/nuklear_menu.c | 2 +- src/nuklear_panel.c | 13 ++--- src/nuklear_popup.c | 91 +++++++++++++++---------------- src/nuklear_tooltip.c | 4 +- 8 files changed, 110 insertions(+), 120 deletions(-) diff --git a/nuklear.h b/nuklear.h index 9b5fe3822..7f4b0e260 100644 --- a/nuklear.h +++ b/nuklear.h @@ -5793,6 +5793,7 @@ struct nk_popup_state { unsigned con_count, con_old; unsigned active_con; struct nk_rect header; + float last_h; /**< last frame's content height; used to flip without a gap */ }; struct nk_edit_state { @@ -20758,15 +20759,14 @@ nk_panel_end(struct nk_context *ctx) } window->flags = layout->flags; - /* persist actual popup height so the next frame can flip/slide without - * using the caller-supplied maximum (DYNAMIC shrinks from the top) */ - if (((int)layout->type & (int)NK_PANEL_SET_POPUP) && - (layout->flags & NK_WINDOW_DYNAMIC) && + /* remember unclamped content height so next frame can flip using the + * real size, not the display-clamped window (that caused flicker). */ + if (window->parent && ((int)layout->type & (int)NK_PANEL_SET_POPUP) && !(layout->flags & NK_WINDOW_MINIMIZED)) { - float bottom = layout->bounds.y + layout->bounds.h + layout->footer_height; - if (bottom > window->bounds.y) - window->bounds.h = bottom - window->bounds.y; + float content_h = (layout->at_y + layout->footer_height) - window->bounds.y; + if (content_h > 0) + window->parent->popup.last_h = content_h; } /* property garbage collector */ @@ -21496,12 +21496,13 @@ nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, struct nk_rect anchor, enum nk_popup_fit fit, float known_h) { struct nk_rect d; - float h; float overlap; float space_below; float space_above; float d_right; float d_bottom; + float req_h; + float need_h; if (!ctx) return body; d = ctx->display_bounds; @@ -21509,10 +21510,13 @@ nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, d_right = d.x + d.w; d_bottom = d.y + d.h; - h = (known_h > 0.0f) ? known_h : body.h; + req_h = body.h; + need_h = (known_h > 0.0f) ? known_h : req_h; overlap = (anchor.y + anchor.h) - body.y; - /* horizontal: flip/align first, then slide, then shrink */ + /* horizontal: flip/align, then slide. Do not shrink width; dynamic + * rows (e.g. the overview color contextual) size to the panel and + * would smush. If the surface is narrower than the popup, clip. */ if (fit == NK_POPUP_FIT_FLIP) { if (body.x + body.w > d_right) body.x = anchor.x + anchor.w - body.w; @@ -21527,62 +21531,52 @@ nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, body.x = d_right - body.w; if (body.x < d.x) body.x = d.x; - if (body.x + body.w > d_right) { - body.w = d_right - body.x; - if (body.w < 1.0f) body.w = 1.0f; - } - /* vertical: flip only when actual height is known so DYNAMIC - * shrink-from-top does not leave a gap above the trigger */ space_below = d_bottom - body.y; space_above = anchor.y - d.y; + if (space_below < 0.0f) space_below = 0.0f; + if (space_above < 0.0f) space_above = 0.0f; if (fit == NK_POPUP_FIT_FLIP) { - if (h > space_below) { - if (known_h > 0.0f && known_h <= space_above) { - body.y = anchor.y - known_h; - if (overlap > 0.0f) - body.y += overlap; - } else if (known_h <= 0.0f) { - if (space_below > 0.0f) - body.h = space_below; - } else if (space_above > space_below) { - body.y = d.y; - body.h = space_above; - if (body.h < 1.0f) body.h = 1.0f; - } else if (space_below > 0.0f) { - body.h = space_below; - } + int flip = 0; + if (req_h > space_below) { + /* flip only when the real content fits above. using the + * clamped window height here caused a flip/restore flicker. */ + if (need_h <= space_above) + flip = 1; + else if (known_h > 0.0f && space_above > space_below) + flip = 1; } - } else if (fit == NK_POPUP_FIT_SLIDE) { - if (known_h > 0.0f) { - if (body.y + known_h > d_bottom) - body.y = d_bottom - known_h; - if (body.y < d.y) - body.y = d.y; - if (body.y + known_h > d_bottom) { - body.h = d_bottom - body.y; - if (body.h < 1.0f) body.h = 1.0f; - } - } else if (body.h > space_below && space_below > 0.0f) { - body.h = space_below; + if (flip) { + float use_h = req_h; + if (use_h > space_above) use_h = space_above; + if (known_h > 0.0f && known_h < use_h) use_h = known_h; + if (use_h < 1.0f) use_h = 1.0f; + body.y = anchor.y - use_h; + if (overlap > 0.0f) body.y += overlap; + body.h = use_h; + } else if (req_h > space_below) { + body.h = (space_below > 1.0f) ? space_below : 1.0f; } + } else if (fit == NK_POPUP_FIT_SLIDE) { + if (body.y + need_h > d_bottom) + body.y = d_bottom - need_h; + if (body.y < d.y) + body.y = d.y; + if (body.y + body.h > d_bottom) + body.h = d_bottom - body.y; + if (body.h < 1.0f) body.h = 1.0f; } else { - if (known_h > 0.0f && body.y + known_h > d_bottom) { - float flipped = anchor.y - known_h; + /* tooltip: flip then slide; do not shrink (clipping is acceptable) */ + if (body.y + need_h > d_bottom) { + float flipped = anchor.y - need_h; if (flipped >= d.y) body.y = flipped; } - if (known_h > 0.0f) { - if (body.y + known_h > d_bottom) - body.y = d_bottom - known_h; - if (body.y < d.y) - body.y = d.y; - if (body.y + known_h > d_bottom) { - body.h = d_bottom - body.y; - if (body.h < 1.0f) body.h = 1.0f; - } - } + if (body.y + need_h > d_bottom) + body.y = d_bottom - need_h; + if (body.y < d.y) + body.y = d.y; } return body; } @@ -21734,6 +21728,7 @@ nk_nonblock_begin(struct nk_context *ctx, root = root->parent; } win->popup.buf.active = 0; + win->popup.last_h = 0; return is_active; } popup->bounds = body; @@ -21902,7 +21897,7 @@ nk_contextual_begin(struct nk_context *ctx, nk_flags flags, struct nk_vec2 size, body.w = size.x; body.h = size.y; - {float known_h = (!is_clicked && popup) ? popup->bounds.h : 0; + {float known_h = (!is_clicked) ? win->popup.last_h : 0; float req_h = body.h; body = nk_fit_popup_rect(ctx, body, body, NK_POPUP_FIT_SLIDE, known_h); flags |= NK_WINDOW_NO_SCROLLBAR; @@ -22182,7 +22177,7 @@ nk_menu_begin(struct nk_context *ctx, struct nk_window *win, if ((is_clicked && is_open && !is_active) || (is_open && !is_active) || (!is_open && !is_active && !is_clicked)) return 0; {nk_flags flags = NK_WINDOW_NO_SCROLLBAR; - float known_h = (is_active && popup) ? popup->bounds.h : 0; + float known_h = (is_active) ? win->popup.last_h : 0; float req_h = body.h; body = nk_fit_popup_rect(ctx, body, header, NK_POPUP_FIT_FLIP, known_h); if (body.h + 0.5f < req_h) @@ -30233,7 +30228,7 @@ nk_combo_begin(struct nk_context *ctx, struct nk_window *win, is_active = (popup && (win->popup.name == hash) && win->popup.type == NK_PANEL_COMBO); if ((is_clicked && is_open && !is_active) || (is_open && !is_active) || (!is_open && !is_active && !is_clicked)) return 0; - {float known_h = (is_active && popup) ? popup->bounds.h : 0; + {float known_h = (is_active) ? win->popup.last_h : 0; body = nk_fit_popup_rect(ctx, body, header, NK_POPUP_FIT_FLIP, known_h);} if (!nk_nonblock_begin(ctx, 0, body, (is_clicked && is_open)?nk_rect(0,0,0,0):header, NK_PANEL_COMBO)) return 0; @@ -31145,8 +31140,8 @@ nk_tooltip_begin_offset(struct nk_context *ctx, float width, enum nk_tooltip_pos float clip_y = win->layout->clip.y; if (win->popup.win && win->popup.type == NK_PANEL_TOOLTIP && - win->popup.win->bounds.h > known_h) - known_h = win->popup.win->bounds.h; + win->popup.last_h > known_h) + known_h = win->popup.last_h; screen.x = (float)x + clip_x; screen.y = (float)y + clip_y; diff --git a/src/nuklear.h b/src/nuklear.h index c0a24bd7d..04b83814d 100644 --- a/src/nuklear.h +++ b/src/nuklear.h @@ -5603,6 +5603,7 @@ struct nk_popup_state { unsigned con_count, con_old; unsigned active_con; struct nk_rect header; + float last_h; /**< last frame's content height; used to flip without a gap */ }; struct nk_edit_state { diff --git a/src/nuklear_combo.c b/src/nuklear_combo.c index ca3b4fac2..529b3c727 100644 --- a/src/nuklear_combo.c +++ b/src/nuklear_combo.c @@ -33,7 +33,7 @@ nk_combo_begin(struct nk_context *ctx, struct nk_window *win, is_active = (popup && (win->popup.name == hash) && win->popup.type == NK_PANEL_COMBO); if ((is_clicked && is_open && !is_active) || (is_open && !is_active) || (!is_open && !is_active && !is_clicked)) return 0; - {float known_h = (is_active && popup) ? popup->bounds.h : 0; + {float known_h = (is_active) ? win->popup.last_h : 0; body = nk_fit_popup_rect(ctx, body, header, NK_POPUP_FIT_FLIP, known_h);} if (!nk_nonblock_begin(ctx, 0, body, (is_clicked && is_open)?nk_rect(0,0,0,0):header, NK_PANEL_COMBO)) return 0; diff --git a/src/nuklear_contextual.c b/src/nuklear_contextual.c index 3009d9abe..454c91517 100644 --- a/src/nuklear_contextual.c +++ b/src/nuklear_contextual.c @@ -57,7 +57,7 @@ nk_contextual_begin(struct nk_context *ctx, nk_flags flags, struct nk_vec2 size, body.w = size.x; body.h = size.y; - {float known_h = (!is_clicked && popup) ? popup->bounds.h : 0; + {float known_h = (!is_clicked) ? win->popup.last_h : 0; float req_h = body.h; body = nk_fit_popup_rect(ctx, body, body, NK_POPUP_FIT_SLIDE, known_h); flags |= NK_WINDOW_NO_SCROLLBAR; diff --git a/src/nuklear_menu.c b/src/nuklear_menu.c index 33be6ca5c..49567ada4 100644 --- a/src/nuklear_menu.c +++ b/src/nuklear_menu.c @@ -103,7 +103,7 @@ nk_menu_begin(struct nk_context *ctx, struct nk_window *win, if ((is_clicked && is_open && !is_active) || (is_open && !is_active) || (!is_open && !is_active && !is_clicked)) return 0; {nk_flags flags = NK_WINDOW_NO_SCROLLBAR; - float known_h = (is_active && popup) ? popup->bounds.h : 0; + float known_h = (is_active) ? win->popup.last_h : 0; float req_h = body.h; body = nk_fit_popup_rect(ctx, body, header, NK_POPUP_FIT_FLIP, known_h); if (body.h + 0.5f < req_h) diff --git a/src/nuklear_panel.c b/src/nuklear_panel.c index 9dc75a3ca..1b8852d4f 100644 --- a/src/nuklear_panel.c +++ b/src/nuklear_panel.c @@ -586,15 +586,14 @@ nk_panel_end(struct nk_context *ctx) } window->flags = layout->flags; - /* persist actual popup height so the next frame can flip/slide without - * using the caller-supplied maximum (DYNAMIC shrinks from the top) */ - if (((int)layout->type & (int)NK_PANEL_SET_POPUP) && - (layout->flags & NK_WINDOW_DYNAMIC) && + /* remember unclamped content height so next frame can flip using the + * real size, not the display-clamped window (that caused flicker). */ + if (window->parent && ((int)layout->type & (int)NK_PANEL_SET_POPUP) && !(layout->flags & NK_WINDOW_MINIMIZED)) { - float bottom = layout->bounds.y + layout->bounds.h + layout->footer_height; - if (bottom > window->bounds.y) - window->bounds.h = bottom - window->bounds.y; + float content_h = (layout->at_y + layout->footer_height) - window->bounds.y; + if (content_h > 0) + window->parent->popup.last_h = content_h; } /* property garbage collector */ diff --git a/src/nuklear_popup.c b/src/nuklear_popup.c index 0a05e42d4..933d49334 100644 --- a/src/nuklear_popup.c +++ b/src/nuklear_popup.c @@ -11,12 +11,13 @@ nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, struct nk_rect anchor, enum nk_popup_fit fit, float known_h) { struct nk_rect d; - float h; float overlap; float space_below; float space_above; float d_right; float d_bottom; + float req_h; + float need_h; if (!ctx) return body; d = ctx->display_bounds; @@ -24,10 +25,13 @@ nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, d_right = d.x + d.w; d_bottom = d.y + d.h; - h = (known_h > 0.0f) ? known_h : body.h; + req_h = body.h; + need_h = (known_h > 0.0f) ? known_h : req_h; overlap = (anchor.y + anchor.h) - body.y; - /* horizontal: flip/align first, then slide, then shrink */ + /* horizontal: flip/align, then slide. Do not shrink width; dynamic + * rows (e.g. the overview color contextual) size to the panel and + * would smush. If the surface is narrower than the popup, clip. */ if (fit == NK_POPUP_FIT_FLIP) { if (body.x + body.w > d_right) body.x = anchor.x + anchor.w - body.w; @@ -42,62 +46,52 @@ nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, body.x = d_right - body.w; if (body.x < d.x) body.x = d.x; - if (body.x + body.w > d_right) { - body.w = d_right - body.x; - if (body.w < 1.0f) body.w = 1.0f; - } - /* vertical: flip only when actual height is known so DYNAMIC - * shrink-from-top does not leave a gap above the trigger */ space_below = d_bottom - body.y; space_above = anchor.y - d.y; + if (space_below < 0.0f) space_below = 0.0f; + if (space_above < 0.0f) space_above = 0.0f; if (fit == NK_POPUP_FIT_FLIP) { - if (h > space_below) { - if (known_h > 0.0f && known_h <= space_above) { - body.y = anchor.y - known_h; - if (overlap > 0.0f) - body.y += overlap; - } else if (known_h <= 0.0f) { - if (space_below > 0.0f) - body.h = space_below; - } else if (space_above > space_below) { - body.y = d.y; - body.h = space_above; - if (body.h < 1.0f) body.h = 1.0f; - } else if (space_below > 0.0f) { - body.h = space_below; - } + int flip = 0; + if (req_h > space_below) { + /* flip only when the real content fits above. using the + * clamped window height here caused a flip/restore flicker. */ + if (need_h <= space_above) + flip = 1; + else if (known_h > 0.0f && space_above > space_below) + flip = 1; } - } else if (fit == NK_POPUP_FIT_SLIDE) { - if (known_h > 0.0f) { - if (body.y + known_h > d_bottom) - body.y = d_bottom - known_h; - if (body.y < d.y) - body.y = d.y; - if (body.y + known_h > d_bottom) { - body.h = d_bottom - body.y; - if (body.h < 1.0f) body.h = 1.0f; - } - } else if (body.h > space_below && space_below > 0.0f) { - body.h = space_below; + if (flip) { + float use_h = req_h; + if (use_h > space_above) use_h = space_above; + if (known_h > 0.0f && known_h < use_h) use_h = known_h; + if (use_h < 1.0f) use_h = 1.0f; + body.y = anchor.y - use_h; + if (overlap > 0.0f) body.y += overlap; + body.h = use_h; + } else if (req_h > space_below) { + body.h = (space_below > 1.0f) ? space_below : 1.0f; } + } else if (fit == NK_POPUP_FIT_SLIDE) { + if (body.y + need_h > d_bottom) + body.y = d_bottom - need_h; + if (body.y < d.y) + body.y = d.y; + if (body.y + body.h > d_bottom) + body.h = d_bottom - body.y; + if (body.h < 1.0f) body.h = 1.0f; } else { - if (known_h > 0.0f && body.y + known_h > d_bottom) { - float flipped = anchor.y - known_h; + /* tooltip: flip then slide; do not shrink (clipping is acceptable) */ + if (body.y + need_h > d_bottom) { + float flipped = anchor.y - need_h; if (flipped >= d.y) body.y = flipped; } - if (known_h > 0.0f) { - if (body.y + known_h > d_bottom) - body.y = d_bottom - known_h; - if (body.y < d.y) - body.y = d.y; - if (body.y + known_h > d_bottom) { - body.h = d_bottom - body.y; - if (body.h < 1.0f) body.h = 1.0f; - } - } + if (body.y + need_h > d_bottom) + body.y = d_bottom - need_h; + if (body.y < d.y) + body.y = d.y; } return body; } @@ -249,6 +243,7 @@ nk_nonblock_begin(struct nk_context *ctx, root = root->parent; } win->popup.buf.active = 0; + win->popup.last_h = 0; return is_active; } popup->bounds = body; diff --git a/src/nuklear_tooltip.c b/src/nuklear_tooltip.c index 49ac2ca9f..2f1c928e4 100644 --- a/src/nuklear_tooltip.c +++ b/src/nuklear_tooltip.c @@ -87,8 +87,8 @@ nk_tooltip_begin_offset(struct nk_context *ctx, float width, enum nk_tooltip_pos float clip_y = win->layout->clip.y; if (win->popup.win && win->popup.type == NK_PANEL_TOOLTIP && - win->popup.win->bounds.h > known_h) - known_h = win->popup.win->bounds.h; + win->popup.last_h > known_h) + known_h = win->popup.last_h; screen.x = (float)x + clip_x; screen.y = (float)y + clip_y; From 84383e9f8896ee23dfc05f1cee99c394f237a276 Mon Sep 17 00:00:00 2001 From: Robert Winkler Date: Wed, 2 Sep 2026 00:30:41 -0700 Subject: [PATCH 3/6] Fix bugs: Use real height not user given max height... ...for determining whether a popup will fit as a dropdown vs needing to drop "up". Additionally, pin the direction so a popup menu won't flip back and forth as you expand/collapse a tree inside of it. --- demo/common/overview.c | 24 ++++++++++++----------- nuklear.h | 42 ++++++++++++++++++++++++++-------------- src/nuklear.h | 1 + src/nuklear_chart.c | 4 +++- src/nuklear_combo.c | 4 +++- src/nuklear_contextual.c | 2 +- src/nuklear_internal.h | 2 +- src/nuklear_menu.c | 4 +++- src/nuklear_popup.c | 23 ++++++++++++++-------- src/nuklear_tooltip.c | 2 +- 10 files changed, 69 insertions(+), 39 deletions(-) diff --git a/demo/common/overview.c b/demo/common/overview.c index f75deea32..1ad60aa39 100644 --- a/demo/common/overview.c +++ b/demo/common/overview.c @@ -102,10 +102,11 @@ overview(struct nk_context *ctx) const float values[]={26.0f,13.0f,30.0f,15.0f,25.0f,10.0f,20.0f,40.0f,12.0f,8.0f,22.0f,28.0f}; menu_state = MENU_CHART; nk_layout_row_dynamic(ctx, 150, 1); - nk_chart_begin(ctx, NK_CHART_COLUMN, NK_LEN(values), 0, 50); - for (i = 0; i < NK_LEN(values); ++i) - nk_chart_push(ctx, values[i]); - nk_chart_end(ctx); + if (nk_chart_begin(ctx, NK_CHART_COLUMN, NK_LEN(values), 0, 50)) { + for (i = 0; i < NK_LEN(values); ++i) + nk_chart_push(ctx, values[i]); + nk_chart_end(ctx); + } nk_tree_pop(ctx); } else menu_state = (menu_state == MENU_CHART) ? MENU_NONE: menu_state; nk_menu_end(ctx); @@ -491,15 +492,16 @@ overview(struct nk_context *ctx) size_t i = 0; static const float values[]={26.0f,13.0f,30.0f,15.0f,25.0f,10.0f,20.0f,40.0f, 12.0f, 8.0f, 22.0f, 28.0f, 5.0f}; nk_layout_row_dynamic(ctx, 150, 1); - nk_chart_begin(ctx, NK_CHART_COLUMN, NK_LEN(values), 0, 50); - for (i = 0; i < NK_LEN(values); ++i) { - nk_flags res = nk_chart_push(ctx, values[i]); - if (res & NK_CHART_CLICKED) { - chart_selection = values[i]; - nk_combo_close(ctx); + if (nk_chart_begin(ctx, NK_CHART_COLUMN, NK_LEN(values), 0, 50)) { + for (i = 0; i < NK_LEN(values); ++i) { + nk_flags res = nk_chart_push(ctx, values[i]); + if (res & NK_CHART_CLICKED) { + chart_selection = values[i]; + nk_combo_close(ctx); + } } + nk_chart_end(ctx); } - nk_chart_end(ctx); nk_combo_end(ctx); } diff --git a/nuklear.h b/nuklear.h index 7f4b0e260..8c0c902eb 100644 --- a/nuklear.h +++ b/nuklear.h @@ -5794,6 +5794,7 @@ struct nk_popup_state { unsigned active_con; struct nk_rect header; float last_h; /**< last frame's content height; used to flip without a gap */ + nk_bool pinned_up; /**< keep drop-up once chosen so expand/collapse does not jump */ }; struct nk_edit_state { @@ -6373,7 +6374,7 @@ enum nk_popup_fit { NK_POPUP_FIT_SLIDE, /* contextual: slide to stay on-screen */ NK_POPUP_FIT_TOOLTIP /* tooltip: flip around cursor, then slide */ }; -NK_LIB struct nk_rect nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, struct nk_rect anchor, enum nk_popup_fit fit, float known_h); +NK_LIB struct nk_rect nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, struct nk_rect anchor, enum nk_popup_fit fit, float known_h, nk_bool stay_up); NK_LIB nk_bool nk_nonblock_begin(struct nk_context *ctx, nk_flags flags, struct nk_rect body, struct nk_rect header, enum nk_panel_type panel_type); /* text */ @@ -21493,7 +21494,8 @@ nk_rule_horizontal(struct nk_context *ctx, struct nk_color color, nk_bool roundi * ===============================================================*/ NK_LIB struct nk_rect nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, - struct nk_rect anchor, enum nk_popup_fit fit, float known_h) + struct nk_rect anchor, enum nk_popup_fit fit, float known_h, + nk_bool stay_up) { struct nk_rect d; float overlap; @@ -21539,23 +21541,28 @@ nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, if (fit == NK_POPUP_FIT_FLIP) { int flip = 0; - if (req_h > space_below) { - /* flip only when the real content fits above. using the - * clamped window height here caused a flip/restore flicker. */ - if (need_h <= space_above) + /* Caller size.y is a maximum (ADVANCED is 600px for expanded trees), + * not the current content. Flip only when last frame's actual height + * does not fit below. First open (known_h == 0) always drops down. + * stay_up keeps a drop-up for the rest of this open so collapsing + * a tree does not jump back to a dropdown. */ + if (stay_up && space_above > 0.0f) + flip = 1; + else if (known_h > 0.0f && known_h > space_below) { + if (known_h <= space_above) flip = 1; - else if (known_h > 0.0f && space_above > space_below) + else if (space_above > space_below) flip = 1; } if (flip) { float use_h = req_h; if (use_h > space_above) use_h = space_above; - if (known_h > 0.0f && known_h < use_h) use_h = known_h; + if (known_h < use_h) use_h = known_h; if (use_h < 1.0f) use_h = 1.0f; body.y = anchor.y - use_h; if (overlap > 0.0f) body.y += overlap; body.h = use_h; - } else if (req_h > space_below) { + } else if (known_h > space_below) { body.h = (space_below > 1.0f) ? space_below : 1.0f; } } else if (fit == NK_POPUP_FIT_SLIDE) { @@ -21729,6 +21736,7 @@ nk_nonblock_begin(struct nk_context *ctx, } win->popup.buf.active = 0; win->popup.last_h = 0; + win->popup.pinned_up = nk_false; return is_active; } popup->bounds = body; @@ -21899,7 +21907,7 @@ nk_contextual_begin(struct nk_context *ctx, nk_flags flags, struct nk_vec2 size, {float known_h = (!is_clicked) ? win->popup.last_h : 0; float req_h = body.h; - body = nk_fit_popup_rect(ctx, body, body, NK_POPUP_FIT_SLIDE, known_h); + body = nk_fit_popup_rect(ctx, body, body, NK_POPUP_FIT_SLIDE, known_h, nk_false); flags |= NK_WINDOW_NO_SCROLLBAR; if (body.h + 0.5f < req_h) flags &= ~(nk_flags)NK_WINDOW_NO_SCROLLBAR;} @@ -22179,7 +22187,9 @@ nk_menu_begin(struct nk_context *ctx, struct nk_window *win, {nk_flags flags = NK_WINDOW_NO_SCROLLBAR; float known_h = (is_active) ? win->popup.last_h : 0; float req_h = body.h; - body = nk_fit_popup_rect(ctx, body, header, NK_POPUP_FIT_FLIP, known_h); + body = nk_fit_popup_rect(ctx, body, header, NK_POPUP_FIT_FLIP, known_h, + is_active && win->popup.pinned_up); + win->popup.pinned_up = (body.y < header.y); if (body.h + 0.5f < req_h) flags = 0; if (!nk_nonblock_begin(ctx, flags, body, header, NK_PANEL_MENU)) @@ -29901,8 +29911,10 @@ nk_chart_push_slot(struct nk_context *ctx, float value, int slot) NK_ASSERT(ctx); NK_ASSERT(ctx->current); NK_ASSERT(slot >= 0 && slot < NK_CHART_MAX_SLOT); - NK_ASSERT(slot < ctx->current->layout->chart.slot); if (!ctx || !ctx->current || slot >= NK_CHART_MAX_SLOT) return nk_false; + /* nk_chart_begin zeros the chart and returns 0 when the widget is + * clipped (e.g. a menu drop-up sized from last frame while a tree + * expands). Pushing in that case is a no-op, not a programmer error. */ if (slot >= ctx->current->layout->chart.slot) return nk_false; win = ctx->current; @@ -30229,7 +30241,9 @@ nk_combo_begin(struct nk_context *ctx, struct nk_window *win, if ((is_clicked && is_open && !is_active) || (is_open && !is_active) || (!is_open && !is_active && !is_clicked)) return 0; {float known_h = (is_active) ? win->popup.last_h : 0; - body = nk_fit_popup_rect(ctx, body, header, NK_POPUP_FIT_FLIP, known_h);} + body = nk_fit_popup_rect(ctx, body, header, NK_POPUP_FIT_FLIP, known_h, + is_active && win->popup.pinned_up); + win->popup.pinned_up = (body.y < header.y);} if (!nk_nonblock_begin(ctx, 0, body, (is_clicked && is_open)?nk_rect(0,0,0,0):header, NK_PANEL_COMBO)) return 0; @@ -31151,7 +31165,7 @@ nk_tooltip_begin_offset(struct nk_context *ctx, float width, enum nk_tooltip_pos anchor.y = in->mouse.pos.y; anchor.w = 1; anchor.h = 1; - screen = nk_fit_popup_rect(ctx, screen, anchor, NK_POPUP_FIT_TOOLTIP, known_h); + screen = nk_fit_popup_rect(ctx, screen, anchor, NK_POPUP_FIT_TOOLTIP, known_h, nk_false); bounds.x = screen.x - clip_x; bounds.y = screen.y - clip_y; diff --git a/src/nuklear.h b/src/nuklear.h index 04b83814d..d100d5f9c 100644 --- a/src/nuklear.h +++ b/src/nuklear.h @@ -5604,6 +5604,7 @@ struct nk_popup_state { unsigned active_con; struct nk_rect header; float last_h; /**< last frame's content height; used to flip without a gap */ + nk_bool pinned_up; /**< keep drop-up once chosen so expand/collapse does not jump */ }; struct nk_edit_state { diff --git a/src/nuklear_chart.c b/src/nuklear_chart.c index 87301faa8..0ca3e1715 100644 --- a/src/nuklear_chart.c +++ b/src/nuklear_chart.c @@ -244,8 +244,10 @@ nk_chart_push_slot(struct nk_context *ctx, float value, int slot) NK_ASSERT(ctx); NK_ASSERT(ctx->current); NK_ASSERT(slot >= 0 && slot < NK_CHART_MAX_SLOT); - NK_ASSERT(slot < ctx->current->layout->chart.slot); if (!ctx || !ctx->current || slot >= NK_CHART_MAX_SLOT) return nk_false; + /* nk_chart_begin zeros the chart and returns 0 when the widget is + * clipped (e.g. a menu drop-up sized from last frame while a tree + * expands). Pushing in that case is a no-op, not a programmer error. */ if (slot >= ctx->current->layout->chart.slot) return nk_false; win = ctx->current; diff --git a/src/nuklear_combo.c b/src/nuklear_combo.c index 529b3c727..f1f11ddf7 100644 --- a/src/nuklear_combo.c +++ b/src/nuklear_combo.c @@ -34,7 +34,9 @@ nk_combo_begin(struct nk_context *ctx, struct nk_window *win, if ((is_clicked && is_open && !is_active) || (is_open && !is_active) || (!is_open && !is_active && !is_clicked)) return 0; {float known_h = (is_active) ? win->popup.last_h : 0; - body = nk_fit_popup_rect(ctx, body, header, NK_POPUP_FIT_FLIP, known_h);} + body = nk_fit_popup_rect(ctx, body, header, NK_POPUP_FIT_FLIP, known_h, + is_active && win->popup.pinned_up); + win->popup.pinned_up = (body.y < header.y);} if (!nk_nonblock_begin(ctx, 0, body, (is_clicked && is_open)?nk_rect(0,0,0,0):header, NK_PANEL_COMBO)) return 0; diff --git a/src/nuklear_contextual.c b/src/nuklear_contextual.c index 454c91517..948998afe 100644 --- a/src/nuklear_contextual.c +++ b/src/nuklear_contextual.c @@ -59,7 +59,7 @@ nk_contextual_begin(struct nk_context *ctx, nk_flags flags, struct nk_vec2 size, {float known_h = (!is_clicked) ? win->popup.last_h : 0; float req_h = body.h; - body = nk_fit_popup_rect(ctx, body, body, NK_POPUP_FIT_SLIDE, known_h); + body = nk_fit_popup_rect(ctx, body, body, NK_POPUP_FIT_SLIDE, known_h, nk_false); flags |= NK_WINDOW_NO_SCROLLBAR; if (body.h + 0.5f < req_h) flags &= ~(nk_flags)NK_WINDOW_NO_SCROLLBAR;} diff --git a/src/nuklear_internal.h b/src/nuklear_internal.h index 4f5cf644e..089ac5192 100644 --- a/src/nuklear_internal.h +++ b/src/nuklear_internal.h @@ -251,7 +251,7 @@ enum nk_popup_fit { NK_POPUP_FIT_SLIDE, /* contextual: slide to stay on-screen */ NK_POPUP_FIT_TOOLTIP /* tooltip: flip around cursor, then slide */ }; -NK_LIB struct nk_rect nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, struct nk_rect anchor, enum nk_popup_fit fit, float known_h); +NK_LIB struct nk_rect nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, struct nk_rect anchor, enum nk_popup_fit fit, float known_h, nk_bool stay_up); NK_LIB nk_bool nk_nonblock_begin(struct nk_context *ctx, nk_flags flags, struct nk_rect body, struct nk_rect header, enum nk_panel_type panel_type); /* text */ diff --git a/src/nuklear_menu.c b/src/nuklear_menu.c index 49567ada4..c45bfd7b9 100644 --- a/src/nuklear_menu.c +++ b/src/nuklear_menu.c @@ -105,7 +105,9 @@ nk_menu_begin(struct nk_context *ctx, struct nk_window *win, {nk_flags flags = NK_WINDOW_NO_SCROLLBAR; float known_h = (is_active) ? win->popup.last_h : 0; float req_h = body.h; - body = nk_fit_popup_rect(ctx, body, header, NK_POPUP_FIT_FLIP, known_h); + body = nk_fit_popup_rect(ctx, body, header, NK_POPUP_FIT_FLIP, known_h, + is_active && win->popup.pinned_up); + win->popup.pinned_up = (body.y < header.y); if (body.h + 0.5f < req_h) flags = 0; if (!nk_nonblock_begin(ctx, flags, body, header, NK_PANEL_MENU)) diff --git a/src/nuklear_popup.c b/src/nuklear_popup.c index 933d49334..304985ad4 100644 --- a/src/nuklear_popup.c +++ b/src/nuklear_popup.c @@ -8,7 +8,8 @@ * ===============================================================*/ NK_LIB struct nk_rect nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, - struct nk_rect anchor, enum nk_popup_fit fit, float known_h) + struct nk_rect anchor, enum nk_popup_fit fit, float known_h, + nk_bool stay_up) { struct nk_rect d; float overlap; @@ -54,23 +55,28 @@ nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, if (fit == NK_POPUP_FIT_FLIP) { int flip = 0; - if (req_h > space_below) { - /* flip only when the real content fits above. using the - * clamped window height here caused a flip/restore flicker. */ - if (need_h <= space_above) + /* Caller size.y is a maximum (ADVANCED is 600px for expanded trees), + * not the current content. Flip only when last frame's actual height + * does not fit below. First open (known_h == 0) always drops down. + * stay_up keeps a drop-up for the rest of this open so collapsing + * a tree does not jump back to a dropdown. */ + if (stay_up && space_above > 0.0f) + flip = 1; + else if (known_h > 0.0f && known_h > space_below) { + if (known_h <= space_above) flip = 1; - else if (known_h > 0.0f && space_above > space_below) + else if (space_above > space_below) flip = 1; } if (flip) { float use_h = req_h; if (use_h > space_above) use_h = space_above; - if (known_h > 0.0f && known_h < use_h) use_h = known_h; + if (known_h < use_h) use_h = known_h; if (use_h < 1.0f) use_h = 1.0f; body.y = anchor.y - use_h; if (overlap > 0.0f) body.y += overlap; body.h = use_h; - } else if (req_h > space_below) { + } else if (known_h > space_below) { body.h = (space_below > 1.0f) ? space_below : 1.0f; } } else if (fit == NK_POPUP_FIT_SLIDE) { @@ -244,6 +250,7 @@ nk_nonblock_begin(struct nk_context *ctx, } win->popup.buf.active = 0; win->popup.last_h = 0; + win->popup.pinned_up = nk_false; return is_active; } popup->bounds = body; diff --git a/src/nuklear_tooltip.c b/src/nuklear_tooltip.c index 2f1c928e4..3de453e7e 100644 --- a/src/nuklear_tooltip.c +++ b/src/nuklear_tooltip.c @@ -98,7 +98,7 @@ nk_tooltip_begin_offset(struct nk_context *ctx, float width, enum nk_tooltip_pos anchor.y = in->mouse.pos.y; anchor.w = 1; anchor.h = 1; - screen = nk_fit_popup_rect(ctx, screen, anchor, NK_POPUP_FIT_TOOLTIP, known_h); + screen = nk_fit_popup_rect(ctx, screen, anchor, NK_POPUP_FIT_TOOLTIP, known_h, nk_false); bounds.x = screen.x - clip_x; bounds.y = screen.y - clip_y; From 9eb9bdccddeb45f0097b4497ec7a918a5c609a0e Mon Sep 17 00:00:00 2001 From: Robert Winkler Date: Wed, 2 Sep 2026 01:01:06 -0700 Subject: [PATCH 4/6] Fix the same bug for contextual menus, then a related bug for unnecessary scrollbars --- nuklear.h | 55 +++++++++++++++++++++++++--------------- src/nuklear_contextual.c | 31 +++++++++++++--------- src/nuklear_menu.c | 4 +-- src/nuklear_popup.c | 20 ++++++++++----- 4 files changed, 68 insertions(+), 42 deletions(-) diff --git a/nuklear.h b/nuklear.h index 8c0c902eb..cb39e9cbc 100644 --- a/nuklear.h +++ b/nuklear.h @@ -21566,13 +21566,19 @@ nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, body.h = (space_below > 1.0f) ? space_below : 1.0f; } } else if (fit == NK_POPUP_FIT_SLIDE) { - if (body.y + need_h > d_bottom) - body.y = d_bottom - need_h; - if (body.y < d.y) - body.y = d.y; - if (body.y + body.h > d_bottom) - body.h = d_bottom - body.y; - if (body.h < 1.0f) body.h = 1.0f; + /* size.y is a maximum, not current content. First open (known_h == 0) + * stays on the click; later frames slide only if actual height + * does not fit below. Sliding with the max left the DYNAMIC-shrunk + * panel floating above the cursor. */ + if (known_h > 0.0f && known_h > space_below) { + body.y = d_bottom - known_h; + if (body.y < d.y) + body.y = d.y; + if (body.y + known_h > d_bottom) + body.h = d_bottom - body.y; + else body.h = known_h; + if (body.h < 1.0f) body.h = 1.0f; + } } else { /* tooltip: flip then slide; do not shrink (clipping is acceptable) */ if (body.y + need_h > d_bottom) { @@ -21892,36 +21898,43 @@ nk_contextual_begin(struct nk_context *ctx, nk_flags flags, struct nk_vec2 size, if ((!is_open && !is_clicked)) return 0; - /* calculate contextual position on click */ + /* calculate contextual position on click; keep the click as the + * anchor every frame (combo/menu re-fit from the trigger). */ win->popup.active_con = win->popup.con_count; + {struct nk_rect anchor; if (is_clicked) { - body.x = in->mouse.pos.x; - body.y = in->mouse.pos.y; + anchor.x = in->mouse.pos.x; + anchor.y = in->mouse.pos.y; } else { - body.x = popup->bounds.x; - body.y = popup->bounds.y; + anchor.x = win->popup.header.x; + anchor.y = win->popup.header.y; } - + anchor.w = 1; + anchor.h = 1; + body.x = anchor.x; + body.y = anchor.y; body.w = size.x; body.h = size.y; {float known_h = (!is_clicked) ? win->popup.last_h : 0; - float req_h = body.h; - body = nk_fit_popup_rect(ctx, body, body, NK_POPUP_FIT_SLIDE, known_h, nk_false); + body = nk_fit_popup_rect(ctx, body, anchor, NK_POPUP_FIT_SLIDE, known_h, nk_false); flags |= NK_WINDOW_NO_SCROLLBAR; - if (body.h + 0.5f < req_h) + /* scrollbar only if actual content does not fit in the display */ + if (known_h > 0.0f && body.h + 0.5f < known_h) flags &= ~(nk_flags)NK_WINDOW_NO_SCROLLBAR;} /* start nonblocking contextual popup */ ret = nk_nonblock_begin(ctx, flags, body, null_rect, NK_PANEL_CONTEXTUAL); - if (ret) win->popup.type = NK_PANEL_CONTEXTUAL; - else { + if (ret) { + win->popup.type = NK_PANEL_CONTEXTUAL; + win->popup.header = anchor; + } else { win->popup.active_con = 0; win->popup.type = NK_PANEL_NONE; if (win->popup.win) win->popup.win->flags = 0; - } + }} } return ret; } @@ -22186,11 +22199,11 @@ nk_menu_begin(struct nk_context *ctx, struct nk_window *win, (!is_open && !is_active && !is_clicked)) return 0; {nk_flags flags = NK_WINDOW_NO_SCROLLBAR; float known_h = (is_active) ? win->popup.last_h : 0; - float req_h = body.h; body = nk_fit_popup_rect(ctx, body, header, NK_POPUP_FIT_FLIP, known_h, is_active && win->popup.pinned_up); win->popup.pinned_up = (body.y < header.y); - if (body.h + 0.5f < req_h) + /* scrollbar only if actual content does not fit in the display */ + if (known_h > 0.0f && body.h + 0.5f < known_h) flags = 0; if (!nk_nonblock_begin(ctx, flags, body, header, NK_PANEL_MENU)) return 0;} diff --git a/src/nuklear_contextual.c b/src/nuklear_contextual.c index 948998afe..f01a6c2f7 100644 --- a/src/nuklear_contextual.c +++ b/src/nuklear_contextual.c @@ -44,36 +44,43 @@ nk_contextual_begin(struct nk_context *ctx, nk_flags flags, struct nk_vec2 size, if ((!is_open && !is_clicked)) return 0; - /* calculate contextual position on click */ + /* calculate contextual position on click; keep the click as the + * anchor every frame (combo/menu re-fit from the trigger). */ win->popup.active_con = win->popup.con_count; + {struct nk_rect anchor; if (is_clicked) { - body.x = in->mouse.pos.x; - body.y = in->mouse.pos.y; + anchor.x = in->mouse.pos.x; + anchor.y = in->mouse.pos.y; } else { - body.x = popup->bounds.x; - body.y = popup->bounds.y; + anchor.x = win->popup.header.x; + anchor.y = win->popup.header.y; } - + anchor.w = 1; + anchor.h = 1; + body.x = anchor.x; + body.y = anchor.y; body.w = size.x; body.h = size.y; {float known_h = (!is_clicked) ? win->popup.last_h : 0; - float req_h = body.h; - body = nk_fit_popup_rect(ctx, body, body, NK_POPUP_FIT_SLIDE, known_h, nk_false); + body = nk_fit_popup_rect(ctx, body, anchor, NK_POPUP_FIT_SLIDE, known_h, nk_false); flags |= NK_WINDOW_NO_SCROLLBAR; - if (body.h + 0.5f < req_h) + /* scrollbar only if actual content does not fit in the display */ + if (known_h > 0.0f && body.h + 0.5f < known_h) flags &= ~(nk_flags)NK_WINDOW_NO_SCROLLBAR;} /* start nonblocking contextual popup */ ret = nk_nonblock_begin(ctx, flags, body, null_rect, NK_PANEL_CONTEXTUAL); - if (ret) win->popup.type = NK_PANEL_CONTEXTUAL; - else { + if (ret) { + win->popup.type = NK_PANEL_CONTEXTUAL; + win->popup.header = anchor; + } else { win->popup.active_con = 0; win->popup.type = NK_PANEL_NONE; if (win->popup.win) win->popup.win->flags = 0; - } + }} } return ret; } diff --git a/src/nuklear_menu.c b/src/nuklear_menu.c index c45bfd7b9..f79f94eaf 100644 --- a/src/nuklear_menu.c +++ b/src/nuklear_menu.c @@ -104,11 +104,11 @@ nk_menu_begin(struct nk_context *ctx, struct nk_window *win, (!is_open && !is_active && !is_clicked)) return 0; {nk_flags flags = NK_WINDOW_NO_SCROLLBAR; float known_h = (is_active) ? win->popup.last_h : 0; - float req_h = body.h; body = nk_fit_popup_rect(ctx, body, header, NK_POPUP_FIT_FLIP, known_h, is_active && win->popup.pinned_up); win->popup.pinned_up = (body.y < header.y); - if (body.h + 0.5f < req_h) + /* scrollbar only if actual content does not fit in the display */ + if (known_h > 0.0f && body.h + 0.5f < known_h) flags = 0; if (!nk_nonblock_begin(ctx, flags, body, header, NK_PANEL_MENU)) return 0;} diff --git a/src/nuklear_popup.c b/src/nuklear_popup.c index 304985ad4..6ec7bc7ab 100644 --- a/src/nuklear_popup.c +++ b/src/nuklear_popup.c @@ -80,13 +80,19 @@ nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, body.h = (space_below > 1.0f) ? space_below : 1.0f; } } else if (fit == NK_POPUP_FIT_SLIDE) { - if (body.y + need_h > d_bottom) - body.y = d_bottom - need_h; - if (body.y < d.y) - body.y = d.y; - if (body.y + body.h > d_bottom) - body.h = d_bottom - body.y; - if (body.h < 1.0f) body.h = 1.0f; + /* size.y is a maximum, not current content. First open (known_h == 0) + * stays on the click; later frames slide only if actual height + * does not fit below. Sliding with the max left the DYNAMIC-shrunk + * panel floating above the cursor. */ + if (known_h > 0.0f && known_h > space_below) { + body.y = d_bottom - known_h; + if (body.y < d.y) + body.y = d.y; + if (body.y + known_h > d_bottom) + body.h = d_bottom - body.y; + else body.h = known_h; + if (body.h < 1.0f) body.h = 1.0f; + } } else { /* tooltip: flip then slide; do not shrink (clipping is acceptable) */ if (body.y + need_h > d_bottom) { From 6b6a02d66d2df8c68272bd8a1220cd92da1393a3 Mon Sep 17 00:00:00 2001 From: Robert Winkler Date: Wed, 2 Sep 2026 01:10:16 -0700 Subject: [PATCH 5/6] Same unecessary scrollbar issue for combos --- nuklear.h | 13 +++++++++---- src/nuklear_combo.c | 13 +++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/nuklear.h b/nuklear.h index cb39e9cbc..d8db90b3d 100644 --- a/nuklear.h +++ b/nuklear.h @@ -30253,12 +30253,17 @@ nk_combo_begin(struct nk_context *ctx, struct nk_window *win, is_active = (popup && (win->popup.name == hash) && win->popup.type == NK_PANEL_COMBO); if ((is_clicked && is_open && !is_active) || (is_open && !is_active) || (!is_open && !is_active && !is_clicked)) return 0; - {float known_h = (is_active) ? win->popup.last_h : 0; + {nk_flags flags = NK_WINDOW_NO_SCROLLBAR; + float known_h = (is_active) ? win->popup.last_h : 0; body = nk_fit_popup_rect(ctx, body, header, NK_POPUP_FIT_FLIP, known_h, is_active && win->popup.pinned_up); - win->popup.pinned_up = (body.y < header.y);} - if (!nk_nonblock_begin(ctx, 0, body, - (is_clicked && is_open)?nk_rect(0,0,0,0):header, NK_PANEL_COMBO)) return 0; + win->popup.pinned_up = (body.y < header.y); + /* scrollbar only if actual content does not fit in the fitted panel + * (caller size.y is a max; flipping to last_h must not force a bar) */ + if (known_h > 0.0f && body.h + 0.5f < known_h) + flags = 0; + if (!nk_nonblock_begin(ctx, flags, body, + (is_clicked && is_open)?nk_rect(0,0,0,0):header, NK_PANEL_COMBO)) return 0;} win->popup.type = NK_PANEL_COMBO; win->popup.name = hash; diff --git a/src/nuklear_combo.c b/src/nuklear_combo.c index f1f11ddf7..0ce65b923 100644 --- a/src/nuklear_combo.c +++ b/src/nuklear_combo.c @@ -33,12 +33,17 @@ nk_combo_begin(struct nk_context *ctx, struct nk_window *win, is_active = (popup && (win->popup.name == hash) && win->popup.type == NK_PANEL_COMBO); if ((is_clicked && is_open && !is_active) || (is_open && !is_active) || (!is_open && !is_active && !is_clicked)) return 0; - {float known_h = (is_active) ? win->popup.last_h : 0; + {nk_flags flags = NK_WINDOW_NO_SCROLLBAR; + float known_h = (is_active) ? win->popup.last_h : 0; body = nk_fit_popup_rect(ctx, body, header, NK_POPUP_FIT_FLIP, known_h, is_active && win->popup.pinned_up); - win->popup.pinned_up = (body.y < header.y);} - if (!nk_nonblock_begin(ctx, 0, body, - (is_clicked && is_open)?nk_rect(0,0,0,0):header, NK_PANEL_COMBO)) return 0; + win->popup.pinned_up = (body.y < header.y); + /* scrollbar only if actual content does not fit in the fitted panel + * (caller size.y is a max; flipping to last_h must not force a bar) */ + if (known_h > 0.0f && body.h + 0.5f < known_h) + flags = 0; + if (!nk_nonblock_begin(ctx, flags, body, + (is_clicked && is_open)?nk_rect(0,0,0,0):header, NK_PANEL_COMBO)) return 0;} win->popup.type = NK_PANEL_COMBO; win->popup.name = hash; From 525cab132f214fb48825f5f39d4c0fcc8906e86a Mon Sep 17 00:00:00 2001 From: Robert Winkler Date: Fri, 4 Sep 2026 19:43:59 -0700 Subject: [PATCH 6/6] Fix unnecessary combo flips When user given height is less than content height, use that (will have a scrollbar) so we don't flip unnecessarily Opposite of Menu example of larger than needed requested height, in which case we use real content height to determine whether to flip --- nuklear.h | 6 ++++++ src/nuklear_popup.c | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/nuklear.h b/nuklear.h index d8db90b3d..b63b91f26 100644 --- a/nuklear.h +++ b/nuklear.h @@ -21513,6 +21513,12 @@ nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, d_right = d.x + d.w; d_bottom = d.y + d.h; req_h = body.h; + /* last_h is unclamped layout height (every row, including those that + * would scroll). Combos pass a smaller panel (e.g. 200 with 370px of + * items); menus pass a larger max (e.g. 600 with 80px of trees). + * Judge the visible panel: min(content, caller max). */ + if (known_h > req_h) + known_h = req_h; need_h = (known_h > 0.0f) ? known_h : req_h; overlap = (anchor.y + anchor.h) - body.y; diff --git a/src/nuklear_popup.c b/src/nuklear_popup.c index 6ec7bc7ab..94465e0e5 100644 --- a/src/nuklear_popup.c +++ b/src/nuklear_popup.c @@ -27,6 +27,12 @@ nk_fit_popup_rect(const struct nk_context *ctx, struct nk_rect body, d_right = d.x + d.w; d_bottom = d.y + d.h; req_h = body.h; + /* last_h is unclamped layout height (every row, including those that + * would scroll). Combos pass a smaller panel (e.g. 200 with 370px of + * items); menus pass a larger max (e.g. 600 with 80px of trees). + * Judge the visible panel: min(content, caller max). */ + if (known_h > req_h) + known_h = req_h; need_h = (known_h > 0.0f) ? known_h : req_h; overlap = (anchor.y + anchor.h) - body.y;