From 02af06ea5f57696b93f0dfe77a9e2525522ba76e Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 4 Sep 2025 18:13:49 +0200 Subject: [PATCH 01/17] Backends: Vulkan: rewrite pColorAttachmentFormats deep-copy to avoid issues when calling multiple times. (#8282, #8110) --- backends/imgui_impl_vulkan.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backends/imgui_impl_vulkan.cpp b/backends/imgui_impl_vulkan.cpp index 65fecb41e..ad520f6b8 100644 --- a/backends/imgui_impl_vulkan.cpp +++ b/backends/imgui_impl_vulkan.cpp @@ -1164,8 +1164,10 @@ void ImGui_ImplVulkan_CreateMainPipeline(const ImGui_ImplVulkan_MainPipelineCrea if (v->PipelineRenderingCreateInfo.pColorAttachmentFormats != NULL) { // Deep copy buffer to reduce error-rate for end user (#8282) - bd->PipelineRenderingCreateInfoColorAttachmentFormats.resize((int)v->PipelineRenderingCreateInfo.colorAttachmentCount); - memcpy(bd->PipelineRenderingCreateInfoColorAttachmentFormats.Data, v->PipelineRenderingCreateInfo.pColorAttachmentFormats, (size_t)bd->PipelineRenderingCreateInfoColorAttachmentFormats.size_in_bytes()); + ImVector formats; + formats.resize((int)v->PipelineRenderingCreateInfo.colorAttachmentCount); + memcpy(formats.Data, v->PipelineRenderingCreateInfo.pColorAttachmentFormats, (size_t)formats.size_in_bytes()); + formats.swap(bd->PipelineRenderingCreateInfoColorAttachmentFormats); v->PipelineRenderingCreateInfo.pColorAttachmentFormats = bd->PipelineRenderingCreateInfoColorAttachmentFormats.Data; } } From 09ebcf1779d77153cfb8e5b17ae316f578760e97 Mon Sep 17 00:00:00 2001 From: fdsa <14tanks999@gmail.com> Date: Sat, 6 Sep 2025 01:18:35 -0700 Subject: [PATCH 02/17] Docs: fixed mismatched parentheses & other small changes. (#8922) --- backends/imgui_impl_glfw.cpp | 2 +- backends/imgui_impl_vulkan.cpp | 6 +++--- backends/imgui_impl_vulkan.h | 6 +++--- docs/CHANGELOG.txt | 6 +++--- imgui.cpp | 12 ++++++------ imgui.h | 20 ++++++++++---------- imgui_demo.cpp | 10 +++++----- imgui_draw.cpp | 2 +- imgui_internal.h | 2 +- imgui_tables.cpp | 2 +- imgui_widgets.cpp | 8 ++++---- 11 files changed, 38 insertions(+), 38 deletions(-) diff --git a/backends/imgui_impl_glfw.cpp b/backends/imgui_impl_glfw.cpp index 98e07ce5c..0cb49fda2 100644 --- a/backends/imgui_impl_glfw.cpp +++ b/backends/imgui_impl_glfw.cpp @@ -594,7 +594,7 @@ void ImGui_ImplGlfw_RestoreCallbacks(GLFWwindow* window) bd->PrevUserCallbackMonitor = nullptr; } -// Set to 'true' to enable chaining installed callbacks for all windows (including secondary viewports created by backends or by user. +// Set to 'true' to enable chaining installed callbacks for all windows (including secondary viewports created by backends or by user). // This is 'false' by default meaning we only chain callbacks for the main viewport. // We cannot set this to 'true' by default because user callbacks code may be not testing the 'window' parameter of their callback. // If you set this to 'true' your user callback code will need to make sure you are testing the 'window' parameter. diff --git a/backends/imgui_impl_vulkan.cpp b/backends/imgui_impl_vulkan.cpp index ad520f6b8..aa8d3092c 100644 --- a/backends/imgui_impl_vulkan.cpp +++ b/backends/imgui_impl_vulkan.cpp @@ -120,7 +120,7 @@ void ImGui_ImplVulkanH_CreateWindowSwapChain(VkPhysicalDevice physical_device, V void ImGui_ImplVulkanH_CreateWindowCommandBuffers(VkPhysicalDevice physical_device, VkDevice device, ImGui_ImplVulkanH_Window* wd, uint32_t queue_family, const VkAllocationCallbacks* allocator); // Vulkan prototypes for use with custom loaders -// (see description of IMGUI_IMPL_VULKAN_NO_PROTOTYPES in imgui_impl_vulkan.h +// (see description of IMGUI_IMPL_VULKAN_NO_PROTOTYPES in imgui_impl_vulkan.h) #if defined(VK_NO_PROTOTYPES) && !defined(VOLK_H_) #define IMGUI_IMPL_VULKAN_USE_LOADER static bool g_FunctionsLoaded = false; @@ -1411,7 +1411,7 @@ void ImGui_ImplVulkan_DestroyWindowRenderBuffers(VkDevice device, ImGui_ImplVulk //------------------------------------------------------------------------- // Internal / Miscellaneous Vulkan Helpers -// (Used by example's main.cpp. Used by multi-viewport features. PROBABLY NOT used by your own app.) +// (Used by example's main.cpp. Used by multi-viewport features. PROBABLY NOT used by your own engine/app.) //------------------------------------------------------------------------- // You probably do NOT need to use or care about those functions. // Those functions only exist because: @@ -1421,7 +1421,7 @@ void ImGui_ImplVulkan_DestroyWindowRenderBuffers(VkDevice device, ImGui_ImplVulk // but it is too much code to duplicate everywhere so we exceptionally expose them. // // Your engine/app will likely _already_ have code to setup all that stuff (swap chain, render pass, frame buffers, etc.). -// You may read this code to learn about Vulkan, but it is recommended you use you own custom tailored code to do equivalent work. +// You may read this code to learn about Vulkan, but it is recommended you use your own custom tailored code to do equivalent work. // (The ImGui_ImplVulkanH_XXX functions do not interact with any of the state used by the regular ImGui_ImplVulkan_XXX functions) //------------------------------------------------------------------------- diff --git a/backends/imgui_impl_vulkan.h b/backends/imgui_impl_vulkan.h index c7f3007dc..a24cb2cdb 100644 --- a/backends/imgui_impl_vulkan.h +++ b/backends/imgui_impl_vulkan.h @@ -124,7 +124,7 @@ struct ImGui_ImplVulkan_MainPipelineCreateInfo VkPipelineRenderingCreateInfoKHR PipelineRenderingCreateInfo; // Optional, valid if .sType == VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR #endif }; -IMGUI_IMPL_API void ImGui_ImplVulkan_CreateMainPipeline(const ImGui_ImplVulkan_MainPipelineCreateInfo& info); // (render_pass xor (p_dynamic_rendering && p_dynamic_rendering is correct (sType and pNext)) +IMGUI_IMPL_API void ImGui_ImplVulkan_CreateMainPipeline(const ImGui_ImplVulkan_MainPipelineCreateInfo& info); // (render_pass xor (p_dynamic_rendering && p_dynamic_rendering is correct (sType and pNext))) // (Advanced) Use e.g. if you need to precisely control the timing of texture updates (e.g. for staged rendering), by setting ImDrawData::Textures = NULL to handle this manually. IMGUI_IMPL_API void ImGui_ImplVulkan_UpdateTexture(ImTextureData* tex); @@ -163,12 +163,12 @@ struct ImGui_ImplVulkan_RenderState // // Your engine/app will likely _already_ have code to setup all that stuff (swap chain, // render pass, frame buffers, etc.). You may read this code if you are curious, but -// it is recommended you use you own custom tailored code to do equivalent work. +// it is recommended you use your own custom tailored code to do equivalent work. // // We don't provide a strong guarantee that we won't change those functions API. // // The ImGui_ImplVulkanH_XXX functions should NOT interact with any of the state used -// by the regular ImGui_ImplVulkan_XXX functions). +// by the regular ImGui_ImplVulkan_XXX functions. //------------------------------------------------------------------------- struct ImGui_ImplVulkanH_Frame; diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index afdcbfc6c..cdc51372c 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -362,7 +362,7 @@ Breaking changes: to 4096 but that limit isn't necessary anymore, and Renderer_TextureMaxWidth covers this) However you may set TexMinWidth = TexMaxWidth for the same effect. - Fonts: if you create and manage ImFontAtlas instances yourself (instead of relying on - ImGuiContext to create one, you'll need to call ImFontAtlasUpdateNewFrame() yourself. + ImGuiContext to create one), you'll need to call ImFontAtlasUpdateNewFrame() yourself. An assert will trigger if you don't. - Fonts: obsoleted ImGui::SetWindowFontScale() which is not useful anymore. Prefer using PushFont(NULL, style.FontSizeBase * factor) or to manipulate other scaling factors. @@ -1144,7 +1144,7 @@ Breaking changes: allows casting any pointer/integer type without warning: - May warn: ImGui::Image((void*)MyTextureData, ...); - May warn: ImGui::Image((void*)(intptr_t)MyTextureData, ...); - - Won't warn: ImGui::Image((ImTextureID)(intptr_t)MyTextureData), ...); + - Won't warn: ImGui::Image((ImTextureID)(intptr_t)MyTextureData, ...); - Note that you can always define ImTextureID to be your own high-level structures (with dedicated constructors and extra render parameters) if you like. - IO: moved ImGuiConfigFlags_NavEnableSetMousePos to standalone io.ConfigNavMoveSetMousePos bool. @@ -4514,7 +4514,7 @@ Breaking Changes: - ShowTestWindow() -> use ShowDemoWindow() - IsRootWindowFocused() -> use IsWindowFocused(ImGuiFocusedFlags_RootWindow) - IsRootWindowOrAnyChildFocused() -> use IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) - - SetNextWindowContentWidth(w) -> use SetNextWindowContentSize(ImVec2(w, 0.0f) + - SetNextWindowContentWidth(w) -> use SetNextWindowContentSize(ImVec2(w, 0.0f)) - GetItemsLineHeightWithSpacing() -> use GetFrameHeightWithSpacing() - ImGuiCol_ChildWindowBg -> use ImGuiCol_ChildBg - ImGuiStyleVar_ChildWindowRounding -> use ImGuiStyleVar_ChildRounding diff --git a/imgui.cpp b/imgui.cpp index a4128f2dc..466f7390d 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -444,7 +444,7 @@ IMPLEMENTING SUPPORT for ImGuiBackendFlags_RendererHasTextures: - Fonts: ImFontConfig::OversampleH/OversampleV default to automatic (== 0) since v1.91.8. It is quite important you keep it automatic until we decide if we want to provide a way to express finer policy, otherwise you will likely waste texture space when using large glyphs. Note that the imgui_freetype backend doesn't use and does not need oversampling. - Fonts: specifying glyph ranges is now unnecessary. The value of ImFontConfig::GlyphRanges[] is only useful for legacy backends. All GetGlyphRangesXXXX() functions are now marked obsolete: GetGlyphRangesDefault(), GetGlyphRangesGreek(), GetGlyphRangesKorean(), GetGlyphRangesJapanese(), GetGlyphRangesChineseSimplifiedCommon(), GetGlyphRangesChineseFull(), GetGlyphRangesCyrillic(), GetGlyphRangesThai(), GetGlyphRangesVietnamese(). - Fonts: removed ImFontAtlas::TexDesiredWidth to enforce a texture width. (#327) - - Fonts: if you create and manage ImFontAtlas instances yourself (instead of relying on ImGuiContext to create one, you'll need to call ImFontAtlasUpdateNewFrame() yourself. An assert will trigger if you don't. + - Fonts: if you create and manage ImFontAtlas instances yourself (instead of relying on ImGuiContext to create one), you'll need to call ImFontAtlasUpdateNewFrame() yourself. An assert will trigger if you don't. - Fonts: obsolete ImGui::SetWindowFontScale() which is not useful anymore. Prefer using 'PushFont(NULL, style.FontSizeBase * factor)' or to manipulate other scaling factors. - Fonts: obsoleted ImFont::Scale which is not useful anymore. - Fonts: generally reworked Internals of ImFontAtlas and ImFont. While in theory a vast majority of users shouldn't be affected, some use cases or extensions might be. Among other things: @@ -526,7 +526,7 @@ IMPLEMENTING SUPPORT for ImGuiBackendFlags_RendererHasTextures: in doubt it is almost always better to do an intermediate intptr_t cast, since it allows casting any pointer/integer type without warning: - May warn: ImGui::Image((void*)MyTextureData, ...); - May warn: ImGui::Image((void*)(intptr_t)MyTextureData, ...); - - Won't warn: ImGui::Image((ImTextureID)(intptr_t)MyTextureData), ...); + - Won't warn: ImGui::Image((ImTextureID)(intptr_t)MyTextureData, ...); - note that you can always define ImTextureID to be your own high-level structures (with dedicated constructors) if you like. - 2024/10/03 (1.91.3) - drags: treat v_min==v_max as a valid clamping range when != 0.0f. Zero is a still special value due to legacy reasons, unless using ImGuiSliderFlags_ClampZeroRange. (#7968, #3361, #76) - drags: extended behavior of ImGuiSliderFlags_AlwaysClamp to include _ClampZeroRange. It considers v_min==v_max==0.0f as a valid clamping range (aka edits not allowed). @@ -813,7 +813,7 @@ IMPLEMENTING SUPPORT for ImGuiBackendFlags_RendererHasTextures: - ShowTestWindow() -> use ShowDemoWindow() - IsRootWindowFocused() -> use IsWindowFocused(ImGuiFocusedFlags_RootWindow) - IsRootWindowOrAnyChildFocused() -> use IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) - - SetNextWindowContentWidth(w) -> use SetNextWindowContentSize(ImVec2(w, 0.0f) + - SetNextWindowContentWidth(w) -> use SetNextWindowContentSize(ImVec2(w, 0.0f)) - GetItemsLineHeightWithSpacing() -> use GetFrameHeightWithSpacing() - ImGuiCol_ChildWindowBg -> use ImGuiCol_ChildBg - ImGuiStyleVar_ChildWindowRounding -> use ImGuiStyleVar_ChildRounding @@ -7329,7 +7329,7 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) } // Process SetNextWindow***() calls - // (FIXME: Consider splitting the HasXXX flags into X/Y components + // (FIXME: Consider splitting the HasXXX flags into X/Y components) bool window_pos_set_by_api = false; bool window_size_x_set_by_api = false, window_size_y_set_by_api = false; if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasPos) @@ -8463,7 +8463,7 @@ void ImGui::SetNextWindowSizeConstraints(const ImVec2& size_min, const ImVec2& s } // Content size = inner scrollable rectangle, padded with WindowPadding. -// SetNextWindowContentSize(ImVec2(100,100) + ImGuiWindowFlags_AlwaysAutoResize will always allow submitting a 100x100 item. +// SetNextWindowContentSize(ImVec2(100,100)) + ImGuiWindowFlags_AlwaysAutoResize will always allow submitting a 100x100 item. void ImGui::SetNextWindowContentSize(const ImVec2& size) { ImGuiContext& g = *GImGui; @@ -17298,7 +17298,7 @@ void ImGui::DebugNodeWindow(ImGuiWindow* window, const char* label) } const ImVec2* pr = window->NavPreferredScoringPosRel; for (int layer = 0; layer < ImGuiNavLayer_COUNT; layer++) - BulletText("NavPreferredScoringPosRel[%d] = {%.1f,%.1f)", layer, (pr[layer].x == FLT_MAX ? -99999.0f : pr[layer].x), (pr[layer].y == FLT_MAX ? -99999.0f : pr[layer].y)); // Display as 99999.0f so it looks neater. + BulletText("NavPreferredScoringPosRel[%d] = (%.1f,%.1f)", layer, (pr[layer].x == FLT_MAX ? -99999.0f : pr[layer].x), (pr[layer].y == FLT_MAX ? -99999.0f : pr[layer].y)); // Display as 99999.0f so it looks neater. BulletText("NavLayersActiveMask: %X, NavLastChildNavWindow: %s", window->DC.NavLayersActiveMask, window->NavLastChildNavWindow ? window->NavLastChildNavWindow->Name : "NULL"); if (window->RootWindow != window) { DebugNodeWindow(window->RootWindow, "RootWindow"); } if (window->ParentWindow != NULL) { DebugNodeWindow(window->ParentWindow, "ParentWindow"); } diff --git a/imgui.h b/imgui.h index cee72416c..befb2d15d 100644 --- a/imgui.h +++ b/imgui.h @@ -1056,8 +1056,8 @@ namespace ImGui // Inputs Utilities: Shortcut Testing & Routing [BETA] // - ImGuiKeyChord = a ImGuiKey + optional ImGuiMod_Alt/ImGuiMod_Ctrl/ImGuiMod_Shift/ImGuiMod_Super. - // ImGuiKey_C // Accepted by functions taking ImGuiKey or ImGuiKeyChord arguments) - // ImGuiMod_Ctrl | ImGuiKey_C // Accepted by functions taking ImGuiKeyChord arguments) + // ImGuiKey_C // Accepted by functions taking ImGuiKey or ImGuiKeyChord arguments + // ImGuiMod_Ctrl | ImGuiKey_C // Accepted by functions taking ImGuiKeyChord arguments // only ImGuiMod_XXX values are legal to combine with an ImGuiKey. You CANNOT combine two ImGuiKey values. // - The general idea is that several callers may register interest in a shortcut, and only one owner gets it. // Parent -> call Shortcut(Ctrl+S) // When Parent is focused, Parent gets the shortcut. @@ -1185,7 +1185,7 @@ enum ImGuiWindowFlags_ }; // Flags for ImGui::BeginChild() -// (Legacy: bit 0 must always correspond to ImGuiChildFlags_Borders to be backward compatible with old API using 'bool border = false'. +// (Legacy: bit 0 must always correspond to ImGuiChildFlags_Borders to be backward compatible with old API using 'bool border = false'.) // About using AutoResizeX/AutoResizeY flags: // - May be combined with SetNextWindowSizeConstraints() to set a min/max size for each axis (see "Demo->Child->Auto-resize with Constraints"). // - Size measurement for a given axis is only performed when the child window is within visible boundaries, or is just appearing. @@ -2433,7 +2433,7 @@ struct ImGuiIO // Option to enable various debug tools showing buttons that will call the IM_DEBUG_BREAK() macro. // - The Item Picker tool will be available regardless of this being enabled, in order to maximize its discoverability. // - Requires a debugger being attached, otherwise IM_DEBUG_BREAK() options will appear to crash your application. - // e.g. io.ConfigDebugIsDebuggerPresent = ::IsDebuggerPresent() on Win32, or refer to ImOsIsDebuggerPresent() imgui_test_engine/imgui_te_utils.cpp for a Unix compatible version). + // e.g. io.ConfigDebugIsDebuggerPresent = ::IsDebuggerPresent() on Win32, or refer to ImOsIsDebuggerPresent() imgui_test_engine/imgui_te_utils.cpp for a Unix compatible version. bool ConfigDebugIsDebuggerPresent; // = false // Enable various tools calling IM_DEBUG_BREAK(). // Tools to detect code submitting items with conflicting/duplicate IDs @@ -2536,7 +2536,7 @@ struct ImGuiIO bool KeySuper; // Keyboard modifier down: Windows/Super (non-macOS), Ctrl (macOS) // Other state maintained from data above + IO function calls - ImGuiKeyChord KeyMods; // Key mods flags (any of ImGuiMod_Ctrl/ImGuiMod_Shift/ImGuiMod_Alt/ImGuiMod_Super flags, same as io.KeyCtrl/KeyShift/KeyAlt/KeySuper but merged into flags. Read-only, updated by NewFrame() + ImGuiKeyChord KeyMods; // Key mods flags (any of ImGuiMod_Ctrl/ImGuiMod_Shift/ImGuiMod_Alt/ImGuiMod_Super flags, same as io.KeyCtrl/KeyShift/KeyAlt/KeySuper but merged into flags). Read-only, updated by NewFrame() ImGuiKeyData KeysData[ImGuiKey_NamedKey_COUNT];// Key state for all known keys. MUST use 'key - ImGuiKey_NamedKey_BEGIN' as index. Use IsKeyXXX() functions to access this. bool WantCaptureMouseUnlessPopupClose; // Alternative to WantCaptureMouse: (WantCaptureMouse == true && WantCaptureMouseUnlessPopupClose == false) when a click over void is expected to close a popup. ImVec2 MousePosPrev; // Previous mouse position (note that MouseDelta is not necessary == MousePos-MousePosPrev, in case either position is invalid) @@ -2613,10 +2613,10 @@ struct ImGuiInputTextCallbackData ImGuiKey EventKey; // Key pressed (Up/Down/TAB) // Read-only // [Completion,History] char* Buf; // Text buffer // Read-write // [Resize] Can replace pointer / [Completion,History,Always] Only write to pointed data, don't replace the actual pointer! int BufTextLen; // Text length (in bytes) // Read-write // [Resize,Completion,History,Always] Exclude zero-terminator storage. In C land: == strlen(some_text), in C++ land: string.length() - int BufSize; // Buffer size (in bytes) = capacity+1 // Read-only // [Resize,Completion,History,Always] Include zero-terminator storage. In C land == ARRAYSIZE(my_char_array), in C++ land: string.capacity()+1 + int BufSize; // Buffer size (in bytes) = capacity+1 // Read-only // [Resize,Completion,History,Always] Include zero-terminator storage. In C land: == ARRAYSIZE(my_char_array), in C++ land: string.capacity()+1 bool BufDirty; // Set if you modify Buf/BufTextLen! // Write // [Completion,History,Always] int CursorPos; // // Read-write // [Completion,History,Always] - int SelectionStart; // // Read-write // [Completion,History,Always] == to SelectionEnd when no selection) + int SelectionStart; // // Read-write // [Completion,History,Always] == to SelectionEnd when no selection int SelectionEnd; // // Read-write // [Completion,History,Always] // Helper functions for text manipulation. @@ -3133,7 +3133,7 @@ struct ImDrawCmd // Since 1.83: returns ImTextureID associated with this draw call. Warning: DO NOT assume this is always same as 'TextureId' (we will change this function for an upcoming feature) // Since 1.92: removed ImDrawCmd::TextureId field, the getter function must be used! - inline ImTextureID GetTexID() const; // == (TexRef._TexData ? TexRef._TexData->TexID : TexRef._TexID + inline ImTextureID GetTexID() const; // == (TexRef._TexData ? TexRef._TexData->TexID : TexRef._TexID) }; // Vertex layout @@ -3630,7 +3630,7 @@ struct ImFontAtlas // - User is in charge of copying the pixels into graphics memory (e.g. create a texture with your engine). Then store your texture handle with SetTexID(). // - The pitch is always = Width * BytesPerPixels (1 or 4) // - Building in RGBA32 format is provided for convenience and compatibility, but note that unless you manually manipulate or copy color data into - // the texture (e.g. when using the AddCustomRect*** api), then the RGB pixels emitted will always be white (~75% of memory/bandwidth waste. + // the texture (e.g. when using the AddCustomRect*** api), then the RGB pixels emitted will always be white (~75% of memory/bandwidth waste). // - From 1.92 with backends supporting ImGuiBackendFlags_RendererHasTextures: // - Calling Build(), GetTexDataAsAlpha8(), GetTexDataAsRGBA32() is not needed. // - In backend: replace calls to ImFontAtlas::SetTexID() with calls to ImTextureData::SetTexID() after honoring texture creation. @@ -3745,7 +3745,7 @@ struct ImFontAtlas IMGUI_API ImFontAtlasRectId AddCustomRectFontGlyphForSize(ImFont* font, float font_size, ImWchar codepoint, int w, int h, float advance_x, const ImVec2& offset = ImVec2(0, 0)); // ADDED AND OBSOLETED in 1.92.X #endif //unsigned int FontBuilderFlags; // OBSOLETED in 1.92.X: Renamed to FontLoaderFlags. - //int TexDesiredWidth; // OBSOLETED in 1.92.X: Force texture width before calling Build(). Must be a power-of-two. If have many glyphs your graphics API have texture size restrictions you may want to increase texture width to decrease height) + //int TexDesiredWidth; // OBSOLETED in 1.92.X: Force texture width before calling Build(). Must be a power-of-two. If have many glyphs your graphics API have texture size restrictions you may want to increase texture width to decrease height. //typedef ImFontAtlasRect ImFontAtlasCustomRect; // OBSOLETED in 1.92.X //typedef ImFontAtlasCustomRect CustomRect; // OBSOLETED in 1.72+ //typedef ImFontGlyphRangesBuilder GlyphRangesBuilder; // OBSOLETED in 1.67+ diff --git a/imgui_demo.cpp b/imgui_demo.cpp index be1698bdd..2a7b9ac39 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3229,7 +3229,7 @@ static void DemoWindowWidgetsSelectionAndMultiSelect(ImGuiDemoWindowData* demo_d ImGui::BeginTable("##Split", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_NoPadOuterX); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch, 0.70f); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch, 0.30f); - //ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacingY, 0.0f); + //ImGui::PushStyleVarY(ImGuiStyleVar_ItemSpacing, 0.0f); } ImGuiListClipper clipper; @@ -5182,7 +5182,7 @@ static void DemoWindowPopups() // Typical use for regular windows: // bool my_tool_is_active = false; if (ImGui::Button("Open")) my_tool_is_active = true; [...] if (my_tool_is_active) Begin("My Tool", &my_tool_is_active) { [...] } End(); // Typical use for popups: - // if (ImGui::Button("Open")) ImGui::OpenPopup("MyPopup"); if (ImGui::BeginPopup("MyPopup") { [...] EndPopup(); } + // if (ImGui::Button("Open")) ImGui::OpenPopup("MyPopup"); if (ImGui::BeginPopup("MyPopup")) { [...] EndPopup(); } // With popups we have to go through a library call (here OpenPopup) to manipulate the visibility state. // This may be a bit confusing at first but it should quickly make sense. Follow on the examples below. @@ -5768,7 +5768,7 @@ static void DemoWindowTables() ImGui::SameLine(); ImGui::RadioButton("Text", &contents_type, CT_Text); ImGui::SameLine(); ImGui::RadioButton("FillButton", &contents_type, CT_FillButton); ImGui::Checkbox("Display headers", &display_headers); - ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBody", &flags, ImGuiTableFlags_NoBordersInBody); ImGui::SameLine(); HelpMarker("Disable vertical borders in columns Body (borders will always appear in Headers"); + ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBody", &flags, ImGuiTableFlags_NoBordersInBody); ImGui::SameLine(); HelpMarker("Disable vertical borders in columns Body (borders will always appear in Headers)"); PopStyleCompact(); if (ImGui::BeginTable("table1", 3, flags)) @@ -7261,7 +7261,7 @@ static void DemoWindowTables() ImGui::CheckboxFlags("ImGuiTableFlags_BordersH", &flags, ImGuiTableFlags_BordersH); ImGui::CheckboxFlags("ImGuiTableFlags_BordersOuterH", &flags, ImGuiTableFlags_BordersOuterH); ImGui::CheckboxFlags("ImGuiTableFlags_BordersInnerH", &flags, ImGuiTableFlags_BordersInnerH); - ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBody", &flags, ImGuiTableFlags_NoBordersInBody); ImGui::SameLine(); HelpMarker("Disable vertical borders in columns Body (borders will always appear in Headers"); + ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBody", &flags, ImGuiTableFlags_NoBordersInBody); ImGui::SameLine(); HelpMarker("Disable vertical borders in columns Body (borders will always appear in Headers)"); ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBodyUntilResize", &flags, ImGuiTableFlags_NoBordersInBodyUntilResize); ImGui::SameLine(); HelpMarker("Disable vertical borders in columns Body until hovered for resize (borders will always appear in Headers)"); ImGui::TreePop(); } @@ -9643,7 +9643,7 @@ static void ShowExampleAppConstrainedResize(bool* p_open) IMGUI_DEMO_MARKER("Examples/Constrained Resizing window"); if (ImGui::GetIO().KeyShift) { - // Display a dummy viewport (in your real app you would likely use ImageButton() to display a texture. + // Display a dummy viewport (in your real app you would likely use ImageButton() to display a texture) ImVec2 avail_size = ImGui::GetContentRegionAvail(); ImVec2 pos = ImGui::GetCursorScreenPos(); ImGui::ColorButton("viewport", ImVec4(0.5f, 0.2f, 0.5f, 1.0f), ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoDragDrop, avail_size); diff --git a/imgui_draw.cpp b/imgui_draw.cpp index ee7f55efa..1eb5b90fe 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -48,7 +48,7 @@ Index of this file: #pragma warning (disable: 4505) // unreferenced local function has been removed (stb stuff) #pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen #pragma warning (disable: 26451) // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to a 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2). -#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). [MSVC Static Analyzer) +#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). #endif // Clang/GCC warnings with -Weverything diff --git a/imgui_internal.h b/imgui_internal.h index 739ae692c..3c792a866 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -77,8 +77,8 @@ Index of this file: #ifdef _MSC_VER #pragma warning (push) #pragma warning (disable: 4251) // class 'xxx' needs to have dll-interface to be used by clients of struct 'xxx' // when IMGUI_API is set to__declspec(dllexport) -#pragma warning (disable: 26812) // The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). [MSVC Static Analyzer) #pragma warning (disable: 26495) // [Static Analyzer] Variable 'XXX' is uninitialized. Always initialize a member variable (type.6). +#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). #if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later #pragma warning (disable: 5054) // operator '|': deprecated between enumerations of different types #endif diff --git a/imgui_tables.cpp b/imgui_tables.cpp index c2b7548c9..ba8f0034f 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -437,7 +437,7 @@ bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImG if (table->InnerWindow->SkipItems && outer_window_is_measuring_size) table->InnerWindow->SkipItems = false; - // When using multiple instances, ensure they have the same amount of horizontal decorations (aka vertical scrollbar) so stretched columns can be aligned) + // When using multiple instances, ensure they have the same amount of horizontal decorations (aka vertical scrollbar) so stretched columns can be aligned if (instance_no == 0) { table->HasScrollbarYPrev = table->HasScrollbarYCurr; diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index f66baf767..31088b7c1 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -4403,7 +4403,7 @@ static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, Im if (c == '.' || c == ',') c = c_decimal_point; - // Full-width -> half-width conversion for numeric fields (https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block) + // Full-width -> half-width conversion for numeric fields: https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block) // While this is mostly convenient, this has the side-effect for uninformed users accidentally inputting full-width characters that they may // scratch their head as to why it works in numerical fields vs in generic text fields it would require support in the font. if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | ImGuiInputTextFlags_CharsHexadecimal)) @@ -8370,7 +8370,7 @@ void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) // - Optimized select can append unsorted, then sort in a second pass. Optimized unselect can clear in-place then compact in a second pass. // - A more optimal version wouldn't even use ImGuiStorage but directly a ImVector to reduce bandwidth, but this is a reasonable trade off to reuse code. // - There are many ways this could be better optimized. The worse case scenario being: using BoxSelect2d in a grid, box-select scrolling down while wiggling - // left and right: it affects coarse clipping + can emit multiple SetRange with 1 item each.) + // left and right: it affects coarse clipping + can emit multiple SetRange with 1 item each. // FIXME-OPT: For each block of consecutive SetRange request: // - add all requests to a sorted list, store ID, selected, offset in ImGuiStorage. // - rewrite sorted storage a single time. @@ -9085,7 +9085,7 @@ bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled) { // Menu inside a regular/vertical menu // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f. - // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system. + // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.) popup_pos = ImVec2(pos.x, pos.y - style.WindowPadding.y); float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f; float checkmark_w = IM_TRUNC(g.FontSize * 1.20f); @@ -9292,7 +9292,7 @@ bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut { // Menu item inside a vertical menu // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f. - // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system. + // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.) float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f; float shortcut_w = (shortcut && shortcut[0]) ? CalcTextSize(shortcut, NULL).x : 0.0f; float checkmark_w = IM_TRUNC(g.FontSize * 1.20f); From bed08daede8b54ca279e9c95235735d26056be8f Mon Sep 17 00:00:00 2001 From: fgungor Date: Mon, 8 Sep 2025 11:32:38 +0200 Subject: [PATCH 03/17] Backends: SDLGPU3: fixed double assignment. (#8924) --- backends/imgui_impl_sdlgpu3.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backends/imgui_impl_sdlgpu3.cpp b/backends/imgui_impl_sdlgpu3.cpp index 8420da4c5..3b6db79e8 100644 --- a/backends/imgui_impl_sdlgpu3.cpp +++ b/backends/imgui_impl_sdlgpu3.cpp @@ -85,7 +85,7 @@ static ImGui_ImplSDLGPU3_Data* ImGui_ImplSDLGPU3_GetBackendData() static void ImGui_ImplSDLGPU3_SetupRenderState(ImDrawData* draw_data, ImGui_ImplSDLGPU3_RenderState* render_state, SDL_GPUGraphicsPipeline* pipeline, SDL_GPUCommandBuffer* command_buffer, SDL_GPURenderPass* render_pass, ImGui_ImplSDLGPU3_FrameData* fd, uint32_t fb_width, uint32_t fb_height) { ImGui_ImplSDLGPU3_Data* bd = ImGui_ImplSDLGPU3_GetBackendData(); - render_state->SamplerCurrent = render_state->SamplerCurrent = bd->TexSampler; + render_state->SamplerCurrent = bd->TexSampler; // Bind graphics pipeline SDL_BindGPUGraphicsPipeline(render_pass, pipeline); From f77f68a5ede94d2113f87713450b936555aa2e7f Mon Sep 17 00:00:00 2001 From: yaz0r <363511+yaz0r@users.noreply.github.com> Date: Sun, 7 Sep 2025 20:27:00 -0700 Subject: [PATCH 04/17] CI: Windows: update to 1.4.326 + pull Vulkan from KhronosGroup's Vulkan-Headers and Vulkan-Loader. (#8925, #8778) --- .github/workflows/build.yml | 19 +++++++++++++++---- docs/CHANGELOG.txt | 1 + 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bb2e502a2..4ff14341e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,14 +30,25 @@ jobs: Invoke-WebRequest -Uri "https://www.libsdl.org/release/SDL2-devel-2.32.8-VC.zip" -OutFile "SDL2-devel-2.32.8-VC.zip" Expand-Archive -Path SDL2-devel-2.32.8-VC.zip echo "SDL2_DIR=$(pwd)\SDL2-devel-2.32.8-VC\SDL2-2.32.8\" >>${env:GITHUB_ENV} - + Invoke-WebRequest -Uri "https://www.libsdl.org/release/SDL3-devel-3.2.18-VC.zip" -OutFile "SDL3-devel-3.2.18-VC.zip" Expand-Archive -Path SDL3-devel-3.2.18-VC.zip echo "SDL3_DIR=$(pwd)\SDL3-devel-3.2.18-VC\SDL3-3.2.18\" >>${env:GITHUB_ENV} - Invoke-WebRequest -Uri "https://github.com/ocornut/imgui/files/3789205/vulkan-sdk-1.1.121.2.zip" -OutFile vulkan-sdk-1.1.121.2.zip - Expand-Archive -Path vulkan-sdk-1.1.121.2.zip - echo "VULKAN_SDK=$(pwd)\vulkan-sdk-1.1.121.2\" >>${env:GITHUB_ENV} + # VulkanSDK (retrieve minimal bits of the SDK from git) + $vulkanVersion = "1.4.326" + # 1. Get the vulkan headers, we will treat that folder as the sdk folder to avoid having to copy headers around + Invoke-WebRequest -Uri "https://github.com/KhronosGroup/Vulkan-Headers/archive/refs/tags/v$($vulkanVersion).zip" -OutFile Vulkan-Headers-$($vulkanVersion).zip + Expand-Archive -Path Vulkan-Headers-$($vulkanVersion).zip + echo "VULKAN_SDK=$(pwd)\Vulkan-Headers-$($vulkanVersion)\Vulkan-Headers-$($vulkanVersion)" >>${env:GITHUB_ENV} + # 2. Get and build the vulkan loader source code (UPDATE_DEPS=On will make it automatically fetch its dependencies) + Invoke-WebRequest -Uri "https://github.com/KhronosGroup/Vulkan-Loader/archive/refs/tags/v$($vulkanVersion).zip" -OutFile Vulkan-Loader-$($vulkanVersion).zip + Expand-Archive -Path Vulkan-Loader-$($vulkanVersion).zip + cmake -S Vulkan-Loader-$($vulkanVersion)\Vulkan-Loader-$($vulkanVersion) -B VulkanLoader-build -D UPDATE_DEPS=On + cmake --build VulkanLoader-build + # 3. Copy the built lib/dll to the expected place + mkdir Vulkan-Headers-$($vulkanVersion)\Vulkan-Headers-$($vulkanVersion)\Lib + copy VulkanLoader-build\loader\Debug\vulkan-1.* Vulkan-Headers-$($vulkanVersion)\Vulkan-Headers-$($vulkanVersion)\Lib\ - name: Fix Projects shell: powershell diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index cdc51372c..836c57a5f 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -177,6 +177,7 @@ Other Changes: to play nice with -fsanitize=undefined. (#8874) [@i25e] - CI: Added SDL3 builds to MacOS and Windows. (#8819, #8778) [@scribam] - CI: Updated Windows CI to use a more recent SDL2. (#8819, #8778) [@scribam] +- CI: Updates Windows CI to use a more recent VulkanSDK. (#8925, #8778) [@yaz0r] - Examples: SDL3+Metal: added SDL3+Metal example. (#8827, #8825) [@shi-yan] - Examples: SDL3+SDL_GPU: use SDL_WaitAndAcquireGPUSwapchainTexture() instead of SDL_AcquireGPUSwapchainTexture(). (#8830) [@itsdanott] From 8a35ce0e982039a9b7399a55792f5737bd1447e3 Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 18 Aug 2025 14:27:15 +0200 Subject: [PATCH 05/17] CalcWordWrapPosition() breaks on \n instead of relying on caller to do it + expose ImTextCalcWordWrapNextLineStart(). (#3237, #952, #1062, #7363) Should be functional no-op, fingers crossed. Breaking on \n allows caller to count lines reliably. --- imgui.h | 2 +- imgui_draw.cpp | 15 +++++---------- imgui_internal.h | 1 + imgui_widgets.cpp | 5 +---- 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/imgui.h b/imgui.h index befb2d15d..f8db83d4b 100644 --- a/imgui.h +++ b/imgui.h @@ -29,7 +29,7 @@ // Library Version // (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM >= 12345') #define IMGUI_VERSION "1.92.3 WIP" -#define IMGUI_VERSION_NUM 19225 +#define IMGUI_VERSION_NUM 19226 #define IMGUI_HAS_TABLE // Added BeginTable() - from IMGUI_VERSION_NUM >= 18000 #define IMGUI_HAS_TEXTURES // Added ImGuiBackendFlags_RendererHasTextures - from IMGUI_VERSION_NUM >= 19198 diff --git a/imgui_draw.cpp b/imgui_draw.cpp index 1eb5b90fe..5eed9bb2e 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -5340,7 +5340,7 @@ ImFontBaked* ImFontAtlasBakedGetOrAdd(ImFontAtlas* atlas, ImFont* font, float fo } // Trim trailing space and find beginning of next line -static inline const char* CalcWordWrapNextLineStartA(const char* text, const char* text_end) +const char* ImTextCalcWordWrapNextLineStart(const char* text, const char* text_end) { while (text < text_end && ImCharIsBlankA(*text)) text++; @@ -5392,12 +5392,7 @@ const char* ImFont::CalcWordWrapPosition(float size, const char* text, const cha if (c < 32) { if (c == '\n') - { - line_width = word_width = blank_width = 0.0f; - inside_word = true; - s = next_s; - continue; - } + return s; // Direct return, skip "Wrap_width is too small to fit anything" path. if (c == '\r') { s = next_s; @@ -5489,7 +5484,7 @@ ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, cons text_size.y += line_height; line_width = 0.0f; word_wrap_eol = NULL; - s = CalcWordWrapNextLineStartA(s, text_end); // Wrapping skips upcoming blanks + s = ImTextCalcWordWrapNextLineStart(s, text_end); // Wrapping skips upcoming blanks continue; } } @@ -5613,7 +5608,7 @@ begin: // If the specs for CalcWordWrapPosition() were reworked to optionally return on \n we could combine both. // However it is still better than nothing performing the fast-forward! s = CalcWordWrapPosition(size, s, line_end ? line_end : text_end, wrap_width); - s = CalcWordWrapNextLineStartA(s, text_end); + s = ImTextCalcWordWrapNextLineStart(s, text_end); } else { @@ -5667,7 +5662,7 @@ begin: if (y > clip_rect.w) break; // break out of main loop word_wrap_eol = NULL; - s = CalcWordWrapNextLineStartA(s, text_end); // Wrapping skips upcoming blanks + s = ImTextCalcWordWrapNextLineStart(s, text_end); // Wrapping skips upcoming blanks continue; } } diff --git a/imgui_internal.h b/imgui_internal.h index 3c792a866..4d6d13efe 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -428,6 +428,7 @@ IMGUI_API int ImTextCountUtf8BytesFromChar(const char* in_text, const IMGUI_API int ImTextCountUtf8BytesFromStr(const ImWchar* in_text, const ImWchar* in_text_end); // return number of bytes to express string in UTF-8 IMGUI_API const char* ImTextFindPreviousUtf8Codepoint(const char* in_text_start, const char* in_text_curr); // return previous UTF-8 code-point. IMGUI_API int ImTextCountLines(const char* in_text, const char* in_text_end); // return number of lines taken by text. trailing carriage return doesn't count as an extra line. +IMGUI_API const char* ImTextCalcWordWrapNextLineStart(const char* text, const char* text_end); // trim trailing space and find beginning of next line // Helpers: File System #ifdef IMGUI_DISABLE_FILE_FUNCTIONS diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 31088b7c1..81b3271a4 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -3911,9 +3911,6 @@ bool ImGui::InputDouble(const char* label, double* v, double step, double step_f // - InputText() // - InputTextWithHint() // - InputTextMultiline() -// - InputTextGetCharInfo() [Internal] -// - InputTextReindexLines() [Internal] -// - InputTextReindexLinesRange() [Internal] // - InputTextEx() [Internal] // - DebugNodeInputTextState() [Internal] //------------------------------------------------------------------------- @@ -4525,7 +4522,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ IM_ASSERT(buf != NULL && buf_size >= 0); IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys) IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key) - IM_ASSERT(!((flags & ImGuiInputTextFlags_ElideLeft) && (flags & ImGuiInputTextFlags_Multiline))); // Multiline will not work with left-trimming + IM_ASSERT(!((flags & ImGuiInputTextFlags_ElideLeft) && (flags & ImGuiInputTextFlags_Multiline))); // Multiline does not not work with left-trimming ImGuiContext& g = *GImGui; ImGuiIO& io = g.IO; From c63b5bd8fb965977784cf7ad13d4c0cf5f81790e Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 18 Aug 2025 16:11:46 +0200 Subject: [PATCH 06/17] Internals: extracted ImFont::CalcWordWrapPosition() into ImFontCalcWordWrapPositionEx() so we can make change to its signature. (for #3237, #952, #1062, #7363) --- imgui_draw.cpp | 16 +++++++++++----- imgui_internal.h | 3 +++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/imgui_draw.cpp b/imgui_draw.cpp index 5eed9bb2e..51633c69a 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -5352,7 +5352,8 @@ const char* ImTextCalcWordWrapNextLineStart(const char* text, const char* text_e // Simple word-wrapping for English, not full-featured. Please submit failing cases! // This will return the next location to wrap from. If no wrapping if necessary, this will fast-forward to e.g. text_end. // FIXME: Much possible improvements (don't cut things like "word !", "word!!!" but cut within "word,,,,", more sensible support for punctuations, support for Unicode punctuations, etc.) -const char* ImFont::CalcWordWrapPosition(float size, const char* text, const char* text_end, float wrap_width) +const char* ImFontCalcWordWrapPositionEx(ImFont* font, float size, const char* text, const char* text_end, float wrap_width) +//, ImDrawTextFlags flags) { // For references, possible wrap point marked with ^ // "aaa bbb, ccc,ddd. eee fff. ggg!" @@ -5366,7 +5367,7 @@ const char* ImFont::CalcWordWrapPosition(float size, const char* text, const cha // Cut words that cannot possibly fit within one line. // e.g.: "The tropical fish" with ~5 characters worth of width --> "The tr" "opical" "fish" - ImFontBaked* baked = GetFontBaked(size); + ImFontBaked* baked = font->GetFontBaked(size); const float scale = size / baked->Size; float line_width = 0.0f; @@ -5453,6 +5454,11 @@ const char* ImFont::CalcWordWrapPosition(float size, const char* text, const cha return s; } +const char* ImFont::CalcWordWrapPosition(float size, const char* text, const char* text_end, float wrap_width) +{ + return ImFontCalcWordWrapPositionEx(this, size, text, text_end, wrap_width); +} + ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** out_remaining) { if (!text_end) @@ -5475,7 +5481,7 @@ ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, cons { // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. if (!word_wrap_eol) - word_wrap_eol = CalcWordWrapPosition(size, s, text_end, wrap_width - line_width); + word_wrap_eol = ImFontCalcWordWrapPositionEx(this, size, s, text_end, wrap_width - line_width); if (s >= word_wrap_eol) { @@ -5607,7 +5613,7 @@ begin: // FIXME-OPT: This is not optimal as do first do a search for \n before calling CalcWordWrapPosition(). // If the specs for CalcWordWrapPosition() were reworked to optionally return on \n we could combine both. // However it is still better than nothing performing the fast-forward! - s = CalcWordWrapPosition(size, s, line_end ? line_end : text_end, wrap_width); + s = ImFontCalcWordWrapPositionEx(this, size, s, line_end ? line_end : text_end, wrap_width); s = ImTextCalcWordWrapNextLineStart(s, text_end); } else @@ -5653,7 +5659,7 @@ begin: { // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. if (!word_wrap_eol) - word_wrap_eol = CalcWordWrapPosition(size, s, text_end, wrap_width - (x - origin_x)); + word_wrap_eol = ImFontCalcWordWrapPositionEx(this, size, s, text_end, wrap_width - (x - origin_x)); if (s >= word_wrap_eol) { diff --git a/imgui_internal.h b/imgui_internal.h index 4d6d13efe..cd4fa4694 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -428,6 +428,9 @@ IMGUI_API int ImTextCountUtf8BytesFromChar(const char* in_text, const IMGUI_API int ImTextCountUtf8BytesFromStr(const ImWchar* in_text, const ImWchar* in_text_end); // return number of bytes to express string in UTF-8 IMGUI_API const char* ImTextFindPreviousUtf8Codepoint(const char* in_text_start, const char* in_text_curr); // return previous UTF-8 code-point. IMGUI_API int ImTextCountLines(const char* in_text, const char* in_text_end); // return number of lines taken by text. trailing carriage return doesn't count as an extra line. + +// Helpers: High-level text functions (DO NOT USE!!! THIS IS A MINIMAL SUBSET OF LARGER UPCOMING CHANGES) +IMGUI_API const char* ImFontCalcWordWrapPositionEx(ImFont* font, float size, const char* text, const char* text_end, float wrap_width); IMGUI_API const char* ImTextCalcWordWrapNextLineStart(const char* text, const char* text_end); // trim trailing space and find beginning of next line // Helpers: File System From 34ab6c8a89fa42463f47a2363335d3d29c57ec7e Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 27 Aug 2025 21:43:07 +0200 Subject: [PATCH 07/17] Internals: extracted ImFont::CalcTextSizeA() into ImFontCalcTextSizeEx() so we can make change to its signature. (for #3237, #952, #1062, #7363) --- imgui_draw.cpp | 29 ++++++++++++++++------------- imgui_internal.h | 1 + 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/imgui_draw.cpp b/imgui_draw.cpp index 51633c69a..a7c5fcba3 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -5459,13 +5459,13 @@ const char* ImFont::CalcWordWrapPosition(float size, const char* text, const cha return ImFontCalcWordWrapPositionEx(this, size, text, text_end, wrap_width); } -ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** out_remaining) +ImVec2 ImFontCalcTextSizeEx(ImFont* font, float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** out_remaining) { if (!text_end) text_end = text_begin + ImStrlen(text_begin); // FIXME-OPT: Need to avoid this. const float line_height = size; - ImFontBaked* baked = GetFontBaked(size); + ImFontBaked* baked = font->GetFontBaked(size); const float scale = size / baked->Size; ImVec2 text_size = ImVec2(0, 0); @@ -5477,11 +5477,12 @@ ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, cons const char* s = text_begin; while (s < text_end) { + // Word-wrapping if (word_wrap_enabled) { // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. if (!word_wrap_eol) - word_wrap_eol = ImFontCalcWordWrapPositionEx(this, size, s, text_end, wrap_width - line_width); + word_wrap_eol = ImFontCalcWordWrapPositionEx(font, size, s, text_end, wrap_width - line_width); if (s >= word_wrap_eol) { @@ -5503,18 +5504,15 @@ ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, cons else s += ImTextCharFromUtf8(&c, s, text_end); - if (c < 32) + if (c == '\n') { - if (c == '\n') - { - text_size.x = ImMax(text_size.x, line_width); - text_size.y += line_height; - line_width = 0.0f; - continue; - } - if (c == '\r') - continue; + text_size.x = ImMax(text_size.x, line_width); + text_size.y += line_height; + line_width = 0.0f; + continue; } + if (c == '\r') + continue; // Optimized inline version of 'float char_width = GetCharAdvance((ImWchar)c);' float char_width = (c < (unsigned int)baked->IndexAdvanceX.Size) ? baked->IndexAdvanceX.Data[c] : -1.0f; @@ -5543,6 +5541,11 @@ ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, cons return text_size; } +ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** out_remaining) +{ + return ImFontCalcTextSizeEx(this, size, max_width, wrap_width, text_begin, text_end, out_remaining); +} + // Note: as with every ImDrawList drawing function, this expects that the font atlas texture is bound. void ImFont::RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, ImWchar c, const ImVec4* cpu_fine_clip) { diff --git a/imgui_internal.h b/imgui_internal.h index cd4fa4694..efb947a62 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -430,6 +430,7 @@ IMGUI_API const char* ImTextFindPreviousUtf8Codepoint(const char* in_text_star IMGUI_API int ImTextCountLines(const char* in_text, const char* in_text_end); // return number of lines taken by text. trailing carriage return doesn't count as an extra line. // Helpers: High-level text functions (DO NOT USE!!! THIS IS A MINIMAL SUBSET OF LARGER UPCOMING CHANGES) +IMGUI_API ImVec2 ImFontCalcTextSizeEx(ImFont* font, float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** out_remaining); IMGUI_API const char* ImFontCalcWordWrapPositionEx(ImFont* font, float size, const char* text, const char* text_end, float wrap_width); IMGUI_API const char* ImTextCalcWordWrapNextLineStart(const char* text, const char* text_end); // trim trailing space and find beginning of next line From bc6478f6518510bd7d8740f867ed781b41c7ea84 Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 18 Aug 2025 16:38:31 +0200 Subject: [PATCH 08/17] Internals: added ImDrawTextFlags_WrapKeepTrailingBlanks required for text-edit style word-wrapping. (for #3237, #952, #1062, #7363) --- imgui_draw.cpp | 7 ++++--- imgui_internal.h | 8 +++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/imgui_draw.cpp b/imgui_draw.cpp index a7c5fcba3..82f740682 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -5352,8 +5352,7 @@ const char* ImTextCalcWordWrapNextLineStart(const char* text, const char* text_e // Simple word-wrapping for English, not full-featured. Please submit failing cases! // This will return the next location to wrap from. If no wrapping if necessary, this will fast-forward to e.g. text_end. // FIXME: Much possible improvements (don't cut things like "word !", "word!!!" but cut within "word,,,,", more sensible support for punctuations, support for Unicode punctuations, etc.) -const char* ImFontCalcWordWrapPositionEx(ImFont* font, float size, const char* text, const char* text_end, float wrap_width) -//, ImDrawTextFlags flags) +const char* ImFontCalcWordWrapPositionEx(ImFont* font, float size, const char* text, const char* text_end, float wrap_width, ImDrawTextFlags flags) { // For references, possible wrap point marked with ^ // "aaa bbb, ccc,ddd. eee fff. ggg!" @@ -5428,6 +5427,8 @@ const char* ImFontCalcWordWrapPositionEx(ImFont* font, float size, const char* t { prev_word_end = word_end; line_width += word_width + blank_width; + if ((flags & ImDrawTextFlags_WrapKeepBlanks) && line_width <= wrap_width) + prev_word_end = s; word_width = blank_width = 0.0f; } @@ -5456,7 +5457,7 @@ const char* ImFontCalcWordWrapPositionEx(ImFont* font, float size, const char* t const char* ImFont::CalcWordWrapPosition(float size, const char* text, const char* text_end, float wrap_width) { - return ImFontCalcWordWrapPositionEx(this, size, text, text_end, wrap_width); + return ImFontCalcWordWrapPositionEx(this, size, text, text_end, wrap_width, ImDrawTextFlags_None); } ImVec2 ImFontCalcTextSizeEx(ImFont* font, float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** out_remaining) diff --git a/imgui_internal.h b/imgui_internal.h index efb947a62..2efed6846 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -193,6 +193,7 @@ enum ImGuiLocKey : int; // -> enum ImGuiLocKey // E typedef int ImGuiLayoutType; // -> enum ImGuiLayoutType_ // Enum: Horizontal or vertical // Flags +typedef int ImDrawTextFlags; // -> enum ImDrawTextFlags_ // Flags: for ImTextCalcWordWrapPositionEx() typedef int ImGuiActivateFlags; // -> enum ImGuiActivateFlags_ // Flags: for navigation/focus function (will be for ActivateItem() later) typedef int ImGuiDebugLogFlags; // -> enum ImGuiDebugLogFlags_ // Flags: for ShowDebugLogWindow(), g.DebugLogFlags typedef int ImGuiFocusRequestFlags; // -> enum ImGuiFocusRequestFlags_ // Flags: for FocusWindow() @@ -430,8 +431,13 @@ IMGUI_API const char* ImTextFindPreviousUtf8Codepoint(const char* in_text_star IMGUI_API int ImTextCountLines(const char* in_text, const char* in_text_end); // return number of lines taken by text. trailing carriage return doesn't count as an extra line. // Helpers: High-level text functions (DO NOT USE!!! THIS IS A MINIMAL SUBSET OF LARGER UPCOMING CHANGES) +enum ImDrawTextFlags_ +{ + ImDrawTextFlags_None = 0, + ImDrawTextFlags_WrapKeepBlanks = 1 << 1, +}; IMGUI_API ImVec2 ImFontCalcTextSizeEx(ImFont* font, float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** out_remaining); -IMGUI_API const char* ImFontCalcWordWrapPositionEx(ImFont* font, float size, const char* text, const char* text_end, float wrap_width); +IMGUI_API const char* ImFontCalcWordWrapPositionEx(ImFont* font, float size, const char* text, const char* text_end, float wrap_width, ImDrawTextFlags flags = 0); IMGUI_API const char* ImTextCalcWordWrapNextLineStart(const char* text, const char* text_end); // trim trailing space and find beginning of next line // Helpers: File System From a9945899c615c914fc1eb2c8a679226847ddfbf2 Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 18 Aug 2025 17:40:39 +0200 Subject: [PATCH 09/17] stb_textedit: extracted stb_textedit_move_line_start()/stb_textedit_move_line_end(), adding STB_TEXTEDIT_MOVELINESTART,STB_TEXTEDIT_MOVELINEEND support. (#3237, #952, #1062, #7363) --- imstb_textedit.h | 61 +++++++++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/imstb_textedit.h b/imstb_textedit.h index ac4db0fb9..605e44adb 100644 --- a/imstb_textedit.h +++ b/imstb_textedit.h @@ -668,6 +668,35 @@ static void stb_textedit_move_to_last(IMSTB_TEXTEDIT_STRING *str, STB_TexteditSt } } +// [DEAR IMGUI] Extracted this function so we can more easily add support for word-wrapping. +#ifndef STB_TEXTEDIT_MOVELINESTART +static int stb_textedit_move_line_start(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int cursor) +{ + if (state->single_line) + return 0; + while (cursor > 0) { + int prev = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, cursor); + if (STB_TEXTEDIT_GETCHAR(str, prev) == STB_TEXTEDIT_NEWLINE) + break; + cursor = prev; + } + return cursor; +} +#define STB_TEXTEDIT_MOVELINESTART stb_textedit_move_line_start +#endif +#ifndef STB_TEXTEDIT_MOVELINEEND +static int stb_textedit_move_line_end(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int cursor) +{ + int n = STB_TEXTEDIT_STRINGLEN(str); + if (state->single_line) + return n; + while (cursor < n && STB_TEXTEDIT_GETCHAR(str, cursor) != STB_TEXTEDIT_NEWLINE) + cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, cursor); + return cursor; +} +#define STB_TEXTEDIT_MOVELINEEND stb_textedit_move_line_end +#endif + #ifdef STB_TEXTEDIT_IS_SPACE static int is_word_boundary( IMSTB_TEXTEDIT_STRING *str, int idx ) { @@ -1024,7 +1053,7 @@ retry: prev_scan = prev; } find.first_char = find.prev_first; - find.prev_first = prev_scan; + find.prev_first = STB_TEXTEDIT_MOVELINESTART(str, state, prev_scan); } break; } @@ -1098,14 +1127,7 @@ retry: case STB_TEXTEDIT_K_LINESTART: stb_textedit_clamp(str, state); stb_textedit_move_to_first(state); - if (state->single_line) - state->cursor = 0; - else while (state->cursor > 0) { - int prev = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, state->cursor); - if (STB_TEXTEDIT_GETCHAR(str, prev) == STB_TEXTEDIT_NEWLINE) - break; - state->cursor = prev; - } + state->cursor = STB_TEXTEDIT_MOVELINESTART(str, state, state->cursor); state->has_preferred_x = 0; break; @@ -1113,13 +1135,9 @@ retry: case STB_TEXTEDIT_K_LINEEND2: #endif case STB_TEXTEDIT_K_LINEEND: { - int n = STB_TEXTEDIT_STRINGLEN(str); stb_textedit_clamp(str, state); stb_textedit_move_to_first(state); - if (state->single_line) - state->cursor = n; - else while (state->cursor < n && STB_TEXTEDIT_GETCHAR(str, state->cursor) != STB_TEXTEDIT_NEWLINE) - state->cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor); + state->cursor = STB_TEXTEDIT_MOVELINEEND(str, state, state->cursor); state->has_preferred_x = 0; break; } @@ -1130,14 +1148,7 @@ retry: case STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT: stb_textedit_clamp(str, state); stb_textedit_prep_selection_at_cursor(state); - if (state->single_line) - state->cursor = 0; - else while (state->cursor > 0) { - int prev = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, state->cursor); - if (STB_TEXTEDIT_GETCHAR(str, prev) == STB_TEXTEDIT_NEWLINE) - break; - state->cursor = prev; - } + state->cursor = STB_TEXTEDIT_MOVELINESTART(str, state, state->cursor); state->select_end = state->cursor; state->has_preferred_x = 0; break; @@ -1146,13 +1157,9 @@ retry: case STB_TEXTEDIT_K_LINEEND2 | STB_TEXTEDIT_K_SHIFT: #endif case STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT: { - int n = STB_TEXTEDIT_STRINGLEN(str); stb_textedit_clamp(str, state); stb_textedit_prep_selection_at_cursor(state); - if (state->single_line) - state->cursor = n; - else while (state->cursor < n && STB_TEXTEDIT_GETCHAR(str, state->cursor) != STB_TEXTEDIT_NEWLINE) - state->cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor); + state->cursor = STB_TEXTEDIT_MOVELINEEND(str, state, state->cursor); state->select_end = state->cursor; state->has_preferred_x = 0; break; From 56189cd814d28819bdb132aac173be2a1efe2e93 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 27 Aug 2025 21:55:40 +0200 Subject: [PATCH 10/17] Internals: added ImDrawTextFlags_StopOnNewLine support to ImFontCalcTextSizeEx(), ImDrawTextFlags_WrapKeepTrailingBlanks to ImTextCalcWordWrapNextLineStart(). (for #3237, #952, #1062, #7363) --- imgui_draw.cpp | 32 +++++++++++++++++++++----------- imgui_internal.h | 5 +++-- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/imgui_draw.cpp b/imgui_draw.cpp index 82f740682..0477ea8a4 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -5340,10 +5340,11 @@ ImFontBaked* ImFontAtlasBakedGetOrAdd(ImFontAtlas* atlas, ImFont* font, float fo } // Trim trailing space and find beginning of next line -const char* ImTextCalcWordWrapNextLineStart(const char* text, const char* text_end) +const char* ImTextCalcWordWrapNextLineStart(const char* text, const char* text_end, ImDrawTextFlags flags) { - while (text < text_end && ImCharIsBlankA(*text)) - text++; + if ((flags & ImDrawTextFlags_WrapKeepBlanks) == 0) + while (text < text_end && ImCharIsBlankA(*text)) + text++; if (*text == '\n') text++; return text; @@ -5460,14 +5461,16 @@ const char* ImFont::CalcWordWrapPosition(float size, const char* text, const cha return ImFontCalcWordWrapPositionEx(this, size, text, text_end, wrap_width, ImDrawTextFlags_None); } -ImVec2 ImFontCalcTextSizeEx(ImFont* font, float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** out_remaining) +ImVec2 ImFontCalcTextSizeEx(ImFont* font, float size, float max_width, float wrap_width, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining, ImVec2* out_offset, ImDrawTextFlags flags) { if (!text_end) text_end = text_begin + ImStrlen(text_begin); // FIXME-OPT: Need to avoid this. + if (!text_end_display) + text_end_display = text_end; - const float line_height = size; ImFontBaked* baked = font->GetFontBaked(size); - const float scale = size / baked->Size; + const float line_height = size; + const float scale = line_height / baked->Size; ImVec2 text_size = ImVec2(0, 0); float line_width = 0.0f; @@ -5476,14 +5479,14 @@ ImVec2 ImFontCalcTextSizeEx(ImFont* font, float size, float max_width, float wra const char* word_wrap_eol = NULL; const char* s = text_begin; - while (s < text_end) + while (s < text_end_display) { // Word-wrapping if (word_wrap_enabled) { // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. if (!word_wrap_eol) - word_wrap_eol = ImFontCalcWordWrapPositionEx(font, size, s, text_end, wrap_width - line_width); + word_wrap_eol = ImFontCalcWordWrapPositionEx(font, size, s, text_end, wrap_width - line_width, flags); if (s >= word_wrap_eol) { @@ -5491,8 +5494,10 @@ ImVec2 ImFontCalcTextSizeEx(ImFont* font, float size, float max_width, float wra text_size.x = line_width; text_size.y += line_height; line_width = 0.0f; + s = ImTextCalcWordWrapNextLineStart(s, text_end, flags); // Wrapping skips upcoming blanks + if (flags & ImDrawTextFlags_StopOnNewLine) + break; word_wrap_eol = NULL; - s = ImTextCalcWordWrapNextLineStart(s, text_end); // Wrapping skips upcoming blanks continue; } } @@ -5510,6 +5515,8 @@ ImVec2 ImFontCalcTextSizeEx(ImFont* font, float size, float max_width, float wra text_size.x = ImMax(text_size.x, line_width); text_size.y += line_height; line_width = 0.0f; + if (flags & ImDrawTextFlags_StopOnNewLine) + break; continue; } if (c == '\r') @@ -5533,7 +5540,10 @@ ImVec2 ImFontCalcTextSizeEx(ImFont* font, float size, float max_width, float wra if (text_size.x < line_width) text_size.x = line_width; - if (line_width > 0 || text_size.y == 0.0f) + if (out_offset != NULL) + *out_offset = ImVec2(line_width, text_size.y + line_height); // offset allow for the possibility of sitting after a trailing \n + + if (line_width > 0 || text_size.y == 0.0f) // whereas size.y will ignore the trailing \n text_size.y += line_height; if (out_remaining != NULL) @@ -5544,7 +5554,7 @@ ImVec2 ImFontCalcTextSizeEx(ImFont* font, float size, float max_width, float wra ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** out_remaining) { - return ImFontCalcTextSizeEx(this, size, max_width, wrap_width, text_begin, text_end, out_remaining); + return ImFontCalcTextSizeEx(this, size, max_width, wrap_width, text_begin, NULL, text_end, out_remaining, NULL, ImDrawTextFlags_None); } // Note: as with every ImDrawList drawing function, this expects that the font atlas texture is bound. diff --git a/imgui_internal.h b/imgui_internal.h index 2efed6846..e473c7302 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -435,10 +435,11 @@ enum ImDrawTextFlags_ { ImDrawTextFlags_None = 0, ImDrawTextFlags_WrapKeepBlanks = 1 << 1, + ImDrawTextFlags_StopOnNewLine = 1 << 2, }; -IMGUI_API ImVec2 ImFontCalcTextSizeEx(ImFont* font, float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** out_remaining); +IMGUI_API ImVec2 ImFontCalcTextSizeEx(ImFont* font, float size, float max_width, float wrap_width, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining, ImVec2* out_offset, ImDrawTextFlags flags); IMGUI_API const char* ImFontCalcWordWrapPositionEx(ImFont* font, float size, const char* text, const char* text_end, float wrap_width, ImDrawTextFlags flags = 0); -IMGUI_API const char* ImTextCalcWordWrapNextLineStart(const char* text, const char* text_end); // trim trailing space and find beginning of next line +IMGUI_API const char* ImTextCalcWordWrapNextLineStart(const char* text, const char* text_end, ImDrawTextFlags flags = 0); // trim trailing space and find beginning of next line // Helpers: File System #ifdef IMGUI_DISABLE_FILE_FUNCTIONS From 3cc7d1c81a3bdac2f376e1aa8023f85736971312 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 27 Aug 2025 21:59:25 +0200 Subject: [PATCH 11/17] InputText: InputTextCalcTextSize() uses ImFontCalcTextSizeEx(). (for #3237, #952, #1062, #7363) --- imgui_draw.cpp | 2 +- imgui_widgets.cpp | 59 ++++++----------------------------------------- 2 files changed, 8 insertions(+), 53 deletions(-) diff --git a/imgui_draw.cpp b/imgui_draw.cpp index 0477ea8a4..2408d0c9b 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -5554,7 +5554,7 @@ ImVec2 ImFontCalcTextSizeEx(ImFont* font, float size, float max_width, float wra ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** out_remaining) { - return ImFontCalcTextSizeEx(this, size, max_width, wrap_width, text_begin, NULL, text_end, out_remaining, NULL, ImDrawTextFlags_None); + return ImFontCalcTextSizeEx(this, size, max_width, wrap_width, text_begin, text_end, text_end, out_remaining, NULL, ImDrawTextFlags_None); } // Note: as with every ImDrawList drawing function, this expects that the font atlas texture is bound. diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 81b3271a4..31c07f32e 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -136,7 +136,7 @@ static const ImU64 IM_U64_MAX = (2ULL * 9223372036854775807LL + 1); // For InputTextEx() static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard = false); static int InputTextCalcTextLenAndLineCount(ImGuiContext* ctx, const char* text_begin, const char** out_text_end); -static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end, const char** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false); +static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining = NULL, ImVec2* out_offset = NULL, ImDrawTextFlags flags = 0); //------------------------------------------------------------------------- // [SECTION] Widgets: Text, etc. @@ -3959,55 +3959,10 @@ static int InputTextCalcTextLenAndLineCount(ImGuiContext*, const char* text_begi return line_count; } -// FIXME: Ideally we'd share code with ImFont::CalcTextSizeA() -static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end, const char** remaining, ImVec2* out_offset, bool stop_on_new_line) +static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining, ImVec2* out_offset, ImDrawTextFlags flags) { ImGuiContext& g = *ctx; - //ImFont* font = g.Font; - ImFontBaked* baked = g.FontBaked; - const float line_height = g.FontSize; - const float scale = line_height / baked->Size; - - ImVec2 text_size = ImVec2(0, 0); - float line_width = 0.0f; - - const char* s = text_begin; - while (s < text_end) - { - unsigned int c = (unsigned int)*s; - if (c < 0x80) - s += 1; - else - s += ImTextCharFromUtf8(&c, s, text_end); - - if (c == '\n') - { - text_size.x = ImMax(text_size.x, line_width); - text_size.y += line_height; - line_width = 0.0f; - if (stop_on_new_line) - break; - continue; - } - if (c == '\r') - continue; - - line_width += baked->GetCharAdvance((ImWchar)c) * scale; - } - - if (text_size.x < line_width) - text_size.x = line_width; - - if (out_offset) - *out_offset = ImVec2(line_width, text_size.y + line_height); // offset allow for the possibility of sitting after a trailing \n - - if (line_width > 0 || text_size.y == 0.0f) // whereas size.y will ignore the trailing \n - text_size.y += line_height; - - if (remaining) - *remaining = s; - - return text_size; + return ImFontCalcTextSizeEx(g.Font, g.FontSize, FLT_MAX, 0.0f, text_begin, text_end_display, text_end, out_remaining, out_offset, flags); } // Wrapper for stb_textedit.h to edit text (our wrapper is for: statically sized buffer, single-line, wchar characters. InputText converts between UTF-8 and wchar) @@ -4025,7 +3980,7 @@ static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* ob { const char* text = obj->TextSrc; const char* text_remaining = NULL; - const ImVec2 size = InputTextCalcTextSize(obj->Ctx, text + line_start_idx, text + obj->TextLen, &text_remaining, NULL, true); + const ImVec2 size = InputTextCalcTextSize(obj->Ctx, text + line_start_idx, text + obj->TextLen, text + obj->TextLen, &text_remaining, NULL, ImDrawTextFlags_StopOnNewLine | ImDrawTextFlags_WrapKeepTrailingBlanks); r->x0 = 0.0f; r->x1 = size.x; r->baseline_y_delta = size.y; @@ -5303,11 +5258,11 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ selmin_line_no = line_count; // Calculate 2d position by finding the beginning of the line and measuring distance - cursor_offset.x = InputTextCalcTextSize(&g, ImStrbol(cursor_ptr, text_begin), cursor_ptr).x; + cursor_offset.x = InputTextCalcTextSize(&g, ImStrbol(cursor_ptr, text_begin), cursor_ptr, cursor_ptr, NULL, NULL, ImDrawTextFlags_WrapKeepTrailingBlanks).x; cursor_offset.y = cursor_line_no * g.FontSize; if (selmin_line_no >= 0) { - select_start_offset.x = InputTextCalcTextSize(&g, ImStrbol(selmin_ptr, text_begin), selmin_ptr).x; + select_start_offset.x = InputTextCalcTextSize(&g, ImStrbol(selmin_ptr, text_begin), selmin_ptr, selmin_ptr, NULL, NULL, ImDrawTextFlags_WrapKeepTrailingBlanks).x; select_start_offset.y = selmin_line_no * g.FontSize; } @@ -5373,7 +5328,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ } else { - ImVec2 rect_size = InputTextCalcTextSize(&g, p, text_selected_end, &p, NULL, true); + ImVec2 rect_size = InputTextCalcTextSize(&g, p, text_selected_end, text_selected_end, &p, NULL, ImDrawTextFlags_StopOnNewLine); if (rect_size.x <= 0.0f) rect_size.x = IM_TRUNC(g.FontBaked->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_size.x, bg_offy_dn)); From 11fff1ccf5b14c1d3785d42ef5b4655be7b002bb Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 27 Aug 2025 22:17:28 +0200 Subject: [PATCH 12/17] ImFont::RenderText() takes ImDrawTextFlags_CpuFineClip instead of bool cpu_fine_clip + forward ImDrawTextFlags to word-wrap code. (for #3237, #952, #1062, #7363) --- imgui.h | 3 ++- imgui_draw.cpp | 14 ++++++++------ imgui_internal.h | 1 + 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/imgui.h b/imgui.h index f8db83d4b..b45e585a8 100644 --- a/imgui.h +++ b/imgui.h @@ -233,6 +233,7 @@ typedef int ImGuiTableBgTarget; // -> enum ImGuiTableBgTarget_ // Enum: A // - In VS Code, CLion, etc.: CTRL+click can follow symbols inside comments. typedef int ImDrawFlags; // -> enum ImDrawFlags_ // Flags: for ImDrawList functions typedef int ImDrawListFlags; // -> enum ImDrawListFlags_ // Flags: for ImDrawList instance +typedef int ImDrawTextFlags; // -> enum ImDrawTextFlags_ // Internal, do not use! typedef int ImFontFlags; // -> enum ImFontFlags_ // Flags: for ImFont typedef int ImFontAtlasFlags; // -> enum ImFontAtlasFlags_ // Flags: for ImFontAtlas typedef int ImGuiBackendFlags; // -> enum ImGuiBackendFlags_ // Flags: for io.BackendFlags @@ -3837,7 +3838,7 @@ struct ImFont IMGUI_API ImVec2 CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end = NULL, const char** out_remaining = NULL); IMGUI_API const char* CalcWordWrapPosition(float size, const char* text, const char* text_end, float wrap_width); IMGUI_API void RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, ImWchar c, const ImVec4* cpu_fine_clip = NULL); - IMGUI_API void RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width = 0.0f, bool cpu_fine_clip = false); + IMGUI_API void RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width = 0.0f, ImDrawTextFlags flags = 0); #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS inline const char* CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width) { return CalcWordWrapPosition(LegacySize * scale, text, text_end, wrap_width); } #endif diff --git a/imgui_draw.cpp b/imgui_draw.cpp index 2408d0c9b..9f2867859 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -1718,7 +1718,7 @@ void ImDrawList::AddText(ImFont* font, float font_size, const ImVec2& pos, ImU32 clip_rect.z = ImMin(clip_rect.z, cpu_fine_clip_rect->z); clip_rect.w = ImMin(clip_rect.w, cpu_fine_clip_rect->w); } - font->RenderText(this, font_size, pos, col, clip_rect, text_begin, text_end, wrap_width, cpu_fine_clip_rect != NULL); + font->RenderText(this, font_size, pos, col, clip_rect, text_begin, text_end, wrap_width, (cpu_fine_clip_rect != NULL) ? ImDrawTextFlags_CpuFineClip : ImDrawTextFlags_None); } void ImDrawList::AddText(const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end) @@ -5597,7 +5597,8 @@ void ImFont::RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, Im } // Note: as with every ImDrawList drawing function, this expects that the font atlas texture is bound. -void ImFont::RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width, bool cpu_fine_clip) +// DO NOT CALL DIRECTLY THIS WILL CHANGE WIDLY IN 2025-2025. Use ImDrawList::AddText(). +void ImFont::RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width, ImDrawTextFlags flags) { // Align to be pixel perfect begin: @@ -5627,8 +5628,8 @@ begin: // FIXME-OPT: This is not optimal as do first do a search for \n before calling CalcWordWrapPosition(). // If the specs for CalcWordWrapPosition() were reworked to optionally return on \n we could combine both. // However it is still better than nothing performing the fast-forward! - s = ImFontCalcWordWrapPositionEx(this, size, s, line_end ? line_end : text_end, wrap_width); - s = ImTextCalcWordWrapNextLineStart(s, text_end); + s = ImFontCalcWordWrapPositionEx(this, size, s, line_end ? line_end : text_end, wrap_width, flags); + s = ImTextCalcWordWrapNextLineStart(s, text_end, flags); } else { @@ -5663,6 +5664,7 @@ begin: ImDrawIdx* idx_write = draw_list->_IdxWritePtr; unsigned int vtx_index = draw_list->_VtxCurrentIdx; const int cmd_count = draw_list->CmdBuffer.Size; + const bool cpu_fine_clip = (flags & ImDrawTextFlags_CpuFineClip) != 0; const ImU32 col_untinted = col | ~IM_COL32_A_MASK; const char* word_wrap_eol = NULL; @@ -5673,7 +5675,7 @@ begin: { // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. if (!word_wrap_eol) - word_wrap_eol = ImFontCalcWordWrapPositionEx(this, size, s, text_end, wrap_width - (x - origin_x)); + word_wrap_eol = ImFontCalcWordWrapPositionEx(this, size, s, text_end, wrap_width - (x - origin_x), flags); if (s >= word_wrap_eol) { @@ -5682,7 +5684,7 @@ begin: if (y > clip_rect.w) break; // break out of main loop word_wrap_eol = NULL; - s = ImTextCalcWordWrapNextLineStart(s, text_end); // Wrapping skips upcoming blanks + s = ImTextCalcWordWrapNextLineStart(s, text_end, flags); // Wrapping skips upcoming blanks continue; } } diff --git a/imgui_internal.h b/imgui_internal.h index e473c7302..d6f385aae 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -434,6 +434,7 @@ IMGUI_API int ImTextCountLines(const char* in_text, const char* in_tex enum ImDrawTextFlags_ { ImDrawTextFlags_None = 0, + ImDrawTextFlags_CpuFineClip = 1 << 0, // Must be == 1/true for legacy with 'bool cpu_fine_clip' arg to RenderText() ImDrawTextFlags_WrapKeepBlanks = 1 << 1, ImDrawTextFlags_StopOnNewLine = 1 << 2, }; From e422a38e4cef48136980262c56fa81c95dcf9eff Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 29 Aug 2025 18:15:43 +0200 Subject: [PATCH 13/17] InputText: internals: expose LineCount, GetPreferredOffsetX(). --- imgui_internal.h | 2 ++ imgui_widgets.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/imgui_internal.h b/imgui_internal.h index d6f385aae..5eef3f95a 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1234,6 +1234,7 @@ struct IMGUI_API ImGuiInputTextState ImVector CallbackTextBackup; // temporary storage for callback to support automatic reconcile of undo-stack int BufCapacity; // end-user buffer capacity (include zero terminator) ImVec2 Scroll; // horizontal offset (managed manually) + vertical scrolling (pulled from child window's own Scroll.y) + int LineCount; // last line count (solely for debugging) float CursorAnim; // timer for cursor blink, reset on every user action so the cursor reappears immediately bool CursorFollow; // set when we want scrolling to follow the current cursor position (not always!) bool SelectedAllMouseLock; // after a double-click to select all, we ignore further mouse drags to update selection @@ -1248,6 +1249,7 @@ struct IMGUI_API ImGuiInputTextState void ClearFreeMemory() { TextA.clear(); TextToRevertTo.clear(); } void OnKeyPressed(int key); // Cannot be inline because we call in code in stb_textedit.h implementation void OnCharPressed(unsigned int c); + float GetPreferredOffsetX() const; // Cursor & Selection void CursorAnimReset(); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 31c07f32e..a708b27c5 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -4206,6 +4206,7 @@ void ImGuiInputTextState::ClearSelection() { Stb->select_start int ImGuiInputTextState::GetCursorPos() const { return Stb->cursor; } int ImGuiInputTextState::GetSelectionStart() const { return Stb->select_start; } int ImGuiInputTextState::GetSelectionEnd() const { return Stb->select_end; } +float ImGuiInputTextState::GetPreferredOffsetX() const { return Stb->has_preferred_x ? Stb->preferred_x : -1; } void ImGuiInputTextState::SelectAll() { Stb->select_start = 0; Stb->cursor = Stb->select_end = TextLen; Stb->has_preferred_x = 0; } void ImGuiInputTextState::ReloadUserBufAndSelectAll() { WantReloadUserBuf = true; ReloadSelectionStart = 0; ReloadSelectionEnd = INT_MAX; } void ImGuiInputTextState::ReloadUserBufAndKeepSelection() { WantReloadUserBuf = true; ReloadSelectionStart = Stb->select_start; ReloadSelectionEnd = Stb->select_end; } @@ -5269,6 +5270,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ // Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224) if (is_multiline) text_size = ImVec2(inner_size.x, line_count * g.FontSize); + state->LineCount = line_count; } // Scroll From a82f66a9b0c07da86afeeddc7ccdfb805f031309 Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 29 Aug 2025 18:17:46 +0200 Subject: [PATCH 14/17] InputText: Word-Wrap: added ImGuiInputTextFlags_WordWrap support. (#3237, #952, #1062, #7363) --- imgui_internal.h | 11 ++++ imgui_widgets.cpp | 150 +++++++++++++++++++++++++++++++++++----------- imstb_textedit.h | 8 +++ 3 files changed, 135 insertions(+), 34 deletions(-) diff --git a/imgui_internal.h b/imgui_internal.h index 5eef3f95a..faeef64e0 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1015,6 +1015,15 @@ enum ImGuiHoveredFlagsPrivate_ // Extend ImGuiInputTextFlags_ enum ImGuiInputTextFlagsPrivate_ { + // [Experimental] + // Word-wrapping caveats: + // - Not well tested yet. Please report any incorrect cursor movement, selection behavior etc. bug to https://github.com/ocornut/imgui/issues/3237. + // - With our current design it is _much_ slower than a regular text field. Editing a <50K buffer will generally be ok, but editing a 1MB buffer will waste meaningful amount of CPU. + // We are likely to not make the feature public until this is fixed (which requires bigger changes to InputText will be be generally desirable for this and other features) + // - Wrapping of long words/sections (e.g. words that are larger than available width) is currently visually not pleasing. + // - Vertical scrollbar is currently always visible. + ImGuiInputTextFlags_WordWrap = 1 << 24, // InputTextMultine(): wrap lines that are too long. (Ref #3237, #952, #1062) + // [Internal] ImGuiInputTextFlags_Multiline = 1 << 26, // For internal use by InputTextMultiline() ImGuiInputTextFlags_MergedItem = 1 << 27, // For internal use by TempInputText(), will skip calling ItemAdd(). Require bounding-box to strictly match. @@ -1235,11 +1244,13 @@ struct IMGUI_API ImGuiInputTextState int BufCapacity; // end-user buffer capacity (include zero terminator) ImVec2 Scroll; // horizontal offset (managed manually) + vertical scrolling (pulled from child window's own Scroll.y) int LineCount; // last line count (solely for debugging) + float WrapWidth; // word-wrapping width float CursorAnim; // timer for cursor blink, reset on every user action so the cursor reappears immediately bool CursorFollow; // set when we want scrolling to follow the current cursor position (not always!) bool SelectedAllMouseLock; // after a double-click to select all, we ignore further mouse drags to update selection bool Edited; // edited this frame bool WantReloadUserBuf; // force a reload of user buf so it may be modified externally. may be automatic in future version. + ImS8 LastMoveDirectionLR; // ImGuiDir_Left or ImGuiDir_Right. track last movement direction so when cursor cross over a word-wrapping boundaries we can display it on either line depending on last move.s int ReloadSelectionStart; int ReloadSelectionEnd; diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index a708b27c5..17ac3c1db 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -135,7 +135,7 @@ static const ImU64 IM_U64_MAX = (2ULL * 9223372036854775807LL + 1); // For InputTextEx() static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard = false); -static int InputTextCalcTextLenAndLineCount(ImGuiContext* ctx, const char* text_begin, const char** out_text_end); +static int InputTextCalcTextLenAndLineCount(ImGuiContext* ctx, const char* text_begin, const char** out_text_end, float wrap_width); static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining = NULL, ImVec2* out_offset = NULL, ImDrawTextFlags flags = 0); //------------------------------------------------------------------------- @@ -3938,10 +3938,11 @@ bool ImGui::InputTextWithHint(const char* label, const char* hint, char* buf, si } // This is only used in the path where the multiline widget is inactive. -static int InputTextCalcTextLenAndLineCount(ImGuiContext*, const char* text_begin, const char** out_text_end) +static int InputTextCalcTextLenAndLineCount(ImGuiContext* ctx, const char* text_begin, const char** out_text_end, float wrap_width) { int line_count = 0; const char* s = text_begin; + if (wrap_width == 0.0f) { while (true) { @@ -3955,6 +3956,23 @@ static int InputTextCalcTextLenAndLineCount(ImGuiContext*, const char* text_begi s = s_eol + 1; } } + else + { + // FIXME-WORDWRAP, FIXME-OPT: This is very suboptimal. + // We basically want both text_end and text_size, they could more optimally be emitted from a RenderText call that uses word-wrapping. + ImGuiContext& g = *ctx; + ImFont* font = g.Font; + const char* text_end = text_begin + strlen(text_begin); + while (s < text_end) + { + s = ImFontCalcWordWrapPositionEx(font, g.FontSize, s, text_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks); + s = (*s == '\n') ? s + 1 : s; + line_count++; + } + if (text_end > text_begin && text_end[-1] == '\n') + line_count++; + IM_ASSERT(s == text_end); + } *out_text_end = s; return line_count; } @@ -3962,7 +3980,8 @@ static int InputTextCalcTextLenAndLineCount(ImGuiContext*, const char* text_begi static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining, ImVec2* out_offset, ImDrawTextFlags flags) { ImGuiContext& g = *ctx; - return ImFontCalcTextSizeEx(g.Font, g.FontSize, FLT_MAX, 0.0f, text_begin, text_end_display, text_end, out_remaining, out_offset, flags); + ImGuiInputTextState* obj = &g.InputTextState; + return ImFontCalcTextSizeEx(g.Font, g.FontSize, FLT_MAX, obj->WrapWidth, text_begin, text_end_display, text_end, out_remaining, out_offset, flags); } // Wrapper for stb_textedit.h to edit text (our wrapper is for: statically sized buffer, single-line, wchar characters. InputText converts between UTF-8 and wchar) @@ -3980,7 +3999,7 @@ static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* ob { const char* text = obj->TextSrc; const char* text_remaining = NULL; - const ImVec2 size = InputTextCalcTextSize(obj->Ctx, text + line_start_idx, text + obj->TextLen, text + obj->TextLen, &text_remaining, NULL, ImDrawTextFlags_StopOnNewLine | ImDrawTextFlags_WrapKeepTrailingBlanks); + const ImVec2 size = InputTextCalcTextSize(obj->Ctx, text + line_start_idx, text + obj->TextLen, text + obj->TextLen, &text_remaining, NULL, ImDrawTextFlags_StopOnNewLine | ImDrawTextFlags_WrapKeepBlanks); r->x0 = 0.0f; r->x1 = size.x; r->baseline_y_delta = size.y; @@ -4185,6 +4204,11 @@ void ImGuiInputTextState::OnKeyPressed(int key) stb_textedit_key(this, Stb, key); CursorFollow = true; CursorAnimReset(); + const int key_u = (key & ~STB_TEXTEDIT_K_SHIFT); + if (key_u == STB_TEXTEDIT_K_LEFT || key_u == STB_TEXTEDIT_K_LINESTART || key_u == STB_TEXTEDIT_K_TEXTSTART || key_u == STB_TEXTEDIT_K_BACKSPACE || key_u == STB_TEXTEDIT_K_WORDLEFT) + LastMoveDirectionLR = ImGuiDir_Left; + else if (key_u == STB_TEXTEDIT_K_RIGHT || key_u == STB_TEXTEDIT_K_LINEEND || key_u == STB_TEXTEDIT_K_TEXTEND || key_u == STB_TEXTEDIT_K_DELETE || key_u == STB_TEXTEDIT_K_WORDRIGHT) + LastMoveDirectionLR = ImGuiDir_Right; } void ImGuiInputTextState::OnCharPressed(unsigned int c) @@ -4479,6 +4503,8 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys) IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key) IM_ASSERT(!((flags & ImGuiInputTextFlags_ElideLeft) && (flags & ImGuiInputTextFlags_Multiline))); // Multiline does not not work with left-trimming + IM_ASSERT((flags & ImGuiInputTextFlags_WordWrap) == 0 || (flags & ImGuiInputTextFlags_Password) == 0); // WordWrap does not work with Password mode. + IM_ASSERT((flags & ImGuiInputTextFlags_WordWrap) == 0 || (flags & ImGuiInputTextFlags_Multiline) != 0); // WordWrap does not work in single-line mode. ImGuiContext& g = *GImGui; ImGuiIO& io = g.IO; @@ -4526,7 +4552,10 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding); PushStyleVar(ImGuiStyleVar_ChildBorderSize, style.FrameBorderSize); PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); // Ensure no clip rect so mouse hover can reach FramePadding edges - bool child_visible = BeginChildEx(label, id, frame_bb.GetSize(), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoMove); + ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoMove; + if (flags & ImGuiInputTextFlags_WordWrap) + window_flags |= ImGuiWindowFlags_AlwaysVerticalScrollbar; // FIXME-WORDWRAP: Makes things much simpler. Otherwise requires more work to track cursor reliably and avoid one-frame glitch. + bool child_visible = BeginChildEx(label, id, frame_bb.GetSize(), ImGuiChildFlags_Borders, window_flags); g.NavActivateId = backup_activate_id; PopStyleVar(3); PopStyleColor(); @@ -4566,6 +4595,8 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0; const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0; const bool is_resizable = (flags & ImGuiInputTextFlags_CallbackResize) != 0; + const bool is_wordwrap = (flags & ImGuiInputTextFlags_WordWrap) != 0; + const float wrap_width = is_wordwrap ? GetContentRegionAvail().x : 0.0f; if (is_resizable) IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag! @@ -4728,6 +4759,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ state->Edited = false; state->BufCapacity = buf_size; state->Flags = flags; + state->WrapWidth = wrap_width; // Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget. // Down the line we should have a cleaner library-wide concept of Selected vs Active. @@ -4765,9 +4797,11 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ { // Triple-click: Select line const bool is_eol = ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb->cursor) == '\n'; + state->WrapWidth = 0.0f; // Temporarily disable wrapping so we use real line start. state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART); state->OnKeyPressed(STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT); state->OnKeyPressed(STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT); + state->WrapWidth = wrap_width; if (!is_eol && is_multiline) { ImSwap(state->Stb->select_start, state->Stb->select_end); @@ -5185,9 +5219,11 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); } - const ImRect clip_rect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + inner_size.x, frame_bb.Min.y + inner_size.y); // Not using frame_bb.Max because we have adjusted size ImVec2 draw_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding; ImVec2 text_size(0.0f, 0.0f); + ImRect clip_rect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + inner_size.x, frame_bb.Min.y + inner_size.y); // Not using frame_bb.Max because we have adjusted size + if (is_multiline) + clip_rect.ClipWith(draw_window->ClipRect); // Set upper limit of single-line InputTextEx() at 2 million characters strings. The current pathological worst case is a long line // without any carriage return, which would makes ImFont::RenderText() reserve too many vertices and probably crash. Avoid it altogether. @@ -5230,7 +5266,8 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ // FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8. const char* text_begin = buf_display; const char* text_end = text_begin + state->TextLen; - ImVec2 cursor_offset, select_start_offset; + ImVec2 cursor_offset; + float select_start_offset_y = 0.0f; // Offset of beginning of non-wrapped line for selection. { // Find lines numbers straddling cursor and selection min position @@ -5238,12 +5275,16 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ int selmin_line_no = render_selection ? -1 : -1000; const char* cursor_ptr = render_cursor ? text_begin + state->Stb->cursor : NULL; const char* selmin_ptr = render_selection ? text_begin + ImMin(state->Stb->select_start, state->Stb->select_end) : NULL; + const char* cursor_line_start = NULL; + const char* selmin_line_start = NULL; + bool cursor_straddle_word_wrap = false; // Count lines and find line number for cursor and selection ends // FIXME: Switch to zero-based index to reduce confusion. int line_count = 1; if (is_multiline) { + if (!is_wordwrap) { for (const char* s = text_begin; (s = (const char*)ImMemchr(s, '\n', (size_t)(text_end - s))) != NULL; s++) { @@ -5252,24 +5293,54 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ line_count++; } } + else + { + bool is_start_of_non_wrapped_line = true; + int line_count_for_non_wrapped_line = 1; + for (const char* s = text_begin; s < text_end; s = (*s == '\n') ? s + 1 : s) + { + const char* s_eol = ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, s, text_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks); + const char* s_prev = s; + s = s_eol; + if (cursor_line_no == -1 && s >= cursor_ptr) { cursor_line_start = s_prev; cursor_line_no = line_count; } + if (selmin_line_no == -1 && s >= selmin_ptr) { selmin_line_start = s_prev; selmin_line_no = line_count_for_non_wrapped_line; } + if (s == cursor_ptr && *cursor_ptr != '\n' && *cursor_ptr != 0) + cursor_straddle_word_wrap = true; + is_start_of_non_wrapped_line = (*s == '\n'); + line_count++; + if (is_start_of_non_wrapped_line) + line_count_for_non_wrapped_line = line_count; + } + } + //IMGUI_DEBUG_LOG("%d\n", selmin_line_no); } if (cursor_line_no == -1) cursor_line_no = line_count; + if (cursor_line_start == NULL) + cursor_line_start = ImStrbol(cursor_ptr, text_begin); if (selmin_line_no == -1) selmin_line_no = line_count; + if (selmin_line_start == NULL) + selmin_line_start = ImStrbol(cursor_ptr, text_begin); // Calculate 2d position by finding the beginning of the line and measuring distance - cursor_offset.x = InputTextCalcTextSize(&g, ImStrbol(cursor_ptr, text_begin), cursor_ptr, cursor_ptr, NULL, NULL, ImDrawTextFlags_WrapKeepTrailingBlanks).x; - cursor_offset.y = cursor_line_no * g.FontSize; - if (selmin_line_no >= 0) + if (render_cursor) { - select_start_offset.x = InputTextCalcTextSize(&g, ImStrbol(selmin_ptr, text_begin), selmin_ptr, selmin_ptr, NULL, NULL, ImDrawTextFlags_WrapKeepTrailingBlanks).x; - select_start_offset.y = selmin_line_no * g.FontSize; + cursor_offset.x = InputTextCalcTextSize(&g, cursor_line_start, cursor_ptr, text_end, NULL, NULL, ImDrawTextFlags_WrapKeepBlanks).x; + cursor_offset.y = cursor_line_no * g.FontSize; + if (is_multiline && cursor_straddle_word_wrap && state->LastMoveDirectionLR == ImGuiDir_Left) + cursor_offset = ImVec2(0.0f, cursor_offset.y + g.FontSize); } + if (selmin_line_no >= 0) + select_start_offset_y = selmin_line_no * g.FontSize; // Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224) if (is_multiline) + { + if (is_wordwrap && text_end > text_begin && text_end[-1] != '\n') + line_count--; text_size = ImVec2(inner_size.x, line_count * g.FontSize); + } state->LineCount = line_count; } @@ -5318,28 +5389,33 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ ImU32 bg_color = GetColorU32(ImGuiCol_TextSelectedBg, render_cursor ? 1.0f : 0.6f); // FIXME: current code flow mandate that render_cursor is always true here, we are leaving the transparent one for tests. float bg_offy_up = is_multiline ? 0.0f : -1.0f; // FIXME: those offsets should be part of the style? they don't play so well with multi-line selection. float bg_offy_dn = is_multiline ? 0.0f : 2.0f; - ImVec2 rect_pos = draw_pos + select_start_offset - draw_scroll; - for (const char* p = text_selected_begin; p < text_selected_end; ) + float bg_min_width = IM_TRUNC(g.FontBaked->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines + ImVec2 rect_pos = draw_pos - draw_scroll; + rect_pos.y += select_start_offset_y; + for (const char* p = ImStrbol(text_selected_begin, text_begin); p < text_selected_end; rect_pos.y += g.FontSize) { if (rect_pos.y > clip_rect.Max.y + g.FontSize) break; - if (rect_pos.y < clip_rect.Min.y) + const char* p_eol = is_wordwrap ? ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, p, text_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks) : (const char*)ImMemchr((void*)p, '\n', text_selected_end - p); + if (p_eol == NULL) + p_eol = text_selected_end; + const char* p_next = is_wordwrap ? (*p_eol == '\n' ? p_eol + 1 : p_eol) : (p_eol + 1); + if (rect_pos.y >= clip_rect.Min.y) { - p = (const char*)ImMemchr((void*)p, '\n', text_selected_end - p); - p = p ? p + 1 : text_selected_end; + const char* line_selected_begin = (text_selected_begin > p) ? text_selected_begin : p; + const char* line_selected_end = (text_selected_end < p_eol) ? text_selected_end : p_eol; + if ((*p_eol == '\n' && text_selected_begin <= p_eol) || (text_selected_begin < p_eol)) + { + ImVec2 rect_offset = CalcTextSize(p, line_selected_begin); + ImVec2 rect_size = CalcTextSize(line_selected_begin, line_selected_end); + rect_size.x = ImMax(rect_size.x, bg_min_width); // So we can see selected empty lines + ImRect rect(rect_pos + ImVec2(rect_offset.x, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_offset.x + rect_size.x, bg_offy_dn)); + rect.ClipWith(clip_rect); + if (rect.Overlaps(clip_rect)) + draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color); + } } - else - { - ImVec2 rect_size = InputTextCalcTextSize(&g, p, text_selected_end, text_selected_end, &p, NULL, ImDrawTextFlags_StopOnNewLine); - if (rect_size.x <= 0.0f) - rect_size.x = IM_TRUNC(g.FontBaked->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines - ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_size.x, bg_offy_dn)); - rect.ClipWith(clip_rect); - if (rect.Overlaps(clip_rect)) - draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color); - rect_pos.x = draw_pos.x - draw_scroll.x; - } - rect_pos.y += g.FontSize; + p = p_next; } } @@ -5348,7 +5424,9 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length) { ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text); - draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect.AsVec4()); + if (col & IM_COL32_A_MASK) + g.Font->RenderText(draw_window->DrawList, g.FontSize, draw_pos - draw_scroll, col, clip_rect.AsVec4(), buf_display, buf_display_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks); + //draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, wrap_width, is_multiline ? NULL : &clip_rect.AsVec4()); } // Draw blinking cursor @@ -5379,7 +5457,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ { // Render text only (no selection, no cursor) if (is_multiline) - text_size = ImVec2(inner_size.x, InputTextCalcTextLenAndLineCount(&g, buf_display, &buf_display_end) * g.FontSize); // We don't need width + text_size = ImVec2(inner_size.x, InputTextCalcTextLenAndLineCount(&g, buf_display, &buf_display_end, wrap_width) * g.FontSize); // We don't need width else if (!is_displaying_hint && g.ActiveId == id) buf_display_end = buf_display + state->TextLen; else if (!is_displaying_hint) @@ -5393,7 +5471,9 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ const ImVec2 draw_scroll = /*state ? ImVec2(state->Scroll.x, 0.0f) :*/ ImVec2(0.0f, 0.0f); // Preserve scroll when inactive? ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text); - draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect.AsVec4()); + if (col & IM_COL32_A_MASK) + g.Font->RenderText(draw_window->DrawList, g.FontSize, draw_pos - draw_scroll, col, clip_rect.AsVec4(), buf_display, buf_display_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks); + //draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, wrap_width, is_multiline ? NULL : &clip_rect.AsVec4()); } } @@ -5449,8 +5529,10 @@ void ImGui::DebugNodeInputTextState(ImGuiInputTextState* state) ImStb::StbUndoState* undo_state = &stb_state->undostate; Text("ID: 0x%08X, ActiveID: 0x%08X", state->ID, g.ActiveId); DebugLocateItemOnHover(state->ID); - Text("CurLenA: %d, Cursor: %d, Selection: %d..%d", state->TextLen, stb_state->cursor, stb_state->select_start, stb_state->select_end); - Text("BufCapacity: %d", state->BufCapacity); + Text("TextLen: %d, Cursor: %d%s, Selection: %d..%d", state->TextLen, stb_state->cursor, + (state->Flags & ImGuiInputTextFlags_WordWrap) ? (state->LastMoveDirectionLR == ImGuiDir_Left ? " (L)" : " (R)") : "", + stb_state->select_start, stb_state->select_end); + Text("BufCapacity: %d, LineCount: %d", state->BufCapacity, state->LineCount); Text("(Internal Buffer: TextA Size: %d, Capacity: %d)", state->TextA.Size, state->TextA.Capacity); Text("has_preferred_x: %d (%.2f)", stb_state->has_preferred_x, stb_state->preferred_x); Text("undo_point: %d, redo_point: %d, undo_char_point: %d, redo_char_point: %d", undo_state->undo_point, undo_state->redo_point, undo_state->undo_char_point, undo_state->redo_char_point); diff --git a/imstb_textedit.h b/imstb_textedit.h index 605e44adb..c4f5cd64f 100644 --- a/imstb_textedit.h +++ b/imstb_textedit.h @@ -572,6 +572,8 @@ static void stb_textedit_find_charpos(StbFindState *find, IMSTB_TEXTEDIT_STRING STB_TEXTEDIT_LAYOUTROW(&r, str, i); if (n < i + r.num_chars) break; + if (str->LastMoveDirectionLR == ImGuiDir_Right && str->Stb->cursor == i + r.num_chars && STB_TEXTEDIT_GETCHAR(str, i + r.num_chars - 1) != STB_TEXTEDIT_NEWLINE) // [DEAR IMGUI] Wrapping point handling + break; if (i + r.num_chars == z && z > 0 && STB_TEXTEDIT_GETCHAR(str, z - 1) != STB_TEXTEDIT_NEWLINE) // [DEAR IMGUI] special handling for last line break; // [DEAR IMGUI] prev_start = i; @@ -972,6 +974,8 @@ retry: } stb_textedit_clamp(str, state); + if (state->cursor == find.first_char + find.length) + str->LastMoveDirectionLR = ImGuiDir_Left; state->has_preferred_x = 1; state->preferred_x = goal_x; @@ -1036,6 +1040,10 @@ retry: } stb_textedit_clamp(str, state); + if (state->cursor == find.first_char) + str->LastMoveDirectionLR = ImGuiDir_Right; + else if (state->cursor == find.prev_first) + str->LastMoveDirectionLR = ImGuiDir_Left; state->has_preferred_x = 1; state->preferred_x = goal_x; From 985723ed94c64934740215075df7a50500bbe568 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 28 Aug 2025 19:37:52 +0200 Subject: [PATCH 15/17] InputText: Word-Wrap: mouse clicks on word-wrapping points set cursor side correctly. (#3237, #952, #1062, #7363) --- imstb_textedit.h | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/imstb_textedit.h b/imstb_textedit.h index c4f5cd64f..49c8933e2 100644 --- a/imstb_textedit.h +++ b/imstb_textedit.h @@ -427,7 +427,7 @@ typedef struct // // traverse the layout to locate the nearest character to a display position -static int stb_text_locate_coord(IMSTB_TEXTEDIT_STRING *str, float x, float y) +static int stb_text_locate_coord(IMSTB_TEXTEDIT_STRING *str, float x, float y, int* out_side_on_line) { StbTexteditRow r; int n = STB_TEXTEDIT_STRINGLEN(str); @@ -437,6 +437,7 @@ static int stb_text_locate_coord(IMSTB_TEXTEDIT_STRING *str, float x, float y) r.x0 = r.x1 = 0; r.ymin = r.ymax = 0; r.num_chars = 0; + *out_side_on_line = 0; // search rows to find one that straddles 'y' while (i < n) { @@ -456,7 +457,10 @@ static int stb_text_locate_coord(IMSTB_TEXTEDIT_STRING *str, float x, float y) // below all text, return 'after' last character if (i >= n) + { + *out_side_on_line = 1; return n; + } // check if it's before the beginning of the line if (x < r.x0) @@ -469,6 +473,7 @@ static int stb_text_locate_coord(IMSTB_TEXTEDIT_STRING *str, float x, float y) for (k=0; k < r.num_chars; k = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, i + k) - i) { float w = STB_TEXTEDIT_GETWIDTH(str, i, k); if (x < prev_x+w) { + *out_side_on_line = (k == 0) ? 0 : 1; if (x < prev_x+w/2) return k+i; else @@ -480,6 +485,7 @@ static int stb_text_locate_coord(IMSTB_TEXTEDIT_STRING *str, float x, float y) } // if the last character is a newline, return that. otherwise return 'after' the last character + *out_side_on_line = 1; if (STB_TEXTEDIT_GETCHAR(str, i+r.num_chars-1) == STB_TEXTEDIT_NEWLINE) return i+r.num_chars-1; else @@ -491,6 +497,7 @@ static void stb_textedit_click(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *st { // In single-line mode, just always make y = 0. This lets the drag keep working if the mouse // goes off the top or bottom of the text + int side_on_line; if( state->single_line ) { StbTexteditRow r; @@ -498,16 +505,18 @@ static void stb_textedit_click(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *st y = r.ymin; } - state->cursor = stb_text_locate_coord(str, x, y); + state->cursor = stb_text_locate_coord(str, x, y, &side_on_line); state->select_start = state->cursor; state->select_end = state->cursor; state->has_preferred_x = 0; + str->LastMoveDirectionLR = (ImS8)(side_on_line ? ImGuiDir_Right : ImGuiDir_Left); } // API drag: on mouse drag, move the cursor and selection endpoint to the clicked location static void stb_textedit_drag(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) { int p = 0; + int side_on_line; // In single-line mode, just always make y = 0. This lets the drag keep working if the mouse // goes off the top or bottom of the text @@ -521,8 +530,9 @@ static void stb_textedit_drag(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *sta if (state->select_start == state->select_end) state->select_start = state->cursor; - p = stb_text_locate_coord(str, x, y); + p = stb_text_locate_coord(str, x, y, &side_on_line); state->cursor = state->select_end = p; + str->LastMoveDirectionLR = (ImS8)(side_on_line ? ImGuiDir_Right : ImGuiDir_Left); } ///////////////////////////////////////////////////////////////////////////// From 16415aa39f50c7ea2015dba65058024328b47a8c Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 29 Aug 2025 18:18:18 +0200 Subject: [PATCH 16/17] InputText: Word-Wrap: added custom implementation for Home/End that is word-wrap friendly. (#3237, #952, #1062, #7363) --- imgui_widgets.cpp | 69 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 17ac3c1db..35dec8113 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -4101,6 +4101,75 @@ static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(ImGuiInputTextState* obj, int idx) #define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h #define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL +// Reimplementation of stb_textedit_move_line_start()/stb_textedit_move_line_end() which supports word-wrapping. +static int STB_TEXTEDIT_MOVELINESTART_IMPL(ImGuiInputTextState* obj, ImStb::STB_TexteditState* state, int cursor) +{ + if (state->single_line) + return 0; + + if (obj->WrapWidth > 0.0f) + { + ImGuiContext& g = *obj->Ctx; + const char* p_cursor = obj->TextSrc + cursor; + const char* p_bol = ImStrbol(p_cursor, obj->TextSrc); + const char* p = p_bol; + const char* text_end = obj->TextSrc + obj->TextLen; // End of line would be enough + while (p >= p_bol) + { + const char* p_eol = ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, p, text_end, obj->WrapWidth, ImDrawTextFlags_WrapKeepBlanks); + if (p == p_cursor) // If we are already on a visible beginning-of-line, return real beginning-of-line (would be same as regular handler below) + return (int)(p_bol - obj->TextSrc); + if (p_eol == p_cursor && obj->TextA[cursor] != '\n' && obj->LastMoveDirectionLR == ImGuiDir_Left) + return (int)(p_bol - obj->TextSrc); + if (p_eol >= p_cursor) + return (int)(p - obj->TextSrc); + p = (*p_eol == '\n') ? p_eol + 1 : p_eol; + } + } + + // Regular handler, same as stb_textedit_move_line_start() + while (cursor > 0) + { + int prev_cursor = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, cursor); + if (STB_TEXTEDIT_GETCHAR(obj, prev_cursor) == STB_TEXTEDIT_NEWLINE) + break; + cursor = prev_cursor; + } + return cursor; +} + +static int STB_TEXTEDIT_MOVELINEEND_IMPL(ImGuiInputTextState* obj, ImStb::STB_TexteditState* state, int cursor) +{ + int n = STB_TEXTEDIT_STRINGLEN(obj); + if (state->single_line) + return n; + + if (obj->WrapWidth > 0.0f) + { + ImGuiContext& g = *obj->Ctx; + const char* p_cursor = obj->TextSrc + cursor; + const char* p = ImStrbol(p_cursor, obj->TextSrc); + const char* text_end = obj->TextSrc + obj->TextLen; // End of line would be enough + while (p < text_end) + { + const char* p_eol = ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, p, text_end, obj->WrapWidth, ImDrawTextFlags_WrapKeepBlanks); + cursor = (int)(p_eol - obj->TextSrc); + if (p_eol == p_cursor && obj->LastMoveDirectionLR != ImGuiDir_Left) // If we are already on a visible end-of-line, switch to regular handle + break; + if (p_eol > p_cursor) + return cursor; + p = (*p_eol == '\n') ? p_eol + 1 : p_eol; + } + } + // Regular handler, same as stb_textedit_move_line_end() + while (cursor < n && STB_TEXTEDIT_GETCHAR(obj, cursor) != STB_TEXTEDIT_NEWLINE) + cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, cursor); + return cursor; +} + +#define STB_TEXTEDIT_MOVELINESTART STB_TEXTEDIT_MOVELINESTART_IMPL +#define STB_TEXTEDIT_MOVELINEEND STB_TEXTEDIT_MOVELINEEND_IMPL + static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n) { // Offset remaining text (+ copy zero terminator) From 230418a75d56ddb5ba51e0b196332b94d74867f2 Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 2 Sep 2025 15:37:27 +0200 Subject: [PATCH 17/17] InputText: Word-Wrap: attempt to track cursor while resizing frame/parent. (#3237, #952, #1062, #7363) --- imgui_internal.h | 1 + imgui_widgets.cpp | 33 ++++++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/imgui_internal.h b/imgui_internal.h index faeef64e0..901c61f06 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1247,6 +1247,7 @@ struct IMGUI_API ImGuiInputTextState float WrapWidth; // word-wrapping width float CursorAnim; // timer for cursor blink, reset on every user action so the cursor reappears immediately bool CursorFollow; // set when we want scrolling to follow the current cursor position (not always!) + bool CursorCenterY; // set when we want scrolling to be centered over the cursor position (while resizing a word-wrapping field) bool SelectedAllMouseLock; // after a double-click to select all, we ignore further mouse drags to update selection bool Edited; // edited this frame bool WantReloadUserBuf; // force a reload of user buf so it may be modified externally. may be automatic in future version. diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 35dec8113..4c4ef5dff 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -4821,6 +4821,15 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ if (is_password && !is_displaying_hint) PushPasswordFont(); + // Word-wrapping: attempt to keep cursor in view while resizing frame/parent + // FIXME-WORDWRAP: It would be better to preserve same relative offset. + if (is_wordwrap && state != NULL && state->ID == id && state->WrapWidth != wrap_width) + { + state->CursorCenterY = true; + state->WrapWidth = wrap_width; + render_cursor = true; + } + // Process mouse inputs and character inputs if (g.ActiveId == id) { @@ -5414,6 +5423,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ } // Scroll + float new_scroll_y = scroll_y; if (render_cursor && state->CursorFollow) { // Horizontal scroll in chunks of quarter width @@ -5436,17 +5446,26 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ { // Test if cursor is vertically visible if (cursor_offset.y - g.FontSize < scroll_y) - scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize); + new_scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize); else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y) - scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f; - const float scroll_max_y = ImMax((text_size.y + style.FramePadding.y * 2.0f) - inner_size.y, 0.0f); - scroll_y = ImClamp(scroll_y, 0.0f, scroll_max_y); - draw_pos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag - draw_window->Scroll.y = scroll_y; + new_scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f; } - state->CursorFollow = false; } + if (state->CursorCenterY) + { + if (is_multiline) + new_scroll_y = cursor_offset.y - g.FontSize - (inner_size.y * 0.5f - style.FramePadding.y); + state->CursorCenterY = false; + render_cursor = false; + } + if (new_scroll_y != scroll_y) + { + const float scroll_max_y = ImMax((text_size.y + style.FramePadding.y * 2.0f) - inner_size.y, 0.0f); + scroll_y = ImClamp(new_scroll_y, 0.0f, scroll_max_y); + draw_pos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag + draw_window->Scroll.y = scroll_y; + } // Draw selection const ImVec2 draw_scroll = ImVec2(state->Scroll.x, 0.0f);