imgui_widgets.cpp (502956B)
1 // dear imgui, v1.91.0 2 // (widgets code) 3 4 /* 5 6 Index of this file: 7 8 // [SECTION] Forward Declarations 9 // [SECTION] Widgets: Text, etc. 10 // [SECTION] Widgets: Main (Button, Image, Checkbox, RadioButton, ProgressBar, Bullet, etc.) 11 // [SECTION] Widgets: Low-level Layout helpers (Spacing, Dummy, NewLine, Separator, etc.) 12 // [SECTION] Widgets: ComboBox 13 // [SECTION] Data Type and Data Formatting Helpers 14 // [SECTION] Widgets: DragScalar, DragFloat, DragInt, etc. 15 // [SECTION] Widgets: SliderScalar, SliderFloat, SliderInt, etc. 16 // [SECTION] Widgets: InputScalar, InputFloat, InputInt, etc. 17 // [SECTION] Widgets: InputText, InputTextMultiline 18 // [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc. 19 // [SECTION] Widgets: TreeNode, CollapsingHeader, etc. 20 // [SECTION] Widgets: Selectable 21 // [SECTION] Widgets: Typing-Select support 22 // [SECTION] Widgets: Box-Select support 23 // [SECTION] Widgets: Multi-Select support 24 // [SECTION] Widgets: Multi-Select helpers 25 // [SECTION] Widgets: ListBox 26 // [SECTION] Widgets: PlotLines, PlotHistogram 27 // [SECTION] Widgets: Value helpers 28 // [SECTION] Widgets: MenuItem, BeginMenu, EndMenu, etc. 29 // [SECTION] Widgets: BeginTabBar, EndTabBar, etc. 30 // [SECTION] Widgets: BeginTabItem, EndTabItem, etc. 31 // [SECTION] Widgets: Columns, BeginColumns, EndColumns, etc. 32 33 */ 34 35 #if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) 36 #define _CRT_SECURE_NO_WARNINGS 37 #endif 38 39 #ifndef IMGUI_DEFINE_MATH_OPERATORS 40 #define IMGUI_DEFINE_MATH_OPERATORS 41 #endif 42 43 #include "imgui.h" 44 #ifndef IMGUI_DISABLE 45 #include "imgui_internal.h" 46 47 // System includes 48 #include <stdint.h> // intptr_t 49 50 //------------------------------------------------------------------------- 51 // Warnings 52 //------------------------------------------------------------------------- 53 54 // Visual Studio warnings 55 #ifdef _MSC_VER 56 #pragma warning (disable: 4127) // condition expression is constant 57 #pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen 58 #if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later 59 #pragma warning (disable: 5054) // operator '|': deprecated between enumerations of different types 60 #endif 61 #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). 62 #pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). 63 #endif 64 65 // Clang/GCC warnings with -Weverything 66 #if defined(__clang__) 67 #if __has_warning("-Wunknown-warning-option") 68 #pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' // not all warnings are known by all Clang versions and they tend to be rename-happy.. so ignoring warnings triggers new warnings on some configuration. Great! 69 #endif 70 #pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' 71 #pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse. 72 #pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok. 73 #pragma clang diagnostic ignored "-Wformat-nonliteral" // warning: format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code. 74 #pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness 75 #pragma clang diagnostic ignored "-Wunused-macros" // warning: macro is not used // we define snprintf/vsnprintf on Windows so they are available, but not always used. 76 #pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0 77 #pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. 78 #pragma clang diagnostic ignored "-Wenum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') 79 #pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated 80 #pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision 81 #pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access 82 #elif defined(__GNUC__) 83 #pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind 84 #pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked 85 #pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead 86 #pragma GCC diagnostic ignored "-Wdeprecated-enum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated 87 #endif 88 89 //------------------------------------------------------------------------- 90 // Data 91 //------------------------------------------------------------------------- 92 93 // Widgets 94 static const float DRAGDROP_HOLD_TO_OPEN_TIMER = 0.70f; // Time for drag-hold to activate items accepting the ImGuiButtonFlags_PressedOnDragDropHold button behavior. 95 static const float DRAG_MOUSE_THRESHOLD_FACTOR = 0.50f; // Multiplier for the default value of io.MouseDragThreshold to make DragFloat/DragInt react faster to mouse drags. 96 97 // Those MIN/MAX values are not define because we need to point to them 98 static const signed char IM_S8_MIN = -128; 99 static const signed char IM_S8_MAX = 127; 100 static const unsigned char IM_U8_MIN = 0; 101 static const unsigned char IM_U8_MAX = 0xFF; 102 static const signed short IM_S16_MIN = -32768; 103 static const signed short IM_S16_MAX = 32767; 104 static const unsigned short IM_U16_MIN = 0; 105 static const unsigned short IM_U16_MAX = 0xFFFF; 106 static const ImS32 IM_S32_MIN = INT_MIN; // (-2147483647 - 1), (0x80000000); 107 static const ImS32 IM_S32_MAX = INT_MAX; // (2147483647), (0x7FFFFFFF) 108 static const ImU32 IM_U32_MIN = 0; 109 static const ImU32 IM_U32_MAX = UINT_MAX; // (0xFFFFFFFF) 110 #ifdef LLONG_MIN 111 static const ImS64 IM_S64_MIN = LLONG_MIN; // (-9223372036854775807ll - 1ll); 112 static const ImS64 IM_S64_MAX = LLONG_MAX; // (9223372036854775807ll); 113 #else 114 static const ImS64 IM_S64_MIN = -9223372036854775807LL - 1; 115 static const ImS64 IM_S64_MAX = 9223372036854775807LL; 116 #endif 117 static const ImU64 IM_U64_MIN = 0; 118 #ifdef ULLONG_MAX 119 static const ImU64 IM_U64_MAX = ULLONG_MAX; // (0xFFFFFFFFFFFFFFFFull); 120 #else 121 static const ImU64 IM_U64_MAX = (2ULL * 9223372036854775807LL + 1); 122 #endif 123 124 //------------------------------------------------------------------------- 125 // [SECTION] Forward Declarations 126 //------------------------------------------------------------------------- 127 128 // For InputTextEx() 129 static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard = false); 130 static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end); 131 static ImVec2 InputTextCalcTextSizeW(ImGuiContext* ctx, const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false); 132 133 //------------------------------------------------------------------------- 134 // [SECTION] Widgets: Text, etc. 135 //------------------------------------------------------------------------- 136 // - TextEx() [Internal] 137 // - TextUnformatted() 138 // - Text() 139 // - TextV() 140 // - TextColored() 141 // - TextColoredV() 142 // - TextDisabled() 143 // - TextDisabledV() 144 // - TextWrapped() 145 // - TextWrappedV() 146 // - LabelText() 147 // - LabelTextV() 148 // - BulletText() 149 // - BulletTextV() 150 //------------------------------------------------------------------------- 151 152 void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags) 153 { 154 ImGuiWindow* window = GetCurrentWindow(); 155 if (window->SkipItems) 156 return; 157 ImGuiContext& g = *GImGui; 158 159 // Accept null ranges 160 if (text == text_end) 161 text = text_end = ""; 162 163 // Calculate length 164 const char* text_begin = text; 165 if (text_end == NULL) 166 text_end = text + strlen(text); // FIXME-OPT 167 168 const ImVec2 text_pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); 169 const float wrap_pos_x = window->DC.TextWrapPos; 170 const bool wrap_enabled = (wrap_pos_x >= 0.0f); 171 if (text_end - text <= 2000 || wrap_enabled) 172 { 173 // Common case 174 const float wrap_width = wrap_enabled ? CalcWrapWidthForPos(window->DC.CursorPos, wrap_pos_x) : 0.0f; 175 const ImVec2 text_size = CalcTextSize(text_begin, text_end, false, wrap_width); 176 177 ImRect bb(text_pos, text_pos + text_size); 178 ItemSize(text_size, 0.0f); 179 if (!ItemAdd(bb, 0)) 180 return; 181 182 // Render (we don't hide text after ## in this end-user function) 183 RenderTextWrapped(bb.Min, text_begin, text_end, wrap_width); 184 } 185 else 186 { 187 // Long text! 188 // Perform manual coarse clipping to optimize for long multi-line text 189 // - From this point we will only compute the width of lines that are visible. Optimization only available when word-wrapping is disabled. 190 // - We also don't vertically center the text within the line full height, which is unlikely to matter because we are likely the biggest and only item on the line. 191 // - We use memchr(), pay attention that well optimized versions of those str/mem functions are much faster than a casually written loop. 192 const char* line = text; 193 const float line_height = GetTextLineHeight(); 194 ImVec2 text_size(0, 0); 195 196 // Lines to skip (can't skip when logging text) 197 ImVec2 pos = text_pos; 198 if (!g.LogEnabled) 199 { 200 int lines_skippable = (int)((window->ClipRect.Min.y - text_pos.y) / line_height); 201 if (lines_skippable > 0) 202 { 203 int lines_skipped = 0; 204 while (line < text_end && lines_skipped < lines_skippable) 205 { 206 const char* line_end = (const char*)memchr(line, '\n', text_end - line); 207 if (!line_end) 208 line_end = text_end; 209 if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0) 210 text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x); 211 line = line_end + 1; 212 lines_skipped++; 213 } 214 pos.y += lines_skipped * line_height; 215 } 216 } 217 218 // Lines to render 219 if (line < text_end) 220 { 221 ImRect line_rect(pos, pos + ImVec2(FLT_MAX, line_height)); 222 while (line < text_end) 223 { 224 if (IsClippedEx(line_rect, 0)) 225 break; 226 227 const char* line_end = (const char*)memchr(line, '\n', text_end - line); 228 if (!line_end) 229 line_end = text_end; 230 text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x); 231 RenderText(pos, line, line_end, false); 232 line = line_end + 1; 233 line_rect.Min.y += line_height; 234 line_rect.Max.y += line_height; 235 pos.y += line_height; 236 } 237 238 // Count remaining lines 239 int lines_skipped = 0; 240 while (line < text_end) 241 { 242 const char* line_end = (const char*)memchr(line, '\n', text_end - line); 243 if (!line_end) 244 line_end = text_end; 245 if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0) 246 text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x); 247 line = line_end + 1; 248 lines_skipped++; 249 } 250 pos.y += lines_skipped * line_height; 251 } 252 text_size.y = (pos - text_pos).y; 253 254 ImRect bb(text_pos, text_pos + text_size); 255 ItemSize(text_size, 0.0f); 256 ItemAdd(bb, 0); 257 } 258 } 259 260 void ImGui::TextUnformatted(const char* text, const char* text_end) 261 { 262 TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText); 263 } 264 265 void ImGui::Text(const char* fmt, ...) 266 { 267 va_list args; 268 va_start(args, fmt); 269 TextV(fmt, args); 270 va_end(args); 271 } 272 273 void ImGui::TextV(const char* fmt, va_list args) 274 { 275 ImGuiWindow* window = GetCurrentWindow(); 276 if (window->SkipItems) 277 return; 278 279 const char* text, *text_end; 280 ImFormatStringToTempBufferV(&text, &text_end, fmt, args); 281 TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText); 282 } 283 284 void ImGui::TextColored(const ImVec4& col, const char* fmt, ...) 285 { 286 va_list args; 287 va_start(args, fmt); 288 TextColoredV(col, fmt, args); 289 va_end(args); 290 } 291 292 void ImGui::TextColoredV(const ImVec4& col, const char* fmt, va_list args) 293 { 294 PushStyleColor(ImGuiCol_Text, col); 295 TextV(fmt, args); 296 PopStyleColor(); 297 } 298 299 void ImGui::TextDisabled(const char* fmt, ...) 300 { 301 va_list args; 302 va_start(args, fmt); 303 TextDisabledV(fmt, args); 304 va_end(args); 305 } 306 307 void ImGui::TextDisabledV(const char* fmt, va_list args) 308 { 309 ImGuiContext& g = *GImGui; 310 PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]); 311 TextV(fmt, args); 312 PopStyleColor(); 313 } 314 315 void ImGui::TextWrapped(const char* fmt, ...) 316 { 317 va_list args; 318 va_start(args, fmt); 319 TextWrappedV(fmt, args); 320 va_end(args); 321 } 322 323 void ImGui::TextWrappedV(const char* fmt, va_list args) 324 { 325 ImGuiContext& g = *GImGui; 326 const bool need_backup = (g.CurrentWindow->DC.TextWrapPos < 0.0f); // Keep existing wrap position if one is already set 327 if (need_backup) 328 PushTextWrapPos(0.0f); 329 TextV(fmt, args); 330 if (need_backup) 331 PopTextWrapPos(); 332 } 333 334 void ImGui::LabelText(const char* label, const char* fmt, ...) 335 { 336 va_list args; 337 va_start(args, fmt); 338 LabelTextV(label, fmt, args); 339 va_end(args); 340 } 341 342 // Add a label+text combo aligned to other label+value widgets 343 void ImGui::LabelTextV(const char* label, const char* fmt, va_list args) 344 { 345 ImGuiWindow* window = GetCurrentWindow(); 346 if (window->SkipItems) 347 return; 348 349 ImGuiContext& g = *GImGui; 350 const ImGuiStyle& style = g.Style; 351 const float w = CalcItemWidth(); 352 353 const char* value_text_begin, *value_text_end; 354 ImFormatStringToTempBufferV(&value_text_begin, &value_text_end, fmt, args); 355 const ImVec2 value_size = CalcTextSize(value_text_begin, value_text_end, false); 356 const ImVec2 label_size = CalcTextSize(label, NULL, true); 357 358 const ImVec2 pos = window->DC.CursorPos; 359 const ImRect value_bb(pos, pos + ImVec2(w, value_size.y + style.FramePadding.y * 2)); 360 const ImRect total_bb(pos, pos + ImVec2(w + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), ImMax(value_size.y, label_size.y) + style.FramePadding.y * 2)); 361 ItemSize(total_bb, style.FramePadding.y); 362 if (!ItemAdd(total_bb, 0)) 363 return; 364 365 // Render 366 RenderTextClipped(value_bb.Min + style.FramePadding, value_bb.Max, value_text_begin, value_text_end, &value_size, ImVec2(0.0f, 0.0f)); 367 if (label_size.x > 0.0f) 368 RenderText(ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), label); 369 } 370 371 void ImGui::BulletText(const char* fmt, ...) 372 { 373 va_list args; 374 va_start(args, fmt); 375 BulletTextV(fmt, args); 376 va_end(args); 377 } 378 379 // Text with a little bullet aligned to the typical tree node. 380 void ImGui::BulletTextV(const char* fmt, va_list args) 381 { 382 ImGuiWindow* window = GetCurrentWindow(); 383 if (window->SkipItems) 384 return; 385 386 ImGuiContext& g = *GImGui; 387 const ImGuiStyle& style = g.Style; 388 389 const char* text_begin, *text_end; 390 ImFormatStringToTempBufferV(&text_begin, &text_end, fmt, args); 391 const ImVec2 label_size = CalcTextSize(text_begin, text_end, false); 392 const ImVec2 total_size = ImVec2(g.FontSize + (label_size.x > 0.0f ? (label_size.x + style.FramePadding.x * 2) : 0.0f), label_size.y); // Empty text doesn't add padding 393 ImVec2 pos = window->DC.CursorPos; 394 pos.y += window->DC.CurrLineTextBaseOffset; 395 ItemSize(total_size, 0.0f); 396 const ImRect bb(pos, pos + total_size); 397 if (!ItemAdd(bb, 0)) 398 return; 399 400 // Render 401 ImU32 text_col = GetColorU32(ImGuiCol_Text); 402 RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, g.FontSize * 0.5f), text_col); 403 RenderText(bb.Min + ImVec2(g.FontSize + style.FramePadding.x * 2, 0.0f), text_begin, text_end, false); 404 } 405 406 //------------------------------------------------------------------------- 407 // [SECTION] Widgets: Main 408 //------------------------------------------------------------------------- 409 // - ButtonBehavior() [Internal] 410 // - Button() 411 // - SmallButton() 412 // - InvisibleButton() 413 // - ArrowButton() 414 // - CloseButton() [Internal] 415 // - CollapseButton() [Internal] 416 // - GetWindowScrollbarID() [Internal] 417 // - GetWindowScrollbarRect() [Internal] 418 // - Scrollbar() [Internal] 419 // - ScrollbarEx() [Internal] 420 // - Image() 421 // - ImageButton() 422 // - Checkbox() 423 // - CheckboxFlagsT() [Internal] 424 // - CheckboxFlags() 425 // - RadioButton() 426 // - ProgressBar() 427 // - Bullet() 428 // - Hyperlink() 429 //------------------------------------------------------------------------- 430 431 // The ButtonBehavior() function is key to many interactions and used by many/most widgets. 432 // Because we handle so many cases (keyboard/gamepad navigation, drag and drop) and many specific behavior (via ImGuiButtonFlags_), 433 // this code is a little complex. 434 // By far the most common path is interacting with the Mouse using the default ImGuiButtonFlags_PressedOnClickRelease button behavior. 435 // See the series of events below and the corresponding state reported by dear imgui: 436 //------------------------------------------------------------------------------------------------------------------------------------------------ 437 // with PressedOnClickRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked() 438 // Frame N+0 (mouse is outside bb) - - - - - - 439 // Frame N+1 (mouse moves inside bb) - true - - - - 440 // Frame N+2 (mouse button is down) - true true true - true 441 // Frame N+3 (mouse button is down) - true true - - - 442 // Frame N+4 (mouse moves outside bb) - - true - - - 443 // Frame N+5 (mouse moves inside bb) - true true - - - 444 // Frame N+6 (mouse button is released) true true - - true - 445 // Frame N+7 (mouse button is released) - true - - - - 446 // Frame N+8 (mouse moves outside bb) - - - - - - 447 //------------------------------------------------------------------------------------------------------------------------------------------------ 448 // with PressedOnClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked() 449 // Frame N+2 (mouse button is down) true true true true - true 450 // Frame N+3 (mouse button is down) - true true - - - 451 // Frame N+6 (mouse button is released) - true - - true - 452 // Frame N+7 (mouse button is released) - true - - - - 453 //------------------------------------------------------------------------------------------------------------------------------------------------ 454 // with PressedOnRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked() 455 // Frame N+2 (mouse button is down) - true - - - true 456 // Frame N+3 (mouse button is down) - true - - - - 457 // Frame N+6 (mouse button is released) true true - - - - 458 // Frame N+7 (mouse button is released) - true - - - - 459 //------------------------------------------------------------------------------------------------------------------------------------------------ 460 // with PressedOnDoubleClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked() 461 // Frame N+0 (mouse button is down) - true - - - true 462 // Frame N+1 (mouse button is down) - true - - - - 463 // Frame N+2 (mouse button is released) - true - - - - 464 // Frame N+3 (mouse button is released) - true - - - - 465 // Frame N+4 (mouse button is down) true true true true - true 466 // Frame N+5 (mouse button is down) - true true - - - 467 // Frame N+6 (mouse button is released) - true - - true - 468 // Frame N+7 (mouse button is released) - true - - - - 469 //------------------------------------------------------------------------------------------------------------------------------------------------ 470 // Note that some combinations are supported, 471 // - PressedOnDragDropHold can generally be associated with any flag. 472 // - PressedOnDoubleClick can be associated by PressedOnClickRelease/PressedOnRelease, in which case the second release event won't be reported. 473 //------------------------------------------------------------------------------------------------------------------------------------------------ 474 // The behavior of the return-value changes when ImGuiButtonFlags_Repeat is set: 475 // Repeat+ Repeat+ Repeat+ Repeat+ 476 // PressedOnClickRelease PressedOnClick PressedOnRelease PressedOnDoubleClick 477 //------------------------------------------------------------------------------------------------------------------------------------------------- 478 // Frame N+0 (mouse button is down) - true - true 479 // ... - - - - 480 // Frame N + RepeatDelay true true - true 481 // ... - - - - 482 // Frame N + RepeatDelay + RepeatRate*N true true - true 483 //------------------------------------------------------------------------------------------------------------------------------------------------- 484 485 // FIXME: For refactor we could output flags, incl mouse hovered vs nav keyboard vs nav triggered etc. 486 // And better standardize how widgets use 'GetColor32((held && hovered) ? ... : hovered ? ...)' vs 'GetColor32(held ? ... : hovered ? ...);' 487 // For mouse feedback we typically prefer the 'held && hovered' test, but for nav feedback not always. Outputting hovered=true on Activation may be misleading. 488 bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags) 489 { 490 ImGuiContext& g = *GImGui; 491 ImGuiWindow* window = GetCurrentWindow(); 492 493 // Default only reacts to left mouse button 494 if ((flags & ImGuiButtonFlags_MouseButtonMask_) == 0) 495 flags |= ImGuiButtonFlags_MouseButtonLeft; 496 497 // Default behavior requires click + release inside bounding box 498 if ((flags & ImGuiButtonFlags_PressedOnMask_) == 0) 499 flags |= ImGuiButtonFlags_PressedOnDefault_; 500 501 // Default behavior inherited from item flags 502 // Note that _both_ ButtonFlags and ItemFlags are valid sources, so copy one into the item_flags and only check that. 503 ImGuiItemFlags item_flags = (g.LastItemData.ID == id ? g.LastItemData.InFlags : g.CurrentItemFlags); 504 if (flags & ImGuiButtonFlags_AllowOverlap) 505 item_flags |= ImGuiItemFlags_AllowOverlap; 506 if (flags & ImGuiButtonFlags_Repeat) 507 item_flags |= ImGuiItemFlags_ButtonRepeat; 508 509 ImGuiWindow* backup_hovered_window = g.HoveredWindow; 510 const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredWindow && g.HoveredWindow->RootWindow == window; 511 if (flatten_hovered_children) 512 g.HoveredWindow = window; 513 514 #ifdef IMGUI_ENABLE_TEST_ENGINE 515 // Alternate registration spot, for when caller didn't use ItemAdd() 516 if (g.LastItemData.ID != id) 517 IMGUI_TEST_ENGINE_ITEM_ADD(id, bb, NULL); 518 #endif 519 520 bool pressed = false; 521 bool hovered = ItemHoverable(bb, id, item_flags); 522 523 // Special mode for Drag and Drop where holding button pressed for a long time while dragging another item triggers the button 524 if (g.DragDropActive && (flags & ImGuiButtonFlags_PressedOnDragDropHold) && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers)) 525 if (IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) 526 { 527 hovered = true; 528 SetHoveredID(id); 529 if (g.HoveredIdTimer - g.IO.DeltaTime <= DRAGDROP_HOLD_TO_OPEN_TIMER && g.HoveredIdTimer >= DRAGDROP_HOLD_TO_OPEN_TIMER) 530 { 531 pressed = true; 532 g.DragDropHoldJustPressedId = id; 533 FocusWindow(window); 534 } 535 } 536 537 if (flatten_hovered_children) 538 g.HoveredWindow = backup_hovered_window; 539 540 // Mouse handling 541 const ImGuiID test_owner_id = (flags & ImGuiButtonFlags_NoTestKeyOwner) ? ImGuiKeyOwner_Any : id; 542 if (hovered) 543 { 544 IM_ASSERT(id != 0); // Lazily check inside rare path. 545 546 // Poll mouse buttons 547 // - 'mouse_button_clicked' is generally carried into ActiveIdMouseButton when setting ActiveId. 548 // - Technically we only need some values in one code path, but since this is gated by hovered test this is fine. 549 int mouse_button_clicked = -1; 550 int mouse_button_released = -1; 551 for (int button = 0; button < 3; button++) 552 if (flags & (ImGuiButtonFlags_MouseButtonLeft << button)) // Handle ImGuiButtonFlags_MouseButtonRight and ImGuiButtonFlags_MouseButtonMiddle here. 553 { 554 if (IsMouseClicked(button, ImGuiInputFlags_None, test_owner_id) && mouse_button_clicked == -1) { mouse_button_clicked = button; } 555 if (IsMouseReleased(button, test_owner_id) && mouse_button_released == -1) { mouse_button_released = button; } 556 } 557 558 // Process initial action 559 if (!(flags & ImGuiButtonFlags_NoKeyModifiers) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt)) 560 { 561 if (mouse_button_clicked != -1 && g.ActiveId != id) 562 { 563 if (!(flags & ImGuiButtonFlags_NoSetKeyOwner)) 564 SetKeyOwner(MouseButtonToKey(mouse_button_clicked), id); 565 if (flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClickReleaseAnywhere)) 566 { 567 SetActiveID(id, window); 568 g.ActiveIdMouseButton = mouse_button_clicked; 569 if (!(flags & ImGuiButtonFlags_NoNavFocus)) 570 { 571 SetFocusID(id, window); 572 FocusWindow(window); 573 } 574 else 575 { 576 FocusWindow(window, ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child 577 } 578 } 579 if ((flags & ImGuiButtonFlags_PressedOnClick) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseClickedCount[mouse_button_clicked] == 2)) 580 { 581 pressed = true; 582 if (flags & ImGuiButtonFlags_NoHoldingActiveId) 583 ClearActiveID(); 584 else 585 SetActiveID(id, window); // Hold on ID 586 g.ActiveIdMouseButton = mouse_button_clicked; 587 if (!(flags & ImGuiButtonFlags_NoNavFocus)) 588 { 589 SetFocusID(id, window); 590 FocusWindow(window); 591 } 592 else 593 { 594 FocusWindow(window, ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child 595 } 596 } 597 } 598 if (flags & ImGuiButtonFlags_PressedOnRelease) 599 { 600 if (mouse_button_released != -1) 601 { 602 const bool has_repeated_at_least_once = (item_flags & ImGuiItemFlags_ButtonRepeat) && g.IO.MouseDownDurationPrev[mouse_button_released] >= g.IO.KeyRepeatDelay; // Repeat mode trumps on release behavior 603 if (!has_repeated_at_least_once) 604 pressed = true; 605 if (!(flags & ImGuiButtonFlags_NoNavFocus)) 606 SetFocusID(id, window); 607 ClearActiveID(); 608 } 609 } 610 611 // 'Repeat' mode acts when held regardless of _PressedOn flags (see table above). 612 // Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings. 613 if (g.ActiveId == id && (item_flags & ImGuiItemFlags_ButtonRepeat)) 614 if (g.IO.MouseDownDuration[g.ActiveIdMouseButton] > 0.0f && IsMouseClicked(g.ActiveIdMouseButton, ImGuiInputFlags_Repeat, test_owner_id)) 615 pressed = true; 616 } 617 618 if (pressed) 619 g.NavDisableHighlight = true; 620 } 621 622 // Gamepad/Keyboard handling 623 // We report navigated and navigation-activated items as hovered but we don't set g.HoveredId to not interfere with mouse. 624 if (g.NavId == id && !g.NavDisableHighlight && g.NavDisableMouseHover) 625 if (!(flags & ImGuiButtonFlags_NoHoveredOnFocus)) 626 hovered = true; 627 if (g.NavActivateDownId == id) 628 { 629 bool nav_activated_by_code = (g.NavActivateId == id); 630 bool nav_activated_by_inputs = (g.NavActivatePressedId == id); 631 if (!nav_activated_by_inputs && (item_flags & ImGuiItemFlags_ButtonRepeat)) 632 { 633 // Avoid pressing multiple keys from triggering excessive amount of repeat events 634 const ImGuiKeyData* key1 = GetKeyData(ImGuiKey_Space); 635 const ImGuiKeyData* key2 = GetKeyData(ImGuiKey_Enter); 636 const ImGuiKeyData* key3 = GetKeyData(ImGuiKey_NavGamepadActivate); 637 const float t1 = ImMax(ImMax(key1->DownDuration, key2->DownDuration), key3->DownDuration); 638 nav_activated_by_inputs = CalcTypematicRepeatAmount(t1 - g.IO.DeltaTime, t1, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate) > 0; 639 } 640 if (nav_activated_by_code || nav_activated_by_inputs) 641 { 642 // Set active id so it can be queried by user via IsItemActive(), equivalent of holding the mouse button. 643 pressed = true; 644 SetActiveID(id, window); 645 g.ActiveIdSource = g.NavInputSource; 646 if (!(flags & ImGuiButtonFlags_NoNavFocus) && !(g.NavActivateFlags & ImGuiActivateFlags_FromShortcut)) 647 SetFocusID(id, window); 648 if (g.NavActivateFlags & ImGuiActivateFlags_FromShortcut) 649 g.ActiveIdFromShortcut = true; 650 } 651 } 652 653 // Process while held 654 bool held = false; 655 if (g.ActiveId == id) 656 { 657 if (g.ActiveIdSource == ImGuiInputSource_Mouse) 658 { 659 if (g.ActiveIdIsJustActivated) 660 g.ActiveIdClickOffset = g.IO.MousePos - bb.Min; 661 662 const int mouse_button = g.ActiveIdMouseButton; 663 if (mouse_button == -1) 664 { 665 // Fallback for the rare situation were g.ActiveId was set programmatically or from another widget (e.g. #6304). 666 ClearActiveID(); 667 } 668 else if (IsMouseDown(mouse_button, test_owner_id)) 669 { 670 held = true; 671 } 672 else 673 { 674 bool release_in = hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease) != 0; 675 bool release_anywhere = (flags & ImGuiButtonFlags_PressedOnClickReleaseAnywhere) != 0; 676 if ((release_in || release_anywhere) && !g.DragDropActive) 677 { 678 // Report as pressed when releasing the mouse (this is the most common path) 679 bool is_double_click_release = (flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseReleased[mouse_button] && g.IO.MouseClickedLastCount[mouse_button] == 2; 680 bool is_repeating_already = (item_flags & ImGuiItemFlags_ButtonRepeat) && g.IO.MouseDownDurationPrev[mouse_button] >= g.IO.KeyRepeatDelay; // Repeat mode trumps <on release> 681 bool is_button_avail_or_owned = TestKeyOwner(MouseButtonToKey(mouse_button), test_owner_id); 682 if (!is_double_click_release && !is_repeating_already && is_button_avail_or_owned) 683 pressed = true; 684 } 685 ClearActiveID(); 686 } 687 if (!(flags & ImGuiButtonFlags_NoNavFocus)) 688 g.NavDisableHighlight = true; 689 } 690 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) 691 { 692 // When activated using Nav, we hold on the ActiveID until activation button is released 693 if (g.NavActivateDownId == id) 694 held = true; // hovered == true not true as we are already likely hovered on direct activation. 695 else 696 ClearActiveID(); 697 } 698 if (pressed) 699 g.ActiveIdHasBeenPressedBefore = true; 700 } 701 702 // Activation highlight (this may be a remote activation) 703 if (g.NavHighlightActivatedId == id) 704 hovered = true; 705 706 if (out_hovered) *out_hovered = hovered; 707 if (out_held) *out_held = held; 708 709 return pressed; 710 } 711 712 bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags) 713 { 714 ImGuiWindow* window = GetCurrentWindow(); 715 if (window->SkipItems) 716 return false; 717 718 ImGuiContext& g = *GImGui; 719 const ImGuiStyle& style = g.Style; 720 const ImGuiID id = window->GetID(label); 721 const ImVec2 label_size = CalcTextSize(label, NULL, true); 722 723 ImVec2 pos = window->DC.CursorPos; 724 if ((flags & ImGuiButtonFlags_AlignTextBaseLine) && style.FramePadding.y < window->DC.CurrLineTextBaseOffset) // Try to vertically align buttons that are smaller/have no padding so that text baseline matches (bit hacky, since it shouldn't be a flag) 725 pos.y += window->DC.CurrLineTextBaseOffset - style.FramePadding.y; 726 ImVec2 size = CalcItemSize(size_arg, label_size.x + style.FramePadding.x * 2.0f, label_size.y + style.FramePadding.y * 2.0f); 727 728 const ImRect bb(pos, pos + size); 729 ItemSize(size, style.FramePadding.y); 730 if (!ItemAdd(bb, id)) 731 return false; 732 733 bool hovered, held; 734 bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); 735 736 // Render 737 const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); 738 RenderNavHighlight(bb, id); 739 RenderFrame(bb.Min, bb.Max, col, true, style.FrameRounding); 740 741 if (g.LogEnabled) 742 LogSetNextTextDecoration("[", "]"); 743 RenderTextClipped(bb.Min + style.FramePadding, bb.Max - style.FramePadding, label, NULL, &label_size, style.ButtonTextAlign, &bb); 744 745 // Automatically close popups 746 //if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup)) 747 // CloseCurrentPopup(); 748 749 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); 750 return pressed; 751 } 752 753 bool ImGui::Button(const char* label, const ImVec2& size_arg) 754 { 755 return ButtonEx(label, size_arg, ImGuiButtonFlags_None); 756 } 757 758 // Small buttons fits within text without additional vertical spacing. 759 bool ImGui::SmallButton(const char* label) 760 { 761 ImGuiContext& g = *GImGui; 762 float backup_padding_y = g.Style.FramePadding.y; 763 g.Style.FramePadding.y = 0.0f; 764 bool pressed = ButtonEx(label, ImVec2(0, 0), ImGuiButtonFlags_AlignTextBaseLine); 765 g.Style.FramePadding.y = backup_padding_y; 766 return pressed; 767 } 768 769 // Tip: use ImGui::PushID()/PopID() to push indices or pointers in the ID stack. 770 // Then you can keep 'str_id' empty or the same for all your buttons (instead of creating a string based on a non-string id) 771 bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg, ImGuiButtonFlags flags) 772 { 773 ImGuiContext& g = *GImGui; 774 ImGuiWindow* window = GetCurrentWindow(); 775 if (window->SkipItems) 776 return false; 777 778 // Cannot use zero-size for InvisibleButton(). Unlike Button() there is not way to fallback using the label size. 779 IM_ASSERT(size_arg.x != 0.0f && size_arg.y != 0.0f); 780 781 const ImGuiID id = window->GetID(str_id); 782 ImVec2 size = CalcItemSize(size_arg, 0.0f, 0.0f); 783 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); 784 ItemSize(size); 785 if (!ItemAdd(bb, id)) 786 return false; 787 788 bool hovered, held; 789 bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); 790 791 IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags); 792 return pressed; 793 } 794 795 bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiButtonFlags flags) 796 { 797 ImGuiContext& g = *GImGui; 798 ImGuiWindow* window = GetCurrentWindow(); 799 if (window->SkipItems) 800 return false; 801 802 const ImGuiID id = window->GetID(str_id); 803 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); 804 const float default_size = GetFrameHeight(); 805 ItemSize(size, (size.y >= default_size) ? g.Style.FramePadding.y : -1.0f); 806 if (!ItemAdd(bb, id)) 807 return false; 808 809 bool hovered, held; 810 bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); 811 812 // Render 813 const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); 814 const ImU32 text_col = GetColorU32(ImGuiCol_Text); 815 RenderNavHighlight(bb, id); 816 RenderFrame(bb.Min, bb.Max, bg_col, true, g.Style.FrameRounding); 817 RenderArrow(window->DrawList, bb.Min + ImVec2(ImMax(0.0f, (size.x - g.FontSize) * 0.5f), ImMax(0.0f, (size.y - g.FontSize) * 0.5f)), text_col, dir); 818 819 IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags); 820 return pressed; 821 } 822 823 bool ImGui::ArrowButton(const char* str_id, ImGuiDir dir) 824 { 825 float sz = GetFrameHeight(); 826 return ArrowButtonEx(str_id, dir, ImVec2(sz, sz), ImGuiButtonFlags_None); 827 } 828 829 // Button to close a window 830 bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos) 831 { 832 ImGuiContext& g = *GImGui; 833 ImGuiWindow* window = g.CurrentWindow; 834 835 // Tweak 1: Shrink hit-testing area if button covers an abnormally large proportion of the visible region. That's in order to facilitate moving the window away. (#3825) 836 // This may better be applied as a general hit-rect reduction mechanism for all widgets to ensure the area to move window is always accessible? 837 const ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize)); 838 ImRect bb_interact = bb; 839 const float area_to_visible_ratio = window->OuterRectClipped.GetArea() / bb.GetArea(); 840 if (area_to_visible_ratio < 1.5f) 841 bb_interact.Expand(ImTrunc(bb_interact.GetSize() * -0.25f)); 842 843 // Tweak 2: We intentionally allow interaction when clipped so that a mechanical Alt,Right,Activate sequence can always close a window. 844 // (this isn't the common behavior of buttons, but it doesn't affect the user because navigation tends to keep items visible in scrolling layer). 845 bool is_clipped = !ItemAdd(bb_interact, id); 846 847 bool hovered, held; 848 bool pressed = ButtonBehavior(bb_interact, id, &hovered, &held); 849 if (is_clipped) 850 return pressed; 851 852 // Render 853 ImU32 bg_col = GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered); 854 if (hovered) 855 window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col); 856 RenderNavHighlight(bb, id, ImGuiNavHighlightFlags_Compact); 857 ImU32 cross_col = GetColorU32(ImGuiCol_Text); 858 ImVec2 cross_center = bb.GetCenter() - ImVec2(0.5f, 0.5f); 859 float cross_extent = g.FontSize * 0.5f * 0.7071f - 1.0f; 860 window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, +cross_extent), cross_center + ImVec2(-cross_extent, -cross_extent), cross_col, 1.0f); 861 window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, -cross_extent), cross_center + ImVec2(-cross_extent, +cross_extent), cross_col, 1.0f); 862 863 return pressed; 864 } 865 866 bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos) 867 { 868 ImGuiContext& g = *GImGui; 869 ImGuiWindow* window = g.CurrentWindow; 870 871 ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize)); 872 bool is_clipped = !ItemAdd(bb, id); 873 bool hovered, held; 874 bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None); 875 if (is_clipped) 876 return pressed; 877 878 // Render 879 ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); 880 ImU32 text_col = GetColorU32(ImGuiCol_Text); 881 if (hovered || held) 882 window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col); 883 RenderNavHighlight(bb, id, ImGuiNavHighlightFlags_Compact); 884 RenderArrow(window->DrawList, bb.Min, text_col, window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, 1.0f); 885 886 // Switch to moving the window after mouse is moved beyond the initial drag threshold 887 if (IsItemActive() && IsMouseDragging(0)) 888 StartMouseMovingWindow(window); 889 890 return pressed; 891 } 892 893 ImGuiID ImGui::GetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis) 894 { 895 return window->GetID(axis == ImGuiAxis_X ? "#SCROLLX" : "#SCROLLY"); 896 } 897 898 // Return scrollbar rectangle, must only be called for corresponding axis if window->ScrollbarX/Y is set. 899 ImRect ImGui::GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis) 900 { 901 const ImRect outer_rect = window->Rect(); 902 const ImRect inner_rect = window->InnerRect; 903 const float border_size = window->WindowBorderSize; 904 const float scrollbar_size = window->ScrollbarSizes[axis ^ 1]; // (ScrollbarSizes.x = width of Y scrollbar; ScrollbarSizes.y = height of X scrollbar) 905 IM_ASSERT(scrollbar_size > 0.0f); 906 if (axis == ImGuiAxis_X) 907 return ImRect(inner_rect.Min.x, ImMax(outer_rect.Min.y, outer_rect.Max.y - border_size - scrollbar_size), inner_rect.Max.x - border_size, outer_rect.Max.y - border_size); 908 else 909 return ImRect(ImMax(outer_rect.Min.x, outer_rect.Max.x - border_size - scrollbar_size), inner_rect.Min.y, outer_rect.Max.x - border_size, inner_rect.Max.y - border_size); 910 } 911 912 void ImGui::Scrollbar(ImGuiAxis axis) 913 { 914 ImGuiContext& g = *GImGui; 915 ImGuiWindow* window = g.CurrentWindow; 916 const ImGuiID id = GetWindowScrollbarID(window, axis); 917 918 // Calculate scrollbar bounding box 919 ImRect bb = GetWindowScrollbarRect(window, axis); 920 ImDrawFlags rounding_corners = ImDrawFlags_RoundCornersNone; 921 if (axis == ImGuiAxis_X) 922 { 923 rounding_corners |= ImDrawFlags_RoundCornersBottomLeft; 924 if (!window->ScrollbarY) 925 rounding_corners |= ImDrawFlags_RoundCornersBottomRight; 926 } 927 else 928 { 929 if ((window->Flags & ImGuiWindowFlags_NoTitleBar) && !(window->Flags & ImGuiWindowFlags_MenuBar)) 930 rounding_corners |= ImDrawFlags_RoundCornersTopRight; 931 if (!window->ScrollbarX) 932 rounding_corners |= ImDrawFlags_RoundCornersBottomRight; 933 } 934 float size_visible = window->InnerRect.Max[axis] - window->InnerRect.Min[axis]; 935 float size_contents = window->ContentSize[axis] + window->WindowPadding[axis] * 2.0f; 936 ImS64 scroll = (ImS64)window->Scroll[axis]; 937 ScrollbarEx(bb, id, axis, &scroll, (ImS64)size_visible, (ImS64)size_contents, rounding_corners); 938 window->Scroll[axis] = (float)scroll; 939 } 940 941 // Vertical/Horizontal scrollbar 942 // The entire piece of code below is rather confusing because: 943 // - We handle absolute seeking (when first clicking outside the grab) and relative manipulation (afterward or when clicking inside the grab) 944 // - We store values as normalized ratio and in a form that allows the window content to change while we are holding on a scrollbar 945 // - We handle both horizontal and vertical scrollbars, which makes the terminology not ideal. 946 // Still, the code should probably be made simpler.. 947 bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, ImS64* p_scroll_v, ImS64 size_visible_v, ImS64 size_contents_v, ImDrawFlags flags) 948 { 949 ImGuiContext& g = *GImGui; 950 ImGuiWindow* window = g.CurrentWindow; 951 if (window->SkipItems) 952 return false; 953 954 const float bb_frame_width = bb_frame.GetWidth(); 955 const float bb_frame_height = bb_frame.GetHeight(); 956 if (bb_frame_width <= 0.0f || bb_frame_height <= 0.0f) 957 return false; 958 959 // When we are too small, start hiding and disabling the grab (this reduce visual noise on very small window and facilitate using the window resize grab) 960 float alpha = 1.0f; 961 if ((axis == ImGuiAxis_Y) && bb_frame_height < g.FontSize + g.Style.FramePadding.y * 2.0f) 962 alpha = ImSaturate((bb_frame_height - g.FontSize) / (g.Style.FramePadding.y * 2.0f)); 963 if (alpha <= 0.0f) 964 return false; 965 966 const ImGuiStyle& style = g.Style; 967 const bool allow_interaction = (alpha >= 1.0f); 968 969 ImRect bb = bb_frame; 970 bb.Expand(ImVec2(-ImClamp(IM_TRUNC((bb_frame_width - 2.0f) * 0.5f), 0.0f, 3.0f), -ImClamp(IM_TRUNC((bb_frame_height - 2.0f) * 0.5f), 0.0f, 3.0f))); 971 972 // V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar) 973 const float scrollbar_size_v = (axis == ImGuiAxis_X) ? bb.GetWidth() : bb.GetHeight(); 974 975 // Calculate the height of our grabbable box. It generally represent the amount visible (vs the total scrollable amount) 976 // But we maintain a minimum size in pixel to allow for the user to still aim inside. 977 IM_ASSERT(ImMax(size_contents_v, size_visible_v) > 0.0f); // Adding this assert to check if the ImMax(XXX,1.0f) is still needed. PLEASE CONTACT ME if this triggers. 978 const ImS64 win_size_v = ImMax(ImMax(size_contents_v, size_visible_v), (ImS64)1); 979 const float grab_h_pixels = ImClamp(scrollbar_size_v * ((float)size_visible_v / (float)win_size_v), style.GrabMinSize, scrollbar_size_v); 980 const float grab_h_norm = grab_h_pixels / scrollbar_size_v; 981 982 // Handle input right away. None of the code of Begin() is relying on scrolling position before calling Scrollbar(). 983 bool held = false; 984 bool hovered = false; 985 ItemAdd(bb_frame, id, NULL, ImGuiItemFlags_NoNav); 986 ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_NoNavFocus); 987 988 const ImS64 scroll_max = ImMax((ImS64)1, size_contents_v - size_visible_v); 989 float scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max); 990 float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; // Grab position in normalized space 991 if (held && allow_interaction && grab_h_norm < 1.0f) 992 { 993 const float scrollbar_pos_v = bb.Min[axis]; 994 const float mouse_pos_v = g.IO.MousePos[axis]; 995 996 // Click position in scrollbar normalized space (0.0f->1.0f) 997 const float clicked_v_norm = ImSaturate((mouse_pos_v - scrollbar_pos_v) / scrollbar_size_v); 998 999 const int held_dir = (clicked_v_norm < grab_v_norm) ? -1 : (clicked_v_norm > grab_v_norm + grab_h_norm) ? +1 : 0; 1000 if (g.ActiveIdIsJustActivated) 1001 { 1002 // On initial click calculate the distance between mouse and the center of the grab 1003 g.ScrollbarSeekMode = (short)held_dir; 1004 g.ScrollbarClickDeltaToGrabCenter = (g.ScrollbarSeekMode == 0.0f) ? clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f : 0.0f; 1005 } 1006 1007 // Apply scroll (p_scroll_v will generally point on one member of window->Scroll) 1008 // It is ok to modify Scroll here because we are being called in Begin() after the calculation of ContentSize and before setting up our starting position 1009 if (g.ScrollbarSeekMode == 0) 1010 { 1011 // Absolute seeking 1012 const float scroll_v_norm = ImSaturate((clicked_v_norm - g.ScrollbarClickDeltaToGrabCenter - grab_h_norm * 0.5f) / (1.0f - grab_h_norm)); 1013 *p_scroll_v = (ImS64)(scroll_v_norm * scroll_max); 1014 } 1015 else 1016 { 1017 // Page by page 1018 if (IsMouseClicked(ImGuiMouseButton_Left, ImGuiInputFlags_Repeat) && held_dir == g.ScrollbarSeekMode) 1019 { 1020 float page_dir = (g.ScrollbarSeekMode > 0.0f) ? +1.0f : -1.0f; 1021 *p_scroll_v = ImClamp(*p_scroll_v + (ImS64)(page_dir * size_visible_v), (ImS64)0, scroll_max); 1022 } 1023 } 1024 1025 // Update values for rendering 1026 scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max); 1027 grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; 1028 1029 // Update distance to grab now that we have seek'ed and saturated 1030 //if (seek_absolute) 1031 // g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f; 1032 } 1033 1034 // Render 1035 const ImU32 bg_col = GetColorU32(ImGuiCol_ScrollbarBg); 1036 const ImU32 grab_col = GetColorU32(held ? ImGuiCol_ScrollbarGrabActive : hovered ? ImGuiCol_ScrollbarGrabHovered : ImGuiCol_ScrollbarGrab, alpha); 1037 window->DrawList->AddRectFilled(bb_frame.Min, bb_frame.Max, bg_col, window->WindowRounding, flags); 1038 ImRect grab_rect; 1039 if (axis == ImGuiAxis_X) 1040 grab_rect = ImRect(ImLerp(bb.Min.x, bb.Max.x, grab_v_norm), bb.Min.y, ImLerp(bb.Min.x, bb.Max.x, grab_v_norm) + grab_h_pixels, bb.Max.y); 1041 else 1042 grab_rect = ImRect(bb.Min.x, ImLerp(bb.Min.y, bb.Max.y, grab_v_norm), bb.Max.x, ImLerp(bb.Min.y, bb.Max.y, grab_v_norm) + grab_h_pixels); 1043 window->DrawList->AddRectFilled(grab_rect.Min, grab_rect.Max, grab_col, style.ScrollbarRounding); 1044 1045 return held; 1046 } 1047 1048 // - Read about ImTextureID here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples 1049 // - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above. 1050 void ImGui::Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col) 1051 { 1052 ImGuiWindow* window = GetCurrentWindow(); 1053 if (window->SkipItems) 1054 return; 1055 1056 const float border_size = (border_col.w > 0.0f) ? 1.0f : 0.0f; 1057 const ImVec2 padding(border_size, border_size); 1058 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f); 1059 ItemSize(bb); 1060 if (!ItemAdd(bb, 0)) 1061 return; 1062 1063 // Render 1064 if (border_size > 0.0f) 1065 window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(border_col), 0.0f, ImDrawFlags_None, border_size); 1066 window->DrawList->AddImage(user_texture_id, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col)); 1067 } 1068 1069 // ImageButton() is flawed as 'id' is always derived from 'texture_id' (see #2464 #1390) 1070 // We provide this internal helper to write your own variant while we figure out how to redesign the public ImageButton() API. 1071 bool ImGui::ImageButtonEx(ImGuiID id, ImTextureID texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col, ImGuiButtonFlags flags) 1072 { 1073 ImGuiContext& g = *GImGui; 1074 ImGuiWindow* window = GetCurrentWindow(); 1075 if (window->SkipItems) 1076 return false; 1077 1078 const ImVec2 padding = g.Style.FramePadding; 1079 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f); 1080 ItemSize(bb); 1081 if (!ItemAdd(bb, id)) 1082 return false; 1083 1084 bool hovered, held; 1085 bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); 1086 1087 // Render 1088 const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); 1089 RenderNavHighlight(bb, id); 1090 RenderFrame(bb.Min, bb.Max, col, true, ImClamp((float)ImMin(padding.x, padding.y), 0.0f, g.Style.FrameRounding)); 1091 if (bg_col.w > 0.0f) 1092 window->DrawList->AddRectFilled(bb.Min + padding, bb.Max - padding, GetColorU32(bg_col)); 1093 window->DrawList->AddImage(texture_id, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col)); 1094 1095 return pressed; 1096 } 1097 1098 // Note that ImageButton() adds style.FramePadding*2.0f to provided size. This is in order to facilitate fitting an image in a button. 1099 bool ImGui::ImageButton(const char* str_id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col) 1100 { 1101 ImGuiContext& g = *GImGui; 1102 ImGuiWindow* window = g.CurrentWindow; 1103 if (window->SkipItems) 1104 return false; 1105 1106 return ImageButtonEx(window->GetID(str_id), user_texture_id, image_size, uv0, uv1, bg_col, tint_col); 1107 } 1108 1109 #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS 1110 // Legacy API obsoleted in 1.89. Two differences with new ImageButton() 1111 // - new ImageButton() requires an explicit 'const char* str_id' Old ImageButton() used opaque imTextureId (created issue with: multiple buttons with same image, transient texture id values, opaque computation of ID) 1112 // - new ImageButton() always use style.FramePadding Old ImageButton() had an override argument. 1113 // If you need to change padding with new ImageButton() you can use PushStyleVar(ImGuiStyleVar_FramePadding, value), consistent with other Button functions. 1114 bool ImGui::ImageButton(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, int frame_padding, const ImVec4& bg_col, const ImVec4& tint_col) 1115 { 1116 ImGuiContext& g = *GImGui; 1117 ImGuiWindow* window = g.CurrentWindow; 1118 if (window->SkipItems) 1119 return false; 1120 1121 // Default to using texture ID as ID. User can still push string/integer prefixes. 1122 PushID((void*)(intptr_t)user_texture_id); 1123 const ImGuiID id = window->GetID("#image"); 1124 PopID(); 1125 1126 if (frame_padding >= 0) 1127 PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2((float)frame_padding, (float)frame_padding)); 1128 bool ret = ImageButtonEx(id, user_texture_id, size, uv0, uv1, bg_col, tint_col); 1129 if (frame_padding >= 0) 1130 PopStyleVar(); 1131 return ret; 1132 } 1133 #endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS 1134 1135 bool ImGui::Checkbox(const char* label, bool* v) 1136 { 1137 ImGuiWindow* window = GetCurrentWindow(); 1138 if (window->SkipItems) 1139 return false; 1140 1141 ImGuiContext& g = *GImGui; 1142 const ImGuiStyle& style = g.Style; 1143 const ImGuiID id = window->GetID(label); 1144 const ImVec2 label_size = CalcTextSize(label, NULL, true); 1145 1146 const float square_sz = GetFrameHeight(); 1147 const ImVec2 pos = window->DC.CursorPos; 1148 const ImRect total_bb(pos, pos + ImVec2(square_sz + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), label_size.y + style.FramePadding.y * 2.0f)); 1149 ItemSize(total_bb, style.FramePadding.y); 1150 const bool is_visible = ItemAdd(total_bb, id); 1151 const bool is_multi_select = (g.LastItemData.InFlags & ImGuiItemFlags_IsMultiSelect) != 0; 1152 if (!is_visible) 1153 if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(total_bb)) // Extra layer of "no logic clip" for box-select support 1154 { 1155 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0)); 1156 return false; 1157 } 1158 1159 // Range-Selection/Multi-selection support (header) 1160 bool checked = *v; 1161 if (is_multi_select) 1162 MultiSelectItemHeader(id, &checked, NULL); 1163 1164 bool hovered, held; 1165 bool pressed = ButtonBehavior(total_bb, id, &hovered, &held); 1166 1167 // Range-Selection/Multi-selection support (footer) 1168 if (is_multi_select) 1169 MultiSelectItemFooter(id, &checked, &pressed); 1170 else if (pressed) 1171 checked = !checked; 1172 1173 if (*v != checked) 1174 { 1175 *v = checked; 1176 pressed = true; // return value 1177 MarkItemEdited(id); 1178 } 1179 1180 const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz)); 1181 const bool mixed_value = (g.LastItemData.InFlags & ImGuiItemFlags_MixedValue) != 0; 1182 if (is_visible) 1183 { 1184 RenderNavHighlight(total_bb, id); 1185 RenderFrame(check_bb.Min, check_bb.Max, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), true, style.FrameRounding); 1186 ImU32 check_col = GetColorU32(ImGuiCol_CheckMark); 1187 if (mixed_value) 1188 { 1189 // Undocumented tristate/mixed/indeterminate checkbox (#2644) 1190 // This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all widgets (not just checkbox) 1191 ImVec2 pad(ImMax(1.0f, IM_TRUNC(square_sz / 3.6f)), ImMax(1.0f, IM_TRUNC(square_sz / 3.6f))); 1192 window->DrawList->AddRectFilled(check_bb.Min + pad, check_bb.Max - pad, check_col, style.FrameRounding); 1193 } 1194 else if (*v) 1195 { 1196 const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f)); 1197 RenderCheckMark(window->DrawList, check_bb.Min + ImVec2(pad, pad), check_col, square_sz - pad * 2.0f); 1198 } 1199 } 1200 const ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y); 1201 if (g.LogEnabled) 1202 LogRenderedText(&label_pos, mixed_value ? "[~]" : *v ? "[x]" : "[ ]"); 1203 if (is_visible && label_size.x > 0.0f) 1204 RenderText(label_pos, label); 1205 1206 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0)); 1207 return pressed; 1208 } 1209 1210 template<typename T> 1211 bool ImGui::CheckboxFlagsT(const char* label, T* flags, T flags_value) 1212 { 1213 bool all_on = (*flags & flags_value) == flags_value; 1214 bool any_on = (*flags & flags_value) != 0; 1215 bool pressed; 1216 if (!all_on && any_on) 1217 { 1218 ImGuiContext& g = *GImGui; 1219 g.NextItemData.ItemFlags |= ImGuiItemFlags_MixedValue; 1220 pressed = Checkbox(label, &all_on); 1221 } 1222 else 1223 { 1224 pressed = Checkbox(label, &all_on); 1225 1226 } 1227 if (pressed) 1228 { 1229 if (all_on) 1230 *flags |= flags_value; 1231 else 1232 *flags &= ~flags_value; 1233 } 1234 return pressed; 1235 } 1236 1237 bool ImGui::CheckboxFlags(const char* label, int* flags, int flags_value) 1238 { 1239 return CheckboxFlagsT(label, flags, flags_value); 1240 } 1241 1242 bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value) 1243 { 1244 return CheckboxFlagsT(label, flags, flags_value); 1245 } 1246 1247 bool ImGui::CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value) 1248 { 1249 return CheckboxFlagsT(label, flags, flags_value); 1250 } 1251 1252 bool ImGui::CheckboxFlags(const char* label, ImU64* flags, ImU64 flags_value) 1253 { 1254 return CheckboxFlagsT(label, flags, flags_value); 1255 } 1256 1257 bool ImGui::RadioButton(const char* label, bool active) 1258 { 1259 ImGuiWindow* window = GetCurrentWindow(); 1260 if (window->SkipItems) 1261 return false; 1262 1263 ImGuiContext& g = *GImGui; 1264 const ImGuiStyle& style = g.Style; 1265 const ImGuiID id = window->GetID(label); 1266 const ImVec2 label_size = CalcTextSize(label, NULL, true); 1267 1268 const float square_sz = GetFrameHeight(); 1269 const ImVec2 pos = window->DC.CursorPos; 1270 const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz)); 1271 const ImRect total_bb(pos, pos + ImVec2(square_sz + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), label_size.y + style.FramePadding.y * 2.0f)); 1272 ItemSize(total_bb, style.FramePadding.y); 1273 if (!ItemAdd(total_bb, id)) 1274 return false; 1275 1276 ImVec2 center = check_bb.GetCenter(); 1277 center.x = IM_ROUND(center.x); 1278 center.y = IM_ROUND(center.y); 1279 const float radius = (square_sz - 1.0f) * 0.5f; 1280 1281 bool hovered, held; 1282 bool pressed = ButtonBehavior(total_bb, id, &hovered, &held); 1283 if (pressed) 1284 MarkItemEdited(id); 1285 1286 RenderNavHighlight(total_bb, id); 1287 const int num_segment = window->DrawList->_CalcCircleAutoSegmentCount(radius); 1288 window->DrawList->AddCircleFilled(center, radius, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), num_segment); 1289 if (active) 1290 { 1291 const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f)); 1292 window->DrawList->AddCircleFilled(center, radius - pad, GetColorU32(ImGuiCol_CheckMark)); 1293 } 1294 1295 if (style.FrameBorderSize > 0.0f) 1296 { 1297 window->DrawList->AddCircle(center + ImVec2(1, 1), radius, GetColorU32(ImGuiCol_BorderShadow), num_segment, style.FrameBorderSize); 1298 window->DrawList->AddCircle(center, radius, GetColorU32(ImGuiCol_Border), num_segment, style.FrameBorderSize); 1299 } 1300 1301 ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y); 1302 if (g.LogEnabled) 1303 LogRenderedText(&label_pos, active ? "(x)" : "( )"); 1304 if (label_size.x > 0.0f) 1305 RenderText(label_pos, label); 1306 1307 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); 1308 return pressed; 1309 } 1310 1311 // FIXME: This would work nicely if it was a public template, e.g. 'template<T> RadioButton(const char* label, T* v, T v_button)', but I'm not sure how we would expose it.. 1312 bool ImGui::RadioButton(const char* label, int* v, int v_button) 1313 { 1314 const bool pressed = RadioButton(label, *v == v_button); 1315 if (pressed) 1316 *v = v_button; 1317 return pressed; 1318 } 1319 1320 // size_arg (for each axis) < 0.0f: align to end, 0.0f: auto, > 0.0f: specified size 1321 void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* overlay) 1322 { 1323 ImGuiWindow* window = GetCurrentWindow(); 1324 if (window->SkipItems) 1325 return; 1326 1327 ImGuiContext& g = *GImGui; 1328 const ImGuiStyle& style = g.Style; 1329 1330 ImVec2 pos = window->DC.CursorPos; 1331 ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), g.FontSize + style.FramePadding.y * 2.0f); 1332 ImRect bb(pos, pos + size); 1333 ItemSize(size, style.FramePadding.y); 1334 if (!ItemAdd(bb, 0)) 1335 return; 1336 1337 // Fraction < 0.0f will display an indeterminate progress bar animation 1338 // The value must be animated along with time, so e.g. passing '-1.0f * ImGui::GetTime()' as fraction works. 1339 const bool is_indeterminate = (fraction < 0.0f); 1340 if (!is_indeterminate) 1341 fraction = ImSaturate(fraction); 1342 1343 // Out of courtesy we accept a NaN fraction without crashing 1344 float fill_n0 = 0.0f; 1345 float fill_n1 = (fraction == fraction) ? fraction : 0.0f; 1346 1347 if (is_indeterminate) 1348 { 1349 const float fill_width_n = 0.2f; 1350 fill_n0 = ImFmod(-fraction, 1.0f) * (1.0f + fill_width_n) - fill_width_n; 1351 fill_n1 = ImSaturate(fill_n0 + fill_width_n); 1352 fill_n0 = ImSaturate(fill_n0); 1353 } 1354 1355 // Render 1356 RenderFrame(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); 1357 bb.Expand(ImVec2(-style.FrameBorderSize, -style.FrameBorderSize)); 1358 RenderRectFilledRangeH(window->DrawList, bb, GetColorU32(ImGuiCol_PlotHistogram), fill_n0, fill_n1, style.FrameRounding); 1359 1360 // Default displaying the fraction as percentage string, but user can override it 1361 // Don't display text for indeterminate bars by default 1362 char overlay_buf[32]; 1363 if (!is_indeterminate || overlay != NULL) 1364 { 1365 if (!overlay) 1366 { 1367 ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%.0f%%", fraction * 100 + 0.01f); 1368 overlay = overlay_buf; 1369 } 1370 1371 ImVec2 overlay_size = CalcTextSize(overlay, NULL); 1372 if (overlay_size.x > 0.0f) 1373 { 1374 float text_x = is_indeterminate ? (bb.Min.x + bb.Max.x - overlay_size.x) * 0.5f : ImLerp(bb.Min.x, bb.Max.x, fill_n1) + style.ItemSpacing.x; 1375 RenderTextClipped(ImVec2(ImClamp(text_x, bb.Min.x, bb.Max.x - overlay_size.x - style.ItemInnerSpacing.x), bb.Min.y), bb.Max, overlay, NULL, &overlay_size, ImVec2(0.0f, 0.5f), &bb); 1376 } 1377 } 1378 } 1379 1380 void ImGui::Bullet() 1381 { 1382 ImGuiWindow* window = GetCurrentWindow(); 1383 if (window->SkipItems) 1384 return; 1385 1386 ImGuiContext& g = *GImGui; 1387 const ImGuiStyle& style = g.Style; 1388 const float line_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y * 2), g.FontSize); 1389 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize, line_height)); 1390 ItemSize(bb); 1391 if (!ItemAdd(bb, 0)) 1392 { 1393 SameLine(0, style.FramePadding.x * 2); 1394 return; 1395 } 1396 1397 // Render and stay on same line 1398 ImU32 text_col = GetColorU32(ImGuiCol_Text); 1399 RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, line_height * 0.5f), text_col); 1400 SameLine(0, style.FramePadding.x * 2.0f); 1401 } 1402 1403 // This is provided as a convenience for being an often requested feature. 1404 // FIXME-STYLE: we delayed adding as there is a larger plan to revamp the styling system. 1405 // Because of this we currently don't provide many styling options for this widget 1406 // (e.g. hovered/active colors are automatically inferred from a single color). 1407 bool ImGui::TextLink(const char* label) 1408 { 1409 ImGuiWindow* window = GetCurrentWindow(); 1410 if (window->SkipItems) 1411 return false; 1412 1413 ImGuiContext& g = *GImGui; 1414 const ImGuiID id = window->GetID(label); 1415 const char* label_end = FindRenderedTextEnd(label); 1416 1417 ImVec2 pos = window->DC.CursorPos; 1418 ImVec2 size = CalcTextSize(label, label_end, true); 1419 ImRect bb(pos, pos + size); 1420 ItemSize(size, 0.0f); 1421 if (!ItemAdd(bb, id)) 1422 return false; 1423 1424 bool hovered, held; 1425 bool pressed = ButtonBehavior(bb, id, &hovered, &held); 1426 RenderNavHighlight(bb, id, ImGuiNavHighlightFlags_None); 1427 1428 ImVec4 text_colf = g.Style.Colors[ImGuiCol_TextLink]; 1429 ImVec4 line_colf = text_colf; 1430 { 1431 // FIXME-STYLE: Read comments above. This widget is NOT written in the same style as some earlier widgets, 1432 // as we are currently experimenting/planning a different styling system. 1433 float h, s, v; 1434 ColorConvertRGBtoHSV(text_colf.x, text_colf.y, text_colf.z, h, s, v); 1435 if (held || hovered) 1436 { 1437 v = ImSaturate(v + (held ? 0.4f : 0.3f)); 1438 h = ImFmod(h + 0.02f, 1.0f); 1439 } 1440 ColorConvertHSVtoRGB(h, s, v, text_colf.x, text_colf.y, text_colf.z); 1441 v = ImSaturate(v - 0.20f); 1442 ColorConvertHSVtoRGB(h, s, v, line_colf.x, line_colf.y, line_colf.z); 1443 } 1444 1445 float line_y = bb.Max.y + ImFloor(g.Font->Descent * g.FontScale * 0.20f); 1446 window->DrawList->AddLine(ImVec2(bb.Min.x, line_y), ImVec2(bb.Max.x, line_y), GetColorU32(line_colf)); // FIXME-TEXT: Underline mode. 1447 1448 PushStyleColor(ImGuiCol_Text, GetColorU32(text_colf)); 1449 RenderText(bb.Min, label, label_end); 1450 PopStyleColor(); 1451 1452 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); 1453 return pressed; 1454 } 1455 1456 void ImGui::TextLinkOpenURL(const char* label, const char* url) 1457 { 1458 ImGuiContext& g = *GImGui; 1459 if (url == NULL) 1460 url = label; 1461 if (TextLink(label)) 1462 if (g.IO.PlatformOpenInShellFn != NULL) 1463 g.IO.PlatformOpenInShellFn(&g, url); 1464 SetItemTooltip("%s", url); // It is more reassuring for user to _always_ display URL when we same as label 1465 if (BeginPopupContextItem()) 1466 { 1467 if (MenuItem(LocalizeGetMsg(ImGuiLocKey_CopyLink))) 1468 SetClipboardText(url); 1469 EndPopup(); 1470 } 1471 } 1472 1473 //------------------------------------------------------------------------- 1474 // [SECTION] Widgets: Low-level Layout helpers 1475 //------------------------------------------------------------------------- 1476 // - Spacing() 1477 // - Dummy() 1478 // - NewLine() 1479 // - AlignTextToFramePadding() 1480 // - SeparatorEx() [Internal] 1481 // - Separator() 1482 // - SplitterBehavior() [Internal] 1483 // - ShrinkWidths() [Internal] 1484 //------------------------------------------------------------------------- 1485 1486 void ImGui::Spacing() 1487 { 1488 ImGuiWindow* window = GetCurrentWindow(); 1489 if (window->SkipItems) 1490 return; 1491 ItemSize(ImVec2(0, 0)); 1492 } 1493 1494 void ImGui::Dummy(const ImVec2& size) 1495 { 1496 ImGuiWindow* window = GetCurrentWindow(); 1497 if (window->SkipItems) 1498 return; 1499 1500 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); 1501 ItemSize(size); 1502 ItemAdd(bb, 0); 1503 } 1504 1505 void ImGui::NewLine() 1506 { 1507 ImGuiWindow* window = GetCurrentWindow(); 1508 if (window->SkipItems) 1509 return; 1510 1511 ImGuiContext& g = *GImGui; 1512 const ImGuiLayoutType backup_layout_type = window->DC.LayoutType; 1513 window->DC.LayoutType = ImGuiLayoutType_Vertical; 1514 window->DC.IsSameLine = false; 1515 if (window->DC.CurrLineSize.y > 0.0f) // In the event that we are on a line with items that is smaller that FontSize high, we will preserve its height. 1516 ItemSize(ImVec2(0, 0)); 1517 else 1518 ItemSize(ImVec2(0.0f, g.FontSize)); 1519 window->DC.LayoutType = backup_layout_type; 1520 } 1521 1522 void ImGui::AlignTextToFramePadding() 1523 { 1524 ImGuiWindow* window = GetCurrentWindow(); 1525 if (window->SkipItems) 1526 return; 1527 1528 ImGuiContext& g = *GImGui; 1529 window->DC.CurrLineSize.y = ImMax(window->DC.CurrLineSize.y, g.FontSize + g.Style.FramePadding.y * 2); 1530 window->DC.CurrLineTextBaseOffset = ImMax(window->DC.CurrLineTextBaseOffset, g.Style.FramePadding.y); 1531 } 1532 1533 // Horizontal/vertical separating line 1534 // FIXME: Surprisingly, this seemingly trivial widget is a victim of many different legacy/tricky layout issues. 1535 // Note how thickness == 1.0f is handled specifically as not moving CursorPos by 'thickness', but other values are. 1536 void ImGui::SeparatorEx(ImGuiSeparatorFlags flags, float thickness) 1537 { 1538 ImGuiWindow* window = GetCurrentWindow(); 1539 if (window->SkipItems) 1540 return; 1541 1542 ImGuiContext& g = *GImGui; 1543 IM_ASSERT(ImIsPowerOfTwo(flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical))); // Check that only 1 option is selected 1544 IM_ASSERT(thickness > 0.0f); 1545 1546 if (flags & ImGuiSeparatorFlags_Vertical) 1547 { 1548 // Vertical separator, for menu bars (use current line height). 1549 float y1 = window->DC.CursorPos.y; 1550 float y2 = window->DC.CursorPos.y + window->DC.CurrLineSize.y; 1551 const ImRect bb(ImVec2(window->DC.CursorPos.x, y1), ImVec2(window->DC.CursorPos.x + thickness, y2)); 1552 ItemSize(ImVec2(thickness, 0.0f)); 1553 if (!ItemAdd(bb, 0)) 1554 return; 1555 1556 // Draw 1557 window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Separator)); 1558 if (g.LogEnabled) 1559 LogText(" |"); 1560 } 1561 else if (flags & ImGuiSeparatorFlags_Horizontal) 1562 { 1563 // Horizontal Separator 1564 float x1 = window->DC.CursorPos.x; 1565 float x2 = window->WorkRect.Max.x; 1566 1567 // Preserve legacy behavior inside Columns() 1568 // Before Tables API happened, we relied on Separator() to span all columns of a Columns() set. 1569 // We currently don't need to provide the same feature for tables because tables naturally have border features. 1570 ImGuiOldColumns* columns = (flags & ImGuiSeparatorFlags_SpanAllColumns) ? window->DC.CurrentColumns : NULL; 1571 if (columns) 1572 { 1573 x1 = window->Pos.x + window->DC.Indent.x; // Used to be Pos.x before 2023/10/03 1574 x2 = window->Pos.x + window->Size.x; 1575 PushColumnsBackground(); 1576 } 1577 1578 // We don't provide our width to the layout so that it doesn't get feed back into AutoFit 1579 // FIXME: This prevents ->CursorMaxPos based bounding box evaluation from working (e.g. TableEndCell) 1580 const float thickness_for_layout = (thickness == 1.0f) ? 0.0f : thickness; // FIXME: See 1.70/1.71 Separator() change: makes legacy 1-px separator not affect layout yet. Should change. 1581 const ImRect bb(ImVec2(x1, window->DC.CursorPos.y), ImVec2(x2, window->DC.CursorPos.y + thickness)); 1582 ItemSize(ImVec2(0.0f, thickness_for_layout)); 1583 1584 if (ItemAdd(bb, 0)) 1585 { 1586 // Draw 1587 window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Separator)); 1588 if (g.LogEnabled) 1589 LogRenderedText(&bb.Min, "--------------------------------\n"); 1590 1591 } 1592 if (columns) 1593 { 1594 PopColumnsBackground(); 1595 columns->LineMinY = window->DC.CursorPos.y; 1596 } 1597 } 1598 } 1599 1600 void ImGui::Separator() 1601 { 1602 ImGuiContext& g = *GImGui; 1603 ImGuiWindow* window = g.CurrentWindow; 1604 if (window->SkipItems) 1605 return; 1606 1607 // Those flags should eventually be configurable by the user 1608 // FIXME: We cannot g.Style.SeparatorTextBorderSize for thickness as it relates to SeparatorText() which is a decorated separator, not defaulting to 1.0f. 1609 ImGuiSeparatorFlags flags = (window->DC.LayoutType == ImGuiLayoutType_Horizontal) ? ImGuiSeparatorFlags_Vertical : ImGuiSeparatorFlags_Horizontal; 1610 1611 // Only applies to legacy Columns() api as they relied on Separator() a lot. 1612 if (window->DC.CurrentColumns) 1613 flags |= ImGuiSeparatorFlags_SpanAllColumns; 1614 1615 SeparatorEx(flags, 1.0f); 1616 } 1617 1618 void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end, float extra_w) 1619 { 1620 ImGuiContext& g = *GImGui; 1621 ImGuiWindow* window = g.CurrentWindow; 1622 ImGuiStyle& style = g.Style; 1623 1624 const ImVec2 label_size = CalcTextSize(label, label_end, false); 1625 const ImVec2 pos = window->DC.CursorPos; 1626 const ImVec2 padding = style.SeparatorTextPadding; 1627 1628 const float separator_thickness = style.SeparatorTextBorderSize; 1629 const ImVec2 min_size(label_size.x + extra_w + padding.x * 2.0f, ImMax(label_size.y + padding.y * 2.0f, separator_thickness)); 1630 const ImRect bb(pos, ImVec2(window->WorkRect.Max.x, pos.y + min_size.y)); 1631 const float text_baseline_y = ImTrunc((bb.GetHeight() - label_size.y) * style.SeparatorTextAlign.y + 0.99999f); //ImMax(padding.y, ImFloor((style.SeparatorTextSize - label_size.y) * 0.5f)); 1632 ItemSize(min_size, text_baseline_y); 1633 if (!ItemAdd(bb, id)) 1634 return; 1635 1636 const float sep1_x1 = pos.x; 1637 const float sep2_x2 = bb.Max.x; 1638 const float seps_y = ImTrunc((bb.Min.y + bb.Max.y) * 0.5f + 0.99999f); 1639 1640 const float label_avail_w = ImMax(0.0f, sep2_x2 - sep1_x1 - padding.x * 2.0f); 1641 const ImVec2 label_pos(pos.x + padding.x + ImMax(0.0f, (label_avail_w - label_size.x - extra_w) * style.SeparatorTextAlign.x), pos.y + text_baseline_y); // FIXME-ALIGN 1642 1643 // This allows using SameLine() to position something in the 'extra_w' 1644 window->DC.CursorPosPrevLine.x = label_pos.x + label_size.x; 1645 1646 const ImU32 separator_col = GetColorU32(ImGuiCol_Separator); 1647 if (label_size.x > 0.0f) 1648 { 1649 const float sep1_x2 = label_pos.x - style.ItemSpacing.x; 1650 const float sep2_x1 = label_pos.x + label_size.x + extra_w + style.ItemSpacing.x; 1651 if (sep1_x2 > sep1_x1 && separator_thickness > 0.0f) 1652 window->DrawList->AddLine(ImVec2(sep1_x1, seps_y), ImVec2(sep1_x2, seps_y), separator_col, separator_thickness); 1653 if (sep2_x2 > sep2_x1 && separator_thickness > 0.0f) 1654 window->DrawList->AddLine(ImVec2(sep2_x1, seps_y), ImVec2(sep2_x2, seps_y), separator_col, separator_thickness); 1655 if (g.LogEnabled) 1656 LogSetNextTextDecoration("---", NULL); 1657 RenderTextEllipsis(window->DrawList, label_pos, ImVec2(bb.Max.x, bb.Max.y + style.ItemSpacing.y), bb.Max.x, bb.Max.x, label, label_end, &label_size); 1658 } 1659 else 1660 { 1661 if (g.LogEnabled) 1662 LogText("---"); 1663 if (separator_thickness > 0.0f) 1664 window->DrawList->AddLine(ImVec2(sep1_x1, seps_y), ImVec2(sep2_x2, seps_y), separator_col, separator_thickness); 1665 } 1666 } 1667 1668 void ImGui::SeparatorText(const char* label) 1669 { 1670 ImGuiWindow* window = GetCurrentWindow(); 1671 if (window->SkipItems) 1672 return; 1673 1674 // The SeparatorText() vs SeparatorTextEx() distinction is designed to be considerate that we may want: 1675 // - allow separator-text to be draggable items (would require a stable ID + a noticeable highlight) 1676 // - this high-level entry point to allow formatting? (which in turns may require ID separate from formatted string) 1677 // - because of this we probably can't turn 'const char* label' into 'const char* fmt, ...' 1678 // Otherwise, we can decide that users wanting to drag this would layout a dedicated drag-item, 1679 // and then we can turn this into a format function. 1680 SeparatorTextEx(0, label, FindRenderedTextEnd(label), 0.0f); 1681 } 1682 1683 // Using 'hover_visibility_delay' allows us to hide the highlight and mouse cursor for a short time, which can be convenient to reduce visual noise. 1684 bool ImGui::SplitterBehavior(const ImRect& bb, ImGuiID id, ImGuiAxis axis, float* size1, float* size2, float min_size1, float min_size2, float hover_extend, float hover_visibility_delay, ImU32 bg_col) 1685 { 1686 ImGuiContext& g = *GImGui; 1687 ImGuiWindow* window = g.CurrentWindow; 1688 1689 if (!ItemAdd(bb, id, NULL, ImGuiItemFlags_NoNav)) 1690 return false; 1691 1692 // FIXME: AFAIK the only leftover reason for passing ImGuiButtonFlags_AllowOverlap here is 1693 // to allow caller of SplitterBehavior() to call SetItemAllowOverlap() after the item. 1694 // Nowadays we would instead want to use SetNextItemAllowOverlap() before the item. 1695 ImGuiButtonFlags button_flags = ImGuiButtonFlags_FlattenChildren; 1696 #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS 1697 button_flags |= ImGuiButtonFlags_AllowOverlap; 1698 #endif 1699 1700 bool hovered, held; 1701 ImRect bb_interact = bb; 1702 bb_interact.Expand(axis == ImGuiAxis_Y ? ImVec2(0.0f, hover_extend) : ImVec2(hover_extend, 0.0f)); 1703 ButtonBehavior(bb_interact, id, &hovered, &held, button_flags); 1704 if (hovered) 1705 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; // for IsItemHovered(), because bb_interact is larger than bb 1706 1707 if (held || (hovered && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= hover_visibility_delay)) 1708 SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW); 1709 1710 ImRect bb_render = bb; 1711 if (held) 1712 { 1713 float mouse_delta = (g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min)[axis]; 1714 1715 // Minimum pane size 1716 float size_1_maximum_delta = ImMax(0.0f, *size1 - min_size1); 1717 float size_2_maximum_delta = ImMax(0.0f, *size2 - min_size2); 1718 if (mouse_delta < -size_1_maximum_delta) 1719 mouse_delta = -size_1_maximum_delta; 1720 if (mouse_delta > size_2_maximum_delta) 1721 mouse_delta = size_2_maximum_delta; 1722 1723 // Apply resize 1724 if (mouse_delta != 0.0f) 1725 { 1726 *size1 = ImMax(*size1 + mouse_delta, min_size1); 1727 *size2 = ImMax(*size2 - mouse_delta, min_size2); 1728 bb_render.Translate((axis == ImGuiAxis_X) ? ImVec2(mouse_delta, 0.0f) : ImVec2(0.0f, mouse_delta)); 1729 MarkItemEdited(id); 1730 } 1731 } 1732 1733 // Render at new position 1734 if (bg_col & IM_COL32_A_MASK) 1735 window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, bg_col, 0.0f); 1736 const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : (hovered && g.HoveredIdTimer >= hover_visibility_delay) ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator); 1737 window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, col, 0.0f); 1738 1739 return held; 1740 } 1741 1742 static int IMGUI_CDECL ShrinkWidthItemComparer(const void* lhs, const void* rhs) 1743 { 1744 const ImGuiShrinkWidthItem* a = (const ImGuiShrinkWidthItem*)lhs; 1745 const ImGuiShrinkWidthItem* b = (const ImGuiShrinkWidthItem*)rhs; 1746 if (int d = (int)(b->Width - a->Width)) 1747 return d; 1748 return (b->Index - a->Index); 1749 } 1750 1751 // Shrink excess width from a set of item, by removing width from the larger items first. 1752 // Set items Width to -1.0f to disable shrinking this item. 1753 void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess) 1754 { 1755 if (count == 1) 1756 { 1757 if (items[0].Width >= 0.0f) 1758 items[0].Width = ImMax(items[0].Width - width_excess, 1.0f); 1759 return; 1760 } 1761 ImQsort(items, (size_t)count, sizeof(ImGuiShrinkWidthItem), ShrinkWidthItemComparer); 1762 int count_same_width = 1; 1763 while (width_excess > 0.0f && count_same_width < count) 1764 { 1765 while (count_same_width < count && items[0].Width <= items[count_same_width].Width) 1766 count_same_width++; 1767 float max_width_to_remove_per_item = (count_same_width < count && items[count_same_width].Width >= 0.0f) ? (items[0].Width - items[count_same_width].Width) : (items[0].Width - 1.0f); 1768 if (max_width_to_remove_per_item <= 0.0f) 1769 break; 1770 float width_to_remove_per_item = ImMin(width_excess / count_same_width, max_width_to_remove_per_item); 1771 for (int item_n = 0; item_n < count_same_width; item_n++) 1772 items[item_n].Width -= width_to_remove_per_item; 1773 width_excess -= width_to_remove_per_item * count_same_width; 1774 } 1775 1776 // Round width and redistribute remainder 1777 // Ensure that e.g. the right-most tab of a shrunk tab-bar always reaches exactly at the same distance from the right-most edge of the tab bar separator. 1778 width_excess = 0.0f; 1779 for (int n = 0; n < count; n++) 1780 { 1781 float width_rounded = ImTrunc(items[n].Width); 1782 width_excess += items[n].Width - width_rounded; 1783 items[n].Width = width_rounded; 1784 } 1785 while (width_excess > 0.0f) 1786 for (int n = 0; n < count && width_excess > 0.0f; n++) 1787 { 1788 float width_to_add = ImMin(items[n].InitialWidth - items[n].Width, 1.0f); 1789 items[n].Width += width_to_add; 1790 width_excess -= width_to_add; 1791 } 1792 } 1793 1794 //------------------------------------------------------------------------- 1795 // [SECTION] Widgets: ComboBox 1796 //------------------------------------------------------------------------- 1797 // - CalcMaxPopupHeightFromItemCount() [Internal] 1798 // - BeginCombo() 1799 // - BeginComboPopup() [Internal] 1800 // - EndCombo() 1801 // - BeginComboPreview() [Internal] 1802 // - EndComboPreview() [Internal] 1803 // - Combo() 1804 //------------------------------------------------------------------------- 1805 1806 static float CalcMaxPopupHeightFromItemCount(int items_count) 1807 { 1808 ImGuiContext& g = *GImGui; 1809 if (items_count <= 0) 1810 return FLT_MAX; 1811 return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2); 1812 } 1813 1814 bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags) 1815 { 1816 ImGuiContext& g = *GImGui; 1817 ImGuiWindow* window = GetCurrentWindow(); 1818 1819 ImGuiNextWindowDataFlags backup_next_window_data_flags = g.NextWindowData.Flags; 1820 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values 1821 if (window->SkipItems) 1822 return false; 1823 1824 const ImGuiStyle& style = g.Style; 1825 const ImGuiID id = window->GetID(label); 1826 IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together 1827 if (flags & ImGuiComboFlags_WidthFitPreview) 1828 IM_ASSERT((flags & (ImGuiComboFlags_NoPreview | (ImGuiComboFlags)ImGuiComboFlags_CustomPreview)) == 0); 1829 1830 const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight(); 1831 const ImVec2 label_size = CalcTextSize(label, NULL, true); 1832 const float preview_width = ((flags & ImGuiComboFlags_WidthFitPreview) && (preview_value != NULL)) ? CalcTextSize(preview_value, NULL, true).x : 0.0f; 1833 const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : ((flags & ImGuiComboFlags_WidthFitPreview) ? (arrow_size + preview_width + style.FramePadding.x * 2.0f) : CalcItemWidth()); 1834 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); 1835 const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); 1836 ItemSize(total_bb, style.FramePadding.y); 1837 if (!ItemAdd(total_bb, id, &bb)) 1838 return false; 1839 1840 // Open on click 1841 bool hovered, held; 1842 bool pressed = ButtonBehavior(bb, id, &hovered, &held); 1843 const ImGuiID popup_id = ImHashStr("##ComboPopup", 0, id); 1844 bool popup_open = IsPopupOpen(popup_id, ImGuiPopupFlags_None); 1845 if (pressed && !popup_open) 1846 { 1847 OpenPopupEx(popup_id, ImGuiPopupFlags_None); 1848 popup_open = true; 1849 } 1850 1851 // Render shape 1852 const ImU32 frame_col = GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); 1853 const float value_x2 = ImMax(bb.Min.x, bb.Max.x - arrow_size); 1854 RenderNavHighlight(bb, id); 1855 if (!(flags & ImGuiComboFlags_NoPreview)) 1856 window->DrawList->AddRectFilled(bb.Min, ImVec2(value_x2, bb.Max.y), frame_col, style.FrameRounding, (flags & ImGuiComboFlags_NoArrowButton) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersLeft); 1857 if (!(flags & ImGuiComboFlags_NoArrowButton)) 1858 { 1859 ImU32 bg_col = GetColorU32((popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button); 1860 ImU32 text_col = GetColorU32(ImGuiCol_Text); 1861 window->DrawList->AddRectFilled(ImVec2(value_x2, bb.Min.y), bb.Max, bg_col, style.FrameRounding, (w <= arrow_size) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersRight); 1862 if (value_x2 + arrow_size - style.FramePadding.x <= bb.Max.x) 1863 RenderArrow(window->DrawList, ImVec2(value_x2 + style.FramePadding.y, bb.Min.y + style.FramePadding.y), text_col, ImGuiDir_Down, 1.0f); 1864 } 1865 RenderFrameBorder(bb.Min, bb.Max, style.FrameRounding); 1866 1867 // Custom preview 1868 if (flags & ImGuiComboFlags_CustomPreview) 1869 { 1870 g.ComboPreviewData.PreviewRect = ImRect(bb.Min.x, bb.Min.y, value_x2, bb.Max.y); 1871 IM_ASSERT(preview_value == NULL || preview_value[0] == 0); 1872 preview_value = NULL; 1873 } 1874 1875 // Render preview and label 1876 if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview)) 1877 { 1878 if (g.LogEnabled) 1879 LogSetNextTextDecoration("{", "}"); 1880 RenderTextClipped(bb.Min + style.FramePadding, ImVec2(value_x2, bb.Max.y), preview_value, NULL, NULL); 1881 } 1882 if (label_size.x > 0) 1883 RenderText(ImVec2(bb.Max.x + style.ItemInnerSpacing.x, bb.Min.y + style.FramePadding.y), label); 1884 1885 if (!popup_open) 1886 return false; 1887 1888 g.NextWindowData.Flags = backup_next_window_data_flags; 1889 return BeginComboPopup(popup_id, bb, flags); 1890 } 1891 1892 bool ImGui::BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags flags) 1893 { 1894 ImGuiContext& g = *GImGui; 1895 if (!IsPopupOpen(popup_id, ImGuiPopupFlags_None)) 1896 { 1897 g.NextWindowData.ClearFlags(); 1898 return false; 1899 } 1900 1901 // Set popup size 1902 float w = bb.GetWidth(); 1903 if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint) 1904 { 1905 g.NextWindowData.SizeConstraintRect.Min.x = ImMax(g.NextWindowData.SizeConstraintRect.Min.x, w); 1906 } 1907 else 1908 { 1909 if ((flags & ImGuiComboFlags_HeightMask_) == 0) 1910 flags |= ImGuiComboFlags_HeightRegular; 1911 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one 1912 int popup_max_height_in_items = -1; 1913 if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8; 1914 else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4; 1915 else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20; 1916 ImVec2 constraint_min(0.0f, 0.0f), constraint_max(FLT_MAX, FLT_MAX); 1917 if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.x <= 0.0f) // Don't apply constraints if user specified a size 1918 constraint_min.x = w; 1919 if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.y <= 0.0f) 1920 constraint_max.y = CalcMaxPopupHeightFromItemCount(popup_max_height_in_items); 1921 SetNextWindowSizeConstraints(constraint_min, constraint_max); 1922 } 1923 1924 // This is essentially a specialized version of BeginPopupEx() 1925 char name[16]; 1926 ImFormatString(name, IM_ARRAYSIZE(name), "##Combo_%02d", g.BeginComboDepth); // Recycle windows based on depth 1927 1928 // Set position given a custom constraint (peak into expected window size so we can position it) 1929 // FIXME: This might be easier to express with an hypothetical SetNextWindowPosConstraints() function? 1930 // FIXME: This might be moved to Begin() or at least around the same spot where Tooltips and other Popups are calling FindBestWindowPosForPopupEx()? 1931 if (ImGuiWindow* popup_window = FindWindowByName(name)) 1932 if (popup_window->WasActive) 1933 { 1934 // Always override 'AutoPosLastDirection' to not leave a chance for a past value to affect us. 1935 ImVec2 size_expected = CalcWindowNextAutoFitSize(popup_window); 1936 popup_window->AutoPosLastDirection = (flags & ImGuiComboFlags_PopupAlignLeft) ? ImGuiDir_Left : ImGuiDir_Down; // Left = "Below, Toward Left", Down = "Below, Toward Right (default)" 1937 ImRect r_outer = GetPopupAllowedExtentRect(popup_window); 1938 ImVec2 pos = FindBestWindowPosForPopupEx(bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, r_outer, bb, ImGuiPopupPositionPolicy_ComboBox); 1939 SetNextWindowPos(pos); 1940 } 1941 1942 // We don't use BeginPopupEx() solely because we have a custom name string, which we could make an argument to BeginPopupEx() 1943 ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove; 1944 PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(g.Style.FramePadding.x, g.Style.WindowPadding.y)); // Horizontally align ourselves with the framed text 1945 bool ret = Begin(name, NULL, window_flags); 1946 PopStyleVar(); 1947 if (!ret) 1948 { 1949 EndPopup(); 1950 IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above 1951 return false; 1952 } 1953 g.BeginComboDepth++; 1954 return true; 1955 } 1956 1957 void ImGui::EndCombo() 1958 { 1959 ImGuiContext& g = *GImGui; 1960 EndPopup(); 1961 g.BeginComboDepth--; 1962 } 1963 1964 // Call directly after the BeginCombo/EndCombo block. The preview is designed to only host non-interactive elements 1965 // (Experimental, see GitHub issues: #1658, #4168) 1966 bool ImGui::BeginComboPreview() 1967 { 1968 ImGuiContext& g = *GImGui; 1969 ImGuiWindow* window = g.CurrentWindow; 1970 ImGuiComboPreviewData* preview_data = &g.ComboPreviewData; 1971 1972 if (window->SkipItems || !(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)) 1973 return false; 1974 IM_ASSERT(g.LastItemData.Rect.Min.x == preview_data->PreviewRect.Min.x && g.LastItemData.Rect.Min.y == preview_data->PreviewRect.Min.y); // Didn't call after BeginCombo/EndCombo block or forgot to pass ImGuiComboFlags_CustomPreview flag? 1975 if (!window->ClipRect.Overlaps(preview_data->PreviewRect)) // Narrower test (optional) 1976 return false; 1977 1978 // FIXME: This could be contained in a PushWorkRect() api 1979 preview_data->BackupCursorPos = window->DC.CursorPos; 1980 preview_data->BackupCursorMaxPos = window->DC.CursorMaxPos; 1981 preview_data->BackupCursorPosPrevLine = window->DC.CursorPosPrevLine; 1982 preview_data->BackupPrevLineTextBaseOffset = window->DC.PrevLineTextBaseOffset; 1983 preview_data->BackupLayout = window->DC.LayoutType; 1984 window->DC.CursorPos = preview_data->PreviewRect.Min + g.Style.FramePadding; 1985 window->DC.CursorMaxPos = window->DC.CursorPos; 1986 window->DC.LayoutType = ImGuiLayoutType_Horizontal; 1987 window->DC.IsSameLine = false; 1988 PushClipRect(preview_data->PreviewRect.Min, preview_data->PreviewRect.Max, true); 1989 1990 return true; 1991 } 1992 1993 void ImGui::EndComboPreview() 1994 { 1995 ImGuiContext& g = *GImGui; 1996 ImGuiWindow* window = g.CurrentWindow; 1997 ImGuiComboPreviewData* preview_data = &g.ComboPreviewData; 1998 1999 // FIXME: Using CursorMaxPos approximation instead of correct AABB which we will store in ImDrawCmd in the future 2000 ImDrawList* draw_list = window->DrawList; 2001 if (window->DC.CursorMaxPos.x < preview_data->PreviewRect.Max.x && window->DC.CursorMaxPos.y < preview_data->PreviewRect.Max.y) 2002 if (draw_list->CmdBuffer.Size > 1) // Unlikely case that the PushClipRect() didn't create a command 2003 { 2004 draw_list->_CmdHeader.ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 2].ClipRect; 2005 draw_list->_TryMergeDrawCmds(); 2006 } 2007 PopClipRect(); 2008 window->DC.CursorPos = preview_data->BackupCursorPos; 2009 window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, preview_data->BackupCursorMaxPos); 2010 window->DC.CursorPosPrevLine = preview_data->BackupCursorPosPrevLine; 2011 window->DC.PrevLineTextBaseOffset = preview_data->BackupPrevLineTextBaseOffset; 2012 window->DC.LayoutType = preview_data->BackupLayout; 2013 window->DC.IsSameLine = false; 2014 preview_data->PreviewRect = ImRect(); 2015 } 2016 2017 // Getter for the old Combo() API: const char*[] 2018 static const char* Items_ArrayGetter(void* data, int idx) 2019 { 2020 const char* const* items = (const char* const*)data; 2021 return items[idx]; 2022 } 2023 2024 // Getter for the old Combo() API: "item1\0item2\0item3\0" 2025 static const char* Items_SingleStringGetter(void* data, int idx) 2026 { 2027 const char* items_separated_by_zeros = (const char*)data; 2028 int items_count = 0; 2029 const char* p = items_separated_by_zeros; 2030 while (*p) 2031 { 2032 if (idx == items_count) 2033 break; 2034 p += strlen(p) + 1; 2035 items_count++; 2036 } 2037 return *p ? p : NULL; 2038 } 2039 2040 // Old API, prefer using BeginCombo() nowadays if you can. 2041 bool ImGui::Combo(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), void* user_data, int items_count, int popup_max_height_in_items) 2042 { 2043 ImGuiContext& g = *GImGui; 2044 2045 // Call the getter to obtain the preview string which is a parameter to BeginCombo() 2046 const char* preview_value = NULL; 2047 if (*current_item >= 0 && *current_item < items_count) 2048 preview_value = getter(user_data, *current_item); 2049 2050 // The old Combo() API exposed "popup_max_height_in_items". The new more general BeginCombo() API doesn't have/need it, but we emulate it here. 2051 if (popup_max_height_in_items != -1 && !(g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint)) 2052 SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items))); 2053 2054 if (!BeginCombo(label, preview_value, ImGuiComboFlags_None)) 2055 return false; 2056 2057 // Display items 2058 bool value_changed = false; 2059 ImGuiListClipper clipper; 2060 clipper.Begin(items_count); 2061 clipper.IncludeItemByIndex(*current_item); 2062 while (clipper.Step()) 2063 for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) 2064 { 2065 const char* item_text = getter(user_data, i); 2066 if (item_text == NULL) 2067 item_text = "*Unknown item*"; 2068 2069 PushID(i); 2070 const bool item_selected = (i == *current_item); 2071 if (Selectable(item_text, item_selected) && *current_item != i) 2072 { 2073 value_changed = true; 2074 *current_item = i; 2075 } 2076 if (item_selected) 2077 SetItemDefaultFocus(); 2078 PopID(); 2079 } 2080 2081 EndCombo(); 2082 if (value_changed) 2083 MarkItemEdited(g.LastItemData.ID); 2084 2085 return value_changed; 2086 } 2087 2088 // Combo box helper allowing to pass an array of strings. 2089 bool ImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items) 2090 { 2091 const bool value_changed = Combo(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_in_items); 2092 return value_changed; 2093 } 2094 2095 // Combo box helper allowing to pass all items in a single string literal holding multiple zero-terminated items "item1\0item2\0" 2096 bool ImGui::Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int height_in_items) 2097 { 2098 int items_count = 0; 2099 const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open 2100 while (*p) 2101 { 2102 p += strlen(p) + 1; 2103 items_count++; 2104 } 2105 bool value_changed = Combo(label, current_item, Items_SingleStringGetter, (void*)items_separated_by_zeros, items_count, height_in_items); 2106 return value_changed; 2107 } 2108 2109 #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS 2110 2111 struct ImGuiGetNameFromIndexOldToNewCallbackData { void* UserData; bool (*OldCallback)(void*, int, const char**); }; 2112 static const char* ImGuiGetNameFromIndexOldToNewCallback(void* user_data, int idx) 2113 { 2114 ImGuiGetNameFromIndexOldToNewCallbackData* data = (ImGuiGetNameFromIndexOldToNewCallbackData*)user_data; 2115 const char* s = NULL; 2116 data->OldCallback(data->UserData, idx, &s); 2117 return s; 2118 } 2119 2120 bool ImGui::ListBox(const char* label, int* current_item, bool (*old_getter)(void*, int, const char**), void* user_data, int items_count, int height_in_items) 2121 { 2122 ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { user_data, old_getter }; 2123 return ListBox(label, current_item, ImGuiGetNameFromIndexOldToNewCallback, &old_to_new_data, items_count, height_in_items); 2124 } 2125 bool ImGui::Combo(const char* label, int* current_item, bool (*old_getter)(void*, int, const char**), void* user_data, int items_count, int popup_max_height_in_items) 2126 { 2127 ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { user_data, old_getter }; 2128 return Combo(label, current_item, ImGuiGetNameFromIndexOldToNewCallback, &old_to_new_data, items_count, popup_max_height_in_items); 2129 } 2130 2131 #endif 2132 2133 //------------------------------------------------------------------------- 2134 // [SECTION] Data Type and Data Formatting Helpers [Internal] 2135 //------------------------------------------------------------------------- 2136 // - DataTypeGetInfo() 2137 // - DataTypeFormatString() 2138 // - DataTypeApplyOp() 2139 // - DataTypeApplyFromText() 2140 // - DataTypeCompare() 2141 // - DataTypeClamp() 2142 // - GetMinimumStepAtDecimalPrecision 2143 // - RoundScalarWithFormat<>() 2144 //------------------------------------------------------------------------- 2145 2146 static const ImGuiDataTypeInfo GDataTypeInfo[] = 2147 { 2148 { sizeof(char), "S8", "%d", "%d" }, // ImGuiDataType_S8 2149 { sizeof(unsigned char), "U8", "%u", "%u" }, 2150 { sizeof(short), "S16", "%d", "%d" }, // ImGuiDataType_S16 2151 { sizeof(unsigned short), "U16", "%u", "%u" }, 2152 { sizeof(int), "S32", "%d", "%d" }, // ImGuiDataType_S32 2153 { sizeof(unsigned int), "U32", "%u", "%u" }, 2154 #ifdef _MSC_VER 2155 { sizeof(ImS64), "S64", "%I64d","%I64d" }, // ImGuiDataType_S64 2156 { sizeof(ImU64), "U64", "%I64u","%I64u" }, 2157 #else 2158 { sizeof(ImS64), "S64", "%lld", "%lld" }, // ImGuiDataType_S64 2159 { sizeof(ImU64), "U64", "%llu", "%llu" }, 2160 #endif 2161 { sizeof(float), "float", "%.3f","%f" }, // ImGuiDataType_Float (float are promoted to double in va_arg) 2162 { sizeof(double), "double","%f", "%lf" }, // ImGuiDataType_Double 2163 { sizeof(bool), "bool", "%d", "%d" }, // ImGuiDataType_Bool 2164 }; 2165 IM_STATIC_ASSERT(IM_ARRAYSIZE(GDataTypeInfo) == ImGuiDataType_COUNT); 2166 2167 const ImGuiDataTypeInfo* ImGui::DataTypeGetInfo(ImGuiDataType data_type) 2168 { 2169 IM_ASSERT(data_type >= 0 && data_type < ImGuiDataType_COUNT); 2170 return &GDataTypeInfo[data_type]; 2171 } 2172 2173 int ImGui::DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* p_data, const char* format) 2174 { 2175 // Signedness doesn't matter when pushing integer arguments 2176 if (data_type == ImGuiDataType_S32 || data_type == ImGuiDataType_U32) 2177 return ImFormatString(buf, buf_size, format, *(const ImU32*)p_data); 2178 if (data_type == ImGuiDataType_S64 || data_type == ImGuiDataType_U64) 2179 return ImFormatString(buf, buf_size, format, *(const ImU64*)p_data); 2180 if (data_type == ImGuiDataType_Float) 2181 return ImFormatString(buf, buf_size, format, *(const float*)p_data); 2182 if (data_type == ImGuiDataType_Double) 2183 return ImFormatString(buf, buf_size, format, *(const double*)p_data); 2184 if (data_type == ImGuiDataType_S8) 2185 return ImFormatString(buf, buf_size, format, *(const ImS8*)p_data); 2186 if (data_type == ImGuiDataType_U8) 2187 return ImFormatString(buf, buf_size, format, *(const ImU8*)p_data); 2188 if (data_type == ImGuiDataType_S16) 2189 return ImFormatString(buf, buf_size, format, *(const ImS16*)p_data); 2190 if (data_type == ImGuiDataType_U16) 2191 return ImFormatString(buf, buf_size, format, *(const ImU16*)p_data); 2192 IM_ASSERT(0); 2193 return 0; 2194 } 2195 2196 void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg1, const void* arg2) 2197 { 2198 IM_ASSERT(op == '+' || op == '-'); 2199 switch (data_type) 2200 { 2201 case ImGuiDataType_S8: 2202 if (op == '+') { *(ImS8*)output = ImAddClampOverflow(*(const ImS8*)arg1, *(const ImS8*)arg2, IM_S8_MIN, IM_S8_MAX); } 2203 if (op == '-') { *(ImS8*)output = ImSubClampOverflow(*(const ImS8*)arg1, *(const ImS8*)arg2, IM_S8_MIN, IM_S8_MAX); } 2204 return; 2205 case ImGuiDataType_U8: 2206 if (op == '+') { *(ImU8*)output = ImAddClampOverflow(*(const ImU8*)arg1, *(const ImU8*)arg2, IM_U8_MIN, IM_U8_MAX); } 2207 if (op == '-') { *(ImU8*)output = ImSubClampOverflow(*(const ImU8*)arg1, *(const ImU8*)arg2, IM_U8_MIN, IM_U8_MAX); } 2208 return; 2209 case ImGuiDataType_S16: 2210 if (op == '+') { *(ImS16*)output = ImAddClampOverflow(*(const ImS16*)arg1, *(const ImS16*)arg2, IM_S16_MIN, IM_S16_MAX); } 2211 if (op == '-') { *(ImS16*)output = ImSubClampOverflow(*(const ImS16*)arg1, *(const ImS16*)arg2, IM_S16_MIN, IM_S16_MAX); } 2212 return; 2213 case ImGuiDataType_U16: 2214 if (op == '+') { *(ImU16*)output = ImAddClampOverflow(*(const ImU16*)arg1, *(const ImU16*)arg2, IM_U16_MIN, IM_U16_MAX); } 2215 if (op == '-') { *(ImU16*)output = ImSubClampOverflow(*(const ImU16*)arg1, *(const ImU16*)arg2, IM_U16_MIN, IM_U16_MAX); } 2216 return; 2217 case ImGuiDataType_S32: 2218 if (op == '+') { *(ImS32*)output = ImAddClampOverflow(*(const ImS32*)arg1, *(const ImS32*)arg2, IM_S32_MIN, IM_S32_MAX); } 2219 if (op == '-') { *(ImS32*)output = ImSubClampOverflow(*(const ImS32*)arg1, *(const ImS32*)arg2, IM_S32_MIN, IM_S32_MAX); } 2220 return; 2221 case ImGuiDataType_U32: 2222 if (op == '+') { *(ImU32*)output = ImAddClampOverflow(*(const ImU32*)arg1, *(const ImU32*)arg2, IM_U32_MIN, IM_U32_MAX); } 2223 if (op == '-') { *(ImU32*)output = ImSubClampOverflow(*(const ImU32*)arg1, *(const ImU32*)arg2, IM_U32_MIN, IM_U32_MAX); } 2224 return; 2225 case ImGuiDataType_S64: 2226 if (op == '+') { *(ImS64*)output = ImAddClampOverflow(*(const ImS64*)arg1, *(const ImS64*)arg2, IM_S64_MIN, IM_S64_MAX); } 2227 if (op == '-') { *(ImS64*)output = ImSubClampOverflow(*(const ImS64*)arg1, *(const ImS64*)arg2, IM_S64_MIN, IM_S64_MAX); } 2228 return; 2229 case ImGuiDataType_U64: 2230 if (op == '+') { *(ImU64*)output = ImAddClampOverflow(*(const ImU64*)arg1, *(const ImU64*)arg2, IM_U64_MIN, IM_U64_MAX); } 2231 if (op == '-') { *(ImU64*)output = ImSubClampOverflow(*(const ImU64*)arg1, *(const ImU64*)arg2, IM_U64_MIN, IM_U64_MAX); } 2232 return; 2233 case ImGuiDataType_Float: 2234 if (op == '+') { *(float*)output = *(const float*)arg1 + *(const float*)arg2; } 2235 if (op == '-') { *(float*)output = *(const float*)arg1 - *(const float*)arg2; } 2236 return; 2237 case ImGuiDataType_Double: 2238 if (op == '+') { *(double*)output = *(const double*)arg1 + *(const double*)arg2; } 2239 if (op == '-') { *(double*)output = *(const double*)arg1 - *(const double*)arg2; } 2240 return; 2241 case ImGuiDataType_COUNT: break; 2242 } 2243 IM_ASSERT(0); 2244 } 2245 2246 // User can input math operators (e.g. +100) to edit a numerical values. 2247 // NB: This is _not_ a full expression evaluator. We should probably add one and replace this dumb mess.. 2248 bool ImGui::DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format, void* p_data_when_empty) 2249 { 2250 // Copy the value in an opaque buffer so we can compare at the end of the function if it changed at all. 2251 const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type); 2252 ImGuiDataTypeStorage data_backup; 2253 memcpy(&data_backup, p_data, type_info->Size); 2254 2255 while (ImCharIsBlankA(*buf)) 2256 buf++; 2257 if (!buf[0]) 2258 { 2259 if (p_data_when_empty != NULL) 2260 { 2261 memcpy(p_data, p_data_when_empty, type_info->Size); 2262 return memcmp(&data_backup, p_data, type_info->Size) != 0; 2263 } 2264 return false; 2265 } 2266 2267 // Sanitize format 2268 // - For float/double we have to ignore format with precision (e.g. "%.2f") because sscanf doesn't take them in, so force them into %f and %lf 2269 // - In theory could treat empty format as using default, but this would only cover rare/bizarre case of using InputScalar() + integer + format string without %. 2270 char format_sanitized[32]; 2271 if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double) 2272 format = type_info->ScanFmt; 2273 else 2274 format = ImParseFormatSanitizeForScanning(format, format_sanitized, IM_ARRAYSIZE(format_sanitized)); 2275 2276 // Small types need a 32-bit buffer to receive the result from scanf() 2277 int v32 = 0; 2278 if (sscanf(buf, format, type_info->Size >= 4 ? p_data : &v32) < 1) 2279 return false; 2280 if (type_info->Size < 4) 2281 { 2282 if (data_type == ImGuiDataType_S8) 2283 *(ImS8*)p_data = (ImS8)ImClamp(v32, (int)IM_S8_MIN, (int)IM_S8_MAX); 2284 else if (data_type == ImGuiDataType_U8) 2285 *(ImU8*)p_data = (ImU8)ImClamp(v32, (int)IM_U8_MIN, (int)IM_U8_MAX); 2286 else if (data_type == ImGuiDataType_S16) 2287 *(ImS16*)p_data = (ImS16)ImClamp(v32, (int)IM_S16_MIN, (int)IM_S16_MAX); 2288 else if (data_type == ImGuiDataType_U16) 2289 *(ImU16*)p_data = (ImU16)ImClamp(v32, (int)IM_U16_MIN, (int)IM_U16_MAX); 2290 else 2291 IM_ASSERT(0); 2292 } 2293 2294 return memcmp(&data_backup, p_data, type_info->Size) != 0; 2295 } 2296 2297 template<typename T> 2298 static int DataTypeCompareT(const T* lhs, const T* rhs) 2299 { 2300 if (*lhs < *rhs) return -1; 2301 if (*lhs > *rhs) return +1; 2302 return 0; 2303 } 2304 2305 int ImGui::DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2) 2306 { 2307 switch (data_type) 2308 { 2309 case ImGuiDataType_S8: return DataTypeCompareT<ImS8 >((const ImS8* )arg_1, (const ImS8* )arg_2); 2310 case ImGuiDataType_U8: return DataTypeCompareT<ImU8 >((const ImU8* )arg_1, (const ImU8* )arg_2); 2311 case ImGuiDataType_S16: return DataTypeCompareT<ImS16 >((const ImS16* )arg_1, (const ImS16* )arg_2); 2312 case ImGuiDataType_U16: return DataTypeCompareT<ImU16 >((const ImU16* )arg_1, (const ImU16* )arg_2); 2313 case ImGuiDataType_S32: return DataTypeCompareT<ImS32 >((const ImS32* )arg_1, (const ImS32* )arg_2); 2314 case ImGuiDataType_U32: return DataTypeCompareT<ImU32 >((const ImU32* )arg_1, (const ImU32* )arg_2); 2315 case ImGuiDataType_S64: return DataTypeCompareT<ImS64 >((const ImS64* )arg_1, (const ImS64* )arg_2); 2316 case ImGuiDataType_U64: return DataTypeCompareT<ImU64 >((const ImU64* )arg_1, (const ImU64* )arg_2); 2317 case ImGuiDataType_Float: return DataTypeCompareT<float >((const float* )arg_1, (const float* )arg_2); 2318 case ImGuiDataType_Double: return DataTypeCompareT<double>((const double*)arg_1, (const double*)arg_2); 2319 case ImGuiDataType_COUNT: break; 2320 } 2321 IM_ASSERT(0); 2322 return 0; 2323 } 2324 2325 template<typename T> 2326 static bool DataTypeClampT(T* v, const T* v_min, const T* v_max) 2327 { 2328 // Clamp, both sides are optional, return true if modified 2329 if (v_min && *v < *v_min) { *v = *v_min; return true; } 2330 if (v_max && *v > *v_max) { *v = *v_max; return true; } 2331 return false; 2332 } 2333 2334 bool ImGui::DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max) 2335 { 2336 switch (data_type) 2337 { 2338 case ImGuiDataType_S8: return DataTypeClampT<ImS8 >((ImS8* )p_data, (const ImS8* )p_min, (const ImS8* )p_max); 2339 case ImGuiDataType_U8: return DataTypeClampT<ImU8 >((ImU8* )p_data, (const ImU8* )p_min, (const ImU8* )p_max); 2340 case ImGuiDataType_S16: return DataTypeClampT<ImS16 >((ImS16* )p_data, (const ImS16* )p_min, (const ImS16* )p_max); 2341 case ImGuiDataType_U16: return DataTypeClampT<ImU16 >((ImU16* )p_data, (const ImU16* )p_min, (const ImU16* )p_max); 2342 case ImGuiDataType_S32: return DataTypeClampT<ImS32 >((ImS32* )p_data, (const ImS32* )p_min, (const ImS32* )p_max); 2343 case ImGuiDataType_U32: return DataTypeClampT<ImU32 >((ImU32* )p_data, (const ImU32* )p_min, (const ImU32* )p_max); 2344 case ImGuiDataType_S64: return DataTypeClampT<ImS64 >((ImS64* )p_data, (const ImS64* )p_min, (const ImS64* )p_max); 2345 case ImGuiDataType_U64: return DataTypeClampT<ImU64 >((ImU64* )p_data, (const ImU64* )p_min, (const ImU64* )p_max); 2346 case ImGuiDataType_Float: return DataTypeClampT<float >((float* )p_data, (const float* )p_min, (const float* )p_max); 2347 case ImGuiDataType_Double: return DataTypeClampT<double>((double*)p_data, (const double*)p_min, (const double*)p_max); 2348 case ImGuiDataType_COUNT: break; 2349 } 2350 IM_ASSERT(0); 2351 return false; 2352 } 2353 2354 static float GetMinimumStepAtDecimalPrecision(int decimal_precision) 2355 { 2356 static const float min_steps[10] = { 1.0f, 0.1f, 0.01f, 0.001f, 0.0001f, 0.00001f, 0.000001f, 0.0000001f, 0.00000001f, 0.000000001f }; 2357 if (decimal_precision < 0) 2358 return FLT_MIN; 2359 return (decimal_precision < IM_ARRAYSIZE(min_steps)) ? min_steps[decimal_precision] : ImPow(10.0f, (float)-decimal_precision); 2360 } 2361 2362 template<typename TYPE> 2363 TYPE ImGui::RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, TYPE v) 2364 { 2365 IM_UNUSED(data_type); 2366 IM_ASSERT(data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double); 2367 const char* fmt_start = ImParseFormatFindStart(format); 2368 if (fmt_start[0] != '%' || fmt_start[1] == '%') // Don't apply if the value is not visible in the format string 2369 return v; 2370 2371 // Sanitize format 2372 char fmt_sanitized[32]; 2373 ImParseFormatSanitizeForPrinting(fmt_start, fmt_sanitized, IM_ARRAYSIZE(fmt_sanitized)); 2374 fmt_start = fmt_sanitized; 2375 2376 // Format value with our rounding, and read back 2377 char v_str[64]; 2378 ImFormatString(v_str, IM_ARRAYSIZE(v_str), fmt_start, v); 2379 const char* p = v_str; 2380 while (*p == ' ') 2381 p++; 2382 v = (TYPE)ImAtof(p); 2383 2384 return v; 2385 } 2386 2387 //------------------------------------------------------------------------- 2388 // [SECTION] Widgets: DragScalar, DragFloat, DragInt, etc. 2389 //------------------------------------------------------------------------- 2390 // - DragBehaviorT<>() [Internal] 2391 // - DragBehavior() [Internal] 2392 // - DragScalar() 2393 // - DragScalarN() 2394 // - DragFloat() 2395 // - DragFloat2() 2396 // - DragFloat3() 2397 // - DragFloat4() 2398 // - DragFloatRange2() 2399 // - DragInt() 2400 // - DragInt2() 2401 // - DragInt3() 2402 // - DragInt4() 2403 // - DragIntRange2() 2404 //------------------------------------------------------------------------- 2405 2406 // This is called by DragBehavior() when the widget is active (held by mouse or being manipulated with Nav controls) 2407 template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE> 2408 bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags) 2409 { 2410 ImGuiContext& g = *GImGui; 2411 const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X; 2412 const bool is_bounded = (v_min < v_max); 2413 const bool is_wrapped = is_bounded && (flags & ImGuiSliderFlags_WrapAround); 2414 const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0; 2415 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double); 2416 2417 // Default tweak speed 2418 if (v_speed == 0.0f && is_bounded && (v_max - v_min < FLT_MAX)) 2419 v_speed = (float)((v_max - v_min) * g.DragSpeedDefaultRatio); 2420 2421 // Inputs accumulates into g.DragCurrentAccum, which is flushed into the current value as soon as it makes a difference with our precision settings 2422 float adjust_delta = 0.0f; 2423 if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid() && IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR)) 2424 { 2425 adjust_delta = g.IO.MouseDelta[axis]; 2426 if (g.IO.KeyAlt) 2427 adjust_delta *= 1.0f / 100.0f; 2428 if (g.IO.KeyShift) 2429 adjust_delta *= 10.0f; 2430 } 2431 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) 2432 { 2433 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0; 2434 const bool tweak_slow = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow); 2435 const bool tweak_fast = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast); 2436 const float tweak_factor = tweak_slow ? 1.0f / 10.0f : tweak_fast ? 10.0f : 1.0f; 2437 adjust_delta = GetNavTweakPressedAmount(axis) * tweak_factor; 2438 v_speed = ImMax(v_speed, GetMinimumStepAtDecimalPrecision(decimal_precision)); 2439 } 2440 adjust_delta *= v_speed; 2441 2442 // For vertical drag we currently assume that Up=higher value (like we do with vertical sliders). This may become a parameter. 2443 if (axis == ImGuiAxis_Y) 2444 adjust_delta = -adjust_delta; 2445 2446 // For logarithmic use our range is effectively 0..1 so scale the delta into that range 2447 if (is_logarithmic && (v_max - v_min < FLT_MAX) && ((v_max - v_min) > 0.000001f)) // Epsilon to avoid /0 2448 adjust_delta /= (float)(v_max - v_min); 2449 2450 // Clear current value on activation 2451 // Avoid altering values and clamping when we are _already_ past the limits and heading in the same direction, so e.g. if range is 0..255, current value is 300 and we are pushing to the right side, keep the 300. 2452 const bool is_just_activated = g.ActiveIdIsJustActivated; 2453 const bool is_already_past_limits_and_pushing_outward = is_bounded && !is_wrapped && ((*v >= v_max && adjust_delta > 0.0f) || (*v <= v_min && adjust_delta < 0.0f)); 2454 if (is_just_activated || is_already_past_limits_and_pushing_outward) 2455 { 2456 g.DragCurrentAccum = 0.0f; 2457 g.DragCurrentAccumDirty = false; 2458 } 2459 else if (adjust_delta != 0.0f) 2460 { 2461 g.DragCurrentAccum += adjust_delta; 2462 g.DragCurrentAccumDirty = true; 2463 } 2464 2465 if (!g.DragCurrentAccumDirty) 2466 return false; 2467 2468 TYPE v_cur = *v; 2469 FLOATTYPE v_old_ref_for_accum_remainder = (FLOATTYPE)0.0f; 2470 2471 float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true 2472 const float zero_deadzone_halfsize = 0.0f; // Drag widgets have no deadzone (as it doesn't make sense) 2473 if (is_logarithmic) 2474 { 2475 // When using logarithmic sliders, we need to clamp to avoid hitting zero, but our choice of clamp value greatly affects slider precision. We attempt to use the specified precision to estimate a good lower bound. 2476 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1; 2477 logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision); 2478 2479 // Convert to parametric space, apply delta, convert back 2480 float v_old_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); 2481 float v_new_parametric = v_old_parametric + g.DragCurrentAccum; 2482 v_cur = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new_parametric, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); 2483 v_old_ref_for_accum_remainder = v_old_parametric; 2484 } 2485 else 2486 { 2487 v_cur += (SIGNEDTYPE)g.DragCurrentAccum; 2488 } 2489 2490 // Round to user desired precision based on format string 2491 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat)) 2492 v_cur = RoundScalarWithFormatT<TYPE>(format, data_type, v_cur); 2493 2494 // Preserve remainder after rounding has been applied. This also allow slow tweaking of values. 2495 g.DragCurrentAccumDirty = false; 2496 if (is_logarithmic) 2497 { 2498 // Convert to parametric space, apply delta, convert back 2499 float v_new_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); 2500 g.DragCurrentAccum -= (float)(v_new_parametric - v_old_ref_for_accum_remainder); 2501 } 2502 else 2503 { 2504 g.DragCurrentAccum -= (float)((SIGNEDTYPE)v_cur - (SIGNEDTYPE)*v); 2505 } 2506 2507 // Lose zero sign for float/double 2508 if (v_cur == (TYPE)-0) 2509 v_cur = (TYPE)0; 2510 2511 if (*v != v_cur && is_bounded) 2512 { 2513 if (is_wrapped) 2514 { 2515 // Wrap values 2516 if (v_cur < v_min) 2517 v_cur += v_max - v_min + (is_floating_point ? 0 : 1); 2518 if (v_cur > v_max) 2519 v_cur -= v_max - v_min + (is_floating_point ? 0 : 1); 2520 } 2521 else 2522 { 2523 // Clamp values + handle overflow/wrap-around for integer types. 2524 if (v_cur < v_min || (v_cur > *v && adjust_delta < 0.0f && !is_floating_point)) 2525 v_cur = v_min; 2526 if (v_cur > v_max || (v_cur < *v && adjust_delta > 0.0f && !is_floating_point)) 2527 v_cur = v_max; 2528 } 2529 } 2530 2531 // Apply result 2532 if (*v == v_cur) 2533 return false; 2534 *v = v_cur; 2535 return true; 2536 } 2537 2538 bool ImGui::DragBehavior(ImGuiID id, ImGuiDataType data_type, void* p_v, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) 2539 { 2540 // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert. 2541 IM_ASSERT((flags == 1 || (flags & ImGuiSliderFlags_InvalidMask_) == 0) && "Invalid ImGuiSliderFlags flags! Has the legacy 'float power' argument been mistakenly cast to flags? Call function with ImGuiSliderFlags_Logarithmic flags instead."); 2542 2543 ImGuiContext& g = *GImGui; 2544 if (g.ActiveId == id) 2545 { 2546 // Those are the things we can do easily outside the DragBehaviorT<> template, saves code generation. 2547 if (g.ActiveIdSource == ImGuiInputSource_Mouse && !g.IO.MouseDown[0]) 2548 ClearActiveID(); 2549 else if ((g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) && g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated) 2550 ClearActiveID(); 2551 } 2552 if (g.ActiveId != id) 2553 return false; 2554 if ((g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly)) 2555 return false; 2556 2557 switch (data_type) 2558 { 2559 case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = DragBehaviorT<ImS32, ImS32, float>(ImGuiDataType_S32, &v32, v_speed, p_min ? *(const ImS8*) p_min : IM_S8_MIN, p_max ? *(const ImS8*)p_max : IM_S8_MAX, format, flags); if (r) *(ImS8*)p_v = (ImS8)v32; return r; } 2560 case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)p_v; bool r = DragBehaviorT<ImU32, ImS32, float>(ImGuiDataType_U32, &v32, v_speed, p_min ? *(const ImU8*) p_min : IM_U8_MIN, p_max ? *(const ImU8*)p_max : IM_U8_MAX, format, flags); if (r) *(ImU8*)p_v = (ImU8)v32; return r; } 2561 case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)p_v; bool r = DragBehaviorT<ImS32, ImS32, float>(ImGuiDataType_S32, &v32, v_speed, p_min ? *(const ImS16*)p_min : IM_S16_MIN, p_max ? *(const ImS16*)p_max : IM_S16_MAX, format, flags); if (r) *(ImS16*)p_v = (ImS16)v32; return r; } 2562 case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)p_v; bool r = DragBehaviorT<ImU32, ImS32, float>(ImGuiDataType_U32, &v32, v_speed, p_min ? *(const ImU16*)p_min : IM_U16_MIN, p_max ? *(const ImU16*)p_max : IM_U16_MAX, format, flags); if (r) *(ImU16*)p_v = (ImU16)v32; return r; } 2563 case ImGuiDataType_S32: return DragBehaviorT<ImS32, ImS32, float >(data_type, (ImS32*)p_v, v_speed, p_min ? *(const ImS32* )p_min : IM_S32_MIN, p_max ? *(const ImS32* )p_max : IM_S32_MAX, format, flags); 2564 case ImGuiDataType_U32: return DragBehaviorT<ImU32, ImS32, float >(data_type, (ImU32*)p_v, v_speed, p_min ? *(const ImU32* )p_min : IM_U32_MIN, p_max ? *(const ImU32* )p_max : IM_U32_MAX, format, flags); 2565 case ImGuiDataType_S64: return DragBehaviorT<ImS64, ImS64, double>(data_type, (ImS64*)p_v, v_speed, p_min ? *(const ImS64* )p_min : IM_S64_MIN, p_max ? *(const ImS64* )p_max : IM_S64_MAX, format, flags); 2566 case ImGuiDataType_U64: return DragBehaviorT<ImU64, ImS64, double>(data_type, (ImU64*)p_v, v_speed, p_min ? *(const ImU64* )p_min : IM_U64_MIN, p_max ? *(const ImU64* )p_max : IM_U64_MAX, format, flags); 2567 case ImGuiDataType_Float: return DragBehaviorT<float, float, float >(data_type, (float*)p_v, v_speed, p_min ? *(const float* )p_min : -FLT_MAX, p_max ? *(const float* )p_max : FLT_MAX, format, flags); 2568 case ImGuiDataType_Double: return DragBehaviorT<double,double,double>(data_type, (double*)p_v, v_speed, p_min ? *(const double*)p_min : -DBL_MAX, p_max ? *(const double*)p_max : DBL_MAX, format, flags); 2569 case ImGuiDataType_COUNT: break; 2570 } 2571 IM_ASSERT(0); 2572 return false; 2573 } 2574 2575 // Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a Drag widget, p_min and p_max are optional. 2576 // Read code of e.g. DragFloat(), DragInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly. 2577 bool ImGui::DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) 2578 { 2579 ImGuiWindow* window = GetCurrentWindow(); 2580 if (window->SkipItems) 2581 return false; 2582 2583 ImGuiContext& g = *GImGui; 2584 const ImGuiStyle& style = g.Style; 2585 const ImGuiID id = window->GetID(label); 2586 const float w = CalcItemWidth(); 2587 2588 const ImVec2 label_size = CalcTextSize(label, NULL, true); 2589 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); 2590 const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); 2591 2592 const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0; 2593 ItemSize(total_bb, style.FramePadding.y); 2594 if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0)) 2595 return false; 2596 2597 // Default format string when passing NULL 2598 if (format == NULL) 2599 format = DataTypeGetInfo(data_type)->PrintFmt; 2600 2601 const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.InFlags); 2602 bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id); 2603 if (!temp_input_is_active) 2604 { 2605 // Tabbing or CTRL-clicking on Drag turns it into an InputText 2606 const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id); 2607 const bool double_clicked = (hovered && g.IO.MouseClickedCount[0] == 2 && TestKeyOwner(ImGuiKey_MouseLeft, id)); 2608 const bool make_active = (clicked || double_clicked || g.NavActivateId == id); 2609 if (make_active && (clicked || double_clicked)) 2610 SetKeyOwner(ImGuiKey_MouseLeft, id); 2611 if (make_active && temp_input_allowed) 2612 if ((clicked && g.IO.KeyCtrl) || double_clicked || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput))) 2613 temp_input_is_active = true; 2614 2615 // (Optional) simple click (without moving) turns Drag into an InputText 2616 if (g.IO.ConfigDragClickToInputText && temp_input_allowed && !temp_input_is_active) 2617 if (g.ActiveId == id && hovered && g.IO.MouseReleased[0] && !IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR)) 2618 { 2619 g.NavActivateId = id; 2620 g.NavActivateFlags = ImGuiActivateFlags_PreferInput; 2621 temp_input_is_active = true; 2622 } 2623 2624 if (make_active && !temp_input_is_active) 2625 { 2626 SetActiveID(id, window); 2627 SetFocusID(id, window); 2628 FocusWindow(window); 2629 g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right); 2630 } 2631 } 2632 2633 if (temp_input_is_active) 2634 { 2635 // Only clamp CTRL+Click input when ImGuiSliderFlags_AlwaysClamp is set 2636 const bool is_clamp_input = (flags & ImGuiSliderFlags_AlwaysClamp) != 0 && (p_min == NULL || p_max == NULL || DataTypeCompare(data_type, p_min, p_max) < 0); 2637 return TempInputScalar(frame_bb, id, label, data_type, p_data, format, is_clamp_input ? p_min : NULL, is_clamp_input ? p_max : NULL); 2638 } 2639 2640 // Draw frame 2641 const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); 2642 RenderNavHighlight(frame_bb, id); 2643 RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, style.FrameRounding); 2644 2645 // Drag behavior 2646 const bool value_changed = DragBehavior(id, data_type, p_data, v_speed, p_min, p_max, format, flags); 2647 if (value_changed) 2648 MarkItemEdited(id); 2649 2650 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. 2651 char value_buf[64]; 2652 const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format); 2653 if (g.LogEnabled) 2654 LogSetNextTextDecoration("{", "}"); 2655 RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f)); 2656 2657 if (label_size.x > 0.0f) 2658 RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); 2659 2660 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0)); 2661 return value_changed; 2662 } 2663 2664 bool ImGui::DragScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) 2665 { 2666 ImGuiWindow* window = GetCurrentWindow(); 2667 if (window->SkipItems) 2668 return false; 2669 2670 ImGuiContext& g = *GImGui; 2671 bool value_changed = false; 2672 BeginGroup(); 2673 PushID(label); 2674 PushMultiItemsWidths(components, CalcItemWidth()); 2675 size_t type_size = GDataTypeInfo[data_type].Size; 2676 for (int i = 0; i < components; i++) 2677 { 2678 PushID(i); 2679 if (i > 0) 2680 SameLine(0, g.Style.ItemInnerSpacing.x); 2681 value_changed |= DragScalar("", data_type, p_data, v_speed, p_min, p_max, format, flags); 2682 PopID(); 2683 PopItemWidth(); 2684 p_data = (void*)((char*)p_data + type_size); 2685 } 2686 PopID(); 2687 2688 const char* label_end = FindRenderedTextEnd(label); 2689 if (label != label_end) 2690 { 2691 SameLine(0, g.Style.ItemInnerSpacing.x); 2692 TextEx(label, label_end); 2693 } 2694 2695 EndGroup(); 2696 return value_changed; 2697 } 2698 2699 bool ImGui::DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) 2700 { 2701 return DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, format, flags); 2702 } 2703 2704 bool ImGui::DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) 2705 { 2706 return DragScalarN(label, ImGuiDataType_Float, v, 2, v_speed, &v_min, &v_max, format, flags); 2707 } 2708 2709 bool ImGui::DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) 2710 { 2711 return DragScalarN(label, ImGuiDataType_Float, v, 3, v_speed, &v_min, &v_max, format, flags); 2712 } 2713 2714 bool ImGui::DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) 2715 { 2716 return DragScalarN(label, ImGuiDataType_Float, v, 4, v_speed, &v_min, &v_max, format, flags); 2717 } 2718 2719 // NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this. 2720 bool ImGui::DragFloatRange2(const char* label, float* v_current_min, float* v_current_max, float v_speed, float v_min, float v_max, const char* format, const char* format_max, ImGuiSliderFlags flags) 2721 { 2722 ImGuiWindow* window = GetCurrentWindow(); 2723 if (window->SkipItems) 2724 return false; 2725 2726 ImGuiContext& g = *GImGui; 2727 PushID(label); 2728 BeginGroup(); 2729 PushMultiItemsWidths(2, CalcItemWidth()); 2730 2731 float min_min = (v_min >= v_max) ? -FLT_MAX : v_min; 2732 float min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max); 2733 ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0); 2734 bool value_changed = DragScalar("##min", ImGuiDataType_Float, v_current_min, v_speed, &min_min, &min_max, format, min_flags); 2735 PopItemWidth(); 2736 SameLine(0, g.Style.ItemInnerSpacing.x); 2737 2738 float max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min); 2739 float max_max = (v_min >= v_max) ? FLT_MAX : v_max; 2740 ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0); 2741 value_changed |= DragScalar("##max", ImGuiDataType_Float, v_current_max, v_speed, &max_min, &max_max, format_max ? format_max : format, max_flags); 2742 PopItemWidth(); 2743 SameLine(0, g.Style.ItemInnerSpacing.x); 2744 2745 TextEx(label, FindRenderedTextEnd(label)); 2746 EndGroup(); 2747 PopID(); 2748 2749 return value_changed; 2750 } 2751 2752 // NB: v_speed is float to allow adjusting the drag speed with more precision 2753 bool ImGui::DragInt(const char* label, int* v, float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) 2754 { 2755 return DragScalar(label, ImGuiDataType_S32, v, v_speed, &v_min, &v_max, format, flags); 2756 } 2757 2758 bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) 2759 { 2760 return DragScalarN(label, ImGuiDataType_S32, v, 2, v_speed, &v_min, &v_max, format, flags); 2761 } 2762 2763 bool ImGui::DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) 2764 { 2765 return DragScalarN(label, ImGuiDataType_S32, v, 3, v_speed, &v_min, &v_max, format, flags); 2766 } 2767 2768 bool ImGui::DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) 2769 { 2770 return DragScalarN(label, ImGuiDataType_S32, v, 4, v_speed, &v_min, &v_max, format, flags); 2771 } 2772 2773 // NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this. 2774 bool ImGui::DragIntRange2(const char* label, int* v_current_min, int* v_current_max, float v_speed, int v_min, int v_max, const char* format, const char* format_max, ImGuiSliderFlags flags) 2775 { 2776 ImGuiWindow* window = GetCurrentWindow(); 2777 if (window->SkipItems) 2778 return false; 2779 2780 ImGuiContext& g = *GImGui; 2781 PushID(label); 2782 BeginGroup(); 2783 PushMultiItemsWidths(2, CalcItemWidth()); 2784 2785 int min_min = (v_min >= v_max) ? INT_MIN : v_min; 2786 int min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max); 2787 ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0); 2788 bool value_changed = DragInt("##min", v_current_min, v_speed, min_min, min_max, format, min_flags); 2789 PopItemWidth(); 2790 SameLine(0, g.Style.ItemInnerSpacing.x); 2791 2792 int max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min); 2793 int max_max = (v_min >= v_max) ? INT_MAX : v_max; 2794 ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0); 2795 value_changed |= DragInt("##max", v_current_max, v_speed, max_min, max_max, format_max ? format_max : format, max_flags); 2796 PopItemWidth(); 2797 SameLine(0, g.Style.ItemInnerSpacing.x); 2798 2799 TextEx(label, FindRenderedTextEnd(label)); 2800 EndGroup(); 2801 PopID(); 2802 2803 return value_changed; 2804 } 2805 2806 //------------------------------------------------------------------------- 2807 // [SECTION] Widgets: SliderScalar, SliderFloat, SliderInt, etc. 2808 //------------------------------------------------------------------------- 2809 // - ScaleRatioFromValueT<> [Internal] 2810 // - ScaleValueFromRatioT<> [Internal] 2811 // - SliderBehaviorT<>() [Internal] 2812 // - SliderBehavior() [Internal] 2813 // - SliderScalar() 2814 // - SliderScalarN() 2815 // - SliderFloat() 2816 // - SliderFloat2() 2817 // - SliderFloat3() 2818 // - SliderFloat4() 2819 // - SliderAngle() 2820 // - SliderInt() 2821 // - SliderInt2() 2822 // - SliderInt3() 2823 // - SliderInt4() 2824 // - VSliderScalar() 2825 // - VSliderFloat() 2826 // - VSliderInt() 2827 //------------------------------------------------------------------------- 2828 2829 // Convert a value v in the output space of a slider into a parametric position on the slider itself (the logical opposite of ScaleValueFromRatioT) 2830 template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE> 2831 float ImGui::ScaleRatioFromValueT(ImGuiDataType data_type, TYPE v, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize) 2832 { 2833 if (v_min == v_max) 2834 return 0.0f; 2835 IM_UNUSED(data_type); 2836 2837 const TYPE v_clamped = (v_min < v_max) ? ImClamp(v, v_min, v_max) : ImClamp(v, v_max, v_min); 2838 if (is_logarithmic) 2839 { 2840 bool flipped = v_max < v_min; 2841 2842 if (flipped) // Handle the case where the range is backwards 2843 ImSwap(v_min, v_max); 2844 2845 // Fudge min/max to avoid getting close to log(0) 2846 FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min; 2847 FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max; 2848 2849 // Awkward special cases - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon) 2850 if ((v_min == 0.0f) && (v_max < 0.0f)) 2851 v_min_fudged = -logarithmic_zero_epsilon; 2852 else if ((v_max == 0.0f) && (v_min < 0.0f)) 2853 v_max_fudged = -logarithmic_zero_epsilon; 2854 2855 float result; 2856 if (v_clamped <= v_min_fudged) 2857 result = 0.0f; // Workaround for values that are in-range but below our fudge 2858 else if (v_clamped >= v_max_fudged) 2859 result = 1.0f; // Workaround for values that are in-range but above our fudge 2860 else if ((v_min * v_max) < 0.0f) // Range crosses zero, so split into two portions 2861 { 2862 float zero_point_center = (-(float)v_min) / ((float)v_max - (float)v_min); // The zero point in parametric space. There's an argument we should take the logarithmic nature into account when calculating this, but for now this should do (and the most common case of a symmetrical range works fine) 2863 float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize; 2864 float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize; 2865 if (v == 0.0f) 2866 result = zero_point_center; // Special case for exactly zero 2867 else if (v < 0.0f) 2868 result = (1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(-v_min_fudged / logarithmic_zero_epsilon))) * zero_point_snap_L; 2869 else 2870 result = zero_point_snap_R + ((float)(ImLog((FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(v_max_fudged / logarithmic_zero_epsilon)) * (1.0f - zero_point_snap_R)); 2871 } 2872 else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider 2873 result = 1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / -v_max_fudged) / ImLog(-v_min_fudged / -v_max_fudged)); 2874 else 2875 result = (float)(ImLog((FLOATTYPE)v_clamped / v_min_fudged) / ImLog(v_max_fudged / v_min_fudged)); 2876 2877 return flipped ? (1.0f - result) : result; 2878 } 2879 else 2880 { 2881 // Linear slider 2882 return (float)((FLOATTYPE)(SIGNEDTYPE)(v_clamped - v_min) / (FLOATTYPE)(SIGNEDTYPE)(v_max - v_min)); 2883 } 2884 } 2885 2886 // Convert a parametric position on a slider into a value v in the output space (the logical opposite of ScaleRatioFromValueT) 2887 template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE> 2888 TYPE ImGui::ScaleValueFromRatioT(ImGuiDataType data_type, float t, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize) 2889 { 2890 // We special-case the extents because otherwise our logarithmic fudging can lead to "mathematically correct" 2891 // but non-intuitive behaviors like a fully-left slider not actually reaching the minimum value. Also generally simpler. 2892 if (t <= 0.0f || v_min == v_max) 2893 return v_min; 2894 if (t >= 1.0f) 2895 return v_max; 2896 2897 TYPE result = (TYPE)0; 2898 if (is_logarithmic) 2899 { 2900 // Fudge min/max to avoid getting silly results close to zero 2901 FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min; 2902 FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max; 2903 2904 const bool flipped = v_max < v_min; // Check if range is "backwards" 2905 if (flipped) 2906 ImSwap(v_min_fudged, v_max_fudged); 2907 2908 // Awkward special case - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon) 2909 if ((v_max == 0.0f) && (v_min < 0.0f)) 2910 v_max_fudged = -logarithmic_zero_epsilon; 2911 2912 float t_with_flip = flipped ? (1.0f - t) : t; // t, but flipped if necessary to account for us flipping the range 2913 2914 if ((v_min * v_max) < 0.0f) // Range crosses zero, so we have to do this in two parts 2915 { 2916 float zero_point_center = (-(float)ImMin(v_min, v_max)) / ImAbs((float)v_max - (float)v_min); // The zero point in parametric space 2917 float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize; 2918 float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize; 2919 if (t_with_flip >= zero_point_snap_L && t_with_flip <= zero_point_snap_R) 2920 result = (TYPE)0.0f; // Special case to make getting exactly zero possible (the epsilon prevents it otherwise) 2921 else if (t_with_flip < zero_point_center) 2922 result = (TYPE)-(logarithmic_zero_epsilon * ImPow(-v_min_fudged / logarithmic_zero_epsilon, (FLOATTYPE)(1.0f - (t_with_flip / zero_point_snap_L)))); 2923 else 2924 result = (TYPE)(logarithmic_zero_epsilon * ImPow(v_max_fudged / logarithmic_zero_epsilon, (FLOATTYPE)((t_with_flip - zero_point_snap_R) / (1.0f - zero_point_snap_R)))); 2925 } 2926 else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider 2927 result = (TYPE)-(-v_max_fudged * ImPow(-v_min_fudged / -v_max_fudged, (FLOATTYPE)(1.0f - t_with_flip))); 2928 else 2929 result = (TYPE)(v_min_fudged * ImPow(v_max_fudged / v_min_fudged, (FLOATTYPE)t_with_flip)); 2930 } 2931 else 2932 { 2933 // Linear slider 2934 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double); 2935 if (is_floating_point) 2936 { 2937 result = ImLerp(v_min, v_max, t); 2938 } 2939 else if (t < 1.0) 2940 { 2941 // - For integer values we want the clicking position to match the grab box so we round above 2942 // This code is carefully tuned to work with large values (e.g. high ranges of U64) while preserving this property.. 2943 // - Not doing a *1.0 multiply at the end of a range as it tends to be lossy. While absolute aiming at a large s64/u64 2944 // range is going to be imprecise anyway, with this check we at least make the edge values matches expected limits. 2945 FLOATTYPE v_new_off_f = (SIGNEDTYPE)(v_max - v_min) * t; 2946 result = (TYPE)((SIGNEDTYPE)v_min + (SIGNEDTYPE)(v_new_off_f + (FLOATTYPE)(v_min > v_max ? -0.5 : 0.5))); 2947 } 2948 } 2949 2950 return result; 2951 } 2952 2953 // FIXME: Try to move more of the code into shared SliderBehavior() 2954 template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE> 2955 bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, TYPE* v, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb) 2956 { 2957 ImGuiContext& g = *GImGui; 2958 const ImGuiStyle& style = g.Style; 2959 2960 const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X; 2961 const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0; 2962 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double); 2963 const float v_range_f = (float)(v_min < v_max ? v_max - v_min : v_min - v_max); // We don't need high precision for what we do with it. 2964 2965 // Calculate bounds 2966 const float grab_padding = 2.0f; // FIXME: Should be part of style. 2967 const float slider_sz = (bb.Max[axis] - bb.Min[axis]) - grab_padding * 2.0f; 2968 float grab_sz = style.GrabMinSize; 2969 if (!is_floating_point && v_range_f >= 0.0f) // v_range_f < 0 may happen on integer overflows 2970 grab_sz = ImMax(slider_sz / (v_range_f + 1), style.GrabMinSize); // For integer sliders: if possible have the grab size represent 1 unit 2971 grab_sz = ImMin(grab_sz, slider_sz); 2972 const float slider_usable_sz = slider_sz - grab_sz; 2973 const float slider_usable_pos_min = bb.Min[axis] + grab_padding + grab_sz * 0.5f; 2974 const float slider_usable_pos_max = bb.Max[axis] - grab_padding - grab_sz * 0.5f; 2975 2976 float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true 2977 float zero_deadzone_halfsize = 0.0f; // Only valid when is_logarithmic is true 2978 if (is_logarithmic) 2979 { 2980 // When using logarithmic sliders, we need to clamp to avoid hitting zero, but our choice of clamp value greatly affects slider precision. We attempt to use the specified precision to estimate a good lower bound. 2981 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1; 2982 logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision); 2983 zero_deadzone_halfsize = (style.LogSliderDeadzone * 0.5f) / ImMax(slider_usable_sz, 1.0f); 2984 } 2985 2986 // Process interacting with the slider 2987 bool value_changed = false; 2988 if (g.ActiveId == id) 2989 { 2990 bool set_new_value = false; 2991 float clicked_t = 0.0f; 2992 if (g.ActiveIdSource == ImGuiInputSource_Mouse) 2993 { 2994 if (!g.IO.MouseDown[0]) 2995 { 2996 ClearActiveID(); 2997 } 2998 else 2999 { 3000 const float mouse_abs_pos = g.IO.MousePos[axis]; 3001 if (g.ActiveIdIsJustActivated) 3002 { 3003 float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); 3004 if (axis == ImGuiAxis_Y) 3005 grab_t = 1.0f - grab_t; 3006 const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t); 3007 const bool clicked_around_grab = (mouse_abs_pos >= grab_pos - grab_sz * 0.5f - 1.0f) && (mouse_abs_pos <= grab_pos + grab_sz * 0.5f + 1.0f); // No harm being extra generous here. 3008 g.SliderGrabClickOffset = (clicked_around_grab && is_floating_point) ? mouse_abs_pos - grab_pos : 0.0f; 3009 } 3010 if (slider_usable_sz > 0.0f) 3011 clicked_t = ImSaturate((mouse_abs_pos - g.SliderGrabClickOffset - slider_usable_pos_min) / slider_usable_sz); 3012 if (axis == ImGuiAxis_Y) 3013 clicked_t = 1.0f - clicked_t; 3014 set_new_value = true; 3015 } 3016 } 3017 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) 3018 { 3019 if (g.ActiveIdIsJustActivated) 3020 { 3021 g.SliderCurrentAccum = 0.0f; // Reset any stored nav delta upon activation 3022 g.SliderCurrentAccumDirty = false; 3023 } 3024 3025 float input_delta = (axis == ImGuiAxis_X) ? GetNavTweakPressedAmount(axis) : -GetNavTweakPressedAmount(axis); 3026 if (input_delta != 0.0f) 3027 { 3028 const bool tweak_slow = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow); 3029 const bool tweak_fast = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast); 3030 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0; 3031 if (decimal_precision > 0) 3032 { 3033 input_delta /= 100.0f; // Gamepad/keyboard tweak speeds in % of slider bounds 3034 if (tweak_slow) 3035 input_delta /= 10.0f; 3036 } 3037 else 3038 { 3039 if ((v_range_f >= -100.0f && v_range_f <= 100.0f && v_range_f != 0.0f) || tweak_slow) 3040 input_delta = ((input_delta < 0.0f) ? -1.0f : +1.0f) / v_range_f; // Gamepad/keyboard tweak speeds in integer steps 3041 else 3042 input_delta /= 100.0f; 3043 } 3044 if (tweak_fast) 3045 input_delta *= 10.0f; 3046 3047 g.SliderCurrentAccum += input_delta; 3048 g.SliderCurrentAccumDirty = true; 3049 } 3050 3051 float delta = g.SliderCurrentAccum; 3052 if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated) 3053 { 3054 ClearActiveID(); 3055 } 3056 else if (g.SliderCurrentAccumDirty) 3057 { 3058 clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); 3059 3060 if ((clicked_t >= 1.0f && delta > 0.0f) || (clicked_t <= 0.0f && delta < 0.0f)) // This is to avoid applying the saturation when already past the limits 3061 { 3062 set_new_value = false; 3063 g.SliderCurrentAccum = 0.0f; // If pushing up against the limits, don't continue to accumulate 3064 } 3065 else 3066 { 3067 set_new_value = true; 3068 float old_clicked_t = clicked_t; 3069 clicked_t = ImSaturate(clicked_t + delta); 3070 3071 // Calculate what our "new" clicked_t will be, and thus how far we actually moved the slider, and subtract this from the accumulator 3072 TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); 3073 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat)) 3074 v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new); 3075 float new_clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); 3076 3077 if (delta > 0) 3078 g.SliderCurrentAccum -= ImMin(new_clicked_t - old_clicked_t, delta); 3079 else 3080 g.SliderCurrentAccum -= ImMax(new_clicked_t - old_clicked_t, delta); 3081 } 3082 3083 g.SliderCurrentAccumDirty = false; 3084 } 3085 } 3086 3087 if (set_new_value) 3088 if ((g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly)) 3089 set_new_value = false; 3090 3091 if (set_new_value) 3092 { 3093 TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); 3094 3095 // Round to user desired precision based on format string 3096 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat)) 3097 v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new); 3098 3099 // Apply result 3100 if (*v != v_new) 3101 { 3102 *v = v_new; 3103 value_changed = true; 3104 } 3105 } 3106 } 3107 3108 if (slider_sz < 1.0f) 3109 { 3110 *out_grab_bb = ImRect(bb.Min, bb.Min); 3111 } 3112 else 3113 { 3114 // Output grab position so it can be displayed by the caller 3115 float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); 3116 if (axis == ImGuiAxis_Y) 3117 grab_t = 1.0f - grab_t; 3118 const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t); 3119 if (axis == ImGuiAxis_X) 3120 *out_grab_bb = ImRect(grab_pos - grab_sz * 0.5f, bb.Min.y + grab_padding, grab_pos + grab_sz * 0.5f, bb.Max.y - grab_padding); 3121 else 3122 *out_grab_bb = ImRect(bb.Min.x + grab_padding, grab_pos - grab_sz * 0.5f, bb.Max.x - grab_padding, grab_pos + grab_sz * 0.5f); 3123 } 3124 3125 return value_changed; 3126 } 3127 3128 // For 32-bit and larger types, slider bounds are limited to half the natural type range. 3129 // So e.g. an integer Slider between INT_MAX-10 and INT_MAX will fail, but an integer Slider between INT_MAX/2-10 and INT_MAX/2 will be ok. 3130 // It would be possible to lift that limitation with some work but it doesn't seem to be worth it for sliders. 3131 bool ImGui::SliderBehavior(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, void* p_v, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb) 3132 { 3133 // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert. 3134 IM_ASSERT((flags == 1 || (flags & ImGuiSliderFlags_InvalidMask_) == 0) && "Invalid ImGuiSliderFlags flags! Has the legacy 'float power' argument been mistakenly cast to flags? Call function with ImGuiSliderFlags_Logarithmic flags instead."); 3135 IM_ASSERT((flags & ImGuiSliderFlags_WrapAround) == 0); // Not supported by SliderXXX(), only by DragXXX() 3136 3137 switch (data_type) 3138 { 3139 case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = SliderBehaviorT<ImS32, ImS32, float>(bb, id, ImGuiDataType_S32, &v32, *(const ImS8*)p_min, *(const ImS8*)p_max, format, flags, out_grab_bb); if (r) *(ImS8*)p_v = (ImS8)v32; return r; } 3140 case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)p_v; bool r = SliderBehaviorT<ImU32, ImS32, float>(bb, id, ImGuiDataType_U32, &v32, *(const ImU8*)p_min, *(const ImU8*)p_max, format, flags, out_grab_bb); if (r) *(ImU8*)p_v = (ImU8)v32; return r; } 3141 case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)p_v; bool r = SliderBehaviorT<ImS32, ImS32, float>(bb, id, ImGuiDataType_S32, &v32, *(const ImS16*)p_min, *(const ImS16*)p_max, format, flags, out_grab_bb); if (r) *(ImS16*)p_v = (ImS16)v32; return r; } 3142 case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)p_v; bool r = SliderBehaviorT<ImU32, ImS32, float>(bb, id, ImGuiDataType_U32, &v32, *(const ImU16*)p_min, *(const ImU16*)p_max, format, flags, out_grab_bb); if (r) *(ImU16*)p_v = (ImU16)v32; return r; } 3143 case ImGuiDataType_S32: 3144 IM_ASSERT(*(const ImS32*)p_min >= IM_S32_MIN / 2 && *(const ImS32*)p_max <= IM_S32_MAX / 2); 3145 return SliderBehaviorT<ImS32, ImS32, float >(bb, id, data_type, (ImS32*)p_v, *(const ImS32*)p_min, *(const ImS32*)p_max, format, flags, out_grab_bb); 3146 case ImGuiDataType_U32: 3147 IM_ASSERT(*(const ImU32*)p_max <= IM_U32_MAX / 2); 3148 return SliderBehaviorT<ImU32, ImS32, float >(bb, id, data_type, (ImU32*)p_v, *(const ImU32*)p_min, *(const ImU32*)p_max, format, flags, out_grab_bb); 3149 case ImGuiDataType_S64: 3150 IM_ASSERT(*(const ImS64*)p_min >= IM_S64_MIN / 2 && *(const ImS64*)p_max <= IM_S64_MAX / 2); 3151 return SliderBehaviorT<ImS64, ImS64, double>(bb, id, data_type, (ImS64*)p_v, *(const ImS64*)p_min, *(const ImS64*)p_max, format, flags, out_grab_bb); 3152 case ImGuiDataType_U64: 3153 IM_ASSERT(*(const ImU64*)p_max <= IM_U64_MAX / 2); 3154 return SliderBehaviorT<ImU64, ImS64, double>(bb, id, data_type, (ImU64*)p_v, *(const ImU64*)p_min, *(const ImU64*)p_max, format, flags, out_grab_bb); 3155 case ImGuiDataType_Float: 3156 IM_ASSERT(*(const float*)p_min >= -FLT_MAX / 2.0f && *(const float*)p_max <= FLT_MAX / 2.0f); 3157 return SliderBehaviorT<float, float, float >(bb, id, data_type, (float*)p_v, *(const float*)p_min, *(const float*)p_max, format, flags, out_grab_bb); 3158 case ImGuiDataType_Double: 3159 IM_ASSERT(*(const double*)p_min >= -DBL_MAX / 2.0f && *(const double*)p_max <= DBL_MAX / 2.0f); 3160 return SliderBehaviorT<double, double, double>(bb, id, data_type, (double*)p_v, *(const double*)p_min, *(const double*)p_max, format, flags, out_grab_bb); 3161 case ImGuiDataType_COUNT: break; 3162 } 3163 IM_ASSERT(0); 3164 return false; 3165 } 3166 3167 // Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a slider, they are all required. 3168 // Read code of e.g. SliderFloat(), SliderInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly. 3169 bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) 3170 { 3171 ImGuiWindow* window = GetCurrentWindow(); 3172 if (window->SkipItems) 3173 return false; 3174 3175 ImGuiContext& g = *GImGui; 3176 const ImGuiStyle& style = g.Style; 3177 const ImGuiID id = window->GetID(label); 3178 const float w = CalcItemWidth(); 3179 3180 const ImVec2 label_size = CalcTextSize(label, NULL, true); 3181 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); 3182 const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); 3183 3184 const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0; 3185 ItemSize(total_bb, style.FramePadding.y); 3186 if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0)) 3187 return false; 3188 3189 // Default format string when passing NULL 3190 if (format == NULL) 3191 format = DataTypeGetInfo(data_type)->PrintFmt; 3192 3193 const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.InFlags); 3194 bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id); 3195 if (!temp_input_is_active) 3196 { 3197 // Tabbing or CTRL-clicking on Slider turns it into an input box 3198 const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id); 3199 const bool make_active = (clicked || g.NavActivateId == id); 3200 if (make_active && clicked) 3201 SetKeyOwner(ImGuiKey_MouseLeft, id); 3202 if (make_active && temp_input_allowed) 3203 if ((clicked && g.IO.KeyCtrl) || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput))) 3204 temp_input_is_active = true; 3205 3206 if (make_active && !temp_input_is_active) 3207 { 3208 SetActiveID(id, window); 3209 SetFocusID(id, window); 3210 FocusWindow(window); 3211 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right); 3212 } 3213 } 3214 3215 if (temp_input_is_active) 3216 { 3217 // Only clamp CTRL+Click input when ImGuiSliderFlags_AlwaysClamp is set 3218 const bool is_clamp_input = (flags & ImGuiSliderFlags_AlwaysClamp) != 0; 3219 return TempInputScalar(frame_bb, id, label, data_type, p_data, format, is_clamp_input ? p_min : NULL, is_clamp_input ? p_max : NULL); 3220 } 3221 3222 // Draw frame 3223 const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); 3224 RenderNavHighlight(frame_bb, id); 3225 RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, g.Style.FrameRounding); 3226 3227 // Slider behavior 3228 ImRect grab_bb; 3229 const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags, &grab_bb); 3230 if (value_changed) 3231 MarkItemEdited(id); 3232 3233 // Render grab 3234 if (grab_bb.Max.x > grab_bb.Min.x) 3235 window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding); 3236 3237 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. 3238 char value_buf[64]; 3239 const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format); 3240 if (g.LogEnabled) 3241 LogSetNextTextDecoration("{", "}"); 3242 RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f)); 3243 3244 if (label_size.x > 0.0f) 3245 RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); 3246 3247 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0)); 3248 return value_changed; 3249 } 3250 3251 // Add multiple sliders on 1 line for compact edition of multiple components 3252 bool ImGui::SliderScalarN(const char* label, ImGuiDataType data_type, void* v, int components, const void* v_min, const void* v_max, const char* format, ImGuiSliderFlags flags) 3253 { 3254 ImGuiWindow* window = GetCurrentWindow(); 3255 if (window->SkipItems) 3256 return false; 3257 3258 ImGuiContext& g = *GImGui; 3259 bool value_changed = false; 3260 BeginGroup(); 3261 PushID(label); 3262 PushMultiItemsWidths(components, CalcItemWidth()); 3263 size_t type_size = GDataTypeInfo[data_type].Size; 3264 for (int i = 0; i < components; i++) 3265 { 3266 PushID(i); 3267 if (i > 0) 3268 SameLine(0, g.Style.ItemInnerSpacing.x); 3269 value_changed |= SliderScalar("", data_type, v, v_min, v_max, format, flags); 3270 PopID(); 3271 PopItemWidth(); 3272 v = (void*)((char*)v + type_size); 3273 } 3274 PopID(); 3275 3276 const char* label_end = FindRenderedTextEnd(label); 3277 if (label != label_end) 3278 { 3279 SameLine(0, g.Style.ItemInnerSpacing.x); 3280 TextEx(label, label_end); 3281 } 3282 3283 EndGroup(); 3284 return value_changed; 3285 } 3286 3287 bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) 3288 { 3289 return SliderScalar(label, ImGuiDataType_Float, v, &v_min, &v_max, format, flags); 3290 } 3291 3292 bool ImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, ImGuiSliderFlags flags) 3293 { 3294 return SliderScalarN(label, ImGuiDataType_Float, v, 2, &v_min, &v_max, format, flags); 3295 } 3296 3297 bool ImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, ImGuiSliderFlags flags) 3298 { 3299 return SliderScalarN(label, ImGuiDataType_Float, v, 3, &v_min, &v_max, format, flags); 3300 } 3301 3302 bool ImGui::SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, ImGuiSliderFlags flags) 3303 { 3304 return SliderScalarN(label, ImGuiDataType_Float, v, 4, &v_min, &v_max, format, flags); 3305 } 3306 3307 bool ImGui::SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max, const char* format, ImGuiSliderFlags flags) 3308 { 3309 if (format == NULL) 3310 format = "%.0f deg"; 3311 float v_deg = (*v_rad) * 360.0f / (2 * IM_PI); 3312 bool value_changed = SliderFloat(label, &v_deg, v_degrees_min, v_degrees_max, format, flags); 3313 *v_rad = v_deg * (2 * IM_PI) / 360.0f; 3314 return value_changed; 3315 } 3316 3317 bool ImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) 3318 { 3319 return SliderScalar(label, ImGuiDataType_S32, v, &v_min, &v_max, format, flags); 3320 } 3321 3322 bool ImGui::SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format, ImGuiSliderFlags flags) 3323 { 3324 return SliderScalarN(label, ImGuiDataType_S32, v, 2, &v_min, &v_max, format, flags); 3325 } 3326 3327 bool ImGui::SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format, ImGuiSliderFlags flags) 3328 { 3329 return SliderScalarN(label, ImGuiDataType_S32, v, 3, &v_min, &v_max, format, flags); 3330 } 3331 3332 bool ImGui::SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format, ImGuiSliderFlags flags) 3333 { 3334 return SliderScalarN(label, ImGuiDataType_S32, v, 4, &v_min, &v_max, format, flags); 3335 } 3336 3337 bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) 3338 { 3339 ImGuiWindow* window = GetCurrentWindow(); 3340 if (window->SkipItems) 3341 return false; 3342 3343 ImGuiContext& g = *GImGui; 3344 const ImGuiStyle& style = g.Style; 3345 const ImGuiID id = window->GetID(label); 3346 3347 const ImVec2 label_size = CalcTextSize(label, NULL, true); 3348 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size); 3349 const ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); 3350 3351 ItemSize(bb, style.FramePadding.y); 3352 if (!ItemAdd(frame_bb, id)) 3353 return false; 3354 3355 // Default format string when passing NULL 3356 if (format == NULL) 3357 format = DataTypeGetInfo(data_type)->PrintFmt; 3358 3359 const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.InFlags); 3360 const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id); 3361 if (clicked || g.NavActivateId == id) 3362 { 3363 if (clicked) 3364 SetKeyOwner(ImGuiKey_MouseLeft, id); 3365 SetActiveID(id, window); 3366 SetFocusID(id, window); 3367 FocusWindow(window); 3368 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); 3369 } 3370 3371 // Draw frame 3372 const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); 3373 RenderNavHighlight(frame_bb, id); 3374 RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, g.Style.FrameRounding); 3375 3376 // Slider behavior 3377 ImRect grab_bb; 3378 const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags | ImGuiSliderFlags_Vertical, &grab_bb); 3379 if (value_changed) 3380 MarkItemEdited(id); 3381 3382 // Render grab 3383 if (grab_bb.Max.y > grab_bb.Min.y) 3384 window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding); 3385 3386 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. 3387 // For the vertical slider we allow centered text to overlap the frame padding 3388 char value_buf[64]; 3389 const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format); 3390 RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.0f)); 3391 if (label_size.x > 0.0f) 3392 RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); 3393 3394 return value_changed; 3395 } 3396 3397 bool ImGui::VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) 3398 { 3399 return VSliderScalar(label, size, ImGuiDataType_Float, v, &v_min, &v_max, format, flags); 3400 } 3401 3402 bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) 3403 { 3404 return VSliderScalar(label, size, ImGuiDataType_S32, v, &v_min, &v_max, format, flags); 3405 } 3406 3407 //------------------------------------------------------------------------- 3408 // [SECTION] Widgets: InputScalar, InputFloat, InputInt, etc. 3409 //------------------------------------------------------------------------- 3410 // - ImParseFormatFindStart() [Internal] 3411 // - ImParseFormatFindEnd() [Internal] 3412 // - ImParseFormatTrimDecorations() [Internal] 3413 // - ImParseFormatSanitizeForPrinting() [Internal] 3414 // - ImParseFormatSanitizeForScanning() [Internal] 3415 // - ImParseFormatPrecision() [Internal] 3416 // - TempInputTextScalar() [Internal] 3417 // - InputScalar() 3418 // - InputScalarN() 3419 // - InputFloat() 3420 // - InputFloat2() 3421 // - InputFloat3() 3422 // - InputFloat4() 3423 // - InputInt() 3424 // - InputInt2() 3425 // - InputInt3() 3426 // - InputInt4() 3427 // - InputDouble() 3428 //------------------------------------------------------------------------- 3429 3430 // We don't use strchr() because our strings are usually very short and often start with '%' 3431 const char* ImParseFormatFindStart(const char* fmt) 3432 { 3433 while (char c = fmt[0]) 3434 { 3435 if (c == '%' && fmt[1] != '%') 3436 return fmt; 3437 else if (c == '%') 3438 fmt++; 3439 fmt++; 3440 } 3441 return fmt; 3442 } 3443 3444 const char* ImParseFormatFindEnd(const char* fmt) 3445 { 3446 // Printf/scanf types modifiers: I/L/h/j/l/t/w/z. Other uppercase letters qualify as types aka end of the format. 3447 if (fmt[0] != '%') 3448 return fmt; 3449 const unsigned int ignored_uppercase_mask = (1 << ('I'-'A')) | (1 << ('L'-'A')); 3450 const unsigned int ignored_lowercase_mask = (1 << ('h'-'a')) | (1 << ('j'-'a')) | (1 << ('l'-'a')) | (1 << ('t'-'a')) | (1 << ('w'-'a')) | (1 << ('z'-'a')); 3451 for (char c; (c = *fmt) != 0; fmt++) 3452 { 3453 if (c >= 'A' && c <= 'Z' && ((1 << (c - 'A')) & ignored_uppercase_mask) == 0) 3454 return fmt + 1; 3455 if (c >= 'a' && c <= 'z' && ((1 << (c - 'a')) & ignored_lowercase_mask) == 0) 3456 return fmt + 1; 3457 } 3458 return fmt; 3459 } 3460 3461 // Extract the format out of a format string with leading or trailing decorations 3462 // fmt = "blah blah" -> return "" 3463 // fmt = "%.3f" -> return fmt 3464 // fmt = "hello %.3f" -> return fmt + 6 3465 // fmt = "%.3f hello" -> return buf written with "%.3f" 3466 const char* ImParseFormatTrimDecorations(const char* fmt, char* buf, size_t buf_size) 3467 { 3468 const char* fmt_start = ImParseFormatFindStart(fmt); 3469 if (fmt_start[0] != '%') 3470 return ""; 3471 const char* fmt_end = ImParseFormatFindEnd(fmt_start); 3472 if (fmt_end[0] == 0) // If we only have leading decoration, we don't need to copy the data. 3473 return fmt_start; 3474 ImStrncpy(buf, fmt_start, ImMin((size_t)(fmt_end - fmt_start) + 1, buf_size)); 3475 return buf; 3476 } 3477 3478 // Sanitize format 3479 // - Zero terminate so extra characters after format (e.g. "%f123") don't confuse atof/atoi 3480 // - stb_sprintf.h supports several new modifiers which format numbers in a way that also makes them incompatible atof/atoi. 3481 void ImParseFormatSanitizeForPrinting(const char* fmt_in, char* fmt_out, size_t fmt_out_size) 3482 { 3483 const char* fmt_end = ImParseFormatFindEnd(fmt_in); 3484 IM_UNUSED(fmt_out_size); 3485 IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you! 3486 while (fmt_in < fmt_end) 3487 { 3488 char c = *fmt_in++; 3489 if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '. 3490 *(fmt_out++) = c; 3491 } 3492 *fmt_out = 0; // Zero-terminate 3493 } 3494 3495 // - For scanning we need to remove all width and precision fields and flags "%+3.7f" -> "%f". BUT don't strip types like "%I64d" which includes digits. ! "%07I64d" -> "%I64d" 3496 const char* ImParseFormatSanitizeForScanning(const char* fmt_in, char* fmt_out, size_t fmt_out_size) 3497 { 3498 const char* fmt_end = ImParseFormatFindEnd(fmt_in); 3499 const char* fmt_out_begin = fmt_out; 3500 IM_UNUSED(fmt_out_size); 3501 IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you! 3502 bool has_type = false; 3503 while (fmt_in < fmt_end) 3504 { 3505 char c = *fmt_in++; 3506 if (!has_type && ((c >= '0' && c <= '9') || c == '.' || c == '+' || c == '#')) 3507 continue; 3508 has_type |= ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')); // Stop skipping digits 3509 if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '. 3510 *(fmt_out++) = c; 3511 } 3512 *fmt_out = 0; // Zero-terminate 3513 return fmt_out_begin; 3514 } 3515 3516 template<typename TYPE> 3517 static const char* ImAtoi(const char* src, TYPE* output) 3518 { 3519 int negative = 0; 3520 if (*src == '-') { negative = 1; src++; } 3521 if (*src == '+') { src++; } 3522 TYPE v = 0; 3523 while (*src >= '0' && *src <= '9') 3524 v = (v * 10) + (*src++ - '0'); 3525 *output = negative ? -v : v; 3526 return src; 3527 } 3528 3529 // Parse display precision back from the display format string 3530 // FIXME: This is still used by some navigation code path to infer a minimum tweak step, but we should aim to rework widgets so it isn't needed. 3531 int ImParseFormatPrecision(const char* fmt, int default_precision) 3532 { 3533 fmt = ImParseFormatFindStart(fmt); 3534 if (fmt[0] != '%') 3535 return default_precision; 3536 fmt++; 3537 while (*fmt >= '0' && *fmt <= '9') 3538 fmt++; 3539 int precision = INT_MAX; 3540 if (*fmt == '.') 3541 { 3542 fmt = ImAtoi<int>(fmt + 1, &precision); 3543 if (precision < 0 || precision > 99) 3544 precision = default_precision; 3545 } 3546 if (*fmt == 'e' || *fmt == 'E') // Maximum precision with scientific notation 3547 precision = -1; 3548 if ((*fmt == 'g' || *fmt == 'G') && precision == INT_MAX) 3549 precision = -1; 3550 return (precision == INT_MAX) ? default_precision : precision; 3551 } 3552 3553 // Create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets) 3554 // FIXME: Facilitate using this in variety of other situations. 3555 bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags) 3556 { 3557 // On the first frame, g.TempInputTextId == 0, then on subsequent frames it becomes == id. 3558 // We clear ActiveID on the first frame to allow the InputText() taking it back. 3559 ImGuiContext& g = *GImGui; 3560 const bool init = (g.TempInputId != id); 3561 if (init) 3562 ClearActiveID(); 3563 3564 g.CurrentWindow->DC.CursorPos = bb.Min; 3565 bool value_changed = InputTextEx(label, NULL, buf, buf_size, bb.GetSize(), flags | ImGuiInputTextFlags_MergedItem); 3566 if (init) 3567 { 3568 // First frame we started displaying the InputText widget, we expect it to take the active id. 3569 IM_ASSERT(g.ActiveId == id); 3570 g.TempInputId = g.ActiveId; 3571 } 3572 return value_changed; 3573 } 3574 3575 // Note that Drag/Slider functions are only forwarding the min/max values clamping values if the ImGuiSliderFlags_AlwaysClamp flag is set! 3576 // This is intended: this way we allow CTRL+Click manual input to set a value out of bounds, for maximum flexibility. 3577 // However this may not be ideal for all uses, as some user code may break on out of bound values. 3578 bool ImGui::TempInputScalar(const ImRect& bb, ImGuiID id, const char* label, ImGuiDataType data_type, void* p_data, const char* format, const void* p_clamp_min, const void* p_clamp_max) 3579 { 3580 // FIXME: May need to clarify display behavior if format doesn't contain %. 3581 // "%d" -> "%d" / "There are %d items" -> "%d" / "items" -> "%d" (fallback). Also see #6405 3582 const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type); 3583 char fmt_buf[32]; 3584 char data_buf[32]; 3585 format = ImParseFormatTrimDecorations(format, fmt_buf, IM_ARRAYSIZE(fmt_buf)); 3586 if (format[0] == 0) 3587 format = type_info->PrintFmt; 3588 DataTypeFormatString(data_buf, IM_ARRAYSIZE(data_buf), data_type, p_data, format); 3589 ImStrTrimBlanks(data_buf); 3590 3591 ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_NoMarkEdited | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint; 3592 3593 bool value_changed = false; 3594 if (TempInputText(bb, id, label, data_buf, IM_ARRAYSIZE(data_buf), flags)) 3595 { 3596 // Backup old value 3597 size_t data_type_size = type_info->Size; 3598 ImGuiDataTypeStorage data_backup; 3599 memcpy(&data_backup, p_data, data_type_size); 3600 3601 // Apply new value (or operations) then clamp 3602 DataTypeApplyFromText(data_buf, data_type, p_data, format, NULL); 3603 if (p_clamp_min || p_clamp_max) 3604 { 3605 if (p_clamp_min && p_clamp_max && DataTypeCompare(data_type, p_clamp_min, p_clamp_max) > 0) 3606 ImSwap(p_clamp_min, p_clamp_max); 3607 DataTypeClamp(data_type, p_data, p_clamp_min, p_clamp_max); 3608 } 3609 3610 // Only mark as edited if new value is different 3611 value_changed = memcmp(&data_backup, p_data, data_type_size) != 0; 3612 if (value_changed) 3613 MarkItemEdited(id); 3614 } 3615 return value_changed; 3616 } 3617 3618 void ImGui::SetNextItemRefVal(ImGuiDataType data_type, void* p_data) 3619 { 3620 ImGuiContext& g = *GImGui; 3621 g.NextItemData.Flags |= ImGuiNextItemDataFlags_HasRefVal; 3622 memcpy(&g.NextItemData.RefVal, p_data, DataTypeGetInfo(data_type)->Size); 3623 } 3624 3625 // Note: p_data, p_step, p_step_fast are _pointers_ to a memory address holding the data. For an Input widget, p_step and p_step_fast are optional. 3626 // Read code of e.g. InputFloat(), InputInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly. 3627 bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_step, const void* p_step_fast, const char* format, ImGuiInputTextFlags flags) 3628 { 3629 ImGuiWindow* window = GetCurrentWindow(); 3630 if (window->SkipItems) 3631 return false; 3632 3633 ImGuiContext& g = *GImGui; 3634 ImGuiStyle& style = g.Style; 3635 3636 if (format == NULL) 3637 format = DataTypeGetInfo(data_type)->PrintFmt; 3638 3639 void* p_data_default = (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasRefVal) ? &g.NextItemData.RefVal : &g.DataTypeZeroValue; 3640 3641 char buf[64]; 3642 if ((flags & ImGuiInputTextFlags_DisplayEmptyRefVal) && DataTypeCompare(data_type, p_data, p_data_default) == 0) 3643 buf[0] = 0; 3644 else 3645 DataTypeFormatString(buf, IM_ARRAYSIZE(buf), data_type, p_data, format); 3646 3647 flags |= ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_NoMarkEdited; // We call MarkItemEdited() ourselves by comparing the actual data rather than the string. 3648 flags |= (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint; 3649 3650 bool value_changed = false; 3651 if (p_step == NULL) 3652 { 3653 if (InputText(label, buf, IM_ARRAYSIZE(buf), flags)) 3654 value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL); 3655 } 3656 else 3657 { 3658 const float button_size = GetFrameHeight(); 3659 3660 BeginGroup(); // The only purpose of the group here is to allow the caller to query item data e.g. IsItemActive() 3661 PushID(label); 3662 SetNextItemWidth(ImMax(1.0f, CalcItemWidth() - (button_size + style.ItemInnerSpacing.x) * 2)); 3663 if (InputText("", buf, IM_ARRAYSIZE(buf), flags)) // PushId(label) + "" gives us the expected ID from outside point of view 3664 value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL); 3665 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable); 3666 3667 // Step buttons 3668 const ImVec2 backup_frame_padding = style.FramePadding; 3669 style.FramePadding.x = style.FramePadding.y; 3670 ImGuiButtonFlags button_flags = ImGuiButtonFlags_Repeat | ImGuiButtonFlags_DontClosePopups; 3671 if (flags & ImGuiInputTextFlags_ReadOnly) 3672 BeginDisabled(); 3673 SameLine(0, style.ItemInnerSpacing.x); 3674 if (ButtonEx("-", ImVec2(button_size, button_size), button_flags)) 3675 { 3676 DataTypeApplyOp(data_type, '-', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step); 3677 value_changed = true; 3678 } 3679 SameLine(0, style.ItemInnerSpacing.x); 3680 if (ButtonEx("+", ImVec2(button_size, button_size), button_flags)) 3681 { 3682 DataTypeApplyOp(data_type, '+', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step); 3683 value_changed = true; 3684 } 3685 if (flags & ImGuiInputTextFlags_ReadOnly) 3686 EndDisabled(); 3687 3688 const char* label_end = FindRenderedTextEnd(label); 3689 if (label != label_end) 3690 { 3691 SameLine(0, style.ItemInnerSpacing.x); 3692 TextEx(label, label_end); 3693 } 3694 style.FramePadding = backup_frame_padding; 3695 3696 PopID(); 3697 EndGroup(); 3698 } 3699 if (value_changed) 3700 MarkItemEdited(g.LastItemData.ID); 3701 3702 return value_changed; 3703 } 3704 3705 bool ImGui::InputScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_step, const void* p_step_fast, const char* format, ImGuiInputTextFlags flags) 3706 { 3707 ImGuiWindow* window = GetCurrentWindow(); 3708 if (window->SkipItems) 3709 return false; 3710 3711 ImGuiContext& g = *GImGui; 3712 bool value_changed = false; 3713 BeginGroup(); 3714 PushID(label); 3715 PushMultiItemsWidths(components, CalcItemWidth()); 3716 size_t type_size = GDataTypeInfo[data_type].Size; 3717 for (int i = 0; i < components; i++) 3718 { 3719 PushID(i); 3720 if (i > 0) 3721 SameLine(0, g.Style.ItemInnerSpacing.x); 3722 value_changed |= InputScalar("", data_type, p_data, p_step, p_step_fast, format, flags); 3723 PopID(); 3724 PopItemWidth(); 3725 p_data = (void*)((char*)p_data + type_size); 3726 } 3727 PopID(); 3728 3729 const char* label_end = FindRenderedTextEnd(label); 3730 if (label != label_end) 3731 { 3732 SameLine(0.0f, g.Style.ItemInnerSpacing.x); 3733 TextEx(label, label_end); 3734 } 3735 3736 EndGroup(); 3737 return value_changed; 3738 } 3739 3740 bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, const char* format, ImGuiInputTextFlags flags) 3741 { 3742 return InputScalar(label, ImGuiDataType_Float, (void*)v, (void*)(step > 0.0f ? &step : NULL), (void*)(step_fast > 0.0f ? &step_fast : NULL), format, flags); 3743 } 3744 3745 bool ImGui::InputFloat2(const char* label, float v[2], const char* format, ImGuiInputTextFlags flags) 3746 { 3747 return InputScalarN(label, ImGuiDataType_Float, v, 2, NULL, NULL, format, flags); 3748 } 3749 3750 bool ImGui::InputFloat3(const char* label, float v[3], const char* format, ImGuiInputTextFlags flags) 3751 { 3752 return InputScalarN(label, ImGuiDataType_Float, v, 3, NULL, NULL, format, flags); 3753 } 3754 3755 bool ImGui::InputFloat4(const char* label, float v[4], const char* format, ImGuiInputTextFlags flags) 3756 { 3757 return InputScalarN(label, ImGuiDataType_Float, v, 4, NULL, NULL, format, flags); 3758 } 3759 3760 bool ImGui::InputInt(const char* label, int* v, int step, int step_fast, ImGuiInputTextFlags flags) 3761 { 3762 // Hexadecimal input provided as a convenience but the flag name is awkward. Typically you'd use InputText() to parse your own data, if you want to handle prefixes. 3763 const char* format = (flags & ImGuiInputTextFlags_CharsHexadecimal) ? "%08X" : "%d"; 3764 return InputScalar(label, ImGuiDataType_S32, (void*)v, (void*)(step > 0 ? &step : NULL), (void*)(step_fast > 0 ? &step_fast : NULL), format, flags); 3765 } 3766 3767 bool ImGui::InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags) 3768 { 3769 return InputScalarN(label, ImGuiDataType_S32, v, 2, NULL, NULL, "%d", flags); 3770 } 3771 3772 bool ImGui::InputInt3(const char* label, int v[3], ImGuiInputTextFlags flags) 3773 { 3774 return InputScalarN(label, ImGuiDataType_S32, v, 3, NULL, NULL, "%d", flags); 3775 } 3776 3777 bool ImGui::InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags) 3778 { 3779 return InputScalarN(label, ImGuiDataType_S32, v, 4, NULL, NULL, "%d", flags); 3780 } 3781 3782 bool ImGui::InputDouble(const char* label, double* v, double step, double step_fast, const char* format, ImGuiInputTextFlags flags) 3783 { 3784 return InputScalar(label, ImGuiDataType_Double, (void*)v, (void*)(step > 0.0 ? &step : NULL), (void*)(step_fast > 0.0 ? &step_fast : NULL), format, flags); 3785 } 3786 3787 //------------------------------------------------------------------------- 3788 // [SECTION] Widgets: InputText, InputTextMultiline, InputTextWithHint 3789 //------------------------------------------------------------------------- 3790 // - InputText() 3791 // - InputTextWithHint() 3792 // - InputTextMultiline() 3793 // - InputTextGetCharInfo() [Internal] 3794 // - InputTextReindexLines() [Internal] 3795 // - InputTextReindexLinesRange() [Internal] 3796 // - InputTextEx() [Internal] 3797 // - DebugNodeInputTextState() [Internal] 3798 //------------------------------------------------------------------------- 3799 3800 bool ImGui::InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) 3801 { 3802 IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() 3803 return InputTextEx(label, NULL, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data); 3804 } 3805 3806 bool ImGui::InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) 3807 { 3808 return InputTextEx(label, NULL, buf, (int)buf_size, size, flags | ImGuiInputTextFlags_Multiline, callback, user_data); 3809 } 3810 3811 bool ImGui::InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) 3812 { 3813 IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() or InputTextEx() manually if you need multi-line + hint. 3814 return InputTextEx(label, hint, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data); 3815 } 3816 3817 static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end) 3818 { 3819 int line_count = 0; 3820 const char* s = text_begin; 3821 while (char c = *s++) // We are only matching for \n so we can ignore UTF-8 decoding 3822 if (c == '\n') 3823 line_count++; 3824 s--; 3825 if (s[0] != '\n' && s[0] != '\r') 3826 line_count++; 3827 *out_text_end = s; 3828 return line_count; 3829 } 3830 3831 static ImVec2 InputTextCalcTextSizeW(ImGuiContext* ctx, const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining, ImVec2* out_offset, bool stop_on_new_line) 3832 { 3833 ImGuiContext& g = *ctx; 3834 ImFont* font = g.Font; 3835 const float line_height = g.FontSize; 3836 const float scale = line_height / font->FontSize; 3837 3838 ImVec2 text_size = ImVec2(0, 0); 3839 float line_width = 0.0f; 3840 3841 const ImWchar* s = text_begin; 3842 while (s < text_end) 3843 { 3844 unsigned int c = (unsigned int)(*s++); 3845 if (c == '\n') 3846 { 3847 text_size.x = ImMax(text_size.x, line_width); 3848 text_size.y += line_height; 3849 line_width = 0.0f; 3850 if (stop_on_new_line) 3851 break; 3852 continue; 3853 } 3854 if (c == '\r') 3855 continue; 3856 3857 const float char_width = font->GetCharAdvance((ImWchar)c) * scale; 3858 line_width += char_width; 3859 } 3860 3861 if (text_size.x < line_width) 3862 text_size.x = line_width; 3863 3864 if (out_offset) 3865 *out_offset = ImVec2(line_width, text_size.y + line_height); // offset allow for the possibility of sitting after a trailing \n 3866 3867 if (line_width > 0 || text_size.y == 0.0f) // whereas size.y will ignore the trailing \n 3868 text_size.y += line_height; 3869 3870 if (remaining) 3871 *remaining = s; 3872 3873 return text_size; 3874 } 3875 3876 // 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) 3877 namespace ImStb 3878 { 3879 3880 static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->CurLenW; } 3881 static ImWchar STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { IM_ASSERT(idx <= obj->CurLenW); return obj->TextW[idx]; } 3882 static float STB_TEXTEDIT_GETWIDTH(ImGuiInputTextState* obj, int line_start_idx, int char_idx) { ImWchar c = obj->TextW[line_start_idx + char_idx]; if (c == '\n') return IMSTB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *obj->Ctx; return g.Font->GetCharAdvance(c) * g.FontScale; } 3883 static int STB_TEXTEDIT_KEYTOTEXT(int key) { return key >= 0x200000 ? 0 : key; } 3884 static ImWchar STB_TEXTEDIT_NEWLINE = '\n'; 3885 static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx) 3886 { 3887 const ImWchar* text = obj->TextW.Data; 3888 const ImWchar* text_remaining = NULL; 3889 const ImVec2 size = InputTextCalcTextSizeW(obj->Ctx, text + line_start_idx, text + obj->CurLenW, &text_remaining, NULL, true); 3890 r->x0 = 0.0f; 3891 r->x1 = size.x; 3892 r->baseline_y_delta = size.y; 3893 r->ymin = 0.0f; 3894 r->ymax = size.y; 3895 r->num_chars = (int)(text_remaining - (text + line_start_idx)); 3896 } 3897 3898 static bool is_separator(unsigned int c) 3899 { 3900 return c==',' || c==';' || c=='(' || c==')' || c=='{' || c=='}' || c=='[' || c==']' || c=='|' || c=='\n' || c=='\r' || c=='.' || c=='!' || c=='\\' || c=='/'; 3901 } 3902 3903 static int is_word_boundary_from_right(ImGuiInputTextState* obj, int idx) 3904 { 3905 // When ImGuiInputTextFlags_Password is set, we don't want actions such as CTRL+Arrow to leak the fact that underlying data are blanks or separators. 3906 if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0) 3907 return 0; 3908 3909 bool prev_white = ImCharIsBlankW(obj->TextW[idx - 1]); 3910 bool prev_separ = is_separator(obj->TextW[idx - 1]); 3911 bool curr_white = ImCharIsBlankW(obj->TextW[idx]); 3912 bool curr_separ = is_separator(obj->TextW[idx]); 3913 return ((prev_white || prev_separ) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ); 3914 } 3915 static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx) 3916 { 3917 if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0) 3918 return 0; 3919 3920 bool prev_white = ImCharIsBlankW(obj->TextW[idx]); 3921 bool prev_separ = is_separator(obj->TextW[idx]); 3922 bool curr_white = ImCharIsBlankW(obj->TextW[idx - 1]); 3923 bool curr_separ = is_separator(obj->TextW[idx - 1]); 3924 return ((prev_white) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ); 3925 } 3926 static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(ImGuiInputTextState* obj, int idx) { idx--; while (idx >= 0 && !is_word_boundary_from_right(obj, idx)) idx--; return idx < 0 ? 0 : idx; } 3927 static int STB_TEXTEDIT_MOVEWORDRIGHT_MAC(ImGuiInputTextState* obj, int idx) { idx++; int len = obj->CurLenW; while (idx < len && !is_word_boundary_from_left(obj, idx)) idx++; return idx > len ? len : idx; } 3928 static int STB_TEXTEDIT_MOVEWORDRIGHT_WIN(ImGuiInputTextState* obj, int idx) { idx++; int len = obj->CurLenW; while (idx < len && !is_word_boundary_from_right(obj, idx)) idx++; return idx > len ? len : idx; } 3929 static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(ImGuiInputTextState* obj, int idx) { ImGuiContext& g = *obj->Ctx; if (g.IO.ConfigMacOSXBehaviors) return STB_TEXTEDIT_MOVEWORDRIGHT_MAC(obj, idx); else return STB_TEXTEDIT_MOVEWORDRIGHT_WIN(obj, idx); } 3930 #define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h 3931 #define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL 3932 3933 static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n) 3934 { 3935 ImWchar* dst = obj->TextW.Data + pos; 3936 3937 // We maintain our buffer length in both UTF-8 and wchar formats 3938 obj->Edited = true; 3939 obj->CurLenA -= ImTextCountUtf8BytesFromStr(dst, dst + n); 3940 obj->CurLenW -= n; 3941 3942 // Offset remaining text (FIXME-OPT: Use memmove) 3943 const ImWchar* src = obj->TextW.Data + pos + n; 3944 while (ImWchar c = *src++) 3945 *dst++ = c; 3946 *dst = '\0'; 3947 } 3948 3949 static bool STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const ImWchar* new_text, int new_text_len) 3950 { 3951 const bool is_resizable = (obj->Flags & ImGuiInputTextFlags_CallbackResize) != 0; 3952 const int text_len = obj->CurLenW; 3953 IM_ASSERT(pos <= text_len); 3954 3955 const int new_text_len_utf8 = ImTextCountUtf8BytesFromStr(new_text, new_text + new_text_len); 3956 if (!is_resizable && (new_text_len_utf8 + obj->CurLenA + 1 > obj->BufCapacityA)) 3957 return false; 3958 3959 // Grow internal buffer if needed 3960 if (new_text_len + text_len + 1 > obj->TextW.Size) 3961 { 3962 if (!is_resizable) 3963 return false; 3964 IM_ASSERT(text_len < obj->TextW.Size); 3965 obj->TextW.resize(text_len + ImClamp(new_text_len * 4, 32, ImMax(256, new_text_len)) + 1); 3966 } 3967 3968 ImWchar* text = obj->TextW.Data; 3969 if (pos != text_len) 3970 memmove(text + pos + new_text_len, text + pos, (size_t)(text_len - pos) * sizeof(ImWchar)); 3971 memcpy(text + pos, new_text, (size_t)new_text_len * sizeof(ImWchar)); 3972 3973 obj->Edited = true; 3974 obj->CurLenW += new_text_len; 3975 obj->CurLenA += new_text_len_utf8; 3976 obj->TextW[obj->CurLenW] = '\0'; 3977 3978 return true; 3979 } 3980 3981 // We don't use an enum so we can build even with conflicting symbols (if another user of stb_textedit.h leak their STB_TEXTEDIT_K_* symbols) 3982 #define STB_TEXTEDIT_K_LEFT 0x200000 // keyboard input to move cursor left 3983 #define STB_TEXTEDIT_K_RIGHT 0x200001 // keyboard input to move cursor right 3984 #define STB_TEXTEDIT_K_UP 0x200002 // keyboard input to move cursor up 3985 #define STB_TEXTEDIT_K_DOWN 0x200003 // keyboard input to move cursor down 3986 #define STB_TEXTEDIT_K_LINESTART 0x200004 // keyboard input to move cursor to start of line 3987 #define STB_TEXTEDIT_K_LINEEND 0x200005 // keyboard input to move cursor to end of line 3988 #define STB_TEXTEDIT_K_TEXTSTART 0x200006 // keyboard input to move cursor to start of text 3989 #define STB_TEXTEDIT_K_TEXTEND 0x200007 // keyboard input to move cursor to end of text 3990 #define STB_TEXTEDIT_K_DELETE 0x200008 // keyboard input to delete selection or character under cursor 3991 #define STB_TEXTEDIT_K_BACKSPACE 0x200009 // keyboard input to delete selection or character left of cursor 3992 #define STB_TEXTEDIT_K_UNDO 0x20000A // keyboard input to perform undo 3993 #define STB_TEXTEDIT_K_REDO 0x20000B // keyboard input to perform redo 3994 #define STB_TEXTEDIT_K_WORDLEFT 0x20000C // keyboard input to move cursor left one word 3995 #define STB_TEXTEDIT_K_WORDRIGHT 0x20000D // keyboard input to move cursor right one word 3996 #define STB_TEXTEDIT_K_PGUP 0x20000E // keyboard input to move cursor up a page 3997 #define STB_TEXTEDIT_K_PGDOWN 0x20000F // keyboard input to move cursor down a page 3998 #define STB_TEXTEDIT_K_SHIFT 0x400000 3999 4000 #define IMSTB_TEXTEDIT_IMPLEMENTATION 4001 #define IMSTB_TEXTEDIT_memmove memmove 4002 #include "imstb_textedit.h" 4003 4004 // stb_textedit internally allows for a single undo record to do addition and deletion, but somehow, calling 4005 // the stb_textedit_paste() function creates two separate records, so we perform it manually. (FIXME: Report to nothings/stb?) 4006 static void stb_textedit_replace(ImGuiInputTextState* str, STB_TexteditState* state, const IMSTB_TEXTEDIT_CHARTYPE* text, int text_len) 4007 { 4008 stb_text_makeundo_replace(str, state, 0, str->CurLenW, text_len); 4009 ImStb::STB_TEXTEDIT_DELETECHARS(str, 0, str->CurLenW); 4010 state->cursor = state->select_start = state->select_end = 0; 4011 if (text_len <= 0) 4012 return; 4013 if (ImStb::STB_TEXTEDIT_INSERTCHARS(str, 0, text, text_len)) 4014 { 4015 state->cursor = state->select_start = state->select_end = text_len; 4016 state->has_preferred_x = 0; 4017 return; 4018 } 4019 IM_ASSERT(0); // Failed to insert character, normally shouldn't happen because of how we currently use stb_textedit_replace() 4020 } 4021 4022 } // namespace ImStb 4023 4024 void ImGuiInputTextState::OnKeyPressed(int key) 4025 { 4026 stb_textedit_key(this, &Stb, key); 4027 CursorFollow = true; 4028 CursorAnimReset(); 4029 } 4030 4031 ImGuiInputTextCallbackData::ImGuiInputTextCallbackData() 4032 { 4033 memset(this, 0, sizeof(*this)); 4034 } 4035 4036 // Public API to manipulate UTF-8 text 4037 // We expose UTF-8 to the user (unlike the STB_TEXTEDIT_* functions which are manipulating wchar) 4038 // FIXME: The existence of this rarely exercised code path is a bit of a nuisance. 4039 void ImGuiInputTextCallbackData::DeleteChars(int pos, int bytes_count) 4040 { 4041 IM_ASSERT(pos + bytes_count <= BufTextLen); 4042 char* dst = Buf + pos; 4043 const char* src = Buf + pos + bytes_count; 4044 while (char c = *src++) 4045 *dst++ = c; 4046 *dst = '\0'; 4047 4048 if (CursorPos >= pos + bytes_count) 4049 CursorPos -= bytes_count; 4050 else if (CursorPos >= pos) 4051 CursorPos = pos; 4052 SelectionStart = SelectionEnd = CursorPos; 4053 BufDirty = true; 4054 BufTextLen -= bytes_count; 4055 } 4056 4057 void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, const char* new_text_end) 4058 { 4059 // Accept null ranges 4060 if (new_text == new_text_end) 4061 return; 4062 4063 const bool is_resizable = (Flags & ImGuiInputTextFlags_CallbackResize) != 0; 4064 const int new_text_len = new_text_end ? (int)(new_text_end - new_text) : (int)strlen(new_text); 4065 if (new_text_len + BufTextLen >= BufSize) 4066 { 4067 if (!is_resizable) 4068 return; 4069 4070 // Contrary to STB_TEXTEDIT_INSERTCHARS() this is working in the UTF8 buffer, hence the mildly similar code (until we remove the U16 buffer altogether!) 4071 ImGuiContext& g = *Ctx; 4072 ImGuiInputTextState* edit_state = &g.InputTextState; 4073 IM_ASSERT(edit_state->ID != 0 && g.ActiveId == edit_state->ID); 4074 IM_ASSERT(Buf == edit_state->TextA.Data); 4075 int new_buf_size = BufTextLen + ImClamp(new_text_len * 4, 32, ImMax(256, new_text_len)) + 1; 4076 edit_state->TextA.reserve(new_buf_size + 1); 4077 Buf = edit_state->TextA.Data; 4078 BufSize = edit_state->BufCapacityA = new_buf_size; 4079 } 4080 4081 if (BufTextLen != pos) 4082 memmove(Buf + pos + new_text_len, Buf + pos, (size_t)(BufTextLen - pos)); 4083 memcpy(Buf + pos, new_text, (size_t)new_text_len * sizeof(char)); 4084 Buf[BufTextLen + new_text_len] = '\0'; 4085 4086 if (CursorPos >= pos) 4087 CursorPos += new_text_len; 4088 SelectionStart = SelectionEnd = CursorPos; 4089 BufDirty = true; 4090 BufTextLen += new_text_len; 4091 } 4092 4093 // Return false to discard a character. 4094 static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard) 4095 { 4096 unsigned int c = *p_char; 4097 4098 // Filter non-printable (NB: isprint is unreliable! see #2467) 4099 bool apply_named_filters = true; 4100 if (c < 0x20) 4101 { 4102 bool pass = false; 4103 pass |= (c == '\n') && (flags & ImGuiInputTextFlags_Multiline) != 0; // Note that an Enter KEY will emit \r and be ignored (we poll for KEY in InputText() code) 4104 pass |= (c == '\t') && (flags & ImGuiInputTextFlags_AllowTabInput) != 0; 4105 if (!pass) 4106 return false; 4107 apply_named_filters = false; // Override named filters below so newline and tabs can still be inserted. 4108 } 4109 4110 if (input_source_is_clipboard == false) 4111 { 4112 // We ignore Ascii representation of delete (emitted from Backspace on OSX, see #2578, #2817) 4113 if (c == 127) 4114 return false; 4115 4116 // Filter private Unicode range. GLFW on OSX seems to send private characters for special keys like arrow keys (FIXME) 4117 if (c >= 0xE000 && c <= 0xF8FF) 4118 return false; 4119 } 4120 4121 // Filter Unicode ranges we are not handling in this build 4122 if (c > IM_UNICODE_CODEPOINT_MAX) 4123 return false; 4124 4125 // Generic named filters 4126 if (apply_named_filters && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint))) 4127 { 4128 // The libc allows overriding locale, with e.g. 'setlocale(LC_NUMERIC, "de_DE.UTF-8");' which affect the output/input of printf/scanf to use e.g. ',' instead of '.'. 4129 // The standard mandate that programs starts in the "C" locale where the decimal point is '.'. 4130 // We don't really intend to provide widespread support for it, but out of empathy for people stuck with using odd API, we support the bare minimum aka overriding the decimal point. 4131 // Change the default decimal_point with: 4132 // ImGui::GetIO()->PlatformLocaleDecimalPoint = *localeconv()->decimal_point; 4133 // Users of non-default decimal point (in particular ',') may be affected by word-selection logic (is_word_boundary_from_right/is_word_boundary_from_left) functions. 4134 ImGuiContext& g = *ctx; 4135 const unsigned c_decimal_point = (unsigned int)g.IO.PlatformLocaleDecimalPoint; 4136 if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint)) 4137 if (c == '.' || c == ',') 4138 c = c_decimal_point; 4139 4140 // Full-width -> half-width conversion for numeric fields (https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block) 4141 // While this is mostly convenient, this has the side-effect for uninformed users accidentally inputting full-width characters that they may 4142 // scratch their head as to why it works in numerical fields vs in generic text fields it would require support in the font. 4143 if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | ImGuiInputTextFlags_CharsHexadecimal)) 4144 if (c >= 0xFF01 && c <= 0xFF5E) 4145 c = c - 0xFF01 + 0x21; 4146 4147 // Allow 0-9 . - + * / 4148 if (flags & ImGuiInputTextFlags_CharsDecimal) 4149 if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/')) 4150 return false; 4151 4152 // Allow 0-9 . - + * / e E 4153 if (flags & ImGuiInputTextFlags_CharsScientific) 4154 if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/') && (c != 'e') && (c != 'E')) 4155 return false; 4156 4157 // Allow 0-9 a-F A-F 4158 if (flags & ImGuiInputTextFlags_CharsHexadecimal) 4159 if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F')) 4160 return false; 4161 4162 // Turn a-z into A-Z 4163 if (flags & ImGuiInputTextFlags_CharsUppercase) 4164 if (c >= 'a' && c <= 'z') 4165 c += (unsigned int)('A' - 'a'); 4166 4167 if (flags & ImGuiInputTextFlags_CharsNoBlank) 4168 if (ImCharIsBlankW(c)) 4169 return false; 4170 4171 *p_char = c; 4172 } 4173 4174 // Custom callback filter 4175 if (flags & ImGuiInputTextFlags_CallbackCharFilter) 4176 { 4177 ImGuiContext& g = *GImGui; 4178 ImGuiInputTextCallbackData callback_data; 4179 callback_data.Ctx = &g; 4180 callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter; 4181 callback_data.EventChar = (ImWchar)c; 4182 callback_data.Flags = flags; 4183 callback_data.UserData = user_data; 4184 if (callback(&callback_data) != 0) 4185 return false; 4186 *p_char = callback_data.EventChar; 4187 if (!callback_data.EventChar) 4188 return false; 4189 } 4190 4191 return true; 4192 } 4193 4194 // Find the shortest single replacement we can make to get the new text from the old text. 4195 // Important: needs to be run before TextW is rewritten with the new characters because calling STB_TEXTEDIT_GETCHAR() at the end. 4196 // FIXME: Ideally we should transition toward (1) making InsertChars()/DeleteChars() update undo-stack (2) discourage (and keep reconcile) or obsolete (and remove reconcile) accessing buffer directly. 4197 static void InputTextReconcileUndoStateAfterUserCallback(ImGuiInputTextState* state, const char* new_buf_a, int new_length_a) 4198 { 4199 ImGuiContext& g = *GImGui; 4200 const ImWchar* old_buf = state->TextW.Data; 4201 const int old_length = state->CurLenW; 4202 const int new_length = ImTextCountCharsFromUtf8(new_buf_a, new_buf_a + new_length_a); 4203 g.TempBuffer.reserve_discard((new_length + 1) * sizeof(ImWchar)); 4204 ImWchar* new_buf = (ImWchar*)(void*)g.TempBuffer.Data; 4205 ImTextStrFromUtf8(new_buf, new_length + 1, new_buf_a, new_buf_a + new_length_a); 4206 4207 const int shorter_length = ImMin(old_length, new_length); 4208 int first_diff; 4209 for (first_diff = 0; first_diff < shorter_length; first_diff++) 4210 if (old_buf[first_diff] != new_buf[first_diff]) 4211 break; 4212 if (first_diff == old_length && first_diff == new_length) 4213 return; 4214 4215 int old_last_diff = old_length - 1; 4216 int new_last_diff = new_length - 1; 4217 for (; old_last_diff >= first_diff && new_last_diff >= first_diff; old_last_diff--, new_last_diff--) 4218 if (old_buf[old_last_diff] != new_buf[new_last_diff]) 4219 break; 4220 4221 const int insert_len = new_last_diff - first_diff + 1; 4222 const int delete_len = old_last_diff - first_diff + 1; 4223 if (insert_len > 0 || delete_len > 0) 4224 if (IMSTB_TEXTEDIT_CHARTYPE* p = stb_text_createundo(&state->Stb.undostate, first_diff, delete_len, insert_len)) 4225 for (int i = 0; i < delete_len; i++) 4226 p[i] = ImStb::STB_TEXTEDIT_GETCHAR(state, first_diff + i); 4227 } 4228 4229 // As InputText() retain textual data and we currently provide a path for user to not retain it (via local variables) 4230 // we need some form of hook to reapply data back to user buffer on deactivation frame. (#4714) 4231 // It would be more desirable that we discourage users from taking advantage of the "user not retaining data" trick, 4232 // but that more likely be attractive when we do have _NoLiveEdit flag available. 4233 void ImGui::InputTextDeactivateHook(ImGuiID id) 4234 { 4235 ImGuiContext& g = *GImGui; 4236 ImGuiInputTextState* state = &g.InputTextState; 4237 if (id == 0 || state->ID != id) 4238 return; 4239 g.InputTextDeactivatedState.ID = state->ID; 4240 if (state->Flags & ImGuiInputTextFlags_ReadOnly) 4241 { 4242 g.InputTextDeactivatedState.TextA.resize(0); // In theory this data won't be used, but clear to be neat. 4243 } 4244 else 4245 { 4246 IM_ASSERT(state->TextA.Data != 0); 4247 g.InputTextDeactivatedState.TextA.resize(state->CurLenA + 1); 4248 memcpy(g.InputTextDeactivatedState.TextA.Data, state->TextA.Data, state->CurLenA + 1); 4249 } 4250 } 4251 4252 // Edit a string of text 4253 // - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!". 4254 // This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match 4255 // Note that in std::string world, capacity() would omit 1 byte used by the zero-terminator. 4256 // - When active, hold on a privately held copy of the text (and apply back to 'buf'). So changing 'buf' while the InputText is active has no effect. 4257 // - If you want to use ImGui::InputText() with std::string, see misc/cpp/imgui_stdlib.h 4258 // (FIXME: Rather confusing and messy function, among the worse part of our codebase, expecting to rewrite a V2 at some point.. Partly because we are 4259 // doing UTF8 > U16 > UTF8 conversions on the go to easily interface with stb_textedit. Ideally should stay in UTF-8 all the time. See https://github.com/nothings/stb/issues/188) 4260 bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_size, const ImVec2& size_arg, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* callback_user_data) 4261 { 4262 ImGuiWindow* window = GetCurrentWindow(); 4263 if (window->SkipItems) 4264 return false; 4265 4266 IM_ASSERT(buf != NULL && buf_size >= 0); 4267 IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys) 4268 IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key) 4269 4270 ImGuiContext& g = *GImGui; 4271 ImGuiIO& io = g.IO; 4272 const ImGuiStyle& style = g.Style; 4273 4274 const bool RENDER_SELECTION_WHEN_INACTIVE = false; 4275 const bool is_multiline = (flags & ImGuiInputTextFlags_Multiline) != 0; 4276 4277 if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope (including the scrollbar) 4278 BeginGroup(); 4279 const ImGuiID id = window->GetID(label); 4280 const ImVec2 label_size = CalcTextSize(label, NULL, true); 4281 const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), (is_multiline ? g.FontSize * 8.0f : label_size.y) + style.FramePadding.y * 2.0f); // Arbitrary default of 8 lines high for multi-line 4282 const ImVec2 total_size = ImVec2(frame_size.x + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), frame_size.y); 4283 4284 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size); 4285 const ImRect total_bb(frame_bb.Min, frame_bb.Min + total_size); 4286 4287 ImGuiWindow* draw_window = window; 4288 ImVec2 inner_size = frame_size; 4289 ImGuiLastItemData item_data_backup; 4290 if (is_multiline) 4291 { 4292 ImVec2 backup_pos = window->DC.CursorPos; 4293 ItemSize(total_bb, style.FramePadding.y); 4294 if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable)) 4295 { 4296 EndGroup(); 4297 return false; 4298 } 4299 item_data_backup = g.LastItemData; 4300 window->DC.CursorPos = backup_pos; 4301 4302 // Prevent NavActivation from Tabbing when our widget accepts Tab inputs: this allows cycling through widgets without stopping. 4303 if (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_FromTabbing) && (flags & ImGuiInputTextFlags_AllowTabInput)) 4304 g.NavActivateId = 0; 4305 4306 // Prevent NavActivate reactivating in BeginChild() when we are already active. 4307 const ImGuiID backup_activate_id = g.NavActivateId; 4308 if (g.ActiveId == id) // Prevent reactivation 4309 g.NavActivateId = 0; 4310 4311 // We reproduce the contents of BeginChildFrame() in order to provide 'label' so our window internal data are easier to read/debug. 4312 PushStyleColor(ImGuiCol_ChildBg, style.Colors[ImGuiCol_FrameBg]); 4313 PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding); 4314 PushStyleVar(ImGuiStyleVar_ChildBorderSize, style.FrameBorderSize); 4315 PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); // Ensure no clip rect so mouse hover can reach FramePadding edges 4316 bool child_visible = BeginChildEx(label, id, frame_bb.GetSize(), ImGuiChildFlags_Border, ImGuiWindowFlags_NoMove); 4317 g.NavActivateId = backup_activate_id; 4318 PopStyleVar(3); 4319 PopStyleColor(); 4320 if (!child_visible) 4321 { 4322 EndChild(); 4323 EndGroup(); 4324 return false; 4325 } 4326 draw_window = g.CurrentWindow; // Child window 4327 draw_window->DC.NavLayersActiveMaskNext |= (1 << draw_window->DC.NavLayerCurrent); // This is to ensure that EndChild() will display a navigation highlight so we can "enter" into it. 4328 draw_window->DC.CursorPos += style.FramePadding; 4329 inner_size.x -= draw_window->ScrollbarSizes.x; 4330 } 4331 else 4332 { 4333 // Support for internal ImGuiInputTextFlags_MergedItem flag, which could be redesigned as an ItemFlags if needed (with test performed in ItemAdd) 4334 ItemSize(total_bb, style.FramePadding.y); 4335 if (!(flags & ImGuiInputTextFlags_MergedItem)) 4336 if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable)) 4337 return false; 4338 } 4339 const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.InFlags); 4340 if (hovered) 4341 g.MouseCursor = ImGuiMouseCursor_TextInput; 4342 4343 // We are only allowed to access the state if we are already the active widget. 4344 ImGuiInputTextState* state = GetInputTextState(id); 4345 4346 if (g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly) 4347 flags |= ImGuiInputTextFlags_ReadOnly; 4348 const bool is_readonly = (flags & ImGuiInputTextFlags_ReadOnly) != 0; 4349 const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0; 4350 const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0; 4351 const bool is_resizable = (flags & ImGuiInputTextFlags_CallbackResize) != 0; 4352 if (is_resizable) 4353 IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag! 4354 4355 const bool input_requested_by_nav = (g.ActiveId != id) && ((g.NavActivateId == id) && ((g.NavActivateFlags & ImGuiActivateFlags_PreferInput) || (g.NavInputSource == ImGuiInputSource_Keyboard || g.NavInputSource == ImGuiInputSource_Gamepad))); 4356 4357 const bool user_clicked = hovered && io.MouseClicked[0]; 4358 const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == GetWindowScrollbarID(draw_window, ImGuiAxis_Y); 4359 const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == GetWindowScrollbarID(draw_window, ImGuiAxis_Y); 4360 bool clear_active_id = false; 4361 bool select_all = false; 4362 4363 float scroll_y = is_multiline ? draw_window->Scroll.y : FLT_MAX; 4364 4365 const bool init_reload_from_user_buf = (state != NULL && state->ReloadUserBuf); 4366 const bool init_changed_specs = (state != NULL && state->Stb.single_line != !is_multiline); // state != NULL means its our state. 4367 const bool init_make_active = (user_clicked || user_scroll_finish || input_requested_by_nav); 4368 const bool init_state = (init_make_active || user_scroll_active); 4369 if ((init_state && g.ActiveId != id) || init_changed_specs || init_reload_from_user_buf) 4370 { 4371 // Access state even if we don't own it yet. 4372 state = &g.InputTextState; 4373 state->CursorAnimReset(); 4374 state->ReloadUserBuf = false; 4375 4376 // Backup state of deactivating item so they'll have a chance to do a write to output buffer on the same frame they report IsItemDeactivatedAfterEdit (#4714) 4377 InputTextDeactivateHook(state->ID); 4378 4379 // From the moment we focused we are normally ignoring the content of 'buf' (unless we are in read-only mode) 4380 const int buf_len = (int)strlen(buf); 4381 if (!init_reload_from_user_buf) 4382 { 4383 // Take a copy of the initial buffer value. 4384 state->InitialTextA.resize(buf_len + 1); // UTF-8. we use +1 to make sure that .Data is always pointing to at least an empty string. 4385 memcpy(state->InitialTextA.Data, buf, buf_len + 1); 4386 } 4387 4388 // Preserve cursor position and undo/redo stack if we come back to same widget 4389 // FIXME: Since we reworked this on 2022/06, may want to differentiate recycle_cursor vs recycle_undostate? 4390 bool recycle_state = (state->ID == id && !init_changed_specs && !init_reload_from_user_buf); 4391 if (recycle_state && (state->CurLenA != buf_len || (state->TextAIsValid && strncmp(state->TextA.Data, buf, buf_len) != 0))) 4392 recycle_state = false; 4393 4394 // Start edition 4395 const char* buf_end = NULL; 4396 state->ID = id; 4397 state->TextW.resize(buf_size + 1); // wchar count <= UTF-8 count. we use +1 to make sure that .Data is always pointing to at least an empty string. 4398 state->TextA.resize(0); 4399 state->TextAIsValid = false; // TextA is not valid yet (we will display buf until then) 4400 state->CurLenW = ImTextStrFromUtf8(state->TextW.Data, buf_size, buf, NULL, &buf_end); 4401 state->CurLenA = (int)(buf_end - buf); // We can't get the result from ImStrncpy() above because it is not UTF-8 aware. Here we'll cut off malformed UTF-8. 4402 4403 if (recycle_state) 4404 { 4405 // Recycle existing cursor/selection/undo stack but clamp position 4406 // Note a single mouse click will override the cursor/position immediately by calling stb_textedit_click handler. 4407 state->CursorClamp(); 4408 } 4409 else 4410 { 4411 state->ScrollX = 0.0f; 4412 stb_textedit_initialize_state(&state->Stb, !is_multiline); 4413 } 4414 4415 if (init_reload_from_user_buf) 4416 { 4417 state->Stb.select_start = state->ReloadSelectionStart; 4418 state->Stb.cursor = state->Stb.select_end = state->ReloadSelectionEnd; 4419 state->CursorClamp(); 4420 } 4421 else if (!is_multiline) 4422 { 4423 if (flags & ImGuiInputTextFlags_AutoSelectAll) 4424 select_all = true; 4425 if (input_requested_by_nav && (!recycle_state || !(g.NavActivateFlags & ImGuiActivateFlags_TryToPreserveState))) 4426 select_all = true; 4427 if (user_clicked && io.KeyCtrl) 4428 select_all = true; 4429 } 4430 4431 if (flags & ImGuiInputTextFlags_AlwaysOverwrite) 4432 state->Stb.insert_mode = 1; // stb field name is indeed incorrect (see #2863) 4433 } 4434 4435 const bool is_osx = io.ConfigMacOSXBehaviors; 4436 if (g.ActiveId != id && init_make_active) 4437 { 4438 IM_ASSERT(state && state->ID == id); 4439 SetActiveID(id, window); 4440 SetFocusID(id, window); 4441 FocusWindow(window); 4442 } 4443 if (g.ActiveId == id) 4444 { 4445 // Declare some inputs, the other are registered and polled via Shortcut() routing system. 4446 if (user_clicked) 4447 SetKeyOwner(ImGuiKey_MouseLeft, id); 4448 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right); 4449 if (is_multiline || (flags & ImGuiInputTextFlags_CallbackHistory)) 4450 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); 4451 SetKeyOwner(ImGuiKey_Enter, id); 4452 SetKeyOwner(ImGuiKey_KeypadEnter, id); 4453 SetKeyOwner(ImGuiKey_Home, id); 4454 SetKeyOwner(ImGuiKey_End, id); 4455 if (is_multiline) 4456 { 4457 SetKeyOwner(ImGuiKey_PageUp, id); 4458 SetKeyOwner(ImGuiKey_PageDown, id); 4459 } 4460 // FIXME: May be a problem to always steal Alt on OSX, would ideally still allow an uninterrupted Alt down-up to toggle menu 4461 if (is_osx) 4462 SetKeyOwner(ImGuiMod_Alt, id); 4463 } 4464 4465 // We have an edge case if ActiveId was set through another widget (e.g. widget being swapped), clear id immediately (don't wait until the end of the function) 4466 if (g.ActiveId == id && state == NULL) 4467 ClearActiveID(); 4468 4469 // Release focus when we click outside 4470 if (g.ActiveId == id && io.MouseClicked[0] && !init_state && !init_make_active) //-V560 4471 clear_active_id = true; 4472 4473 // Lock the decision of whether we are going to take the path displaying the cursor or selection 4474 bool render_cursor = (g.ActiveId == id) || (state && user_scroll_active); 4475 bool render_selection = state && (state->HasSelection() || select_all) && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor); 4476 bool value_changed = false; 4477 bool validated = false; 4478 4479 // When read-only we always use the live data passed to the function 4480 // FIXME-OPT: Because our selection/cursor code currently needs the wide text we need to convert it when active, which is not ideal :( 4481 if (is_readonly && state != NULL && (render_cursor || render_selection)) 4482 { 4483 const char* buf_end = NULL; 4484 state->TextW.resize(buf_size + 1); 4485 state->CurLenW = ImTextStrFromUtf8(state->TextW.Data, state->TextW.Size, buf, NULL, &buf_end); 4486 state->CurLenA = (int)(buf_end - buf); 4487 state->CursorClamp(); 4488 render_selection &= state->HasSelection(); 4489 } 4490 4491 // Select the buffer to render. 4492 const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state && state->TextAIsValid; 4493 const bool is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0); 4494 4495 // Password pushes a temporary font with only a fallback glyph 4496 if (is_password && !is_displaying_hint) 4497 { 4498 const ImFontGlyph* glyph = g.Font->FindGlyph('*'); 4499 ImFont* password_font = &g.InputTextPasswordFont; 4500 password_font->FontSize = g.Font->FontSize; 4501 password_font->Scale = g.Font->Scale; 4502 password_font->Ascent = g.Font->Ascent; 4503 password_font->Descent = g.Font->Descent; 4504 password_font->ContainerAtlas = g.Font->ContainerAtlas; 4505 password_font->FallbackGlyph = glyph; 4506 password_font->FallbackAdvanceX = glyph->AdvanceX; 4507 IM_ASSERT(password_font->Glyphs.empty() && password_font->IndexAdvanceX.empty() && password_font->IndexLookup.empty()); 4508 PushFont(password_font); 4509 } 4510 4511 // Process mouse inputs and character inputs 4512 int backup_current_text_length = 0; 4513 if (g.ActiveId == id) 4514 { 4515 IM_ASSERT(state != NULL); 4516 backup_current_text_length = state->CurLenA; 4517 state->Edited = false; 4518 state->BufCapacityA = buf_size; 4519 state->Flags = flags; 4520 4521 // Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget. 4522 // Down the line we should have a cleaner library-wide concept of Selected vs Active. 4523 g.ActiveIdAllowOverlap = !io.MouseDown[0]; 4524 4525 // Edit in progress 4526 const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + state->ScrollX; 4527 const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y) : (g.FontSize * 0.5f)); 4528 4529 if (select_all) 4530 { 4531 state->SelectAll(); 4532 state->SelectedAllMouseLock = true; 4533 } 4534 else if (hovered && io.MouseClickedCount[0] >= 2 && !io.KeyShift) 4535 { 4536 stb_textedit_click(state, &state->Stb, mouse_x, mouse_y); 4537 const int multiclick_count = (io.MouseClickedCount[0] - 2); 4538 if ((multiclick_count % 2) == 0) 4539 { 4540 // Double-click: Select word 4541 // We always use the "Mac" word advance for double-click select vs CTRL+Right which use the platform dependent variant: 4542 // FIXME: There are likely many ways to improve this behavior, but there's no "right" behavior (depends on use-case, software, OS) 4543 const bool is_bol = (state->Stb.cursor == 0) || ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb.cursor - 1) == '\n'; 4544 if (STB_TEXT_HAS_SELECTION(&state->Stb) || !is_bol) 4545 state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT); 4546 //state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT); 4547 if (!STB_TEXT_HAS_SELECTION(&state->Stb)) 4548 ImStb::stb_textedit_prep_selection_at_cursor(&state->Stb); 4549 state->Stb.cursor = ImStb::STB_TEXTEDIT_MOVEWORDRIGHT_MAC(state, state->Stb.cursor); 4550 state->Stb.select_end = state->Stb.cursor; 4551 ImStb::stb_textedit_clamp(state, &state->Stb); 4552 } 4553 else 4554 { 4555 // Triple-click: Select line 4556 const bool is_eol = ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb.cursor) == '\n'; 4557 state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART); 4558 state->OnKeyPressed(STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT); 4559 state->OnKeyPressed(STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT); 4560 if (!is_eol && is_multiline) 4561 { 4562 ImSwap(state->Stb.select_start, state->Stb.select_end); 4563 state->Stb.cursor = state->Stb.select_end; 4564 } 4565 state->CursorFollow = false; 4566 } 4567 state->CursorAnimReset(); 4568 } 4569 else if (io.MouseClicked[0] && !state->SelectedAllMouseLock) 4570 { 4571 if (hovered) 4572 { 4573 if (io.KeyShift) 4574 stb_textedit_drag(state, &state->Stb, mouse_x, mouse_y); 4575 else 4576 stb_textedit_click(state, &state->Stb, mouse_x, mouse_y); 4577 state->CursorAnimReset(); 4578 } 4579 } 4580 else if (io.MouseDown[0] && !state->SelectedAllMouseLock && (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f)) 4581 { 4582 stb_textedit_drag(state, &state->Stb, mouse_x, mouse_y); 4583 state->CursorAnimReset(); 4584 state->CursorFollow = true; 4585 } 4586 if (state->SelectedAllMouseLock && !io.MouseDown[0]) 4587 state->SelectedAllMouseLock = false; 4588 4589 // We expect backends to emit a Tab key but some also emit a Tab character which we ignore (#2467, #1336) 4590 // (For Tab and Enter: Win32/SFML/Allegro are sending both keys and chars, GLFW and SDL are only sending keys. For Space they all send all threes) 4591 if ((flags & ImGuiInputTextFlags_AllowTabInput) && !is_readonly) 4592 { 4593 if (Shortcut(ImGuiKey_Tab, ImGuiInputFlags_Repeat, id)) 4594 { 4595 unsigned int c = '\t'; // Insert TAB 4596 if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data)) 4597 state->OnKeyPressed((int)c); 4598 } 4599 // FIXME: Implement Shift+Tab 4600 /* 4601 if (Shortcut(ImGuiKey_Tab | ImGuiMod_Shift, ImGuiInputFlags_Repeat, id)) 4602 { 4603 } 4604 */ 4605 } 4606 4607 // Process regular text input (before we check for Return because using some IME will effectively send a Return?) 4608 // We ignore CTRL inputs, but need to allow ALT+CTRL as some keyboards (e.g. German) use AltGR (which _is_ Alt+Ctrl) to input certain characters. 4609 const bool ignore_char_inputs = (io.KeyCtrl && !io.KeyAlt) || (is_osx && io.KeyCtrl); 4610 if (io.InputQueueCharacters.Size > 0) 4611 { 4612 if (!ignore_char_inputs && !is_readonly && !input_requested_by_nav) 4613 for (int n = 0; n < io.InputQueueCharacters.Size; n++) 4614 { 4615 // Insert character if they pass filtering 4616 unsigned int c = (unsigned int)io.InputQueueCharacters[n]; 4617 if (c == '\t') // Skip Tab, see above. 4618 continue; 4619 if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data)) 4620 state->OnKeyPressed((int)c); 4621 } 4622 4623 // Consume characters 4624 io.InputQueueCharacters.resize(0); 4625 } 4626 } 4627 4628 // Process other shortcuts/key-presses 4629 bool revert_edit = false; 4630 if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id) 4631 { 4632 IM_ASSERT(state != NULL); 4633 4634 const int row_count_per_page = ImMax((int)((inner_size.y - style.FramePadding.y) / g.FontSize), 1); 4635 state->Stb.row_count_per_page = row_count_per_page; 4636 4637 const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0); 4638 const bool is_wordmove_key_down = is_osx ? io.KeyAlt : io.KeyCtrl; // OS X style: Text editing cursor movement using Alt instead of Ctrl 4639 const bool is_startend_key_down = is_osx && io.KeyCtrl && !io.KeySuper && !io.KeyAlt; // OS X style: Line/Text Start and End using Cmd+Arrows instead of Home/End 4640 4641 // Using Shortcut() with ImGuiInputFlags_RouteFocused (default policy) to allow routing operations for other code (e.g. calling window trying to use CTRL+A and CTRL+B: formet would be handled by InputText) 4642 // Otherwise we could simply assume that we own the keys as we are active. 4643 const ImGuiInputFlags f_repeat = ImGuiInputFlags_Repeat; 4644 const bool is_cut = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_X, f_repeat, id) || Shortcut(ImGuiMod_Shift | ImGuiKey_Delete, f_repeat, id)) && !is_readonly && !is_password && (!is_multiline || state->HasSelection()); 4645 const bool is_copy = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_C, 0, id) || Shortcut(ImGuiMod_Ctrl | ImGuiKey_Insert, 0, id)) && !is_password && (!is_multiline || state->HasSelection()); 4646 const bool is_paste = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_V, f_repeat, id) || Shortcut(ImGuiMod_Shift | ImGuiKey_Insert, f_repeat, id)) && !is_readonly; 4647 const bool is_undo = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_Z, f_repeat, id)) && !is_readonly && is_undoable; 4648 const bool is_redo = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_Y, f_repeat, id) || (is_osx && Shortcut(ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Z, f_repeat, id))) && !is_readonly && is_undoable; 4649 const bool is_select_all = Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, 0, id); 4650 4651 // We allow validate/cancel with Nav source (gamepad) to makes it easier to undo an accidental NavInput press with no keyboard wired, but otherwise it isn't very useful. 4652 const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; 4653 const bool is_enter_pressed = IsKeyPressed(ImGuiKey_Enter, true) || IsKeyPressed(ImGuiKey_KeypadEnter, true); 4654 const bool is_gamepad_validate = nav_gamepad_active && (IsKeyPressed(ImGuiKey_NavGamepadActivate, false) || IsKeyPressed(ImGuiKey_NavGamepadInput, false)); 4655 const bool is_cancel = Shortcut(ImGuiKey_Escape, f_repeat, id) || (nav_gamepad_active && Shortcut(ImGuiKey_NavGamepadCancel, f_repeat, id)); 4656 4657 // FIXME: Should use more Shortcut() and reduce IsKeyPressed()+SetKeyOwner(), but requires modifiers combination to be taken account of. 4658 // FIXME-OSX: Missing support for Alt(option)+Right/Left = go to end of line, or next line if already in end of line. 4659 if (IsKeyPressed(ImGuiKey_LeftArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINESTART : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDLEFT : STB_TEXTEDIT_K_LEFT) | k_mask); } 4660 else if (IsKeyPressed(ImGuiKey_RightArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINEEND : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDRIGHT : STB_TEXTEDIT_K_RIGHT) | k_mask); } 4661 else if (IsKeyPressed(ImGuiKey_UpArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(draw_window, ImMax(draw_window->Scroll.y - g.FontSize, 0.0f)); else state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTSTART : STB_TEXTEDIT_K_UP) | k_mask); } 4662 else if (IsKeyPressed(ImGuiKey_DownArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(draw_window, ImMin(draw_window->Scroll.y + g.FontSize, GetScrollMaxY())); else state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTEND : STB_TEXTEDIT_K_DOWN) | k_mask); } 4663 else if (IsKeyPressed(ImGuiKey_PageUp) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGUP | k_mask); scroll_y -= row_count_per_page * g.FontSize; } 4664 else if (IsKeyPressed(ImGuiKey_PageDown) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGDOWN | k_mask); scroll_y += row_count_per_page * g.FontSize; } 4665 else if (IsKeyPressed(ImGuiKey_Home)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); } 4666 else if (IsKeyPressed(ImGuiKey_End)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); } 4667 else if (IsKeyPressed(ImGuiKey_Delete) && !is_readonly && !is_cut) 4668 { 4669 if (!state->HasSelection()) 4670 { 4671 // OSX doesn't seem to have Super+Delete to delete until end-of-line, so we don't emulate that (as opposed to Super+Backspace) 4672 if (is_wordmove_key_down) 4673 state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT); 4674 } 4675 state->OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask); 4676 } 4677 else if (IsKeyPressed(ImGuiKey_Backspace) && !is_readonly) 4678 { 4679 if (!state->HasSelection()) 4680 { 4681 if (is_wordmove_key_down) 4682 state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT); 4683 else if (is_osx && io.KeyCtrl && !io.KeyAlt && !io.KeySuper) 4684 state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT); 4685 } 4686 state->OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask); 4687 } 4688 else if (is_enter_pressed || is_gamepad_validate) 4689 { 4690 // Determine if we turn Enter into a \n character 4691 bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0; 4692 if (!is_multiline || is_gamepad_validate || (ctrl_enter_for_new_line && !io.KeyCtrl) || (!ctrl_enter_for_new_line && io.KeyCtrl)) 4693 { 4694 validated = true; 4695 if (io.ConfigInputTextEnterKeepActive && !is_multiline) 4696 state->SelectAll(); // No need to scroll 4697 else 4698 clear_active_id = true; 4699 } 4700 else if (!is_readonly) 4701 { 4702 unsigned int c = '\n'; // Insert new line 4703 if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data)) 4704 state->OnKeyPressed((int)c); 4705 } 4706 } 4707 else if (is_cancel) 4708 { 4709 if (flags & ImGuiInputTextFlags_EscapeClearsAll) 4710 { 4711 if (buf[0] != 0) 4712 { 4713 revert_edit = true; 4714 } 4715 else 4716 { 4717 render_cursor = render_selection = false; 4718 clear_active_id = true; 4719 } 4720 } 4721 else 4722 { 4723 clear_active_id = revert_edit = true; 4724 render_cursor = render_selection = false; 4725 } 4726 } 4727 else if (is_undo || is_redo) 4728 { 4729 state->OnKeyPressed(is_undo ? STB_TEXTEDIT_K_UNDO : STB_TEXTEDIT_K_REDO); 4730 state->ClearSelection(); 4731 } 4732 else if (is_select_all) 4733 { 4734 state->SelectAll(); 4735 state->CursorFollow = true; 4736 } 4737 else if (is_cut || is_copy) 4738 { 4739 // Cut, Copy 4740 if (io.SetClipboardTextFn) 4741 { 4742 const int ib = state->HasSelection() ? ImMin(state->Stb.select_start, state->Stb.select_end) : 0; 4743 const int ie = state->HasSelection() ? ImMax(state->Stb.select_start, state->Stb.select_end) : state->CurLenW; 4744 const int clipboard_data_len = ImTextCountUtf8BytesFromStr(state->TextW.Data + ib, state->TextW.Data + ie) + 1; 4745 char* clipboard_data = (char*)IM_ALLOC(clipboard_data_len * sizeof(char)); 4746 ImTextStrToUtf8(clipboard_data, clipboard_data_len, state->TextW.Data + ib, state->TextW.Data + ie); 4747 SetClipboardText(clipboard_data); 4748 MemFree(clipboard_data); 4749 } 4750 if (is_cut) 4751 { 4752 if (!state->HasSelection()) 4753 state->SelectAll(); 4754 state->CursorFollow = true; 4755 stb_textedit_cut(state, &state->Stb); 4756 } 4757 } 4758 else if (is_paste) 4759 { 4760 if (const char* clipboard = GetClipboardText()) 4761 { 4762 // Filter pasted buffer 4763 const int clipboard_len = (int)strlen(clipboard); 4764 ImWchar* clipboard_filtered = (ImWchar*)IM_ALLOC((clipboard_len + 1) * sizeof(ImWchar)); 4765 int clipboard_filtered_len = 0; 4766 for (const char* s = clipboard; *s != 0; ) 4767 { 4768 unsigned int c; 4769 s += ImTextCharFromUtf8(&c, s, NULL); 4770 if (!InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data, true)) 4771 continue; 4772 clipboard_filtered[clipboard_filtered_len++] = (ImWchar)c; 4773 } 4774 clipboard_filtered[clipboard_filtered_len] = 0; 4775 if (clipboard_filtered_len > 0) // If everything was filtered, ignore the pasting operation 4776 { 4777 stb_textedit_paste(state, &state->Stb, clipboard_filtered, clipboard_filtered_len); 4778 state->CursorFollow = true; 4779 } 4780 MemFree(clipboard_filtered); 4781 } 4782 } 4783 4784 // Update render selection flag after events have been handled, so selection highlight can be displayed during the same frame. 4785 render_selection |= state->HasSelection() && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor); 4786 } 4787 4788 // Process callbacks and apply result back to user's buffer. 4789 const char* apply_new_text = NULL; 4790 int apply_new_text_length = 0; 4791 if (g.ActiveId == id) 4792 { 4793 IM_ASSERT(state != NULL); 4794 if (revert_edit && !is_readonly) 4795 { 4796 if (flags & ImGuiInputTextFlags_EscapeClearsAll) 4797 { 4798 // Clear input 4799 IM_ASSERT(buf[0] != 0); 4800 apply_new_text = ""; 4801 apply_new_text_length = 0; 4802 value_changed = true; 4803 IMSTB_TEXTEDIT_CHARTYPE empty_string; 4804 stb_textedit_replace(state, &state->Stb, &empty_string, 0); 4805 } 4806 else if (strcmp(buf, state->InitialTextA.Data) != 0) 4807 { 4808 // Restore initial value. Only return true if restoring to the initial value changes the current buffer contents. 4809 // Push records into the undo stack so we can CTRL+Z the revert operation itself 4810 apply_new_text = state->InitialTextA.Data; 4811 apply_new_text_length = state->InitialTextA.Size - 1; 4812 value_changed = true; 4813 ImVector<ImWchar> w_text; 4814 if (apply_new_text_length > 0) 4815 { 4816 w_text.resize(ImTextCountCharsFromUtf8(apply_new_text, apply_new_text + apply_new_text_length) + 1); 4817 ImTextStrFromUtf8(w_text.Data, w_text.Size, apply_new_text, apply_new_text + apply_new_text_length); 4818 } 4819 stb_textedit_replace(state, &state->Stb, w_text.Data, (apply_new_text_length > 0) ? (w_text.Size - 1) : 0); 4820 } 4821 } 4822 4823 // Apply ASCII value 4824 if (!is_readonly) 4825 { 4826 state->TextAIsValid = true; 4827 state->TextA.resize(state->TextW.Size * 4 + 1); 4828 ImTextStrToUtf8(state->TextA.Data, state->TextA.Size, state->TextW.Data, NULL); 4829 } 4830 4831 // When using 'ImGuiInputTextFlags_EnterReturnsTrue' as a special case we reapply the live buffer back to the input buffer 4832 // before clearing ActiveId, even though strictly speaking it wasn't modified on this frame. 4833 // If we didn't do that, code like InputInt() with ImGuiInputTextFlags_EnterReturnsTrue would fail. 4834 // This also allows the user to use InputText() with ImGuiInputTextFlags_EnterReturnsTrue without maintaining any user-side storage 4835 // (please note that if you use this property along ImGuiInputTextFlags_CallbackResize you can end up with your temporary string object 4836 // unnecessarily allocating once a frame, either store your string data, either if you don't then don't use ImGuiInputTextFlags_CallbackResize). 4837 const bool apply_edit_back_to_user_buffer = !revert_edit || (validated && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0); 4838 if (apply_edit_back_to_user_buffer) 4839 { 4840 // Apply new value immediately - copy modified buffer back 4841 // Note that as soon as the input box is active, the in-widget value gets priority over any underlying modification of the input buffer 4842 // FIXME: We actually always render 'buf' when calling DrawList->AddText, making the comment above incorrect. 4843 // FIXME-OPT: CPU waste to do this every time the widget is active, should mark dirty state from the stb_textedit callbacks. 4844 4845 // User callback 4846 if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackEdit | ImGuiInputTextFlags_CallbackAlways)) != 0) 4847 { 4848 IM_ASSERT(callback != NULL); 4849 4850 // The reason we specify the usage semantic (Completion/History) is that Completion needs to disable keyboard TABBING at the moment. 4851 ImGuiInputTextFlags event_flag = 0; 4852 ImGuiKey event_key = ImGuiKey_None; 4853 if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && Shortcut(ImGuiKey_Tab, 0, id)) 4854 { 4855 event_flag = ImGuiInputTextFlags_CallbackCompletion; 4856 event_key = ImGuiKey_Tab; 4857 } 4858 else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_UpArrow)) 4859 { 4860 event_flag = ImGuiInputTextFlags_CallbackHistory; 4861 event_key = ImGuiKey_UpArrow; 4862 } 4863 else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_DownArrow)) 4864 { 4865 event_flag = ImGuiInputTextFlags_CallbackHistory; 4866 event_key = ImGuiKey_DownArrow; 4867 } 4868 else if ((flags & ImGuiInputTextFlags_CallbackEdit) && state->Edited) 4869 { 4870 event_flag = ImGuiInputTextFlags_CallbackEdit; 4871 } 4872 else if (flags & ImGuiInputTextFlags_CallbackAlways) 4873 { 4874 event_flag = ImGuiInputTextFlags_CallbackAlways; 4875 } 4876 4877 if (event_flag) 4878 { 4879 ImGuiInputTextCallbackData callback_data; 4880 callback_data.Ctx = &g; 4881 callback_data.EventFlag = event_flag; 4882 callback_data.Flags = flags; 4883 callback_data.UserData = callback_user_data; 4884 4885 char* callback_buf = is_readonly ? buf : state->TextA.Data; 4886 callback_data.EventKey = event_key; 4887 callback_data.Buf = callback_buf; 4888 callback_data.BufTextLen = state->CurLenA; 4889 callback_data.BufSize = state->BufCapacityA; 4890 callback_data.BufDirty = false; 4891 4892 // We have to convert from wchar-positions to UTF-8-positions, which can be pretty slow (an incentive to ditch the ImWchar buffer, see https://github.com/nothings/stb/issues/188) 4893 ImWchar* text = state->TextW.Data; 4894 const int utf8_cursor_pos = callback_data.CursorPos = ImTextCountUtf8BytesFromStr(text, text + state->Stb.cursor); 4895 const int utf8_selection_start = callback_data.SelectionStart = ImTextCountUtf8BytesFromStr(text, text + state->Stb.select_start); 4896 const int utf8_selection_end = callback_data.SelectionEnd = ImTextCountUtf8BytesFromStr(text, text + state->Stb.select_end); 4897 4898 // Call user code 4899 callback(&callback_data); 4900 4901 // Read back what user may have modified 4902 callback_buf = is_readonly ? buf : state->TextA.Data; // Pointer may have been invalidated by a resize callback 4903 IM_ASSERT(callback_data.Buf == callback_buf); // Invalid to modify those fields 4904 IM_ASSERT(callback_data.BufSize == state->BufCapacityA); 4905 IM_ASSERT(callback_data.Flags == flags); 4906 const bool buf_dirty = callback_data.BufDirty; 4907 if (callback_data.CursorPos != utf8_cursor_pos || buf_dirty) { state->Stb.cursor = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.CursorPos); state->CursorFollow = true; } 4908 if (callback_data.SelectionStart != utf8_selection_start || buf_dirty) { state->Stb.select_start = (callback_data.SelectionStart == callback_data.CursorPos) ? state->Stb.cursor : ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionStart); } 4909 if (callback_data.SelectionEnd != utf8_selection_end || buf_dirty) { state->Stb.select_end = (callback_data.SelectionEnd == callback_data.SelectionStart) ? state->Stb.select_start : ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionEnd); } 4910 if (buf_dirty) 4911 { 4912 IM_ASSERT(!is_readonly); 4913 IM_ASSERT(callback_data.BufTextLen == (int)strlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text! 4914 InputTextReconcileUndoStateAfterUserCallback(state, callback_data.Buf, callback_data.BufTextLen); // FIXME: Move the rest of this block inside function and rename to InputTextReconcileStateAfterUserCallback() ? 4915 if (callback_data.BufTextLen > backup_current_text_length && is_resizable) 4916 state->TextW.resize(state->TextW.Size + (callback_data.BufTextLen - backup_current_text_length)); // Worse case scenario resize 4917 state->CurLenW = ImTextStrFromUtf8(state->TextW.Data, state->TextW.Size, callback_data.Buf, NULL); 4918 state->CurLenA = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen() 4919 state->CursorAnimReset(); 4920 } 4921 } 4922 } 4923 4924 // Will copy result string if modified 4925 if (!is_readonly && strcmp(state->TextA.Data, buf) != 0) 4926 { 4927 apply_new_text = state->TextA.Data; 4928 apply_new_text_length = state->CurLenA; 4929 value_changed = true; 4930 } 4931 } 4932 } 4933 4934 // Handle reapplying final data on deactivation (see InputTextDeactivateHook() for details) 4935 if (g.InputTextDeactivatedState.ID == id) 4936 { 4937 if (g.ActiveId != id && IsItemDeactivatedAfterEdit() && !is_readonly && strcmp(g.InputTextDeactivatedState.TextA.Data, buf) != 0) 4938 { 4939 apply_new_text = g.InputTextDeactivatedState.TextA.Data; 4940 apply_new_text_length = g.InputTextDeactivatedState.TextA.Size - 1; 4941 value_changed = true; 4942 //IMGUI_DEBUG_LOG("InputText(): apply Deactivated data for 0x%08X: \"%.*s\".\n", id, apply_new_text_length, apply_new_text); 4943 } 4944 g.InputTextDeactivatedState.ID = 0; 4945 } 4946 4947 // Copy result to user buffer. This can currently only happen when (g.ActiveId == id) 4948 if (apply_new_text != NULL) 4949 { 4950 // We cannot test for 'backup_current_text_length != apply_new_text_length' here because we have no guarantee that the size 4951 // of our owned buffer matches the size of the string object held by the user, and by design we allow InputText() to be used 4952 // without any storage on user's side. 4953 IM_ASSERT(apply_new_text_length >= 0); 4954 if (is_resizable) 4955 { 4956 ImGuiInputTextCallbackData callback_data; 4957 callback_data.Ctx = &g; 4958 callback_data.EventFlag = ImGuiInputTextFlags_CallbackResize; 4959 callback_data.Flags = flags; 4960 callback_data.Buf = buf; 4961 callback_data.BufTextLen = apply_new_text_length; 4962 callback_data.BufSize = ImMax(buf_size, apply_new_text_length + 1); 4963 callback_data.UserData = callback_user_data; 4964 callback(&callback_data); 4965 buf = callback_data.Buf; 4966 buf_size = callback_data.BufSize; 4967 apply_new_text_length = ImMin(callback_data.BufTextLen, buf_size - 1); 4968 IM_ASSERT(apply_new_text_length <= buf_size); 4969 } 4970 //IMGUI_DEBUG_PRINT("InputText(\"%s\"): apply_new_text length %d\n", label, apply_new_text_length); 4971 4972 // If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size. 4973 ImStrncpy(buf, apply_new_text, ImMin(apply_new_text_length + 1, buf_size)); 4974 } 4975 4976 // Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value) 4977 // Otherwise request text input ahead for next frame. 4978 if (g.ActiveId == id && clear_active_id) 4979 ClearActiveID(); 4980 else if (g.ActiveId == id) 4981 g.WantTextInputNextFrame = 1; 4982 4983 // Render frame 4984 if (!is_multiline) 4985 { 4986 RenderNavHighlight(frame_bb, id); 4987 RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); 4988 } 4989 4990 const ImVec4 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 4991 ImVec2 draw_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding; 4992 ImVec2 text_size(0.0f, 0.0f); 4993 4994 // Set upper limit of single-line InputTextEx() at 2 million characters strings. The current pathological worst case is a long line 4995 // without any carriage return, which would makes ImFont::RenderText() reserve too many vertices and probably crash. Avoid it altogether. 4996 // Note that we only use this limit on single-line InputText(), so a pathologically large line on a InputTextMultiline() would still crash. 4997 const int buf_display_max_length = 2 * 1024 * 1024; 4998 const char* buf_display = buf_display_from_state ? state->TextA.Data : buf; //-V595 4999 const char* buf_display_end = NULL; // We have specialized paths below for setting the length 5000 if (is_displaying_hint) 5001 { 5002 buf_display = hint; 5003 buf_display_end = hint + strlen(hint); 5004 } 5005 5006 // Render text. We currently only render selection when the widget is active or while scrolling. 5007 // FIXME: We could remove the '&& render_cursor' to keep rendering selection when inactive. 5008 if (render_cursor || render_selection) 5009 { 5010 IM_ASSERT(state != NULL); 5011 if (!is_displaying_hint) 5012 buf_display_end = buf_display + state->CurLenA; 5013 5014 // Render text (with cursor and selection) 5015 // This is going to be messy. We need to: 5016 // - Display the text (this alone can be more easily clipped) 5017 // - Handle scrolling, highlight selection, display cursor (those all requires some form of 1d->2d cursor position calculation) 5018 // - Measure text height (for scrollbar) 5019 // We are attempting to do most of that in **one main pass** to minimize the computation cost (non-negligible for large amount of text) + 2nd pass for selection rendering (we could merge them by an extra refactoring effort) 5020 // FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8. 5021 const ImWchar* text_begin = state->TextW.Data; 5022 ImVec2 cursor_offset, select_start_offset; 5023 5024 { 5025 // Find lines numbers straddling 'cursor' (slot 0) and 'select_start' (slot 1) positions. 5026 const ImWchar* searches_input_ptr[2] = { NULL, NULL }; 5027 int searches_result_line_no[2] = { -1000, -1000 }; 5028 int searches_remaining = 0; 5029 if (render_cursor) 5030 { 5031 searches_input_ptr[0] = text_begin + state->Stb.cursor; 5032 searches_result_line_no[0] = -1; 5033 searches_remaining++; 5034 } 5035 if (render_selection) 5036 { 5037 searches_input_ptr[1] = text_begin + ImMin(state->Stb.select_start, state->Stb.select_end); 5038 searches_result_line_no[1] = -1; 5039 searches_remaining++; 5040 } 5041 5042 // Iterate all lines to find our line numbers 5043 // In multi-line mode, we never exit the loop until all lines are counted, so add one extra to the searches_remaining counter. 5044 searches_remaining += is_multiline ? 1 : 0; 5045 int line_count = 0; 5046 //for (const ImWchar* s = text_begin; (s = (const ImWchar*)wcschr((const wchar_t*)s, (wchar_t)'\n')) != NULL; s++) // FIXME-OPT: Could use this when wchar_t are 16-bit 5047 for (const ImWchar* s = text_begin; *s != 0; s++) 5048 if (*s == '\n') 5049 { 5050 line_count++; 5051 if (searches_result_line_no[0] == -1 && s >= searches_input_ptr[0]) { searches_result_line_no[0] = line_count; if (--searches_remaining <= 0) break; } 5052 if (searches_result_line_no[1] == -1 && s >= searches_input_ptr[1]) { searches_result_line_no[1] = line_count; if (--searches_remaining <= 0) break; } 5053 } 5054 line_count++; 5055 if (searches_result_line_no[0] == -1) 5056 searches_result_line_no[0] = line_count; 5057 if (searches_result_line_no[1] == -1) 5058 searches_result_line_no[1] = line_count; 5059 5060 // Calculate 2d position by finding the beginning of the line and measuring distance 5061 cursor_offset.x = InputTextCalcTextSizeW(&g, ImStrbolW(searches_input_ptr[0], text_begin), searches_input_ptr[0]).x; 5062 cursor_offset.y = searches_result_line_no[0] * g.FontSize; 5063 if (searches_result_line_no[1] >= 0) 5064 { 5065 select_start_offset.x = InputTextCalcTextSizeW(&g, ImStrbolW(searches_input_ptr[1], text_begin), searches_input_ptr[1]).x; 5066 select_start_offset.y = searches_result_line_no[1] * g.FontSize; 5067 } 5068 5069 // Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224) 5070 if (is_multiline) 5071 text_size = ImVec2(inner_size.x, line_count * g.FontSize); 5072 } 5073 5074 // Scroll 5075 if (render_cursor && state->CursorFollow) 5076 { 5077 // Horizontal scroll in chunks of quarter width 5078 if (!(flags & ImGuiInputTextFlags_NoHorizontalScroll)) 5079 { 5080 const float scroll_increment_x = inner_size.x * 0.25f; 5081 const float visible_width = inner_size.x - style.FramePadding.x; 5082 if (cursor_offset.x < state->ScrollX) 5083 state->ScrollX = IM_TRUNC(ImMax(0.0f, cursor_offset.x - scroll_increment_x)); 5084 else if (cursor_offset.x - visible_width >= state->ScrollX) 5085 state->ScrollX = IM_TRUNC(cursor_offset.x - visible_width + scroll_increment_x); 5086 } 5087 else 5088 { 5089 state->ScrollX = 0.0f; 5090 } 5091 5092 // Vertical scroll 5093 if (is_multiline) 5094 { 5095 // Test if cursor is vertically visible 5096 if (cursor_offset.y - g.FontSize < scroll_y) 5097 scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize); 5098 else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y) 5099 scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f; 5100 const float scroll_max_y = ImMax((text_size.y + style.FramePadding.y * 2.0f) - inner_size.y, 0.0f); 5101 scroll_y = ImClamp(scroll_y, 0.0f, scroll_max_y); 5102 draw_pos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag 5103 draw_window->Scroll.y = scroll_y; 5104 } 5105 5106 state->CursorFollow = false; 5107 } 5108 5109 // Draw selection 5110 const ImVec2 draw_scroll = ImVec2(state->ScrollX, 0.0f); 5111 if (render_selection) 5112 { 5113 const ImWchar* text_selected_begin = text_begin + ImMin(state->Stb.select_start, state->Stb.select_end); 5114 const ImWchar* text_selected_end = text_begin + ImMax(state->Stb.select_start, state->Stb.select_end); 5115 5116 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. 5117 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. 5118 float bg_offy_dn = is_multiline ? 0.0f : 2.0f; 5119 ImVec2 rect_pos = draw_pos + select_start_offset - draw_scroll; 5120 for (const ImWchar* p = text_selected_begin; p < text_selected_end; ) 5121 { 5122 if (rect_pos.y > clip_rect.w + g.FontSize) 5123 break; 5124 if (rect_pos.y < clip_rect.y) 5125 { 5126 //p = (const ImWchar*)wmemchr((const wchar_t*)p, '\n', text_selected_end - p); // FIXME-OPT: Could use this when wchar_t are 16-bit 5127 //p = p ? p + 1 : text_selected_end; 5128 while (p < text_selected_end) 5129 if (*p++ == '\n') 5130 break; 5131 } 5132 else 5133 { 5134 ImVec2 rect_size = InputTextCalcTextSizeW(&g, p, text_selected_end, &p, NULL, true); 5135 if (rect_size.x <= 0.0f) rect_size.x = IM_TRUNC(g.Font->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines 5136 ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_size.x, bg_offy_dn)); 5137 rect.ClipWith(clip_rect); 5138 if (rect.Overlaps(clip_rect)) 5139 draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color); 5140 } 5141 rect_pos.x = draw_pos.x - draw_scroll.x; 5142 rect_pos.y += g.FontSize; 5143 } 5144 } 5145 5146 // We test for 'buf_display_max_length' as a way to avoid some pathological cases (e.g. single-line 1 MB string) which would make ImDrawList crash. 5147 if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length) 5148 { 5149 ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text); 5150 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); 5151 } 5152 5153 // Draw blinking cursor 5154 if (render_cursor) 5155 { 5156 state->CursorAnim += io.DeltaTime; 5157 bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f; 5158 ImVec2 cursor_screen_pos = ImTrunc(draw_pos + cursor_offset - draw_scroll); 5159 ImRect cursor_screen_rect(cursor_screen_pos.x, cursor_screen_pos.y - g.FontSize + 0.5f, cursor_screen_pos.x + 1.0f, cursor_screen_pos.y - 1.5f); 5160 if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect)) 5161 draw_window->DrawList->AddLine(cursor_screen_rect.Min, cursor_screen_rect.GetBL(), GetColorU32(ImGuiCol_Text)); 5162 5163 // Notify OS of text input position for advanced IME (-1 x offset so that Windows IME can cover our cursor. Bit of an extra nicety.) 5164 if (!is_readonly) 5165 { 5166 g.PlatformImeData.WantVisible = true; 5167 g.PlatformImeData.InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize); 5168 g.PlatformImeData.InputLineHeight = g.FontSize; 5169 } 5170 } 5171 } 5172 else 5173 { 5174 // Render text only (no selection, no cursor) 5175 if (is_multiline) 5176 text_size = ImVec2(inner_size.x, InputTextCalcTextLenAndLineCount(buf_display, &buf_display_end) * g.FontSize); // We don't need width 5177 else if (!is_displaying_hint && g.ActiveId == id) 5178 buf_display_end = buf_display + state->CurLenA; 5179 else if (!is_displaying_hint) 5180 buf_display_end = buf_display + strlen(buf_display); 5181 5182 if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length) 5183 { 5184 ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text); 5185 draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect); 5186 } 5187 } 5188 5189 if (is_password && !is_displaying_hint) 5190 PopFont(); 5191 5192 if (is_multiline) 5193 { 5194 // For focus requests to work on our multiline we need to ensure our child ItemAdd() call specifies the ImGuiItemFlags_Inputable (ref issue #4761)... 5195 Dummy(ImVec2(text_size.x, text_size.y + style.FramePadding.y)); 5196 g.NextItemData.ItemFlags |= ImGuiItemFlags_Inputable | ImGuiItemFlags_NoTabStop; 5197 EndChild(); 5198 item_data_backup.StatusFlags |= (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredWindow); 5199 5200 // ...and then we need to undo the group overriding last item data, which gets a bit messy as EndGroup() tries to forward scrollbar being active... 5201 // FIXME: This quite messy/tricky, should attempt to get rid of the child window. 5202 EndGroup(); 5203 if (g.LastItemData.ID == 0) 5204 { 5205 g.LastItemData.ID = id; 5206 g.LastItemData.InFlags = item_data_backup.InFlags; 5207 g.LastItemData.StatusFlags = item_data_backup.StatusFlags; 5208 } 5209 } 5210 5211 // Log as text 5212 if (g.LogEnabled && (!is_password || is_displaying_hint)) 5213 { 5214 LogSetNextTextDecoration("{", "}"); 5215 LogRenderedText(&draw_pos, buf_display, buf_display_end); 5216 } 5217 5218 if (label_size.x > 0) 5219 RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); 5220 5221 if (value_changed && !(flags & ImGuiInputTextFlags_NoMarkEdited)) 5222 MarkItemEdited(id); 5223 5224 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable); 5225 if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0) 5226 return validated; 5227 else 5228 return value_changed; 5229 } 5230 5231 void ImGui::DebugNodeInputTextState(ImGuiInputTextState* state) 5232 { 5233 #ifndef IMGUI_DISABLE_DEBUG_TOOLS 5234 ImGuiContext& g = *GImGui; 5235 ImStb::STB_TexteditState* stb_state = &state->Stb; 5236 ImStb::StbUndoState* undo_state = &stb_state->undostate; 5237 Text("ID: 0x%08X, ActiveID: 0x%08X", state->ID, g.ActiveId); 5238 DebugLocateItemOnHover(state->ID); 5239 Text("CurLenW: %d, CurLenA: %d, Cursor: %d, Selection: %d..%d", state->CurLenW, state->CurLenA, stb_state->cursor, stb_state->select_start, stb_state->select_end); 5240 Text("has_preferred_x: %d (%.2f)", stb_state->has_preferred_x, stb_state->preferred_x); 5241 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); 5242 if (BeginChild("undopoints", ImVec2(0.0f, GetTextLineHeight() * 10), ImGuiChildFlags_Border | ImGuiChildFlags_ResizeY)) // Visualize undo state 5243 { 5244 PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); 5245 for (int n = 0; n < IMSTB_TEXTEDIT_UNDOSTATECOUNT; n++) 5246 { 5247 ImStb::StbUndoRecord* undo_rec = &undo_state->undo_rec[n]; 5248 const char undo_rec_type = (n < undo_state->undo_point) ? 'u' : (n >= undo_state->redo_point) ? 'r' : ' '; 5249 if (undo_rec_type == ' ') 5250 BeginDisabled(); 5251 char buf[64] = ""; 5252 if (undo_rec_type != ' ' && undo_rec->char_storage != -1) 5253 ImTextStrToUtf8(buf, IM_ARRAYSIZE(buf), undo_state->undo_char + undo_rec->char_storage, undo_state->undo_char + undo_rec->char_storage + undo_rec->insert_length); 5254 Text("%c [%02d] where %03d, insert %03d, delete %03d, char_storage %03d \"%s\"", 5255 undo_rec_type, n, undo_rec->where, undo_rec->insert_length, undo_rec->delete_length, undo_rec->char_storage, buf); 5256 if (undo_rec_type == ' ') 5257 EndDisabled(); 5258 } 5259 PopStyleVar(); 5260 } 5261 EndChild(); 5262 #else 5263 IM_UNUSED(state); 5264 #endif 5265 } 5266 5267 //------------------------------------------------------------------------- 5268 // [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc. 5269 //------------------------------------------------------------------------- 5270 // - ColorEdit3() 5271 // - ColorEdit4() 5272 // - ColorPicker3() 5273 // - RenderColorRectWithAlphaCheckerboard() [Internal] 5274 // - ColorPicker4() 5275 // - ColorButton() 5276 // - SetColorEditOptions() 5277 // - ColorTooltip() [Internal] 5278 // - ColorEditOptionsPopup() [Internal] 5279 // - ColorPickerOptionsPopup() [Internal] 5280 //------------------------------------------------------------------------- 5281 5282 bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags) 5283 { 5284 return ColorEdit4(label, col, flags | ImGuiColorEditFlags_NoAlpha); 5285 } 5286 5287 static void ColorEditRestoreH(const float* col, float* H) 5288 { 5289 ImGuiContext& g = *GImGui; 5290 IM_ASSERT(g.ColorEditCurrentID != 0); 5291 if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0))) 5292 return; 5293 *H = g.ColorEditSavedHue; 5294 } 5295 5296 // ColorEdit supports RGB and HSV inputs. In case of RGB input resulting color may have undefined hue and/or saturation. 5297 // Since widget displays both RGB and HSV values we must preserve hue and saturation to prevent these values resetting. 5298 static void ColorEditRestoreHS(const float* col, float* H, float* S, float* V) 5299 { 5300 ImGuiContext& g = *GImGui; 5301 IM_ASSERT(g.ColorEditCurrentID != 0); 5302 if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0))) 5303 return; 5304 5305 // When S == 0, H is undefined. 5306 // When H == 1 it wraps around to 0. 5307 if (*S == 0.0f || (*H == 0.0f && g.ColorEditSavedHue == 1)) 5308 *H = g.ColorEditSavedHue; 5309 5310 // When V == 0, S is undefined. 5311 if (*V == 0.0f) 5312 *S = g.ColorEditSavedSat; 5313 } 5314 5315 // Edit colors components (each component in 0.0f..1.0f range). 5316 // See enum ImGuiColorEditFlags_ for available options. e.g. Only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. 5317 // With typical options: Left-click on color square to open color picker. Right-click to open option menu. CTRL-Click over input fields to edit them and TAB to go to next item. 5318 bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags) 5319 { 5320 ImGuiWindow* window = GetCurrentWindow(); 5321 if (window->SkipItems) 5322 return false; 5323 5324 ImGuiContext& g = *GImGui; 5325 const ImGuiStyle& style = g.Style; 5326 const float square_sz = GetFrameHeight(); 5327 const char* label_display_end = FindRenderedTextEnd(label); 5328 float w_full = CalcItemWidth(); 5329 g.NextItemData.ClearFlags(); 5330 5331 BeginGroup(); 5332 PushID(label); 5333 const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0); 5334 if (set_current_color_edit_id) 5335 g.ColorEditCurrentID = window->IDStack.back(); 5336 5337 // If we're not showing any slider there's no point in doing any HSV conversions 5338 const ImGuiColorEditFlags flags_untouched = flags; 5339 if (flags & ImGuiColorEditFlags_NoInputs) 5340 flags = (flags & (~ImGuiColorEditFlags_DisplayMask_)) | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_NoOptions; 5341 5342 // Context menu: display and modify options (before defaults are applied) 5343 if (!(flags & ImGuiColorEditFlags_NoOptions)) 5344 ColorEditOptionsPopup(col, flags); 5345 5346 // Read stored options 5347 if (!(flags & ImGuiColorEditFlags_DisplayMask_)) 5348 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DisplayMask_); 5349 if (!(flags & ImGuiColorEditFlags_DataTypeMask_)) 5350 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DataTypeMask_); 5351 if (!(flags & ImGuiColorEditFlags_PickerMask_)) 5352 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_); 5353 if (!(flags & ImGuiColorEditFlags_InputMask_)) 5354 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_InputMask_); 5355 flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_)); 5356 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check that only 1 is selected 5357 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected 5358 5359 const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0; 5360 const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0; 5361 const int components = alpha ? 4 : 3; 5362 const float w_button = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x); 5363 const float w_inputs = ImMax(w_full - w_button, 1.0f); 5364 w_full = w_inputs + w_button; 5365 5366 // Convert to the formats we need 5367 float f[4] = { col[0], col[1], col[2], alpha ? col[3] : 1.0f }; 5368 if ((flags & ImGuiColorEditFlags_InputHSV) && (flags & ImGuiColorEditFlags_DisplayRGB)) 5369 ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]); 5370 else if ((flags & ImGuiColorEditFlags_InputRGB) && (flags & ImGuiColorEditFlags_DisplayHSV)) 5371 { 5372 // Hue is lost when converting from grayscale rgb (saturation=0). Restore it. 5373 ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]); 5374 ColorEditRestoreHS(col, &f[0], &f[1], &f[2]); 5375 } 5376 int i[4] = { IM_F32_TO_INT8_UNBOUND(f[0]), IM_F32_TO_INT8_UNBOUND(f[1]), IM_F32_TO_INT8_UNBOUND(f[2]), IM_F32_TO_INT8_UNBOUND(f[3]) }; 5377 5378 bool value_changed = false; 5379 bool value_changed_as_float = false; 5380 5381 const ImVec2 pos = window->DC.CursorPos; 5382 const float inputs_offset_x = (style.ColorButtonPosition == ImGuiDir_Left) ? w_button : 0.0f; 5383 window->DC.CursorPos.x = pos.x + inputs_offset_x; 5384 5385 if ((flags & (ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0) 5386 { 5387 // RGB/HSV 0..255 Sliders 5388 const float w_items = w_inputs - style.ItemInnerSpacing.x * (components - 1); 5389 5390 const bool hide_prefix = (IM_TRUNC(w_items / components) <= CalcTextSize((flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x); 5391 static const char* ids[4] = { "##X", "##Y", "##Z", "##W" }; 5392 static const char* fmt_table_int[3][4] = 5393 { 5394 { "%3d", "%3d", "%3d", "%3d" }, // Short display 5395 { "R:%3d", "G:%3d", "B:%3d", "A:%3d" }, // Long display for RGBA 5396 { "H:%3d", "S:%3d", "V:%3d", "A:%3d" } // Long display for HSVA 5397 }; 5398 static const char* fmt_table_float[3][4] = 5399 { 5400 { "%0.3f", "%0.3f", "%0.3f", "%0.3f" }, // Short display 5401 { "R:%0.3f", "G:%0.3f", "B:%0.3f", "A:%0.3f" }, // Long display for RGBA 5402 { "H:%0.3f", "S:%0.3f", "V:%0.3f", "A:%0.3f" } // Long display for HSVA 5403 }; 5404 const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_DisplayHSV) ? 2 : 1; 5405 5406 float prev_split = 0.0f; 5407 for (int n = 0; n < components; n++) 5408 { 5409 if (n > 0) 5410 SameLine(0, style.ItemInnerSpacing.x); 5411 float next_split = IM_TRUNC(w_items * (n + 1) / components); 5412 SetNextItemWidth(ImMax(next_split - prev_split, 1.0f)); 5413 prev_split = next_split; 5414 5415 // FIXME: When ImGuiColorEditFlags_HDR flag is passed HS values snap in weird ways when SV values go below 0. 5416 if (flags & ImGuiColorEditFlags_Float) 5417 { 5418 value_changed |= DragFloat(ids[n], &f[n], 1.0f / 255.0f, 0.0f, hdr ? 0.0f : 1.0f, fmt_table_float[fmt_idx][n]); 5419 value_changed_as_float |= value_changed; 5420 } 5421 else 5422 { 5423 value_changed |= DragInt(ids[n], &i[n], 1.0f, 0, hdr ? 0 : 255, fmt_table_int[fmt_idx][n]); 5424 } 5425 if (!(flags & ImGuiColorEditFlags_NoOptions)) 5426 OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); 5427 } 5428 } 5429 else if ((flags & ImGuiColorEditFlags_DisplayHex) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0) 5430 { 5431 // RGB Hexadecimal Input 5432 char buf[64]; 5433 if (alpha) 5434 ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255), ImClamp(i[3], 0, 255)); 5435 else 5436 ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255)); 5437 SetNextItemWidth(w_inputs); 5438 if (InputText("##Text", buf, IM_ARRAYSIZE(buf), ImGuiInputTextFlags_CharsUppercase)) 5439 { 5440 value_changed = true; 5441 char* p = buf; 5442 while (*p == '#' || ImCharIsBlankA(*p)) 5443 p++; 5444 i[0] = i[1] = i[2] = 0; 5445 i[3] = 0xFF; // alpha default to 255 is not parsed by scanf (e.g. inputting #FFFFFF omitting alpha) 5446 int r; 5447 if (alpha) 5448 r = sscanf(p, "%02X%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2], (unsigned int*)&i[3]); // Treat at unsigned (%X is unsigned) 5449 else 5450 r = sscanf(p, "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]); 5451 IM_UNUSED(r); // Fixes C6031: Return value ignored: 'sscanf'. 5452 } 5453 if (!(flags & ImGuiColorEditFlags_NoOptions)) 5454 OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); 5455 } 5456 5457 ImGuiWindow* picker_active_window = NULL; 5458 if (!(flags & ImGuiColorEditFlags_NoSmallPreview)) 5459 { 5460 const float button_offset_x = ((flags & ImGuiColorEditFlags_NoInputs) || (style.ColorButtonPosition == ImGuiDir_Left)) ? 0.0f : w_inputs + style.ItemInnerSpacing.x; 5461 window->DC.CursorPos = ImVec2(pos.x + button_offset_x, pos.y); 5462 5463 const ImVec4 col_v4(col[0], col[1], col[2], alpha ? col[3] : 1.0f); 5464 if (ColorButton("##ColorButton", col_v4, flags)) 5465 { 5466 if (!(flags & ImGuiColorEditFlags_NoPicker)) 5467 { 5468 // Store current color and open a picker 5469 g.ColorPickerRef = col_v4; 5470 OpenPopup("picker"); 5471 SetNextWindowPos(g.LastItemData.Rect.GetBL() + ImVec2(0.0f, style.ItemSpacing.y)); 5472 } 5473 } 5474 if (!(flags & ImGuiColorEditFlags_NoOptions)) 5475 OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); 5476 5477 if (BeginPopup("picker")) 5478 { 5479 if (g.CurrentWindow->BeginCount == 1) 5480 { 5481 picker_active_window = g.CurrentWindow; 5482 if (label != label_display_end) 5483 { 5484 TextEx(label, label_display_end); 5485 Spacing(); 5486 } 5487 ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar; 5488 ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf; 5489 SetNextItemWidth(square_sz * 12.0f); // Use 256 + bar sizes? 5490 value_changed |= ColorPicker4("##picker", col, picker_flags, &g.ColorPickerRef.x); 5491 } 5492 EndPopup(); 5493 } 5494 } 5495 5496 if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel)) 5497 { 5498 // Position not necessarily next to last submitted button (e.g. if style.ColorButtonPosition == ImGuiDir_Left), 5499 // but we need to use SameLine() to setup baseline correctly. Might want to refactor SameLine() to simplify this. 5500 SameLine(0.0f, style.ItemInnerSpacing.x); 5501 window->DC.CursorPos.x = pos.x + ((flags & ImGuiColorEditFlags_NoInputs) ? w_button : w_full + style.ItemInnerSpacing.x); 5502 TextEx(label, label_display_end); 5503 } 5504 5505 // Convert back 5506 if (value_changed && picker_active_window == NULL) 5507 { 5508 if (!value_changed_as_float) 5509 for (int n = 0; n < 4; n++) 5510 f[n] = i[n] / 255.0f; 5511 if ((flags & ImGuiColorEditFlags_DisplayHSV) && (flags & ImGuiColorEditFlags_InputRGB)) 5512 { 5513 g.ColorEditSavedHue = f[0]; 5514 g.ColorEditSavedSat = f[1]; 5515 ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]); 5516 g.ColorEditSavedID = g.ColorEditCurrentID; 5517 g.ColorEditSavedColor = ColorConvertFloat4ToU32(ImVec4(f[0], f[1], f[2], 0)); 5518 } 5519 if ((flags & ImGuiColorEditFlags_DisplayRGB) && (flags & ImGuiColorEditFlags_InputHSV)) 5520 ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]); 5521 5522 col[0] = f[0]; 5523 col[1] = f[1]; 5524 col[2] = f[2]; 5525 if (alpha) 5526 col[3] = f[3]; 5527 } 5528 5529 if (set_current_color_edit_id) 5530 g.ColorEditCurrentID = 0; 5531 PopID(); 5532 EndGroup(); 5533 5534 // Drag and Drop Target 5535 // NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test. 5536 if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget()) 5537 { 5538 bool accepted_drag_drop = false; 5539 if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F)) 5540 { 5541 memcpy((float*)col, payload->Data, sizeof(float) * 3); // Preserve alpha if any //-V512 //-V1086 5542 value_changed = accepted_drag_drop = true; 5543 } 5544 if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F)) 5545 { 5546 memcpy((float*)col, payload->Data, sizeof(float) * components); 5547 value_changed = accepted_drag_drop = true; 5548 } 5549 5550 // Drag-drop payloads are always RGB 5551 if (accepted_drag_drop && (flags & ImGuiColorEditFlags_InputHSV)) 5552 ColorConvertRGBtoHSV(col[0], col[1], col[2], col[0], col[1], col[2]); 5553 EndDragDropTarget(); 5554 } 5555 5556 // When picker is being actively used, use its active id so IsItemActive() will function on ColorEdit4(). 5557 if (picker_active_window && g.ActiveId != 0 && g.ActiveIdWindow == picker_active_window) 5558 g.LastItemData.ID = g.ActiveId; 5559 5560 if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId 5561 MarkItemEdited(g.LastItemData.ID); 5562 5563 return value_changed; 5564 } 5565 5566 bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags) 5567 { 5568 float col4[4] = { col[0], col[1], col[2], 1.0f }; 5569 if (!ColorPicker4(label, col4, flags | ImGuiColorEditFlags_NoAlpha)) 5570 return false; 5571 col[0] = col4[0]; col[1] = col4[1]; col[2] = col4[2]; 5572 return true; 5573 } 5574 5575 // Helper for ColorPicker4() 5576 static void RenderArrowsForVerticalBar(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w, float alpha) 5577 { 5578 ImU32 alpha8 = IM_F32_TO_INT8_SAT(alpha); 5579 ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + half_sz.x + 1, pos.y), ImVec2(half_sz.x + 2, half_sz.y + 1), ImGuiDir_Right, IM_COL32(0,0,0,alpha8)); 5580 ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + half_sz.x, pos.y), half_sz, ImGuiDir_Right, IM_COL32(255,255,255,alpha8)); 5581 ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + bar_w - half_sz.x - 1, pos.y), ImVec2(half_sz.x + 2, half_sz.y + 1), ImGuiDir_Left, IM_COL32(0,0,0,alpha8)); 5582 ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + bar_w - half_sz.x, pos.y), half_sz, ImGuiDir_Left, IM_COL32(255,255,255,alpha8)); 5583 } 5584 5585 // Note: ColorPicker4() only accesses 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. 5586 // (In C++ the 'float col[4]' notation for a function argument is equivalent to 'float* col', we only specify a size to facilitate understanding of the code.) 5587 // FIXME: we adjust the big color square height based on item width, which may cause a flickering feedback loop (if automatic height makes a vertical scrollbar appears, affecting automatic width..) 5588 // FIXME: this is trying to be aware of style.Alpha but not fully correct. Also, the color wheel will have overlapping glitches with (style.Alpha < 1.0) 5589 bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags, const float* ref_col) 5590 { 5591 ImGuiContext& g = *GImGui; 5592 ImGuiWindow* window = GetCurrentWindow(); 5593 if (window->SkipItems) 5594 return false; 5595 5596 ImDrawList* draw_list = window->DrawList; 5597 ImGuiStyle& style = g.Style; 5598 ImGuiIO& io = g.IO; 5599 5600 const float width = CalcItemWidth(); 5601 const bool is_readonly = ((g.NextItemData.ItemFlags | g.CurrentItemFlags) & ImGuiItemFlags_ReadOnly) != 0; 5602 g.NextItemData.ClearFlags(); 5603 5604 PushID(label); 5605 const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0); 5606 if (set_current_color_edit_id) 5607 g.ColorEditCurrentID = window->IDStack.back(); 5608 BeginGroup(); 5609 5610 if (!(flags & ImGuiColorEditFlags_NoSidePreview)) 5611 flags |= ImGuiColorEditFlags_NoSmallPreview; 5612 5613 // Context menu: display and store options. 5614 if (!(flags & ImGuiColorEditFlags_NoOptions)) 5615 ColorPickerOptionsPopup(col, flags); 5616 5617 // Read stored options 5618 if (!(flags & ImGuiColorEditFlags_PickerMask_)) 5619 flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_PickerMask_; 5620 if (!(flags & ImGuiColorEditFlags_InputMask_)) 5621 flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_InputMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_InputMask_; 5622 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check that only 1 is selected 5623 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected 5624 if (!(flags & ImGuiColorEditFlags_NoOptions)) 5625 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_AlphaBar); 5626 5627 // Setup 5628 int components = (flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4; 5629 bool alpha_bar = (flags & ImGuiColorEditFlags_AlphaBar) && !(flags & ImGuiColorEditFlags_NoAlpha); 5630 ImVec2 picker_pos = window->DC.CursorPos; 5631 float square_sz = GetFrameHeight(); 5632 float bars_width = square_sz; // Arbitrary smallish width of Hue/Alpha picking bars 5633 float sv_picker_size = ImMax(bars_width * 1, width - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box 5634 float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x; 5635 float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x; 5636 float bars_triangles_half_sz = IM_TRUNC(bars_width * 0.20f); 5637 5638 float backup_initial_col[4]; 5639 memcpy(backup_initial_col, col, components * sizeof(float)); 5640 5641 float wheel_thickness = sv_picker_size * 0.08f; 5642 float wheel_r_outer = sv_picker_size * 0.50f; 5643 float wheel_r_inner = wheel_r_outer - wheel_thickness; 5644 ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width)*0.5f, picker_pos.y + sv_picker_size * 0.5f); 5645 5646 // Note: the triangle is displayed rotated with triangle_pa pointing to Hue, but most coordinates stays unrotated for logic. 5647 float triangle_r = wheel_r_inner - (int)(sv_picker_size * 0.027f); 5648 ImVec2 triangle_pa = ImVec2(triangle_r, 0.0f); // Hue point. 5649 ImVec2 triangle_pb = ImVec2(triangle_r * -0.5f, triangle_r * -0.866025f); // Black point. 5650 ImVec2 triangle_pc = ImVec2(triangle_r * -0.5f, triangle_r * +0.866025f); // White point. 5651 5652 float H = col[0], S = col[1], V = col[2]; 5653 float R = col[0], G = col[1], B = col[2]; 5654 if (flags & ImGuiColorEditFlags_InputRGB) 5655 { 5656 // Hue is lost when converting from grayscale rgb (saturation=0). Restore it. 5657 ColorConvertRGBtoHSV(R, G, B, H, S, V); 5658 ColorEditRestoreHS(col, &H, &S, &V); 5659 } 5660 else if (flags & ImGuiColorEditFlags_InputHSV) 5661 { 5662 ColorConvertHSVtoRGB(H, S, V, R, G, B); 5663 } 5664 5665 bool value_changed = false, value_changed_h = false, value_changed_sv = false; 5666 5667 PushItemFlag(ImGuiItemFlags_NoNav, true); 5668 if (flags & ImGuiColorEditFlags_PickerHueWheel) 5669 { 5670 // Hue wheel + SV triangle logic 5671 InvisibleButton("hsv", ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size)); 5672 if (IsItemActive() && !is_readonly) 5673 { 5674 ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center; 5675 ImVec2 current_off = g.IO.MousePos - wheel_center; 5676 float initial_dist2 = ImLengthSqr(initial_off); 5677 if (initial_dist2 >= (wheel_r_inner - 1) * (wheel_r_inner - 1) && initial_dist2 <= (wheel_r_outer + 1) * (wheel_r_outer + 1)) 5678 { 5679 // Interactive with Hue wheel 5680 H = ImAtan2(current_off.y, current_off.x) / IM_PI * 0.5f; 5681 if (H < 0.0f) 5682 H += 1.0f; 5683 value_changed = value_changed_h = true; 5684 } 5685 float cos_hue_angle = ImCos(-H * 2.0f * IM_PI); 5686 float sin_hue_angle = ImSin(-H * 2.0f * IM_PI); 5687 if (ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, ImRotate(initial_off, cos_hue_angle, sin_hue_angle))) 5688 { 5689 // Interacting with SV triangle 5690 ImVec2 current_off_unrotated = ImRotate(current_off, cos_hue_angle, sin_hue_angle); 5691 if (!ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated)) 5692 current_off_unrotated = ImTriangleClosestPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated); 5693 float uu, vv, ww; 5694 ImTriangleBarycentricCoords(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated, uu, vv, ww); 5695 V = ImClamp(1.0f - vv, 0.0001f, 1.0f); 5696 S = ImClamp(uu / V, 0.0001f, 1.0f); 5697 value_changed = value_changed_sv = true; 5698 } 5699 } 5700 if (!(flags & ImGuiColorEditFlags_NoOptions)) 5701 OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); 5702 } 5703 else if (flags & ImGuiColorEditFlags_PickerHueBar) 5704 { 5705 // SV rectangle logic 5706 InvisibleButton("sv", ImVec2(sv_picker_size, sv_picker_size)); 5707 if (IsItemActive() && !is_readonly) 5708 { 5709 S = ImSaturate((io.MousePos.x - picker_pos.x) / (sv_picker_size - 1)); 5710 V = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1)); 5711 ColorEditRestoreH(col, &H); // Greatly reduces hue jitter and reset to 0 when hue == 255 and color is rapidly modified using SV square. 5712 value_changed = value_changed_sv = true; 5713 } 5714 if (!(flags & ImGuiColorEditFlags_NoOptions)) 5715 OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); 5716 5717 // Hue bar logic 5718 SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y)); 5719 InvisibleButton("hue", ImVec2(bars_width, sv_picker_size)); 5720 if (IsItemActive() && !is_readonly) 5721 { 5722 H = ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1)); 5723 value_changed = value_changed_h = true; 5724 } 5725 } 5726 5727 // Alpha bar logic 5728 if (alpha_bar) 5729 { 5730 SetCursorScreenPos(ImVec2(bar1_pos_x, picker_pos.y)); 5731 InvisibleButton("alpha", ImVec2(bars_width, sv_picker_size)); 5732 if (IsItemActive()) 5733 { 5734 col[3] = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1)); 5735 value_changed = true; 5736 } 5737 } 5738 PopItemFlag(); // ImGuiItemFlags_NoNav 5739 5740 if (!(flags & ImGuiColorEditFlags_NoSidePreview)) 5741 { 5742 SameLine(0, style.ItemInnerSpacing.x); 5743 BeginGroup(); 5744 } 5745 5746 if (!(flags & ImGuiColorEditFlags_NoLabel)) 5747 { 5748 const char* label_display_end = FindRenderedTextEnd(label); 5749 if (label != label_display_end) 5750 { 5751 if ((flags & ImGuiColorEditFlags_NoSidePreview)) 5752 SameLine(0, style.ItemInnerSpacing.x); 5753 TextEx(label, label_display_end); 5754 } 5755 } 5756 5757 if (!(flags & ImGuiColorEditFlags_NoSidePreview)) 5758 { 5759 PushItemFlag(ImGuiItemFlags_NoNavDefaultFocus, true); 5760 ImVec4 col_v4(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]); 5761 if ((flags & ImGuiColorEditFlags_NoLabel)) 5762 Text("Current"); 5763 5764 ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf | ImGuiColorEditFlags_NoTooltip; 5765 ColorButton("##current", col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2)); 5766 if (ref_col != NULL) 5767 { 5768 Text("Original"); 5769 ImVec4 ref_col_v4(ref_col[0], ref_col[1], ref_col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : ref_col[3]); 5770 if (ColorButton("##original", ref_col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2))) 5771 { 5772 memcpy(col, ref_col, components * sizeof(float)); 5773 value_changed = true; 5774 } 5775 } 5776 PopItemFlag(); 5777 EndGroup(); 5778 } 5779 5780 // Convert back color to RGB 5781 if (value_changed_h || value_changed_sv) 5782 { 5783 if (flags & ImGuiColorEditFlags_InputRGB) 5784 { 5785 ColorConvertHSVtoRGB(H, S, V, col[0], col[1], col[2]); 5786 g.ColorEditSavedHue = H; 5787 g.ColorEditSavedSat = S; 5788 g.ColorEditSavedID = g.ColorEditCurrentID; 5789 g.ColorEditSavedColor = ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0)); 5790 } 5791 else if (flags & ImGuiColorEditFlags_InputHSV) 5792 { 5793 col[0] = H; 5794 col[1] = S; 5795 col[2] = V; 5796 } 5797 } 5798 5799 // R,G,B and H,S,V slider color editor 5800 bool value_changed_fix_hue_wrap = false; 5801 if ((flags & ImGuiColorEditFlags_NoInputs) == 0) 5802 { 5803 PushItemWidth((alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x); 5804 ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoSmallPreview | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf; 5805 ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker; 5806 if (flags & ImGuiColorEditFlags_DisplayRGB || (flags & ImGuiColorEditFlags_DisplayMask_) == 0) 5807 if (ColorEdit4("##rgb", col, sub_flags | ImGuiColorEditFlags_DisplayRGB)) 5808 { 5809 // FIXME: Hackily differentiating using the DragInt (ActiveId != 0 && !ActiveIdAllowOverlap) vs. using the InputText or DropTarget. 5810 // For the later we don't want to run the hue-wrap canceling code. If you are well versed in HSV picker please provide your input! (See #2050) 5811 value_changed_fix_hue_wrap = (g.ActiveId != 0 && !g.ActiveIdAllowOverlap); 5812 value_changed = true; 5813 } 5814 if (flags & ImGuiColorEditFlags_DisplayHSV || (flags & ImGuiColorEditFlags_DisplayMask_) == 0) 5815 value_changed |= ColorEdit4("##hsv", col, sub_flags | ImGuiColorEditFlags_DisplayHSV); 5816 if (flags & ImGuiColorEditFlags_DisplayHex || (flags & ImGuiColorEditFlags_DisplayMask_) == 0) 5817 value_changed |= ColorEdit4("##hex", col, sub_flags | ImGuiColorEditFlags_DisplayHex); 5818 PopItemWidth(); 5819 } 5820 5821 // Try to cancel hue wrap (after ColorEdit4 call), if any 5822 if (value_changed_fix_hue_wrap && (flags & ImGuiColorEditFlags_InputRGB)) 5823 { 5824 float new_H, new_S, new_V; 5825 ColorConvertRGBtoHSV(col[0], col[1], col[2], new_H, new_S, new_V); 5826 if (new_H <= 0 && H > 0) 5827 { 5828 if (new_V <= 0 && V != new_V) 5829 ColorConvertHSVtoRGB(H, S, new_V <= 0 ? V * 0.5f : new_V, col[0], col[1], col[2]); 5830 else if (new_S <= 0) 5831 ColorConvertHSVtoRGB(H, new_S <= 0 ? S * 0.5f : new_S, new_V, col[0], col[1], col[2]); 5832 } 5833 } 5834 5835 if (value_changed) 5836 { 5837 if (flags & ImGuiColorEditFlags_InputRGB) 5838 { 5839 R = col[0]; 5840 G = col[1]; 5841 B = col[2]; 5842 ColorConvertRGBtoHSV(R, G, B, H, S, V); 5843 ColorEditRestoreHS(col, &H, &S, &V); // Fix local Hue as display below will use it immediately. 5844 } 5845 else if (flags & ImGuiColorEditFlags_InputHSV) 5846 { 5847 H = col[0]; 5848 S = col[1]; 5849 V = col[2]; 5850 ColorConvertHSVtoRGB(H, S, V, R, G, B); 5851 } 5852 } 5853 5854 const int style_alpha8 = IM_F32_TO_INT8_SAT(style.Alpha); 5855 const ImU32 col_black = IM_COL32(0,0,0,style_alpha8); 5856 const ImU32 col_white = IM_COL32(255,255,255,style_alpha8); 5857 const ImU32 col_midgrey = IM_COL32(128,128,128,style_alpha8); 5858 const ImU32 col_hues[6 + 1] = { IM_COL32(255,0,0,style_alpha8), IM_COL32(255,255,0,style_alpha8), IM_COL32(0,255,0,style_alpha8), IM_COL32(0,255,255,style_alpha8), IM_COL32(0,0,255,style_alpha8), IM_COL32(255,0,255,style_alpha8), IM_COL32(255,0,0,style_alpha8) }; 5859 5860 ImVec4 hue_color_f(1, 1, 1, style.Alpha); ColorConvertHSVtoRGB(H, 1, 1, hue_color_f.x, hue_color_f.y, hue_color_f.z); 5861 ImU32 hue_color32 = ColorConvertFloat4ToU32(hue_color_f); 5862 ImU32 user_col32_striped_of_alpha = ColorConvertFloat4ToU32(ImVec4(R, G, B, style.Alpha)); // Important: this is still including the main rendering/style alpha!! 5863 5864 ImVec2 sv_cursor_pos; 5865 5866 if (flags & ImGuiColorEditFlags_PickerHueWheel) 5867 { 5868 // Render Hue Wheel 5869 const float aeps = 0.5f / wheel_r_outer; // Half a pixel arc length in radians (2pi cancels out). 5870 const int segment_per_arc = ImMax(4, (int)wheel_r_outer / 12); 5871 for (int n = 0; n < 6; n++) 5872 { 5873 const float a0 = (n) /6.0f * 2.0f * IM_PI - aeps; 5874 const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps; 5875 const int vert_start_idx = draw_list->VtxBuffer.Size; 5876 draw_list->PathArcTo(wheel_center, (wheel_r_inner + wheel_r_outer)*0.5f, a0, a1, segment_per_arc); 5877 draw_list->PathStroke(col_white, 0, wheel_thickness); 5878 const int vert_end_idx = draw_list->VtxBuffer.Size; 5879 5880 // Paint colors over existing vertices 5881 ImVec2 gradient_p0(wheel_center.x + ImCos(a0) * wheel_r_inner, wheel_center.y + ImSin(a0) * wheel_r_inner); 5882 ImVec2 gradient_p1(wheel_center.x + ImCos(a1) * wheel_r_inner, wheel_center.y + ImSin(a1) * wheel_r_inner); 5883 ShadeVertsLinearColorGradientKeepAlpha(draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, col_hues[n], col_hues[n + 1]); 5884 } 5885 5886 // Render Cursor + preview on Hue Wheel 5887 float cos_hue_angle = ImCos(H * 2.0f * IM_PI); 5888 float sin_hue_angle = ImSin(H * 2.0f * IM_PI); 5889 ImVec2 hue_cursor_pos(wheel_center.x + cos_hue_angle * (wheel_r_inner + wheel_r_outer) * 0.5f, wheel_center.y + sin_hue_angle * (wheel_r_inner + wheel_r_outer) * 0.5f); 5890 float hue_cursor_rad = value_changed_h ? wheel_thickness * 0.65f : wheel_thickness * 0.55f; 5891 int hue_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(hue_cursor_rad); // Lock segment count so the +1 one matches others. 5892 draw_list->AddCircleFilled(hue_cursor_pos, hue_cursor_rad, hue_color32, hue_cursor_segments); 5893 draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad + 1, col_midgrey, hue_cursor_segments); 5894 draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad, col_white, hue_cursor_segments); 5895 5896 // Render SV triangle (rotated according to hue) 5897 ImVec2 tra = wheel_center + ImRotate(triangle_pa, cos_hue_angle, sin_hue_angle); 5898 ImVec2 trb = wheel_center + ImRotate(triangle_pb, cos_hue_angle, sin_hue_angle); 5899 ImVec2 trc = wheel_center + ImRotate(triangle_pc, cos_hue_angle, sin_hue_angle); 5900 ImVec2 uv_white = GetFontTexUvWhitePixel(); 5901 draw_list->PrimReserve(3, 3); 5902 draw_list->PrimVtx(tra, uv_white, hue_color32); 5903 draw_list->PrimVtx(trb, uv_white, col_black); 5904 draw_list->PrimVtx(trc, uv_white, col_white); 5905 draw_list->AddTriangle(tra, trb, trc, col_midgrey, 1.5f); 5906 sv_cursor_pos = ImLerp(ImLerp(trc, tra, ImSaturate(S)), trb, ImSaturate(1 - V)); 5907 } 5908 else if (flags & ImGuiColorEditFlags_PickerHueBar) 5909 { 5910 // Render SV Square 5911 draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), col_white, hue_color32, hue_color32, col_white); 5912 draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0, 0, col_black, col_black); 5913 RenderFrameBorder(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0.0f); 5914 sv_cursor_pos.x = ImClamp(IM_ROUND(picker_pos.x + ImSaturate(S) * sv_picker_size), picker_pos.x + 2, picker_pos.x + sv_picker_size - 2); // Sneakily prevent the circle to stick out too much 5915 sv_cursor_pos.y = ImClamp(IM_ROUND(picker_pos.y + ImSaturate(1 - V) * sv_picker_size), picker_pos.y + 2, picker_pos.y + sv_picker_size - 2); 5916 5917 // Render Hue Bar 5918 for (int i = 0; i < 6; ++i) 5919 draw_list->AddRectFilledMultiColor(ImVec2(bar0_pos_x, picker_pos.y + i * (sv_picker_size / 6)), ImVec2(bar0_pos_x + bars_width, picker_pos.y + (i + 1) * (sv_picker_size / 6)), col_hues[i], col_hues[i], col_hues[i + 1], col_hues[i + 1]); 5920 float bar0_line_y = IM_ROUND(picker_pos.y + H * sv_picker_size); 5921 RenderFrameBorder(ImVec2(bar0_pos_x, picker_pos.y), ImVec2(bar0_pos_x + bars_width, picker_pos.y + sv_picker_size), 0.0f); 5922 RenderArrowsForVerticalBar(draw_list, ImVec2(bar0_pos_x - 1, bar0_line_y), ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bars_width + 2.0f, style.Alpha); 5923 } 5924 5925 // Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range) 5926 float sv_cursor_rad = value_changed_sv ? wheel_thickness * 0.55f : wheel_thickness * 0.40f; 5927 int sv_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(sv_cursor_rad); // Lock segment count so the +1 one matches others. 5928 draw_list->AddCircleFilled(sv_cursor_pos, sv_cursor_rad, user_col32_striped_of_alpha, sv_cursor_segments); 5929 draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad + 1, col_midgrey, sv_cursor_segments); 5930 draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad, col_white, sv_cursor_segments); 5931 5932 // Render alpha bar 5933 if (alpha_bar) 5934 { 5935 float alpha = ImSaturate(col[3]); 5936 ImRect bar1_bb(bar1_pos_x, picker_pos.y, bar1_pos_x + bars_width, picker_pos.y + sv_picker_size); 5937 RenderColorRectWithAlphaCheckerboard(draw_list, bar1_bb.Min, bar1_bb.Max, 0, bar1_bb.GetWidth() / 2.0f, ImVec2(0.0f, 0.0f)); 5938 draw_list->AddRectFilledMultiColor(bar1_bb.Min, bar1_bb.Max, user_col32_striped_of_alpha, user_col32_striped_of_alpha, user_col32_striped_of_alpha & ~IM_COL32_A_MASK, user_col32_striped_of_alpha & ~IM_COL32_A_MASK); 5939 float bar1_line_y = IM_ROUND(picker_pos.y + (1.0f - alpha) * sv_picker_size); 5940 RenderFrameBorder(bar1_bb.Min, bar1_bb.Max, 0.0f); 5941 RenderArrowsForVerticalBar(draw_list, ImVec2(bar1_pos_x - 1, bar1_line_y), ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bars_width + 2.0f, style.Alpha); 5942 } 5943 5944 EndGroup(); 5945 5946 if (value_changed && memcmp(backup_initial_col, col, components * sizeof(float)) == 0) 5947 value_changed = false; 5948 if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId 5949 MarkItemEdited(g.LastItemData.ID); 5950 5951 if (set_current_color_edit_id) 5952 g.ColorEditCurrentID = 0; 5953 PopID(); 5954 5955 return value_changed; 5956 } 5957 5958 // A little color square. Return true when clicked. 5959 // FIXME: May want to display/ignore the alpha component in the color display? Yet show it in the tooltip. 5960 // 'desc_id' is not called 'label' because we don't display it next to the button, but only in the tooltip. 5961 // Note that 'col' may be encoded in HSV if ImGuiColorEditFlags_InputHSV is set. 5962 bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, const ImVec2& size_arg) 5963 { 5964 ImGuiWindow* window = GetCurrentWindow(); 5965 if (window->SkipItems) 5966 return false; 5967 5968 ImGuiContext& g = *GImGui; 5969 const ImGuiID id = window->GetID(desc_id); 5970 const float default_size = GetFrameHeight(); 5971 const ImVec2 size(size_arg.x == 0.0f ? default_size : size_arg.x, size_arg.y == 0.0f ? default_size : size_arg.y); 5972 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); 5973 ItemSize(bb, (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f); 5974 if (!ItemAdd(bb, id)) 5975 return false; 5976 5977 bool hovered, held; 5978 bool pressed = ButtonBehavior(bb, id, &hovered, &held); 5979 5980 if (flags & ImGuiColorEditFlags_NoAlpha) 5981 flags &= ~(ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf); 5982 5983 ImVec4 col_rgb = col; 5984 if (flags & ImGuiColorEditFlags_InputHSV) 5985 ColorConvertHSVtoRGB(col_rgb.x, col_rgb.y, col_rgb.z, col_rgb.x, col_rgb.y, col_rgb.z); 5986 5987 ImVec4 col_rgb_without_alpha(col_rgb.x, col_rgb.y, col_rgb.z, 1.0f); 5988 float grid_step = ImMin(size.x, size.y) / 2.99f; 5989 float rounding = ImMin(g.Style.FrameRounding, grid_step * 0.5f); 5990 ImRect bb_inner = bb; 5991 float off = 0.0f; 5992 if ((flags & ImGuiColorEditFlags_NoBorder) == 0) 5993 { 5994 off = -0.75f; // The border (using Col_FrameBg) tends to look off when color is near-opaque and rounding is enabled. This offset seemed like a good middle ground to reduce those artifacts. 5995 bb_inner.Expand(off); 5996 } 5997 if ((flags & ImGuiColorEditFlags_AlphaPreviewHalf) && col_rgb.w < 1.0f) 5998 { 5999 float mid_x = IM_ROUND((bb_inner.Min.x + bb_inner.Max.x) * 0.5f); 6000 RenderColorRectWithAlphaCheckerboard(window->DrawList, ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), bb_inner.Max, GetColorU32(col_rgb), grid_step, ImVec2(-grid_step + off, off), rounding, ImDrawFlags_RoundCornersRight); 6001 window->DrawList->AddRectFilled(bb_inner.Min, ImVec2(mid_x, bb_inner.Max.y), GetColorU32(col_rgb_without_alpha), rounding, ImDrawFlags_RoundCornersLeft); 6002 } 6003 else 6004 { 6005 // Because GetColorU32() multiplies by the global style Alpha and we don't want to display a checkerboard if the source code had no alpha 6006 ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaPreview) ? col_rgb : col_rgb_without_alpha; 6007 if (col_source.w < 1.0f) 6008 RenderColorRectWithAlphaCheckerboard(window->DrawList, bb_inner.Min, bb_inner.Max, GetColorU32(col_source), grid_step, ImVec2(off, off), rounding); 6009 else 6010 window->DrawList->AddRectFilled(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), rounding); 6011 } 6012 RenderNavHighlight(bb, id); 6013 if ((flags & ImGuiColorEditFlags_NoBorder) == 0) 6014 { 6015 if (g.Style.FrameBorderSize > 0.0f) 6016 RenderFrameBorder(bb.Min, bb.Max, rounding); 6017 else 6018 window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), rounding); // Color button are often in need of some sort of border 6019 } 6020 6021 // Drag and Drop Source 6022 // NB: The ActiveId test is merely an optional micro-optimization, BeginDragDropSource() does the same test. 6023 if (g.ActiveId == id && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropSource()) 6024 { 6025 if (flags & ImGuiColorEditFlags_NoAlpha) 6026 SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F, &col_rgb, sizeof(float) * 3, ImGuiCond_Once); 6027 else 6028 SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, &col_rgb, sizeof(float) * 4, ImGuiCond_Once); 6029 ColorButton(desc_id, col, flags); 6030 SameLine(); 6031 TextEx("Color"); 6032 EndDragDropSource(); 6033 } 6034 6035 // Tooltip 6036 if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered && IsItemHovered(ImGuiHoveredFlags_ForTooltip)) 6037 ColorTooltip(desc_id, &col.x, flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)); 6038 6039 return pressed; 6040 } 6041 6042 // Initialize/override default color options 6043 void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags) 6044 { 6045 ImGuiContext& g = *GImGui; 6046 if ((flags & ImGuiColorEditFlags_DisplayMask_) == 0) 6047 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DisplayMask_; 6048 if ((flags & ImGuiColorEditFlags_DataTypeMask_) == 0) 6049 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DataTypeMask_; 6050 if ((flags & ImGuiColorEditFlags_PickerMask_) == 0) 6051 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_PickerMask_; 6052 if ((flags & ImGuiColorEditFlags_InputMask_) == 0) 6053 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_InputMask_; 6054 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check only 1 option is selected 6055 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DataTypeMask_)); // Check only 1 option is selected 6056 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check only 1 option is selected 6057 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check only 1 option is selected 6058 g.ColorEditOptions = flags; 6059 } 6060 6061 // Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. 6062 void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags) 6063 { 6064 ImGuiContext& g = *GImGui; 6065 6066 if (!BeginTooltipEx(ImGuiTooltipFlags_OverridePrevious, ImGuiWindowFlags_None)) 6067 return; 6068 const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text; 6069 if (text_end > text) 6070 { 6071 TextEx(text, text_end); 6072 Separator(); 6073 } 6074 6075 ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2); 6076 ImVec4 cf(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]); 6077 int cr = IM_F32_TO_INT8_SAT(col[0]), cg = IM_F32_TO_INT8_SAT(col[1]), cb = IM_F32_TO_INT8_SAT(col[2]), ca = (flags & ImGuiColorEditFlags_NoAlpha) ? 255 : IM_F32_TO_INT8_SAT(col[3]); 6078 ColorButton("##preview", cf, (flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)) | ImGuiColorEditFlags_NoTooltip, sz); 6079 SameLine(); 6080 if ((flags & ImGuiColorEditFlags_InputRGB) || !(flags & ImGuiColorEditFlags_InputMask_)) 6081 { 6082 if (flags & ImGuiColorEditFlags_NoAlpha) 6083 Text("#%02X%02X%02X\nR: %d, G: %d, B: %d\n(%.3f, %.3f, %.3f)", cr, cg, cb, cr, cg, cb, col[0], col[1], col[2]); 6084 else 6085 Text("#%02X%02X%02X%02X\nR:%d, G:%d, B:%d, A:%d\n(%.3f, %.3f, %.3f, %.3f)", cr, cg, cb, ca, cr, cg, cb, ca, col[0], col[1], col[2], col[3]); 6086 } 6087 else if (flags & ImGuiColorEditFlags_InputHSV) 6088 { 6089 if (flags & ImGuiColorEditFlags_NoAlpha) 6090 Text("H: %.3f, S: %.3f, V: %.3f", col[0], col[1], col[2]); 6091 else 6092 Text("H: %.3f, S: %.3f, V: %.3f, A: %.3f", col[0], col[1], col[2], col[3]); 6093 } 6094 EndTooltip(); 6095 } 6096 6097 void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags) 6098 { 6099 bool allow_opt_inputs = !(flags & ImGuiColorEditFlags_DisplayMask_); 6100 bool allow_opt_datatype = !(flags & ImGuiColorEditFlags_DataTypeMask_); 6101 if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup("context")) 6102 return; 6103 ImGuiContext& g = *GImGui; 6104 g.LockMarkEdited++; 6105 ImGuiColorEditFlags opts = g.ColorEditOptions; 6106 if (allow_opt_inputs) 6107 { 6108 if (RadioButton("RGB", (opts & ImGuiColorEditFlags_DisplayRGB) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayRGB; 6109 if (RadioButton("HSV", (opts & ImGuiColorEditFlags_DisplayHSV) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHSV; 6110 if (RadioButton("Hex", (opts & ImGuiColorEditFlags_DisplayHex) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHex; 6111 } 6112 if (allow_opt_datatype) 6113 { 6114 if (allow_opt_inputs) Separator(); 6115 if (RadioButton("0..255", (opts & ImGuiColorEditFlags_Uint8) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Uint8; 6116 if (RadioButton("0.00..1.00", (opts & ImGuiColorEditFlags_Float) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Float; 6117 } 6118 6119 if (allow_opt_inputs || allow_opt_datatype) 6120 Separator(); 6121 if (Button("Copy as..", ImVec2(-1, 0))) 6122 OpenPopup("Copy"); 6123 if (BeginPopup("Copy")) 6124 { 6125 int cr = IM_F32_TO_INT8_SAT(col[0]), cg = IM_F32_TO_INT8_SAT(col[1]), cb = IM_F32_TO_INT8_SAT(col[2]), ca = (flags & ImGuiColorEditFlags_NoAlpha) ? 255 : IM_F32_TO_INT8_SAT(col[3]); 6126 char buf[64]; 6127 ImFormatString(buf, IM_ARRAYSIZE(buf), "(%.3ff, %.3ff, %.3ff, %.3ff)", col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]); 6128 if (Selectable(buf)) 6129 SetClipboardText(buf); 6130 ImFormatString(buf, IM_ARRAYSIZE(buf), "(%d,%d,%d,%d)", cr, cg, cb, ca); 6131 if (Selectable(buf)) 6132 SetClipboardText(buf); 6133 ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", cr, cg, cb); 6134 if (Selectable(buf)) 6135 SetClipboardText(buf); 6136 if (!(flags & ImGuiColorEditFlags_NoAlpha)) 6137 { 6138 ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X%02X", cr, cg, cb, ca); 6139 if (Selectable(buf)) 6140 SetClipboardText(buf); 6141 } 6142 EndPopup(); 6143 } 6144 6145 g.ColorEditOptions = opts; 6146 EndPopup(); 6147 g.LockMarkEdited--; 6148 } 6149 6150 void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags) 6151 { 6152 bool allow_opt_picker = !(flags & ImGuiColorEditFlags_PickerMask_); 6153 bool allow_opt_alpha_bar = !(flags & ImGuiColorEditFlags_NoAlpha) && !(flags & ImGuiColorEditFlags_AlphaBar); 6154 if ((!allow_opt_picker && !allow_opt_alpha_bar) || !BeginPopup("context")) 6155 return; 6156 ImGuiContext& g = *GImGui; 6157 g.LockMarkEdited++; 6158 if (allow_opt_picker) 6159 { 6160 ImVec2 picker_size(g.FontSize * 8, ImMax(g.FontSize * 8 - (GetFrameHeight() + g.Style.ItemInnerSpacing.x), 1.0f)); // FIXME: Picker size copied from main picker function 6161 PushItemWidth(picker_size.x); 6162 for (int picker_type = 0; picker_type < 2; picker_type++) 6163 { 6164 // Draw small/thumbnail version of each picker type (over an invisible button for selection) 6165 if (picker_type > 0) Separator(); 6166 PushID(picker_type); 6167 ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoSidePreview | (flags & ImGuiColorEditFlags_NoAlpha); 6168 if (picker_type == 0) picker_flags |= ImGuiColorEditFlags_PickerHueBar; 6169 if (picker_type == 1) picker_flags |= ImGuiColorEditFlags_PickerHueWheel; 6170 ImVec2 backup_pos = GetCursorScreenPos(); 6171 if (Selectable("##selectable", false, 0, picker_size)) // By default, Selectable() is closing popup 6172 g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags_PickerMask_) | (picker_flags & ImGuiColorEditFlags_PickerMask_); 6173 SetCursorScreenPos(backup_pos); 6174 ImVec4 previewing_ref_col; 6175 memcpy(&previewing_ref_col, ref_col, sizeof(float) * ((picker_flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4)); 6176 ColorPicker4("##previewing_picker", &previewing_ref_col.x, picker_flags); 6177 PopID(); 6178 } 6179 PopItemWidth(); 6180 } 6181 if (allow_opt_alpha_bar) 6182 { 6183 if (allow_opt_picker) Separator(); 6184 CheckboxFlags("Alpha Bar", &g.ColorEditOptions, ImGuiColorEditFlags_AlphaBar); 6185 } 6186 EndPopup(); 6187 g.LockMarkEdited--; 6188 } 6189 6190 //------------------------------------------------------------------------- 6191 // [SECTION] Widgets: TreeNode, CollapsingHeader, etc. 6192 //------------------------------------------------------------------------- 6193 // - TreeNode() 6194 // - TreeNodeV() 6195 // - TreeNodeEx() 6196 // - TreeNodeExV() 6197 // - TreeNodeBehavior() [Internal] 6198 // - TreePush() 6199 // - TreePop() 6200 // - GetTreeNodeToLabelSpacing() 6201 // - SetNextItemOpen() 6202 // - CollapsingHeader() 6203 //------------------------------------------------------------------------- 6204 6205 bool ImGui::TreeNode(const char* str_id, const char* fmt, ...) 6206 { 6207 va_list args; 6208 va_start(args, fmt); 6209 bool is_open = TreeNodeExV(str_id, 0, fmt, args); 6210 va_end(args); 6211 return is_open; 6212 } 6213 6214 bool ImGui::TreeNode(const void* ptr_id, const char* fmt, ...) 6215 { 6216 va_list args; 6217 va_start(args, fmt); 6218 bool is_open = TreeNodeExV(ptr_id, 0, fmt, args); 6219 va_end(args); 6220 return is_open; 6221 } 6222 6223 bool ImGui::TreeNode(const char* label) 6224 { 6225 ImGuiWindow* window = GetCurrentWindow(); 6226 if (window->SkipItems) 6227 return false; 6228 ImGuiID id = window->GetID(label); 6229 return TreeNodeBehavior(id, ImGuiTreeNodeFlags_None, label, NULL); 6230 } 6231 6232 bool ImGui::TreeNodeV(const char* str_id, const char* fmt, va_list args) 6233 { 6234 return TreeNodeExV(str_id, 0, fmt, args); 6235 } 6236 6237 bool ImGui::TreeNodeV(const void* ptr_id, const char* fmt, va_list args) 6238 { 6239 return TreeNodeExV(ptr_id, 0, fmt, args); 6240 } 6241 6242 bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags) 6243 { 6244 ImGuiWindow* window = GetCurrentWindow(); 6245 if (window->SkipItems) 6246 return false; 6247 ImGuiID id = window->GetID(label); 6248 return TreeNodeBehavior(id, flags, label, NULL); 6249 } 6250 6251 bool ImGui::TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) 6252 { 6253 va_list args; 6254 va_start(args, fmt); 6255 bool is_open = TreeNodeExV(str_id, flags, fmt, args); 6256 va_end(args); 6257 return is_open; 6258 } 6259 6260 bool ImGui::TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) 6261 { 6262 va_list args; 6263 va_start(args, fmt); 6264 bool is_open = TreeNodeExV(ptr_id, flags, fmt, args); 6265 va_end(args); 6266 return is_open; 6267 } 6268 6269 bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) 6270 { 6271 ImGuiWindow* window = GetCurrentWindow(); 6272 if (window->SkipItems) 6273 return false; 6274 6275 ImGuiID id = window->GetID(str_id); 6276 const char* label, *label_end; 6277 ImFormatStringToTempBufferV(&label, &label_end, fmt, args); 6278 return TreeNodeBehavior(id, flags, label, label_end); 6279 } 6280 6281 bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) 6282 { 6283 ImGuiWindow* window = GetCurrentWindow(); 6284 if (window->SkipItems) 6285 return false; 6286 6287 ImGuiID id = window->GetID(ptr_id); 6288 const char* label, *label_end; 6289 ImFormatStringToTempBufferV(&label, &label_end, fmt, args); 6290 return TreeNodeBehavior(id, flags, label, label_end); 6291 } 6292 6293 bool ImGui::TreeNodeGetOpen(ImGuiID storage_id) 6294 { 6295 ImGuiContext& g = *GImGui; 6296 ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage; 6297 return storage->GetInt(storage_id, 0) != 0; 6298 } 6299 6300 void ImGui::TreeNodeSetOpen(ImGuiID storage_id, bool open) 6301 { 6302 ImGuiContext& g = *GImGui; 6303 ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage; 6304 storage->SetInt(storage_id, open ? 1 : 0); 6305 } 6306 6307 bool ImGui::TreeNodeUpdateNextOpen(ImGuiID storage_id, ImGuiTreeNodeFlags flags) 6308 { 6309 if (flags & ImGuiTreeNodeFlags_Leaf) 6310 return true; 6311 6312 // We only write to the tree storage if the user clicks, or explicitly use the SetNextItemOpen function 6313 ImGuiContext& g = *GImGui; 6314 ImGuiWindow* window = g.CurrentWindow; 6315 ImGuiStorage* storage = window->DC.StateStorage; 6316 6317 bool is_open; 6318 if (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasOpen) 6319 { 6320 if (g.NextItemData.OpenCond & ImGuiCond_Always) 6321 { 6322 is_open = g.NextItemData.OpenVal; 6323 TreeNodeSetOpen(storage_id, is_open); 6324 } 6325 else 6326 { 6327 // We treat ImGuiCond_Once and ImGuiCond_FirstUseEver the same because tree node state are not saved persistently. 6328 const int stored_value = storage->GetInt(storage_id, -1); 6329 if (stored_value == -1) 6330 { 6331 is_open = g.NextItemData.OpenVal; 6332 TreeNodeSetOpen(storage_id, is_open); 6333 } 6334 else 6335 { 6336 is_open = stored_value != 0; 6337 } 6338 } 6339 } 6340 else 6341 { 6342 is_open = storage->GetInt(storage_id, (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0; 6343 } 6344 6345 // When logging is enabled, we automatically expand tree nodes (but *NOT* collapsing headers.. seems like sensible behavior). 6346 // NB- If we are above max depth we still allow manually opened nodes to be logged. 6347 if (g.LogEnabled && !(flags & ImGuiTreeNodeFlags_NoAutoOpenOnLog) && (window->DC.TreeDepth - g.LogDepthRef) < g.LogDepthToExpand) 6348 is_open = true; 6349 6350 return is_open; 6351 } 6352 6353 // Store ImGuiTreeNodeStackData for just submitted node. 6354 // Currently only supports 32 level deep and we are fine with (1 << Depth) overflowing into a zero, easy to increase. 6355 static void TreeNodeStoreStackData(ImGuiTreeNodeFlags flags) 6356 { 6357 ImGuiContext& g = *GImGui; 6358 ImGuiWindow* window = g.CurrentWindow; 6359 6360 g.TreeNodeStack.resize(g.TreeNodeStack.Size + 1); 6361 ImGuiTreeNodeStackData* tree_node_data = &g.TreeNodeStack.back(); 6362 tree_node_data->ID = g.LastItemData.ID; 6363 tree_node_data->TreeFlags = flags; 6364 tree_node_data->InFlags = g.LastItemData.InFlags; 6365 tree_node_data->NavRect = g.LastItemData.NavRect; 6366 window->DC.TreeHasStackDataDepthMask |= (1 << window->DC.TreeDepth); 6367 } 6368 6369 // When using public API, currently 'id == storage_id' is always true, but we separate the values to facilitate advanced user code doing storage queries outside of UI loop. 6370 bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end) 6371 { 6372 ImGuiWindow* window = GetCurrentWindow(); 6373 if (window->SkipItems) 6374 return false; 6375 6376 ImGuiContext& g = *GImGui; 6377 const ImGuiStyle& style = g.Style; 6378 const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0; 6379 const ImVec2 padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, ImMin(window->DC.CurrLineTextBaseOffset, style.FramePadding.y)); 6380 6381 if (!label_end) 6382 label_end = FindRenderedTextEnd(label); 6383 const ImVec2 label_size = CalcTextSize(label, label_end, false); 6384 6385 const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapsing arrow width + Spacing 6386 const float text_offset_y = ImMax(padding.y, window->DC.CurrLineTextBaseOffset); // Latch before ItemSize changes it 6387 const float text_width = g.FontSize + label_size.x + padding.x * 2; // Include collapsing arrow 6388 6389 // We vertically grow up to current line height up the typical widget height. 6390 const float frame_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y * 2), label_size.y + padding.y * 2); 6391 const bool span_all_columns = (flags & ImGuiTreeNodeFlags_SpanAllColumns) != 0 && (g.CurrentTable != NULL); 6392 ImRect frame_bb; 6393 frame_bb.Min.x = span_all_columns ? window->ParentWorkRect.Min.x : (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x; 6394 frame_bb.Min.y = window->DC.CursorPos.y; 6395 frame_bb.Max.x = span_all_columns ? window->ParentWorkRect.Max.x : (flags & ImGuiTreeNodeFlags_SpanTextWidth) ? window->DC.CursorPos.x + text_width + padding.x : window->WorkRect.Max.x; 6396 frame_bb.Max.y = window->DC.CursorPos.y + frame_height; 6397 if (display_frame) 6398 { 6399 const float outer_extend = IM_TRUNC(window->WindowPadding.x * 0.5f); // Framed header expand a little outside of current limits 6400 frame_bb.Min.x -= outer_extend; 6401 frame_bb.Max.x += outer_extend; 6402 } 6403 6404 ImVec2 text_pos(window->DC.CursorPos.x + text_offset_x, window->DC.CursorPos.y + text_offset_y); 6405 ItemSize(ImVec2(text_width, frame_height), padding.y); 6406 6407 // For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing 6408 ImRect interact_bb = frame_bb; 6409 if ((flags & (ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_SpanTextWidth | ImGuiTreeNodeFlags_SpanAllColumns)) == 0) 6410 interact_bb.Max.x = frame_bb.Min.x + text_width + (label_size.x > 0.0f ? style.ItemSpacing.x * 2.0f : 0.0f); 6411 6412 // Compute open and multi-select states before ItemAdd() as it clear NextItem data. 6413 ImGuiID storage_id = (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasStorageID) ? g.NextItemData.StorageId : id; 6414 bool is_open = TreeNodeUpdateNextOpen(storage_id, flags); 6415 6416 bool is_visible; 6417 if (span_all_columns) 6418 { 6419 // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable.. 6420 const float backup_clip_rect_min_x = window->ClipRect.Min.x; 6421 const float backup_clip_rect_max_x = window->ClipRect.Max.x; 6422 window->ClipRect.Min.x = window->ParentWorkRect.Min.x; 6423 window->ClipRect.Max.x = window->ParentWorkRect.Max.x; 6424 is_visible = ItemAdd(interact_bb, id); 6425 window->ClipRect.Min.x = backup_clip_rect_min_x; 6426 window->ClipRect.Max.x = backup_clip_rect_max_x; 6427 } 6428 else 6429 { 6430 is_visible = ItemAdd(interact_bb, id); 6431 } 6432 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect; 6433 g.LastItemData.DisplayRect = frame_bb; 6434 6435 // If a NavLeft request is happening and ImGuiTreeNodeFlags_NavLeftJumpsBackHere enabled: 6436 // Store data for the current depth to allow returning to this node from any child item. 6437 // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop(). 6438 // It will become tempting to enable ImGuiTreeNodeFlags_NavLeftJumpsBackHere by default or move it to ImGuiStyle. 6439 bool store_tree_node_stack_data = false; 6440 if (!(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) 6441 { 6442 if ((flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && is_open && !g.NavIdIsAlive) 6443 if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet()) 6444 store_tree_node_stack_data = true; 6445 } 6446 6447 const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0; 6448 if (!is_visible) 6449 { 6450 if (store_tree_node_stack_data && is_open) 6451 TreeNodeStoreStackData(flags); // Call before TreePushOverrideID() 6452 if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) 6453 TreePushOverrideID(id); 6454 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0)); 6455 return is_open; 6456 } 6457 6458 if (span_all_columns) 6459 { 6460 TablePushBackgroundChannel(); 6461 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect; 6462 g.LastItemData.ClipRect = window->ClipRect; 6463 } 6464 6465 ImGuiButtonFlags button_flags = ImGuiTreeNodeFlags_None; 6466 if ((flags & ImGuiTreeNodeFlags_AllowOverlap) || (g.LastItemData.InFlags & ImGuiItemFlags_AllowOverlap)) 6467 button_flags |= ImGuiButtonFlags_AllowOverlap; 6468 if (!is_leaf) 6469 button_flags |= ImGuiButtonFlags_PressedOnDragDropHold; 6470 6471 // We allow clicking on the arrow section with keyboard modifiers held, in order to easily 6472 // allow browsing a tree while preserving selection with code implementing multi-selection patterns. 6473 // When clicking on the rest of the tree node we always disallow keyboard modifiers. 6474 const float arrow_hit_x1 = (text_pos.x - text_offset_x) - style.TouchExtraPadding.x; 6475 const float arrow_hit_x2 = (text_pos.x - text_offset_x) + (g.FontSize + padding.x * 2.0f) + style.TouchExtraPadding.x; 6476 const bool is_mouse_x_over_arrow = (g.IO.MousePos.x >= arrow_hit_x1 && g.IO.MousePos.x < arrow_hit_x2); 6477 6478 // Open behaviors can be altered with the _OpenOnArrow and _OnOnDoubleClick flags. 6479 // Some alteration have subtle effects (e.g. toggle on MouseUp vs MouseDown events) due to requirements for multi-selection and drag and drop support. 6480 // - Single-click on label = Toggle on MouseUp (default, when _OpenOnArrow=0) 6481 // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=0) 6482 // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=1) 6483 // - Double-click on label = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1) 6484 // - Double-click on arrow = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1 and _OpenOnArrow=0) 6485 // It is rather standard that arrow click react on Down rather than Up. 6486 // We set ImGuiButtonFlags_PressedOnClickRelease on OpenOnDoubleClick because we want the item to be active on the initial MouseDown in order for drag and drop to work. 6487 if (is_mouse_x_over_arrow) 6488 button_flags |= ImGuiButtonFlags_PressedOnClick; 6489 else if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) 6490 button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; 6491 else 6492 button_flags |= ImGuiButtonFlags_PressedOnClickRelease; 6493 6494 bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0; 6495 const bool was_selected = selected; 6496 6497 // Multi-selection support (header) 6498 const bool is_multi_select = (g.LastItemData.InFlags & ImGuiItemFlags_IsMultiSelect) != 0; 6499 if (is_multi_select) 6500 { 6501 // Handle multi-select + alter button flags for it 6502 MultiSelectItemHeader(id, &selected, &button_flags); 6503 if (is_mouse_x_over_arrow) 6504 button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease; 6505 6506 // We absolutely need to distinguish open vs select so comes by default 6507 flags |= ImGuiTreeNodeFlags_OpenOnArrow; 6508 } 6509 else 6510 { 6511 if (window != g.HoveredWindow || !is_mouse_x_over_arrow) 6512 button_flags |= ImGuiButtonFlags_NoKeyModifiers; 6513 } 6514 6515 bool hovered, held; 6516 bool pressed = ButtonBehavior(interact_bb, id, &hovered, &held, button_flags); 6517 bool toggled = false; 6518 if (!is_leaf) 6519 { 6520 if (pressed && g.DragDropHoldJustPressedId != id) 6521 { 6522 if ((flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) == 0 || (g.NavActivateId == id && !is_multi_select)) 6523 toggled = true; 6524 if (flags & ImGuiTreeNodeFlags_OpenOnArrow) 6525 toggled |= is_mouse_x_over_arrow && !g.NavDisableMouseHover; // Lightweight equivalent of IsMouseHoveringRect() since ButtonBehavior() already did the job 6526 if ((flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) && g.IO.MouseClickedCount[0] == 2) 6527 toggled = true; 6528 } 6529 else if (pressed && g.DragDropHoldJustPressedId == id) 6530 { 6531 IM_ASSERT(button_flags & ImGuiButtonFlags_PressedOnDragDropHold); 6532 if (!is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again. 6533 toggled = true; 6534 } 6535 6536 if (g.NavId == id && g.NavMoveDir == ImGuiDir_Left && is_open) 6537 { 6538 toggled = true; 6539 NavClearPreferredPosForAxis(ImGuiAxis_X); 6540 NavMoveRequestCancel(); 6541 } 6542 if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right && !is_open) // If there's something upcoming on the line we may want to give it the priority? 6543 { 6544 toggled = true; 6545 NavClearPreferredPosForAxis(ImGuiAxis_X); 6546 NavMoveRequestCancel(); 6547 } 6548 6549 if (toggled) 6550 { 6551 is_open = !is_open; 6552 window->DC.StateStorage->SetInt(storage_id, is_open); 6553 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledOpen; 6554 } 6555 } 6556 6557 // Multi-selection support (footer) 6558 if (is_multi_select) 6559 { 6560 bool pressed_copy = pressed && !toggled; 6561 MultiSelectItemFooter(id, &selected, &pressed_copy); 6562 if (pressed) 6563 SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, interact_bb); 6564 } 6565 6566 if (selected != was_selected) 6567 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection; 6568 6569 // Render 6570 { 6571 const ImU32 text_col = GetColorU32(ImGuiCol_Text); 6572 ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_Compact; 6573 if (is_multi_select) 6574 nav_highlight_flags |= ImGuiNavHighlightFlags_AlwaysDraw; // Always show the nav rectangle 6575 if (display_frame) 6576 { 6577 // Framed type 6578 const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); 6579 RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, true, style.FrameRounding); 6580 RenderNavHighlight(frame_bb, id, nav_highlight_flags); 6581 if (flags & ImGuiTreeNodeFlags_Bullet) 6582 RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.60f, text_pos.y + g.FontSize * 0.5f), text_col); 6583 else if (!is_leaf) 6584 RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y), text_col, is_open ? ((flags & ImGuiTreeNodeFlags_UpsideDownArrow) ? ImGuiDir_Up : ImGuiDir_Down) : ImGuiDir_Right, 1.0f); 6585 else // Leaf without bullet, left-adjusted text 6586 text_pos.x -= text_offset_x - padding.x; 6587 if (flags & ImGuiTreeNodeFlags_ClipLabelForTrailingButton) 6588 frame_bb.Max.x -= g.FontSize + style.FramePadding.x; 6589 if (g.LogEnabled) 6590 LogSetNextTextDecoration("###", "###"); 6591 } 6592 else 6593 { 6594 // Unframed typed for tree nodes 6595 if (hovered || selected) 6596 { 6597 const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); 6598 RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, false); 6599 } 6600 RenderNavHighlight(frame_bb, id, nav_highlight_flags); 6601 if (flags & ImGuiTreeNodeFlags_Bullet) 6602 RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.5f, text_pos.y + g.FontSize * 0.5f), text_col); 6603 else if (!is_leaf) 6604 RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y + g.FontSize * 0.15f), text_col, is_open ? ((flags & ImGuiTreeNodeFlags_UpsideDownArrow) ? ImGuiDir_Up : ImGuiDir_Down) : ImGuiDir_Right, 0.70f); 6605 if (g.LogEnabled) 6606 LogSetNextTextDecoration(">", NULL); 6607 } 6608 6609 if (span_all_columns) 6610 TablePopBackgroundChannel(); 6611 6612 // Label 6613 if (display_frame) 6614 RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size); 6615 else 6616 RenderText(text_pos, label, label_end, false); 6617 } 6618 6619 if (store_tree_node_stack_data && is_open) 6620 TreeNodeStoreStackData(flags); // Call before TreePushOverrideID() 6621 if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) 6622 TreePushOverrideID(id); // Could use TreePush(label) but this avoid computing twice 6623 6624 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0)); 6625 return is_open; 6626 } 6627 6628 void ImGui::TreePush(const char* str_id) 6629 { 6630 ImGuiWindow* window = GetCurrentWindow(); 6631 Indent(); 6632 window->DC.TreeDepth++; 6633 PushID(str_id); 6634 } 6635 6636 void ImGui::TreePush(const void* ptr_id) 6637 { 6638 ImGuiWindow* window = GetCurrentWindow(); 6639 Indent(); 6640 window->DC.TreeDepth++; 6641 PushID(ptr_id); 6642 } 6643 6644 void ImGui::TreePushOverrideID(ImGuiID id) 6645 { 6646 ImGuiContext& g = *GImGui; 6647 ImGuiWindow* window = g.CurrentWindow; 6648 Indent(); 6649 window->DC.TreeDepth++; 6650 PushOverrideID(id); 6651 } 6652 6653 void ImGui::TreePop() 6654 { 6655 ImGuiContext& g = *GImGui; 6656 ImGuiWindow* window = g.CurrentWindow; 6657 Unindent(); 6658 6659 window->DC.TreeDepth--; 6660 ImU32 tree_depth_mask = (1 << window->DC.TreeDepth); 6661 6662 if (window->DC.TreeHasStackDataDepthMask & tree_depth_mask) // Only set during request 6663 { 6664 ImGuiTreeNodeStackData* data = &g.TreeNodeStack.back(); 6665 IM_ASSERT(data->ID == window->IDStack.back()); 6666 if (data->TreeFlags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) 6667 { 6668 // Handle Left arrow to move to parent tree node (when ImGuiTreeNodeFlags_NavLeftJumpsBackHere is enabled) 6669 if (g.NavIdIsAlive && g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet()) 6670 NavMoveRequestResolveWithPastTreeNode(&g.NavMoveResultLocal, data); 6671 } 6672 g.TreeNodeStack.pop_back(); 6673 window->DC.TreeHasStackDataDepthMask &= ~tree_depth_mask; 6674 } 6675 6676 IM_ASSERT(window->IDStack.Size > 1); // There should always be 1 element in the IDStack (pushed during window creation). If this triggers you called TreePop/PopID too much. 6677 PopID(); 6678 } 6679 6680 // Horizontal distance preceding label when using TreeNode() or Bullet() 6681 float ImGui::GetTreeNodeToLabelSpacing() 6682 { 6683 ImGuiContext& g = *GImGui; 6684 return g.FontSize + (g.Style.FramePadding.x * 2.0f); 6685 } 6686 6687 // Set next TreeNode/CollapsingHeader open state. 6688 void ImGui::SetNextItemOpen(bool is_open, ImGuiCond cond) 6689 { 6690 ImGuiContext& g = *GImGui; 6691 if (g.CurrentWindow->SkipItems) 6692 return; 6693 g.NextItemData.Flags |= ImGuiNextItemDataFlags_HasOpen; 6694 g.NextItemData.OpenVal = is_open; 6695 g.NextItemData.OpenCond = (ImU8)(cond ? cond : ImGuiCond_Always); 6696 } 6697 6698 // Set next TreeNode/CollapsingHeader storage id. 6699 void ImGui::SetNextItemStorageID(ImGuiID storage_id) 6700 { 6701 ImGuiContext& g = *GImGui; 6702 if (g.CurrentWindow->SkipItems) 6703 return; 6704 g.NextItemData.Flags |= ImGuiNextItemDataFlags_HasStorageID; 6705 g.NextItemData.StorageId = storage_id; 6706 } 6707 6708 // CollapsingHeader returns true when opened but do not indent nor push into the ID stack (because of the ImGuiTreeNodeFlags_NoTreePushOnOpen flag). 6709 // This is basically the same as calling TreeNodeEx(label, ImGuiTreeNodeFlags_CollapsingHeader). You can remove the _NoTreePushOnOpen flag if you want behavior closer to normal TreeNode(). 6710 bool ImGui::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags) 6711 { 6712 ImGuiWindow* window = GetCurrentWindow(); 6713 if (window->SkipItems) 6714 return false; 6715 ImGuiID id = window->GetID(label); 6716 return TreeNodeBehavior(id, flags | ImGuiTreeNodeFlags_CollapsingHeader, label); 6717 } 6718 6719 // p_visible == NULL : regular collapsing header 6720 // p_visible != NULL && *p_visible == true : show a small close button on the corner of the header, clicking the button will set *p_visible = false 6721 // p_visible != NULL && *p_visible == false : do not show the header at all 6722 // Do not mistake this with the Open state of the header itself, which you can adjust with SetNextItemOpen() or ImGuiTreeNodeFlags_DefaultOpen. 6723 bool ImGui::CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags) 6724 { 6725 ImGuiWindow* window = GetCurrentWindow(); 6726 if (window->SkipItems) 6727 return false; 6728 6729 if (p_visible && !*p_visible) 6730 return false; 6731 6732 ImGuiID id = window->GetID(label); 6733 flags |= ImGuiTreeNodeFlags_CollapsingHeader; 6734 if (p_visible) 6735 flags |= ImGuiTreeNodeFlags_AllowOverlap | (ImGuiTreeNodeFlags)ImGuiTreeNodeFlags_ClipLabelForTrailingButton; 6736 bool is_open = TreeNodeBehavior(id, flags, label); 6737 if (p_visible != NULL) 6738 { 6739 // Create a small overlapping close button 6740 // FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc. 6741 // FIXME: CloseButton can overlap into text, need find a way to clip the text somehow. 6742 ImGuiContext& g = *GImGui; 6743 ImGuiLastItemData last_item_backup = g.LastItemData; 6744 float button_size = g.FontSize; 6745 float button_x = ImMax(g.LastItemData.Rect.Min.x, g.LastItemData.Rect.Max.x - g.Style.FramePadding.x - button_size); 6746 float button_y = g.LastItemData.Rect.Min.y + g.Style.FramePadding.y; 6747 ImGuiID close_button_id = GetIDWithSeed("#CLOSE", NULL, id); 6748 if (CloseButton(close_button_id, ImVec2(button_x, button_y))) 6749 *p_visible = false; 6750 g.LastItemData = last_item_backup; 6751 } 6752 6753 return is_open; 6754 } 6755 6756 //------------------------------------------------------------------------- 6757 // [SECTION] Widgets: Selectable 6758 //------------------------------------------------------------------------- 6759 // - Selectable() 6760 //------------------------------------------------------------------------- 6761 6762 // Tip: pass a non-visible label (e.g. "##hello") then you can use the space to draw other text or image. 6763 // But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID or use ##unique_id. 6764 // With this scheme, ImGuiSelectableFlags_SpanAllColumns and ImGuiSelectableFlags_AllowOverlap are also frequently used flags. 6765 // FIXME: Selectable() with (size.x == 0.0f) and (SelectableTextAlign.x > 0.0f) followed by SameLine() is currently not supported. 6766 bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg) 6767 { 6768 ImGuiWindow* window = GetCurrentWindow(); 6769 if (window->SkipItems) 6770 return false; 6771 6772 ImGuiContext& g = *GImGui; 6773 const ImGuiStyle& style = g.Style; 6774 6775 // Submit label or explicit size to ItemSize(), whereas ItemAdd() will submit a larger/spanning rectangle. 6776 ImGuiID id = window->GetID(label); 6777 ImVec2 label_size = CalcTextSize(label, NULL, true); 6778 ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y); 6779 ImVec2 pos = window->DC.CursorPos; 6780 pos.y += window->DC.CurrLineTextBaseOffset; 6781 ItemSize(size, 0.0f); 6782 6783 // Fill horizontal space 6784 // We don't support (size < 0.0f) in Selectable() because the ItemSpacing extension would make explicitly right-aligned sizes not visibly match other widgets. 6785 const bool span_all_columns = (flags & ImGuiSelectableFlags_SpanAllColumns) != 0; 6786 const float min_x = span_all_columns ? window->ParentWorkRect.Min.x : pos.x; 6787 const float max_x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x; 6788 if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_SpanAvailWidth)) 6789 size.x = ImMax(label_size.x, max_x - min_x); 6790 6791 // Text stays at the submission position, but bounding box may be extended on both sides 6792 const ImVec2 text_min = pos; 6793 const ImVec2 text_max(min_x + size.x, pos.y + size.y); 6794 6795 // Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable. 6796 // FIXME: Not part of layout so not included in clipper calculation, but ItemSize currently doesn't allow offsetting CursorPos. 6797 ImRect bb(min_x, pos.y, text_max.x, text_max.y); 6798 if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0) 6799 { 6800 const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x; 6801 const float spacing_y = style.ItemSpacing.y; 6802 const float spacing_L = IM_TRUNC(spacing_x * 0.50f); 6803 const float spacing_U = IM_TRUNC(spacing_y * 0.50f); 6804 bb.Min.x -= spacing_L; 6805 bb.Min.y -= spacing_U; 6806 bb.Max.x += (spacing_x - spacing_L); 6807 bb.Max.y += (spacing_y - spacing_U); 6808 } 6809 //if (g.IO.KeyCtrl) { GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(0, 255, 0, 255)); } 6810 6811 const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0; 6812 const ImGuiItemFlags extra_item_flags = disabled_item ? (ImGuiItemFlags)ImGuiItemFlags_Disabled : ImGuiItemFlags_None; 6813 bool is_visible; 6814 if (span_all_columns) 6815 { 6816 // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable.. 6817 const float backup_clip_rect_min_x = window->ClipRect.Min.x; 6818 const float backup_clip_rect_max_x = window->ClipRect.Max.x; 6819 window->ClipRect.Min.x = window->ParentWorkRect.Min.x; 6820 window->ClipRect.Max.x = window->ParentWorkRect.Max.x; 6821 is_visible = ItemAdd(bb, id, NULL, extra_item_flags); 6822 window->ClipRect.Min.x = backup_clip_rect_min_x; 6823 window->ClipRect.Max.x = backup_clip_rect_max_x; 6824 } 6825 else 6826 { 6827 is_visible = ItemAdd(bb, id, NULL, extra_item_flags); 6828 } 6829 6830 const bool is_multi_select = (g.LastItemData.InFlags & ImGuiItemFlags_IsMultiSelect) != 0; 6831 if (!is_visible) 6832 if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(bb)) // Extra layer of "no logic clip" for box-select support (would be more overhead to add to ItemAdd) 6833 return false; 6834 6835 const bool disabled_global = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0; 6836 if (disabled_item && !disabled_global) // Only testing this as an optimization 6837 BeginDisabled(); 6838 6839 // FIXME: We can standardize the behavior of those two, we could also keep the fast path of override ClipRect + full push on render only, 6840 // which would be advantageous since most selectable are not selected. 6841 if (span_all_columns) 6842 { 6843 if (g.CurrentTable) 6844 TablePushBackgroundChannel(); 6845 else if (window->DC.CurrentColumns) 6846 PushColumnsBackground(); 6847 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect; 6848 g.LastItemData.ClipRect = window->ClipRect; 6849 } 6850 6851 // We use NoHoldingActiveID on menus so user can click and _hold_ on a menu then drag to browse child entries 6852 ImGuiButtonFlags button_flags = 0; 6853 if (flags & ImGuiSelectableFlags_NoHoldingActiveID) { button_flags |= ImGuiButtonFlags_NoHoldingActiveId; } 6854 if (flags & ImGuiSelectableFlags_NoSetKeyOwner) { button_flags |= ImGuiButtonFlags_NoSetKeyOwner; } 6855 if (flags & ImGuiSelectableFlags_SelectOnClick) { button_flags |= ImGuiButtonFlags_PressedOnClick; } 6856 if (flags & ImGuiSelectableFlags_SelectOnRelease) { button_flags |= ImGuiButtonFlags_PressedOnRelease; } 6857 if (flags & ImGuiSelectableFlags_AllowDoubleClick) { button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; } 6858 if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.InFlags & ImGuiItemFlags_AllowOverlap)) { button_flags |= ImGuiButtonFlags_AllowOverlap; } 6859 6860 // Multi-selection support (header) 6861 const bool was_selected = selected; 6862 if (is_multi_select) 6863 { 6864 // Handle multi-select + alter button flags for it 6865 MultiSelectItemHeader(id, &selected, &button_flags); 6866 } 6867 6868 bool hovered, held; 6869 bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags); 6870 6871 // Multi-selection support (footer) 6872 if (is_multi_select) 6873 { 6874 MultiSelectItemFooter(id, &selected, &pressed); 6875 } 6876 else 6877 { 6878 // Auto-select when moved into 6879 // - This will be more fully fleshed in the range-select branch 6880 // - This is not exposed as it won't nicely work with some user side handling of shift/control 6881 // - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons 6882 // - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope()) 6883 // - (2) usage will fail with clipped items 6884 // The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API. 6885 if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId) 6886 if (g.NavJustMovedToId == id) 6887 selected = pressed = true; 6888 } 6889 6890 // Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with gamepad/keyboard 6891 if (pressed || (hovered && (flags & ImGuiSelectableFlags_SetNavIdOnHover))) 6892 { 6893 if (!g.NavDisableMouseHover && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent) 6894 { 6895 SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, WindowRectAbsToRel(window, bb)); // (bb == NavRect) 6896 g.NavDisableHighlight = true; 6897 } 6898 } 6899 if (pressed) 6900 MarkItemEdited(id); 6901 6902 if (selected != was_selected) 6903 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection; 6904 6905 // Render 6906 if (is_visible) 6907 { 6908 const bool highlighted = hovered || (flags & ImGuiSelectableFlags_Highlight); 6909 if (highlighted || selected) 6910 { 6911 // FIXME-MULTISELECT: Styling: Color for 'selected' elements? ImGuiCol_HeaderSelected 6912 ImU32 col; 6913 if (selected && !highlighted) 6914 col = GetColorU32(ImLerp(GetStyleColorVec4(ImGuiCol_Header), GetStyleColorVec4(ImGuiCol_HeaderHovered), 0.5f)); 6915 else 6916 col = GetColorU32((held && highlighted) ? ImGuiCol_HeaderActive : highlighted ? ImGuiCol_HeaderHovered : ImGuiCol_Header); 6917 RenderFrame(bb.Min, bb.Max, col, false, 0.0f); 6918 } 6919 if (g.NavId == id) 6920 { 6921 ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_Compact | ImGuiNavHighlightFlags_NoRounding; 6922 if (is_multi_select) 6923 nav_highlight_flags |= ImGuiNavHighlightFlags_AlwaysDraw; // Always show the nav rectangle 6924 RenderNavHighlight(bb, id, nav_highlight_flags); 6925 } 6926 } 6927 6928 if (span_all_columns) 6929 { 6930 if (g.CurrentTable) 6931 TablePopBackgroundChannel(); 6932 else if (window->DC.CurrentColumns) 6933 PopColumnsBackground(); 6934 } 6935 6936 if (is_visible) 6937 RenderTextClipped(text_min, text_max, label, NULL, &label_size, style.SelectableTextAlign, &bb); 6938 6939 // Automatically close popups 6940 if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_NoAutoClosePopups) && (g.LastItemData.InFlags & ImGuiItemFlags_AutoClosePopups)) 6941 CloseCurrentPopup(); 6942 6943 if (disabled_item && !disabled_global) 6944 EndDisabled(); 6945 6946 // Selectable() always returns a pressed state! 6947 // Users of BeginMultiSelect()/EndMultiSelect() scope: you may call ImGui::IsItemToggledSelection() to retrieve 6948 // selection toggle, only useful if you need that state updated (e.g. for rendering purpose) before reaching EndMultiSelect(). 6949 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); 6950 return pressed; //-V1020 6951 } 6952 6953 bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg) 6954 { 6955 if (Selectable(label, *p_selected, flags, size_arg)) 6956 { 6957 *p_selected = !*p_selected; 6958 return true; 6959 } 6960 return false; 6961 } 6962 6963 6964 //------------------------------------------------------------------------- 6965 // [SECTION] Widgets: Typing-Select support 6966 //------------------------------------------------------------------------- 6967 6968 // [Experimental] Currently not exposed in public API. 6969 // Consume character inputs and return search request, if any. 6970 // This would typically only be called on the focused window or location you want to grab inputs for, e.g. 6971 // if (ImGui::IsWindowFocused(...)) 6972 // if (ImGuiTypingSelectRequest* req = ImGui::GetTypingSelectRequest()) 6973 // focus_idx = ImGui::TypingSelectFindMatch(req, my_items.size(), [](void*, int n) { return my_items[n]->Name; }, &my_items, -1); 6974 // However the code is written in a way where calling it from multiple locations is safe (e.g. to obtain buffer). 6975 ImGuiTypingSelectRequest* ImGui::GetTypingSelectRequest(ImGuiTypingSelectFlags flags) 6976 { 6977 ImGuiContext& g = *GImGui; 6978 ImGuiTypingSelectState* data = &g.TypingSelectState; 6979 ImGuiTypingSelectRequest* out_request = &data->Request; 6980 6981 // Clear buffer 6982 const float TYPING_SELECT_RESET_TIMER = 1.80f; // FIXME: Potentially move to IO config. 6983 const int TYPING_SELECT_SINGLE_CHAR_COUNT_FOR_LOCK = 4; // Lock single char matching when repeating same char 4 times 6984 if (data->SearchBuffer[0] != 0) 6985 { 6986 bool clear_buffer = false; 6987 clear_buffer |= (g.NavFocusScopeId != data->FocusScope); 6988 clear_buffer |= (data->LastRequestTime + TYPING_SELECT_RESET_TIMER < g.Time); 6989 clear_buffer |= g.NavAnyRequest; 6990 clear_buffer |= g.ActiveId != 0 && g.NavActivateId == 0; // Allow temporary SPACE activation to not interfere 6991 clear_buffer |= IsKeyPressed(ImGuiKey_Escape) || IsKeyPressed(ImGuiKey_Enter); 6992 clear_buffer |= IsKeyPressed(ImGuiKey_Backspace) && (flags & ImGuiTypingSelectFlags_AllowBackspace) == 0; 6993 //if (clear_buffer) { IMGUI_DEBUG_LOG("GetTypingSelectRequest(): Clear SearchBuffer.\n"); } 6994 if (clear_buffer) 6995 data->Clear(); 6996 } 6997 6998 // Append to buffer 6999 const int buffer_max_len = IM_ARRAYSIZE(data->SearchBuffer) - 1; 7000 int buffer_len = (int)strlen(data->SearchBuffer); 7001 bool select_request = false; 7002 for (ImWchar w : g.IO.InputQueueCharacters) 7003 { 7004 const int w_len = ImTextCountUtf8BytesFromStr(&w, &w + 1); 7005 if (w < 32 || (buffer_len == 0 && ImCharIsBlankW(w)) || (buffer_len + w_len > buffer_max_len)) // Ignore leading blanks 7006 continue; 7007 char w_buf[5]; 7008 ImTextCharToUtf8(w_buf, (unsigned int)w); 7009 if (data->SingleCharModeLock && w_len == out_request->SingleCharSize && memcmp(w_buf, data->SearchBuffer, w_len) == 0) 7010 { 7011 select_request = true; // Same character: don't need to append to buffer. 7012 continue; 7013 } 7014 if (data->SingleCharModeLock) 7015 { 7016 data->Clear(); // Different character: clear 7017 buffer_len = 0; 7018 } 7019 memcpy(data->SearchBuffer + buffer_len, w_buf, w_len + 1); // Append 7020 buffer_len += w_len; 7021 select_request = true; 7022 } 7023 g.IO.InputQueueCharacters.resize(0); 7024 7025 // Handle backspace 7026 if ((flags & ImGuiTypingSelectFlags_AllowBackspace) && IsKeyPressed(ImGuiKey_Backspace, ImGuiInputFlags_Repeat)) 7027 { 7028 char* p = (char*)(void*)ImTextFindPreviousUtf8Codepoint(data->SearchBuffer, data->SearchBuffer + buffer_len); 7029 *p = 0; 7030 buffer_len = (int)(p - data->SearchBuffer); 7031 } 7032 7033 // Return request if any 7034 if (buffer_len == 0) 7035 return NULL; 7036 if (select_request) 7037 { 7038 data->FocusScope = g.NavFocusScopeId; 7039 data->LastRequestFrame = g.FrameCount; 7040 data->LastRequestTime = (float)g.Time; 7041 } 7042 out_request->Flags = flags; 7043 out_request->SearchBufferLen = buffer_len; 7044 out_request->SearchBuffer = data->SearchBuffer; 7045 out_request->SelectRequest = (data->LastRequestFrame == g.FrameCount); 7046 out_request->SingleCharMode = false; 7047 out_request->SingleCharSize = 0; 7048 7049 // Calculate if buffer contains the same character repeated. 7050 // - This can be used to implement a special search mode on first character. 7051 // - Performed on UTF-8 codepoint for correctness. 7052 // - SingleCharMode is always set for first input character, because it usually leads to a "next". 7053 if (flags & ImGuiTypingSelectFlags_AllowSingleCharMode) 7054 { 7055 const char* buf_begin = out_request->SearchBuffer; 7056 const char* buf_end = out_request->SearchBuffer + out_request->SearchBufferLen; 7057 const int c0_len = ImTextCountUtf8BytesFromChar(buf_begin, buf_end); 7058 const char* p = buf_begin + c0_len; 7059 for (; p < buf_end; p += c0_len) 7060 if (memcmp(buf_begin, p, (size_t)c0_len) != 0) 7061 break; 7062 const int single_char_count = (p == buf_end) ? (out_request->SearchBufferLen / c0_len) : 0; 7063 out_request->SingleCharMode = (single_char_count > 0 || data->SingleCharModeLock); 7064 out_request->SingleCharSize = (ImS8)c0_len; 7065 data->SingleCharModeLock |= (single_char_count >= TYPING_SELECT_SINGLE_CHAR_COUNT_FOR_LOCK); // From now on we stop search matching to lock to single char mode. 7066 } 7067 7068 return out_request; 7069 } 7070 7071 static int ImStrimatchlen(const char* s1, const char* s1_end, const char* s2) 7072 { 7073 int match_len = 0; 7074 while (s1 < s1_end && ImToUpper(*s1++) == ImToUpper(*s2++)) 7075 match_len++; 7076 return match_len; 7077 } 7078 7079 // Default handler for finding a result for typing-select. You may implement your own. 7080 // You might want to display a tooltip to visualize the current request SearchBuffer 7081 // When SingleCharMode is set: 7082 // - it is better to NOT display a tooltip of other on-screen display indicator. 7083 // - the index of the currently focused item is required. 7084 // if your SetNextItemSelectionUserData() values are indices, you can obtain it from ImGuiMultiSelectIO::NavIdItem, otherwise from g.NavLastValidSelectionUserData. 7085 int ImGui::TypingSelectFindMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx) 7086 { 7087 if (req == NULL || req->SelectRequest == false) // Support NULL parameter so both calls can be done from same spot. 7088 return -1; 7089 int idx = -1; 7090 if (req->SingleCharMode && (req->Flags & ImGuiTypingSelectFlags_AllowSingleCharMode)) 7091 idx = TypingSelectFindNextSingleCharMatch(req, items_count, get_item_name_func, user_data, nav_item_idx); 7092 else 7093 idx = TypingSelectFindBestLeadingMatch(req, items_count, get_item_name_func, user_data); 7094 if (idx != -1) 7095 NavRestoreHighlightAfterMove(); 7096 return idx; 7097 } 7098 7099 // Special handling when a single character is repeated: perform search on a single letter and goes to next. 7100 int ImGui::TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx) 7101 { 7102 // FIXME: Assume selection user data is index. Would be extremely practical. 7103 //if (nav_item_idx == -1) 7104 // nav_item_idx = (int)g.NavLastValidSelectionUserData; 7105 7106 int first_match_idx = -1; 7107 bool return_next_match = false; 7108 for (int idx = 0; idx < items_count; idx++) 7109 { 7110 const char* item_name = get_item_name_func(user_data, idx); 7111 if (ImStrimatchlen(req->SearchBuffer, req->SearchBuffer + req->SingleCharSize, item_name) < req->SingleCharSize) 7112 continue; 7113 if (return_next_match) // Return next matching item after current item. 7114 return idx; 7115 if (first_match_idx == -1 && nav_item_idx == -1) // Return first match immediately if we don't have a nav_item_idx value. 7116 return idx; 7117 if (first_match_idx == -1) // Record first match for wrapping. 7118 first_match_idx = idx; 7119 if (nav_item_idx == idx) // Record that we encountering nav_item so we can return next match. 7120 return_next_match = true; 7121 } 7122 return first_match_idx; // First result 7123 } 7124 7125 int ImGui::TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data) 7126 { 7127 int longest_match_idx = -1; 7128 int longest_match_len = 0; 7129 for (int idx = 0; idx < items_count; idx++) 7130 { 7131 const char* item_name = get_item_name_func(user_data, idx); 7132 const int match_len = ImStrimatchlen(req->SearchBuffer, req->SearchBuffer + req->SearchBufferLen, item_name); 7133 if (match_len <= longest_match_len) 7134 continue; 7135 longest_match_idx = idx; 7136 longest_match_len = match_len; 7137 if (match_len == req->SearchBufferLen) 7138 break; 7139 } 7140 return longest_match_idx; 7141 } 7142 7143 void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data) 7144 { 7145 #ifndef IMGUI_DISABLE_DEBUG_TOOLS 7146 Text("SearchBuffer = \"%s\"", data->SearchBuffer); 7147 Text("SingleCharMode = %d, Size = %d, Lock = %d", data->Request.SingleCharMode, data->Request.SingleCharSize, data->SingleCharModeLock); 7148 Text("LastRequest = time: %.2f, frame: %d", data->LastRequestTime, data->LastRequestFrame); 7149 #else 7150 IM_UNUSED(data); 7151 #endif 7152 } 7153 7154 //------------------------------------------------------------------------- 7155 // [SECTION] Widgets: Box-Select support 7156 // This has been extracted away from Multi-Select logic in the hope that it could eventually be used elsewhere, but hasn't been yet. 7157 //------------------------------------------------------------------------- 7158 // Extra logic in MultiSelectItemFooter() and ImGuiListClipper::Step() 7159 //------------------------------------------------------------------------- 7160 // - BoxSelectPreStartDrag() [Internal] 7161 // - BoxSelectActivateDrag() [Internal] 7162 // - BoxSelectDeactivateDrag() [Internal] 7163 // - BoxSelectScrollWithMouseDrag() [Internal] 7164 // - BeginBoxSelect() [Internal] 7165 // - EndBoxSelect() [Internal] 7166 //------------------------------------------------------------------------- 7167 7168 // Call on the initial click. 7169 static void BoxSelectPreStartDrag(ImGuiID id, ImGuiSelectionUserData clicked_item) 7170 { 7171 ImGuiContext& g = *GImGui; 7172 ImGuiBoxSelectState* bs = &g.BoxSelectState; 7173 bs->ID = id; 7174 bs->IsStarting = true; // Consider starting box-select. 7175 bs->IsStartedFromVoid = (clicked_item == ImGuiSelectionUserData_Invalid); 7176 bs->IsStartedSetNavIdOnce = bs->IsStartedFromVoid; 7177 bs->KeyMods = g.IO.KeyMods; 7178 bs->StartPosRel = bs->EndPosRel = ImGui::WindowPosAbsToRel(g.CurrentWindow, g.IO.MousePos); 7179 bs->ScrollAccum = ImVec2(0.0f, 0.0f); 7180 } 7181 7182 static void BoxSelectActivateDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window) 7183 { 7184 ImGuiContext& g = *GImGui; 7185 IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Activate\n", bs->ID); 7186 bs->IsActive = true; 7187 bs->Window = window; 7188 bs->IsStarting = false; 7189 ImGui::SetActiveID(bs->ID, window); 7190 ImGui::SetActiveIdUsingAllKeyboardKeys(); 7191 if (bs->IsStartedFromVoid && (bs->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) 7192 bs->RequestClear = true; 7193 } 7194 7195 static void BoxSelectDeactivateDrag(ImGuiBoxSelectState* bs) 7196 { 7197 ImGuiContext& g = *GImGui; 7198 bs->IsActive = bs->IsStarting = false; 7199 if (g.ActiveId == bs->ID) 7200 { 7201 IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Deactivate\n", bs->ID); 7202 ImGui::ClearActiveID(); 7203 } 7204 bs->ID = 0; 7205 } 7206 7207 static void BoxSelectScrollWithMouseDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window, const ImRect& inner_r) 7208 { 7209 ImGuiContext& g = *GImGui; 7210 IM_ASSERT(bs->Window == window); 7211 for (int n = 0; n < 2; n++) // each axis 7212 { 7213 const float mouse_pos = g.IO.MousePos[n]; 7214 const float dist = (mouse_pos > inner_r.Max[n]) ? mouse_pos - inner_r.Max[n] : (mouse_pos < inner_r.Min[n]) ? mouse_pos - inner_r.Min[n] : 0.0f; 7215 const float scroll_curr = window->Scroll[n]; 7216 if (dist == 0.0f || (dist < 0.0f && scroll_curr < 0.0f) || (dist > 0.0f && scroll_curr >= window->ScrollMax[n])) 7217 continue; 7218 7219 const float speed_multiplier = ImLinearRemapClamp(g.FontSize, g.FontSize * 5.0f, 1.0f, 4.0f, ImAbs(dist)); // x1 to x4 depending on distance 7220 const float scroll_step = g.FontSize * 35.0f * speed_multiplier * ImSign(dist) * g.IO.DeltaTime; 7221 bs->ScrollAccum[n] += scroll_step; 7222 7223 // Accumulate into a stored value so we can handle high-framerate 7224 const float scroll_step_i = ImFloor(bs->ScrollAccum[n]); 7225 if (scroll_step_i == 0.0f) 7226 continue; 7227 if (n == 0) 7228 ImGui::SetScrollX(window, scroll_curr + scroll_step_i); 7229 else 7230 ImGui::SetScrollY(window, scroll_curr + scroll_step_i); 7231 bs->ScrollAccum[n] -= scroll_step_i; 7232 } 7233 } 7234 7235 bool ImGui::BeginBoxSelect(const ImRect& scope_rect, ImGuiWindow* window, ImGuiID box_select_id, ImGuiMultiSelectFlags ms_flags) 7236 { 7237 ImGuiContext& g = *GImGui; 7238 ImGuiBoxSelectState* bs = &g.BoxSelectState; 7239 KeepAliveID(box_select_id); 7240 if (bs->ID != box_select_id) 7241 return false; 7242 7243 // IsStarting is set by MultiSelectItemFooter() when considering a possible box-select. We validate it here and lock geometry. 7244 bs->UnclipMode = false; 7245 bs->RequestClear = false; 7246 if (bs->IsStarting && IsMouseDragPastThreshold(0)) 7247 BoxSelectActivateDrag(bs, window); 7248 else if ((bs->IsStarting || bs->IsActive) && g.IO.MouseDown[0] == false) 7249 BoxSelectDeactivateDrag(bs); 7250 if (!bs->IsActive) 7251 return false; 7252 7253 // Current frame absolute prev/current rectangles are used to toggle selection. 7254 // They are derived from positions relative to scrolling space. 7255 ImVec2 start_pos_abs = WindowPosRelToAbs(window, bs->StartPosRel); 7256 ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, bs->EndPosRel); // Clamped already 7257 ImVec2 curr_end_pos_abs = g.IO.MousePos; 7258 if (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) // Box-select scrolling only happens with ScopeWindow 7259 curr_end_pos_abs = ImClamp(curr_end_pos_abs, scope_rect.Min, scope_rect.Max); 7260 bs->BoxSelectRectPrev.Min = ImMin(start_pos_abs, prev_end_pos_abs); 7261 bs->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs); 7262 bs->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs); 7263 bs->BoxSelectRectCurr.Max = ImMax(start_pos_abs, curr_end_pos_abs); 7264 7265 // Box-select 2D mode detects horizontal changes (vertical ones are already picked by Clipper) 7266 // Storing an extra rect used by widgets supporting box-select. 7267 if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d) 7268 if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x) 7269 { 7270 bs->UnclipMode = true; 7271 bs->UnclipRect = bs->BoxSelectRectPrev; // FIXME-OPT: UnclipRect x coordinates could be intersection of Prev and Curr rect on X axis. 7272 bs->UnclipRect.Add(bs->BoxSelectRectCurr); 7273 } 7274 7275 //GetForegroundDrawList()->AddRect(bs->UnclipRect.Min, bs->UnclipRect.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); 7276 //GetForegroundDrawList()->AddRect(bs->BoxSelectRectPrev.Min, bs->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); 7277 //GetForegroundDrawList()->AddRect(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f); 7278 return true; 7279 } 7280 7281 void ImGui::EndBoxSelect(const ImRect& scope_rect, ImGuiMultiSelectFlags ms_flags) 7282 { 7283 ImGuiContext& g = *GImGui; 7284 ImGuiWindow* window = g.CurrentWindow; 7285 ImGuiBoxSelectState* bs = &g.BoxSelectState; 7286 IM_ASSERT(bs->IsActive); 7287 bs->UnclipMode = false; 7288 7289 // Render selection rectangle 7290 bs->EndPosRel = WindowPosAbsToRel(window, ImClamp(g.IO.MousePos, scope_rect.Min, scope_rect.Max)); // Clamp stored position according to current scrolling view 7291 ImRect box_select_r = bs->BoxSelectRectCurr; 7292 box_select_r.ClipWith(scope_rect); 7293 window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling 7294 window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavHighlight)); // FIXME-MULTISELECT: Styling 7295 7296 // Scroll 7297 const bool enable_scroll = (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms_flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0; 7298 if (enable_scroll) 7299 { 7300 ImRect scroll_r = scope_rect; 7301 scroll_r.Expand(-g.FontSize); 7302 //GetForegroundDrawList()->AddRect(scroll_r.Min, scroll_r.Max, IM_COL32(0, 255, 0, 255)); 7303 if (!scroll_r.Contains(g.IO.MousePos)) 7304 BoxSelectScrollWithMouseDrag(bs, window, scroll_r); 7305 } 7306 } 7307 7308 //------------------------------------------------------------------------- 7309 // [SECTION] Widgets: Multi-Select support 7310 //------------------------------------------------------------------------- 7311 // - DebugLogMultiSelectRequests() [Internal] 7312 // - CalcScopeRect() [Internal] 7313 // - BeginMultiSelect() 7314 // - EndMultiSelect() 7315 // - SetNextItemSelectionUserData() 7316 // - MultiSelectItemHeader() [Internal] 7317 // - MultiSelectItemFooter() [Internal] 7318 // - DebugNodeMultiSelectState() [Internal] 7319 //------------------------------------------------------------------------- 7320 7321 static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* io) 7322 { 7323 ImGuiContext& g = *GImGui; 7324 for (const ImGuiSelectionRequest& req : io->Requests) 7325 { 7326 if (req.Type == ImGuiSelectionRequestType_SetAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetAll %d (= %s)\n", function, req.Selected, req.Selected ? "SelectAll" : "Clear"); 7327 if (req.Type == ImGuiSelectionRequestType_SetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetRange %" IM_PRId64 "..%" IM_PRId64 " (0x%" IM_PRIX64 "..0x%" IM_PRIX64 ") = %d (dir %d)\n", function, req.RangeFirstItem, req.RangeLastItem, req.RangeFirstItem, req.RangeLastItem, req.Selected, req.RangeDirection); 7328 } 7329 } 7330 7331 static ImRect CalcScopeRect(ImGuiMultiSelectTempData* ms, ImGuiWindow* window) 7332 { 7333 if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) 7334 { 7335 // Warning: this depends on CursorMaxPos so it means to be called by EndMultiSelect() only 7336 return ImRect(ms->ScopeRectMin, ImMax(window->DC.CursorMaxPos, ms->ScopeRectMin)); 7337 } 7338 else 7339 { 7340 // Add inner table decoration (#7821) // FIXME: Why not baking in InnerClipRect? 7341 ImRect scope_rect = window->InnerClipRect; 7342 scope_rect.Min = ImMin(scope_rect.Min + ImVec2(window->DecoInnerSizeX1, window->DecoInnerSizeY1), scope_rect.Max); 7343 return scope_rect; 7344 } 7345 } 7346 7347 // Return ImGuiMultiSelectIO structure. 7348 // Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect(). 7349 // Passing 'selection_size' and 'items_count' parameters is currently optional. 7350 // - 'selection_size' is useful to disable some shortcut routing: e.g. ImGuiMultiSelectFlags_ClearOnEscape won't claim Escape key when selection_size 0, 7351 // allowing a first press to clear selection THEN the second press to leave child window and return to parent. 7352 // - 'items_count' is stored in ImGuiMultiSelectIO which makes it a convenient way to pass the information to your ApplyRequest() handler (but you may pass it differently). 7353 // - If they are costly for you to compute (e.g. external intrusive selection without maintaining size), you may avoid them and pass -1. 7354 // - If you can easily tell if your selection is empty or not, you may pass 0/1, or you may enable ImGuiMultiSelectFlags_ClearOnEscape flag dynamically. 7355 ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int selection_size, int items_count) 7356 { 7357 ImGuiContext& g = *GImGui; 7358 ImGuiWindow* window = g.CurrentWindow; 7359 7360 if (++g.MultiSelectTempDataStacked > g.MultiSelectTempData.Size) 7361 g.MultiSelectTempData.resize(g.MultiSelectTempDataStacked, ImGuiMultiSelectTempData()); 7362 ImGuiMultiSelectTempData* ms = &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1]; 7363 IM_STATIC_ASSERT(offsetof(ImGuiMultiSelectTempData, IO) == 0); // Clear() relies on that. 7364 g.CurrentMultiSelect = ms; 7365 if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0) 7366 flags |= ImGuiMultiSelectFlags_ScopeWindow; 7367 if (flags & ImGuiMultiSelectFlags_SingleSelect) 7368 flags &= ~(ImGuiMultiSelectFlags_BoxSelect2d | ImGuiMultiSelectFlags_BoxSelect1d); 7369 if (flags & ImGuiMultiSelectFlags_BoxSelect2d) 7370 flags &= ~ImGuiMultiSelectFlags_BoxSelect1d; 7371 7372 // FIXME: BeginFocusScope() 7373 const ImGuiID id = window->IDStack.back(); 7374 ms->Clear(); 7375 ms->FocusScopeId = id; 7376 ms->Flags = flags; 7377 ms->IsFocused = (ms->FocusScopeId == g.NavFocusScopeId); 7378 ms->BackupCursorMaxPos = window->DC.CursorMaxPos; 7379 ms->ScopeRectMin = window->DC.CursorMaxPos = window->DC.CursorPos; 7380 PushFocusScope(ms->FocusScopeId); 7381 if (flags & ImGuiMultiSelectFlags_ScopeWindow) // Mark parent child window as navigable into, with highlight. Assume user will always submit interactive items. 7382 window->DC.NavLayersActiveMask |= 1 << ImGuiNavLayer_Main; 7383 7384 // Use copy of keyboard mods at the time of the request, otherwise we would requires mods to be held for an extra frame. 7385 ms->KeyMods = g.NavJustMovedToId ? (g.NavJustMovedToIsTabbing ? 0 : g.NavJustMovedToKeyMods) : g.IO.KeyMods; 7386 if (flags & ImGuiMultiSelectFlags_NoRangeSelect) 7387 ms->KeyMods &= ~ImGuiMod_Shift; 7388 7389 // Bind storage 7390 ImGuiMultiSelectState* storage = g.MultiSelectStorage.GetOrAddByKey(id); 7391 storage->ID = id; 7392 storage->LastFrameActive = g.FrameCount; 7393 storage->LastSelectionSize = selection_size; 7394 storage->Window = window; 7395 ms->Storage = storage; 7396 7397 // Output to user 7398 ms->IO.Requests.resize(0); 7399 ms->IO.RangeSrcItem = storage->RangeSrcItem; 7400 ms->IO.NavIdItem = storage->NavIdItem; 7401 ms->IO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false; 7402 ms->IO.ItemsCount = items_count; 7403 7404 // Clear when using Navigation to move within the scope 7405 // (we compare FocusScopeId so it possible to use multiple selections inside a same window) 7406 bool request_clear = false; 7407 bool request_select_all = false; 7408 if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData) 7409 { 7410 if (ms->KeyMods & ImGuiMod_Shift) 7411 ms->IsKeyboardSetRange = true; 7412 if (ms->IsKeyboardSetRange) 7413 IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid); // Not ready -> could clear? 7414 if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0) 7415 request_clear = true; 7416 } 7417 else if (g.NavJustMovedFromFocusScopeId == ms->FocusScopeId) 7418 { 7419 // Also clear on leaving scope (may be optional?) 7420 if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0) 7421 request_clear = true; 7422 } 7423 7424 // Box-select handling: update active state. 7425 ImGuiBoxSelectState* bs = &g.BoxSelectState; 7426 if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) 7427 { 7428 ms->BoxSelectId = GetID("##BoxSelect"); 7429 if (BeginBoxSelect(CalcScopeRect(ms, window), window, ms->BoxSelectId, flags)) 7430 request_clear |= bs->RequestClear; 7431 } 7432 7433 if (ms->IsFocused) 7434 { 7435 // Shortcut: Clear selection (Escape) 7436 // - Only claim shortcut if selection is not empty, allowing further presses on Escape to e.g. leave current child window. 7437 // - Box select also handle Escape and needs to pass an id to bypass ActiveIdUsingAllKeyboardKeys lock. 7438 if (flags & ImGuiMultiSelectFlags_ClearOnEscape) 7439 { 7440 if (selection_size != 0 || bs->IsActive) 7441 if (Shortcut(ImGuiKey_Escape, ImGuiInputFlags_None, bs->IsActive ? bs->ID : 0)) 7442 { 7443 request_clear = true; 7444 if (bs->IsActive) 7445 BoxSelectDeactivateDrag(bs); 7446 } 7447 } 7448 7449 // Shortcut: Select all (CTRL+A) 7450 if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll)) 7451 if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A)) 7452 request_select_all = true; 7453 } 7454 7455 if (request_clear || request_select_all) 7456 { 7457 MultiSelectAddSetAll(ms, request_select_all); 7458 if (!request_select_all) 7459 storage->LastSelectionSize = 0; 7460 } 7461 ms->LoopRequestSetAll = request_select_all ? 1 : request_clear ? 0 : -1; 7462 ms->LastSubmittedItem = ImGuiSelectionUserData_Invalid; 7463 7464 if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) 7465 DebugLogMultiSelectRequests("BeginMultiSelect", &ms->IO); 7466 7467 return &ms->IO; 7468 } 7469 7470 // Return updated ImGuiMultiSelectIO structure. 7471 // Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect(). 7472 ImGuiMultiSelectIO* ImGui::EndMultiSelect() 7473 { 7474 ImGuiContext& g = *GImGui; 7475 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect; 7476 ImGuiMultiSelectState* storage = ms->Storage; 7477 ImGuiWindow* window = g.CurrentWindow; 7478 IM_ASSERT(ms->FocusScopeId == g.CurrentFocusScopeId); 7479 IM_ASSERT(g.CurrentMultiSelect != NULL && storage->Window == g.CurrentWindow); 7480 IM_ASSERT(g.MultiSelectTempDataStacked > 0 && &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] == g.CurrentMultiSelect); 7481 7482 ImRect scope_rect = CalcScopeRect(ms, window); 7483 if (ms->IsFocused) 7484 { 7485 // We currently don't allow user code to modify RangeSrcItem by writing to BeginIO's version, but that would be an easy change here. 7486 if (ms->IO.RangeSrcReset || (ms->RangeSrcPassedBy == false && ms->IO.RangeSrcItem != ImGuiSelectionUserData_Invalid)) // Can't read storage->RangeSrcItem here -> we want the state at begining of the scope (see tests for easy failure) 7487 { 7488 IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset RangeSrcItem.\n"); // Will set be to NavId. 7489 storage->RangeSrcItem = ImGuiSelectionUserData_Invalid; 7490 } 7491 if (ms->NavIdPassedBy == false && storage->NavIdItem != ImGuiSelectionUserData_Invalid) 7492 { 7493 IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset NavIdItem.\n"); 7494 storage->NavIdItem = ImGuiSelectionUserData_Invalid; 7495 storage->NavIdSelected = -1; 7496 } 7497 7498 if ((ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) && GetBoxSelectState(ms->BoxSelectId)) 7499 EndBoxSelect(scope_rect, ms->Flags); 7500 } 7501 7502 if (ms->IsEndIO == false) 7503 ms->IO.Requests.resize(0); 7504 7505 // Clear selection when clicking void? 7506 // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection! 7507 // The InnerRect test is necessary for non-child/decorated windows. 7508 bool scope_hovered = IsWindowHovered() && window->InnerRect.Contains(g.IO.MousePos); 7509 if (scope_hovered && (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)) 7510 scope_hovered &= scope_rect.Contains(g.IO.MousePos); 7511 if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0) 7512 { 7513 if (ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) 7514 { 7515 if (!g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && g.IO.MouseClickedCount[0] == 1) 7516 { 7517 BoxSelectPreStartDrag(ms->BoxSelectId, ImGuiSelectionUserData_Invalid); 7518 FocusWindow(window, ImGuiFocusRequestFlags_UnlessBelowModal); 7519 SetHoveredID(ms->BoxSelectId); 7520 if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) 7521 SetNavID(0, ImGuiNavLayer_Main, ms->FocusScopeId, ImRect(g.IO.MousePos, g.IO.MousePos)); // Automatically switch FocusScope for initial click from void to box-select. 7522 } 7523 } 7524 7525 if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid) 7526 if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None) 7527 MultiSelectAddSetAll(ms, false); 7528 } 7529 7530 // Courtesy nav wrapping helper flag 7531 if (ms->Flags & ImGuiMultiSelectFlags_NavWrapX) 7532 { 7533 IM_ASSERT(ms->Flags & ImGuiMultiSelectFlags_ScopeWindow); // Only supported at window scope 7534 ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX); 7535 } 7536 7537 // Unwind 7538 window->DC.CursorMaxPos = ImMax(ms->BackupCursorMaxPos, window->DC.CursorMaxPos); 7539 PopFocusScope(); 7540 7541 if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) 7542 DebugLogMultiSelectRequests("EndMultiSelect", &ms->IO); 7543 7544 ms->FocusScopeId = 0; 7545 ms->Flags = ImGuiMultiSelectFlags_None; 7546 g.CurrentMultiSelect = (--g.MultiSelectTempDataStacked > 0) ? &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] : NULL; 7547 7548 return &ms->IO; 7549 } 7550 7551 void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data) 7552 { 7553 // Note that flags will be cleared by ItemAdd(), so it's only useful for Navigation code! 7554 // This designed so widgets can also cheaply set this before calling ItemAdd(), so we are not tied to MultiSelect api. 7555 ImGuiContext& g = *GImGui; 7556 g.NextItemData.SelectionUserData = selection_user_data; 7557 g.NextItemData.FocusScopeId = g.CurrentFocusScopeId; 7558 7559 if (ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect) 7560 { 7561 // Auto updating RangeSrcPassedBy for cases were clipper is not used (done before ItemAdd() clipping) 7562 g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData | ImGuiItemFlags_IsMultiSelect; 7563 if (ms->IO.RangeSrcItem == selection_user_data) 7564 ms->RangeSrcPassedBy = true; 7565 } 7566 else 7567 { 7568 g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData; 7569 } 7570 } 7571 7572 // In charge of: 7573 // - Applying SetAll for submitted items. 7574 // - Applying SetRange for submitted items and record end points. 7575 // - Altering button behavior flags to facilitate use with drag and drop. 7576 void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags) 7577 { 7578 ImGuiContext& g = *GImGui; 7579 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect; 7580 7581 bool selected = *p_selected; 7582 if (ms->IsFocused) 7583 { 7584 ImGuiMultiSelectState* storage = ms->Storage; 7585 ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData; 7586 IM_ASSERT(g.NextItemData.FocusScopeId == g.CurrentFocusScopeId && "Forgot to call SetNextItemSelectionUserData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope"); 7587 7588 // Apply SetAll (Clear/SelectAll) requests requested by BeginMultiSelect(). 7589 // This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper. 7590 // If you are using a clipper you need to process the SetAll request after calling BeginMultiSelect() 7591 if (ms->LoopRequestSetAll != -1) 7592 selected = (ms->LoopRequestSetAll == 1); 7593 7594 // When using SHIFT+Nav: because it can incur scrolling we cannot afford a frame of lag with the selection highlight (otherwise scrolling would happen before selection) 7595 // For this to work, we need someone to set 'RangeSrcPassedBy = true' at some point (either clipper either SetNextItemSelectionUserData() function) 7596 if (ms->IsKeyboardSetRange) 7597 { 7598 IM_ASSERT(id != 0 && (ms->KeyMods & ImGuiMod_Shift) != 0); 7599 const bool is_range_dst = (ms->RangeDstPassedBy == false) && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped. 7600 if (is_range_dst) 7601 ms->RangeDstPassedBy = true; 7602 if (is_range_dst && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) // If we don't have RangeSrc, assign RangeSrc = RangeDst 7603 { 7604 storage->RangeSrcItem = item_data; 7605 storage->RangeSelected = selected ? 1 : 0; 7606 } 7607 const bool is_range_src = storage->RangeSrcItem == item_data; 7608 if (is_range_src || is_range_dst || ms->RangeSrcPassedBy != ms->RangeDstPassedBy) 7609 { 7610 // Apply range-select value to visible items 7611 IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid && storage->RangeSelected != -1); 7612 selected = (storage->RangeSelected != 0); 7613 } 7614 else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0 && (ms->Flags & ImGuiMultiSelectFlags_NoAutoClear) == 0) 7615 { 7616 // Clear other items 7617 selected = false; 7618 } 7619 } 7620 *p_selected = selected; 7621 } 7622 7623 // Alter button behavior flags 7624 // To handle drag and drop of multiple items we need to avoid clearing selection on click. 7625 // Enabling this test makes actions using CTRL+SHIFT delay their effect on MouseUp which is annoying, but it allows drag and drop of multiple items. 7626 if (p_button_flags != NULL) 7627 { 7628 ImGuiButtonFlags button_flags = *p_button_flags; 7629 button_flags |= ImGuiButtonFlags_NoHoveredOnFocus; 7630 if ((!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) && !(ms->Flags & ImGuiMultiSelectFlags_SelectOnClickRelease)) 7631 button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease; 7632 else 7633 button_flags |= ImGuiButtonFlags_PressedOnClickRelease; 7634 *p_button_flags = button_flags; 7635 } 7636 } 7637 7638 // In charge of: 7639 // - Auto-select on navigation. 7640 // - Box-select toggle handling. 7641 // - Right-click handling. 7642 // - Altering selection based on Ctrl/Shift modifiers, both for keyboard and mouse. 7643 // - Record current selection state for RangeSrc 7644 // This is all rather complex, best to run and refer to "widgets_multiselect_xxx" tests in imgui_test_suite. 7645 void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) 7646 { 7647 ImGuiContext& g = *GImGui; 7648 ImGuiWindow* window = g.CurrentWindow; 7649 7650 bool selected = *p_selected; 7651 bool pressed = *p_pressed; 7652 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect; 7653 ImGuiMultiSelectState* storage = ms->Storage; 7654 if (pressed) 7655 ms->IsFocused = true; 7656 7657 bool hovered = false; 7658 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) 7659 hovered = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup); 7660 if (!ms->IsFocused && !hovered) 7661 return; 7662 7663 ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData; 7664 7665 ImGuiMultiSelectFlags flags = ms->Flags; 7666 const bool is_singleselect = (flags & ImGuiMultiSelectFlags_SingleSelect) != 0; 7667 bool is_ctrl = (ms->KeyMods & ImGuiMod_Ctrl) != 0; 7668 bool is_shift = (ms->KeyMods & ImGuiMod_Shift) != 0; 7669 7670 bool apply_to_range_src = false; 7671 7672 if (g.NavId == id && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) 7673 apply_to_range_src = true; 7674 if (ms->IsEndIO == false) 7675 { 7676 ms->IO.Requests.resize(0); 7677 ms->IsEndIO = true; 7678 } 7679 7680 // Auto-select as you navigate a list 7681 if (g.NavJustMovedToId == id) 7682 { 7683 if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0) 7684 { 7685 if (is_ctrl && is_shift) 7686 pressed = true; 7687 else if (!is_ctrl) 7688 selected = pressed = true; 7689 } 7690 else 7691 { 7692 // With NoAutoSelect, using Shift+keyboard performs a write/copy 7693 if (is_shift) 7694 pressed = true; 7695 else if (!is_ctrl) 7696 apply_to_range_src = true; // Since if (pressed) {} main block is not running we update this 7697 } 7698 } 7699 7700 if (apply_to_range_src) 7701 { 7702 storage->RangeSrcItem = item_data; 7703 storage->RangeSelected = selected; // Will be updated at the end of this function anyway. 7704 } 7705 7706 // Box-select toggle handling 7707 if (ms->BoxSelectId != 0) 7708 if (ImGuiBoxSelectState* bs = GetBoxSelectState(ms->BoxSelectId)) 7709 { 7710 const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(g.LastItemData.Rect); 7711 const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(g.LastItemData.Rect); 7712 if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr)) 7713 { 7714 if (storage->LastSelectionSize <= 0 && bs->IsStartedSetNavIdOnce) 7715 { 7716 pressed = true; // First item act as a pressed: code below will emit selection request and set NavId (whatever we emit here will be overridden anyway) 7717 bs->IsStartedSetNavIdOnce = false; 7718 } 7719 else 7720 { 7721 selected = !selected; 7722 MultiSelectAddSetRange(ms, selected, +1, item_data, item_data); 7723 } 7724 storage->LastSelectionSize = ImMax(storage->LastSelectionSize + 1, 1); 7725 } 7726 } 7727 7728 // Right-click handling. 7729 // FIXME-MULTISELECT: Currently filtered out by ImGuiMultiSelectFlags_NoAutoSelect but maybe should be moved to Selectable(). See https://github.com/ocornut/imgui/pull/5816 7730 if (hovered && IsMouseClicked(1) && (flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0) 7731 { 7732 if (g.ActiveId != 0 && g.ActiveId != id) 7733 ClearActiveID(); 7734 SetFocusID(id, window); 7735 if (!pressed && !selected) 7736 { 7737 pressed = true; 7738 is_ctrl = is_shift = false; 7739 } 7740 } 7741 7742 // Unlike Space, Enter doesn't alter selection (but can still return a press) unless current item is not selected. 7743 // The later, "unless current item is not select", may become optional? It seems like a better default if Enter doesn't necessarily open something 7744 // (unlike e.g. Windows explorer). For use case where Enter always open something, we might decide to make this optional? 7745 const bool enter_pressed = pressed && (g.NavActivateId == id) && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput); 7746 7747 // Alter selection 7748 if (pressed && (!enter_pressed || !selected)) 7749 { 7750 // Box-select 7751 ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; 7752 if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) 7753 if (selected == false && !g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1) 7754 BoxSelectPreStartDrag(ms->BoxSelectId, item_data); 7755 7756 //---------------------------------------------------------------------------------------- 7757 // ACTION | Begin | Pressed/Activated | End 7758 //---------------------------------------------------------------------------------------- 7759 // Keys Navigated: | Clear | Src=item, Sel=1 SetRange 1 7760 // Keys Navigated: Ctrl | n/a | n/a 7761 // Keys Navigated: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1 7762 // Keys Navigated: Ctrl+Shift | n/a | Dst=item, Sel=Src => Clear + SetRange Src-Dst 7763 // Keys Activated: | n/a | Src=item, Sel=1 => Clear + SetRange 1 7764 // Keys Activated: Ctrl | n/a | Src=item, Sel=!Sel => SetSange 1 7765 // Keys Activated: Shift | n/a | Dst=item, Sel=1 => Clear + SetSange 1 7766 //---------------------------------------------------------------------------------------- 7767 // Mouse Pressed: | n/a | Src=item, Sel=1, => Clear + SetRange 1 7768 // Mouse Pressed: Ctrl | n/a | Src=item, Sel=!Sel => SetRange 1 7769 // Mouse Pressed: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1 7770 // Mouse Pressed: Ctrl+Shift | n/a | Dst=item, Sel=!Sel => SetRange Src-Dst 7771 //---------------------------------------------------------------------------------------- 7772 7773 if ((flags & ImGuiMultiSelectFlags_NoAutoClear) == 0) 7774 { 7775 bool request_clear = false; 7776 if (is_singleselect) 7777 request_clear = true; 7778 else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl) 7779 request_clear = (flags & ImGuiMultiSelectFlags_NoAutoClearOnReselect) ? !selected : true; 7780 else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl) 7781 request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again. 7782 if (request_clear) 7783 MultiSelectAddSetAll(ms, false); 7784 } 7785 7786 int range_direction; 7787 bool range_selected; 7788 if (is_shift && !is_singleselect) 7789 { 7790 //IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue); 7791 if (storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) 7792 storage->RangeSrcItem = item_data; 7793 if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0) 7794 { 7795 // Shift+Arrow always select 7796 // Ctrl+Shift+Arrow copy source selection state (already stored by BeginMultiSelect() in storage->RangeSelected) 7797 range_selected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; 7798 } 7799 else 7800 { 7801 // Shift+Arrow copy source selection state 7802 // Shift+Click always copy from target selection state 7803 if (ms->IsKeyboardSetRange) 7804 range_selected = (storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; 7805 else 7806 range_selected = !selected; 7807 } 7808 range_direction = ms->RangeSrcPassedBy ? +1 : -1; 7809 } 7810 else 7811 { 7812 // Ctrl inverts selection, otherwise always select 7813 if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0) 7814 selected = is_ctrl ? !selected : true; 7815 else 7816 selected = !selected; 7817 storage->RangeSrcItem = item_data; 7818 range_selected = selected; 7819 range_direction = +1; 7820 } 7821 MultiSelectAddSetRange(ms, range_selected, range_direction, storage->RangeSrcItem, item_data); 7822 } 7823 7824 // Update/store the selection state of the Source item (used by CTRL+SHIFT, when Source is unselected we perform a range unselect) 7825 if (storage->RangeSrcItem == item_data) 7826 storage->RangeSelected = selected ? 1 : 0; 7827 7828 // Update/store the selection state of focused item 7829 if (g.NavId == id) 7830 { 7831 storage->NavIdItem = item_data; 7832 storage->NavIdSelected = selected ? 1 : 0; 7833 } 7834 if (storage->NavIdItem == item_data) 7835 ms->NavIdPassedBy = true; 7836 ms->LastSubmittedItem = item_data; 7837 7838 *p_selected = selected; 7839 *p_pressed = pressed; 7840 } 7841 7842 void ImGui::MultiSelectAddSetAll(ImGuiMultiSelectTempData* ms, bool selected) 7843 { 7844 ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, selected, 0, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid }; 7845 ms->IO.Requests.resize(0); // Can always clear previous requests 7846 ms->IO.Requests.push_back(req); // Add new request 7847 } 7848 7849 void ImGui::MultiSelectAddSetRange(ImGuiMultiSelectTempData* ms, bool selected, int range_dir, ImGuiSelectionUserData first_item, ImGuiSelectionUserData last_item) 7850 { 7851 // Merge contiguous spans into same request (unless NoRangeSelect is set which guarantees single-item ranges) 7852 if (ms->IO.Requests.Size > 0 && first_item == last_item && (ms->Flags & ImGuiMultiSelectFlags_NoRangeSelect) == 0) 7853 { 7854 ImGuiSelectionRequest* prev = &ms->IO.Requests.Data[ms->IO.Requests.Size - 1]; 7855 if (prev->Type == ImGuiSelectionRequestType_SetRange && prev->RangeLastItem == ms->LastSubmittedItem && prev->Selected == selected) 7856 { 7857 prev->RangeLastItem = last_item; 7858 return; 7859 } 7860 } 7861 7862 ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, (ImS8)range_dir, (range_dir > 0) ? first_item : last_item, (range_dir > 0) ? last_item : first_item }; 7863 ms->IO.Requests.push_back(req); // Add new request 7864 } 7865 7866 void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage) 7867 { 7868 #ifndef IMGUI_DISABLE_DEBUG_TOOLS 7869 const bool is_active = (storage->LastFrameActive >= GetFrameCount() - 2); // Note that fully clipped early out scrolling tables will appear as inactive here. 7870 if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); } 7871 bool open = TreeNode((void*)(intptr_t)storage->ID, "MultiSelect 0x%08X in '%s'%s", storage->ID, storage->Window ? storage->Window->Name : "N/A", is_active ? "" : " *Inactive*"); 7872 if (!is_active) { PopStyleColor(); } 7873 if (!open) 7874 return; 7875 Text("RangeSrcItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), RangeSelected = %d", storage->RangeSrcItem, storage->RangeSrcItem, storage->RangeSelected); 7876 Text("NavIdItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), NavIdSelected = %d", storage->NavIdItem, storage->NavIdItem, storage->NavIdSelected); 7877 Text("LastSelectionSize = %d", storage->LastSelectionSize); // Provided by user 7878 TreePop(); 7879 #else 7880 IM_UNUSED(storage); 7881 #endif 7882 } 7883 7884 //------------------------------------------------------------------------- 7885 // [SECTION] Widgets: Multi-Select helpers 7886 //------------------------------------------------------------------------- 7887 // - ImGuiSelectionBasicStorage 7888 // - ImGuiSelectionExternalStorage 7889 //------------------------------------------------------------------------- 7890 7891 ImGuiSelectionBasicStorage::ImGuiSelectionBasicStorage() 7892 { 7893 Size = 0; 7894 PreserveOrder = false; 7895 UserData = NULL; 7896 AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; 7897 _SelectionOrder = 1; // Always >0 7898 } 7899 7900 void ImGuiSelectionBasicStorage::Clear() 7901 { 7902 Size = 0; 7903 _SelectionOrder = 1; // Always >0 7904 _Storage.Data.resize(0); 7905 } 7906 7907 void ImGuiSelectionBasicStorage::Swap(ImGuiSelectionBasicStorage& r) 7908 { 7909 ImSwap(Size, r.Size); 7910 ImSwap(_SelectionOrder, r._SelectionOrder); 7911 _Storage.Data.swap(r._Storage.Data); 7912 } 7913 7914 bool ImGuiSelectionBasicStorage::Contains(ImGuiID id) const 7915 { 7916 return _Storage.GetInt(id, 0) != 0; 7917 } 7918 7919 static int IMGUI_CDECL PairComparerByValueInt(const void* lhs, const void* rhs) 7920 { 7921 int lhs_v = ((const ImGuiStoragePair*)lhs)->val_i; 7922 int rhs_v = ((const ImGuiStoragePair*)rhs)->val_i; 7923 return (lhs_v > rhs_v ? +1 : lhs_v < rhs_v ? -1 : 0); 7924 } 7925 7926 // GetNextSelectedItem() is an abstraction allowing us to change our underlying actual storage system without impacting user. 7927 // (e.g. store unselected vs compact down, compact down on demand, use raw ImVector<ImGuiID> instead of ImGuiStorage...) 7928 bool ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it, ImGuiID* out_id) 7929 { 7930 ImGuiStoragePair* it = (ImGuiStoragePair*)*opaque_it; 7931 ImGuiStoragePair* it_end = _Storage.Data.Data + _Storage.Data.Size; 7932 if (PreserveOrder && it == NULL && it_end != NULL) 7933 ImQsort(_Storage.Data.Data, (size_t)_Storage.Data.Size, sizeof(ImGuiStoragePair), PairComparerByValueInt); // ~ImGuiStorage::BuildSortByValueInt() 7934 if (it == NULL) 7935 it = _Storage.Data.Data; 7936 IM_ASSERT(it >= _Storage.Data.Data && it <= it_end); 7937 if (it != it_end) 7938 while (it->val_i == 0 && it < it_end) 7939 it++; 7940 const bool has_more = (it != it_end); 7941 *opaque_it = has_more ? (void**)(it + 1) : (void**)(it); 7942 *out_id = has_more ? it->key : 0; 7943 if (PreserveOrder && !has_more) 7944 _Storage.BuildSortByKey(); 7945 return has_more; 7946 } 7947 7948 void ImGuiSelectionBasicStorage::SetItemSelected(ImGuiID id, bool selected) 7949 { 7950 int* p_int = _Storage.GetIntRef(id, 0); 7951 if (selected && *p_int == 0) { *p_int = _SelectionOrder++; Size++; } 7952 else if (!selected && *p_int != 0) { *p_int = 0; Size--; } 7953 } 7954 7955 // Optimized for batch edits (with same value of 'selected') 7956 static void ImGuiSelectionBasicStorage_BatchSetItemSelected(ImGuiSelectionBasicStorage* selection, ImGuiID id, bool selected, int size_before_amends, int selection_order) 7957 { 7958 ImGuiStorage* storage = &selection->_Storage; 7959 ImGuiStoragePair* it = ImLowerBound(storage->Data.Data, storage->Data.Data + size_before_amends, id); 7960 const bool is_contained = (it != storage->Data.Data + size_before_amends) && (it->key == id); 7961 if (selected == (is_contained && it->val_i != 0)) 7962 return; 7963 if (selected && !is_contained) 7964 storage->Data.push_back(ImGuiStoragePair(id, selection_order)); // Push unsorted at end of vector, will be sorted in SelectionMultiAmendsFinish() 7965 else if (is_contained) 7966 it->val_i = selected ? selection_order : 0; // Modify in-place. 7967 selection->Size += selected ? +1 : -1; 7968 } 7969 7970 static void ImGuiSelectionBasicStorage_BatchFinish(ImGuiSelectionBasicStorage* selection, bool selected, int size_before_amends) 7971 { 7972 ImGuiStorage* storage = &selection->_Storage; 7973 if (selected && selection->Size != size_before_amends) 7974 storage->BuildSortByKey(); // When done selecting: sort everything 7975 } 7976 7977 // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). 7978 // - Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen. 7979 // - Honoring SetRange requests requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem. 7980 // - In this demo we often submit indices to SetNextItemSelectionUserData() + store the same indices in persistent selection. 7981 // - Your code may do differently. If you store pointers or objects ID in ImGuiSelectionUserData you may need to perform 7982 // a lookup in order to have some way to iterate/interpolate between two items. 7983 // - A full-featured application is likely to allow search/filtering which is likely to lead to using indices 7984 // and constructing a view index <> object id/ptr data structure anyway. 7985 // WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THIS UNNECESSARY 'ImGuiSelectionBasicStorage' INDIRECTION LOGIC. 7986 // Notice that with the simplest adapter (using indices everywhere), all functions return their parameters. 7987 // The most simple implementation (using indices everywhere) would look like: 7988 // for (ImGuiSelectionRequest& req : ms_io->Requests) 7989 // { 7990 // if (req.Type == ImGuiSelectionRequestType_SetAll) { Clear(); if (req.Selected) { for (int n = 0; n < items_count; n++) { SetItemSelected(n, true); } } 7991 // if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { SetItemSelected(n, ms_io->Selected); } } 7992 // } 7993 void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) 7994 { 7995 // For convenience we obtain ItemsCount as passed to BeginMultiSelect(), which is optional. 7996 // It makes sense when using ImGuiSelectionBasicStorage to simply pass your items count to BeginMultiSelect(). 7997 // Other scheme may handle SetAll differently. 7998 IM_ASSERT(ms_io->ItemsCount != -1 && "Missing value for items_count in BeginMultiSelect() call!"); 7999 IM_ASSERT(AdapterIndexToStorageId != NULL); 8000 8001 // This is optimized/specialized to cope with very large selections (e.g. 100k+ items) 8002 // - A simpler version could call SetItemSelected() directly instead of ImGuiSelectionBasicStorage_BatchSetItemSelected() + ImGuiSelectionBasicStorage_BatchFinish(). 8003 // - Optimized select can append unsorted, then sort in a second pass. Optimized unselect can clear in-place then compact in a second pass. 8004 // - A more optimal version wouldn't even use ImGuiStorage but directly a ImVector<ImGuiID> to reduce bandwidth, but this is a reasonable trade off to reuse code. 8005 // - 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 8006 // left and right: it affects coarse clipping + can emit multiple SetRange with 1 item each.) 8007 // FIXME-OPT: For each block of consecutive SetRange request: 8008 // - add all requests to a sorted list, store ID, selected, offset in ImGuiStorage. 8009 // - rewrite sorted storage a single time. 8010 for (ImGuiSelectionRequest& req : ms_io->Requests) 8011 { 8012 if (req.Type == ImGuiSelectionRequestType_SetAll) 8013 { 8014 Clear(); 8015 if (req.Selected) 8016 { 8017 _Storage.Data.reserve(ms_io->ItemsCount); 8018 const int size_before_amends = _Storage.Data.Size; 8019 for (int idx = 0; idx < ms_io->ItemsCount; idx++, _SelectionOrder++) 8020 ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, _SelectionOrder); 8021 ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends); 8022 } 8023 } 8024 else if (req.Type == ImGuiSelectionRequestType_SetRange) 8025 { 8026 const int selection_changes = (int)req.RangeLastItem - (int)req.RangeFirstItem + 1; 8027 //ImGuiContext& g = *GImGui; IMGUI_DEBUG_LOG_SELECTION("Req %d/%d: set %d to %d\n", ms_io->Requests.index_from_ptr(&req), ms_io->Requests.Size, selection_changes, req.Selected); 8028 if (selection_changes == 1 || (selection_changes < Size / 100)) 8029 { 8030 // Multiple sorted insertion + copy likely to be faster. 8031 // Technically we could do a single copy with a little more work (sort sequential SetRange requests) 8032 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++) 8033 SetItemSelected(GetStorageIdFromIndex(idx), req.Selected); 8034 } 8035 else 8036 { 8037 // Append insertion + single sort likely be faster. 8038 // Use req.RangeDirection to set order field so that shift+clicking from 1 to 5 is different than shift+clicking from 5 to 1 8039 const int size_before_amends = _Storage.Data.Size; 8040 int selection_order = _SelectionOrder + ((req.RangeDirection < 0) ? selection_changes - 1 : 0); 8041 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++, selection_order += req.RangeDirection) 8042 ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, selection_order); 8043 if (req.Selected) 8044 _SelectionOrder += selection_changes; 8045 ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends); 8046 } 8047 } 8048 } 8049 } 8050 8051 //------------------------------------------------------------------------- 8052 8053 ImGuiSelectionExternalStorage::ImGuiSelectionExternalStorage() 8054 { 8055 UserData = NULL; 8056 AdapterSetItemSelected = NULL; 8057 } 8058 8059 // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). 8060 // We also pull 'ms_io->ItemsCount' as passed for BeginMultiSelect() for consistency with ImGuiSelectionBasicStorage 8061 // This makes no assumption about underlying storage. 8062 void ImGuiSelectionExternalStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) 8063 { 8064 IM_ASSERT(AdapterSetItemSelected); 8065 for (ImGuiSelectionRequest& req : ms_io->Requests) 8066 { 8067 if (req.Type == ImGuiSelectionRequestType_SetAll) 8068 for (int idx = 0; idx < ms_io->ItemsCount; idx++) 8069 AdapterSetItemSelected(this, idx, req.Selected); 8070 if (req.Type == ImGuiSelectionRequestType_SetRange) 8071 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++) 8072 AdapterSetItemSelected(this, idx, req.Selected); 8073 } 8074 } 8075 8076 //------------------------------------------------------------------------- 8077 // [SECTION] Widgets: ListBox 8078 //------------------------------------------------------------------------- 8079 // - BeginListBox() 8080 // - EndListBox() 8081 // - ListBox() 8082 //------------------------------------------------------------------------- 8083 8084 // This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label. 8085 // Tip: To have a list filling the entire window width, use size.x = -FLT_MIN and pass an non-visible label e.g. "##empty" 8086 // Tip: If your vertical size is calculated from an item count (e.g. 10 * item_height) consider adding a fractional part to facilitate seeing scrolling boundaries (e.g. 10.25 * item_height). 8087 bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg) 8088 { 8089 ImGuiContext& g = *GImGui; 8090 ImGuiWindow* window = GetCurrentWindow(); 8091 if (window->SkipItems) 8092 return false; 8093 8094 const ImGuiStyle& style = g.Style; 8095 const ImGuiID id = GetID(label); 8096 const ImVec2 label_size = CalcTextSize(label, NULL, true); 8097 8098 // Size default to hold ~7.25 items. 8099 // Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar. 8100 ImVec2 size = ImTrunc(CalcItemSize(size_arg, CalcItemWidth(), GetTextLineHeightWithSpacing() * 7.25f + style.FramePadding.y * 2.0f)); 8101 ImVec2 frame_size = ImVec2(size.x, ImMax(size.y, label_size.y)); 8102 ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size); 8103 ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); 8104 g.NextItemData.ClearFlags(); 8105 8106 if (!IsRectVisible(bb.Min, bb.Max)) 8107 { 8108 ItemSize(bb.GetSize(), style.FramePadding.y); 8109 ItemAdd(bb, 0, &frame_bb); 8110 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values 8111 return false; 8112 } 8113 8114 // FIXME-OPT: We could omit the BeginGroup() if label_size.x == 0.0f but would need to omit the EndGroup() as well. 8115 BeginGroup(); 8116 if (label_size.x > 0.0f) 8117 { 8118 ImVec2 label_pos = ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y); 8119 RenderText(label_pos, label); 8120 window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, label_pos + label_size); 8121 AlignTextToFramePadding(); 8122 } 8123 8124 BeginChild(id, frame_bb.GetSize(), ImGuiChildFlags_FrameStyle); 8125 return true; 8126 } 8127 8128 void ImGui::EndListBox() 8129 { 8130 ImGuiContext& g = *GImGui; 8131 ImGuiWindow* window = g.CurrentWindow; 8132 IM_ASSERT((window->Flags & ImGuiWindowFlags_ChildWindow) && "Mismatched BeginListBox/EndListBox calls. Did you test the return value of BeginListBox?"); 8133 IM_UNUSED(window); 8134 8135 EndChild(); 8136 EndGroup(); // This is only required to be able to do IsItemXXX query on the whole ListBox including label 8137 } 8138 8139 bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items) 8140 { 8141 const bool value_changed = ListBox(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_items); 8142 return value_changed; 8143 } 8144 8145 // This is merely a helper around BeginListBox(), EndListBox(). 8146 // Considering using those directly to submit custom data or store selection differently. 8147 bool ImGui::ListBox(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), void* user_data, int items_count, int height_in_items) 8148 { 8149 ImGuiContext& g = *GImGui; 8150 8151 // Calculate size from "height_in_items" 8152 if (height_in_items < 0) 8153 height_in_items = ImMin(items_count, 7); 8154 float height_in_items_f = height_in_items + 0.25f; 8155 ImVec2 size(0.0f, ImTrunc(GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f)); 8156 8157 if (!BeginListBox(label, size)) 8158 return false; 8159 8160 // Assume all items have even height (= 1 line of text). If you need items of different height, 8161 // you can create a custom version of ListBox() in your code without using the clipper. 8162 bool value_changed = false; 8163 ImGuiListClipper clipper; 8164 clipper.Begin(items_count, GetTextLineHeightWithSpacing()); // We know exactly our line height here so we pass it as a minor optimization, but generally you don't need to. 8165 clipper.IncludeItemByIndex(*current_item); 8166 while (clipper.Step()) 8167 for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) 8168 { 8169 const char* item_text = getter(user_data, i); 8170 if (item_text == NULL) 8171 item_text = "*Unknown item*"; 8172 8173 PushID(i); 8174 const bool item_selected = (i == *current_item); 8175 if (Selectable(item_text, item_selected)) 8176 { 8177 *current_item = i; 8178 value_changed = true; 8179 } 8180 if (item_selected) 8181 SetItemDefaultFocus(); 8182 PopID(); 8183 } 8184 EndListBox(); 8185 8186 if (value_changed) 8187 MarkItemEdited(g.LastItemData.ID); 8188 8189 return value_changed; 8190 } 8191 8192 //------------------------------------------------------------------------- 8193 // [SECTION] Widgets: PlotLines, PlotHistogram 8194 //------------------------------------------------------------------------- 8195 // - PlotEx() [Internal] 8196 // - PlotLines() 8197 // - PlotHistogram() 8198 //------------------------------------------------------------------------- 8199 // Plot/Graph widgets are not very good. 8200 // Consider writing your own, or using a third-party one, see: 8201 // - ImPlot https://github.com/epezent/implot 8202 // - others https://github.com/ocornut/imgui/wiki/Useful-Extensions 8203 //------------------------------------------------------------------------- 8204 8205 int ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, const ImVec2& size_arg) 8206 { 8207 ImGuiContext& g = *GImGui; 8208 ImGuiWindow* window = GetCurrentWindow(); 8209 if (window->SkipItems) 8210 return -1; 8211 8212 const ImGuiStyle& style = g.Style; 8213 const ImGuiID id = window->GetID(label); 8214 8215 const ImVec2 label_size = CalcTextSize(label, NULL, true); 8216 const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), label_size.y + style.FramePadding.y * 2.0f); 8217 8218 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size); 8219 const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding); 8220 const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0)); 8221 ItemSize(total_bb, style.FramePadding.y); 8222 if (!ItemAdd(total_bb, 0, &frame_bb)) 8223 return -1; 8224 const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.InFlags); 8225 8226 // Determine scale from values if not specified 8227 if (scale_min == FLT_MAX || scale_max == FLT_MAX) 8228 { 8229 float v_min = FLT_MAX; 8230 float v_max = -FLT_MAX; 8231 for (int i = 0; i < values_count; i++) 8232 { 8233 const float v = values_getter(data, i); 8234 if (v != v) // Ignore NaN values 8235 continue; 8236 v_min = ImMin(v_min, v); 8237 v_max = ImMax(v_max, v); 8238 } 8239 if (scale_min == FLT_MAX) 8240 scale_min = v_min; 8241 if (scale_max == FLT_MAX) 8242 scale_max = v_max; 8243 } 8244 8245 RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); 8246 8247 const int values_count_min = (plot_type == ImGuiPlotType_Lines) ? 2 : 1; 8248 int idx_hovered = -1; 8249 if (values_count >= values_count_min) 8250 { 8251 int res_w = ImMin((int)frame_size.x, values_count) + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0); 8252 int item_count = values_count + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0); 8253 8254 // Tooltip on hover 8255 if (hovered && inner_bb.Contains(g.IO.MousePos)) 8256 { 8257 const float t = ImClamp((g.IO.MousePos.x - inner_bb.Min.x) / (inner_bb.Max.x - inner_bb.Min.x), 0.0f, 0.9999f); 8258 const int v_idx = (int)(t * item_count); 8259 IM_ASSERT(v_idx >= 0 && v_idx < values_count); 8260 8261 const float v0 = values_getter(data, (v_idx + values_offset) % values_count); 8262 const float v1 = values_getter(data, (v_idx + 1 + values_offset) % values_count); 8263 if (plot_type == ImGuiPlotType_Lines) 8264 SetTooltip("%d: %8.4g\n%d: %8.4g", v_idx, v0, v_idx + 1, v1); 8265 else if (plot_type == ImGuiPlotType_Histogram) 8266 SetTooltip("%d: %8.4g", v_idx, v0); 8267 idx_hovered = v_idx; 8268 } 8269 8270 const float t_step = 1.0f / (float)res_w; 8271 const float inv_scale = (scale_min == scale_max) ? 0.0f : (1.0f / (scale_max - scale_min)); 8272 8273 float v0 = values_getter(data, (0 + values_offset) % values_count); 8274 float t0 = 0.0f; 8275 ImVec2 tp0 = ImVec2( t0, 1.0f - ImSaturate((v0 - scale_min) * inv_scale) ); // Point in the normalized space of our target rectangle 8276 float histogram_zero_line_t = (scale_min * scale_max < 0.0f) ? (1 + scale_min * inv_scale) : (scale_min < 0.0f ? 0.0f : 1.0f); // Where does the zero line stands 8277 8278 const ImU32 col_base = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLines : ImGuiCol_PlotHistogram); 8279 const ImU32 col_hovered = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLinesHovered : ImGuiCol_PlotHistogramHovered); 8280 8281 for (int n = 0; n < res_w; n++) 8282 { 8283 const float t1 = t0 + t_step; 8284 const int v1_idx = (int)(t0 * item_count + 0.5f); 8285 IM_ASSERT(v1_idx >= 0 && v1_idx < values_count); 8286 const float v1 = values_getter(data, (v1_idx + values_offset + 1) % values_count); 8287 const ImVec2 tp1 = ImVec2( t1, 1.0f - ImSaturate((v1 - scale_min) * inv_scale) ); 8288 8289 // NB: Draw calls are merged together by the DrawList system. Still, we should render our batch are lower level to save a bit of CPU. 8290 ImVec2 pos0 = ImLerp(inner_bb.Min, inner_bb.Max, tp0); 8291 ImVec2 pos1 = ImLerp(inner_bb.Min, inner_bb.Max, (plot_type == ImGuiPlotType_Lines) ? tp1 : ImVec2(tp1.x, histogram_zero_line_t)); 8292 if (plot_type == ImGuiPlotType_Lines) 8293 { 8294 window->DrawList->AddLine(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base); 8295 } 8296 else if (plot_type == ImGuiPlotType_Histogram) 8297 { 8298 if (pos1.x >= pos0.x + 2.0f) 8299 pos1.x -= 1.0f; 8300 window->DrawList->AddRectFilled(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base); 8301 } 8302 8303 t0 = t1; 8304 tp0 = tp1; 8305 } 8306 } 8307 8308 // Text overlay 8309 if (overlay_text) 8310 RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, overlay_text, NULL, NULL, ImVec2(0.5f, 0.0f)); 8311 8312 if (label_size.x > 0.0f) 8313 RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), label); 8314 8315 // Return hovered index or -1 if none are hovered. 8316 // This is currently not exposed in the public API because we need a larger redesign of the whole thing, but in the short-term we are making it available in PlotEx(). 8317 return idx_hovered; 8318 } 8319 8320 struct ImGuiPlotArrayGetterData 8321 { 8322 const float* Values; 8323 int Stride; 8324 8325 ImGuiPlotArrayGetterData(const float* values, int stride) { Values = values; Stride = stride; } 8326 }; 8327 8328 static float Plot_ArrayGetter(void* data, int idx) 8329 { 8330 ImGuiPlotArrayGetterData* plot_data = (ImGuiPlotArrayGetterData*)data; 8331 const float v = *(const float*)(const void*)((const unsigned char*)plot_data->Values + (size_t)idx * plot_data->Stride); 8332 return v; 8333 } 8334 8335 void ImGui::PlotLines(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride) 8336 { 8337 ImGuiPlotArrayGetterData data(values, stride); 8338 PlotEx(ImGuiPlotType_Lines, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); 8339 } 8340 8341 void ImGui::PlotLines(const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size) 8342 { 8343 PlotEx(ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); 8344 } 8345 8346 void ImGui::PlotHistogram(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride) 8347 { 8348 ImGuiPlotArrayGetterData data(values, stride); 8349 PlotEx(ImGuiPlotType_Histogram, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); 8350 } 8351 8352 void ImGui::PlotHistogram(const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size) 8353 { 8354 PlotEx(ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); 8355 } 8356 8357 //------------------------------------------------------------------------- 8358 // [SECTION] Widgets: Value helpers 8359 // Those is not very useful, legacy API. 8360 //------------------------------------------------------------------------- 8361 // - Value() 8362 //------------------------------------------------------------------------- 8363 8364 void ImGui::Value(const char* prefix, bool b) 8365 { 8366 Text("%s: %s", prefix, (b ? "true" : "false")); 8367 } 8368 8369 void ImGui::Value(const char* prefix, int v) 8370 { 8371 Text("%s: %d", prefix, v); 8372 } 8373 8374 void ImGui::Value(const char* prefix, unsigned int v) 8375 { 8376 Text("%s: %d", prefix, v); 8377 } 8378 8379 void ImGui::Value(const char* prefix, float v, const char* float_format) 8380 { 8381 if (float_format) 8382 { 8383 char fmt[64]; 8384 ImFormatString(fmt, IM_ARRAYSIZE(fmt), "%%s: %s", float_format); 8385 Text(fmt, prefix, v); 8386 } 8387 else 8388 { 8389 Text("%s: %.3f", prefix, v); 8390 } 8391 } 8392 8393 //------------------------------------------------------------------------- 8394 // [SECTION] MenuItem, BeginMenu, EndMenu, etc. 8395 //------------------------------------------------------------------------- 8396 // - ImGuiMenuColumns [Internal] 8397 // - BeginMenuBar() 8398 // - EndMenuBar() 8399 // - BeginMainMenuBar() 8400 // - EndMainMenuBar() 8401 // - BeginMenu() 8402 // - EndMenu() 8403 // - MenuItemEx() [Internal] 8404 // - MenuItem() 8405 //------------------------------------------------------------------------- 8406 8407 // Helpers for internal use 8408 void ImGuiMenuColumns::Update(float spacing, bool window_reappearing) 8409 { 8410 if (window_reappearing) 8411 memset(Widths, 0, sizeof(Widths)); 8412 Spacing = (ImU16)spacing; 8413 CalcNextTotalWidth(true); 8414 memset(Widths, 0, sizeof(Widths)); 8415 TotalWidth = NextTotalWidth; 8416 NextTotalWidth = 0; 8417 } 8418 8419 void ImGuiMenuColumns::CalcNextTotalWidth(bool update_offsets) 8420 { 8421 ImU16 offset = 0; 8422 bool want_spacing = false; 8423 for (int i = 0; i < IM_ARRAYSIZE(Widths); i++) 8424 { 8425 ImU16 width = Widths[i]; 8426 if (want_spacing && width > 0) 8427 offset += Spacing; 8428 want_spacing |= (width > 0); 8429 if (update_offsets) 8430 { 8431 if (i == 1) { OffsetLabel = offset; } 8432 if (i == 2) { OffsetShortcut = offset; } 8433 if (i == 3) { OffsetMark = offset; } 8434 } 8435 offset += width; 8436 } 8437 NextTotalWidth = offset; 8438 } 8439 8440 float ImGuiMenuColumns::DeclColumns(float w_icon, float w_label, float w_shortcut, float w_mark) 8441 { 8442 Widths[0] = ImMax(Widths[0], (ImU16)w_icon); 8443 Widths[1] = ImMax(Widths[1], (ImU16)w_label); 8444 Widths[2] = ImMax(Widths[2], (ImU16)w_shortcut); 8445 Widths[3] = ImMax(Widths[3], (ImU16)w_mark); 8446 CalcNextTotalWidth(false); 8447 return (float)ImMax(TotalWidth, NextTotalWidth); 8448 } 8449 8450 // FIXME: Provided a rectangle perhaps e.g. a BeginMenuBarEx() could be used anywhere.. 8451 // Currently the main responsibility of this function being to setup clip-rect + horizontal layout + menu navigation layer. 8452 // Ideally we also want this to be responsible for claiming space out of the main window scrolling rectangle, in which case ImGuiWindowFlags_MenuBar will become unnecessary. 8453 // Then later the same system could be used for multiple menu-bars, scrollbars, side-bars. 8454 bool ImGui::BeginMenuBar() 8455 { 8456 ImGuiWindow* window = GetCurrentWindow(); 8457 if (window->SkipItems) 8458 return false; 8459 if (!(window->Flags & ImGuiWindowFlags_MenuBar)) 8460 return false; 8461 8462 IM_ASSERT(!window->DC.MenuBarAppending); 8463 BeginGroup(); // Backup position on layer 0 // FIXME: Misleading to use a group for that backup/restore 8464 PushID("##menubar"); 8465 8466 // We don't clip with current window clipping rectangle as it is already set to the area below. However we clip with window full rect. 8467 // We remove 1 worth of rounding to Max.x to that text in long menus and small windows don't tend to display over the lower-right rounded area, which looks particularly glitchy. 8468 ImRect bar_rect = window->MenuBarRect(); 8469 ImRect clip_rect(IM_ROUND(bar_rect.Min.x + window->WindowBorderSize), IM_ROUND(bar_rect.Min.y + window->WindowBorderSize), IM_ROUND(ImMax(bar_rect.Min.x, bar_rect.Max.x - ImMax(window->WindowRounding, window->WindowBorderSize))), IM_ROUND(bar_rect.Max.y)); 8470 clip_rect.ClipWith(window->OuterRectClipped); 8471 PushClipRect(clip_rect.Min, clip_rect.Max, false); 8472 8473 // We overwrite CursorMaxPos because BeginGroup sets it to CursorPos (essentially the .EmitItem hack in EndMenuBar() would need something analogous here, maybe a BeginGroupEx() with flags). 8474 window->DC.CursorPos = window->DC.CursorMaxPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffset.x, bar_rect.Min.y + window->DC.MenuBarOffset.y); 8475 window->DC.LayoutType = ImGuiLayoutType_Horizontal; 8476 window->DC.IsSameLine = false; 8477 window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; 8478 window->DC.MenuBarAppending = true; 8479 AlignTextToFramePadding(); 8480 return true; 8481 } 8482 8483 void ImGui::EndMenuBar() 8484 { 8485 ImGuiWindow* window = GetCurrentWindow(); 8486 if (window->SkipItems) 8487 return; 8488 ImGuiContext& g = *GImGui; 8489 8490 // Nav: When a move request within one of our child menu failed, capture the request to navigate among our siblings. 8491 if (NavMoveRequestButNoResultYet() && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) && (g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu)) 8492 { 8493 // Try to find out if the request is for one of our child menu 8494 ImGuiWindow* nav_earliest_child = g.NavWindow; 8495 while (nav_earliest_child->ParentWindow && (nav_earliest_child->ParentWindow->Flags & ImGuiWindowFlags_ChildMenu)) 8496 nav_earliest_child = nav_earliest_child->ParentWindow; 8497 if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0) 8498 { 8499 // To do so we claim focus back, restore NavId and then process the movement request for yet another frame. 8500 // This involve a one-frame delay which isn't very problematic in this situation. We could remove it by scoring in advance for multiple window (probably not worth bothering) 8501 const ImGuiNavLayer layer = ImGuiNavLayer_Menu; 8502 IM_ASSERT(window->DC.NavLayersActiveMaskNext & (1 << layer)); // Sanity check (FIXME: Seems unnecessary) 8503 FocusWindow(window); 8504 SetNavID(window->NavLastIds[layer], layer, 0, window->NavRectRel[layer]); 8505 g.NavDisableHighlight = true; // Hide highlight for the current frame so we don't see the intermediary selection. 8506 g.NavDisableMouseHover = g.NavMousePosDirty = true; 8507 NavMoveRequestForward(g.NavMoveDir, g.NavMoveClipDir, g.NavMoveFlags, g.NavMoveScrollFlags); // Repeat 8508 } 8509 } 8510 8511 IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'" 8512 IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar); 8513 IM_ASSERT(window->DC.MenuBarAppending); 8514 PopClipRect(); 8515 PopID(); 8516 window->DC.MenuBarOffset.x = window->DC.CursorPos.x - window->Pos.x; // Save horizontal position so next append can reuse it. This is kinda equivalent to a per-layer CursorPos. 8517 8518 // FIXME: Extremely confusing, cleanup by (a) working on WorkRect stack system (b) not using a Group confusingly here. 8519 ImGuiGroupData& group_data = g.GroupStack.back(); 8520 group_data.EmitItem = false; 8521 ImVec2 restore_cursor_max_pos = group_data.BackupCursorMaxPos; 8522 window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, window->DC.CursorMaxPos.x - window->Scroll.x); // Convert ideal extents for scrolling layer equivalent. 8523 EndGroup(); // Restore position on layer 0 // FIXME: Misleading to use a group for that backup/restore 8524 window->DC.LayoutType = ImGuiLayoutType_Vertical; 8525 window->DC.IsSameLine = false; 8526 window->DC.NavLayerCurrent = ImGuiNavLayer_Main; 8527 window->DC.MenuBarAppending = false; 8528 window->DC.CursorMaxPos = restore_cursor_max_pos; 8529 } 8530 8531 // Important: calling order matters! 8532 // FIXME: Somehow overlapping with docking tech. 8533 // FIXME: The "rect-cut" aspect of this could be formalized into a lower-level helper (rect-cut: https://halt.software/dead-simple-layouts) 8534 bool ImGui::BeginViewportSideBar(const char* name, ImGuiViewport* viewport_p, ImGuiDir dir, float axis_size, ImGuiWindowFlags window_flags) 8535 { 8536 IM_ASSERT(dir != ImGuiDir_None); 8537 8538 ImGuiWindow* bar_window = FindWindowByName(name); 8539 if (bar_window == NULL || bar_window->BeginCount == 0) 8540 { 8541 // Calculate and set window size/position 8542 ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)(viewport_p ? viewport_p : GetMainViewport()); 8543 ImRect avail_rect = viewport->GetBuildWorkRect(); 8544 ImGuiAxis axis = (dir == ImGuiDir_Up || dir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X; 8545 ImVec2 pos = avail_rect.Min; 8546 if (dir == ImGuiDir_Right || dir == ImGuiDir_Down) 8547 pos[axis] = avail_rect.Max[axis] - axis_size; 8548 ImVec2 size = avail_rect.GetSize(); 8549 size[axis] = axis_size; 8550 SetNextWindowPos(pos); 8551 SetNextWindowSize(size); 8552 8553 // Report our size into work area (for next frame) using actual window size 8554 if (dir == ImGuiDir_Up || dir == ImGuiDir_Left) 8555 viewport->BuildWorkOffsetMin[axis] += axis_size; 8556 else if (dir == ImGuiDir_Down || dir == ImGuiDir_Right) 8557 viewport->BuildWorkOffsetMax[axis] -= axis_size; 8558 } 8559 8560 window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; 8561 PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); 8562 PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0)); // Lift normal size constraint 8563 bool is_open = Begin(name, NULL, window_flags); 8564 PopStyleVar(2); 8565 8566 return is_open; 8567 } 8568 8569 bool ImGui::BeginMainMenuBar() 8570 { 8571 ImGuiContext& g = *GImGui; 8572 ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport(); 8573 8574 // For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set. 8575 // FIXME: This could be generalized as an opt-in way to clamp window->DC.CursorStartPos to avoid SafeArea? 8576 // FIXME: Consider removing support for safe area down the line... it's messy. Nowadays consoles have support for TV calibration in OS settings. 8577 g.NextWindowData.MenuBarOffsetMinVal = ImVec2(g.Style.DisplaySafeAreaPadding.x, ImMax(g.Style.DisplaySafeAreaPadding.y - g.Style.FramePadding.y, 0.0f)); 8578 ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar; 8579 float height = GetFrameHeight(); 8580 bool is_open = BeginViewportSideBar("##MainMenuBar", viewport, ImGuiDir_Up, height, window_flags); 8581 g.NextWindowData.MenuBarOffsetMinVal = ImVec2(0.0f, 0.0f); 8582 8583 if (is_open) 8584 BeginMenuBar(); 8585 else 8586 End(); 8587 return is_open; 8588 } 8589 8590 void ImGui::EndMainMenuBar() 8591 { 8592 EndMenuBar(); 8593 8594 // When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window 8595 // FIXME: With this strategy we won't be able to restore a NULL focus. 8596 ImGuiContext& g = *GImGui; 8597 if (g.CurrentWindow == g.NavWindow && g.NavLayer == ImGuiNavLayer_Main && !g.NavAnyRequest) 8598 FocusTopMostWindowUnderOne(g.NavWindow, NULL, NULL, ImGuiFocusRequestFlags_UnlessBelowModal | ImGuiFocusRequestFlags_RestoreFocusedChild); 8599 8600 End(); 8601 } 8602 8603 static bool IsRootOfOpenMenuSet() 8604 { 8605 ImGuiContext& g = *GImGui; 8606 ImGuiWindow* window = g.CurrentWindow; 8607 if ((g.OpenPopupStack.Size <= g.BeginPopupStack.Size) || (window->Flags & ImGuiWindowFlags_ChildMenu)) 8608 return false; 8609 8610 // Initially we used 'upper_popup->OpenParentId == window->IDStack.back()' to differentiate multiple menu sets from each others 8611 // (e.g. inside menu bar vs loose menu items) based on parent ID. 8612 // This would however prevent the use of e.g. PushID() user code submitting menus. 8613 // Previously this worked between popup and a first child menu because the first child menu always had the _ChildWindow flag, 8614 // making hovering on parent popup possible while first child menu was focused - but this was generally a bug with other side effects. 8615 // Instead we don't treat Popup specifically (in order to consistently support menu features in them), maybe the first child menu of a Popup 8616 // doesn't have the _ChildWindow flag, and we rely on this IsRootOfOpenMenuSet() check to allow hovering between root window/popup and first child menu. 8617 // In the end, lack of ID check made it so we could no longer differentiate between separate menu sets. To compensate for that, we at least check parent window nav layer. 8618 // This fixes the most common case of menu opening on hover when moving between window content and menu bar. Multiple different menu sets in same nav layer would still 8619 // open on hover, but that should be a lesser problem, because if such menus are close in proximity in window content then it won't feel weird and if they are far apart 8620 // it likely won't be a problem anyone runs into. 8621 const ImGuiPopupData* upper_popup = &g.OpenPopupStack[g.BeginPopupStack.Size]; 8622 if (window->DC.NavLayerCurrent != upper_popup->ParentNavLayer) 8623 return false; 8624 return upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu) && ImGui::IsWindowChildOf(upper_popup->Window, window, true); 8625 } 8626 8627 bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled) 8628 { 8629 ImGuiWindow* window = GetCurrentWindow(); 8630 if (window->SkipItems) 8631 return false; 8632 8633 ImGuiContext& g = *GImGui; 8634 const ImGuiStyle& style = g.Style; 8635 const ImGuiID id = window->GetID(label); 8636 bool menu_is_open = IsPopupOpen(id, ImGuiPopupFlags_None); 8637 8638 // Sub-menus are ChildWindow so that mouse can be hovering across them (otherwise top-most popup menu would steal focus and not allow hovering on parent menu) 8639 // The first menu in a hierarchy isn't so hovering doesn't get across (otherwise e.g. resizing borders with ImGuiButtonFlags_FlattenChildren would react), but top-most BeginMenu() will bypass that limitation. 8640 ImGuiWindowFlags window_flags = ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus; 8641 if (window->Flags & ImGuiWindowFlags_ChildMenu) 8642 window_flags |= ImGuiWindowFlags_ChildWindow; 8643 8644 // If a menu with same the ID was already submitted, we will append to it, matching the behavior of Begin(). 8645 // We are relying on a O(N) search - so O(N log N) over the frame - which seems like the most efficient for the expected small amount of BeginMenu() calls per frame. 8646 // If somehow this is ever becoming a problem we can switch to use e.g. ImGuiStorage mapping key to last frame used. 8647 if (g.MenusIdSubmittedThisFrame.contains(id)) 8648 { 8649 if (menu_is_open) 8650 menu_is_open = BeginPopupEx(id, window_flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display) 8651 else 8652 g.NextWindowData.ClearFlags(); // we behave like Begin() and need to consume those values 8653 return menu_is_open; 8654 } 8655 8656 // Tag menu as used. Next time BeginMenu() with same ID is called it will append to existing menu 8657 g.MenusIdSubmittedThisFrame.push_back(id); 8658 8659 ImVec2 label_size = CalcTextSize(label, NULL, true); 8660 8661 // Odd hack to allow hovering across menus of a same menu-set (otherwise we wouldn't be able to hover parent without always being a Child window) 8662 // This is only done for items for the menu set and not the full parent window. 8663 const bool menuset_is_open = IsRootOfOpenMenuSet(); 8664 if (menuset_is_open) 8665 PushItemFlag(ImGuiItemFlags_NoWindowHoverableCheck, true); 8666 8667 // The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu, 8668 // However the final position is going to be different! It is chosen by FindBestWindowPosForPopup(). 8669 // e.g. Menus tend to overlap each other horizontally to amplify relative Z-ordering. 8670 ImVec2 popup_pos, pos = window->DC.CursorPos; 8671 PushID(label); 8672 if (!enabled) 8673 BeginDisabled(); 8674 const ImGuiMenuColumns* offsets = &window->DC.MenuColumns; 8675 bool pressed; 8676 8677 // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another. 8678 const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SelectOnClick | ImGuiSelectableFlags_NoAutoClosePopups; 8679 if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) 8680 { 8681 // Menu inside an horizontal menu bar 8682 // Selectable extend their highlight by half ItemSpacing in each direction. 8683 // For ChildMenu, the popup position will be overwritten by the call to FindBestWindowPosForPopup() in Begin() 8684 popup_pos = ImVec2(pos.x - 1.0f - IM_TRUNC(style.ItemSpacing.x * 0.5f), pos.y - style.FramePadding.y + window->MenuBarHeight); 8685 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f); 8686 PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(style.ItemSpacing.x * 2.0f, style.ItemSpacing.y)); 8687 float w = label_size.x; 8688 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); 8689 pressed = Selectable("", menu_is_open, selectable_flags, ImVec2(w, label_size.y)); 8690 RenderText(text_pos, label); 8691 PopStyleVar(); 8692 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar(). 8693 } 8694 else 8695 { 8696 // Menu inside a regular/vertical menu 8697 // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f. 8698 // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system. 8699 popup_pos = ImVec2(pos.x, pos.y - style.WindowPadding.y); 8700 float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f; 8701 float checkmark_w = IM_TRUNC(g.FontSize * 1.20f); 8702 float min_w = window->DC.MenuColumns.DeclColumns(icon_w, label_size.x, 0.0f, checkmark_w); // Feedback to next frame 8703 float extra_w = ImMax(0.0f, GetContentRegionAvail().x - min_w); 8704 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); 8705 pressed = Selectable("", menu_is_open, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y)); 8706 RenderText(text_pos, label); 8707 if (icon_w > 0.0f) 8708 RenderText(pos + ImVec2(offsets->OffsetIcon, 0.0f), icon); 8709 RenderArrow(window->DrawList, pos + ImVec2(offsets->OffsetMark + extra_w + g.FontSize * 0.30f, 0.0f), GetColorU32(ImGuiCol_Text), ImGuiDir_Right); 8710 } 8711 if (!enabled) 8712 EndDisabled(); 8713 8714 const bool hovered = (g.HoveredId == id) && enabled && !g.NavDisableMouseHover; 8715 if (menuset_is_open) 8716 PopItemFlag(); 8717 8718 bool want_open = false; 8719 bool want_open_nav_init = false; 8720 bool want_close = false; 8721 if (window->DC.LayoutType == ImGuiLayoutType_Vertical) // (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu)) 8722 { 8723 // Close menu when not hovering it anymore unless we are moving roughly in the direction of the menu 8724 // Implement http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown to avoid using timers, so menus feels more reactive. 8725 bool moving_toward_child_menu = false; 8726 ImGuiPopupData* child_popup = (g.BeginPopupStack.Size < g.OpenPopupStack.Size) ? &g.OpenPopupStack[g.BeginPopupStack.Size] : NULL; // Popup candidate (testing below) 8727 ImGuiWindow* child_menu_window = (child_popup && child_popup->Window && child_popup->Window->ParentWindow == window) ? child_popup->Window : NULL; 8728 if (g.HoveredWindow == window && child_menu_window != NULL) 8729 { 8730 const float ref_unit = g.FontSize; // FIXME-DPI 8731 const float child_dir = (window->Pos.x < child_menu_window->Pos.x) ? 1.0f : -1.0f; 8732 const ImRect next_window_rect = child_menu_window->Rect(); 8733 ImVec2 ta = (g.IO.MousePos - g.IO.MouseDelta); 8734 ImVec2 tb = (child_dir > 0.0f) ? next_window_rect.GetTL() : next_window_rect.GetTR(); 8735 ImVec2 tc = (child_dir > 0.0f) ? next_window_rect.GetBL() : next_window_rect.GetBR(); 8736 const float pad_farmost_h = ImClamp(ImFabs(ta.x - tb.x) * 0.30f, ref_unit * 0.5f, ref_unit * 2.5f); // Add a bit of extra slack. 8737 ta.x += child_dir * -0.5f; 8738 tb.x += child_dir * ref_unit; 8739 tc.x += child_dir * ref_unit; 8740 tb.y = ta.y + ImMax((tb.y - pad_farmost_h) - ta.y, -ref_unit * 8.0f); // Triangle has maximum height to limit the slope and the bias toward large sub-menus 8741 tc.y = ta.y + ImMin((tc.y + pad_farmost_h) - ta.y, +ref_unit * 8.0f); 8742 moving_toward_child_menu = ImTriangleContainsPoint(ta, tb, tc, g.IO.MousePos); 8743 //GetForegroundDrawList()->AddTriangleFilled(ta, tb, tc, moving_toward_child_menu ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); // [DEBUG] 8744 } 8745 8746 // The 'HovereWindow == window' check creates an inconsistency (e.g. moving away from menu slowly tends to hit same window, whereas moving away fast does not) 8747 // But we also need to not close the top-menu menu when moving over void. Perhaps we should extend the triangle check to a larger polygon. 8748 // (Remember to test this on BeginPopup("A")->BeginMenu("B") sequence which behaves slightly differently as B isn't a Child of A and hovering isn't shared.) 8749 if (menu_is_open && !hovered && g.HoveredWindow == window && !moving_toward_child_menu && !g.NavDisableMouseHover && g.ActiveId == 0) 8750 want_close = true; 8751 8752 // Open 8753 // (note: at this point 'hovered' actually includes the NavDisableMouseHover == false test) 8754 if (!menu_is_open && pressed) // Click/activate to open 8755 want_open = true; 8756 else if (!menu_is_open && hovered && !moving_toward_child_menu) // Hover to open 8757 want_open = true; 8758 else if (!menu_is_open && hovered && g.HoveredIdTimer >= 0.30f && g.MouseStationaryTimer >= 0.30f) // Hover to open (timer fallback) 8759 want_open = true; 8760 if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open 8761 { 8762 want_open = want_open_nav_init = true; 8763 NavMoveRequestCancel(); 8764 NavRestoreHighlightAfterMove(); 8765 } 8766 } 8767 else 8768 { 8769 // Menu bar 8770 if (menu_is_open && pressed && menuset_is_open) // Click an open menu again to close it 8771 { 8772 want_close = true; 8773 want_open = menu_is_open = false; 8774 } 8775 else if (pressed || (hovered && menuset_is_open && !menu_is_open)) // First click to open, then hover to open others 8776 { 8777 want_open = true; 8778 } 8779 else if (g.NavId == id && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open 8780 { 8781 want_open = true; 8782 NavMoveRequestCancel(); 8783 } 8784 } 8785 8786 if (!enabled) // explicitly close if an open menu becomes disabled, facilitate users code a lot in pattern such as 'if (BeginMenu("options", has_object)) { ..use object.. }' 8787 want_close = true; 8788 if (want_close && IsPopupOpen(id, ImGuiPopupFlags_None)) 8789 ClosePopupToLevel(g.BeginPopupStack.Size, true); 8790 8791 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Openable | (menu_is_open ? ImGuiItemStatusFlags_Opened : 0)); 8792 PopID(); 8793 8794 if (want_open && !menu_is_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size) 8795 { 8796 // Don't reopen/recycle same menu level in the same frame if it is a different menu ID, first close the other menu and yield for a frame. 8797 OpenPopup(label); 8798 } 8799 else if (want_open) 8800 { 8801 menu_is_open = true; 8802 OpenPopup(label, ImGuiPopupFlags_NoReopen);// | (want_open_nav_init ? ImGuiPopupFlags_NoReopenAlwaysNavInit : 0)); 8803 } 8804 8805 if (menu_is_open) 8806 { 8807 ImGuiLastItemData last_item_in_parent = g.LastItemData; 8808 SetNextWindowPos(popup_pos, ImGuiCond_Always); // Note: misleading: the value will serve as reference for FindBestWindowPosForPopup(), not actual pos. 8809 PushStyleVar(ImGuiStyleVar_ChildRounding, style.PopupRounding); // First level will use _PopupRounding, subsequent will use _ChildRounding 8810 menu_is_open = BeginPopupEx(id, window_flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display) 8811 PopStyleVar(); 8812 if (menu_is_open) 8813 { 8814 // Implement what ImGuiPopupFlags_NoReopenAlwaysNavInit would do: 8815 // Perform an init request in the case the popup was already open (via a previous mouse hover) 8816 if (want_open && want_open_nav_init && !g.NavInitRequest) 8817 { 8818 FocusWindow(g.CurrentWindow, ImGuiFocusRequestFlags_UnlessBelowModal); 8819 NavInitWindow(g.CurrentWindow, false); 8820 } 8821 8822 // Restore LastItemData so IsItemXXXX functions can work after BeginMenu()/EndMenu() 8823 // (This fixes using IsItemClicked() and IsItemHovered(), but IsItemHovered() also relies on its support for ImGuiItemFlags_NoWindowHoverableCheck) 8824 g.LastItemData = last_item_in_parent; 8825 if (g.HoveredWindow == window) 8826 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow; 8827 } 8828 } 8829 else 8830 { 8831 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values 8832 } 8833 8834 return menu_is_open; 8835 } 8836 8837 bool ImGui::BeginMenu(const char* label, bool enabled) 8838 { 8839 return BeginMenuEx(label, NULL, enabled); 8840 } 8841 8842 void ImGui::EndMenu() 8843 { 8844 // Nav: When a left move request our menu failed, close ourselves. 8845 ImGuiContext& g = *GImGui; 8846 ImGuiWindow* window = g.CurrentWindow; 8847 IM_ASSERT(window->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginMenu()/EndMenu() calls 8848 ImGuiWindow* parent_window = window->ParentWindow; // Should always be != NULL is we passed assert. 8849 if (window->BeginCount == window->BeginCountPreviousFrame) 8850 if (g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet()) 8851 if (g.NavWindow && (g.NavWindow->RootWindowForNav == window) && parent_window->DC.LayoutType == ImGuiLayoutType_Vertical) 8852 { 8853 ClosePopupToLevel(g.BeginPopupStack.Size - 1, true); 8854 NavMoveRequestCancel(); 8855 } 8856 8857 EndPopup(); 8858 } 8859 8860 bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut, bool selected, bool enabled) 8861 { 8862 ImGuiWindow* window = GetCurrentWindow(); 8863 if (window->SkipItems) 8864 return false; 8865 8866 ImGuiContext& g = *GImGui; 8867 ImGuiStyle& style = g.Style; 8868 ImVec2 pos = window->DC.CursorPos; 8869 ImVec2 label_size = CalcTextSize(label, NULL, true); 8870 8871 // See BeginMenuEx() for comments about this. 8872 const bool menuset_is_open = IsRootOfOpenMenuSet(); 8873 if (menuset_is_open) 8874 PushItemFlag(ImGuiItemFlags_NoWindowHoverableCheck, true); 8875 8876 // We've been using the equivalent of ImGuiSelectableFlags_SetNavIdOnHover on all Selectable() since early Nav system days (commit 43ee5d73), 8877 // but I am unsure whether this should be kept at all. For now moved it to be an opt-in feature used by menus only. 8878 bool pressed; 8879 PushID(label); 8880 if (!enabled) 8881 BeginDisabled(); 8882 8883 // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another. 8884 const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_SelectOnRelease | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SetNavIdOnHover; 8885 const ImGuiMenuColumns* offsets = &window->DC.MenuColumns; 8886 if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) 8887 { 8888 // Mimic the exact layout spacing of BeginMenu() to allow MenuItem() inside a menu bar, which is a little misleading but may be useful 8889 // Note that in this situation: we don't render the shortcut, we render a highlight instead of the selected tick mark. 8890 float w = label_size.x; 8891 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f); 8892 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); 8893 PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(style.ItemSpacing.x * 2.0f, style.ItemSpacing.y)); 8894 pressed = Selectable("", selected, selectable_flags, ImVec2(w, 0.0f)); 8895 PopStyleVar(); 8896 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) 8897 RenderText(text_pos, label); 8898 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar(). 8899 } 8900 else 8901 { 8902 // Menu item inside a vertical menu 8903 // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f. 8904 // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system. 8905 float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f; 8906 float shortcut_w = (shortcut && shortcut[0]) ? CalcTextSize(shortcut, NULL).x : 0.0f; 8907 float checkmark_w = IM_TRUNC(g.FontSize * 1.20f); 8908 float min_w = window->DC.MenuColumns.DeclColumns(icon_w, label_size.x, shortcut_w, checkmark_w); // Feedback for next frame 8909 float stretch_w = ImMax(0.0f, GetContentRegionAvail().x - min_w); 8910 pressed = Selectable("", false, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y)); 8911 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) 8912 { 8913 RenderText(pos + ImVec2(offsets->OffsetLabel, 0.0f), label); 8914 if (icon_w > 0.0f) 8915 RenderText(pos + ImVec2(offsets->OffsetIcon, 0.0f), icon); 8916 if (shortcut_w > 0.0f) 8917 { 8918 PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]); 8919 RenderText(pos + ImVec2(offsets->OffsetShortcut + stretch_w, 0.0f), shortcut, NULL, false); 8920 PopStyleColor(); 8921 } 8922 if (selected) 8923 RenderCheckMark(window->DrawList, pos + ImVec2(offsets->OffsetMark + stretch_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), GetColorU32(ImGuiCol_Text), g.FontSize * 0.866f); 8924 } 8925 } 8926 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0)); 8927 if (!enabled) 8928 EndDisabled(); 8929 PopID(); 8930 if (menuset_is_open) 8931 PopItemFlag(); 8932 8933 return pressed; 8934 } 8935 8936 bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled) 8937 { 8938 return MenuItemEx(label, NULL, shortcut, selected, enabled); 8939 } 8940 8941 bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled) 8942 { 8943 if (MenuItemEx(label, NULL, shortcut, p_selected ? *p_selected : false, enabled)) 8944 { 8945 if (p_selected) 8946 *p_selected = !*p_selected; 8947 return true; 8948 } 8949 return false; 8950 } 8951 8952 //------------------------------------------------------------------------- 8953 // [SECTION] Widgets: BeginTabBar, EndTabBar, etc. 8954 //------------------------------------------------------------------------- 8955 // - BeginTabBar() 8956 // - BeginTabBarEx() [Internal] 8957 // - EndTabBar() 8958 // - TabBarLayout() [Internal] 8959 // - TabBarCalcTabID() [Internal] 8960 // - TabBarCalcMaxTabWidth() [Internal] 8961 // - TabBarFindTabById() [Internal] 8962 // - TabBarFindTabByOrder() [Internal] 8963 // - TabBarGetCurrentTab() [Internal] 8964 // - TabBarGetTabName() [Internal] 8965 // - TabBarRemoveTab() [Internal] 8966 // - TabBarCloseTab() [Internal] 8967 // - TabBarScrollClamp() [Internal] 8968 // - TabBarScrollToTab() [Internal] 8969 // - TabBarQueueFocus() [Internal] 8970 // - TabBarQueueReorder() [Internal] 8971 // - TabBarProcessReorderFromMousePos() [Internal] 8972 // - TabBarProcessReorder() [Internal] 8973 // - TabBarScrollingButtons() [Internal] 8974 // - TabBarTabListPopupButton() [Internal] 8975 //------------------------------------------------------------------------- 8976 8977 struct ImGuiTabBarSection 8978 { 8979 int TabCount; // Number of tabs in this section. 8980 float Width; // Sum of width of tabs in this section (after shrinking down) 8981 float Spacing; // Horizontal spacing at the end of the section. 8982 8983 ImGuiTabBarSection() { memset(this, 0, sizeof(*this)); } 8984 }; 8985 8986 namespace ImGui 8987 { 8988 static void TabBarLayout(ImGuiTabBar* tab_bar); 8989 static ImU32 TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window); 8990 static float TabBarCalcMaxTabWidth(); 8991 static float TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling); 8992 static void TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections); 8993 static ImGuiTabItem* TabBarScrollingButtons(ImGuiTabBar* tab_bar); 8994 static ImGuiTabItem* TabBarTabListPopupButton(ImGuiTabBar* tab_bar); 8995 } 8996 8997 ImGuiTabBar::ImGuiTabBar() 8998 { 8999 memset(this, 0, sizeof(*this)); 9000 CurrFrameVisible = PrevFrameVisible = -1; 9001 LastTabItemIdx = -1; 9002 } 9003 9004 static inline int TabItemGetSectionIdx(const ImGuiTabItem* tab) 9005 { 9006 return (tab->Flags & ImGuiTabItemFlags_Leading) ? 0 : (tab->Flags & ImGuiTabItemFlags_Trailing) ? 2 : 1; 9007 } 9008 9009 static int IMGUI_CDECL TabItemComparerBySection(const void* lhs, const void* rhs) 9010 { 9011 const ImGuiTabItem* a = (const ImGuiTabItem*)lhs; 9012 const ImGuiTabItem* b = (const ImGuiTabItem*)rhs; 9013 const int a_section = TabItemGetSectionIdx(a); 9014 const int b_section = TabItemGetSectionIdx(b); 9015 if (a_section != b_section) 9016 return a_section - b_section; 9017 return (int)(a->IndexDuringLayout - b->IndexDuringLayout); 9018 } 9019 9020 static int IMGUI_CDECL TabItemComparerByBeginOrder(const void* lhs, const void* rhs) 9021 { 9022 const ImGuiTabItem* a = (const ImGuiTabItem*)lhs; 9023 const ImGuiTabItem* b = (const ImGuiTabItem*)rhs; 9024 return (int)(a->BeginOrder - b->BeginOrder); 9025 } 9026 9027 static ImGuiTabBar* GetTabBarFromTabBarRef(const ImGuiPtrOrIndex& ref) 9028 { 9029 ImGuiContext& g = *GImGui; 9030 return ref.Ptr ? (ImGuiTabBar*)ref.Ptr : g.TabBars.GetByIndex(ref.Index); 9031 } 9032 9033 static ImGuiPtrOrIndex GetTabBarRefFromTabBar(ImGuiTabBar* tab_bar) 9034 { 9035 ImGuiContext& g = *GImGui; 9036 if (g.TabBars.Contains(tab_bar)) 9037 return ImGuiPtrOrIndex(g.TabBars.GetIndex(tab_bar)); 9038 return ImGuiPtrOrIndex(tab_bar); 9039 } 9040 9041 bool ImGui::BeginTabBar(const char* str_id, ImGuiTabBarFlags flags) 9042 { 9043 ImGuiContext& g = *GImGui; 9044 ImGuiWindow* window = g.CurrentWindow; 9045 if (window->SkipItems) 9046 return false; 9047 9048 ImGuiID id = window->GetID(str_id); 9049 ImGuiTabBar* tab_bar = g.TabBars.GetOrAddByKey(id); 9050 ImRect tab_bar_bb = ImRect(window->DC.CursorPos.x, window->DC.CursorPos.y, window->WorkRect.Max.x, window->DC.CursorPos.y + g.FontSize + g.Style.FramePadding.y * 2); 9051 tab_bar->ID = id; 9052 tab_bar->SeparatorMinX = tab_bar->BarRect.Min.x - IM_TRUNC(window->WindowPadding.x * 0.5f); 9053 tab_bar->SeparatorMaxX = tab_bar->BarRect.Max.x + IM_TRUNC(window->WindowPadding.x * 0.5f); 9054 //if (g.NavWindow && IsWindowChildOf(g.NavWindow, window, false, false)) 9055 flags |= ImGuiTabBarFlags_IsFocused; 9056 return BeginTabBarEx(tab_bar, tab_bar_bb, flags); 9057 } 9058 9059 bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImGuiTabBarFlags flags) 9060 { 9061 ImGuiContext& g = *GImGui; 9062 ImGuiWindow* window = g.CurrentWindow; 9063 if (window->SkipItems) 9064 return false; 9065 9066 IM_ASSERT(tab_bar->ID != 0); 9067 if ((flags & ImGuiTabBarFlags_DockNode) == 0) 9068 PushOverrideID(tab_bar->ID); 9069 9070 // Add to stack 9071 g.CurrentTabBarStack.push_back(GetTabBarRefFromTabBar(tab_bar)); 9072 g.CurrentTabBar = tab_bar; 9073 9074 // Append with multiple BeginTabBar()/EndTabBar() pairs. 9075 tab_bar->BackupCursorPos = window->DC.CursorPos; 9076 if (tab_bar->CurrFrameVisible == g.FrameCount) 9077 { 9078 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY); 9079 tab_bar->BeginCount++; 9080 return true; 9081 } 9082 9083 // Ensure correct ordering when toggling ImGuiTabBarFlags_Reorderable flag, or when a new tab was added while being not reorderable 9084 if ((flags & ImGuiTabBarFlags_Reorderable) != (tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (tab_bar->TabsAddedNew && !(flags & ImGuiTabBarFlags_Reorderable))) 9085 ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerByBeginOrder); 9086 tab_bar->TabsAddedNew = false; 9087 9088 // Flags 9089 if ((flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0) 9090 flags |= ImGuiTabBarFlags_FittingPolicyDefault_; 9091 9092 tab_bar->Flags = flags; 9093 tab_bar->BarRect = tab_bar_bb; 9094 tab_bar->WantLayout = true; // Layout will be done on the first call to ItemTab() 9095 tab_bar->PrevFrameVisible = tab_bar->CurrFrameVisible; 9096 tab_bar->CurrFrameVisible = g.FrameCount; 9097 tab_bar->PrevTabsContentsHeight = tab_bar->CurrTabsContentsHeight; 9098 tab_bar->CurrTabsContentsHeight = 0.0f; 9099 tab_bar->ItemSpacingY = g.Style.ItemSpacing.y; 9100 tab_bar->FramePadding = g.Style.FramePadding; 9101 tab_bar->TabsActiveCount = 0; 9102 tab_bar->LastTabItemIdx = -1; 9103 tab_bar->BeginCount = 1; 9104 9105 // Set cursor pos in a way which only be used in the off-chance the user erroneously submits item before BeginTabItem(): items will overlap 9106 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY); 9107 9108 // Draw separator 9109 // (it would be misleading to draw this in EndTabBar() suggesting that it may be drawn over tabs, as tab bar are appendable) 9110 const ImU32 col = GetColorU32((flags & ImGuiTabBarFlags_IsFocused) ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected); 9111 if (g.Style.TabBarBorderSize > 0.0f) 9112 { 9113 const float y = tab_bar->BarRect.Max.y; 9114 window->DrawList->AddRectFilled(ImVec2(tab_bar->SeparatorMinX, y - g.Style.TabBarBorderSize), ImVec2(tab_bar->SeparatorMaxX, y), col); 9115 } 9116 return true; 9117 } 9118 9119 void ImGui::EndTabBar() 9120 { 9121 ImGuiContext& g = *GImGui; 9122 ImGuiWindow* window = g.CurrentWindow; 9123 if (window->SkipItems) 9124 return; 9125 9126 ImGuiTabBar* tab_bar = g.CurrentTabBar; 9127 if (tab_bar == NULL) 9128 { 9129 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Mismatched BeginTabBar()/EndTabBar()!"); 9130 return; 9131 } 9132 9133 // Fallback in case no TabItem have been submitted 9134 if (tab_bar->WantLayout) 9135 TabBarLayout(tab_bar); 9136 9137 // Restore the last visible height if no tab is visible, this reduce vertical flicker/movement when a tabs gets removed without calling SetTabItemClosed(). 9138 const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount); 9139 if (tab_bar->VisibleTabWasSubmitted || tab_bar->VisibleTabId == 0 || tab_bar_appearing) 9140 { 9141 tab_bar->CurrTabsContentsHeight = ImMax(window->DC.CursorPos.y - tab_bar->BarRect.Max.y, tab_bar->CurrTabsContentsHeight); 9142 window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->CurrTabsContentsHeight; 9143 } 9144 else 9145 { 9146 window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->PrevTabsContentsHeight; 9147 } 9148 if (tab_bar->BeginCount > 1) 9149 window->DC.CursorPos = tab_bar->BackupCursorPos; 9150 9151 tab_bar->LastTabItemIdx = -1; 9152 if ((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0) 9153 PopID(); 9154 9155 g.CurrentTabBarStack.pop_back(); 9156 g.CurrentTabBar = g.CurrentTabBarStack.empty() ? NULL : GetTabBarFromTabBarRef(g.CurrentTabBarStack.back()); 9157 } 9158 9159 // Scrolling happens only in the central section (leading/trailing sections are not scrolling) 9160 static float TabBarCalcScrollableWidth(ImGuiTabBar* tab_bar, ImGuiTabBarSection* sections) 9161 { 9162 return tab_bar->BarRect.GetWidth() - sections[0].Width - sections[2].Width - sections[1].Spacing; 9163 } 9164 9165 // This is called only once a frame before by the first call to ItemTab() 9166 // The reason we're not calling it in BeginTabBar() is to leave a chance to the user to call the SetTabItemClosed() functions. 9167 static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar) 9168 { 9169 ImGuiContext& g = *GImGui; 9170 tab_bar->WantLayout = false; 9171 9172 // Garbage collect by compacting list 9173 // Detect if we need to sort out tab list (e.g. in rare case where a tab changed section) 9174 int tab_dst_n = 0; 9175 bool need_sort_by_section = false; 9176 ImGuiTabBarSection sections[3]; // Layout sections: Leading, Central, Trailing 9177 for (int tab_src_n = 0; tab_src_n < tab_bar->Tabs.Size; tab_src_n++) 9178 { 9179 ImGuiTabItem* tab = &tab_bar->Tabs[tab_src_n]; 9180 if (tab->LastFrameVisible < tab_bar->PrevFrameVisible || tab->WantClose) 9181 { 9182 // Remove tab 9183 if (tab_bar->VisibleTabId == tab->ID) { tab_bar->VisibleTabId = 0; } 9184 if (tab_bar->SelectedTabId == tab->ID) { tab_bar->SelectedTabId = 0; } 9185 if (tab_bar->NextSelectedTabId == tab->ID) { tab_bar->NextSelectedTabId = 0; } 9186 continue; 9187 } 9188 if (tab_dst_n != tab_src_n) 9189 tab_bar->Tabs[tab_dst_n] = tab_bar->Tabs[tab_src_n]; 9190 9191 tab = &tab_bar->Tabs[tab_dst_n]; 9192 tab->IndexDuringLayout = (ImS16)tab_dst_n; 9193 9194 // We will need sorting if tabs have changed section (e.g. moved from one of Leading/Central/Trailing to another) 9195 int curr_tab_section_n = TabItemGetSectionIdx(tab); 9196 if (tab_dst_n > 0) 9197 { 9198 ImGuiTabItem* prev_tab = &tab_bar->Tabs[tab_dst_n - 1]; 9199 int prev_tab_section_n = TabItemGetSectionIdx(prev_tab); 9200 if (curr_tab_section_n == 0 && prev_tab_section_n != 0) 9201 need_sort_by_section = true; 9202 if (prev_tab_section_n == 2 && curr_tab_section_n != 2) 9203 need_sort_by_section = true; 9204 } 9205 9206 sections[curr_tab_section_n].TabCount++; 9207 tab_dst_n++; 9208 } 9209 if (tab_bar->Tabs.Size != tab_dst_n) 9210 tab_bar->Tabs.resize(tab_dst_n); 9211 9212 if (need_sort_by_section) 9213 ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerBySection); 9214 9215 // Calculate spacing between sections 9216 sections[0].Spacing = sections[0].TabCount > 0 && (sections[1].TabCount + sections[2].TabCount) > 0 ? g.Style.ItemInnerSpacing.x : 0.0f; 9217 sections[1].Spacing = sections[1].TabCount > 0 && sections[2].TabCount > 0 ? g.Style.ItemInnerSpacing.x : 0.0f; 9218 9219 // Setup next selected tab 9220 ImGuiID scroll_to_tab_id = 0; 9221 if (tab_bar->NextSelectedTabId) 9222 { 9223 tab_bar->SelectedTabId = tab_bar->NextSelectedTabId; 9224 tab_bar->NextSelectedTabId = 0; 9225 scroll_to_tab_id = tab_bar->SelectedTabId; 9226 } 9227 9228 // Process order change request (we could probably process it when requested but it's just saner to do it in a single spot). 9229 if (tab_bar->ReorderRequestTabId != 0) 9230 { 9231 if (TabBarProcessReorder(tab_bar)) 9232 if (tab_bar->ReorderRequestTabId == tab_bar->SelectedTabId) 9233 scroll_to_tab_id = tab_bar->ReorderRequestTabId; 9234 tab_bar->ReorderRequestTabId = 0; 9235 } 9236 9237 // Tab List Popup (will alter tab_bar->BarRect and therefore the available width!) 9238 const bool tab_list_popup_button = (tab_bar->Flags & ImGuiTabBarFlags_TabListPopupButton) != 0; 9239 if (tab_list_popup_button) 9240 if (ImGuiTabItem* tab_to_select = TabBarTabListPopupButton(tab_bar)) // NB: Will alter BarRect.Min.x! 9241 scroll_to_tab_id = tab_bar->SelectedTabId = tab_to_select->ID; 9242 9243 // Leading/Trailing tabs will be shrink only if central one aren't visible anymore, so layout the shrink data as: leading, trailing, central 9244 // (whereas our tabs are stored as: leading, central, trailing) 9245 int shrink_buffer_indexes[3] = { 0, sections[0].TabCount + sections[2].TabCount, sections[0].TabCount }; 9246 g.ShrinkWidthBuffer.resize(tab_bar->Tabs.Size); 9247 9248 // Compute ideal tabs widths + store them into shrink buffer 9249 ImGuiTabItem* most_recently_selected_tab = NULL; 9250 int curr_section_n = -1; 9251 bool found_selected_tab_id = false; 9252 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) 9253 { 9254 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; 9255 IM_ASSERT(tab->LastFrameVisible >= tab_bar->PrevFrameVisible); 9256 9257 if ((most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) && !(tab->Flags & ImGuiTabItemFlags_Button)) 9258 most_recently_selected_tab = tab; 9259 if (tab->ID == tab_bar->SelectedTabId) 9260 found_selected_tab_id = true; 9261 if (scroll_to_tab_id == 0 && g.NavJustMovedToId == tab->ID) 9262 scroll_to_tab_id = tab->ID; 9263 9264 // Refresh tab width immediately, otherwise changes of style e.g. style.FramePadding.x would noticeably lag in the tab bar. 9265 // Additionally, when using TabBarAddTab() to manipulate tab bar order we occasionally insert new tabs that don't have a width yet, 9266 // and we cannot wait for the next BeginTabItem() call. We cannot compute this width within TabBarAddTab() because font size depends on the active window. 9267 const char* tab_name = TabBarGetTabName(tab_bar, tab); 9268 const bool has_close_button_or_unsaved_marker = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) == 0 || (tab->Flags & ImGuiTabItemFlags_UnsavedDocument); 9269 tab->ContentWidth = (tab->RequestedWidth >= 0.0f) ? tab->RequestedWidth : TabItemCalcSize(tab_name, has_close_button_or_unsaved_marker).x; 9270 9271 int section_n = TabItemGetSectionIdx(tab); 9272 ImGuiTabBarSection* section = §ions[section_n]; 9273 section->Width += tab->ContentWidth + (section_n == curr_section_n ? g.Style.ItemInnerSpacing.x : 0.0f); 9274 curr_section_n = section_n; 9275 9276 // Store data so we can build an array sorted by width if we need to shrink tabs down 9277 IM_MSVC_WARNING_SUPPRESS(6385); 9278 ImGuiShrinkWidthItem* shrink_width_item = &g.ShrinkWidthBuffer[shrink_buffer_indexes[section_n]++]; 9279 shrink_width_item->Index = tab_n; 9280 shrink_width_item->Width = shrink_width_item->InitialWidth = tab->ContentWidth; 9281 tab->Width = ImMax(tab->ContentWidth, 1.0f); 9282 } 9283 9284 // Compute total ideal width (used for e.g. auto-resizing a window) 9285 tab_bar->WidthAllTabsIdeal = 0.0f; 9286 for (int section_n = 0; section_n < 3; section_n++) 9287 tab_bar->WidthAllTabsIdeal += sections[section_n].Width + sections[section_n].Spacing; 9288 9289 // Horizontal scrolling buttons 9290 // (note that TabBarScrollButtons() will alter BarRect.Max.x) 9291 if ((tab_bar->WidthAllTabsIdeal > tab_bar->BarRect.GetWidth() && tab_bar->Tabs.Size > 1) && !(tab_bar->Flags & ImGuiTabBarFlags_NoTabListScrollingButtons) && (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll)) 9292 if (ImGuiTabItem* scroll_and_select_tab = TabBarScrollingButtons(tab_bar)) 9293 { 9294 scroll_to_tab_id = scroll_and_select_tab->ID; 9295 if ((scroll_and_select_tab->Flags & ImGuiTabItemFlags_Button) == 0) 9296 tab_bar->SelectedTabId = scroll_to_tab_id; 9297 } 9298 9299 // Shrink widths if full tabs don't fit in their allocated space 9300 float section_0_w = sections[0].Width + sections[0].Spacing; 9301 float section_1_w = sections[1].Width + sections[1].Spacing; 9302 float section_2_w = sections[2].Width + sections[2].Spacing; 9303 bool central_section_is_visible = (section_0_w + section_2_w) < tab_bar->BarRect.GetWidth(); 9304 float width_excess; 9305 if (central_section_is_visible) 9306 width_excess = ImMax(section_1_w - (tab_bar->BarRect.GetWidth() - section_0_w - section_2_w), 0.0f); // Excess used to shrink central section 9307 else 9308 width_excess = (section_0_w + section_2_w) - tab_bar->BarRect.GetWidth(); // Excess used to shrink leading/trailing section 9309 9310 // With ImGuiTabBarFlags_FittingPolicyScroll policy, we will only shrink leading/trailing if the central section is not visible anymore 9311 if (width_excess >= 1.0f && ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyResizeDown) || !central_section_is_visible)) 9312 { 9313 int shrink_data_count = (central_section_is_visible ? sections[1].TabCount : sections[0].TabCount + sections[2].TabCount); 9314 int shrink_data_offset = (central_section_is_visible ? sections[0].TabCount + sections[2].TabCount : 0); 9315 ShrinkWidths(g.ShrinkWidthBuffer.Data + shrink_data_offset, shrink_data_count, width_excess); 9316 9317 // Apply shrunk values into tabs and sections 9318 for (int tab_n = shrink_data_offset; tab_n < shrink_data_offset + shrink_data_count; tab_n++) 9319 { 9320 ImGuiTabItem* tab = &tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index]; 9321 float shrinked_width = IM_TRUNC(g.ShrinkWidthBuffer[tab_n].Width); 9322 if (shrinked_width < 0.0f) 9323 continue; 9324 9325 shrinked_width = ImMax(1.0f, shrinked_width); 9326 int section_n = TabItemGetSectionIdx(tab); 9327 sections[section_n].Width -= (tab->Width - shrinked_width); 9328 tab->Width = shrinked_width; 9329 } 9330 } 9331 9332 // Layout all active tabs 9333 int section_tab_index = 0; 9334 float tab_offset = 0.0f; 9335 tab_bar->WidthAllTabs = 0.0f; 9336 for (int section_n = 0; section_n < 3; section_n++) 9337 { 9338 ImGuiTabBarSection* section = §ions[section_n]; 9339 if (section_n == 2) 9340 tab_offset = ImMin(ImMax(0.0f, tab_bar->BarRect.GetWidth() - section->Width), tab_offset); 9341 9342 for (int tab_n = 0; tab_n < section->TabCount; tab_n++) 9343 { 9344 ImGuiTabItem* tab = &tab_bar->Tabs[section_tab_index + tab_n]; 9345 tab->Offset = tab_offset; 9346 tab->NameOffset = -1; 9347 tab_offset += tab->Width + (tab_n < section->TabCount - 1 ? g.Style.ItemInnerSpacing.x : 0.0f); 9348 } 9349 tab_bar->WidthAllTabs += ImMax(section->Width + section->Spacing, 0.0f); 9350 tab_offset += section->Spacing; 9351 section_tab_index += section->TabCount; 9352 } 9353 9354 // Clear name buffers 9355 tab_bar->TabsNames.Buf.resize(0); 9356 9357 // If we have lost the selected tab, select the next most recently active one 9358 if (found_selected_tab_id == false) 9359 tab_bar->SelectedTabId = 0; 9360 if (tab_bar->SelectedTabId == 0 && tab_bar->NextSelectedTabId == 0 && most_recently_selected_tab != NULL) 9361 scroll_to_tab_id = tab_bar->SelectedTabId = most_recently_selected_tab->ID; 9362 9363 // Lock in visible tab 9364 tab_bar->VisibleTabId = tab_bar->SelectedTabId; 9365 tab_bar->VisibleTabWasSubmitted = false; 9366 9367 // Apply request requests 9368 if (scroll_to_tab_id != 0) 9369 TabBarScrollToTab(tab_bar, scroll_to_tab_id, sections); 9370 else if ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll) && IsMouseHoveringRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max, true) && IsWindowContentHoverable(g.CurrentWindow)) 9371 { 9372 const float wheel = g.IO.MouseWheelRequestAxisSwap ? g.IO.MouseWheel : g.IO.MouseWheelH; 9373 const ImGuiKey wheel_key = g.IO.MouseWheelRequestAxisSwap ? ImGuiKey_MouseWheelY : ImGuiKey_MouseWheelX; 9374 if (TestKeyOwner(wheel_key, tab_bar->ID) && wheel != 0.0f) 9375 { 9376 const float scroll_step = wheel * TabBarCalcScrollableWidth(tab_bar, sections) / 3.0f; 9377 tab_bar->ScrollingTargetDistToVisibility = 0.0f; 9378 tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, tab_bar->ScrollingTarget - scroll_step); 9379 } 9380 SetKeyOwner(wheel_key, tab_bar->ID); 9381 } 9382 9383 // Update scrolling 9384 tab_bar->ScrollingAnim = TabBarScrollClamp(tab_bar, tab_bar->ScrollingAnim); 9385 tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, tab_bar->ScrollingTarget); 9386 if (tab_bar->ScrollingAnim != tab_bar->ScrollingTarget) 9387 { 9388 // Scrolling speed adjust itself so we can always reach our target in 1/3 seconds. 9389 // Teleport if we are aiming far off the visible line 9390 tab_bar->ScrollingSpeed = ImMax(tab_bar->ScrollingSpeed, 70.0f * g.FontSize); 9391 tab_bar->ScrollingSpeed = ImMax(tab_bar->ScrollingSpeed, ImFabs(tab_bar->ScrollingTarget - tab_bar->ScrollingAnim) / 0.3f); 9392 const bool teleport = (tab_bar->PrevFrameVisible + 1 < g.FrameCount) || (tab_bar->ScrollingTargetDistToVisibility > 10.0f * g.FontSize); 9393 tab_bar->ScrollingAnim = teleport ? tab_bar->ScrollingTarget : ImLinearSweep(tab_bar->ScrollingAnim, tab_bar->ScrollingTarget, g.IO.DeltaTime * tab_bar->ScrollingSpeed); 9394 } 9395 else 9396 { 9397 tab_bar->ScrollingSpeed = 0.0f; 9398 } 9399 tab_bar->ScrollingRectMinX = tab_bar->BarRect.Min.x + sections[0].Width + sections[0].Spacing; 9400 tab_bar->ScrollingRectMaxX = tab_bar->BarRect.Max.x - sections[2].Width - sections[1].Spacing; 9401 9402 // Actual layout in host window (we don't do it in BeginTabBar() so as not to waste an extra frame) 9403 ImGuiWindow* window = g.CurrentWindow; 9404 window->DC.CursorPos = tab_bar->BarRect.Min; 9405 ItemSize(ImVec2(tab_bar->WidthAllTabs, tab_bar->BarRect.GetHeight()), tab_bar->FramePadding.y); 9406 window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, tab_bar->BarRect.Min.x + tab_bar->WidthAllTabsIdeal); 9407 } 9408 9409 // Dockable windows uses Name/ID in the global namespace. Non-dockable items use the ID stack. 9410 static ImU32 ImGui::TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window) 9411 { 9412 IM_ASSERT(docked_window == NULL); // master branch only 9413 IM_UNUSED(docked_window); 9414 if (tab_bar->Flags & ImGuiTabBarFlags_DockNode) 9415 { 9416 ImGuiID id = ImHashStr(label); 9417 KeepAliveID(id); 9418 return id; 9419 } 9420 else 9421 { 9422 ImGuiWindow* window = GImGui->CurrentWindow; 9423 return window->GetID(label); 9424 } 9425 } 9426 9427 static float ImGui::TabBarCalcMaxTabWidth() 9428 { 9429 ImGuiContext& g = *GImGui; 9430 return g.FontSize * 20.0f; 9431 } 9432 9433 ImGuiTabItem* ImGui::TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id) 9434 { 9435 if (tab_id != 0) 9436 for (int n = 0; n < tab_bar->Tabs.Size; n++) 9437 if (tab_bar->Tabs[n].ID == tab_id) 9438 return &tab_bar->Tabs[n]; 9439 return NULL; 9440 } 9441 9442 // Order = visible order, not submission order! (which is tab->BeginOrder) 9443 ImGuiTabItem* ImGui::TabBarFindTabByOrder(ImGuiTabBar* tab_bar, int order) 9444 { 9445 if (order < 0 || order >= tab_bar->Tabs.Size) 9446 return NULL; 9447 return &tab_bar->Tabs[order]; 9448 } 9449 9450 ImGuiTabItem* ImGui::TabBarGetCurrentTab(ImGuiTabBar* tab_bar) 9451 { 9452 if (tab_bar->LastTabItemIdx < 0 || tab_bar->LastTabItemIdx >= tab_bar->Tabs.Size) 9453 return NULL; 9454 return &tab_bar->Tabs[tab_bar->LastTabItemIdx]; 9455 } 9456 9457 const char* ImGui::TabBarGetTabName(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) 9458 { 9459 if (tab->NameOffset == -1) 9460 return "N/A"; 9461 IM_ASSERT(tab->NameOffset < tab_bar->TabsNames.Buf.Size); 9462 return tab_bar->TabsNames.Buf.Data + tab->NameOffset; 9463 } 9464 9465 // The *TabId fields are already set by the docking system _before_ the actual TabItem was created, so we clear them regardless. 9466 void ImGui::TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id) 9467 { 9468 if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id)) 9469 tab_bar->Tabs.erase(tab); 9470 if (tab_bar->VisibleTabId == tab_id) { tab_bar->VisibleTabId = 0; } 9471 if (tab_bar->SelectedTabId == tab_id) { tab_bar->SelectedTabId = 0; } 9472 if (tab_bar->NextSelectedTabId == tab_id) { tab_bar->NextSelectedTabId = 0; } 9473 } 9474 9475 // Called on manual closure attempt 9476 void ImGui::TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) 9477 { 9478 if (tab->Flags & ImGuiTabItemFlags_Button) 9479 return; // A button appended with TabItemButton(). 9480 9481 if ((tab->Flags & (ImGuiTabItemFlags_UnsavedDocument | ImGuiTabItemFlags_NoAssumedClosure)) == 0) 9482 { 9483 // This will remove a frame of lag for selecting another tab on closure. 9484 // However we don't run it in the case where the 'Unsaved' flag is set, so user gets a chance to fully undo the closure 9485 tab->WantClose = true; 9486 if (tab_bar->VisibleTabId == tab->ID) 9487 { 9488 tab->LastFrameVisible = -1; 9489 tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = 0; 9490 } 9491 } 9492 else 9493 { 9494 // Actually select before expecting closure attempt (on an UnsavedDocument tab user is expect to e.g. show a popup) 9495 if (tab_bar->VisibleTabId != tab->ID) 9496 TabBarQueueFocus(tab_bar, tab); 9497 } 9498 } 9499 9500 static float ImGui::TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling) 9501 { 9502 scrolling = ImMin(scrolling, tab_bar->WidthAllTabs - tab_bar->BarRect.GetWidth()); 9503 return ImMax(scrolling, 0.0f); 9504 } 9505 9506 // Note: we may scroll to tab that are not selected! e.g. using keyboard arrow keys 9507 static void ImGui::TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections) 9508 { 9509 ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id); 9510 if (tab == NULL) 9511 return; 9512 if (tab->Flags & ImGuiTabItemFlags_SectionMask_) 9513 return; 9514 9515 ImGuiContext& g = *GImGui; 9516 float margin = g.FontSize * 1.0f; // When to scroll to make Tab N+1 visible always make a bit of N visible to suggest more scrolling area (since we don't have a scrollbar) 9517 int order = TabBarGetTabOrder(tab_bar, tab); 9518 9519 // Scrolling happens only in the central section (leading/trailing sections are not scrolling) 9520 float scrollable_width = TabBarCalcScrollableWidth(tab_bar, sections); 9521 9522 // We make all tabs positions all relative Sections[0].Width to make code simpler 9523 float tab_x1 = tab->Offset - sections[0].Width + (order > sections[0].TabCount - 1 ? -margin : 0.0f); 9524 float tab_x2 = tab->Offset - sections[0].Width + tab->Width + (order + 1 < tab_bar->Tabs.Size - sections[2].TabCount ? margin : 1.0f); 9525 tab_bar->ScrollingTargetDistToVisibility = 0.0f; 9526 if (tab_bar->ScrollingTarget > tab_x1 || (tab_x2 - tab_x1 >= scrollable_width)) 9527 { 9528 // Scroll to the left 9529 tab_bar->ScrollingTargetDistToVisibility = ImMax(tab_bar->ScrollingAnim - tab_x2, 0.0f); 9530 tab_bar->ScrollingTarget = tab_x1; 9531 } 9532 else if (tab_bar->ScrollingTarget < tab_x2 - scrollable_width) 9533 { 9534 // Scroll to the right 9535 tab_bar->ScrollingTargetDistToVisibility = ImMax((tab_x1 - scrollable_width) - tab_bar->ScrollingAnim, 0.0f); 9536 tab_bar->ScrollingTarget = tab_x2 - scrollable_width; 9537 } 9538 } 9539 9540 void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) 9541 { 9542 tab_bar->NextSelectedTabId = tab->ID; 9543 } 9544 9545 void ImGui::TabBarQueueReorder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab, int offset) 9546 { 9547 IM_ASSERT(offset != 0); 9548 IM_ASSERT(tab_bar->ReorderRequestTabId == 0); 9549 tab_bar->ReorderRequestTabId = tab->ID; 9550 tab_bar->ReorderRequestOffset = (ImS16)offset; 9551 } 9552 9553 void ImGui::TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, ImGuiTabItem* src_tab, ImVec2 mouse_pos) 9554 { 9555 ImGuiContext& g = *GImGui; 9556 IM_ASSERT(tab_bar->ReorderRequestTabId == 0); 9557 if ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) == 0) 9558 return; 9559 9560 const bool is_central_section = (src_tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0; 9561 const float bar_offset = tab_bar->BarRect.Min.x - (is_central_section ? tab_bar->ScrollingTarget : 0); 9562 9563 // Count number of contiguous tabs we are crossing over 9564 const int dir = (bar_offset + src_tab->Offset) > mouse_pos.x ? -1 : +1; 9565 const int src_idx = tab_bar->Tabs.index_from_ptr(src_tab); 9566 int dst_idx = src_idx; 9567 for (int i = src_idx; i >= 0 && i < tab_bar->Tabs.Size; i += dir) 9568 { 9569 // Reordered tabs must share the same section 9570 const ImGuiTabItem* dst_tab = &tab_bar->Tabs[i]; 9571 if (dst_tab->Flags & ImGuiTabItemFlags_NoReorder) 9572 break; 9573 if ((dst_tab->Flags & ImGuiTabItemFlags_SectionMask_) != (src_tab->Flags & ImGuiTabItemFlags_SectionMask_)) 9574 break; 9575 dst_idx = i; 9576 9577 // Include spacing after tab, so when mouse cursor is between tabs we would not continue checking further tabs that are not hovered. 9578 const float x1 = bar_offset + dst_tab->Offset - g.Style.ItemInnerSpacing.x; 9579 const float x2 = bar_offset + dst_tab->Offset + dst_tab->Width + g.Style.ItemInnerSpacing.x; 9580 //GetForegroundDrawList()->AddRect(ImVec2(x1, tab_bar->BarRect.Min.y), ImVec2(x2, tab_bar->BarRect.Max.y), IM_COL32(255, 0, 0, 255)); 9581 if ((dir < 0 && mouse_pos.x > x1) || (dir > 0 && mouse_pos.x < x2)) 9582 break; 9583 } 9584 9585 if (dst_idx != src_idx) 9586 TabBarQueueReorder(tab_bar, src_tab, dst_idx - src_idx); 9587 } 9588 9589 bool ImGui::TabBarProcessReorder(ImGuiTabBar* tab_bar) 9590 { 9591 ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_bar->ReorderRequestTabId); 9592 if (tab1 == NULL || (tab1->Flags & ImGuiTabItemFlags_NoReorder)) 9593 return false; 9594 9595 //IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_Reorderable); // <- this may happen when using debug tools 9596 int tab2_order = TabBarGetTabOrder(tab_bar, tab1) + tab_bar->ReorderRequestOffset; 9597 if (tab2_order < 0 || tab2_order >= tab_bar->Tabs.Size) 9598 return false; 9599 9600 // Reordered tabs must share the same section 9601 // (Note: TabBarQueueReorderFromMousePos() also has a similar test but since we allow direct calls to TabBarQueueReorder() we do it here too) 9602 ImGuiTabItem* tab2 = &tab_bar->Tabs[tab2_order]; 9603 if (tab2->Flags & ImGuiTabItemFlags_NoReorder) 9604 return false; 9605 if ((tab1->Flags & ImGuiTabItemFlags_SectionMask_) != (tab2->Flags & ImGuiTabItemFlags_SectionMask_)) 9606 return false; 9607 9608 ImGuiTabItem item_tmp = *tab1; 9609 ImGuiTabItem* src_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 + 1 : tab2; 9610 ImGuiTabItem* dst_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 : tab2 + 1; 9611 const int move_count = (tab_bar->ReorderRequestOffset > 0) ? tab_bar->ReorderRequestOffset : -tab_bar->ReorderRequestOffset; 9612 memmove(dst_tab, src_tab, move_count * sizeof(ImGuiTabItem)); 9613 *tab2 = item_tmp; 9614 9615 if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings) 9616 MarkIniSettingsDirty(); 9617 return true; 9618 } 9619 9620 static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar) 9621 { 9622 ImGuiContext& g = *GImGui; 9623 ImGuiWindow* window = g.CurrentWindow; 9624 9625 const ImVec2 arrow_button_size(g.FontSize - 2.0f, g.FontSize + g.Style.FramePadding.y * 2.0f); 9626 const float scrolling_buttons_width = arrow_button_size.x * 2.0f; 9627 9628 const ImVec2 backup_cursor_pos = window->DC.CursorPos; 9629 //window->DrawList->AddRect(ImVec2(tab_bar->BarRect.Max.x - scrolling_buttons_width, tab_bar->BarRect.Min.y), ImVec2(tab_bar->BarRect.Max.x, tab_bar->BarRect.Max.y), IM_COL32(255,0,0,255)); 9630 9631 int select_dir = 0; 9632 ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text]; 9633 arrow_col.w *= 0.5f; 9634 9635 PushStyleColor(ImGuiCol_Text, arrow_col); 9636 PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); 9637 const float backup_repeat_delay = g.IO.KeyRepeatDelay; 9638 const float backup_repeat_rate = g.IO.KeyRepeatRate; 9639 g.IO.KeyRepeatDelay = 0.250f; 9640 g.IO.KeyRepeatRate = 0.200f; 9641 float x = ImMax(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.x - scrolling_buttons_width); 9642 window->DC.CursorPos = ImVec2(x, tab_bar->BarRect.Min.y); 9643 if (ArrowButtonEx("##<", ImGuiDir_Left, arrow_button_size, ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_Repeat)) 9644 select_dir = -1; 9645 window->DC.CursorPos = ImVec2(x + arrow_button_size.x, tab_bar->BarRect.Min.y); 9646 if (ArrowButtonEx("##>", ImGuiDir_Right, arrow_button_size, ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_Repeat)) 9647 select_dir = +1; 9648 PopStyleColor(2); 9649 g.IO.KeyRepeatRate = backup_repeat_rate; 9650 g.IO.KeyRepeatDelay = backup_repeat_delay; 9651 9652 ImGuiTabItem* tab_to_scroll_to = NULL; 9653 if (select_dir != 0) 9654 if (ImGuiTabItem* tab_item = TabBarFindTabByID(tab_bar, tab_bar->SelectedTabId)) 9655 { 9656 int selected_order = TabBarGetTabOrder(tab_bar, tab_item); 9657 int target_order = selected_order + select_dir; 9658 9659 // Skip tab item buttons until another tab item is found or end is reached 9660 while (tab_to_scroll_to == NULL) 9661 { 9662 // If we are at the end of the list, still scroll to make our tab visible 9663 tab_to_scroll_to = &tab_bar->Tabs[(target_order >= 0 && target_order < tab_bar->Tabs.Size) ? target_order : selected_order]; 9664 9665 // Cross through buttons 9666 // (even if first/last item is a button, return it so we can update the scroll) 9667 if (tab_to_scroll_to->Flags & ImGuiTabItemFlags_Button) 9668 { 9669 target_order += select_dir; 9670 selected_order += select_dir; 9671 tab_to_scroll_to = (target_order < 0 || target_order >= tab_bar->Tabs.Size) ? tab_to_scroll_to : NULL; 9672 } 9673 } 9674 } 9675 window->DC.CursorPos = backup_cursor_pos; 9676 tab_bar->BarRect.Max.x -= scrolling_buttons_width + 1.0f; 9677 9678 return tab_to_scroll_to; 9679 } 9680 9681 static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar) 9682 { 9683 ImGuiContext& g = *GImGui; 9684 ImGuiWindow* window = g.CurrentWindow; 9685 9686 // We use g.Style.FramePadding.y to match the square ArrowButton size 9687 const float tab_list_popup_button_width = g.FontSize + g.Style.FramePadding.y; 9688 const ImVec2 backup_cursor_pos = window->DC.CursorPos; 9689 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x - g.Style.FramePadding.y, tab_bar->BarRect.Min.y); 9690 tab_bar->BarRect.Min.x += tab_list_popup_button_width; 9691 9692 ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text]; 9693 arrow_col.w *= 0.5f; 9694 PushStyleColor(ImGuiCol_Text, arrow_col); 9695 PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); 9696 bool open = BeginCombo("##v", NULL, ImGuiComboFlags_NoPreview | ImGuiComboFlags_HeightLargest); 9697 PopStyleColor(2); 9698 9699 ImGuiTabItem* tab_to_select = NULL; 9700 if (open) 9701 { 9702 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) 9703 { 9704 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; 9705 if (tab->Flags & ImGuiTabItemFlags_Button) 9706 continue; 9707 9708 const char* tab_name = TabBarGetTabName(tab_bar, tab); 9709 if (Selectable(tab_name, tab_bar->SelectedTabId == tab->ID)) 9710 tab_to_select = tab; 9711 } 9712 EndCombo(); 9713 } 9714 9715 window->DC.CursorPos = backup_cursor_pos; 9716 return tab_to_select; 9717 } 9718 9719 //------------------------------------------------------------------------- 9720 // [SECTION] Widgets: BeginTabItem, EndTabItem, etc. 9721 //------------------------------------------------------------------------- 9722 // - BeginTabItem() 9723 // - EndTabItem() 9724 // - TabItemButton() 9725 // - TabItemEx() [Internal] 9726 // - SetTabItemClosed() 9727 // - TabItemCalcSize() [Internal] 9728 // - TabItemBackground() [Internal] 9729 // - TabItemLabelAndCloseButton() [Internal] 9730 //------------------------------------------------------------------------- 9731 9732 bool ImGui::BeginTabItem(const char* label, bool* p_open, ImGuiTabItemFlags flags) 9733 { 9734 ImGuiContext& g = *GImGui; 9735 ImGuiWindow* window = g.CurrentWindow; 9736 if (window->SkipItems) 9737 return false; 9738 9739 ImGuiTabBar* tab_bar = g.CurrentTabBar; 9740 if (tab_bar == NULL) 9741 { 9742 IM_ASSERT_USER_ERROR(tab_bar, "Needs to be called between BeginTabBar() and EndTabBar()!"); 9743 return false; 9744 } 9745 IM_ASSERT(!(flags & ImGuiTabItemFlags_Button)); // BeginTabItem() Can't be used with button flags, use TabItemButton() instead! 9746 9747 bool ret = TabItemEx(tab_bar, label, p_open, flags, NULL); 9748 if (ret && !(flags & ImGuiTabItemFlags_NoPushId)) 9749 { 9750 ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx]; 9751 PushOverrideID(tab->ID); // We already hashed 'label' so push into the ID stack directly instead of doing another hash through PushID(label) 9752 } 9753 return ret; 9754 } 9755 9756 void ImGui::EndTabItem() 9757 { 9758 ImGuiContext& g = *GImGui; 9759 ImGuiWindow* window = g.CurrentWindow; 9760 if (window->SkipItems) 9761 return; 9762 9763 ImGuiTabBar* tab_bar = g.CurrentTabBar; 9764 if (tab_bar == NULL) 9765 { 9766 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!"); 9767 return; 9768 } 9769 IM_ASSERT(tab_bar->LastTabItemIdx >= 0); 9770 ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx]; 9771 if (!(tab->Flags & ImGuiTabItemFlags_NoPushId)) 9772 PopID(); 9773 } 9774 9775 bool ImGui::TabItemButton(const char* label, ImGuiTabItemFlags flags) 9776 { 9777 ImGuiContext& g = *GImGui; 9778 ImGuiWindow* window = g.CurrentWindow; 9779 if (window->SkipItems) 9780 return false; 9781 9782 ImGuiTabBar* tab_bar = g.CurrentTabBar; 9783 if (tab_bar == NULL) 9784 { 9785 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!"); 9786 return false; 9787 } 9788 return TabItemEx(tab_bar, label, NULL, flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder, NULL); 9789 } 9790 9791 bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags, ImGuiWindow* docked_window) 9792 { 9793 // Layout whole tab bar if not already done 9794 ImGuiContext& g = *GImGui; 9795 if (tab_bar->WantLayout) 9796 { 9797 ImGuiNextItemData backup_next_item_data = g.NextItemData; 9798 TabBarLayout(tab_bar); 9799 g.NextItemData = backup_next_item_data; 9800 } 9801 ImGuiWindow* window = g.CurrentWindow; 9802 if (window->SkipItems) 9803 return false; 9804 9805 const ImGuiStyle& style = g.Style; 9806 const ImGuiID id = TabBarCalcTabID(tab_bar, label, docked_window); 9807 9808 // If the user called us with *p_open == false, we early out and don't render. 9809 // We make a call to ItemAdd() so that attempts to use a contextual popup menu with an implicit ID won't use an older ID. 9810 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); 9811 if (p_open && !*p_open) 9812 { 9813 ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav); 9814 return false; 9815 } 9816 9817 IM_ASSERT(!p_open || !(flags & ImGuiTabItemFlags_Button)); 9818 IM_ASSERT((flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)) != (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)); // Can't use both Leading and Trailing 9819 9820 // Store into ImGuiTabItemFlags_NoCloseButton, also honor ImGuiTabItemFlags_NoCloseButton passed by user (although not documented) 9821 if (flags & ImGuiTabItemFlags_NoCloseButton) 9822 p_open = NULL; 9823 else if (p_open == NULL) 9824 flags |= ImGuiTabItemFlags_NoCloseButton; 9825 9826 // Acquire tab data 9827 ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, id); 9828 bool tab_is_new = false; 9829 if (tab == NULL) 9830 { 9831 tab_bar->Tabs.push_back(ImGuiTabItem()); 9832 tab = &tab_bar->Tabs.back(); 9833 tab->ID = id; 9834 tab_bar->TabsAddedNew = tab_is_new = true; 9835 } 9836 tab_bar->LastTabItemIdx = (ImS16)tab_bar->Tabs.index_from_ptr(tab); 9837 9838 // Calculate tab contents size 9839 ImVec2 size = TabItemCalcSize(label, (p_open != NULL) || (flags & ImGuiTabItemFlags_UnsavedDocument)); 9840 tab->RequestedWidth = -1.0f; 9841 if (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasWidth) 9842 size.x = tab->RequestedWidth = g.NextItemData.Width; 9843 if (tab_is_new) 9844 tab->Width = ImMax(1.0f, size.x); 9845 tab->ContentWidth = size.x; 9846 tab->BeginOrder = tab_bar->TabsActiveCount++; 9847 9848 const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount); 9849 const bool tab_bar_focused = (tab_bar->Flags & ImGuiTabBarFlags_IsFocused) != 0; 9850 const bool tab_appearing = (tab->LastFrameVisible + 1 < g.FrameCount); 9851 const bool tab_just_unsaved = (flags & ImGuiTabItemFlags_UnsavedDocument) && !(tab->Flags & ImGuiTabItemFlags_UnsavedDocument); 9852 const bool is_tab_button = (flags & ImGuiTabItemFlags_Button) != 0; 9853 tab->LastFrameVisible = g.FrameCount; 9854 tab->Flags = flags; 9855 9856 // Append name _WITH_ the zero-terminator 9857 if (docked_window != NULL) 9858 { 9859 IM_ASSERT(docked_window == NULL); // master branch only 9860 } 9861 else 9862 { 9863 tab->NameOffset = (ImS32)tab_bar->TabsNames.size(); 9864 tab_bar->TabsNames.append(label, label + strlen(label) + 1); 9865 } 9866 9867 // Update selected tab 9868 if (!is_tab_button) 9869 { 9870 if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0) 9871 if (!tab_bar_appearing || tab_bar->SelectedTabId == 0) 9872 TabBarQueueFocus(tab_bar, tab); // New tabs gets activated 9873 if ((flags & ImGuiTabItemFlags_SetSelected) && (tab_bar->SelectedTabId != id)) // _SetSelected can only be passed on explicit tab bar 9874 TabBarQueueFocus(tab_bar, tab); 9875 } 9876 9877 // Lock visibility 9878 // (Note: tab_contents_visible != tab_selected... because CTRL+TAB operations may preview some tabs without selecting them!) 9879 bool tab_contents_visible = (tab_bar->VisibleTabId == id); 9880 if (tab_contents_visible) 9881 tab_bar->VisibleTabWasSubmitted = true; 9882 9883 // On the very first frame of a tab bar we let first tab contents be visible to minimize appearing glitches 9884 if (!tab_contents_visible && tab_bar->SelectedTabId == 0 && tab_bar_appearing) 9885 if (tab_bar->Tabs.Size == 1 && !(tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs)) 9886 tab_contents_visible = true; 9887 9888 // Note that tab_is_new is not necessarily the same as tab_appearing! When a tab bar stops being submitted 9889 // and then gets submitted again, the tabs will have 'tab_appearing=true' but 'tab_is_new=false'. 9890 if (tab_appearing && (!tab_bar_appearing || tab_is_new)) 9891 { 9892 ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav); 9893 if (is_tab_button) 9894 return false; 9895 return tab_contents_visible; 9896 } 9897 9898 if (tab_bar->SelectedTabId == id) 9899 tab->LastFrameSelected = g.FrameCount; 9900 9901 // Backup current layout position 9902 const ImVec2 backup_main_cursor_pos = window->DC.CursorPos; 9903 9904 // Layout 9905 const bool is_central_section = (tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0; 9906 size.x = tab->Width; 9907 if (is_central_section) 9908 window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_TRUNC(tab->Offset - tab_bar->ScrollingAnim), 0.0f); 9909 else 9910 window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(tab->Offset, 0.0f); 9911 ImVec2 pos = window->DC.CursorPos; 9912 ImRect bb(pos, pos + size); 9913 9914 // We don't have CPU clipping primitives to clip the CloseButton (until it becomes a texture), so need to add an extra draw call (temporary in the case of vertical animation) 9915 const bool want_clip_rect = is_central_section && (bb.Min.x < tab_bar->ScrollingRectMinX || bb.Max.x > tab_bar->ScrollingRectMaxX); 9916 if (want_clip_rect) 9917 PushClipRect(ImVec2(ImMax(bb.Min.x, tab_bar->ScrollingRectMinX), bb.Min.y - 1), ImVec2(tab_bar->ScrollingRectMaxX, bb.Max.y), true); 9918 9919 ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos; 9920 ItemSize(bb.GetSize(), style.FramePadding.y); 9921 window->DC.CursorMaxPos = backup_cursor_max_pos; 9922 9923 if (!ItemAdd(bb, id)) 9924 { 9925 if (want_clip_rect) 9926 PopClipRect(); 9927 window->DC.CursorPos = backup_main_cursor_pos; 9928 return tab_contents_visible; 9929 } 9930 9931 // Click to Select a tab 9932 // Allow the close button to overlap 9933 ImGuiButtonFlags button_flags = ((is_tab_button ? ImGuiButtonFlags_PressedOnClickRelease : ImGuiButtonFlags_PressedOnClick) | ImGuiButtonFlags_AllowOverlap); 9934 if (g.DragDropActive) 9935 button_flags |= ImGuiButtonFlags_PressedOnDragDropHold; 9936 bool hovered, held; 9937 bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags); 9938 if (pressed && !is_tab_button) 9939 TabBarQueueFocus(tab_bar, tab); 9940 9941 // Drag and drop: re-order tabs 9942 if (held && !tab_appearing && IsMouseDragging(0)) 9943 { 9944 if (!g.DragDropActive && (tab_bar->Flags & ImGuiTabBarFlags_Reorderable)) 9945 { 9946 // While moving a tab it will jump on the other side of the mouse, so we also test for MouseDelta.x 9947 if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < bb.Min.x) 9948 { 9949 TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos); 9950 } 9951 else if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > bb.Max.x) 9952 { 9953 TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos); 9954 } 9955 } 9956 } 9957 9958 #if 0 9959 if (hovered && g.HoveredIdNotActiveTimer > TOOLTIP_DELAY && bb.GetWidth() < tab->ContentWidth) 9960 { 9961 // Enlarge tab display when hovering 9962 bb.Max.x = bb.Min.x + IM_TRUNC(ImLerp(bb.GetWidth(), tab->ContentWidth, ImSaturate((g.HoveredIdNotActiveTimer - 0.40f) * 6.0f))); 9963 display_draw_list = GetForegroundDrawList(window); 9964 TabItemBackground(display_draw_list, bb, flags, GetColorU32(ImGuiCol_TitleBgActive)); 9965 } 9966 #endif 9967 9968 // Render tab shape 9969 ImDrawList* display_draw_list = window->DrawList; 9970 const ImU32 tab_col = GetColorU32((held || hovered) ? ImGuiCol_TabHovered : tab_contents_visible ? (tab_bar_focused ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected) : (tab_bar_focused ? ImGuiCol_Tab : ImGuiCol_TabDimmed)); 9971 TabItemBackground(display_draw_list, bb, flags, tab_col); 9972 if (tab_contents_visible && (tab_bar->Flags & ImGuiTabBarFlags_DrawSelectedOverline) && style.TabBarOverlineSize > 0.0f) 9973 { 9974 float x_offset = IM_TRUNC(0.4f * style.TabRounding); 9975 if (x_offset < 2.0f * g.CurrentDpiScale) 9976 x_offset = 0.0f; 9977 float y_offset = 1.0f * g.CurrentDpiScale; 9978 display_draw_list->AddLine(bb.GetTL() + ImVec2(x_offset, y_offset), bb.GetTR() + ImVec2(-x_offset, y_offset), GetColorU32(tab_bar_focused ? ImGuiCol_TabSelectedOverline : ImGuiCol_TabDimmedSelectedOverline), style.TabBarOverlineSize); 9979 } 9980 RenderNavHighlight(bb, id); 9981 9982 // Select with right mouse button. This is so the common idiom for context menu automatically highlight the current widget. 9983 const bool hovered_unblocked = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup); 9984 if (hovered_unblocked && (IsMouseClicked(1) || IsMouseReleased(1)) && !is_tab_button) 9985 TabBarQueueFocus(tab_bar, tab); 9986 9987 if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton) 9988 flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton; 9989 9990 // Render tab label, process close button 9991 const ImGuiID close_button_id = p_open ? GetIDWithSeed("#CLOSE", NULL, id) : 0; 9992 bool just_closed; 9993 bool text_clipped; 9994 TabItemLabelAndCloseButton(display_draw_list, bb, tab_just_unsaved ? (flags & ~ImGuiTabItemFlags_UnsavedDocument) : flags, tab_bar->FramePadding, label, id, close_button_id, tab_contents_visible, &just_closed, &text_clipped); 9995 if (just_closed && p_open != NULL) 9996 { 9997 *p_open = false; 9998 TabBarCloseTab(tab_bar, tab); 9999 } 10000 10001 // Restore main window position so user can draw there 10002 if (want_clip_rect) 10003 PopClipRect(); 10004 window->DC.CursorPos = backup_main_cursor_pos; 10005 10006 // Tooltip 10007 // (Won't work over the close button because ItemOverlap systems messes up with HoveredIdTimer-> seems ok) 10008 // (We test IsItemHovered() to discard e.g. when another item is active or drag and drop over the tab bar, which g.HoveredId ignores) 10009 // FIXME: This is a mess. 10010 // FIXME: We may want disabled tab to still display the tooltip? 10011 if (text_clipped && g.HoveredId == id && !held) 10012 if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip) && !(tab->Flags & ImGuiTabItemFlags_NoTooltip)) 10013 SetItemTooltip("%.*s", (int)(FindRenderedTextEnd(label) - label), label); 10014 10015 IM_ASSERT(!is_tab_button || !(tab_bar->SelectedTabId == tab->ID && is_tab_button)); // TabItemButton should not be selected 10016 if (is_tab_button) 10017 return pressed; 10018 return tab_contents_visible; 10019 } 10020 10021 // [Public] This is call is 100% optional but it allows to remove some one-frame glitches when a tab has been unexpectedly removed. 10022 // To use it to need to call the function SetTabItemClosed() between BeginTabBar() and EndTabBar(). 10023 // Tabs closed by the close button will automatically be flagged to avoid this issue. 10024 void ImGui::SetTabItemClosed(const char* label) 10025 { 10026 ImGuiContext& g = *GImGui; 10027 bool is_within_manual_tab_bar = g.CurrentTabBar && !(g.CurrentTabBar->Flags & ImGuiTabBarFlags_DockNode); 10028 if (is_within_manual_tab_bar) 10029 { 10030 ImGuiTabBar* tab_bar = g.CurrentTabBar; 10031 ImGuiID tab_id = TabBarCalcTabID(tab_bar, label, NULL); 10032 if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id)) 10033 tab->WantClose = true; // Will be processed by next call to TabBarLayout() 10034 } 10035 } 10036 10037 ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button_or_unsaved_marker) 10038 { 10039 ImGuiContext& g = *GImGui; 10040 ImVec2 label_size = CalcTextSize(label, NULL, true); 10041 ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x, label_size.y + g.Style.FramePadding.y * 2.0f); 10042 if (has_close_button_or_unsaved_marker) 10043 size.x += g.Style.FramePadding.x + (g.Style.ItemInnerSpacing.x + g.FontSize); // We use Y intentionally to fit the close button circle. 10044 else 10045 size.x += g.Style.FramePadding.x + 1.0f; 10046 return ImVec2(ImMin(size.x, TabBarCalcMaxTabWidth()), size.y); 10047 } 10048 10049 ImVec2 ImGui::TabItemCalcSize(ImGuiWindow*) 10050 { 10051 IM_ASSERT(0); // This function exists to facilitate merge with 'docking' branch. 10052 return ImVec2(0.0f, 0.0f); 10053 } 10054 10055 void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImU32 col) 10056 { 10057 // While rendering tabs, we trim 1 pixel off the top of our bounding box so they can fit within a regular frame height while looking "detached" from it. 10058 ImGuiContext& g = *GImGui; 10059 const float width = bb.GetWidth(); 10060 IM_UNUSED(flags); 10061 IM_ASSERT(width > 0.0f); 10062 const float rounding = ImMax(0.0f, ImMin((flags & ImGuiTabItemFlags_Button) ? g.Style.FrameRounding : g.Style.TabRounding, width * 0.5f - 1.0f)); 10063 const float y1 = bb.Min.y + 1.0f; 10064 const float y2 = bb.Max.y - g.Style.TabBarBorderSize; 10065 draw_list->PathLineTo(ImVec2(bb.Min.x, y2)); 10066 draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding, y1 + rounding), rounding, 6, 9); 10067 draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding, y1 + rounding), rounding, 9, 12); 10068 draw_list->PathLineTo(ImVec2(bb.Max.x, y2)); 10069 draw_list->PathFillConvex(col); 10070 if (g.Style.TabBorderSize > 0.0f) 10071 { 10072 draw_list->PathLineTo(ImVec2(bb.Min.x + 0.5f, y2)); 10073 draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding + 0.5f, y1 + rounding + 0.5f), rounding, 6, 9); 10074 draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding - 0.5f, y1 + rounding + 0.5f), rounding, 9, 12); 10075 draw_list->PathLineTo(ImVec2(bb.Max.x - 0.5f, y2)); 10076 draw_list->PathStroke(GetColorU32(ImGuiCol_Border), 0, g.Style.TabBorderSize); 10077 } 10078 } 10079 10080 // Render text label (with custom clipping) + Unsaved Document marker + Close Button logic 10081 // We tend to lock style.FramePadding for a given tab-bar, hence the 'frame_padding' parameter. 10082 void ImGui::TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImVec2 frame_padding, const char* label, ImGuiID tab_id, ImGuiID close_button_id, bool is_contents_visible, bool* out_just_closed, bool* out_text_clipped) 10083 { 10084 ImGuiContext& g = *GImGui; 10085 ImVec2 label_size = CalcTextSize(label, NULL, true); 10086 10087 if (out_just_closed) 10088 *out_just_closed = false; 10089 if (out_text_clipped) 10090 *out_text_clipped = false; 10091 10092 if (bb.GetWidth() <= 1.0f) 10093 return; 10094 10095 // In Style V2 we'll have full override of all colors per state (e.g. focused, selected) 10096 // But right now if you want to alter text color of tabs this is what you need to do. 10097 #if 0 10098 const float backup_alpha = g.Style.Alpha; 10099 if (!is_contents_visible) 10100 g.Style.Alpha *= 0.7f; 10101 #endif 10102 10103 // Render text label (with clipping + alpha gradient) + unsaved marker 10104 ImRect text_pixel_clip_bb(bb.Min.x + frame_padding.x, bb.Min.y + frame_padding.y, bb.Max.x - frame_padding.x, bb.Max.y); 10105 ImRect text_ellipsis_clip_bb = text_pixel_clip_bb; 10106 10107 // Return clipped state ignoring the close button 10108 if (out_text_clipped) 10109 { 10110 *out_text_clipped = (text_ellipsis_clip_bb.Min.x + label_size.x) > text_pixel_clip_bb.Max.x; 10111 //draw_list->AddCircle(text_ellipsis_clip_bb.Min, 3.0f, *out_text_clipped ? IM_COL32(255, 0, 0, 255) : IM_COL32(0, 255, 0, 255)); 10112 } 10113 10114 const float button_sz = g.FontSize; 10115 const ImVec2 button_pos(ImMax(bb.Min.x, bb.Max.x - frame_padding.x - button_sz), bb.Min.y + frame_padding.y); 10116 10117 // Close Button & Unsaved Marker 10118 // We are relying on a subtle and confusing distinction between 'hovered' and 'g.HoveredId' which happens because we are using ImGuiButtonFlags_AllowOverlapMode + SetItemAllowOverlap() 10119 // 'hovered' will be true when hovering the Tab but NOT when hovering the close button 10120 // 'g.HoveredId==id' will be true when hovering the Tab including when hovering the close button 10121 // 'g.ActiveId==close_button_id' will be true when we are holding on the close button, in which case both hovered booleans are false 10122 bool close_button_pressed = false; 10123 bool close_button_visible = false; 10124 if (close_button_id != 0) 10125 if (is_contents_visible || bb.GetWidth() >= ImMax(button_sz, g.Style.TabMinWidthForCloseButton)) 10126 if (g.HoveredId == tab_id || g.HoveredId == close_button_id || g.ActiveId == tab_id || g.ActiveId == close_button_id) 10127 close_button_visible = true; 10128 bool unsaved_marker_visible = (flags & ImGuiTabItemFlags_UnsavedDocument) != 0 && (button_pos.x + button_sz <= bb.Max.x); 10129 10130 if (close_button_visible) 10131 { 10132 ImGuiLastItemData last_item_backup = g.LastItemData; 10133 if (CloseButton(close_button_id, button_pos)) 10134 close_button_pressed = true; 10135 g.LastItemData = last_item_backup; 10136 10137 // Close with middle mouse button 10138 if (!(flags & ImGuiTabItemFlags_NoCloseWithMiddleMouseButton) && IsMouseClicked(2)) 10139 close_button_pressed = true; 10140 } 10141 else if (unsaved_marker_visible) 10142 { 10143 const ImRect bullet_bb(button_pos, button_pos + ImVec2(button_sz, button_sz)); 10144 RenderBullet(draw_list, bullet_bb.GetCenter(), GetColorU32(ImGuiCol_Text)); 10145 } 10146 10147 // This is all rather complicated 10148 // (the main idea is that because the close button only appears on hover, we don't want it to alter the ellipsis position) 10149 // FIXME: if FramePadding is noticeably large, ellipsis_max_x will be wrong here (e.g. #3497), maybe for consistency that parameter of RenderTextEllipsis() shouldn't exist.. 10150 float ellipsis_max_x = close_button_visible ? text_pixel_clip_bb.Max.x : bb.Max.x - 1.0f; 10151 if (close_button_visible || unsaved_marker_visible) 10152 { 10153 text_pixel_clip_bb.Max.x -= close_button_visible ? (button_sz) : (button_sz * 0.80f); 10154 text_ellipsis_clip_bb.Max.x -= unsaved_marker_visible ? (button_sz * 0.80f) : 0.0f; 10155 ellipsis_max_x = text_pixel_clip_bb.Max.x; 10156 } 10157 RenderTextEllipsis(draw_list, text_ellipsis_clip_bb.Min, text_ellipsis_clip_bb.Max, text_pixel_clip_bb.Max.x, ellipsis_max_x, label, NULL, &label_size); 10158 10159 #if 0 10160 if (!is_contents_visible) 10161 g.Style.Alpha = backup_alpha; 10162 #endif 10163 10164 if (out_just_closed) 10165 *out_just_closed = close_button_pressed; 10166 } 10167 10168 10169 #endif // #ifndef IMGUI_DISABLE