upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/imgui/imgui_widgets.cpp
diff options
context:
space:
mode:
authorbitflash-coin <bookofshamers@outlook.com>2026-08-04 06:34:32 -0700
committerbitflash-coin <bookofshamers@outlook.com>2026-08-04 06:34:32 -0700
commit5ce650382352c1503f40abf08129ada69402af55 (patch)
tree6a18198901f1e2c4933f1ac53c9365eec512bf63 /src/imgui/imgui_widgets.cpp
parentf34113291da7e5e3dc7d2468d4f9c212c05c584b (diff)
public commit
Diffstat (limited to 'src/imgui/imgui_widgets.cpp')
-rw-r--r--src/imgui/imgui_widgets.cpp11040
1 files changed, 11040 insertions, 0 deletions
diff --git a/src/imgui/imgui_widgets.cpp b/src/imgui/imgui_widgets.cpp
new file mode 100644
index 0000000..6c324e7
--- /dev/null
+++ b/src/imgui/imgui_widgets.cpp
@@ -0,0 +1,11040 @@
1// dear imgui, v1.92.9
2// (widgets code)
3
4/*
5
6Index 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" // warning: format specifies type 'int' but the argument has type 'unsigned int'
74#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.
75#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness
76#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.
77#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0
78#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.
79#pragma clang diagnostic ignored "-Wenum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_')
80#pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated
81#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision
82#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access
83#pragma clang diagnostic ignored "-Wnontrivial-memaccess" // warning: first argument in call to 'memset' is a pointer to non-trivially copyable type
84#pragma clang diagnostic ignored "-Wswitch-default" // warning: 'switch' missing 'default' label
85#elif defined(__GNUC__)
86#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind
87#pragma GCC diagnostic ignored "-Wfloat-equal" // warning: comparing floating-point with '==' or '!=' is unsafe
88#pragma GCC diagnostic ignored "-Wformat" // warning: format '%p' expects argument of type 'int'/'void*', but argument X has type 'unsigned int'/'ImGuiWindow*'
89#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked
90#pragma GCC diagnostic ignored "-Wdeprecated-enum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated
91#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function
92#pragma GCC diagnostic ignored "-Wstrict-overflow" // warning: assuming signed overflow does not occur when simplifying division / ..when changing X +- C1 cmp C2 to X cmp C2 -+ C1
93#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
94#pragma GCC diagnostic ignored "-Wcast-qual" // warning: cast from type 'const xxxx *' to type 'xxxx *' casts away qualifiers
95#pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may change value
96#pragma GCC diagnostic ignored "-Wsign-conversion" // warning: conversion to 'xxxx' from 'xxxx' may change the sign of the result
97#endif
98
99//-------------------------------------------------------------------------
100// Data
101//-------------------------------------------------------------------------
102
103// Widgets
104static const float DRAGDROP_HOLD_TO_OPEN_TIMER = 0.70f; // Time for drag-hold to activate items accepting the ImGuiButtonFlags_PressedOnDragDropHold button behavior.
105static 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.
106
107// Those MIN/MAX values are not define because we need to point to them
108static const signed char IM_S8_MIN = -128;
109static const signed char IM_S8_MAX = 127;
110static const unsigned char IM_U8_MIN = 0;
111static const unsigned char IM_U8_MAX = 0xFF;
112static const signed short IM_S16_MIN = -32768;
113static const signed short IM_S16_MAX = 32767;
114static const unsigned short IM_U16_MIN = 0;
115static const unsigned short IM_U16_MAX = 0xFFFF;
116static const ImS32 IM_S32_MIN = INT_MIN; // (-2147483647 - 1), (0x80000000);
117static const ImS32 IM_S32_MAX = INT_MAX; // (2147483647), (0x7FFFFFFF)
118static const ImU32 IM_U32_MIN = 0;
119static const ImU32 IM_U32_MAX = UINT_MAX; // (0xFFFFFFFF)
120#ifdef LLONG_MIN
121static const ImS64 IM_S64_MIN = LLONG_MIN; // (-9223372036854775807ll - 1ll);
122static const ImS64 IM_S64_MAX = LLONG_MAX; // (9223372036854775807ll);
123#else
124static const ImS64 IM_S64_MIN = -9223372036854775807LL - 1;
125static const ImS64 IM_S64_MAX = 9223372036854775807LL;
126#endif
127static const ImU64 IM_U64_MIN = 0;
128#ifdef ULLONG_MAX
129static const ImU64 IM_U64_MAX = ULLONG_MAX; // (0xFFFFFFFFFFFFFFFFull);
130#else
131static const ImU64 IM_U64_MAX = (2ULL * 9223372036854775807LL + 1);
132#endif
133
134//-------------------------------------------------------------------------
135// [SECTION] Forward Declarations
136//-------------------------------------------------------------------------
137
138// For InputTextEx()
139static bool InputTextFilterCharacter(ImGuiContext* ctx, ImGuiInputTextState* state, unsigned int* p_char, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard = false);
140static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining = NULL, ImVec2* out_offset = NULL, ImDrawTextFlags flags = 0);
141
142//-------------------------------------------------------------------------
143// [SECTION] Widgets: Text, etc.
144//-------------------------------------------------------------------------
145// - TextEx() [Internal]
146// - TextUnformatted()
147// - Text()
148// - TextV()
149// - TextColored()
150// - TextColoredV()
151// - TextDisabled()
152// - TextDisabledV()
153// - TextWrapped()
154// - TextWrappedV()
155// - LabelText()
156// - LabelTextV()
157// - BulletText()
158// - BulletTextV()
159//-------------------------------------------------------------------------
160
161void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags)
162{
163 ImGuiWindow* window = GetCurrentWindow();
164 if (window->SkipItems)
165 return;
166 ImGuiContext& g = *GImGui;
167
168 // Accept null ranges
169 if (text == text_end)
170 text = text_end = "";
171
172 // Calculate length
173 const char* text_begin = text;
174 if (text_end == NULL)
175 text_end = text + ImStrlen(text); // FIXME-OPT
176
177 const ImVec2 text_pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
178 const float wrap_pos_x = window->DC.TextWrapPos;
179 const bool wrap_enabled = (wrap_pos_x >= 0.0f);
180 if (text_end - text <= 2000 || wrap_enabled)
181 {
182 // Common case
183 const float wrap_width = wrap_enabled ? CalcWrapWidthForPos(window->DC.CursorPos, wrap_pos_x) : 0.0f;
184 const ImVec2 text_size = CalcTextSize(text_begin, text_end, false, wrap_width);
185
186 ImRect bb(text_pos, text_pos + text_size);
187 ItemSize(text_size, 0.0f);
188 if (!ItemAdd(bb, 0))
189 return;
190
191 // Render (we don't hide text after ## in this end-user function)
192 RenderTextWrapped(bb.Min, text_begin, text_end, wrap_width);
193 }
194 else
195 {
196 // Long text!
197 // Perform manual coarse clipping to optimize for long multi-line text
198 // - From this point we will only compute the width of lines that are visible. Optimization only available when word-wrapping is disabled.
199 // - 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.
200 // - We use memchr(), pay attention that well optimized versions of those str/mem functions are much faster than a casually written loop.
201 const char* line = text;
202 const float line_height = GetTextLineHeight();
203 ImVec2 text_size(0, 0);
204
205 // Lines to skip (can't skip when logging text)
206 ImVec2 pos = text_pos;
207 if (!g.LogEnabled)
208 {
209 int lines_skippable = (int)((window->ClipRect.Min.y - text_pos.y) / line_height);
210 if (lines_skippable > 0)
211 {
212 int lines_skipped = 0;
213 while (line < text_end && lines_skipped < lines_skippable)
214 {
215 const char* line_end = (const char*)ImMemchr(line, '\n', text_end - line);
216 if (!line_end)
217 line_end = text_end;
218 if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0)
219 text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x);
220 line = line_end + 1;
221 lines_skipped++;
222 }
223 pos.y += lines_skipped * line_height;
224 }
225 }
226
227 // Lines to render
228 if (line < text_end)
229 {
230 ImRect line_rect(pos, pos + ImVec2(FLT_MAX, line_height));
231 while (line < text_end)
232 {
233 if (IsClippedEx(line_rect, 0))
234 break;
235
236 const char* line_end = (const char*)ImMemchr(line, '\n', text_end - line);
237 if (!line_end)
238 line_end = text_end;
239 text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x);
240 RenderText(pos, line, line_end, false);
241 line = line_end + 1;
242 line_rect.Min.y += line_height;
243 line_rect.Max.y += line_height;
244 pos.y += line_height;
245 }
246
247 // Count remaining lines
248 int lines_skipped = 0;
249 while (line < text_end)
250 {
251 const char* line_end = (const char*)ImMemchr(line, '\n', text_end - line);
252 if (!line_end)
253 line_end = text_end;
254 if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0)
255 text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x);
256 line = line_end + 1;
257 lines_skipped++;
258 }
259 pos.y += lines_skipped * line_height;
260 }
261 text_size.y = (pos - text_pos).y;
262
263 ImRect bb(text_pos, text_pos + text_size);
264 ItemSize(text_size, 0.0f);
265 ItemAdd(bb, 0);
266 }
267}
268
269// Note that all functions taking format strings in the API may be passed ("%s", text) or ("%.*s", text_len, text),
270// which will automatically bypass the formatter.
271void ImGui::TextUnformatted(const char* text, const char* text_end)
272{
273 TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText);
274}
275
276void ImGui::Text(const char* fmt, ...)
277{
278 va_list args;
279 va_start(args, fmt);
280 TextV(fmt, args);
281 va_end(args);
282}
283
284void ImGui::TextV(const char* fmt, va_list args)
285{
286 ImGuiWindow* window = GetCurrentWindow();
287 if (window->SkipItems)
288 return;
289
290 const char* text, *text_end;
291 ImFormatStringToTempBufferV(&text, &text_end, fmt, args);
292 TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText);
293}
294
295void ImGui::TextColored(const ImVec4& col, const char* fmt, ...)
296{
297 va_list args;
298 va_start(args, fmt);
299 TextColoredV(col, fmt, args);
300 va_end(args);
301}
302
303void ImGui::TextColoredV(const ImVec4& col, const char* fmt, va_list args)
304{
305 PushStyleColor(ImGuiCol_Text, col);
306 TextV(fmt, args);
307 PopStyleColor();
308}
309
310void ImGui::TextDisabled(const char* fmt, ...)
311{
312 va_list args;
313 va_start(args, fmt);
314 TextDisabledV(fmt, args);
315 va_end(args);
316}
317
318void ImGui::TextDisabledV(const char* fmt, va_list args)
319{
320 ImGuiContext& g = *GImGui;
321 PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]);
322 TextV(fmt, args);
323 PopStyleColor();
324}
325
326void ImGui::TextWrapped(const char* fmt, ...)
327{
328 va_list args;
329 va_start(args, fmt);
330 TextWrappedV(fmt, args);
331 va_end(args);
332}
333
334void ImGui::TextWrappedV(const char* fmt, va_list args)
335{
336 ImGuiContext& g = *GImGui;
337 const bool need_backup = (g.CurrentWindow->DC.TextWrapPos < 0.0f); // Keep existing wrap position if one is already set
338 if (need_backup)
339 PushTextWrapPos(0.0f);
340 TextV(fmt, args);
341 if (need_backup)
342 PopTextWrapPos();
343}
344
345void ImGui::TextAligned(float align_x, float size_x, const char* fmt, ...)
346{
347 va_list args;
348 va_start(args, fmt);
349 TextAlignedV(align_x, size_x, fmt, args);
350 va_end(args);
351}
352
353// align_x: 0.0f = left, 0.5f = center, 1.0f = right.
354// size_x : 0.0f = shortcut for GetContentRegionAvail().x
355// FIXME-WIP: Works but API is likely to be reworked. This is designed for 1 item on the line. (#7024)
356void ImGui::TextAlignedV(float align_x, float size_x, const char* fmt, va_list args)
357{
358 ImGuiWindow* window = GetCurrentWindow();
359 if (window->SkipItems)
360 return;
361
362 const char* text, *text_end;
363 ImFormatStringToTempBufferV(&text, &text_end, fmt, args);
364 const ImVec2 text_size = CalcTextSize(text, text_end);
365 size_x = CalcItemSize(ImVec2(size_x, 0.0f), 0.0f, text_size.y).x;
366
367 ImVec2 pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
368 ImVec2 pos_max(pos.x + size_x, window->ClipRect.Max.y);
369 ImVec2 size(ImMin(size_x, text_size.x), text_size.y);
370 window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, pos.x + text_size.x);
371 window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, pos.x + text_size.x);
372 if (align_x > 0.0f && text_size.x < size_x)
373 pos.x += ImTrunc((size_x - text_size.x) * align_x);
374 RenderTextEllipsis(window->DrawList, pos, pos_max, pos_max.x, text, text_end, &text_size);
375
376 const ImVec2 backup_max_pos = window->DC.CursorMaxPos;
377 ItemSize(size);
378 ItemAdd(ImRect(pos, pos + size), 0);
379 window->DC.CursorMaxPos.x = backup_max_pos.x; // Cancel out extending content size because right-aligned text would otherwise mess it up.
380
381 if (size_x < text_size.x && IsItemHovered(ImGuiHoveredFlags_NoNavOverride | ImGuiHoveredFlags_AllowWhenDisabled | ImGuiHoveredFlags_ForTooltip))
382 SetTooltip("%.*s", (int)(text_end - text), text);
383}
384
385void ImGui::LabelText(const char* label, const char* fmt, ...)
386{
387 va_list args;
388 va_start(args, fmt);
389 LabelTextV(label, fmt, args);
390 va_end(args);
391}
392
393// Add a label+text combo aligned to other label+value widgets
394void ImGui::LabelTextV(const char* label, const char* fmt, va_list args)
395{
396 ImGuiWindow* window = GetCurrentWindow();
397 if (window->SkipItems)
398 return;
399
400 ImGuiContext& g = *GImGui;
401 const ImGuiStyle& style = g.Style;
402 const float w = CalcItemWidth();
403
404 const char* value_text_begin, *value_text_end;
405 ImFormatStringToTempBufferV(&value_text_begin, &value_text_end, fmt, args);
406 const ImVec2 value_size = CalcTextSize(value_text_begin, value_text_end, false);
407 const char* label_end = FindRenderedTextEnd(label);
408 const ImVec2 label_size = CalcTextSize(label, label_end, false);
409
410 const ImVec2 pos = window->DC.CursorPos;
411 const ImRect value_bb(pos, pos + ImVec2(w, value_size.y + style.FramePadding.y * 2));
412 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));
413 ItemSize(total_bb, style.FramePadding.y);
414 if (!ItemAdd(total_bb, 0))
415 return;
416
417 // Render
418 RenderTextClipped(value_bb.Min + style.FramePadding, value_bb.Max, value_text_begin, value_text_end, &value_size, ImVec2(0.0f, 0.0f));
419 if (label_size.x > 0.0f)
420 RenderText(ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), label, label_end, false);
421}
422
423void ImGui::BulletText(const char* fmt, ...)
424{
425 va_list args;
426 va_start(args, fmt);
427 BulletTextV(fmt, args);
428 va_end(args);
429}
430
431// Text with a little bullet aligned to the typical tree node.
432void ImGui::BulletTextV(const char* fmt, va_list args)
433{
434 ImGuiWindow* window = GetCurrentWindow();
435 if (window->SkipItems)
436 return;
437
438 ImGuiContext& g = *GImGui;
439 const ImGuiStyle& style = g.Style;
440
441 const char* text_begin, *text_end;
442 ImFormatStringToTempBufferV(&text_begin, &text_end, fmt, args);
443 const ImVec2 label_size = CalcTextSize(text_begin, text_end, false);
444 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
445 ImVec2 pos = window->DC.CursorPos;
446 pos.y += window->DC.CurrLineTextBaseOffset;
447 ItemSize(total_size, 0.0f);
448 const ImRect bb(pos, pos + total_size);
449 if (!ItemAdd(bb, 0))
450 return;
451
452 // Render
453 ImU32 text_col = GetColorU32(ImGuiCol_Text);
454 RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, g.FontSize * 0.5f), text_col);
455 RenderText(bb.Min + ImVec2(g.FontSize + style.FramePadding.x * 2, 0.0f), text_begin, text_end, false);
456}
457
458//-------------------------------------------------------------------------
459// [SECTION] Widgets: Main
460//-------------------------------------------------------------------------
461// - ButtonBehavior() [Internal]
462// - Button()
463// - SmallButton()
464// - InvisibleButton()
465// - ArrowButton()
466// - CloseButton() [Internal]
467// - CollapseButton() [Internal]
468// - GetWindowScrollbarID() [Internal]
469// - GetWindowScrollbarRect() [Internal]
470// - Scrollbar() [Internal]
471// - ScrollbarEx() [Internal]
472// - Image()
473// - ImageButton()
474// - Checkbox()
475// - CheckboxFlagsT() [Internal]
476// - CheckboxFlags()
477// - RadioButton()
478// - ProgressBar()
479// - Bullet()
480// - Hyperlink()
481//-------------------------------------------------------------------------
482
483// The ButtonBehavior() function is key to many interactions and used by many/most widgets.
484// Because we handle so many cases (keyboard/gamepad navigation, drag and drop) and many specific behavior (via ImGuiButtonFlags_),
485// this code is a little complex.
486// By far the most common path is interacting with the Mouse using the default ImGuiButtonFlags_PressedOnClickRelease button behavior.
487// See the series of events below and the corresponding state reported by dear imgui:
488//------------------------------------------------------------------------------------------------------------------------------------------------
489// with PressedOnClickRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
490// Frame N+0 (mouse is outside bb) - - - - - -
491// Frame N+1 (mouse moves inside bb) - true - - - -
492// Frame N+2 (mouse button is down) - true true true - true
493// Frame N+3 (mouse button is down) - true true - - -
494// Frame N+4 (mouse moves outside bb) - - true - - -
495// Frame N+5 (mouse moves inside bb) - true true - - -
496// Frame N+6 (mouse button is released) true true - - true -
497// Frame N+7 (mouse button is released) - true - - - -
498// Frame N+8 (mouse moves outside bb) - - - - - -
499//------------------------------------------------------------------------------------------------------------------------------------------------
500// with PressedOnClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
501// Frame N+2 (mouse button is down) true true true true - true
502// Frame N+3 (mouse button is down) - true true - - -
503// Frame N+6 (mouse button is released) - true - - true -
504// Frame N+7 (mouse button is released) - true - - - -
505//------------------------------------------------------------------------------------------------------------------------------------------------
506// with PressedOnRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
507// Frame N+2 (mouse button is down) - true - - - true
508// Frame N+3 (mouse button is down) - true - - - -
509// Frame N+6 (mouse button is released) true true - - - -
510// Frame N+7 (mouse button is released) - true - - - -
511//------------------------------------------------------------------------------------------------------------------------------------------------
512// with PressedOnDoubleClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
513// Frame N+0 (mouse button is down) - true - - - true
514// Frame N+1 (mouse button is down) - true - - - -
515// Frame N+2 (mouse button is released) - true - - - -
516// Frame N+3 (mouse button is released) - true - - - -
517// Frame N+4 (mouse button is down) true true true true - true
518// Frame N+5 (mouse button is down) - true true - - -
519// Frame N+6 (mouse button is released) - true - - true -
520// Frame N+7 (mouse button is released) - true - - - -
521//------------------------------------------------------------------------------------------------------------------------------------------------
522// Note that some combinations are supported,
523// - PressedOnDragDropHold can generally be associated with any flag.
524// - PressedOnDoubleClick can be associated by PressedOnClickRelease/PressedOnRelease, in which case the second release event won't be reported.
525//------------------------------------------------------------------------------------------------------------------------------------------------
526// The behavior of the return-value changes when ImGuiItemFlags_ButtonRepeat is set:
527// Repeat+ Repeat+ Repeat+ Repeat+
528// PressedOnClickRelease PressedOnClick PressedOnRelease PressedOnDoubleClick
529//-------------------------------------------------------------------------------------------------------------------------------------------------
530// Frame N+0 (mouse button is down) - true - true
531// ... - - - -
532// Frame N + RepeatDelay true true - true
533// ... - - - -
534// Frame N + RepeatDelay + RepeatRate*N true true - true
535//-------------------------------------------------------------------------------------------------------------------------------------------------
536
537// - FIXME: For refactor we could output flags, incl mouse hovered vs nav keyboard vs nav triggered etc.
538// And better standardize how widgets use 'GetColor32((held && hovered) ? ... : hovered ? ...)' vs 'GetColor32(held ? ... : hovered ? ...);'
539// For mouse feedback we typically prefer the 'held && hovered' test, but for nav feedback not always. Outputting hovered=true on Activation may be misleading.
540// - Since v1.91.2 (Sept 2024) we included io.ConfigDebugHighlightIdConflicts feature.
541// One idiom which was previously valid which will now emit a warning is when using multiple overlaid ButtonBehavior()
542// with same ID and different MouseButton (see #8030). You can fix it by:
543// (1) switching to use a single ButtonBehavior() with multiple _MouseButton flags.
544// or (2) surrounding those calls with PushItemFlag(ImGuiItemFlags_AllowDuplicateId, true); ... PopItemFlag()
545bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags)
546{
547 ImGuiContext& g = *GImGui;
548 ImGuiWindow* window = GetCurrentWindow();
549
550 // Default behavior inherited from item flags
551 // Note that _both_ ButtonFlags and ItemFlags are valid sources, so copy one into the item_flags and only check that.
552 ImGuiItemFlags item_flags = (g.LastItemData.ID == id ? g.LastItemData.ItemFlags : g.CurrentItemFlags);
553 if (flags & ImGuiButtonFlags_AllowOverlap)
554 item_flags |= ImGuiItemFlags_AllowOverlap;
555 if (item_flags & ImGuiItemFlags_NoFocus)
556 flags |= ImGuiButtonFlags_NoFocus | ImGuiButtonFlags_NoNavFocus;
557
558 // Default only reacts to left mouse button
559 if ((flags & ImGuiButtonFlags_MouseButtonMask_) == 0)
560 flags |= ImGuiButtonFlags_MouseButtonLeft;
561
562 // Default behavior requires click + release inside bounding box
563 if ((flags & ImGuiButtonFlags_PressedOnMask_) == 0)
564 flags |= (item_flags & ImGuiItemFlags_ButtonRepeat) ? ImGuiButtonFlags_PressedOnClick : ImGuiButtonFlags_PressedOnDefault_;
565
566 ImGuiWindow* backup_hovered_window = g.HoveredWindow;
567 const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredWindow && g.HoveredWindow->RootWindow == window->RootWindow;
568 if (flatten_hovered_children)
569 g.HoveredWindow = window;
570
571#ifdef IMGUI_ENABLE_TEST_ENGINE
572 // Alternate registration spot, for when caller didn't use ItemAdd()
573 if (g.LastItemData.ID != id)
574 IMGUI_TEST_ENGINE_ITEM_ADD(id, bb, NULL);
575#endif
576
577 bool pressed = false;
578 bool hovered = ItemHoverable(bb, id, item_flags);
579
580 // Special mode for Drag and Drop used by openables (tree nodes, tabs etc.)
581 // where holding the button pressed for a long time while drag a payload item triggers the button.
582 if (g.DragDropActive)
583 {
584 if ((flags & ImGuiButtonFlags_PressedOnDragDropHold) && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers) && IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem))
585 {
586 hovered = true;
587 SetHoveredID(id);
588 if (g.HoveredIdTimer - g.IO.DeltaTime <= DRAGDROP_HOLD_TO_OPEN_TIMER && g.HoveredIdTimer >= DRAGDROP_HOLD_TO_OPEN_TIMER)
589 {
590 pressed = true;
591 g.DragDropHoldJustPressedId = id;
592 FocusWindow(window);
593 }
594 }
595 if (g.DragDropAcceptIdPrev == id && (g.DragDropAcceptFlagsPrev & ImGuiDragDropFlags_AcceptDrawAsHovered))
596 hovered = true;
597 }
598
599 if (flatten_hovered_children)
600 g.HoveredWindow = backup_hovered_window;
601
602 // Mouse handling
603 const ImGuiID test_owner_id = (flags & ImGuiButtonFlags_NoTestKeyOwner) ? ImGuiKeyOwner_Any : id;
604 if (hovered)
605 {
606 IM_ASSERT(id != 0); // Lazily check inside rare path.
607
608 // Poll mouse buttons
609 // - 'mouse_button_clicked' is generally carried into ActiveIdMouseButton when setting ActiveId.
610 // - Technically we only need some values in one code path, but since this is gated by hovered test this is fine.
611 int mouse_button_clicked = -1;
612 int mouse_button_released = -1;
613 for (int button = 0; button < 3; button++)
614 if (flags & (ImGuiButtonFlags_MouseButtonLeft << button)) // Handle ImGuiButtonFlags_MouseButtonRight and ImGuiButtonFlags_MouseButtonMiddle here.
615 {
616 if (IsMouseClicked(button, ImGuiInputFlags_None, test_owner_id) && mouse_button_clicked == -1) { mouse_button_clicked = button; }
617 if (IsMouseReleased(button, test_owner_id) && mouse_button_released == -1) { mouse_button_released = button; }
618 }
619
620 // Process initial action
621 const bool mods_ok = !(flags & ImGuiButtonFlags_NoKeyModsAllowed) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt);
622 if (mods_ok)
623 {
624 if (mouse_button_clicked != -1 && g.ActiveId != id)
625 {
626 if (!(flags & ImGuiButtonFlags_NoSetKeyOwner))
627 SetKeyOwner(MouseButtonToKey(mouse_button_clicked), id);
628 if (flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClickReleaseAnywhere))
629 {
630 SetActiveID(id, window);
631 g.ActiveIdMouseButton = (ImS8)mouse_button_clicked;
632 if (!(flags & ImGuiButtonFlags_NoNavFocus))
633 {
634 SetFocusID(id, window);
635 FocusWindow(window);
636 }
637 else if (!(flags & ImGuiButtonFlags_NoFocus))
638 {
639 FocusWindow(window, ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child
640 }
641 }
642 if ((flags & ImGuiButtonFlags_PressedOnClick) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseClickedCount[mouse_button_clicked] == 2))
643 {
644 pressed = true;
645 if (flags & ImGuiButtonFlags_NoHoldingActiveId)
646 ClearActiveID();
647 else
648 SetActiveID(id, window); // Hold on ID
649 g.ActiveIdMouseButton = (ImS8)mouse_button_clicked;
650 if (!(flags & ImGuiButtonFlags_NoNavFocus))
651 {
652 SetFocusID(id, window);
653 FocusWindow(window);
654 }
655 else if (!(flags & ImGuiButtonFlags_NoFocus))
656 {
657 FocusWindow(window, ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child
658 }
659 }
660 if (flags & ImGuiButtonFlags_PressedOnRelease)
661 {
662 // FIXME: Traditionally ImGuiButtonFlags_PressedOnRelease never took ActiveId. Adding it in 2026-03-20 since ImGuiButtonFlags_NoHoldingActiveId can always be added.
663 // We don't yet perform an explicit ClearActiveID() to reduce scope of change, but this possibility could be investigated.
664 if (!(flags & ImGuiButtonFlags_NoHoldingActiveId))
665 SetActiveID(id, window); // Hold on ID
666 g.ActiveIdMouseButton = (ImS8)mouse_button_clicked;
667 }
668 }
669 if (flags & ImGuiButtonFlags_PressedOnRelease)
670 {
671 if (mouse_button_released != -1)
672 {
673 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
674 if (!has_repeated_at_least_once)
675 pressed = true;
676 if (!(flags & ImGuiButtonFlags_NoNavFocus))
677 SetFocusID(id, window); // FIXME: Lack of FocusWindow() call here is inconsistent with other paths. Research why.
678 ClearActiveID();
679 }
680 }
681
682 // 'Repeat' mode acts when held regardless of _PressedOn flags (see table above).
683 // Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings.
684 if (g.ActiveId == id && (item_flags & ImGuiItemFlags_ButtonRepeat))
685 if (g.IO.MouseDownDuration[g.ActiveIdMouseButton] > 0.0f && IsMouseClicked(g.ActiveIdMouseButton, ImGuiInputFlags_Repeat, test_owner_id))
686 pressed = true;
687 }
688
689 if (pressed && g.IO.ConfigNavCursorVisibleAuto)
690 g.NavCursorVisible = false;
691 }
692
693 // Keyboard/Gamepad navigation handling
694 // We report navigated and navigation-activated items as hovered but we don't set g.HoveredId to not interfere with mouse.
695 if ((item_flags & ImGuiItemFlags_Disabled) == 0)
696 {
697 if (g.NavId == id && g.NavCursorVisible && g.NavHighlightItemUnderNav)
698 if (!(flags & ImGuiButtonFlags_NoHoveredOnFocus))
699 hovered = true;
700 if (g.NavActivateDownId == id)
701 {
702 bool nav_activated_by_code = (g.NavActivateId == id);
703 bool nav_activated_by_inputs = (g.NavActivatePressedId == id);
704 if (!nav_activated_by_inputs && (item_flags & ImGuiItemFlags_ButtonRepeat))
705 {
706 // Avoid pressing multiple keys from triggering excessive amount of repeat events
707 const ImGuiKeyData* key1 = GetKeyData(ImGuiKey_Space);
708 const ImGuiKeyData* key2 = GetKeyData(ImGuiKey_Enter);
709 const ImGuiKeyData* key3 = GetKeyData(ImGuiKey_NavGamepadActivate);
710 const float t1 = ImMax(ImMax(key1->DownDuration, key2->DownDuration), key3->DownDuration);
711 nav_activated_by_inputs = CalcTypematicRepeatAmount(t1 - g.IO.DeltaTime, t1, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate) > 0;
712 }
713 if (nav_activated_by_code || nav_activated_by_inputs)
714 {
715 // Set active id so it can be queried by user via IsItemActive(), equivalent of holding the mouse button.
716 pressed = true;
717 SetActiveID(id, window);
718 g.ActiveIdSource = g.NavInputSource;
719 if (!(flags & ImGuiButtonFlags_NoNavFocus) && !(g.NavActivateFlags & ImGuiActivateFlags_FromShortcut))
720 SetFocusID(id, window);
721 if (g.NavActivateFlags & ImGuiActivateFlags_FromShortcut)
722 g.ActiveIdFromShortcut = true;
723 }
724 }
725 }
726
727 // Process while held
728 bool held = false;
729 if (g.ActiveId == id)
730 {
731 if (g.ActiveIdSource == ImGuiInputSource_Mouse)
732 {
733 if (g.ActiveIdIsJustActivated)
734 g.ActiveIdClickOffset = g.IO.MousePos - bb.Min;
735
736 const int mouse_button = g.ActiveIdMouseButton;
737 if (mouse_button == -1)
738 {
739 // Fallback for the rare situation were g.ActiveId was set programmatically or from another widget (e.g. #6304).
740 ClearActiveID();
741 }
742 else if (IsMouseDown(mouse_button, test_owner_id))
743 {
744 held = true;
745 }
746 else
747 {
748 bool release_in = hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease) != 0;
749 bool release_anywhere = (flags & ImGuiButtonFlags_PressedOnClickReleaseAnywhere) != 0;
750 if ((release_in || release_anywhere) && !g.DragDropActive)
751 {
752 // Report as pressed when releasing the mouse (this is the most common path)
753 bool is_double_click_release = (flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseReleased[mouse_button] && g.IO.MouseClickedLastCount[mouse_button] == 2;
754 bool is_repeating_already = (item_flags & ImGuiItemFlags_ButtonRepeat) && g.IO.MouseDownDurationPrev[mouse_button] >= g.IO.KeyRepeatDelay; // Repeat mode trumps <on release>
755 bool is_button_avail_or_owned = TestKeyOwner(MouseButtonToKey(mouse_button), test_owner_id);
756 if (!is_double_click_release && !is_repeating_already && is_button_avail_or_owned)
757 pressed = true;
758 }
759 ClearActiveID();
760 }
761 if (!(flags & ImGuiButtonFlags_NoNavFocus) && g.IO.ConfigNavCursorVisibleAuto)
762 g.NavCursorVisible = false;
763 }
764 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
765 {
766 // When activated using Nav, we hold on the ActiveID until activation button is released
767 if (g.NavActivateDownId == id)
768 held = true; // hovered == true not true as we are already likely hovered on direct activation.
769 else
770 ClearActiveID();
771 }
772 if (pressed)
773 g.ActiveIdHasBeenPressedBefore = true;
774 }
775
776 // Activation highlight (this may be a remote activation)
777 if (g.NavHighlightActivatedId == id && (item_flags & ImGuiItemFlags_Disabled) == 0)
778 hovered = true;
779
780 if (out_hovered) *out_hovered = hovered;
781 if (out_held) *out_held = held;
782
783 return pressed;
784}
785
786bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags)
787{
788 ImGuiWindow* window = GetCurrentWindow();
789 if (window->SkipItems)
790 return false;
791
792 ImGuiContext& g = *GImGui;
793 const ImGuiStyle& style = g.Style;
794 const ImGuiID id = window->GetID(label);
795 const char* label_end = FindRenderedTextEnd(label);
796 const ImVec2 label_size = CalcTextSize(label, label_end, false);
797
798 ImVec2 pos = window->DC.CursorPos;
799 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)
800 pos.y += window->DC.CurrLineTextBaseOffset - style.FramePadding.y;
801 ImVec2 size = CalcItemSize(size_arg, label_size.x + style.FramePadding.x * 2.0f, label_size.y + style.FramePadding.y * 2.0f);
802
803 const ImRect bb(pos, pos + size);
804 ItemSize(size, style.FramePadding.y);
805 if (!ItemAdd(bb, id))
806 return false;
807
808 bool hovered, held;
809 bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
810
811 // Render
812 const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
813 RenderNavCursor(bb, id);
814 RenderFrame(bb.Min, bb.Max, col, true, style.FrameRounding);
815
816 if (g.LogEnabled)
817 LogSetNextTextDecoration("[", "]");
818 RenderTextClipped(bb.Min + style.FramePadding, bb.Max - style.FramePadding, label, label_end, &label_size, style.ButtonTextAlign, &bb);
819
820 // Automatically close popups
821 //if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup))
822 // CloseCurrentPopup();
823
824 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
825 return pressed;
826}
827
828bool ImGui::Button(const char* label, const ImVec2& size_arg)
829{
830 return ButtonEx(label, size_arg, ImGuiButtonFlags_None);
831}
832
833// Small buttons fits within text without additional vertical spacing.
834bool ImGui::SmallButton(const char* label)
835{
836 ImGuiContext& g = *GImGui;
837 float backup_padding_y = g.Style.FramePadding.y;
838 g.Style.FramePadding.y = 0.0f;
839 bool pressed = ButtonEx(label, ImVec2(0, 0), ImGuiButtonFlags_AlignTextBaseLine);
840 g.Style.FramePadding.y = backup_padding_y;
841 return pressed;
842}
843
844// Tip: use ImGui::PushID()/PopID() to push indices or pointers in the ID stack.
845// 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)
846bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg, ImGuiButtonFlags flags)
847{
848 ImGuiContext& g = *GImGui;
849 ImGuiWindow* window = GetCurrentWindow();
850 if (window->SkipItems)
851 return false;
852
853 // Ensure zero-size fits to contents
854 ImVec2 size = CalcItemSize(ImVec2(size_arg.x != 0.0f ? size_arg.x : -FLT_MIN, size_arg.y != 0.0f ? size_arg.y : -FLT_MIN), 0.0f, 0.0f);
855
856 const ImGuiID id = window->GetID(str_id);
857 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
858 ItemSize(size);
859 if (!ItemAdd(bb, id, NULL, (flags & ImGuiButtonFlags_EnableNav) ? ImGuiItemFlags_None : ImGuiItemFlags_NoNav))
860 return false;
861
862 bool hovered, held;
863 bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
864 RenderNavCursor(bb, id);
865
866 IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
867 return pressed;
868}
869
870bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiButtonFlags flags)
871{
872 ImGuiContext& g = *GImGui;
873 ImGuiWindow* window = GetCurrentWindow();
874 if (window->SkipItems)
875 return false;
876
877 const ImGuiID id = window->GetID(str_id);
878 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
879 const float default_size = GetFrameHeight();
880 ItemSize(size, (size.y >= default_size) ? g.Style.FramePadding.y : -1.0f);
881 if (!ItemAdd(bb, id))
882 return false;
883
884 bool hovered, held;
885 bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
886
887 // Render
888 const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
889 const ImU32 text_col = GetColorU32(ImGuiCol_Text);
890 RenderNavCursor(bb, id);
891 RenderFrame(bb.Min, bb.Max, bg_col, true, g.Style.FrameRounding);
892 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);
893
894 IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
895 return pressed;
896}
897
898bool ImGui::ArrowButton(const char* str_id, ImGuiDir dir)
899{
900 float sz = GetFrameHeight();
901 return ArrowButtonEx(str_id, dir, ImVec2(sz, sz), ImGuiButtonFlags_None);
902}
903
904// Button to close a window
905bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos)
906{
907 ImGuiContext& g = *GImGui;
908 ImGuiWindow* window = g.CurrentWindow;
909
910 // 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)
911 // 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?
912 const ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize));
913 ImRect bb_interact = bb;
914 const float area_to_visible_ratio = window->OuterRectClipped.GetArea() / bb.GetArea();
915 if (area_to_visible_ratio < 1.5f)
916 bb_interact.Expand(ImTrunc(bb_interact.GetSize() * -0.25f));
917
918 // Tweak 2: We intentionally allow interaction when clipped so that a mechanical Alt,Right,Activate sequence can always close a window.
919 // (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).
920 bool is_clipped = !ItemAdd(bb_interact, id);
921
922 bool hovered, held;
923 bool pressed = ButtonBehavior(bb_interact, id, &hovered, &held);
924 if (is_clipped)
925 return pressed;
926
927 // Render
928 ImU32 bg_col = GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered);
929 if (hovered)
930 window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col);
931 RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact);
932 const ImU32 cross_col = GetColorU32(ImGuiCol_Text);
933 const ImVec2 cross_center = bb.GetCenter() - ImVec2(0.5f, 0.5f);
934 const float cross_extent = g.FontSize * 0.5f * 0.7071f - 1.0f;
935 const float cross_thickness = 1.0f * (float)(int)g.Style._MainScale; // FIXME-DPI
936 window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, +cross_extent), cross_center + ImVec2(-cross_extent, -cross_extent), cross_col, cross_thickness);
937 window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, -cross_extent), cross_center + ImVec2(-cross_extent, +cross_extent), cross_col, cross_thickness);
938
939 return pressed;
940}
941
942bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos)
943{
944 ImGuiContext& g = *GImGui;
945 ImGuiWindow* window = g.CurrentWindow;
946
947 ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize));
948 bool is_clipped = !ItemAdd(bb, id);
949 bool hovered, held;
950 bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None);
951 if (is_clipped)
952 return pressed;
953
954 // Render
955 ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
956 ImU32 text_col = GetColorU32(ImGuiCol_Text);
957 if (hovered || held)
958 window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col);
959 RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact);
960 RenderArrow(window->DrawList, bb.Min, text_col, window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, 1.0f);
961
962 // Switch to moving the window after mouse is moved beyond the initial drag threshold
963 if (IsItemActive() && IsMouseDragging(0))
964 StartMouseMovingWindow(window);
965
966 return pressed;
967}
968
969ImGuiID ImGui::GetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis)
970{
971 return window->GetID(axis == ImGuiAxis_X ? "#SCROLLX" : "#SCROLLY");
972}
973
974// Return scrollbar rectangle, must only be called for corresponding axis if window->ScrollbarX/Y is set.
975ImRect ImGui::GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis)
976{
977 ImGuiContext& g = *GImGui;
978 const ImRect outer_rect = window->Rect();
979 const ImRect inner_rect = window->InnerRect;
980 const float scrollbar_size = window->ScrollbarSizes[axis ^ 1]; // (ScrollbarSizes.x = width of Y scrollbar; ScrollbarSizes.y = height of X scrollbar)
981 IM_ASSERT(scrollbar_size >= 0.0f);
982 const float border_size = IM_ROUND(window->WindowBorderSize * 0.5f);
983 const float border_top = (window->Flags & ImGuiWindowFlags_MenuBar) ? IM_ROUND(g.Style.FrameBorderSize * 0.5f) : (window->Flags & ImGuiWindowFlags_NoTitleBar) ? border_size : 0;
984 if (axis == ImGuiAxis_X)
985 return ImRect(inner_rect.Min.x + border_size, ImMax(outer_rect.Min.y + border_size, outer_rect.Max.y - border_size - scrollbar_size), inner_rect.Max.x - border_size, outer_rect.Max.y - border_size);
986 else
987 return ImRect(ImMax(outer_rect.Min.x, outer_rect.Max.x - border_size - scrollbar_size), inner_rect.Min.y + border_top, outer_rect.Max.x - border_size, inner_rect.Max.y - border_size);
988}
989
990void ImGui::ExtendHitBoxWhenNearViewportEdge(ImGuiWindow* window, ImRect* bb, float threshold, ImGuiAxis axis)
991{
992 ImRect window_rect = window->RootWindow->Rect();
993 ImRect viewport_rect = window->Viewport->GetMainRect();
994 if (window_rect.Min[axis] == viewport_rect.Min[axis] && bb->Min[axis] > window_rect.Min[axis] && bb->Min[axis] - threshold <= window_rect.Min[axis])
995 bb->Min[axis] = window_rect.Min[axis];
996 if (window_rect.Max[axis] == viewport_rect.Max[axis] && bb->Max[axis] < window_rect.Max[axis] && bb->Max[axis] + threshold >= window_rect.Max[axis])
997 bb->Max[axis] = window_rect.Max[axis];
998}
999
1000void ImGui::Scrollbar(ImGuiAxis axis)
1001{
1002 ImGuiContext& g = *GImGui;
1003 ImGuiWindow* window = g.CurrentWindow;
1004 const ImGuiID id = GetWindowScrollbarID(window, axis);
1005
1006 // Calculate scrollbar bounding box
1007 ImRect bb = GetWindowScrollbarRect(window, axis);
1008 ImDrawFlags rounding_corners = CalcRoundingFlagsForRectInRect(bb, window->Rect(), g.Style.WindowBorderSize);
1009 float size_visible = window->InnerRect.Max[axis] - window->InnerRect.Min[axis];
1010 float size_contents = window->ContentSize[axis] + window->WindowPadding[axis] * 2.0f;
1011 ImS64 scroll = (ImS64)window->Scroll[axis];
1012 ScrollbarEx(bb, id, axis, &scroll, (ImS64)size_visible, (ImS64)size_contents, rounding_corners);
1013 window->Scroll[axis] = (float)scroll;
1014}
1015
1016// Vertical/Horizontal scrollbar
1017// The entire piece of code below is rather confusing because:
1018// - We handle absolute seeking (when first clicking outside the grab) and relative manipulation (afterward or when clicking inside the grab)
1019// - We store values as normalized ratio and in a form that allows the window content to change while we are holding on a scrollbar
1020// - We handle both horizontal and vertical scrollbars, which makes the terminology not ideal.
1021// Still, the code should probably be made simpler..
1022bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, ImS64* p_scroll_v, ImS64 size_visible_v, ImS64 size_contents_v, ImDrawFlags draw_rounding_flags)
1023{
1024 ImGuiContext& g = *GImGui;
1025 ImGuiWindow* window = g.CurrentWindow;
1026 if (window->SkipItems)
1027 return false;
1028
1029 const float bb_frame_width = bb_frame.GetWidth();
1030 const float bb_frame_height = bb_frame.GetHeight();
1031 if (bb_frame_width <= 0.0f || bb_frame_height <= 0.0f)
1032 return false;
1033
1034 // 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)
1035 float alpha = 1.0f;
1036 if ((axis == ImGuiAxis_Y) && bb_frame_height < bb_frame_width)
1037 alpha = ImSaturate(bb_frame_height / ImMax(bb_frame_width * 2.0f, 1.0f));
1038 if (alpha <= 0.0f)
1039 return false;
1040
1041 const ImGuiStyle& style = g.Style;
1042 const bool allow_interaction = (alpha >= 1.0f);
1043
1044 ImRect bb = bb_frame;
1045 float padding = IM_TRUNC(ImMin(style.ScrollbarPadding, ImMin(bb_frame_width, bb_frame_height) * 0.5f));
1046 bb.Expand(-padding);
1047
1048 // V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar)
1049 const float scrollbar_size_v = (axis == ImGuiAxis_X) ? bb.GetWidth() : bb.GetHeight();
1050 if (scrollbar_size_v < 1.0f)
1051 return false;
1052
1053 // Calculate the height of our grabbable box. It generally represent the amount visible (vs the total scrollable amount)
1054 // But we maintain a minimum size in pixel to allow for the user to still aim inside.
1055 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.
1056 const ImS64 win_size_v = ImMax(ImMax(size_contents_v, size_visible_v), (ImS64)1);
1057 const float grab_h_minsize = ImMin(bb.GetSize()[axis], style.GrabMinSize);
1058 const float grab_h_pixels = (float)(int)ImClamp(scrollbar_size_v * ((float)size_visible_v / (float)win_size_v), grab_h_minsize, scrollbar_size_v);
1059 const float grab_h_norm = grab_h_pixels / scrollbar_size_v;
1060
1061 // As a special thing, we allow scrollbar near the edge of a screen/viewport to be reachable with mouse at the extreme edge (#9276)
1062 ImRect bb_hit = bb_frame;
1063 ExtendHitBoxWhenNearViewportEdge(window, &bb_hit, g.Style.WindowBorderSize, (ImGuiAxis)(axis ^ 1));
1064
1065 // Handle input right away. None of the code of Begin() is relying on scrolling position before calling Scrollbar().
1066 bool held = false;
1067 bool hovered = false;
1068 ItemAdd(bb_frame, id, NULL, ImGuiItemFlags_NoNav);
1069 ButtonBehavior(bb_hit, id, &hovered, &held, ImGuiButtonFlags_NoNavFocus);
1070
1071 const ImS64 scroll_max = ImMax((ImS64)1, size_contents_v - size_visible_v);
1072 float scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max);
1073 float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; // Grab position in normalized space
1074 if (held && allow_interaction && grab_h_norm < 1.0f)
1075 {
1076 const float scrollbar_pos_v = bb.Min[axis];
1077 const float mouse_pos_v = g.IO.MousePos[axis];
1078
1079 // Click position in scrollbar normalized space (0.0f->1.0f)
1080 const float clicked_v_norm = ImSaturate((mouse_pos_v - scrollbar_pos_v) / scrollbar_size_v);
1081
1082 const int held_dir = (clicked_v_norm < grab_v_norm) ? -1 : (clicked_v_norm > grab_v_norm + grab_h_norm) ? +1 : 0;
1083 if (g.ActiveIdIsJustActivated)
1084 {
1085 // On initial click when held_dir == 0 (clicked over grab): calculate the distance between mouse and the center of the grab
1086 const bool scroll_to_clicked_location = (g.IO.ConfigScrollbarScrollByPage == false || g.IO.KeyShift || held_dir == 0);
1087 g.ScrollbarSeekMode = scroll_to_clicked_location ? 0 : (short)held_dir;
1088 g.ScrollbarClickDeltaToGrabCenter = (held_dir == 0 && !g.IO.KeyShift) ? clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f : 0.0f;
1089 }
1090
1091 // Apply scroll (p_scroll_v will generally point on one member of window->Scroll)
1092 // 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
1093 if (g.ScrollbarSeekMode == 0)
1094 {
1095 // Absolute seeking
1096 const float scroll_v_norm = ImSaturate((clicked_v_norm - g.ScrollbarClickDeltaToGrabCenter - grab_h_norm * 0.5f) / (1.0f - grab_h_norm));
1097 *p_scroll_v = (ImS64)(scroll_v_norm * scroll_max);
1098 }
1099 else
1100 {
1101 // Page by page
1102 if (IsMouseClicked(ImGuiMouseButton_Left, ImGuiInputFlags_Repeat) && held_dir == g.ScrollbarSeekMode)
1103 {
1104 float page_dir = (g.ScrollbarSeekMode > 0.0f) ? +1.0f : -1.0f;
1105 *p_scroll_v = ImClamp(*p_scroll_v + (ImS64)(page_dir * size_visible_v), (ImS64)0, scroll_max);
1106 }
1107 }
1108
1109 // Update values for rendering
1110 scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max);
1111 grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v;
1112
1113 // Update distance to grab now that we have seek'ed and saturated
1114 //if (seek_absolute)
1115 // g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f;
1116 }
1117
1118 // Render
1119 const ImU32 bg_col = GetColorU32(ImGuiCol_ScrollbarBg);
1120 const ImU32 grab_col = GetColorU32(held ? ImGuiCol_ScrollbarGrabActive : hovered ? ImGuiCol_ScrollbarGrabHovered : ImGuiCol_ScrollbarGrab, alpha);
1121 window->DrawList->AddRectFilled(bb_frame.Min, bb_frame.Max, bg_col, window->WindowRounding, draw_rounding_flags);
1122 ImRect grab_rect;
1123 if (axis == ImGuiAxis_X)
1124 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);
1125 else
1126 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);
1127 window->DrawList->AddRectFilled(grab_rect.Min, grab_rect.Max, grab_col, style.ScrollbarRounding);
1128
1129 return held;
1130}
1131
1132// - Read about ImTextureID/ImTextureRef here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples
1133// - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above.
1134void ImGui::ImageWithBg(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col)
1135{
1136 ImGuiContext& g = *GImGui;
1137 ImGuiWindow* window = GetCurrentWindow();
1138 if (window->SkipItems)
1139 return;
1140
1141 const ImVec2 padding(g.Style.ImageBorderSize, g.Style.ImageBorderSize);
1142 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f);
1143 ItemSize(bb);
1144 if (!ItemAdd(bb, 0))
1145 return;
1146
1147 // Render
1148 float rounding = g.Style.ImageRounding;
1149 if (bg_col.w > 0.0f)
1150 window->DrawList->AddRectFilled(bb.Min + padding, bb.Max - padding, GetColorU32(bg_col), rounding);
1151 if (rounding > 0.0f)
1152 window->DrawList->AddImageRounded(tex_ref, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col), rounding);
1153 else
1154 window->DrawList->AddImage(tex_ref, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col));
1155 if (g.Style.ImageBorderSize > 0.0f)
1156 window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border), rounding, g.Style.ImageBorderSize);
1157}
1158
1159void ImGui::Image(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1)
1160{
1161 ImageWithBg(tex_ref, image_size, uv0, uv1);
1162}
1163
1164// 1.91.9 (February 2025) removed 'tint_col' and 'border_col' parameters, made border size not depend on color value. (#8131, #8238)
1165#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1166void ImGui::Image(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col)
1167{
1168 ImGuiContext& g = *GImGui;
1169 PushStyleVar(ImGuiStyleVar_ImageBorderSize, (border_col.w > 0.0f) ? ImMax(1.0f, g.Style.ImageBorderSize) : 0.0f); // Preserve legacy behavior where border is always visible when border_col's Alpha is >0.0f
1170 PushStyleColor(ImGuiCol_Border, border_col);
1171 ImageWithBg(tex_ref, image_size, uv0, uv1, ImVec4(0, 0, 0, 0), tint_col);
1172 PopStyleColor();
1173 PopStyleVar();
1174}
1175#endif
1176
1177bool ImGui::ImageButtonEx(ImGuiID id, ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col, ImGuiButtonFlags flags)
1178{
1179 ImGuiContext& g = *GImGui;
1180 ImGuiWindow* window = GetCurrentWindow();
1181 if (window->SkipItems)
1182 return false;
1183
1184 const ImVec2 padding = g.Style.FramePadding;
1185 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f);
1186 ItemSize(bb);
1187 if (!ItemAdd(bb, id))
1188 return false;
1189
1190 bool hovered, held;
1191 bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
1192
1193 // Render
1194 const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1195 RenderNavCursor(bb, id);
1196 RenderFrame(bb.Min, bb.Max, col, true, g.Style.FrameRounding);
1197 if (bg_col.w > 0.0f)
1198 window->DrawList->AddRectFilled(bb.Min + padding, bb.Max - padding, GetColorU32(bg_col));
1199 float image_rounding = ImMax(g.Style.FrameRounding - ImMax(padding.x, padding.y), g.Style.ImageRounding);
1200 if (image_rounding > 0.0f)
1201 window->DrawList->AddImageRounded(tex_ref, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col), image_rounding);
1202 else
1203 window->DrawList->AddImage(tex_ref, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col));
1204
1205 return pressed;
1206}
1207
1208// - ImageButton() adds style.FramePadding*2.0f to provided size. This is in order to facilitate fitting an image in a button.
1209// - ImageButton() draws a background based on regular Button() color + optionally an inner background if specified. (#8165) // FIXME: Maybe that's not the best design?
1210bool ImGui::ImageButton(const char* str_id, ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col)
1211{
1212 ImGuiContext& g = *GImGui;
1213 ImGuiWindow* window = g.CurrentWindow;
1214 if (window->SkipItems)
1215 return false;
1216
1217 return ImageButtonEx(window->GetID(str_id), tex_ref, image_size, uv0, uv1, bg_col, tint_col);
1218}
1219
1220#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1221// Legacy API obsoleted in 1.89. Two differences with new ImageButton()
1222// - old ImageButton() used ImTextureID as item id (created issue with multiple buttons with same image, transient texture id values, opaque computation of ID)
1223// - new ImageButton() requires an explicit 'const char* str_id'
1224// - old ImageButton() had frame_padding' override argument.
1225// - new ImageButton() always use style.FramePadding.
1226/*
1227bool 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)
1228{
1229 // Default to using texture ID as ID. User can still push string/integer prefixes.
1230 PushID((ImTextureID)(intptr_t)user_texture_id);
1231 if (frame_padding >= 0)
1232 PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2((float)frame_padding, (float)frame_padding));
1233 bool ret = ImageButton("", user_texture_id, size, uv0, uv1, bg_col, tint_col);
1234 if (frame_padding >= 0)
1235 PopStyleVar();
1236 PopID();
1237 return ret;
1238}
1239*/
1240#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1241
1242bool ImGui::Checkbox(const char* label, bool* v)
1243{
1244 ImGuiWindow* window = GetCurrentWindow();
1245 if (window->SkipItems)
1246 return false;
1247
1248 ImGuiContext& g = *GImGui;
1249 const ImGuiStyle& style = g.Style;
1250 const ImGuiID id = window->GetID(label);
1251 const char* label_end = FindRenderedTextEnd(label);
1252 const ImVec2 label_size = CalcTextSize(label, label_end, false);
1253
1254 const float square_sz = GetFrameHeight();
1255 const ImVec2 pos = window->DC.CursorPos;
1256 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));
1257 ItemSize(total_bb, style.FramePadding.y);
1258 const bool is_visible = ItemAdd(total_bb, id);
1259 const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
1260 if (!is_visible)
1261 if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(total_bb)) // Extra layer of "no logic clip" for box-select support
1262 {
1263 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1264 return false;
1265 }
1266
1267 // Range-Selection/Multi-selection support (header)
1268 bool checked = *v;
1269 if (is_multi_select)
1270 MultiSelectItemHeader(id, &checked, NULL);
1271
1272 bool hovered, held;
1273 bool pressed = ButtonBehavior(total_bb, id, &hovered, &held);
1274
1275 // Range-Selection/Multi-selection support (footer)
1276 if (is_multi_select)
1277 MultiSelectItemFooter(id, &checked, &pressed, ImGuiMultiSelectFlags_CheckboxMode_);
1278 else if (pressed)
1279 checked = !checked;
1280
1281 if (*v != checked)
1282 {
1283 *v = checked;
1284 pressed = true; // return value
1285 MarkItemEdited(id);
1286 }
1287
1288 const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1289 const bool mixed_value = (g.LastItemData.ItemFlags & ImGuiItemFlags_MixedValue) != 0;
1290 if (is_visible)
1291 {
1292 RenderNavCursor(total_bb, id);
1293 ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : (mixed_value || checked) ? ImGuiCol_CheckboxSelectedBg : ImGuiCol_FrameBg);
1294 ImU32 check_col = GetColorU32(ImGuiCol_CheckMark);
1295 RenderFrame(check_bb.Min, check_bb.Max, bg_col, true, style.FrameRounding);
1296 if (mixed_value)
1297 {
1298 // Undocumented tristate/mixed/indeterminate checkbox (#2644)
1299 // This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all widgets (not just checkbox)
1300 ImVec2 pad(ImMax(1.0f, IM_TRUNC(square_sz / 3.6f)), ImMax(1.0f, IM_TRUNC(square_sz / 3.6f)));
1301 window->DrawList->AddRectFilled(check_bb.Min + pad, check_bb.Max - pad, check_col, style.FrameRounding);
1302 }
1303 else if (*v)
1304 {
1305 const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f));
1306 RenderCheckMark(window->DrawList, check_bb.Min + ImVec2(pad, pad), check_col, square_sz - pad * 2.0f);
1307 }
1308 }
1309 const ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1310 if (g.LogEnabled)
1311 LogRenderedText(&label_pos, mixed_value ? "[~]" : *v ? "[x]" : "[ ]");
1312 if (is_visible && label_size.x > 0.0f)
1313 RenderText(label_pos, label, label_end, false);
1314
1315 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1316 return pressed;
1317}
1318
1319template<typename T>
1320bool ImGui::CheckboxFlagsT(const char* label, T* flags, T flags_value)
1321{
1322 bool all_on = (*flags & flags_value) == flags_value;
1323 bool any_on = (*flags & flags_value) != 0;
1324 bool pressed;
1325 if (!all_on && any_on)
1326 {
1327 ImGuiContext& g = *GImGui;
1328 g.NextItemData.ItemFlagsSet |= ImGuiItemFlags_MixedValue;
1329 pressed = Checkbox(label, &all_on);
1330 }
1331 else
1332 {
1333 pressed = Checkbox(label, &all_on);
1334
1335 }
1336 if (pressed)
1337 {
1338 if (all_on)
1339 *flags |= flags_value;
1340 else
1341 *flags &= ~flags_value;
1342 }
1343 return pressed;
1344}
1345
1346bool ImGui::CheckboxFlags(const char* label, int* flags, int flags_value)
1347{
1348 return CheckboxFlagsT(label, flags, flags_value);
1349}
1350
1351bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value)
1352{
1353 return CheckboxFlagsT(label, flags, flags_value);
1354}
1355
1356bool ImGui::CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value)
1357{
1358 return CheckboxFlagsT(label, flags, flags_value);
1359}
1360
1361bool ImGui::CheckboxFlags(const char* label, ImU64* flags, ImU64 flags_value)
1362{
1363 return CheckboxFlagsT(label, flags, flags_value);
1364}
1365
1366bool ImGui::RadioButton(const char* label, bool active)
1367{
1368 ImGuiWindow* window = GetCurrentWindow();
1369 if (window->SkipItems)
1370 return false;
1371
1372 ImGuiContext& g = *GImGui;
1373 const ImGuiStyle& style = g.Style;
1374 const ImGuiID id = window->GetID(label);
1375 const char* label_end = FindRenderedTextEnd(label);
1376 const ImVec2 label_size = CalcTextSize(label, label_end, false);
1377
1378 const float square_sz = GetFrameHeight();
1379 const ImVec2 pos = window->DC.CursorPos;
1380 const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1381 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));
1382 ItemSize(total_bb, style.FramePadding.y);
1383 if (!ItemAdd(total_bb, id))
1384 return false;
1385
1386 ImVec2 center = check_bb.GetCenter();
1387 center.x = IM_ROUND(center.x);
1388 center.y = IM_ROUND(center.y);
1389 const float radius = (square_sz - 1.0f) * 0.5f;
1390
1391 bool hovered, held;
1392 bool pressed = ButtonBehavior(total_bb, id, &hovered, &held);
1393 if (pressed)
1394 MarkItemEdited(id);
1395
1396 RenderNavCursor(total_bb, id);
1397 const int num_segment = window->DrawList->_CalcCircleAutoSegmentCount(radius);
1398 window->DrawList->AddCircleFilled(center, radius, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), num_segment);
1399 if (active)
1400 {
1401 const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f));
1402 window->DrawList->AddCircleFilled(center, radius - pad, GetColorU32(ImGuiCol_CheckMark));
1403 }
1404
1405 if (style.FrameBorderSize > 0.0f)
1406 {
1407 window->DrawList->AddCircle(center + ImVec2(1, 1), radius, GetColorU32(ImGuiCol_BorderShadow), num_segment, style.FrameBorderSize);
1408 window->DrawList->AddCircle(center, radius, GetColorU32(ImGuiCol_Border), num_segment, style.FrameBorderSize);
1409 }
1410
1411 ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1412 if (g.LogEnabled)
1413 LogRenderedText(&label_pos, active ? "(x)" : "( )");
1414 if (label_size.x > 0.0f)
1415 RenderText(label_pos, label, label_end, false);
1416
1417 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
1418 return pressed;
1419}
1420
1421// 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..
1422bool ImGui::RadioButton(const char* label, int* v, int v_button)
1423{
1424 const bool pressed = RadioButton(label, *v == v_button);
1425 if (pressed)
1426 *v = v_button;
1427 return pressed;
1428}
1429
1430// size_arg (for each axis) < 0.0f: align to end, 0.0f: auto, > 0.0f: specified size
1431void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* overlay)
1432{
1433 ImGuiWindow* window = GetCurrentWindow();
1434 if (window->SkipItems)
1435 return;
1436
1437 ImGuiContext& g = *GImGui;
1438 const ImGuiStyle& style = g.Style;
1439
1440 ImVec2 pos = window->DC.CursorPos;
1441 ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), g.FontSize + style.FramePadding.y * 2.0f);
1442 ImRect bb(pos, pos + size);
1443 ItemSize(size, style.FramePadding.y);
1444 if (!ItemAdd(bb, 0))
1445 return;
1446
1447 // Fraction < 0.0f will display an indeterminate progress bar animation
1448 // The value must be animated along with time, so e.g. passing '-1.0f * ImGui::GetTime()' as fraction works.
1449 const bool is_indeterminate = (fraction < 0.0f);
1450 if (!is_indeterminate)
1451 fraction = ImSaturate(fraction);
1452
1453 // Out of courtesy we accept a NaN fraction without crashing
1454 float fill_n0 = 0.0f;
1455 float fill_n1 = (fraction == fraction) ? fraction : 0.0f;
1456
1457 if (is_indeterminate)
1458 {
1459 const float fill_width_n = 0.2f;
1460 fill_n0 = ImFmod(-fraction, 1.0f) * (1.0f + fill_width_n) - fill_width_n;
1461 fill_n1 = ImSaturate(fill_n0 + fill_width_n);
1462 fill_n0 = ImSaturate(fill_n0);
1463 }
1464
1465 // Render
1466 RenderFrame(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
1467 bb.Expand(ImVec2(-style.FrameBorderSize, -style.FrameBorderSize));
1468 float fill_x0 = ImLerp(bb.Min.x, bb.Max.x, fill_n0);
1469 float fill_x1 = ImLerp(bb.Min.x, bb.Max.x, fill_n1);
1470 if (fill_x0 < fill_x1)
1471 RenderRectFilledInRangeH(window->DrawList, bb, GetColorU32(ImGuiCol_PlotHistogram), fill_x0, fill_x1, style.FrameRounding);
1472
1473 // Default displaying the fraction as percentage string, but user can override it
1474 // Don't display text for indeterminate bars by default
1475 char overlay_buf[32];
1476 if (!is_indeterminate || overlay != NULL)
1477 {
1478 if (!overlay)
1479 {
1480 ImFormatString(overlay_buf, IM_COUNTOF(overlay_buf), "%.0f%%", fraction * 100 + 0.01f);
1481 overlay = overlay_buf;
1482 }
1483
1484 ImVec2 overlay_size = CalcTextSize(overlay, NULL);
1485 if (overlay_size.x > 0.0f)
1486 {
1487 float text_x = is_indeterminate ? (bb.Min.x + bb.Max.x - overlay_size.x) * 0.5f : fill_x1 + style.ItemSpacing.x;
1488 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);
1489 }
1490 }
1491}
1492
1493void ImGui::Bullet()
1494{
1495 ImGuiWindow* window = GetCurrentWindow();
1496 if (window->SkipItems)
1497 return;
1498
1499 ImGuiContext& g = *GImGui;
1500 const ImGuiStyle& style = g.Style;
1501 const float line_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y * 2), g.FontSize);
1502 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize, line_height));
1503 ItemSize(bb);
1504 if (!ItemAdd(bb, 0))
1505 {
1506 SameLine(0, style.FramePadding.x * 2);
1507 return;
1508 }
1509
1510 // Render and stay on same line
1511 ImU32 text_col = GetColorU32(ImGuiCol_Text);
1512 RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, line_height * 0.5f), text_col);
1513 SameLine(0, style.FramePadding.x * 2.0f);
1514}
1515
1516// This is provided as a convenience for being an often requested feature.
1517// FIXME-STYLE: we delayed adding as there is a larger plan to revamp the styling system.
1518// Because of this we currently don't provide many styling options for this widget
1519// (e.g. hovered/active colors are automatically inferred from a single color).
1520bool ImGui::TextLink(const char* label)
1521{
1522 ImGuiWindow* window = GetCurrentWindow();
1523 if (window->SkipItems)
1524 return false;
1525
1526 ImGuiContext& g = *GImGui;
1527 const ImGuiID id = window->GetID(label);
1528 const char* label_end = FindRenderedTextEnd(label);
1529
1530 ImVec2 pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
1531 ImVec2 size = CalcTextSize(label, label_end, false);
1532 ImRect bb(pos, pos + size);
1533 ItemSize(size, 0.0f);
1534 if (!ItemAdd(bb, id))
1535 return false;
1536
1537 bool hovered, held;
1538 bool pressed = ButtonBehavior(bb, id, &hovered, &held);
1539 RenderNavCursor(bb, id);
1540
1541 if (hovered)
1542 SetMouseCursor(ImGuiMouseCursor_Hand);
1543
1544 ImVec4 text_colf = g.Style.Colors[ImGuiCol_TextLink];
1545 ImVec4 line_colf = text_colf;
1546 {
1547 // FIXME-STYLE: Read comments above. This widget is NOT written in the same style as some earlier widgets,
1548 // as we are currently experimenting/planning a different styling system.
1549 float h, s, v;
1550 ColorConvertRGBtoHSV(text_colf.x, text_colf.y, text_colf.z, h, s, v);
1551 if (held || hovered)
1552 {
1553 v = ImSaturate(v + (held ? 0.4f : 0.3f));
1554 h = ImFmod(h + 0.02f, 1.0f);
1555 }
1556 ColorConvertHSVtoRGB(h, s, v, text_colf.x, text_colf.y, text_colf.z);
1557 v = ImSaturate(v - 0.20f);
1558 ColorConvertHSVtoRGB(h, s, v, line_colf.x, line_colf.y, line_colf.z);
1559 }
1560
1561 float line_y = bb.Max.y + ImFloor(g.FontBaked->Descent * g.FontBakedScale * 0.20f);
1562 window->DrawList->AddLineH(bb.Min.x, bb.Max.x, line_y, GetColorU32(line_colf), 1.0f * (float)(int)g.Style._MainScale); // FIXME-TEXT: Underline mode // FIXME-DPI
1563
1564 PushStyleColor(ImGuiCol_Text, GetColorU32(text_colf));
1565 RenderText(bb.Min, label, label_end, false);
1566 PopStyleColor();
1567
1568 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
1569 return pressed;
1570}
1571
1572bool ImGui::TextLinkOpenURL(const char* label, const char* url)
1573{
1574 ImGuiContext& g = *GImGui;
1575 if (url == NULL)
1576 url = label;
1577 bool pressed = TextLink(label);
1578 if (pressed && g.PlatformIO.Platform_OpenInShellFn != NULL)
1579 g.PlatformIO.Platform_OpenInShellFn(&g, url);
1580 SetItemTooltip(LocalizeGetMsg(ImGuiLocKey_OpenLink_s), url); // It is more reassuring for user to _always_ display URL when it is the same as the label.
1581 if (BeginPopupContextItem())
1582 {
1583 if (MenuItem(LocalizeGetMsg(ImGuiLocKey_CopyLink)))
1584 SetClipboardText(url);
1585 EndPopup();
1586 }
1587 return pressed;
1588}
1589
1590//-------------------------------------------------------------------------
1591// [SECTION] Widgets: Low-level Layout helpers
1592//-------------------------------------------------------------------------
1593// - Spacing()
1594// - Dummy()
1595// - NewLine()
1596// - AlignTextToFramePadding()
1597// - SeparatorEx() [Internal]
1598// - Separator()
1599// - SplitterBehavior() [Internal]
1600// - ShrinkWidths() [Internal]
1601//-------------------------------------------------------------------------
1602
1603void ImGui::Spacing()
1604{
1605 ImGuiWindow* window = GetCurrentWindow();
1606 if (window->SkipItems)
1607 return;
1608 ItemSize(ImVec2(0, 0));
1609}
1610
1611void ImGui::Dummy(const ImVec2& size)
1612{
1613 ImGuiWindow* window = GetCurrentWindow();
1614 if (window->SkipItems)
1615 return;
1616
1617 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
1618 ItemSize(size);
1619 ItemAdd(bb, 0);
1620}
1621
1622void ImGui::NewLine()
1623{
1624 ImGuiWindow* window = GetCurrentWindow();
1625 if (window->SkipItems)
1626 return;
1627
1628 ImGuiContext& g = *GImGui;
1629 const ImGuiLayoutType backup_layout_type = window->DC.LayoutType;
1630 window->DC.LayoutType = ImGuiLayoutType_Vertical;
1631 window->DC.IsSameLine = false;
1632 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.
1633 ItemSize(ImVec2(0, 0));
1634 else
1635 ItemSize(ImVec2(0.0f, g.FontSize));
1636 window->DC.LayoutType = backup_layout_type;
1637}
1638
1639void ImGui::AlignTextToFramePadding()
1640{
1641 ImGuiWindow* window = GetCurrentWindow();
1642 if (window->SkipItems)
1643 return;
1644
1645 ImGuiContext& g = *GImGui;
1646 window->DC.CurrLineSize.y = ImMax(window->DC.CurrLineSize.y, g.FontSize + g.Style.FramePadding.y * 2);
1647 window->DC.CurrLineTextBaseOffset = ImMax(window->DC.CurrLineTextBaseOffset, g.Style.FramePadding.y);
1648}
1649
1650// Horizontal/vertical separating line
1651// FIXME: Surprisingly, this seemingly trivial widget is a victim of many different legacy/tricky layout issues.
1652// Note how thickness == 1.0f is handled specifically as not moving CursorPos by 'thickness', but other values are.
1653void ImGui::SeparatorEx(ImGuiSeparatorFlags flags, float thickness)
1654{
1655 ImGuiWindow* window = GetCurrentWindow();
1656 if (window->SkipItems)
1657 return;
1658
1659 ImGuiContext& g = *GImGui;
1660 IM_ASSERT(ImIsPowerOfTwo(flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical))); // Check that only 1 option is selected
1661 IM_ASSERT(thickness > 0.0f);
1662
1663 if (flags & ImGuiSeparatorFlags_Vertical)
1664 {
1665 // Vertical separator, for menu bars (use current line height).
1666 float y1 = window->DC.CursorPos.y;
1667 float y2 = window->DC.CursorPos.y + window->DC.CurrLineSize.y;
1668 const ImRect bb(ImVec2(window->DC.CursorPos.x, y1), ImVec2(window->DC.CursorPos.x + thickness, y2));
1669 ItemSize(ImVec2(thickness, 0.0f));
1670 if (!ItemAdd(bb, 0))
1671 return;
1672
1673 // Draw
1674 window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Separator));
1675 if (g.LogEnabled)
1676 LogText(" |");
1677 }
1678 else if (flags & ImGuiSeparatorFlags_Horizontal)
1679 {
1680 // Horizontal Separator
1681 float x1 = window->DC.CursorPos.x;
1682 float x2 = window->WorkRect.Max.x;
1683
1684 // Preserve legacy behavior inside Columns()
1685 // Before Tables API happened, we relied on Separator() to span all columns of a Columns() set.
1686 // We currently don't need to provide the same feature for tables because tables naturally have border features.
1687 ImGuiOldColumns* columns = (flags & ImGuiSeparatorFlags_SpanAllColumns) ? window->DC.CurrentColumns : NULL;
1688 if (columns)
1689 {
1690 x1 = window->Pos.x + window->DC.Indent.x; // Used to be Pos.x before 2023/10/03
1691 x2 = window->Pos.x + window->Size.x;
1692 PushColumnsBackground();
1693 }
1694
1695 // We don't provide our width to the layout so that it doesn't get feed back into AutoFit
1696 // FIXME: This prevents ->CursorMaxPos based bounding box evaluation from working (e.g. TableEndCell)
1697
1698 // Between 1.71 and 1.92.7, we maintained a hack where a 1.0f thin Separator() would not impact layout.
1699 // This was mostly chosen to allow backward compatibility with user's code assuming zero-height when calculating height for layout (e.g. bottom alignment of a status bar).
1700 // In order to handle scaling we need to scale separator thickness and it would not makes sense to have a disparity depending on height.
1701 ////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.
1702 const ImRect bb(ImVec2(x1, window->DC.CursorPos.y), ImVec2(x2, window->DC.CursorPos.y + thickness));
1703 ItemSize(ImVec2(0.0f, thickness));
1704
1705 if (ItemAdd(bb, 0))
1706 {
1707 // Draw
1708 window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Separator));
1709 if (g.LogEnabled)
1710 LogRenderedText(&bb.Min, "--------------------------------\n");
1711
1712 }
1713 if (columns)
1714 {
1715 PopColumnsBackground();
1716 columns->LineMinY = window->DC.CursorPos.y;
1717 }
1718 }
1719}
1720
1721void ImGui::Separator()
1722{
1723 ImGuiContext& g = *GImGui;
1724 ImGuiWindow* window = g.CurrentWindow;
1725 if (window->SkipItems)
1726 return;
1727
1728 // Those flags should eventually be configurable by the user
1729 ImGuiSeparatorFlags flags = (window->DC.LayoutType == ImGuiLayoutType_Horizontal) ? ImGuiSeparatorFlags_Vertical : ImGuiSeparatorFlags_Horizontal;
1730
1731 // Only applies to legacy Columns() api as they relied on Separator() a lot.
1732 if (window->DC.CurrentColumns)
1733 flags |= ImGuiSeparatorFlags_SpanAllColumns;
1734
1735 SeparatorEx(flags, ImMax(g.Style.SeparatorSize, 1.0f));
1736}
1737
1738void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end, float extra_w)
1739{
1740 ImGuiContext& g = *GImGui;
1741 ImGuiWindow* window = g.CurrentWindow;
1742 ImGuiStyle& style = g.Style;
1743
1744 const ImVec2 label_size = CalcTextSize(label, label_end, false);
1745 const ImVec2 pos = window->DC.CursorPos;
1746 const ImVec2 padding = style.SeparatorTextPadding;
1747
1748 const float separator_thickness = style.SeparatorTextBorderSize;
1749 const ImVec2 min_size(label_size.x + extra_w + padding.x * 2.0f, ImMax(label_size.y + padding.y * 2.0f, separator_thickness));
1750 const ImRect bb(pos, ImVec2(window->WorkRect.Max.x, pos.y + min_size.y));
1751 const float text_baseline_y = ImTrunc((bb.GetHeight() - label_size.y) * style.SeparatorTextAlign.y + 0.999f); //ImMax(padding.y, ImTrunc((style.SeparatorTextSize - label_size.y) * 0.5f));
1752 ItemSize(min_size, text_baseline_y);
1753 if (!ItemAdd(bb, id))
1754 return;
1755
1756 const float sep1_x1 = pos.x;
1757 const float sep2_x2 = bb.Max.x;
1758 const float seps_y = ImTrunc((bb.Min.y + bb.Max.y) * 0.5f + 0.999f);
1759
1760 const float label_avail_w = ImMax(0.0f, sep2_x2 - sep1_x1 - padding.x * 2.0f);
1761 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
1762
1763 // This allows using SameLine() to position something in the 'extra_w'
1764 window->DC.CursorPosPrevLine.x = label_pos.x + label_size.x;
1765
1766 const ImU32 separator_col = GetColorU32(ImGuiCol_Separator);
1767 if (label_size.x > 0.0f)
1768 {
1769 const float sep1_x2 = label_pos.x - style.ItemSpacing.x;
1770 const float sep2_x1 = label_pos.x + label_size.x + extra_w + style.ItemSpacing.x;
1771 if (sep1_x2 > sep1_x1 && separator_thickness > 0.0f)
1772 window->DrawList->AddLineH(sep1_x1, sep1_x2, seps_y, separator_col, separator_thickness);
1773 if (sep2_x2 > sep2_x1 && separator_thickness > 0.0f)
1774 window->DrawList->AddLineH(sep2_x1, sep2_x2, seps_y, separator_col, separator_thickness);
1775 if (g.LogEnabled)
1776 LogSetNextTextDecoration("---", NULL);
1777 RenderTextEllipsis(window->DrawList, label_pos, ImVec2(bb.Max.x, bb.Max.y + style.ItemSpacing.y), bb.Max.x, label, label_end, &label_size);
1778 }
1779 else
1780 {
1781 if (g.LogEnabled)
1782 LogText("---");
1783 if (separator_thickness > 0.0f)
1784 window->DrawList->AddLineH(sep1_x1, sep2_x2, seps_y, separator_col, separator_thickness);
1785 }
1786}
1787
1788void ImGui::SeparatorText(const char* label)
1789{
1790 ImGuiWindow* window = GetCurrentWindow();
1791 if (window->SkipItems)
1792 return;
1793
1794 // The SeparatorText() vs SeparatorTextEx() distinction is designed to be considerate that we may want:
1795 // - allow separator-text to be draggable items (would require a stable ID + a noticeable highlight)
1796 // - this high-level entry point to allow formatting? (which in turns may require ID separate from formatted string)
1797 // - because of this we probably can't turn 'const char* label' into 'const char* fmt, ...'
1798 // Otherwise, we can decide that users wanting to drag this would layout a dedicated drag-item,
1799 // and then we can turn this into a format function.
1800 SeparatorTextEx(0, label, FindRenderedTextEnd(label), 0.0f);
1801}
1802
1803// 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.
1804bool 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)
1805{
1806 ImGuiContext& g = *GImGui;
1807 ImGuiWindow* window = g.CurrentWindow;
1808
1809 if (!ItemAdd(bb, id, NULL, ImGuiItemFlags_NoNav))
1810 return false;
1811
1812 // FIXME: AFAIK the only leftover reason for passing ImGuiButtonFlags_AllowOverlap here is
1813 // to allow caller of SplitterBehavior() to call SetItemAllowOverlap() after the item.
1814 // Nowadays we would instead want to use SetNextItemAllowOverlap() before the item.
1815 ImGuiButtonFlags button_flags = ImGuiButtonFlags_FlattenChildren;
1816#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1817 button_flags |= ImGuiButtonFlags_AllowOverlap;
1818#endif
1819
1820 bool hovered, held;
1821 ImRect bb_interact = bb;
1822 bb_interact.Expand(axis == ImGuiAxis_Y ? ImVec2(0.0f, hover_extend) : ImVec2(hover_extend, 0.0f));
1823 ButtonBehavior(bb_interact, id, &hovered, &held, button_flags);
1824 if (hovered)
1825 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; // for IsItemHovered(), because bb_interact is larger than bb
1826
1827 if (held || (hovered && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= hover_visibility_delay))
1828 SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW);
1829
1830 ImRect bb_render = bb;
1831 if (held)
1832 {
1833 float mouse_delta = (g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min)[axis];
1834
1835 // Minimum pane size
1836 float size_1_maximum_delta = ImMax(0.0f, *size1 - min_size1);
1837 float size_2_maximum_delta = ImMax(0.0f, *size2 - min_size2);
1838 if (mouse_delta < -size_1_maximum_delta)
1839 mouse_delta = -size_1_maximum_delta;
1840 if (mouse_delta > size_2_maximum_delta)
1841 mouse_delta = size_2_maximum_delta;
1842
1843 // Apply resize
1844 if (mouse_delta != 0.0f)
1845 {
1846 *size1 = ImMax(*size1 + mouse_delta, min_size1);
1847 *size2 = ImMax(*size2 - mouse_delta, min_size2);
1848 bb_render.Translate((axis == ImGuiAxis_X) ? ImVec2(mouse_delta, 0.0f) : ImVec2(0.0f, mouse_delta));
1849 MarkItemEdited(id);
1850 }
1851 }
1852
1853 // Render at new position
1854 if (bg_col & IM_COL32_A_MASK)
1855 window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, bg_col, 0.0f);
1856 const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : (hovered && g.HoveredIdTimer >= hover_visibility_delay) ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator);
1857 window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, col, 0.0f);
1858
1859 return held;
1860}
1861
1862static int IMGUI_CDECL ShrinkWidthItemComparer(const void* lhs, const void* rhs)
1863{
1864 const ImGuiShrinkWidthItem* a = (const ImGuiShrinkWidthItem*)lhs;
1865 const ImGuiShrinkWidthItem* b = (const ImGuiShrinkWidthItem*)rhs;
1866 if (int d = (int)(b->Width - a->Width))
1867 return d;
1868 return b->Index - a->Index;
1869}
1870
1871// Shrink excess width from a set of item, by removing width from the larger items first.
1872// Set items Width to -1.0f to disable shrinking this item.
1873void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess, float width_min)
1874{
1875 if (count == 1)
1876 {
1877 if (items[0].Width >= 0.0f)
1878 items[0].Width = ImMax(items[0].Width - width_excess, width_min);
1879 return;
1880 }
1881 ImQsort(items, (size_t)count, sizeof(ImGuiShrinkWidthItem), ShrinkWidthItemComparer); // Sort largest first, smallest last.
1882 int count_same_width = 1;
1883 while (width_excess > 0.001f && count_same_width < count)
1884 {
1885 while (count_same_width < count && items[0].Width <= items[count_same_width].Width)
1886 count_same_width++;
1887 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);
1888 max_width_to_remove_per_item = ImMin(items[0].Width - width_min, max_width_to_remove_per_item);
1889 if (max_width_to_remove_per_item <= 0.0f)
1890 break;
1891 float base_width_to_remove_per_item = ImMin(width_excess / count_same_width, max_width_to_remove_per_item);
1892 for (int item_n = 0; item_n < count_same_width; item_n++)
1893 {
1894 float width_to_remove_for_this_item = ImMin(base_width_to_remove_per_item, items[item_n].Width - width_min);
1895 items[item_n].Width -= width_to_remove_for_this_item;
1896 width_excess -= width_to_remove_for_this_item;
1897 }
1898 }
1899
1900 // Round width and redistribute remainder
1901 // 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.
1902 width_excess = 0.0f;
1903 for (int n = 0; n < count; n++)
1904 {
1905 float width_rounded = ImTrunc(items[n].Width);
1906 width_excess += items[n].Width - width_rounded;
1907 items[n].Width = width_rounded;
1908 }
1909 while (width_excess > 0.0f)
1910 for (int n = 0; n < count && width_excess > 0.0f; n++)
1911 {
1912 float width_to_add = ImMin(items[n].InitialWidth - items[n].Width, 1.0f);
1913 items[n].Width += width_to_add;
1914 width_excess -= width_to_add;
1915 }
1916}
1917
1918//-------------------------------------------------------------------------
1919// [SECTION] Widgets: ComboBox
1920//-------------------------------------------------------------------------
1921// - CalcMaxPopupHeightFromItemCount() [Internal]
1922// - BeginCombo()
1923// - BeginComboPopup() [Internal]
1924// - EndCombo()
1925// - BeginComboPreview() [Internal]
1926// - EndComboPreview() [Internal]
1927// - Combo()
1928//-------------------------------------------------------------------------
1929
1930static float CalcMaxPopupHeightFromItemCount(int items_count)
1931{
1932 ImGuiContext& g = *GImGui;
1933 if (items_count <= 0)
1934 return FLT_MAX;
1935 return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2);
1936}
1937
1938bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags)
1939{
1940 ImGuiContext& g = *GImGui;
1941 ImGuiWindow* window = GetCurrentWindow();
1942
1943 ImGuiNextWindowDataFlags backup_next_window_data_flags = g.NextWindowData.HasFlags;
1944 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
1945 if (window->SkipItems)
1946 return false;
1947
1948 const ImGuiStyle& style = g.Style;
1949 const ImGuiID id = window->GetID(label);
1950 IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together
1951 if (flags & ImGuiComboFlags_WidthFitPreview)
1952 IM_ASSERT((flags & (ImGuiComboFlags_NoPreview | (ImGuiComboFlags)ImGuiComboFlags_CustomPreview)) == 0);
1953
1954 const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight();
1955 const char* label_end = FindRenderedTextEnd(label);
1956 const ImVec2 label_size = CalcTextSize(label, label_end, false);
1957 const float preview_width = ((flags & ImGuiComboFlags_WidthFitPreview) && (preview_value != NULL)) ? CalcTextSize(preview_value, NULL, false).x : 0.0f;
1958 const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : ((flags & ImGuiComboFlags_WidthFitPreview) ? (arrow_size + preview_width + style.FramePadding.x * 2.0f) : CalcItemWidth());
1959 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
1960 const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
1961 ItemSize(total_bb, style.FramePadding.y);
1962 if (!ItemAdd(total_bb, id, &bb))
1963 return false;
1964
1965 // Open on click
1966 bool hovered, held;
1967 bool pressed = ButtonBehavior(bb, id, &hovered, &held);
1968 const ImGuiID popup_id = ImHashStr("##ComboPopup", 0, id);
1969 bool popup_open = IsPopupOpen(popup_id, ImGuiPopupFlags_None);
1970 if (pressed && !popup_open)
1971 {
1972 OpenPopupEx(popup_id, ImGuiPopupFlags_None);
1973 popup_open = true;
1974 }
1975
1976 // Render shape
1977 const ImU32 frame_col = GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
1978 const float value_x2 = ImMax(bb.Min.x, bb.Max.x - arrow_size);
1979 RenderNavCursor(bb, id);
1980 if (!(flags & ImGuiComboFlags_NoPreview))
1981 window->DrawList->AddRectFilled(bb.Min, ImVec2(value_x2, bb.Max.y), frame_col, style.FrameRounding, (flags & ImGuiComboFlags_NoArrowButton) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersLeft);
1982 if (!(flags & ImGuiComboFlags_NoArrowButton))
1983 {
1984 ImU32 bg_col = GetColorU32((popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1985 ImU32 text_col = GetColorU32(ImGuiCol_Text);
1986 window->DrawList->AddRectFilled(ImVec2(value_x2, bb.Min.y), bb.Max, bg_col, style.FrameRounding, (w <= arrow_size) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersRight);
1987 if (value_x2 + arrow_size - style.FramePadding.x <= bb.Max.x)
1988 RenderArrow(window->DrawList, ImVec2(value_x2 + style.FramePadding.y, bb.Min.y + style.FramePadding.y), text_col, ImGuiDir_Down, 1.0f);
1989 }
1990 RenderFrameBorder(bb.Min, bb.Max, style.FrameRounding);
1991
1992 // Custom preview
1993 if (flags & ImGuiComboFlags_CustomPreview)
1994 {
1995 g.ComboPreviewData.PreviewRect = ImRect(bb.Min.x, bb.Min.y, value_x2, bb.Max.y);
1996 IM_ASSERT(preview_value == NULL || preview_value[0] == 0);
1997 preview_value = NULL;
1998 }
1999
2000 // Render preview and label
2001 if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview))
2002 {
2003 if (g.LogEnabled)
2004 LogSetNextTextDecoration("{", "}");
2005 RenderTextClipped(bb.Min + style.FramePadding, ImVec2(value_x2, bb.Max.y), preview_value, NULL, NULL);
2006 }
2007 if (label_size.x > 0)
2008 RenderText(ImVec2(bb.Max.x + style.ItemInnerSpacing.x, bb.Min.y + style.FramePadding.y), label, label_end, false);
2009
2010 if (!popup_open)
2011 return false;
2012
2013 g.NextWindowData.HasFlags = backup_next_window_data_flags;
2014 return BeginComboPopup(popup_id, bb, flags);
2015}
2016
2017bool ImGui::BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags flags)
2018{
2019 ImGuiContext& g = *GImGui;
2020 if (!IsPopupOpen(popup_id, ImGuiPopupFlags_None))
2021 {
2022 g.NextWindowData.ClearFlags();
2023 return false;
2024 }
2025
2026 // Set popup size
2027 float w = bb.GetWidth();
2028 if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSizeConstraint)
2029 {
2030 g.NextWindowData.SizeConstraintRect.Min.x = ImMax(g.NextWindowData.SizeConstraintRect.Min.x, w);
2031 }
2032 else
2033 {
2034 if ((flags & ImGuiComboFlags_HeightMask_) == 0)
2035 flags |= ImGuiComboFlags_HeightRegular;
2036 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one
2037 int popup_max_height_in_items = -1;
2038 if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8;
2039 else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4;
2040 else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20;
2041 ImVec2 constraint_min(0.0f, 0.0f), constraint_max(FLT_MAX, FLT_MAX);
2042 if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.x <= 0.0f) // Don't apply constraints if user specified a size
2043 constraint_min.x = w;
2044 if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.y <= 0.0f)
2045 constraint_max.y = CalcMaxPopupHeightFromItemCount(popup_max_height_in_items);
2046 SetNextWindowSizeConstraints(constraint_min, constraint_max);
2047 }
2048
2049 // This is essentially a specialized version of BeginPopupEx()
2050 char name[16];
2051 ImFormatString(name, IM_COUNTOF(name), "##Combo_%02d", g.BeginComboDepth); // Recycle windows based on depth
2052
2053 // Set position given a custom constraint (peak into expected window size so we can position it)
2054 // FIXME: This might be easier to express with an hypothetical SetNextWindowPosConstraints() function?
2055 // FIXME: This might be moved to Begin() or at least around the same spot where Tooltips and other Popups are calling FindBestWindowPosForPopupEx()?
2056 if (ImGuiWindow* popup_window = FindWindowByName(name))
2057 if (popup_window->WasActive)
2058 {
2059 // Always override 'AutoPosLastDirection' to not leave a chance for a past value to affect us.
2060 ImVec2 size_expected = CalcWindowNextAutoFitSize(popup_window);
2061 popup_window->AutoPosLastDirection = (flags & ImGuiComboFlags_PopupAlignLeft) ? ImGuiDir_Left : ImGuiDir_Down; // Left = "Below, Toward Left", Down = "Below, Toward Right (default)"
2062 ImRect r_outer = GetPopupAllowedExtentRect(popup_window);
2063 ImVec2 pos = FindBestWindowPosForPopupEx(bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, r_outer, bb, ImGuiPopupPositionPolicy_ComboBox);
2064 SetNextWindowPos(pos);
2065 }
2066
2067 // We don't use BeginPopupEx() solely because we have a custom name string, which we could make an argument to BeginPopupEx()
2068 ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove;
2069 PushStyleVarX(ImGuiStyleVar_WindowPadding, g.Style.FramePadding.x); // Horizontally align ourselves with the framed text
2070 bool ret = Begin(name, NULL, window_flags);
2071 PopStyleVar();
2072 if (!ret)
2073 {
2074 EndPopup();
2075 if (!g.IO.ConfigDebugBeginReturnValueOnce && !g.IO.ConfigDebugBeginReturnValueLoop) // Begin may only return false with those debug tools activated.
2076 IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above
2077 return false;
2078 }
2079 g.BeginComboDepth++;
2080 return true;
2081}
2082
2083void ImGui::EndCombo()
2084{
2085 ImGuiContext& g = *GImGui;
2086 g.BeginComboDepth--;
2087 char name[16];
2088 ImFormatString(name, IM_COUNTOF(name), "##Combo_%02d", g.BeginComboDepth); // FIXME: Move those to helpers?
2089 if (strcmp(g.CurrentWindow->Name, name) != 0)
2090 IM_ASSERT_USER_ERROR_RET(0, "Calling EndCombo() in wrong window!");
2091 EndPopup();
2092}
2093
2094// Call directly after the BeginCombo/EndCombo block. The preview is designed to only host non-interactive elements
2095// (Experimental, see GitHub issues: #1658, #4168)
2096bool ImGui::BeginComboPreview()
2097{
2098 ImGuiContext& g = *GImGui;
2099 ImGuiWindow* window = g.CurrentWindow;
2100 ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
2101
2102 if (window->SkipItems || !(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible))
2103 return false;
2104 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?
2105 if (!window->ClipRect.Overlaps(preview_data->PreviewRect)) // Narrower test (optional)
2106 return false;
2107
2108 // FIXME: This could be contained in a PushWorkRect() api
2109 preview_data->BackupCursorPos = window->DC.CursorPos;
2110 preview_data->BackupCursorMaxPos = window->DC.CursorMaxPos;
2111 preview_data->BackupCursorPosPrevLine = window->DC.CursorPosPrevLine;
2112 preview_data->BackupPrevLineTextBaseOffset = window->DC.PrevLineTextBaseOffset;
2113 preview_data->BackupLayout = window->DC.LayoutType;
2114 window->DC.CursorPos = preview_data->PreviewRect.Min + g.Style.FramePadding;
2115 window->DC.CursorMaxPos = window->DC.CursorPos;
2116 window->DC.LayoutType = ImGuiLayoutType_Horizontal;
2117 window->DC.IsSameLine = false;
2118 PushClipRect(preview_data->PreviewRect.Min, preview_data->PreviewRect.Max, true);
2119
2120 return true;
2121}
2122
2123void ImGui::EndComboPreview()
2124{
2125 ImGuiContext& g = *GImGui;
2126 ImGuiWindow* window = g.CurrentWindow;
2127 ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
2128
2129 // FIXME: Using CursorMaxPos approximation instead of correct AABB which we will store in ImDrawCmd in the future
2130 ImDrawList* draw_list = window->DrawList;
2131 if (window->DC.CursorMaxPos.x < preview_data->PreviewRect.Max.x && window->DC.CursorMaxPos.y < preview_data->PreviewRect.Max.y)
2132 if (draw_list->CmdBuffer.Size > 1) // Unlikely case that the PushClipRect() didn't create a command
2133 {
2134 draw_list->_CmdHeader.ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 2].ClipRect;
2135 draw_list->_TryMergeDrawCmds();
2136 }
2137 PopClipRect();
2138 window->DC.CursorPos = preview_data->BackupCursorPos;
2139 window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, preview_data->BackupCursorMaxPos);
2140 window->DC.CursorPosPrevLine = preview_data->BackupCursorPosPrevLine;
2141 window->DC.PrevLineTextBaseOffset = preview_data->BackupPrevLineTextBaseOffset;
2142 window->DC.LayoutType = preview_data->BackupLayout;
2143 window->DC.IsSameLine = false;
2144 preview_data->PreviewRect = ImRect();
2145}
2146
2147// Getter for the old Combo() API: const char*[]
2148static const char* Items_ArrayGetter(void* data, int idx)
2149{
2150 const char* const* items = (const char* const*)data;
2151 return items[idx];
2152}
2153
2154// Getter for the old Combo() API: "item1\0item2\0item3\0"
2155static const char* Items_SingleStringGetter(void* data, int idx)
2156{
2157 const char* items_separated_by_zeros = (const char*)data;
2158 int items_count = 0;
2159 const char* p = items_separated_by_zeros;
2160 while (*p)
2161 {
2162 if (idx == items_count)
2163 break;
2164 p += ImStrlen(p) + 1;
2165 items_count++;
2166 }
2167 return *p ? p : NULL;
2168}
2169
2170// Old API, prefer using BeginCombo() nowadays if you can.
2171bool 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)
2172{
2173 ImGuiContext& g = *GImGui;
2174
2175 // Call the getter to obtain the preview string which is a parameter to BeginCombo()
2176 const char* preview_value = NULL;
2177 if (*current_item >= 0 && *current_item < items_count)
2178 preview_value = getter(user_data, *current_item);
2179
2180 // 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.
2181 if (popup_max_height_in_items != -1 && !(g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSizeConstraint))
2182 SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items)));
2183
2184 if (!BeginCombo(label, preview_value, ImGuiComboFlags_None))
2185 return false;
2186
2187 // Display items
2188 bool value_changed = false;
2189 ImGuiListClipper clipper;
2190 clipper.Begin(items_count);
2191 clipper.IncludeItemByIndex(*current_item);
2192 while (clipper.Step())
2193 for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
2194 {
2195 const char* item_text = getter(user_data, i);
2196 if (item_text == NULL)
2197 item_text = "*Unknown item*";
2198
2199 PushID(i);
2200 const bool item_selected = (i == *current_item);
2201 if (Selectable(item_text, item_selected) && *current_item != i)
2202 {
2203 value_changed = true;
2204 *current_item = i;
2205 }
2206 if (item_selected)
2207 SetItemDefaultFocus();
2208 PopID();
2209 }
2210
2211 EndCombo();
2212 if (value_changed)
2213 MarkItemEdited(g.LastItemData.ID);
2214
2215 return value_changed;
2216}
2217
2218// Combo box helper allowing to pass an array of strings.
2219bool ImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items)
2220{
2221 const bool value_changed = Combo(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_in_items);
2222 return value_changed;
2223}
2224
2225// Combo box helper allowing to pass all items in a single string literal holding multiple zero-terminated items "item1\0item2\0"
2226bool ImGui::Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int height_in_items)
2227{
2228 int items_count = 0;
2229 const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open
2230 while (*p)
2231 {
2232 p += ImStrlen(p) + 1;
2233 items_count++;
2234 }
2235 bool value_changed = Combo(label, current_item, Items_SingleStringGetter, (void*)items_separated_by_zeros, items_count, height_in_items);
2236 return value_changed;
2237}
2238
2239//-------------------------------------------------------------------------
2240// [SECTION] Data Type and Data Formatting Helpers [Internal]
2241//-------------------------------------------------------------------------
2242// - DataTypeGetInfo()
2243// - DataTypeFormatString()
2244// - DataTypeApplyOp()
2245// - DataTypeApplyFromText()
2246// - DataTypeCompare()
2247// - DataTypeClamp()
2248// - GetMinimumStepAtDecimalPrecision
2249// - RoundScalarWithFormat<>()
2250//-------------------------------------------------------------------------
2251
2252static const ImU32 GDefaultRgbaColorMarkers[4] =
2253{
2254 IM_COL32(240,20,20,255), IM_COL32(20,240,20,255), IM_COL32(20,20,240,255), IM_COL32(140,140,140,255)
2255};
2256
2257static const ImGuiDataTypeInfo GDataTypeInfo[] =
2258{
2259 { sizeof(char), "S8", "%d", "%d" }, // ImGuiDataType_S8
2260 { sizeof(unsigned char), "U8", "%u", "%u" },
2261 { sizeof(short), "S16", "%d", "%d" }, // ImGuiDataType_S16
2262 { sizeof(unsigned short), "U16", "%u", "%u" },
2263 { sizeof(int), "S32", "%d", "%d" }, // ImGuiDataType_S32
2264 { sizeof(unsigned int), "U32", "%u", "%u" },
2265#ifdef _MSC_VER
2266 { sizeof(ImS64), "S64", "%I64d","%I64d" }, // ImGuiDataType_S64
2267 { sizeof(ImU64), "U64", "%I64u","%I64u" },
2268#else
2269 { sizeof(ImS64), "S64", "%lld", "%lld" }, // ImGuiDataType_S64
2270 { sizeof(ImU64), "U64", "%llu", "%llu" },
2271#endif
2272 { sizeof(float), "float", "%.3f","%f" }, // ImGuiDataType_Float (float are promoted to double in va_arg)
2273 { sizeof(double), "double","%f", "%lf" }, // ImGuiDataType_Double
2274 { sizeof(bool), "bool", "%d", "%d" }, // ImGuiDataType_Bool
2275 { 0, "char*","%s", "%s" }, // ImGuiDataType_String
2276};
2277IM_STATIC_ASSERT(IM_COUNTOF(GDataTypeInfo) == ImGuiDataType_COUNT);
2278
2279const ImGuiDataTypeInfo* ImGui::DataTypeGetInfo(ImGuiDataType data_type)
2280{
2281 IM_ASSERT(data_type >= 0 && data_type < ImGuiDataType_COUNT);
2282 return &GDataTypeInfo[data_type];
2283}
2284
2285int ImGui::DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* p_data, const char* format)
2286{
2287 // Signedness doesn't matter when pushing integer arguments
2288 if (data_type == ImGuiDataType_S32 || data_type == ImGuiDataType_U32)
2289 return ImFormatString(buf, buf_size, format, *(const ImU32*)p_data);
2290 if (data_type == ImGuiDataType_S64 || data_type == ImGuiDataType_U64)
2291 return ImFormatString(buf, buf_size, format, *(const ImU64*)p_data);
2292 if (data_type == ImGuiDataType_Float)
2293 return ImFormatString(buf, buf_size, format, *(const float*)p_data);
2294 if (data_type == ImGuiDataType_Double)
2295 return ImFormatString(buf, buf_size, format, *(const double*)p_data);
2296 if (data_type == ImGuiDataType_S8)
2297 return ImFormatString(buf, buf_size, format, *(const ImS8*)p_data);
2298 if (data_type == ImGuiDataType_U8)
2299 return ImFormatString(buf, buf_size, format, *(const ImU8*)p_data);
2300 if (data_type == ImGuiDataType_S16)
2301 return ImFormatString(buf, buf_size, format, *(const ImS16*)p_data);
2302 if (data_type == ImGuiDataType_U16)
2303 return ImFormatString(buf, buf_size, format, *(const ImU16*)p_data);
2304 IM_ASSERT(0);
2305 return 0;
2306}
2307
2308void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg1, const void* arg2)
2309{
2310 IM_ASSERT(op == '+' || op == '-');
2311 switch (data_type)
2312 {
2313 case ImGuiDataType_S8:
2314 if (op == '+') { *(ImS8*)output = ImAddClampOverflow(*(const ImS8*)arg1, *(const ImS8*)arg2, IM_S8_MIN, IM_S8_MAX); }
2315 if (op == '-') { *(ImS8*)output = ImSubClampOverflow(*(const ImS8*)arg1, *(const ImS8*)arg2, IM_S8_MIN, IM_S8_MAX); }
2316 return;
2317 case ImGuiDataType_U8:
2318 if (op == '+') { *(ImU8*)output = ImAddClampOverflow(*(const ImU8*)arg1, *(const ImU8*)arg2, IM_U8_MIN, IM_U8_MAX); }
2319 if (op == '-') { *(ImU8*)output = ImSubClampOverflow(*(const ImU8*)arg1, *(const ImU8*)arg2, IM_U8_MIN, IM_U8_MAX); }
2320 return;
2321 case ImGuiDataType_S16:
2322 if (op == '+') { *(ImS16*)output = ImAddClampOverflow(*(const ImS16*)arg1, *(const ImS16*)arg2, IM_S16_MIN, IM_S16_MAX); }
2323 if (op == '-') { *(ImS16*)output = ImSubClampOverflow(*(const ImS16*)arg1, *(const ImS16*)arg2, IM_S16_MIN, IM_S16_MAX); }
2324 return;
2325 case ImGuiDataType_U16:
2326 if (op == '+') { *(ImU16*)output = ImAddClampOverflow(*(const ImU16*)arg1, *(const ImU16*)arg2, IM_U16_MIN, IM_U16_MAX); }
2327 if (op == '-') { *(ImU16*)output = ImSubClampOverflow(*(const ImU16*)arg1, *(const ImU16*)arg2, IM_U16_MIN, IM_U16_MAX); }
2328 return;
2329 case ImGuiDataType_S32:
2330 if (op == '+') { *(ImS32*)output = ImAddClampOverflow(*(const ImS32*)arg1, *(const ImS32*)arg2, IM_S32_MIN, IM_S32_MAX); }
2331 if (op == '-') { *(ImS32*)output = ImSubClampOverflow(*(const ImS32*)arg1, *(const ImS32*)arg2, IM_S32_MIN, IM_S32_MAX); }
2332 return;
2333 case ImGuiDataType_U32:
2334 if (op == '+') { *(ImU32*)output = ImAddClampOverflow(*(const ImU32*)arg1, *(const ImU32*)arg2, IM_U32_MIN, IM_U32_MAX); }
2335 if (op == '-') { *(ImU32*)output = ImSubClampOverflow(*(const ImU32*)arg1, *(const ImU32*)arg2, IM_U32_MIN, IM_U32_MAX); }
2336 return;
2337 case ImGuiDataType_S64:
2338 if (op == '+') { *(ImS64*)output = ImAddClampOverflow(*(const ImS64*)arg1, *(const ImS64*)arg2, IM_S64_MIN, IM_S64_MAX); }
2339 if (op == '-') { *(ImS64*)output = ImSubClampOverflow(*(const ImS64*)arg1, *(const ImS64*)arg2, IM_S64_MIN, IM_S64_MAX); }
2340 return;
2341 case ImGuiDataType_U64:
2342 if (op == '+') { *(ImU64*)output = ImAddClampOverflow(*(const ImU64*)arg1, *(const ImU64*)arg2, IM_U64_MIN, IM_U64_MAX); }
2343 if (op == '-') { *(ImU64*)output = ImSubClampOverflow(*(const ImU64*)arg1, *(const ImU64*)arg2, IM_U64_MIN, IM_U64_MAX); }
2344 return;
2345 case ImGuiDataType_Float:
2346 if (op == '+') { *(float*)output = *(const float*)arg1 + *(const float*)arg2; }
2347 if (op == '-') { *(float*)output = *(const float*)arg1 - *(const float*)arg2; }
2348 return;
2349 case ImGuiDataType_Double:
2350 if (op == '+') { *(double*)output = *(const double*)arg1 + *(const double*)arg2; }
2351 if (op == '-') { *(double*)output = *(const double*)arg1 - *(const double*)arg2; }
2352 return;
2353 case ImGuiDataType_COUNT: break;
2354 }
2355 IM_ASSERT(0);
2356}
2357
2358// User can input math operators (e.g. +100) to edit a numerical values.
2359// NB: This is _not_ a full expression evaluator. We should probably add one and replace this dumb mess..
2360bool ImGui::DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format, void* p_data_when_empty)
2361{
2362 // Copy the value in an opaque buffer so we can compare at the end of the function if it changed at all.
2363 const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
2364 ImGuiDataTypeStorage data_backup;
2365 memcpy(&data_backup, p_data, type_info->Size);
2366
2367 while (ImCharIsBlankA(*buf))
2368 buf++;
2369 if (!buf[0])
2370 {
2371 if (p_data_when_empty != NULL)
2372 {
2373 memcpy(p_data, p_data_when_empty, type_info->Size);
2374 return memcmp(&data_backup, p_data, type_info->Size) != 0;
2375 }
2376 return false;
2377 }
2378
2379 // Sanitize format
2380 // - 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
2381 char format_sanitized[32];
2382 if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double)
2383 {
2384 format = type_info->ScanFmt;
2385 }
2386 else
2387 {
2388 format = ImParseFormatSanitizeForScanning(format, format_sanitized, IM_COUNTOF(format_sanitized));
2389 if (format[0] == '\0')
2390 format = type_info->ScanFmt; // Format doesn't want us to show the number currently, but we still need to parse the resulting input
2391 }
2392
2393 // Small types need a 32-bit buffer to receive the result from scanf()
2394 int v32 = 0;
2395 if (sscanf(buf, format, type_info->Size >= 4 ? p_data : &v32) < 1)
2396 return false;
2397 if (type_info->Size < 4)
2398 {
2399 if (data_type == ImGuiDataType_S8)
2400 *(ImS8*)p_data = (ImS8)ImClamp(v32, (int)IM_S8_MIN, (int)IM_S8_MAX);
2401 else if (data_type == ImGuiDataType_U8)
2402 *(ImU8*)p_data = (ImU8)ImClamp(v32, (int)IM_U8_MIN, (int)IM_U8_MAX);
2403 else if (data_type == ImGuiDataType_S16)
2404 *(ImS16*)p_data = (ImS16)ImClamp(v32, (int)IM_S16_MIN, (int)IM_S16_MAX);
2405 else if (data_type == ImGuiDataType_U16)
2406 *(ImU16*)p_data = (ImU16)ImClamp(v32, (int)IM_U16_MIN, (int)IM_U16_MAX);
2407 else
2408 IM_ASSERT(0);
2409 }
2410
2411 return memcmp(&data_backup, p_data, type_info->Size) != 0;
2412}
2413
2414template<typename T>
2415static int DataTypeCompareT(const T* lhs, const T* rhs)
2416{
2417 if (*lhs < *rhs) return -1;
2418 if (*lhs > *rhs) return +1;
2419 return 0;
2420}
2421
2422int ImGui::DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2)
2423{
2424 switch (data_type)
2425 {
2426 case ImGuiDataType_S8: return DataTypeCompareT<ImS8 >((const ImS8* )arg_1, (const ImS8* )arg_2);
2427 case ImGuiDataType_U8: return DataTypeCompareT<ImU8 >((const ImU8* )arg_1, (const ImU8* )arg_2);
2428 case ImGuiDataType_S16: return DataTypeCompareT<ImS16 >((const ImS16* )arg_1, (const ImS16* )arg_2);
2429 case ImGuiDataType_U16: return DataTypeCompareT<ImU16 >((const ImU16* )arg_1, (const ImU16* )arg_2);
2430 case ImGuiDataType_S32: return DataTypeCompareT<ImS32 >((const ImS32* )arg_1, (const ImS32* )arg_2);
2431 case ImGuiDataType_U32: return DataTypeCompareT<ImU32 >((const ImU32* )arg_1, (const ImU32* )arg_2);
2432 case ImGuiDataType_S64: return DataTypeCompareT<ImS64 >((const ImS64* )arg_1, (const ImS64* )arg_2);
2433 case ImGuiDataType_U64: return DataTypeCompareT<ImU64 >((const ImU64* )arg_1, (const ImU64* )arg_2);
2434 case ImGuiDataType_Float: return DataTypeCompareT<float >((const float* )arg_1, (const float* )arg_2);
2435 case ImGuiDataType_Double: return DataTypeCompareT<double>((const double*)arg_1, (const double*)arg_2);
2436 case ImGuiDataType_COUNT: break;
2437 }
2438 IM_ASSERT(0);
2439 return 0;
2440}
2441
2442template<typename T>
2443static bool DataTypeClampT(T* v, const T* v_min, const T* v_max)
2444{
2445 // Clamp, both sides are optional, return true if modified
2446 if (v_min && *v < *v_min) { *v = *v_min; return true; }
2447 if (v_max && *v > *v_max) { *v = *v_max; return true; }
2448 return false;
2449}
2450
2451bool ImGui::DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max)
2452{
2453 switch (data_type)
2454 {
2455 case ImGuiDataType_S8: return DataTypeClampT<ImS8 >((ImS8* )p_data, (const ImS8* )p_min, (const ImS8* )p_max);
2456 case ImGuiDataType_U8: return DataTypeClampT<ImU8 >((ImU8* )p_data, (const ImU8* )p_min, (const ImU8* )p_max);
2457 case ImGuiDataType_S16: return DataTypeClampT<ImS16 >((ImS16* )p_data, (const ImS16* )p_min, (const ImS16* )p_max);
2458 case ImGuiDataType_U16: return DataTypeClampT<ImU16 >((ImU16* )p_data, (const ImU16* )p_min, (const ImU16* )p_max);
2459 case ImGuiDataType_S32: return DataTypeClampT<ImS32 >((ImS32* )p_data, (const ImS32* )p_min, (const ImS32* )p_max);
2460 case ImGuiDataType_U32: return DataTypeClampT<ImU32 >((ImU32* )p_data, (const ImU32* )p_min, (const ImU32* )p_max);
2461 case ImGuiDataType_S64: return DataTypeClampT<ImS64 >((ImS64* )p_data, (const ImS64* )p_min, (const ImS64* )p_max);
2462 case ImGuiDataType_U64: return DataTypeClampT<ImU64 >((ImU64* )p_data, (const ImU64* )p_min, (const ImU64* )p_max);
2463 case ImGuiDataType_Float: return DataTypeClampT<float >((float* )p_data, (const float* )p_min, (const float* )p_max);
2464 case ImGuiDataType_Double: return DataTypeClampT<double>((double*)p_data, (const double*)p_min, (const double*)p_max);
2465 case ImGuiDataType_COUNT: break;
2466 }
2467 IM_ASSERT(0);
2468 return false;
2469}
2470
2471bool ImGui::DataTypeIsZero(ImGuiDataType data_type, const void* p_data)
2472{
2473 ImGuiContext& g = *GImGui;
2474 return DataTypeCompare(data_type, p_data, &g.DataTypeZeroValue) == 0;
2475}
2476
2477static float GetMinimumStepAtDecimalPrecision(int decimal_precision)
2478{
2479 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 };
2480 if (decimal_precision < 0)
2481 return FLT_MIN;
2482 return (decimal_precision < IM_COUNTOF(min_steps)) ? min_steps[decimal_precision] : ImPow(10.0f, (float)-decimal_precision);
2483}
2484
2485template<typename TYPE>
2486TYPE ImGui::RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, TYPE v)
2487{
2488 IM_UNUSED(data_type);
2489 IM_ASSERT(data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double);
2490 const char* fmt_start = ImParseFormatFindStart(format);
2491 if (fmt_start[0] != '%' || fmt_start[1] == '%') // Don't apply if the value is not visible in the format string
2492 return v;
2493
2494 // Sanitize format
2495 char fmt_sanitized[32];
2496 ImParseFormatSanitizeForPrinting(fmt_start, fmt_sanitized, IM_COUNTOF(fmt_sanitized));
2497 fmt_start = fmt_sanitized;
2498
2499 // Format value with our rounding, and read back
2500 char v_str[64];
2501 ImFormatString(v_str, IM_COUNTOF(v_str), fmt_start, v);
2502 const char* p = v_str;
2503 while (*p == ' ')
2504 p++;
2505 v = (TYPE)ImAtof(p);
2506
2507 return v;
2508}
2509
2510//-------------------------------------------------------------------------
2511// [SECTION] Widgets: DragScalar, DragFloat, DragInt, etc.
2512//-------------------------------------------------------------------------
2513// - DragBehaviorT<>() [Internal]
2514// - DragBehavior() [Internal]
2515// - DragScalar()
2516// - DragScalarN()
2517// - DragFloat()
2518// - DragFloat2()
2519// - DragFloat3()
2520// - DragFloat4()
2521// - DragFloatRange2()
2522// - DragInt()
2523// - DragInt2()
2524// - DragInt3()
2525// - DragInt4()
2526// - DragIntRange2()
2527//-------------------------------------------------------------------------
2528
2529// This is called by DragBehavior() when the widget is active (held by mouse or being manipulated with Nav controls)
2530template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2531bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags)
2532{
2533 ImGuiContext& g = *GImGui;
2534 const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
2535 const bool is_bounded = (v_min < v_max) || ((v_min == v_max) && (v_min != 0.0f || (flags & ImGuiSliderFlags_ClampZeroRange)));
2536 const bool is_wrapped = is_bounded && (flags & ImGuiSliderFlags_WrapAround);
2537 const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
2538 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2539
2540 // Default tweak speed
2541 if (v_speed == 0.0f && is_bounded && (v_max - v_min < FLT_MAX))
2542 v_speed = (float)((v_max - v_min) * g.DragSpeedDefaultRatio);
2543
2544 // Inputs accumulates into g.DragCurrentAccum, which is flushed into the current value as soon as it makes a difference with our precision settings
2545 float adjust_delta = 0.0f;
2546 if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid() && IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2547 {
2548 adjust_delta = g.IO.MouseDelta[axis];
2549 if (g.IO.KeyAlt && !(flags & ImGuiSliderFlags_NoSpeedTweaks))
2550 adjust_delta *= 1.0f / 100.0f;
2551 if (g.IO.KeyShift && !(flags & ImGuiSliderFlags_NoSpeedTweaks))
2552 adjust_delta *= 10.0f;
2553 }
2554 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
2555 {
2556 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0;
2557 const bool tweak_slow = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
2558 const bool tweak_fast = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
2559 const float tweak_factor = (flags & ImGuiSliderFlags_NoSpeedTweaks) ? 1.0f : tweak_slow ? 1.0f / 10.0f : tweak_fast ? 10.0f : 1.0f;
2560 adjust_delta = GetNavTweakPressedAmount(axis) * tweak_factor;
2561 v_speed = ImMax(v_speed, GetMinimumStepAtDecimalPrecision(decimal_precision));
2562 }
2563 adjust_delta *= v_speed;
2564
2565 // For vertical drag we currently assume that Up=higher value (like we do with vertical sliders). This may become a parameter.
2566 if (axis == ImGuiAxis_Y)
2567 adjust_delta = -adjust_delta;
2568
2569 // For logarithmic use our range is effectively 0..1 so scale the delta into that range
2570 if (is_logarithmic && (v_max - v_min < FLT_MAX) && ((v_max - v_min) > 0.000001f)) // Epsilon to avoid /0
2571 adjust_delta /= (float)(v_max - v_min);
2572
2573 // Clear current value on activation
2574 // 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.
2575 const bool is_just_activated = g.ActiveIdIsJustActivated;
2576 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));
2577 if (is_just_activated || is_already_past_limits_and_pushing_outward)
2578 {
2579 g.DragCurrentAccum = 0.0f;
2580 g.DragCurrentAccumDirty = false;
2581 }
2582 else if (adjust_delta != 0.0f)
2583 {
2584 g.DragCurrentAccum += adjust_delta;
2585 g.DragCurrentAccumDirty = true;
2586 }
2587
2588 if (!g.DragCurrentAccumDirty)
2589 return false;
2590
2591 TYPE v_cur = *v;
2592 FLOATTYPE v_old_ref_for_accum_remainder = (FLOATTYPE)0.0f;
2593
2594 float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
2595 const float zero_deadzone_halfsize = 0.0f; // Drag widgets have no deadzone (as it doesn't make sense)
2596 if (is_logarithmic)
2597 {
2598 // 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.
2599 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1;
2600 logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision);
2601
2602 // Convert to parametric space, apply delta, convert back
2603 float v_old_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2604 float v_new_parametric = v_old_parametric + g.DragCurrentAccum;
2605 v_cur = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new_parametric, v_min, v_max, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2606 v_old_ref_for_accum_remainder = v_old_parametric;
2607 }
2608 else
2609 {
2610 v_cur += (SIGNEDTYPE)g.DragCurrentAccum;
2611 }
2612
2613 // Round to user desired precision based on format string
2614 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
2615 v_cur = RoundScalarWithFormatT<TYPE>(format, data_type, v_cur);
2616
2617 // Preserve remainder after rounding has been applied. This also allow slow tweaking of values.
2618 g.DragCurrentAccumDirty = false;
2619 if (is_logarithmic)
2620 {
2621 // Convert to parametric space, apply delta, convert back
2622 float v_new_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2623 g.DragCurrentAccum -= (float)(v_new_parametric - v_old_ref_for_accum_remainder);
2624 }
2625 else
2626 {
2627 g.DragCurrentAccum -= (float)((SIGNEDTYPE)v_cur - (SIGNEDTYPE)*v);
2628 }
2629
2630 // Lose zero sign for float/double
2631 if (v_cur == (TYPE)-0)
2632 v_cur = (TYPE)0;
2633
2634 if (*v != v_cur && is_bounded)
2635 {
2636 if (is_wrapped)
2637 {
2638 // Wrap values
2639 if (v_cur < v_min)
2640 v_cur += v_max - v_min + (is_floating_point ? 0 : 1);
2641 if (v_cur > v_max)
2642 v_cur -= v_max - v_min + (is_floating_point ? 0 : 1);
2643 }
2644 else
2645 {
2646 // Clamp values + handle overflow/wrap-around for integer types.
2647 if (v_cur < v_min || (v_cur > *v && adjust_delta < 0.0f && !is_floating_point))
2648 v_cur = v_min;
2649 if (v_cur > v_max || (v_cur < *v && adjust_delta > 0.0f && !is_floating_point))
2650 v_cur = v_max;
2651 }
2652 }
2653
2654 // Apply result
2655 if (*v == v_cur)
2656 return false;
2657 *v = v_cur;
2658 return true;
2659}
2660
2661bool 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)
2662{
2663 // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
2664 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.");
2665
2666 ImGuiContext& g = *GImGui;
2667 if (g.ActiveId == id)
2668 {
2669 // Those are the things we can do easily outside the DragBehaviorT<> template, saves code generation.
2670 if (g.ActiveIdSource == ImGuiInputSource_Mouse && !g.IO.MouseDown[0])
2671 ClearActiveID();
2672 else if ((g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) && g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
2673 ClearActiveID();
2674 }
2675 if (g.ActiveId != id)
2676 return false;
2677 if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
2678 return false;
2679
2680 switch (data_type)
2681 {
2682 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; }
2683 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; }
2684 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; }
2685 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; }
2686 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);
2687 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);
2688 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);
2689 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);
2690 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);
2691 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);
2692 case ImGuiDataType_COUNT: break;
2693 }
2694 IM_ASSERT(0);
2695 return false;
2696}
2697
2698// Only clamp Ctrl+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp)
2699static bool TempInputIsClampEnabled(ImGuiSliderFlags flags, ImGuiDataType data_type, const void* p_min, const void* p_max)
2700{
2701 if ((flags & ImGuiSliderFlags_ClampOnInput) && (p_min != NULL || p_max != NULL))
2702 {
2703 const int clamp_range_dir = (p_min != NULL && p_max != NULL) ? ImGui::DataTypeCompare(data_type, p_min, p_max) : 0; // -1 when *p_min < *p_max, == 0 when *p_min == *p_max
2704 if (p_min == NULL || p_max == NULL || clamp_range_dir < 0)
2705 return true;
2706 if (clamp_range_dir == 0)
2707 return ImGui::DataTypeIsZero(data_type, p_min) ? ((flags & ImGuiSliderFlags_ClampZeroRange) != 0) : true;
2708 }
2709 return false;
2710}
2711
2712// 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.
2713// Read code of e.g. DragFloat(), DragInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
2714bool 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)
2715{
2716 ImGuiWindow* window = GetCurrentWindow();
2717 if (window->SkipItems)
2718 return false;
2719
2720 ImGuiContext& g = *GImGui;
2721 const ImGuiStyle& style = g.Style;
2722 const ImGuiID id = window->GetID(label);
2723 const float w = CalcItemWidth();
2724 const ImU32 color_marker = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasColorMarker) ? g.NextItemData.ColorMarker : 0;
2725
2726 const char* label_end = FindRenderedTextEnd(label);
2727 const ImVec2 label_size = CalcTextSize(label, label_end, false);
2728 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
2729 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));
2730
2731 const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
2732 ItemSize(total_bb, style.FramePadding.y);
2733 if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
2734 return false;
2735
2736 // Default format string when passing NULL
2737 if (format == NULL)
2738 format = DataTypeGetInfo(data_type)->PrintFmt;
2739
2740 const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags);
2741 bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
2742 if (!temp_input_is_active)
2743 {
2744 // Tabbing or Ctrl+Click on Drag turns it into an InputText
2745 const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id);
2746 const bool double_clicked = (hovered && g.IO.MouseClickedCount[0] == 2 && TestKeyOwner(ImGuiKey_MouseLeft, id));
2747 const bool make_active = (clicked || double_clicked || g.NavActivateId == id);
2748 if (make_active && (clicked || double_clicked))
2749 SetKeyOwner(ImGuiKey_MouseLeft, id);
2750 if (make_active && temp_input_allowed)
2751 if ((clicked && g.IO.KeyCtrl) || double_clicked || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
2752 temp_input_is_active = true;
2753
2754 // (Optional) simple click (without moving) turns Drag into an InputText
2755 if (g.IO.ConfigDragClickToInputText && temp_input_allowed && !temp_input_is_active)
2756 if (g.ActiveId == id && hovered && g.IO.MouseReleased[0] && !IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2757 {
2758 g.NavActivateId = id;
2759 g.NavActivateFlags = ImGuiActivateFlags_PreferInput;
2760 temp_input_is_active = true;
2761 }
2762
2763 // Store initial value (not used by main lib but available as a convenience but some mods e.g. to revert)
2764 if (make_active)
2765 memcpy(&g.ActiveIdValueOnActivation, p_data, DataTypeGetInfo(data_type)->Size);
2766
2767 if (make_active && !temp_input_is_active)
2768 {
2769 SetActiveID(id, window);
2770 SetFocusID(id, window);
2771 FocusWindow(window);
2772 g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
2773 }
2774 }
2775
2776 if (temp_input_is_active)
2777 {
2778 const bool clamp_enabled = TempInputIsClampEnabled(flags, data_type, p_min, p_max);
2779 return TempInputScalar(frame_bb, id, label, data_type, p_data, format, clamp_enabled ? p_min : NULL, clamp_enabled ? p_max : NULL);
2780 }
2781
2782 // Draw frame
2783 const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
2784 RenderNavCursor(frame_bb, id);
2785 RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, false, style.FrameRounding);
2786 if (color_marker != 0 && style.ColorMarkerSize > 0.0f)
2787 RenderColorComponentMarker(frame_bb, GetColorU32(color_marker), style.FrameRounding);
2788 RenderFrameBorder(frame_bb.Min, frame_bb.Max, g.Style.FrameRounding);
2789
2790 // Drag behavior
2791 const bool value_changed = DragBehavior(id, data_type, p_data, v_speed, p_min, p_max, format, flags);
2792 if (value_changed)
2793 MarkItemEdited(id);
2794
2795 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
2796 char value_buf[64];
2797 const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_COUNTOF(value_buf), data_type, p_data, format);
2798 if (g.LogEnabled)
2799 LogSetNextTextDecoration("{", "}");
2800 RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f));
2801
2802 if (label_size.x > 0.0f)
2803 RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label, label_end, false);
2804
2805 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
2806 return value_changed;
2807}
2808
2809bool 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)
2810{
2811 ImGuiWindow* window = GetCurrentWindow();
2812 if (window->SkipItems)
2813 return false;
2814
2815 ImGuiContext& g = *GImGui;
2816 bool value_changed = false;
2817 BeginGroup();
2818 PushID(label);
2819 PushMultiItemsWidths(components, CalcItemWidth());
2820 size_t type_size = GDataTypeInfo[data_type].Size;
2821 for (int i = 0; i < components; i++)
2822 {
2823 PushID(i);
2824 if (i > 0)
2825 SameLine(0, g.Style.ItemInnerSpacing.x);
2826 if (flags & ImGuiSliderFlags_ColorMarkers)
2827 SetNextItemColorMarker(GDefaultRgbaColorMarkers[i]);
2828 value_changed |= DragScalar("", data_type, p_data, v_speed, p_min, p_max, format, flags);
2829 PopID();
2830 PopItemWidth();
2831 p_data = (void*)((char*)p_data + type_size);
2832 }
2833 PopID();
2834
2835 const char* label_end = FindRenderedTextEnd(label);
2836 if (label != label_end)
2837 {
2838 SameLine(0, g.Style.ItemInnerSpacing.x);
2839 TextEx(label, label_end);
2840 }
2841
2842 EndGroup();
2843 return value_changed;
2844}
2845
2846bool ImGui::DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2847{
2848 return DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, format, flags);
2849}
2850
2851bool ImGui::DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2852{
2853 return DragScalarN(label, ImGuiDataType_Float, v, 2, v_speed, &v_min, &v_max, format, flags);
2854}
2855
2856bool ImGui::DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2857{
2858 return DragScalarN(label, ImGuiDataType_Float, v, 3, v_speed, &v_min, &v_max, format, flags);
2859}
2860
2861bool ImGui::DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2862{
2863 return DragScalarN(label, ImGuiDataType_Float, v, 4, v_speed, &v_min, &v_max, format, flags);
2864}
2865
2866// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2867bool 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)
2868{
2869 ImGuiWindow* window = GetCurrentWindow();
2870 if (window->SkipItems)
2871 return false;
2872
2873 ImGuiContext& g = *GImGui;
2874 PushID(label);
2875 BeginGroup();
2876 PushMultiItemsWidths(2, CalcItemWidth());
2877
2878 float min_min = (v_min >= v_max) ? -FLT_MAX : v_min;
2879 float min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max);
2880 ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2881 bool value_changed = DragScalar("##min", ImGuiDataType_Float, v_current_min, v_speed, &min_min, &min_max, format, min_flags);
2882 PopItemWidth();
2883 SameLine(0, g.Style.ItemInnerSpacing.x);
2884
2885 float max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min);
2886 float max_max = (v_min >= v_max) ? FLT_MAX : v_max;
2887 ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2888 value_changed |= DragScalar("##max", ImGuiDataType_Float, v_current_max, v_speed, &max_min, &max_max, format_max ? format_max : format, max_flags);
2889 PopItemWidth();
2890 SameLine(0, g.Style.ItemInnerSpacing.x);
2891
2892 TextEx(label, FindRenderedTextEnd(label));
2893 EndGroup();
2894 PopID();
2895
2896 return value_changed;
2897}
2898
2899// NB: v_speed is float to allow adjusting the drag speed with more precision
2900bool ImGui::DragInt(const char* label, int* v, float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2901{
2902 return DragScalar(label, ImGuiDataType_S32, v, v_speed, &v_min, &v_max, format, flags);
2903}
2904
2905bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2906{
2907 return DragScalarN(label, ImGuiDataType_S32, v, 2, v_speed, &v_min, &v_max, format, flags);
2908}
2909
2910bool ImGui::DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2911{
2912 return DragScalarN(label, ImGuiDataType_S32, v, 3, v_speed, &v_min, &v_max, format, flags);
2913}
2914
2915bool ImGui::DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2916{
2917 return DragScalarN(label, ImGuiDataType_S32, v, 4, v_speed, &v_min, &v_max, format, flags);
2918}
2919
2920// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2921bool 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)
2922{
2923 ImGuiWindow* window = GetCurrentWindow();
2924 if (window->SkipItems)
2925 return false;
2926
2927 ImGuiContext& g = *GImGui;
2928 PushID(label);
2929 BeginGroup();
2930 PushMultiItemsWidths(2, CalcItemWidth());
2931
2932 int min_min = (v_min >= v_max) ? INT_MIN : v_min;
2933 int min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max);
2934 ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2935 bool value_changed = DragInt("##min", v_current_min, v_speed, min_min, min_max, format, min_flags);
2936 PopItemWidth();
2937 SameLine(0, g.Style.ItemInnerSpacing.x);
2938
2939 int max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min);
2940 int max_max = (v_min >= v_max) ? INT_MAX : v_max;
2941 ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2942 value_changed |= DragInt("##max", v_current_max, v_speed, max_min, max_max, format_max ? format_max : format, max_flags);
2943 PopItemWidth();
2944 SameLine(0, g.Style.ItemInnerSpacing.x);
2945
2946 TextEx(label, FindRenderedTextEnd(label));
2947 EndGroup();
2948 PopID();
2949
2950 return value_changed;
2951}
2952
2953//-------------------------------------------------------------------------
2954// [SECTION] Widgets: SliderScalar, SliderFloat, SliderInt, etc.
2955//-------------------------------------------------------------------------
2956// - ScaleRatioFromValueT<> [Internal]
2957// - ScaleValueFromRatioT<> [Internal]
2958// - SliderBehaviorT<>() [Internal]
2959// - SliderBehavior() [Internal]
2960// - SliderScalar()
2961// - SliderScalarN()
2962// - SliderFloat()
2963// - SliderFloat2()
2964// - SliderFloat3()
2965// - SliderFloat4()
2966// - SliderAngle()
2967// - SliderInt()
2968// - SliderInt2()
2969// - SliderInt3()
2970// - SliderInt4()
2971// - VSliderScalar()
2972// - VSliderFloat()
2973// - VSliderInt()
2974//-------------------------------------------------------------------------
2975
2976// Convert a value v in the output space of a slider into a parametric position on the slider itself (the logical opposite of ScaleValueFromRatioT)
2977template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2978float ImGui::ScaleRatioFromValueT(ImGuiDataType data_type, TYPE v, TYPE v_min, TYPE v_max, float logarithmic_zero_epsilon, float zero_deadzone_halfsize)
2979{
2980 if (v_min == v_max)
2981 return 0.0f;
2982 IM_UNUSED(data_type);
2983
2984 const TYPE v_clamped = (v_min < v_max) ? ImClamp(v, v_min, v_max) : ImClamp(v, v_max, v_min);
2985 if (logarithmic_zero_epsilon > 0.0f) // == is_logarithmic from caller
2986 {
2987 bool flipped = v_max < v_min;
2988
2989 if (flipped) // Handle the case where the range is backwards
2990 ImSwap(v_min, v_max);
2991
2992 // Fudge min/max to avoid getting close to log(0)
2993 FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
2994 FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
2995
2996 // Awkward special cases - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
2997 if ((v_min == 0.0f) && (v_max < 0.0f))
2998 v_min_fudged = -logarithmic_zero_epsilon;
2999 else if ((v_max == 0.0f) && (v_min < 0.0f))
3000 v_max_fudged = -logarithmic_zero_epsilon;
3001
3002 float result;
3003 if (v_clamped <= v_min_fudged)
3004 result = 0.0f; // Workaround for values that are in-range but below our fudge
3005 else if (v_clamped >= v_max_fudged)
3006 result = 1.0f; // Workaround for values that are in-range but above our fudge
3007 else if ((v_min * v_max) < 0.0f) // Range crosses zero, so split into two portions
3008 {
3009 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)
3010 float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
3011 float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
3012 if (v == 0.0f)
3013 result = zero_point_center; // Special case for exactly zero
3014 else if (v < 0.0f)
3015 result = (1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(-v_min_fudged / logarithmic_zero_epsilon))) * zero_point_snap_L;
3016 else
3017 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));
3018 }
3019 else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
3020 result = 1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / -v_max_fudged) / ImLog(-v_min_fudged / -v_max_fudged));
3021 else
3022 result = (float)(ImLog((FLOATTYPE)v_clamped / v_min_fudged) / ImLog(v_max_fudged / v_min_fudged));
3023
3024 return flipped ? (1.0f - result) : result;
3025 }
3026 else
3027 {
3028 // Linear slider
3029 return (float)((FLOATTYPE)(SIGNEDTYPE)(v_clamped - v_min) / (FLOATTYPE)(SIGNEDTYPE)(v_max - v_min));
3030 }
3031}
3032
3033// Convert a parametric position on a slider into a value v in the output space (the logical opposite of ScaleRatioFromValueT)
3034template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
3035TYPE ImGui::ScaleValueFromRatioT(ImGuiDataType data_type, float t, TYPE v_min, TYPE v_max, float logarithmic_zero_epsilon, float zero_deadzone_halfsize)
3036{
3037 // We special-case the extents because otherwise our logarithmic fudging can lead to "mathematically correct"
3038 // but non-intuitive behaviors like a fully-left slider not actually reaching the minimum value. Also generally simpler.
3039 if (t <= 0.0f || v_min == v_max)
3040 return v_min;
3041 if (t >= 1.0f)
3042 return v_max;
3043
3044 TYPE result = (TYPE)0;
3045 if (logarithmic_zero_epsilon > 0.0f) // == is_logarithmic from caller
3046 {
3047 // Fudge min/max to avoid getting silly results close to zero
3048 FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
3049 FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
3050
3051 const bool flipped = v_max < v_min; // Check if range is "backwards"
3052 if (flipped)
3053 ImSwap(v_min_fudged, v_max_fudged);
3054
3055 // Awkward special case - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
3056 if ((v_max == 0.0f) && (v_min < 0.0f))
3057 v_max_fudged = -logarithmic_zero_epsilon;
3058
3059 float t_with_flip = flipped ? (1.0f - t) : t; // t, but flipped if necessary to account for us flipping the range
3060
3061 if ((v_min * v_max) < 0.0f) // Range crosses zero, so we have to do this in two parts
3062 {
3063 float zero_point_center = (-(float)ImMin(v_min, v_max)) / ImAbs((float)v_max - (float)v_min); // The zero point in parametric space
3064 float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
3065 float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
3066 if (t_with_flip >= zero_point_snap_L && t_with_flip <= zero_point_snap_R)
3067 result = (TYPE)0.0f; // Special case to make getting exactly zero possible (the epsilon prevents it otherwise)
3068 else if (t_with_flip < zero_point_center)
3069 result = (TYPE)-(logarithmic_zero_epsilon * ImPow(-v_min_fudged / logarithmic_zero_epsilon, (FLOATTYPE)(1.0f - (t_with_flip / zero_point_snap_L))));
3070 else
3071 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))));
3072 }
3073 else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
3074 result = (TYPE)-(-v_max_fudged * ImPow(-v_min_fudged / -v_max_fudged, (FLOATTYPE)(1.0f - t_with_flip)));
3075 else
3076 result = (TYPE)(v_min_fudged * ImPow(v_max_fudged / v_min_fudged, (FLOATTYPE)t_with_flip));
3077 }
3078 else
3079 {
3080 // Linear slider
3081 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
3082 if (is_floating_point)
3083 {
3084 result = ImLerp(v_min, v_max, t);
3085 }
3086 else if (t < 1.0)
3087 {
3088 // - For integer values we want the clicking position to match the grab box so we round above
3089 // This code is carefully tuned to work with large values (e.g. high ranges of U64) while preserving this property..
3090 // - 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
3091 // range is going to be imprecise anyway, with this check we at least make the edge values matches expected limits.
3092 FLOATTYPE v_new_off_f = (SIGNEDTYPE)(v_max - v_min) * t;
3093 result = (TYPE)((SIGNEDTYPE)v_min + (SIGNEDTYPE)(v_new_off_f + (FLOATTYPE)(v_min > v_max ? -0.5 : 0.5)));
3094 }
3095 }
3096
3097 return result;
3098}
3099
3100// FIXME: Try to move more of the code into shared SliderBehavior()
3101template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
3102bool 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)
3103{
3104 ImGuiContext& g = *GImGui;
3105 const ImGuiStyle& style = g.Style;
3106
3107 const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
3108 const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
3109 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
3110 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.
3111
3112 // Calculate bounds
3113 const float grab_padding = 2.0f; // FIXME: Should be part of style.
3114 const float slider_sz = (bb.Max[axis] - bb.Min[axis]) - grab_padding * 2.0f;
3115 float grab_sz = style.GrabMinSize;
3116 if (!is_floating_point && v_range_f >= 0.0f) // v_range_f < 0 may happen on integer overflows
3117 grab_sz = ImMax(slider_sz / (v_range_f + 1), style.GrabMinSize); // For integer sliders: if possible have the grab size represent 1 unit
3118 grab_sz = ImMin(grab_sz, slider_sz);
3119 const float slider_usable_sz = slider_sz - grab_sz;
3120 const float slider_usable_pos_min = bb.Min[axis] + grab_padding + grab_sz * 0.5f;
3121 const float slider_usable_pos_max = bb.Max[axis] - grab_padding - grab_sz * 0.5f;
3122
3123 float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
3124 float zero_deadzone_halfsize = 0.0f; // Only valid when is_logarithmic is true
3125 if (is_logarithmic)
3126 {
3127 // 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.
3128 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1;
3129 logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision);
3130 zero_deadzone_halfsize = (style.LogSliderDeadzone * 0.5f) / ImMax(slider_usable_sz, 1.0f);
3131 }
3132
3133 // Process interacting with the slider
3134 bool value_changed = false;
3135 if (g.ActiveId == id)
3136 {
3137 bool set_new_value = false;
3138 float clicked_t = 0.0f;
3139 if (g.ActiveIdSource == ImGuiInputSource_Mouse)
3140 {
3141 if (!g.IO.MouseDown[0])
3142 {
3143 ClearActiveID();
3144 }
3145 else
3146 {
3147 const float mouse_abs_pos = g.IO.MousePos[axis];
3148 if (g.ActiveIdIsJustActivated)
3149 {
3150 float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3151 if (axis == ImGuiAxis_Y)
3152 grab_t = 1.0f - grab_t;
3153 const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t);
3154 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.
3155 g.SliderGrabClickOffset = (clicked_around_grab && is_floating_point) ? mouse_abs_pos - grab_pos : 0.0f;
3156 }
3157 if (slider_usable_sz > 0.0f)
3158 clicked_t = ImSaturate((mouse_abs_pos - g.SliderGrabClickOffset - slider_usable_pos_min) / slider_usable_sz);
3159 if (axis == ImGuiAxis_Y)
3160 clicked_t = 1.0f - clicked_t;
3161 set_new_value = true;
3162 }
3163 }
3164 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
3165 {
3166 if (g.ActiveIdIsJustActivated)
3167 {
3168 g.SliderCurrentAccum = 0.0f; // Reset any stored nav delta upon activation
3169 g.SliderCurrentAccumDirty = false;
3170 }
3171
3172 float input_delta = (axis == ImGuiAxis_X) ? GetNavTweakPressedAmount(axis) : -GetNavTweakPressedAmount(axis);
3173 if (input_delta != 0.0f)
3174 {
3175 const bool tweak_slow = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
3176 const bool tweak_fast = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
3177 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0;
3178 if (decimal_precision > 0)
3179 {
3180 input_delta /= 100.0f; // Keyboard/Gamepad tweak speeds in % of slider bounds
3181 if (tweak_slow)
3182 input_delta /= 10.0f;
3183 }
3184 else
3185 {
3186 if ((v_range_f >= -100.0f && v_range_f <= 100.0f && v_range_f != 0.0f) || tweak_slow)
3187 input_delta = ((input_delta < 0.0f) ? -1.0f : +1.0f) / v_range_f; // Keyboard/Gamepad tweak speeds in integer steps
3188 else
3189 input_delta /= 100.0f;
3190 }
3191 if (tweak_fast)
3192 input_delta *= 10.0f;
3193
3194 g.SliderCurrentAccum += input_delta;
3195 g.SliderCurrentAccumDirty = true;
3196 }
3197
3198 float delta = g.SliderCurrentAccum;
3199 if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
3200 {
3201 ClearActiveID();
3202 }
3203 else if (g.SliderCurrentAccumDirty)
3204 {
3205 clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3206
3207 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
3208 {
3209 set_new_value = false;
3210 g.SliderCurrentAccum = 0.0f; // If pushing up against the limits, don't continue to accumulate
3211 }
3212 else
3213 {
3214 set_new_value = true;
3215 float old_clicked_t = clicked_t;
3216 clicked_t = ImSaturate(clicked_t + delta);
3217
3218 // Calculate what our "new" clicked_t will be, and thus how far we actually moved the slider, and subtract this from the accumulator
3219 TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3220 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
3221 v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
3222 float new_clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new, v_min, v_max, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3223
3224 if (delta > 0)
3225 g.SliderCurrentAccum -= ImMin(new_clicked_t - old_clicked_t, delta);
3226 else
3227 g.SliderCurrentAccum -= ImMax(new_clicked_t - old_clicked_t, delta);
3228 }
3229
3230 g.SliderCurrentAccumDirty = false;
3231 }
3232 }
3233
3234 if (set_new_value)
3235 if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
3236 set_new_value = false;
3237
3238 if (set_new_value)
3239 {
3240 TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3241
3242 // Round to user desired precision based on format string
3243 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
3244 v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
3245
3246 // Apply result
3247 if (*v != v_new)
3248 {
3249 *v = v_new;
3250 value_changed = true;
3251 }
3252 }
3253 }
3254
3255 if (slider_sz < 1.0f)
3256 {
3257 *out_grab_bb = ImRect(bb.Min, bb.Min);
3258 }
3259 else
3260 {
3261 // Output grab position so it can be displayed by the caller
3262 float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3263 if (axis == ImGuiAxis_Y)
3264 grab_t = 1.0f - grab_t;
3265 const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t);
3266 if (axis == ImGuiAxis_X)
3267 *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);
3268 else
3269 *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);
3270 }
3271
3272 return value_changed;
3273}
3274
3275// For 32-bit and larger types, slider bounds are limited to half the natural type range.
3276// 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.
3277// It would be possible to lift that limitation with some work but it doesn't seem to be worth it for sliders.
3278bool 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)
3279{
3280 // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
3281 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.");
3282 IM_ASSERT((flags & ImGuiSliderFlags_WrapAround) == 0); // Not supported by SliderXXX(), only by DragXXX()
3283
3284 switch (data_type)
3285 {
3286 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; }
3287 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; }
3288 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; }
3289 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; }
3290 case ImGuiDataType_S32:
3291 IM_ASSERT(*(const ImS32*)p_min >= IM_S32_MIN / 2 && *(const ImS32*)p_max <= IM_S32_MAX / 2);
3292 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);
3293 case ImGuiDataType_U32:
3294 IM_ASSERT(*(const ImU32*)p_max <= IM_U32_MAX / 2);
3295 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);
3296 case ImGuiDataType_S64:
3297 IM_ASSERT(*(const ImS64*)p_min >= IM_S64_MIN / 2 && *(const ImS64*)p_max <= IM_S64_MAX / 2);
3298 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);
3299 case ImGuiDataType_U64:
3300 IM_ASSERT(*(const ImU64*)p_max <= IM_U64_MAX / 2);
3301 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);
3302 case ImGuiDataType_Float:
3303 IM_ASSERT(*(const float*)p_min >= -FLT_MAX / 2.0f && *(const float*)p_max <= FLT_MAX / 2.0f);
3304 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);
3305 case ImGuiDataType_Double:
3306 IM_ASSERT(*(const double*)p_min >= -DBL_MAX / 2.0f && *(const double*)p_max <= DBL_MAX / 2.0f);
3307 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);
3308 case ImGuiDataType_COUNT: break;
3309 }
3310 IM_ASSERT(0);
3311 return false;
3312}
3313
3314// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a slider, they are all required.
3315// Read code of e.g. SliderFloat(), SliderInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3316bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags)
3317{
3318 ImGuiWindow* window = GetCurrentWindow();
3319 if (window->SkipItems)
3320 return false;
3321
3322 ImGuiContext& g = *GImGui;
3323 const ImGuiStyle& style = g.Style;
3324 const ImGuiID id = window->GetID(label);
3325 const float w = CalcItemWidth();
3326 const ImU32 color_marker = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasColorMarker) ? g.NextItemData.ColorMarker : 0;
3327
3328 const char* label_end = FindRenderedTextEnd(label);
3329 const ImVec2 label_size = CalcTextSize(label, label_end, false);
3330 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
3331 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));
3332
3333 const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
3334 ItemSize(total_bb, style.FramePadding.y);
3335 if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
3336 return false;
3337
3338 // Default format string when passing NULL
3339 if (format == NULL)
3340 format = DataTypeGetInfo(data_type)->PrintFmt;
3341
3342 const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags);
3343 bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
3344 if (!temp_input_is_active)
3345 {
3346 // Tabbing or Ctrl+Click on Slider turns it into an input box
3347 const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id);
3348 const bool make_active = (clicked || g.NavActivateId == id);
3349 if (make_active && clicked)
3350 SetKeyOwner(ImGuiKey_MouseLeft, id);
3351 if (make_active && temp_input_allowed)
3352 if ((clicked && g.IO.KeyCtrl) || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
3353 temp_input_is_active = true;
3354
3355 // Store initial value (not used by main lib but available as a convenience but some mods e.g. to revert)
3356 if (make_active)
3357 memcpy(&g.ActiveIdValueOnActivation, p_data, DataTypeGetInfo(data_type)->Size);
3358
3359 if (make_active && !temp_input_is_active)
3360 {
3361 SetActiveID(id, window);
3362 SetFocusID(id, window);
3363 FocusWindow(window);
3364 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
3365 }
3366 }
3367
3368 if (temp_input_is_active)
3369 {
3370 // Only clamp Ctrl+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp)
3371 const bool clamp_enabled = (flags & ImGuiSliderFlags_ClampOnInput) != 0; // Don't use TempInputIsClampEnabled()
3372 return TempInputScalar(frame_bb, id, label, data_type, p_data, format, clamp_enabled ? p_min : NULL, clamp_enabled ? p_max : NULL);
3373 }
3374
3375 // Draw frame
3376 const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3377 RenderNavCursor(frame_bb, id);
3378 RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, false, style.FrameRounding);
3379 if (color_marker != 0 && style.ColorMarkerSize > 0.0f)
3380 RenderColorComponentMarker(frame_bb, GetColorU32(color_marker), style.FrameRounding);
3381 RenderFrameBorder(frame_bb.Min, frame_bb.Max, g.Style.FrameRounding);
3382
3383 // Slider behavior
3384 ImRect grab_bb;
3385 const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags, &grab_bb);
3386 if (value_changed)
3387 MarkItemEdited(id);
3388
3389 // Render grab
3390 if (grab_bb.Max.x > grab_bb.Min.x)
3391 window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding);
3392
3393 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3394 char value_buf[64];
3395 const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_COUNTOF(value_buf), data_type, p_data, format);
3396 if (g.LogEnabled)
3397 LogSetNextTextDecoration("{", "}");
3398 RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f));
3399
3400 if (label_size.x > 0.0f)
3401 RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label, label_end, false);
3402
3403 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
3404 return value_changed;
3405}
3406
3407// Add multiple sliders on 1 line for compact edition of multiple components
3408bool 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)
3409{
3410 ImGuiWindow* window = GetCurrentWindow();
3411 if (window->SkipItems)
3412 return false;
3413
3414 ImGuiContext& g = *GImGui;
3415 bool value_changed = false;
3416 BeginGroup();
3417 PushID(label);
3418 PushMultiItemsWidths(components, CalcItemWidth());
3419 size_t type_size = GDataTypeInfo[data_type].Size;
3420 for (int i = 0; i < components; i++)
3421 {
3422 PushID(i);
3423 if (i > 0)
3424 SameLine(0, g.Style.ItemInnerSpacing.x);
3425 if (flags & ImGuiSliderFlags_ColorMarkers)
3426 SetNextItemColorMarker(GDefaultRgbaColorMarkers[i]);
3427 value_changed |= SliderScalar("", data_type, v, v_min, v_max, format, flags);
3428 PopID();
3429 PopItemWidth();
3430 v = (void*)((char*)v + type_size);
3431 }
3432 PopID();
3433
3434 const char* label_end = FindRenderedTextEnd(label);
3435 if (label != label_end)
3436 {
3437 SameLine(0, g.Style.ItemInnerSpacing.x);
3438 TextEx(label, label_end);
3439 }
3440
3441 EndGroup();
3442 return value_changed;
3443}
3444
3445bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3446{
3447 return SliderScalar(label, ImGuiDataType_Float, v, &v_min, &v_max, format, flags);
3448}
3449
3450bool ImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3451{
3452 return SliderScalarN(label, ImGuiDataType_Float, v, 2, &v_min, &v_max, format, flags);
3453}
3454
3455bool ImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3456{
3457 return SliderScalarN(label, ImGuiDataType_Float, v, 3, &v_min, &v_max, format, flags);
3458}
3459
3460bool ImGui::SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3461{
3462 return SliderScalarN(label, ImGuiDataType_Float, v, 4, &v_min, &v_max, format, flags);
3463}
3464
3465bool ImGui::SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max, const char* format, ImGuiSliderFlags flags)
3466{
3467 if (format == NULL)
3468 format = "%.0f deg";
3469 float v_deg = (*v_rad) * 360.0f / (2 * IM_PI);
3470 bool value_changed = SliderFloat(label, &v_deg, v_degrees_min, v_degrees_max, format, flags);
3471 if (value_changed)
3472 *v_rad = v_deg * (2 * IM_PI) / 360.0f;
3473 return value_changed;
3474}
3475
3476bool ImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3477{
3478 return SliderScalar(label, ImGuiDataType_S32, v, &v_min, &v_max, format, flags);
3479}
3480
3481bool ImGui::SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3482{
3483 return SliderScalarN(label, ImGuiDataType_S32, v, 2, &v_min, &v_max, format, flags);
3484}
3485
3486bool ImGui::SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3487{
3488 return SliderScalarN(label, ImGuiDataType_S32, v, 3, &v_min, &v_max, format, flags);
3489}
3490
3491bool ImGui::SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3492{
3493 return SliderScalarN(label, ImGuiDataType_S32, v, 4, &v_min, &v_max, format, flags);
3494}
3495
3496bool 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)
3497{
3498 ImGuiWindow* window = GetCurrentWindow();
3499 if (window->SkipItems)
3500 return false;
3501
3502 ImGuiContext& g = *GImGui;
3503 const ImGuiStyle& style = g.Style;
3504 const ImGuiID id = window->GetID(label);
3505
3506 const char* label_end = FindRenderedTextEnd(label);
3507 const ImVec2 label_size = CalcTextSize(label, label_end, false);
3508 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size);
3509 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));
3510
3511 ItemSize(bb, style.FramePadding.y);
3512 if (!ItemAdd(frame_bb, id))
3513 return false;
3514
3515 // Default format string when passing NULL
3516 if (format == NULL)
3517 format = DataTypeGetInfo(data_type)->PrintFmt;
3518
3519 const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags);
3520 const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id);
3521 if (clicked || g.NavActivateId == id)
3522 {
3523 if (clicked)
3524 SetKeyOwner(ImGuiKey_MouseLeft, id);
3525 SetActiveID(id, window);
3526 SetFocusID(id, window);
3527 FocusWindow(window);
3528 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
3529 }
3530
3531 // Draw frame
3532 const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3533 RenderNavCursor(frame_bb, id);
3534 RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, g.Style.FrameRounding);
3535
3536 // Slider behavior
3537 ImRect grab_bb;
3538 const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags | ImGuiSliderFlags_Vertical, &grab_bb);
3539 if (value_changed)
3540 MarkItemEdited(id);
3541
3542 // Render grab
3543 if (grab_bb.Max.y > grab_bb.Min.y)
3544 window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding);
3545
3546 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3547 // For the vertical slider we allow centered text to overlap the frame padding
3548 char value_buf[64];
3549 const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_COUNTOF(value_buf), data_type, p_data, format);
3550 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));
3551 if (label_size.x > 0.0f)
3552 RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label, label_end, false);
3553
3554 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
3555 return value_changed;
3556}
3557
3558bool ImGui::VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3559{
3560 return VSliderScalar(label, size, ImGuiDataType_Float, v, &v_min, &v_max, format, flags);
3561}
3562
3563bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3564{
3565 return VSliderScalar(label, size, ImGuiDataType_S32, v, &v_min, &v_max, format, flags);
3566}
3567
3568//-------------------------------------------------------------------------
3569// [SECTION] Widgets: InputScalar, InputFloat, InputInt, etc.
3570//-------------------------------------------------------------------------
3571// - ImParseFormatFindStart() [Internal]
3572// - ImParseFormatFindEnd() [Internal]
3573// - ImParseFormatTrimDecorations() [Internal]
3574// - ImParseFormatSanitizeForPrinting() [Internal]
3575// - ImParseFormatSanitizeForScanning() [Internal]
3576// - ImParseFormatPrecision() [Internal]
3577// - TempInputText() [Internal]
3578// - TempInputScalar() [Internal]
3579// - InputScalar()
3580// - InputScalarN()
3581// - InputFloat()
3582// - InputFloat2()
3583// - InputFloat3()
3584// - InputFloat4()
3585// - InputInt()
3586// - InputInt2()
3587// - InputInt3()
3588// - InputInt4()
3589// - InputDouble()
3590//-------------------------------------------------------------------------
3591
3592// We don't use strchr() because our strings are usually very short and often start with '%'
3593const char* ImParseFormatFindStart(const char* fmt)
3594{
3595 while (char c = fmt[0])
3596 {
3597 if (c == '%' && fmt[1] != '%')
3598 return fmt;
3599 else if (c == '%')
3600 fmt++;
3601 fmt++;
3602 }
3603 return fmt;
3604}
3605
3606const char* ImParseFormatFindEnd(const char* fmt)
3607{
3608 // Printf/scanf types modifiers: I/L/h/j/l/t/w/z. Other uppercase letters qualify as types aka end of the format.
3609 if (fmt[0] != '%')
3610 return fmt;
3611 const unsigned int ignored_uppercase_mask = (1 << ('I'-'A')) | (1 << ('L'-'A'));
3612 const unsigned int ignored_lowercase_mask = (1 << ('h'-'a')) | (1 << ('j'-'a')) | (1 << ('l'-'a')) | (1 << ('t'-'a')) | (1 << ('w'-'a')) | (1 << ('z'-'a'));
3613 for (char c; (c = *fmt) != 0; fmt++)
3614 {
3615 if (c >= 'A' && c <= 'Z' && ((1 << (c - 'A')) & ignored_uppercase_mask) == 0)
3616 return fmt + 1;
3617 if (c >= 'a' && c <= 'z' && ((1 << (c - 'a')) & ignored_lowercase_mask) == 0)
3618 return fmt + 1;
3619 }
3620 return fmt;
3621}
3622
3623// Extract the format out of a format string with leading or trailing decorations
3624// fmt = "blah blah" -> return ""
3625// fmt = "%.3f" -> return fmt
3626// fmt = "hello %.3f" -> return fmt + 6
3627// fmt = "%.3f hello" -> return buf written with "%.3f"
3628const char* ImParseFormatTrimDecorations(const char* fmt, char* buf, size_t buf_size)
3629{
3630 const char* fmt_start = ImParseFormatFindStart(fmt);
3631 if (fmt_start[0] != '%')
3632 return "";
3633 const char* fmt_end = ImParseFormatFindEnd(fmt_start);
3634 if (fmt_end[0] == 0) // If we only have leading decoration, we don't need to copy the data.
3635 return fmt_start;
3636 ImStrncpy(buf, fmt_start, ImMin((size_t)(fmt_end - fmt_start) + 1, buf_size));
3637 return buf;
3638}
3639
3640// Sanitize format
3641// - Zero terminate so extra characters after format (e.g. "%f123") don't confuse atof/atoi
3642// - stb_sprintf.h supports several new modifiers which format numbers in a way that also makes them incompatible atof/atoi.
3643void ImParseFormatSanitizeForPrinting(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3644{
3645 const char* fmt_end = ImParseFormatFindEnd(fmt_in);
3646 IM_UNUSED(fmt_out_size);
3647 IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3648 while (fmt_in < fmt_end)
3649 {
3650 char c = *fmt_in++;
3651 if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3652 *(fmt_out++) = c;
3653 }
3654 *fmt_out = 0; // Zero-terminate
3655}
3656
3657// - 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"
3658const char* ImParseFormatSanitizeForScanning(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3659{
3660 const char* fmt_end = ImParseFormatFindEnd(fmt_in);
3661 const char* fmt_out_begin = fmt_out;
3662 IM_UNUSED(fmt_out_size);
3663 IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3664 bool has_type = false;
3665 while (fmt_in < fmt_end)
3666 {
3667 char c = *fmt_in++;
3668 if (!has_type && ((c >= '0' && c <= '9') || c == '.' || c == '+' || c == '#'))
3669 continue;
3670 has_type |= ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')); // Stop skipping digits
3671 if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3672 *(fmt_out++) = c;
3673 }
3674 *fmt_out = 0; // Zero-terminate
3675 return fmt_out_begin;
3676}
3677
3678template<typename TYPE>
3679static const char* ImAtoi(const char* src, TYPE* output)
3680{
3681 int negative = 0;
3682 if (*src == '-') { negative = 1; src++; }
3683 if (*src == '+') { src++; }
3684 TYPE v = 0;
3685 while (*src >= '0' && *src <= '9')
3686 v = (v * 10) + (*src++ - '0');
3687 *output = negative ? -v : v;
3688 return src;
3689}
3690
3691// Parse display precision back from the display format string
3692// 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.
3693int ImParseFormatPrecision(const char* fmt, int default_precision)
3694{
3695 fmt = ImParseFormatFindStart(fmt);
3696 if (fmt[0] != '%')
3697 return default_precision;
3698 fmt++;
3699 while (*fmt >= '0' && *fmt <= '9')
3700 fmt++;
3701 int precision = INT_MAX;
3702 if (*fmt == '.')
3703 {
3704 fmt = ImAtoi<int>(fmt + 1, &precision);
3705 if (precision < 0 || precision > 99)
3706 precision = default_precision;
3707 }
3708 if (*fmt == 'e' || *fmt == 'E') // Maximum precision with scientific notation
3709 precision = -1;
3710 if ((*fmt == 'g' || *fmt == 'G') && precision == INT_MAX)
3711 precision = -1;
3712 return (precision == INT_MAX) ? default_precision : precision;
3713}
3714
3715// Create text input in place of another active widget (e.g. used when doing a Ctrl+Click on drag/slider widgets)
3716// - This must be submitted right after the item it is overlaying.
3717// FIXME: Facilitate using this in variety of other situations.
3718bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3719{
3720 // On the first frame, g.TempInputTextId == 0, then on subsequent frames it becomes == id.
3721 // We clear ActiveID on the first frame to allow the InputText() taking it back.
3722 ImGuiContext& g = *GImGui;
3723 ImGuiWindow* window = g.CurrentWindow;
3724
3725 const bool is_deactivated = (g.InputTextDeactivatedState.ID == id);
3726 const bool is_active = (g.TempInputId == id);
3727 if (!is_active && !is_deactivated)
3728 ClearActiveID();
3729
3730 ImVec2 backup_pos = window->DC.CursorPos;
3731 window->DC.CursorPos = bb.Min;
3732 g.LastItemData.ItemFlags |= ImGuiItemFlags_AllowDuplicateId; // Using ImGuiInputTextFlags_MergedItem above will skip ItemAdd() so we poke here.
3733 bool value_changed = InputTextEx(label, NULL, buf, (int)buf_size, bb.GetSize(), flags | ImGuiInputTextFlags_TempInput | ImGuiInputTextFlags_AutoSelectAll, callback, user_data);
3734 KeepAliveID(id); // Not done because of ImGuiInputTextFlags_TempInput
3735 if (!is_active && !is_deactivated)
3736 {
3737 // First frame we started displaying the InputText widget, we expect it to take the active id.
3738 IM_ASSERT(g.ActiveId == id);
3739 g.TempInputId = g.ActiveId;
3740 }
3741 if (is_active && g.ActiveId != id)
3742 g.TempInputId = 0;
3743 window->DC.CursorPos = backup_pos;
3744 return value_changed;
3745}
3746
3747// Note that Drag/Slider functions are only forwarding the min/max values clamping values if the ImGuiSliderFlags_AlwaysClamp flag is set!
3748// This is intended: this way we allow Ctrl+Click manual input to set a value out of bounds, for maximum flexibility.
3749// However this may not be ideal for all uses, as some user code may break on out of bound values.
3750bool 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)
3751{
3752 // FIXME: May need to clarify display behavior if format doesn't contain %.
3753 // "%d" -> "%d" / "There are %d items" -> "%d" / "items" -> "%d" (fallback). Also see #6405
3754 ImGuiContext& g = *GImGui;
3755 const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
3756 char fmt_buf[32];
3757 char data_buf[32];
3758 format = ImParseFormatTrimDecorations(format, fmt_buf, IM_COUNTOF(fmt_buf));
3759 if (format[0] == 0)
3760 format = type_info->PrintFmt;
3761 DataTypeFormatString(data_buf, IM_COUNTOF(data_buf), data_type, p_data, format);
3762 ImStrTrimBlanks(data_buf);
3763
3764 ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
3765 g.LastItemData.ItemFlags |= ImGuiItemFlags_NoMarkEdited; // Because TempInputText() uses ImGuiInputTextFlags_MergedItem it doesn't submit a new item, so we poke LastItemData.
3766 g.LastItemData.ItemFlags = (g.LastItemData.ItemFlags & ImGuiItemFlags_LiveEditOnInputScalar) ? (g.LastItemData.ItemFlags | ImGuiItemFlags_LiveEditOnInputText) : (g.LastItemData.ItemFlags & ~ImGuiItemFlags_LiveEditOnInputText);
3767 if (!TempInputText(bb, id, label, data_buf, IM_COUNTOF(data_buf), flags))
3768 return false;
3769
3770 // Backup old value
3771 size_t data_type_size = type_info->Size;
3772 ImGuiDataTypeStorage data_backup;
3773 memcpy(&data_backup, p_data, data_type_size);
3774
3775 // Apply new value (or operations) then clamp
3776 DataTypeApplyFromText(data_buf, data_type, p_data, format, NULL);
3777 if (p_clamp_min || p_clamp_max)
3778 {
3779 if (p_clamp_min && p_clamp_max && DataTypeCompare(data_type, p_clamp_min, p_clamp_max) > 0)
3780 ImSwap(p_clamp_min, p_clamp_max);
3781 DataTypeClamp(data_type, p_data, p_clamp_min, p_clamp_max);
3782 }
3783
3784 // Only mark as edited if new value is different
3785 g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited;
3786 bool value_changed = memcmp(&data_backup, p_data, data_type_size) != 0;
3787 if (value_changed)
3788 MarkItemEdited(id);
3789 return value_changed;
3790}
3791
3792void ImGui::SetNextItemRefVal(ImGuiDataType data_type, void* p_data)
3793{
3794 ImGuiContext& g = *GImGui;
3795 g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasRefVal;
3796 memcpy(&g.NextItemData.RefVal, p_data, DataTypeGetInfo(data_type)->Size);
3797}
3798
3799// 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.
3800// Read code of e.g. InputFloat(), InputInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3801bool 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)
3802{
3803 ImGuiWindow* window = GetCurrentWindow();
3804 if (window->SkipItems)
3805 return false;
3806
3807 ImGuiContext& g = *GImGui;
3808 ImGuiStyle& style = g.Style;
3809 //IM_ASSERT((flags & ImGuiInputTextFlags_EnterReturnsTrue) == 0); // Not supported by InputScalar(). Please open an issue if you this would be useful to you. Otherwise use IsItemDeactivatedAfterEdit()!
3810
3811 if (format == NULL)
3812 format = DataTypeGetInfo(data_type)->PrintFmt;
3813
3814 void* p_data_default = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasRefVal) ? &g.NextItemData.RefVal : &g.DataTypeZeroValue;
3815
3816 char buf[64];
3817 if ((flags & ImGuiInputTextFlags_DisplayEmptyRefVal) && DataTypeCompare(data_type, p_data, p_data_default) == 0)
3818 buf[0] = 0;
3819 else
3820 DataTypeFormatString(buf, IM_COUNTOF(buf), data_type, p_data, format);
3821
3822 // Disable the MarkItemEdited() call in InputText but keep ImGuiItemStatusFlags_Edited.
3823 // We call MarkItemEdited() ourselves by comparing the actual data rather than the string.
3824 g.NextItemData.ItemFlagsSet |= ImGuiItemFlags_NoMarkEdited;
3825 flags |= ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
3826
3827 const bool has_step_buttons = (p_step != NULL);
3828 const float button_size = has_step_buttons ? GetFrameHeight() : 0.0f;
3829 bool ret;
3830 if (has_step_buttons)
3831 {
3832 // With Step Buttons
3833 BeginGroup(); // The only purpose of the group here is to allow the caller to query item data e.g. IsItemActive()
3834 PushID(label);
3835 SetNextItemWidth(ImMax(1.0f, CalcItemWidth() - (button_size + style.ItemInnerSpacing.x) * 2));
3836 ret = InputText("", buf, IM_COUNTOF(buf), flags); // PushID(label) + "" gives us the expected ID from outside point of view
3837 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
3838 }
3839 else
3840 {
3841 // Without Step Buttons
3842 ret = InputText(label, buf, IM_COUNTOF(buf), flags);
3843 }
3844
3845 // Apply
3846 bool value_changed = false;
3847 if (g.LastItemData.ItemFlags & ImGuiItemFlags_LiveEditOnInputScalar)
3848 {
3849 bool input_edited = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_EditedInternal) != 0; // We would be using 'ret' if ImGuiInputTextFlags_EnterReturnsTrue was not involved.
3850 if (input_edited)
3851 value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL);
3852 }
3853 else
3854 {
3855 //g.LastItemData.StatusFlags &= ~ImGuiItemStatusFlags_Edited;
3856 if (g.DeactivatedItemData.ID == g.LastItemData.ID)
3857 {
3858 //g.DeactivatedItemData.HasBeenEditedBefore = false; // Will be set below by MarkItemEdited()
3859 //if (IsItemDeactivated()) // Should be unnecessary
3860 value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL);
3861 }
3862 }
3863
3864 // Step buttons
3865 if (has_step_buttons)
3866 {
3867 const ImVec2 backup_frame_padding = style.FramePadding;
3868 style.FramePadding.x = style.FramePadding.y;
3869 if (flags & ImGuiInputTextFlags_ReadOnly)
3870 BeginDisabled();
3871 PushItemFlag(ImGuiItemFlags_ButtonRepeat, true);
3872 SameLine(0, style.ItemInnerSpacing.x);
3873 if (ButtonEx("-", ImVec2(button_size, button_size)))
3874 {
3875 DataTypeApplyOp(data_type, '-', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3876 value_changed = ret = true;
3877 }
3878 SameLine(0, style.ItemInnerSpacing.x);
3879 if (ButtonEx("+", ImVec2(button_size, button_size)))
3880 {
3881 DataTypeApplyOp(data_type, '+', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3882 value_changed = ret = true;
3883 }
3884 PopItemFlag();
3885 if (flags & ImGuiInputTextFlags_ReadOnly)
3886 EndDisabled();
3887
3888 const char* label_end = FindRenderedTextEnd(label);
3889 if (label != label_end)
3890 {
3891 SameLine(0, style.ItemInnerSpacing.x);
3892 TextEx(label, label_end);
3893 }
3894 style.FramePadding = backup_frame_padding;
3895
3896 PopID();
3897 EndGroup();
3898 }
3899
3900 g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited;
3901 if (value_changed)
3902 MarkItemEdited(g.LastItemData.ID);
3903
3904 if (flags & ImGuiInputTextFlags_EnterReturnsTrue)
3905 return ret;
3906 return value_changed;
3907}
3908
3909bool 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)
3910{
3911 ImGuiWindow* window = GetCurrentWindow();
3912 if (window->SkipItems)
3913 return false;
3914
3915 ImGuiContext& g = *GImGui;
3916 bool value_changed = false;
3917 BeginGroup();
3918 PushID(label);
3919 PushMultiItemsWidths(components, CalcItemWidth());
3920 size_t type_size = GDataTypeInfo[data_type].Size;
3921 for (int i = 0; i < components; i++)
3922 {
3923 PushID(i);
3924 if (i > 0)
3925 SameLine(0, g.Style.ItemInnerSpacing.x);
3926 value_changed |= InputScalar("", data_type, p_data, p_step, p_step_fast, format, flags);
3927 PopID();
3928 PopItemWidth();
3929 p_data = (void*)((char*)p_data + type_size);
3930 }
3931 PopID();
3932
3933 const char* label_end = FindRenderedTextEnd(label);
3934 if (label != label_end)
3935 {
3936 SameLine(0.0f, g.Style.ItemInnerSpacing.x);
3937 TextEx(label, label_end);
3938 }
3939
3940 EndGroup();
3941 return value_changed;
3942}
3943
3944bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, const char* format, ImGuiInputTextFlags flags)
3945{
3946 return InputScalar(label, ImGuiDataType_Float, (void*)v, (void*)(step > 0.0f ? &step : NULL), (void*)(step_fast > 0.0f ? &step_fast : NULL), format, flags);
3947}
3948
3949bool ImGui::InputFloat2(const char* label, float v[2], const char* format, ImGuiInputTextFlags flags)
3950{
3951 return InputScalarN(label, ImGuiDataType_Float, v, 2, NULL, NULL, format, flags);
3952}
3953
3954bool ImGui::InputFloat3(const char* label, float v[3], const char* format, ImGuiInputTextFlags flags)
3955{
3956 return InputScalarN(label, ImGuiDataType_Float, v, 3, NULL, NULL, format, flags);
3957}
3958
3959bool ImGui::InputFloat4(const char* label, float v[4], const char* format, ImGuiInputTextFlags flags)
3960{
3961 return InputScalarN(label, ImGuiDataType_Float, v, 4, NULL, NULL, format, flags);
3962}
3963
3964bool ImGui::InputInt(const char* label, int* v, int step, int step_fast, ImGuiInputTextFlags flags)
3965{
3966 // 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.
3967 const char* format = (flags & ImGuiInputTextFlags_CharsHexadecimal) ? "%08X" : "%d";
3968 return InputScalar(label, ImGuiDataType_S32, (void*)v, (void*)(step > 0 ? &step : NULL), (void*)(step_fast > 0 ? &step_fast : NULL), format, flags);
3969}
3970
3971bool ImGui::InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags)
3972{
3973 return InputScalarN(label, ImGuiDataType_S32, v, 2, NULL, NULL, "%d", flags);
3974}
3975
3976bool ImGui::InputInt3(const char* label, int v[3], ImGuiInputTextFlags flags)
3977{
3978 return InputScalarN(label, ImGuiDataType_S32, v, 3, NULL, NULL, "%d", flags);
3979}
3980
3981bool ImGui::InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags)
3982{
3983 return InputScalarN(label, ImGuiDataType_S32, v, 4, NULL, NULL, "%d", flags);
3984}
3985
3986bool ImGui::InputDouble(const char* label, double* v, double step, double step_fast, const char* format, ImGuiInputTextFlags flags)
3987{
3988 return InputScalar(label, ImGuiDataType_Double, (void*)v, (void*)(step > 0.0 ? &step : NULL), (void*)(step_fast > 0.0 ? &step_fast : NULL), format, flags);
3989}
3990
3991//-------------------------------------------------------------------------
3992// [SECTION] Widgets: InputText, InputTextMultiline, InputTextWithHint
3993//-------------------------------------------------------------------------
3994// - imstb_textedit.h include
3995// - InputText()
3996// - InputTextWithHint()
3997// - InputTextMultiline()
3998// - InputTextEx() [Internal]
3999// - DebugNodeInputTextState() [Internal]
4000//-------------------------------------------------------------------------
4001
4002namespace ImStb
4003{
4004#include "imstb_textedit.h"
4005}
4006
4007// If you want to use InputText() with std::string or any custom dynamic string type, use the wrapper in misc/cpp/imgui_stdlib.h/.cpp!
4008bool ImGui::InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
4009{
4010 IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline()
4011 return InputTextEx(label, NULL, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data);
4012}
4013
4014bool ImGui::InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
4015{
4016 return InputTextEx(label, NULL, buf, (int)buf_size, size, flags | ImGuiInputTextFlags_Multiline, callback, user_data);
4017}
4018
4019bool ImGui::InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
4020{
4021 IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() or InputTextEx() manually if you need multi-line + hint.
4022 return InputTextEx(label, hint, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data);
4023}
4024
4025static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining, ImVec2* out_offset, ImDrawTextFlags flags)
4026{
4027 ImGuiContext& g = *ctx;
4028 ImGuiInputTextState* obj = &g.InputTextState;
4029 IM_ASSERT(text_end_display >= text_begin && text_end_display <= text_end);
4030 return ImFontCalcTextSizeEx(g.Font, g.FontSize, FLT_MAX, obj->WrapWidth, text_begin, text_end_display, text_end, out_remaining, out_offset, flags);
4031}
4032
4033// 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)
4034// With our UTF-8 use of stb_textedit:
4035// - STB_TEXTEDIT_GETCHAR is nothing more than a a "GETBYTE". It's only used to compare to ascii or to copy blocks of text so we are fine.
4036// - One exception is the STB_TEXTEDIT_IS_SPACE feature which would expect a full char in order to handle full-width space such as 0x3000 (see ImCharIsBlankW).
4037// - ...but we don't use that feature.
4038namespace ImStb
4039{
4040static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->TextLen; }
4041static char STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { IM_ASSERT(idx >= 0 && idx <= obj->TextLen); return obj->TextSrc[idx]; }
4042static float STB_TEXTEDIT_GETWIDTH(ImGuiInputTextState* obj, int line_start_idx, int char_idx) { unsigned int c; ImTextCharFromUtf8(&c, obj->TextSrc + line_start_idx + char_idx, obj->TextSrc + obj->TextLen); if ((ImWchar)c == '\n') return IMSTB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *obj->Ctx; return g.FontBaked->GetCharAdvance((ImWchar)c) * g.FontBakedScale; }
4043static char STB_TEXTEDIT_NEWLINE = '\n';
4044static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx)
4045{
4046 const char* text = obj->TextSrc;
4047 const char* text_remaining = NULL;
4048 const ImVec2 size = InputTextCalcTextSize(obj->Ctx, text + line_start_idx, text + obj->TextLen, text + obj->TextLen, &text_remaining, NULL, ImDrawTextFlags_StopOnNewLine | ImDrawTextFlags_WrapKeepBlanks);
4049 r->x0 = 0.0f;
4050 r->x1 = size.x;
4051 r->baseline_y_delta = size.y;
4052 r->ymin = 0.0f;
4053 r->ymax = size.y;
4054 r->num_chars = (int)(text_remaining - (text + line_start_idx));
4055}
4056
4057#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL
4058#define IMSTB_TEXTEDIT_GETPREVCHARINDEX IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL
4059
4060static int IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx)
4061{
4062 if (idx >= obj->TextLen)
4063 return obj->TextLen + 1;
4064 unsigned int c;
4065 return idx + ImTextCharFromUtf8(&c, obj->TextSrc + idx, obj->TextSrc + obj->TextLen);
4066}
4067
4068static int IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx)
4069{
4070 if (idx <= 0)
4071 return -1;
4072 const char* p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, obj->TextSrc + idx);
4073 return (int)(p - obj->TextSrc);
4074}
4075
4076static bool ImCharIsSeparatorW(unsigned int c)
4077{
4078 static const unsigned int separator_list[] =
4079 {
4080 ',', 0x3001, '.', 0x3002, ';', 0xFF1B, '(', 0xFF08, ')', 0xFF09, '{', 0xFF5B, '}', 0xFF5D,
4081 '[', 0x300C, ']', 0x300D, '|', 0xFF5C, '!', 0xFF01, '\\', 0xFFE5, '/', 0x30FB, 0xFF0F,
4082 '\n', '\r',
4083 };
4084 for (unsigned int separator : separator_list)
4085 if (c == separator)
4086 return true;
4087 return false;
4088}
4089
4090static int is_word_boundary_from_right(ImGuiInputTextState* obj, int idx)
4091{
4092 // 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.
4093 if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
4094 return 0;
4095
4096 const char* curr_p = obj->TextSrc + idx;
4097 const char* prev_p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, curr_p);
4098 unsigned int curr_c; ImTextCharFromUtf8(&curr_c, curr_p, obj->TextSrc + obj->TextLen);
4099 unsigned int prev_c; ImTextCharFromUtf8(&prev_c, prev_p, obj->TextSrc + obj->TextLen);
4100
4101 bool prev_white = ImCharIsBlankW(prev_c);
4102 bool prev_separ = ImCharIsSeparatorW(prev_c);
4103 bool curr_white = ImCharIsBlankW(curr_c);
4104 bool curr_separ = ImCharIsSeparatorW(curr_c);
4105 return ((prev_white || prev_separ) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ);
4106}
4107static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx)
4108{
4109 if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
4110 return 0;
4111
4112 const char* curr_p = obj->TextSrc + idx;
4113 const char* prev_p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, curr_p);
4114 unsigned int prev_c; ImTextCharFromUtf8(&prev_c, curr_p, obj->TextSrc + obj->TextLen);
4115 unsigned int curr_c; ImTextCharFromUtf8(&curr_c, prev_p, obj->TextSrc + obj->TextLen);
4116
4117 bool prev_white = ImCharIsBlankW(prev_c);
4118 bool prev_separ = ImCharIsSeparatorW(prev_c);
4119 bool curr_white = ImCharIsBlankW(curr_c);
4120 bool curr_separ = ImCharIsSeparatorW(curr_c);
4121 return ((prev_white) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ);
4122}
4123static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(ImGuiInputTextState* obj, int idx)
4124{
4125 idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx);
4126 while (idx >= 0 && !is_word_boundary_from_right(obj, idx))
4127 idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx);
4128 return idx < 0 ? 0 : idx;
4129}
4130static int STB_TEXTEDIT_MOVEWORDRIGHT_MAC(ImGuiInputTextState* obj, int idx)
4131{
4132 int len = obj->TextLen;
4133 idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4134 while (idx < len && !is_word_boundary_from_left(obj, idx))
4135 idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4136 return idx > len ? len : idx;
4137}
4138static int STB_TEXTEDIT_MOVEWORDRIGHT_WIN(ImGuiInputTextState* obj, int idx)
4139{
4140 idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4141 int len = obj->TextLen;
4142 while (idx < len && !is_word_boundary_from_right(obj, idx))
4143 idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4144 return idx > len ? len : idx;
4145}
4146static 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); }
4147#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h
4148#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL
4149
4150// Reimplementation of stb_textedit_move_line_start()/stb_textedit_move_line_end() which supports word-wrapping.
4151static int STB_TEXTEDIT_MOVELINESTART_IMPL(ImGuiInputTextState* obj, ImStb::STB_TexteditState* state, int cursor)
4152{
4153 if (state->single_line)
4154 return 0;
4155
4156 if (obj->WrapWidth > 0.0f)
4157 {
4158 ImGuiContext& g = *obj->Ctx;
4159 const char* p_cursor = obj->TextSrc + cursor;
4160 const char* p_bol = ImStrbol(p_cursor, obj->TextSrc);
4161 const char* p = p_bol;
4162 const char* text_end = obj->TextSrc + obj->TextLen; // End of line would be enough
4163 while (p >= p_bol)
4164 {
4165 const char* p_eol = ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, p, text_end, obj->WrapWidth, ImDrawTextFlags_WrapKeepBlanks);
4166 if (p == p_cursor) // If we are already on a visible beginning-of-line, return real beginning-of-line (would be same as regular handler below)
4167 return (int)(p_bol - obj->TextSrc);
4168 if (p_eol == p_cursor && obj->TextA[cursor] != '\n' && obj->LastMoveDirectionLR == ImGuiDir_Left)
4169 return (int)(p_bol - obj->TextSrc);
4170 if (p_eol >= p_cursor)
4171 return (int)(p - obj->TextSrc);
4172 p = (*p_eol == '\n') ? p_eol + 1 : p_eol;
4173 }
4174 }
4175
4176 // Regular handler, same as stb_textedit_move_line_start()
4177 while (cursor > 0)
4178 {
4179 int prev_cursor = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, cursor);
4180 if (STB_TEXTEDIT_GETCHAR(obj, prev_cursor) == STB_TEXTEDIT_NEWLINE)
4181 break;
4182 cursor = prev_cursor;
4183 }
4184 return cursor;
4185}
4186
4187static int STB_TEXTEDIT_MOVELINEEND_IMPL(ImGuiInputTextState* obj, ImStb::STB_TexteditState* state, int cursor)
4188{
4189 int n = STB_TEXTEDIT_STRINGLEN(obj);
4190 if (state->single_line)
4191 return n;
4192
4193 if (obj->WrapWidth > 0.0f)
4194 {
4195 ImGuiContext& g = *obj->Ctx;
4196 const char* p_cursor = obj->TextSrc + cursor;
4197 const char* p = ImStrbol(p_cursor, obj->TextSrc);
4198 const char* text_end = obj->TextSrc + obj->TextLen; // End of line would be enough
4199 while (p < text_end)
4200 {
4201 const char* p_eol = ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, p, text_end, obj->WrapWidth, ImDrawTextFlags_WrapKeepBlanks);
4202 cursor = (int)(p_eol - obj->TextSrc);
4203 if (p_eol == p_cursor && obj->LastMoveDirectionLR != ImGuiDir_Left) // If we are already on a visible end-of-line, switch to regular handle
4204 break;
4205 if (p_eol > p_cursor)
4206 return cursor;
4207 p = (*p_eol == '\n') ? p_eol + 1 : p_eol;
4208 }
4209 }
4210 // Regular handler, same as stb_textedit_move_line_end()
4211 while (cursor < n && STB_TEXTEDIT_GETCHAR(obj, cursor) != STB_TEXTEDIT_NEWLINE)
4212 cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, cursor);
4213 return cursor;
4214}
4215
4216#define STB_TEXTEDIT_MOVELINESTART STB_TEXTEDIT_MOVELINESTART_IMPL
4217#define STB_TEXTEDIT_MOVELINEEND STB_TEXTEDIT_MOVELINEEND_IMPL
4218
4219static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n)
4220{
4221 // Offset remaining text (+ copy zero terminator)
4222 IM_ASSERT(obj->TextSrc == obj->TextA.Data);
4223 char* dst = obj->TextA.Data + pos;
4224 char* src = obj->TextA.Data + pos + n;
4225 memmove(dst, src, obj->TextLen - n - pos + 1);
4226 obj->EditedBefore = obj->EditedThisFrame = true;
4227 obj->TextLen -= n;
4228}
4229
4230static int STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const char* new_text, int new_text_len)
4231{
4232 const bool is_resizable = (obj->Flags & ImGuiInputTextFlags_CallbackResize) != 0;
4233 const int text_len = obj->TextLen;
4234 IM_ASSERT(pos <= text_len);
4235
4236 // We support partial insertion (with a mod in stb_textedit.h)
4237 const int avail = obj->BufCapacity - 1 - obj->TextLen;
4238 if (!is_resizable && new_text_len > avail)
4239 new_text_len = (int)(ImTextFindValidUtf8CodepointEnd(new_text, new_text + new_text_len, new_text + avail) - new_text); // Truncate to closest UTF-8 codepoint. Alternative: return 0 to cancel insertion.
4240 if (new_text_len == 0)
4241 return 0;
4242
4243 // Grow internal buffer if needed
4244 IM_ASSERT(obj->TextSrc == obj->TextA.Data);
4245 if (text_len + new_text_len + 1 > obj->TextA.Size && is_resizable)
4246 {
4247 obj->TextA.resize(text_len + ImClamp(new_text_len, 32, ImMax(256, new_text_len)) + 1);
4248 obj->TextSrc = obj->TextA.Data;
4249 }
4250
4251 char* text = obj->TextA.Data;
4252 if (pos != text_len)
4253 memmove(text + pos + new_text_len, text + pos, (size_t)(text_len - pos));
4254 memcpy(text + pos, new_text, (size_t)new_text_len);
4255
4256 obj->EditedBefore = obj->EditedThisFrame = true;
4257 obj->TextLen += new_text_len;
4258 obj->TextA[obj->TextLen] = '\0';
4259
4260 return new_text_len;
4261}
4262
4263// 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)
4264#define STB_TEXTEDIT_K_LEFT 0x200000 // keyboard input to move cursor left
4265#define STB_TEXTEDIT_K_RIGHT 0x200001 // keyboard input to move cursor right
4266#define STB_TEXTEDIT_K_UP 0x200002 // keyboard input to move cursor up
4267#define STB_TEXTEDIT_K_DOWN 0x200003 // keyboard input to move cursor down
4268#define STB_TEXTEDIT_K_LINESTART 0x200004 // keyboard input to move cursor to start of line
4269#define STB_TEXTEDIT_K_LINEEND 0x200005 // keyboard input to move cursor to end of line
4270#define STB_TEXTEDIT_K_TEXTSTART 0x200006 // keyboard input to move cursor to start of text
4271#define STB_TEXTEDIT_K_TEXTEND 0x200007 // keyboard input to move cursor to end of text
4272#define STB_TEXTEDIT_K_DELETE 0x200008 // keyboard input to delete selection or character under cursor
4273#define STB_TEXTEDIT_K_BACKSPACE 0x200009 // keyboard input to delete selection or character left of cursor
4274#define STB_TEXTEDIT_K_UNDO 0x20000A // keyboard input to perform undo
4275#define STB_TEXTEDIT_K_REDO 0x20000B // keyboard input to perform redo
4276#define STB_TEXTEDIT_K_WORDLEFT 0x20000C // keyboard input to move cursor left one word
4277#define STB_TEXTEDIT_K_WORDRIGHT 0x20000D // keyboard input to move cursor right one word
4278#define STB_TEXTEDIT_K_PGUP 0x20000E // keyboard input to move cursor up a page
4279#define STB_TEXTEDIT_K_PGDOWN 0x20000F // keyboard input to move cursor down a page
4280#define STB_TEXTEDIT_K_SHIFT 0x400000
4281
4282#define IMSTB_TEXTEDIT_IMPLEMENTATION
4283#define IMSTB_TEXTEDIT_memmove memmove
4284#include "imstb_textedit.h"
4285
4286// stb_textedit internally allows for a single undo record to do addition and deletion, but somehow, calling
4287// the stb_textedit_paste() function creates two separate records, so we perform it manually. (FIXME: Report to nothings/stb?)
4288static void stb_textedit_replace(ImGuiInputTextState* str, STB_TexteditState* state, const IMSTB_TEXTEDIT_CHARTYPE* text, int text_len)
4289{
4290 stb_text_makeundo_replace(str, state, 0, str->TextLen, text_len);
4291 ImStb::STB_TEXTEDIT_DELETECHARS(str, 0, str->TextLen);
4292 state->cursor = state->select_start = state->select_end = 0;
4293 if (text_len <= 0)
4294 return;
4295 int text_len_inserted = ImStb::STB_TEXTEDIT_INSERTCHARS(str, 0, text, text_len);
4296 if (text_len_inserted > 0)
4297 {
4298 state->cursor = state->select_start = state->select_end = text_len;
4299 state->has_preferred_x = 0;
4300 return;
4301 }
4302 IM_ASSERT(0); // Failed to insert character, normally shouldn't happen because of how we currently use stb_textedit_replace()
4303}
4304
4305} // namespace ImStb
4306
4307// We added an extra indirection where 'Stb' is heap-allocated, in order facilitate the work of bindings generators.
4308ImGuiInputTextState::ImGuiInputTextState()
4309{
4310 memset((void*)this, 0, sizeof(*this));
4311 Stb = IM_NEW(ImStbTexteditState);
4312 memset(Stb, 0, sizeof(*Stb));
4313}
4314
4315ImGuiInputTextState::~ImGuiInputTextState()
4316{
4317 IM_DELETE(Stb);
4318}
4319
4320void ImGuiInputTextState::OnKeyPressed(int key)
4321{
4322 stb_textedit_key(this, Stb, key);
4323 CursorFollow = true;
4324 CursorAnimReset();
4325 const int key_u = (key & ~STB_TEXTEDIT_K_SHIFT);
4326 if (key_u == STB_TEXTEDIT_K_LEFT || key_u == STB_TEXTEDIT_K_LINESTART || key_u == STB_TEXTEDIT_K_TEXTSTART || key_u == STB_TEXTEDIT_K_BACKSPACE || key_u == STB_TEXTEDIT_K_WORDLEFT)
4327 LastMoveDirectionLR = ImGuiDir_Left;
4328 else if (key_u == STB_TEXTEDIT_K_RIGHT || key_u == STB_TEXTEDIT_K_LINEEND || key_u == STB_TEXTEDIT_K_TEXTEND || key_u == STB_TEXTEDIT_K_DELETE || key_u == STB_TEXTEDIT_K_WORDRIGHT)
4329 LastMoveDirectionLR = ImGuiDir_Right;
4330}
4331
4332void ImGuiInputTextState::OnCharPressed(unsigned int c)
4333{
4334 // Convert the key to a UTF8 byte sequence.
4335 // The changes we had to make to stb_textedit_key made it very much UTF-8 specific which is not too great.
4336 char utf8[5];
4337 ImTextCharToUtf8(utf8, c);
4338 stb_textedit_text(this, Stb, utf8, (int)ImStrlen(utf8));
4339 CursorFollow = true;
4340 CursorAnimReset();
4341}
4342
4343// Those functions are not inlined in imgui_internal.h, allowing us to hide ImStbTexteditState from that header.
4344void ImGuiInputTextState::CursorAnimReset() { CursorAnim = -0.30f; } // After a user-input the cursor stays on for a while without blinking
4345void ImGuiInputTextState::CursorClamp() { Stb->cursor = ImMin(Stb->cursor, TextLen); Stb->select_start = ImMin(Stb->select_start, TextLen); Stb->select_end = ImMin(Stb->select_end, TextLen); }
4346bool ImGuiInputTextState::HasSelection() const { return Stb->select_start != Stb->select_end; }
4347void ImGuiInputTextState::ClearSelection() { Stb->select_start = Stb->select_end = Stb->cursor; }
4348int ImGuiInputTextState::GetCursorPos() const { return Stb->cursor; }
4349int ImGuiInputTextState::GetSelectionStart() const { return Stb->select_start; }
4350int ImGuiInputTextState::GetSelectionEnd() const { return Stb->select_end; }
4351void ImGuiInputTextState::SetSelection(int start, int end) { Stb->select_start = start; Stb->cursor = Stb->select_end = end; }
4352float ImGuiInputTextState::GetPreferredOffsetX() const { return Stb->has_preferred_x ? Stb->preferred_x : -1; }
4353void ImGuiInputTextState::SelectAll() { Stb->select_start = 0; Stb->cursor = Stb->select_end = TextLen; Stb->has_preferred_x = 0; }
4354void ImGuiInputTextState::ReloadUserBufAndSelectAll() { WantReloadUserBuf = true; ReloadSelectionStart = 0; ReloadSelectionEnd = INT_MAX; }
4355void ImGuiInputTextState::ReloadUserBufAndKeepSelection() { WantReloadUserBuf = true; ReloadSelectionStart = Stb->select_start; ReloadSelectionEnd = Stb->select_end; }
4356void ImGuiInputTextState::ReloadUserBufAndMoveToEnd() { WantReloadUserBuf = true; ReloadSelectionStart = ReloadSelectionEnd = INT_MAX; }
4357
4358ImGuiInputTextCallbackData::ImGuiInputTextCallbackData()
4359{
4360 memset((void*)this, 0, sizeof(*this));
4361}
4362
4363// Public API to manipulate UTF-8 text from within a callback.
4364// FIXME: The existence of this rarely exercised code path is a bit of a nuisance.
4365// Historically they existed because STB_TEXTEDIT_INSERTCHARS() etc. worked on our ImWchar
4366// buffer, but nowadays they both work on UTF-8 data. Should aim to merge both.
4367void ImGuiInputTextCallbackData::DeleteChars(int pos, int bytes_count)
4368{
4369 IM_ASSERT(pos + bytes_count <= BufTextLen);
4370 char* dst = Buf + pos;
4371 const char* src = Buf + pos + bytes_count;
4372 memmove(dst, src, BufTextLen - bytes_count - pos + 1);
4373
4374 if (CursorPos >= pos + bytes_count)
4375 CursorPos -= bytes_count;
4376 else if (CursorPos >= pos)
4377 CursorPos = pos;
4378 SelectionStart = SelectionEnd = CursorPos;
4379 BufDirty = true;
4380 BufTextLen -= bytes_count;
4381}
4382
4383void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, const char* new_text_end)
4384{
4385 // Accept null ranges
4386 if (new_text == new_text_end)
4387 return;
4388
4389 ImGuiContext& g = *Ctx;
4390 ImGuiInputTextState* obj = &g.InputTextState;
4391 IM_ASSERT(obj->ID != 0 && g.ActiveId == obj->ID);
4392 const bool is_resizable = (Flags & ImGuiInputTextFlags_CallbackResize) != 0;
4393 const bool is_readonly = (Flags & ImGuiInputTextFlags_ReadOnly) != 0;
4394 int new_text_len = new_text_end ? (int)(new_text_end - new_text) : (int)ImStrlen(new_text);
4395
4396 // We support partial insertion (with a mod in stb_textedit.h)
4397 const int avail = BufSize - 1 - BufTextLen;
4398 if (!is_resizable && new_text_len > avail)
4399 new_text_len = (int)(ImTextFindValidUtf8CodepointEnd(new_text, new_text + new_text_len, new_text + avail) - new_text); // Truncate to closest UTF-8 codepoint. Alternative: return 0 to cancel insertion.
4400 if (new_text_len == 0)
4401 return;
4402
4403 // Grow internal buffer if needed
4404 if (new_text_len + BufTextLen + 1 > obj->TextA.Size && is_resizable && !is_readonly)
4405 {
4406 IM_ASSERT(Buf == obj->TextA.Data);
4407 int new_buf_size = BufTextLen + ImClamp(new_text_len * 4, 32, ImMax(256, new_text_len)) + 1;
4408 obj->TextA.resize(new_buf_size + 1);
4409 obj->TextSrc = obj->TextA.Data;
4410 Buf = obj->TextA.Data;
4411 BufSize = obj->BufCapacity = new_buf_size;
4412 }
4413
4414 if (BufTextLen != pos)
4415 memmove(Buf + pos + new_text_len, Buf + pos, (size_t)(BufTextLen - pos));
4416 memcpy(Buf + pos, new_text, (size_t)new_text_len * sizeof(char));
4417 Buf[BufTextLen + new_text_len] = '\0';
4418
4419 BufDirty = true;
4420 BufTextLen += new_text_len;
4421 if (CursorPos >= pos)
4422 CursorPos += new_text_len;
4423 CursorPos = ImClamp(CursorPos, 0, BufTextLen);
4424 SelectionStart = SelectionEnd = CursorPos;
4425}
4426
4427void ImGui::PushPasswordFont()
4428{
4429 ImGuiContext& g = *GImGui;
4430 ImFontBaked* backup = &g.InputTextPasswordFontBackupBaked;
4431 IM_ASSERT(backup->IndexAdvanceX.Size == 0 && backup->IndexLookup.Size == 0);
4432 ImFontGlyph* glyph = g.FontBaked->FindGlyph('*');
4433 g.InputTextPasswordFontBackupFlags = g.Font->Flags;
4434 backup->FallbackGlyphIndex = g.FontBaked->FallbackGlyphIndex;
4435 backup->FallbackAdvanceX = g.FontBaked->FallbackAdvanceX;
4436 backup->IndexLookup.swap(g.FontBaked->IndexLookup);
4437 backup->IndexAdvanceX.swap(g.FontBaked->IndexAdvanceX);
4438 g.Font->Flags |= ImFontFlags_NoLoadGlyphs;
4439 g.FontBaked->FallbackGlyphIndex = g.FontBaked->Glyphs.index_from_ptr(glyph);
4440 g.FontBaked->FallbackAdvanceX = glyph->AdvanceX;
4441}
4442
4443void ImGui::PopPasswordFont()
4444{
4445 ImGuiContext& g = *GImGui;
4446 ImFontBaked* backup = &g.InputTextPasswordFontBackupBaked;
4447 g.Font->Flags = g.InputTextPasswordFontBackupFlags;
4448 g.FontBaked->FallbackGlyphIndex = backup->FallbackGlyphIndex;
4449 g.FontBaked->FallbackAdvanceX = backup->FallbackAdvanceX;
4450 g.FontBaked->IndexLookup.swap(backup->IndexLookup);
4451 g.FontBaked->IndexAdvanceX.swap(backup->IndexAdvanceX);
4452 IM_ASSERT(backup->IndexAdvanceX.Size == 0 && backup->IndexLookup.Size == 0);
4453}
4454
4455// Return false to discard a character.
4456static bool InputTextFilterCharacter(ImGuiContext* ctx, ImGuiInputTextState* state, unsigned int* p_char, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard)
4457{
4458 IM_ASSERT(state != NULL);
4459 unsigned int c = *p_char;
4460 ImGuiInputTextFlags flags = state->Flags;
4461
4462 // Filter non-printable (NB: isprint is unreliable! see #2467)
4463 bool apply_named_filters = true;
4464 if (c < 0x20)
4465 {
4466 bool pass = false;
4467 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)
4468 if (c == '\n' && input_source_is_clipboard && (flags & ImGuiInputTextFlags_Multiline) == 0) // In single line mode, replace \n with a space
4469 {
4470 c = *p_char = ' ';
4471 pass = true;
4472 }
4473 pass |= (c == '\n') && (flags & ImGuiInputTextFlags_Multiline) != 0;
4474 pass |= (c == '\t') && (flags & ImGuiInputTextFlags_AllowTabInput) != 0;
4475 if (!pass)
4476 return false;
4477 apply_named_filters = false; // Override named filters below so newline and tabs can still be inserted.
4478 }
4479
4480 if (input_source_is_clipboard == false)
4481 {
4482 // We ignore Ascii representation of delete (emitted from Backspace on OSX, see #2578, #2817)
4483 if (c == 127)
4484 return false;
4485
4486 // Filter private Unicode range. GLFW on OSX seems to send private characters for special keys like arrow keys (FIXME)
4487 if (c >= 0xE000 && c <= 0xF8FF)
4488 return false;
4489 }
4490
4491 // Filter Unicode ranges we are not handling in this build
4492 if (c > IM_UNICODE_CODEPOINT_MAX)
4493 return false;
4494
4495 // Generic named filters
4496 if (apply_named_filters && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint)))
4497 {
4498 // 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 '.'.
4499 // The standard mandate that programs starts in the "C" locale where the decimal point is '.'.
4500 // 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.
4501 // Change the default decimal_point with:
4502 // ImGui::GetPlatformIO()->Platform_LocaleDecimalPoint = *localeconv()->decimal_point;
4503 // 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.
4504 ImGuiContext& g = *ctx;
4505 const unsigned c_decimal_point = (unsigned int)g.PlatformIO.Platform_LocaleDecimalPoint;
4506 if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint))
4507 if (c == '.' || c == ',')
4508 c = c_decimal_point;
4509
4510 // Full-width -> half-width conversion for numeric fields: https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
4511 // While this is mostly convenient, this has the side-effect for uninformed users accidentally inputting full-width characters that they may
4512 // scratch their head as to why it works in numerical fields vs in generic text fields it would require support in the font.
4513 if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | ImGuiInputTextFlags_CharsHexadecimal))
4514 if (c >= 0xFF01 && c <= 0xFF5E)
4515 c = c - 0xFF01 + 0x21;
4516
4517 // Allow 0-9 . - + * /
4518 if (flags & ImGuiInputTextFlags_CharsDecimal)
4519 if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/'))
4520 return false;
4521
4522 // Allow 0-9 . - + * / e E
4523 if (flags & ImGuiInputTextFlags_CharsScientific)
4524 if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/') && (c != 'e') && (c != 'E'))
4525 return false;
4526
4527 // Allow 0-9 a-F A-F
4528 if (flags & ImGuiInputTextFlags_CharsHexadecimal)
4529 if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
4530 return false;
4531
4532 // Turn a-z into A-Z
4533 if (flags & ImGuiInputTextFlags_CharsUppercase)
4534 if (c >= 'a' && c <= 'z')
4535 c += (unsigned int)('A' - 'a');
4536
4537 if (flags & ImGuiInputTextFlags_CharsNoBlank)
4538 if (ImCharIsBlankW(c))
4539 return false;
4540
4541 *p_char = c;
4542 }
4543
4544 // Custom callback filter
4545 if (flags & ImGuiInputTextFlags_CallbackCharFilter)
4546 {
4547 ImGuiContext& g = *GImGui;
4548 ImGuiInputTextCallbackData callback_data;
4549 callback_data.Ctx = &g;
4550 callback_data.ID = state->ID;
4551 callback_data.Flags = flags;
4552 callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter;
4553 callback_data.EventChar = (ImWchar)c;
4554 callback_data.EventActivated = (g.ActiveId == state->ID && g.ActiveIdIsJustActivated);
4555 callback_data.CursorPos = state->Stb->cursor;
4556 callback_data.SelectionStart = state->Stb->select_start;
4557 callback_data.SelectionEnd = state->Stb->select_end;
4558 callback_data.UserData = user_data;
4559 if (callback(&callback_data) != 0)
4560 return false;
4561 *p_char = callback_data.EventChar;
4562 if (!callback_data.EventChar)
4563 return false;
4564 }
4565
4566 return true;
4567}
4568
4569// Find the shortest single replacement we can make to get from old_buf to new_buf
4570// Note that this doesn't directly alter state->TextA, state->TextLen. They are expected to be made valid separately.
4571// 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.
4572static void InputTextReconcileUndoState(ImGuiInputTextState* state, const char* old_buf, int old_length, const char* new_buf, int new_length)
4573{
4574 const int shorter_length = ImMin(old_length, new_length);
4575 int first_diff;
4576 for (first_diff = 0; first_diff < shorter_length; first_diff++)
4577 if (old_buf[first_diff] != new_buf[first_diff])
4578 break;
4579 if (first_diff == old_length && first_diff == new_length)
4580 return;
4581
4582 int old_last_diff = old_length - 1;
4583 int new_last_diff = new_length - 1;
4584 for (; old_last_diff >= first_diff && new_last_diff >= first_diff; old_last_diff--, new_last_diff--)
4585 if (old_buf[old_last_diff] != new_buf[new_last_diff])
4586 break;
4587
4588 const int insert_len = new_last_diff - first_diff + 1;
4589 const int delete_len = old_last_diff - first_diff + 1;
4590 if (insert_len > 0 || delete_len > 0)
4591 if (IMSTB_TEXTEDIT_CHARTYPE* p = stb_text_createundo(&state->Stb->undostate, first_diff, delete_len, insert_len))
4592 for (int i = 0; i < delete_len; i++)
4593 p[i] = old_buf[first_diff + i];
4594}
4595
4596// As InputText() retain textual data and we currently provide a path for user to not retain it (via local variables)
4597// we need some form of hook to reapply data back to user buffer on deactivation frame. (#4714)
4598// It would be more desirable that we discourage users from taking advantage of the "user not retaining data" trick,
4599// but that more likely be attractive when we do have _NoLiveEdit flag available.
4600void ImGui::InputTextDeactivateHook(ImGuiID id)
4601{
4602 ImGuiContext& g = *GImGui;
4603 ImGuiInputTextState* state = &g.InputTextState;
4604 if (id == 0 || state->ID != id || g.ActiveId != id)
4605 return;
4606 if (!state->EditedBefore)
4607 return;
4608 //IMGUI_DEBUG_LOG_ACTIVEID("InputTextDeactivateHook() id = 0x%08X\n", id);
4609 g.InputTextDeactivatedState.ID = state->ID;
4610 g.InputTextDeactivatedState.ElapseFrame = g.FrameCount + 1;
4611 if (state->Flags & ImGuiInputTextFlags_ReadOnly)
4612 {
4613 g.InputTextDeactivatedState.TextA.resize(0); // In theory this data won't be used, but clear to be neat.
4614 }
4615 else
4616 {
4617 IM_ASSERT(state->TextA.Data != 0);
4618 IM_ASSERT(state->TextA[state->TextLen] == 0);
4619 g.InputTextDeactivatedState.TextA.resize(state->TextLen + 1);
4620 memcpy(g.InputTextDeactivatedState.TextA.Data, state->TextA.Data, state->TextLen + 1);
4621 }
4622}
4623
4624static int* ImLowerBound(int* in_begin, int* in_end, int v)
4625{
4626 int* in_p = in_begin;
4627 for (size_t count = (size_t)(in_end - in_p); count > 0; )
4628 {
4629 size_t count2 = count >> 1;
4630 int* mid = in_p + count2;
4631 if (*mid < v)
4632 {
4633 in_p = ++mid;
4634 count -= count2 + 1;
4635 }
4636 else
4637 {
4638 count = count2;
4639 }
4640 }
4641 return in_p;
4642}
4643
4644// FIXME-WORDWRAP: Bundle some of this into ImGuiTextIndex and/or extract as a different tool?
4645// 'max_output_buffer_size' happens to be a meaningful optimization to avoid writing the full line_index when not necessarily needed (e.g. very large buffer, scrolled up, inactive)
4646static int InputTextLineIndexBuild(ImGuiInputTextFlags flags, ImGuiTextIndex* line_index, const char* buf, const char* buf_end, float wrap_width, int max_output_buffer_size, const char** out_buf_end)
4647{
4648 ImGuiContext& g = *GImGui;
4649 int size = 0;
4650 const char* s;
4651 bool trailing_line_already_counted = false;
4652 if (flags & ImGuiInputTextFlags_WordWrap)
4653 {
4654 for (s = buf; s < buf_end; s = (*s == '\n') ? s + 1 : s)
4655 {
4656 if (size++ <= max_output_buffer_size)
4657 line_index->Offsets.push_back((int)(s - buf));
4658 s = ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, s, buf_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks);
4659 }
4660 }
4661 else if (buf_end != NULL)
4662 {
4663 for (s = buf; s < buf_end; s = s ? s + 1 : buf_end)
4664 {
4665 if (size++ <= max_output_buffer_size)
4666 line_index->Offsets.push_back((int)(s - buf));
4667 s = (const char*)ImMemchr(s, '\n', buf_end - s);
4668 }
4669 }
4670 else
4671 {
4672 // Inactive path: we don't know buf_end ahead of time.
4673 const char* s_eol;
4674 for (s = buf; ; s = s_eol + 1)
4675 {
4676 if (size++ <= max_output_buffer_size)
4677 line_index->Offsets.push_back((int)(s - buf));
4678 if ((s_eol = strchr(s, '\n')) != NULL)
4679 continue;
4680 s += strlen(s);
4681 trailing_line_already_counted = true;
4682 break;
4683 }
4684 }
4685 if (out_buf_end != NULL)
4686 *out_buf_end = buf_end = s;
4687 if (size == 0)
4688 {
4689 line_index->Offsets.push_back(0);
4690 size++;
4691 }
4692 if (buf_end > buf && buf_end[-1] == '\n' && !trailing_line_already_counted && size++ <= max_output_buffer_size)
4693 line_index->Offsets.push_back((int)(buf_end - buf));
4694 return size;
4695}
4696
4697static ImVec2 InputTextLineIndexGetPosOffset(ImGuiContext& g, ImGuiInputTextState* state, ImGuiTextIndex* line_index, const char* buf, const char* buf_end, int cursor_n)
4698{
4699 const char* cursor_ptr = buf + cursor_n;
4700 int* it_begin = line_index->Offsets.begin();
4701 int* it_end = line_index->Offsets.end();
4702 const int* it = ImLowerBound(it_begin, it_end, cursor_n);
4703 if (it > it_begin)
4704 if (it == it_end || *it != cursor_n || (state != NULL && state->WrapWidth > 0.0f && state->LastMoveDirectionLR == ImGuiDir_Right && cursor_ptr[-1] != '\n' && cursor_ptr[-1] != 0))
4705 it--;
4706
4707 const int line_no = (it == it_begin) ? 0 : line_index->Offsets.index_from_ptr(it);
4708 const char* line_start = line_index->get_line_begin(buf, line_no);
4709 ImVec2 offset;
4710 offset.x = InputTextCalcTextSize(&g, line_start, cursor_ptr, buf_end, NULL, NULL, ImDrawTextFlags_WrapKeepBlanks).x;
4711 offset.y = (line_no + 1) * g.FontSize;
4712 return offset;
4713}
4714
4715// Edit a string of text
4716// - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!".
4717// This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match
4718// Note that in std::string world, capacity() would omit 1 byte used by the zero-terminator.
4719// - 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.
4720// - If you want to use InputText() with std::string or any custom dynamic string type, use the wrapper in misc/cpp/imgui_stdlib.h/.cpp!
4721bool 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)
4722{
4723 ImGuiWindow* window = GetCurrentWindow();
4724 if (window->SkipItems)
4725 return false;
4726
4727 IM_ASSERT(buf != NULL && buf_size >= 0);
4728 IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys)
4729 IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key)
4730 IM_ASSERT(!((flags & ImGuiInputTextFlags_ElideLeft) && (flags & ImGuiInputTextFlags_Multiline))); // Multiline does not not work with left-trimming
4731 IM_ASSERT((flags & ImGuiInputTextFlags_WordWrap) == 0 || (flags & ImGuiInputTextFlags_Password) == 0); // WordWrap does not work with Password mode.
4732 IM_ASSERT((flags & ImGuiInputTextFlags_WordWrap) == 0 || (flags & ImGuiInputTextFlags_Multiline) != 0); // WordWrap does not work in single-line mode.
4733
4734 ImGuiContext& g = *GImGui;
4735 ImGuiIO& io = g.IO;
4736 const ImGuiStyle& style = g.Style;
4737
4738 const bool RENDER_SELECTION_WHEN_INACTIVE = false;
4739 const bool is_multiline = (flags & ImGuiInputTextFlags_Multiline) != 0;
4740
4741 if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope (including the scrollbar)
4742 BeginGroup();
4743 const ImGuiID id = window->GetID(label);
4744 const char* label_end = FindRenderedTextEnd(label);
4745 const ImVec2 label_size = CalcTextSize(label, label_end, false);
4746 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
4747 const ImVec2 total_size = ImVec2(frame_size.x + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), frame_size.y);
4748
4749 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
4750 const ImRect total_bb(frame_bb.Min, frame_bb.Min + total_size);
4751
4752 ImGuiWindow* draw_window = window;
4753 ImVec2 inner_size = frame_size;
4754 ImGuiLastItemData item_data_backup;
4755 if (is_multiline)
4756 {
4757 ImVec2 backup_pos = window->DC.CursorPos;
4758 ItemSize(total_bb, style.FramePadding.y);
4759 bool no_clip = (g.InputTextDeactivatedState.ID == id) || (g.ActiveId == id) || (id == g.NavActivateId); // Mimic some of ItemAdd() logic + add InputTextDeactivatedState.ID check.
4760 if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable) && !no_clip)
4761 {
4762 EndGroup();
4763 return false;
4764 }
4765 item_data_backup = g.LastItemData;
4766 window->DC.CursorPos = backup_pos;
4767
4768 // Prevent NavActivation from explicit Tabbing when our widget accepts Tab inputs: this allows cycling through widgets without stopping.
4769 if (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_FromTabbing) && !(g.NavActivateFlags & ImGuiActivateFlags_FromFocusApi) && (flags & ImGuiInputTextFlags_AllowTabInput))
4770 g.NavActivateId = 0;
4771
4772 // Prevent NavActivate reactivating in BeginChild() when we are already active.
4773 const ImGuiID backup_activate_id = g.NavActivateId;
4774 if (g.ActiveId == id) // Prevent reactivation
4775 g.NavActivateId = 0;
4776
4777 // We reproduce the contents of BeginChildFrame() in order to provide 'label' so our window internal data are easier to read/debug.
4778 PushStyleColor(ImGuiCol_ChildBg, style.Colors[ImGuiCol_FrameBg]);
4779 PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding);
4780 PushStyleVar(ImGuiStyleVar_ChildBorderSize, style.FrameBorderSize);
4781 PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); // Ensure no clip rect so mouse hover can reach FramePadding edges
4782 bool child_visible = BeginChildEx(label, id, frame_bb.GetSize(), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoMove);
4783 g.NavActivateId = backup_activate_id;
4784 PopStyleVar(3);
4785 PopStyleColor();
4786 if (!child_visible && !no_clip)
4787 {
4788 EndChild();
4789 EndGroup();
4790 return false;
4791 }
4792 draw_window = g.CurrentWindow; // Child window
4793 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.
4794 draw_window->DC.CursorPos += style.FramePadding;
4795 inner_size.x -= draw_window->ScrollbarSizes.x;
4796
4797 // FIXME: Could this be a ImGuiChildFlags to affect the SetLastItemDataForWindow() call?
4798 g.LastItemData.ID = id;
4799 g.LastItemData.ItemFlags = item_data_backup.ItemFlags;
4800 g.LastItemData.StatusFlags = item_data_backup.StatusFlags;
4801 }
4802 else
4803 {
4804 // Support for internal ImGuiInputTextFlags_MergedItem flag, which could be redesigned as an ItemFlags if needed (with test performed in ItemAdd)
4805 ItemSize(total_bb, style.FramePadding.y);
4806 if (!(flags & ImGuiInputTextFlags_TempInput))
4807 if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable))
4808 return false;
4809 }
4810
4811 // Ensure mouse cursor is set even after switching to keyboard/gamepad mode. May generalize further? (#6417)
4812 bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags | ImGuiItemFlags_NoNavDisableMouseHover);
4813 if (hovered)
4814 SetMouseCursor(ImGuiMouseCursor_TextInput);
4815 if (hovered && g.NavHighlightItemUnderNav)
4816 hovered = false;
4817
4818 // We are only allowed to access the state if we are already the active widget.
4819 ImGuiInputTextState* state = GetInputTextState(id);
4820
4821 if (g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly)
4822 flags |= ImGuiInputTextFlags_ReadOnly;
4823 const bool is_readonly = (flags & ImGuiInputTextFlags_ReadOnly) != 0;
4824 const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0;
4825 const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0;
4826 const bool is_resizable = (flags & ImGuiInputTextFlags_CallbackResize) != 0;
4827 if (is_resizable)
4828 IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag!
4829
4830 // Word-wrapping: enforcing a fixed width not altered by vertical scrollbar makes things easier, notably to track cursor reliably and avoid one-frame glitches.
4831 // Instead of using ImGuiWindowFlags_AlwaysVerticalScrollbar we account for that space if the scrollbar is not visible.
4832 const bool is_wordwrap = (flags & ImGuiInputTextFlags_WordWrap) != 0;
4833 float wrap_width = 0.0f;
4834 if (is_wordwrap)
4835 wrap_width = ImMax(1.0f, GetContentRegionAvail().x + (draw_window->ScrollbarY ? 0.0f : -g.Style.ScrollbarSize));
4836
4837 const bool user_clicked = hovered && io.MouseClicked[0];
4838 const bool input_requested_by_nav = (g.ActiveId != id) && (g.NavActivateId == id);
4839 const bool input_requested_by_reactivate = (g.InputTextReactivateId == id); // for io.ConfigInputTextEnterKeepActive
4840 const bool input_requested_by_user = (user_clicked) || (g.ActiveId == 0 && (flags & ImGuiInputTextFlags_TempInput) && g.InputTextDeactivatedState.ID != id);
4841 const ImGuiID scrollbar_id = (is_multiline && state != NULL) ? GetWindowScrollbarID(draw_window, ImGuiAxis_Y) : 0;
4842 const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == scrollbar_id;
4843 const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == scrollbar_id;
4844 bool clear_active_id = false;
4845 bool select_all = false;
4846
4847 float scroll_y = is_multiline ? draw_window->Scroll.y : FLT_MAX;
4848
4849 const bool init_reload_from_user_buf = (state != NULL && state->WantReloadUserBuf);
4850 const bool init_changed_specs_multiline = (state != NULL && (state->Stb->single_line != !is_multiline)); // state != NULL means its our state.
4851 const bool init_changed_specs_readonly = (state != NULL && ((state->Flags ^ flags) & ImGuiInputTextFlags_ReadOnly)); // state != NULL means its our state.
4852 const bool init_make_active = (input_requested_by_user || input_requested_by_nav || input_requested_by_reactivate || user_scroll_finish);
4853 if (init_reload_from_user_buf)
4854 {
4855 int new_len = (int)ImStrlen(buf);
4856 IM_ASSERT(new_len + 1 <= buf_size && "Is your input buffer properly zero-terminated?");
4857 state->WantReloadUserBuf = false;
4858 InputTextReconcileUndoState(state, state->TextA.Data, state->TextLen, buf, new_len);
4859 state->TextA.resize(buf_size + 1); // we use +1 to make sure that .Data is always pointing to at least an empty string.
4860 state->TextLen = new_len;
4861 memcpy(state->TextA.Data, buf, state->TextLen + 1);
4862 state->Stb->select_start = state->ReloadSelectionStart;
4863 state->Stb->cursor = state->Stb->select_end = state->ReloadSelectionEnd; // will be clamped to bounds below
4864 }
4865 else if ((init_make_active && g.ActiveId != id) || init_changed_specs_multiline || init_changed_specs_readonly)
4866 {
4867 // Access state even if we don't own it yet.
4868 state = &g.InputTextState;
4869 state->CursorAnimReset();
4870
4871 // 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)
4872 if (state->ID != id && state->ID == g.ActiveId && (init_make_active && g.ActiveId != id)) //-V560
4873 InputTextDeactivateHook(state->ID); // <-- this is essentially an earlier call to what SetActiveID() would do below.
4874
4875 // Take a copy of the initial buffer value.
4876 // From the moment we focused we are normally ignoring the content of 'buf' (unless we are in read-only mode)
4877 const int buf_len = (int)ImStrlen(buf);
4878 IM_ASSERT(((buf_len + 1 <= buf_size) || (buf_len == 0 && buf_size == 0)) && "Is your input buffer properly zero-terminated?");
4879 if (!user_scroll_finish)
4880 {
4881 state->TextToRevertTo.resize(buf_len + 1); // UTF-8. we use +1 to make sure that .Data is always pointing to at least an empty string.
4882 memcpy(state->TextToRevertTo.Data, buf, buf_len + 1);
4883 }
4884
4885 // Preserve cursor position and undo/redo stack if we come back to same widget
4886 // FIXME: Since we reworked this on 2022/06, may want to differentiate recycle_cursor vs recycle_undostate?
4887 bool recycle_state = (state->ID == id && !init_changed_specs_multiline);
4888 if (recycle_state && !init_changed_specs_readonly && (state->TextLen != buf_len || (state->TextA.Data == NULL || strncmp(state->TextA.Data, buf, buf_len) != 0)))
4889 recycle_state = false;
4890
4891 // Start edition
4892 state->ID = id;
4893 state->TextLen = buf_len;
4894 state->EditedBefore = false;
4895 if (!is_readonly)
4896 {
4897 state->TextA.resize(buf_size + 1); // we use +1 to make sure that .Data is always pointing to at least an empty string.
4898 memcpy(state->TextA.Data, buf, state->TextLen + 1);
4899 }
4900
4901 // Find initial scroll position for right alignment
4902 state->Scroll = ImVec2(0.0f, 0.0f);
4903 if (flags & ImGuiInputTextFlags_ElideLeft)
4904 state->Scroll.x += ImMax(0.0f, CalcTextSize(buf).x - frame_size.x + style.FramePadding.x * 2.0f);
4905
4906 // Recycle existing cursor/selection/undo stack but clamp position
4907 // Note a single mouse click will override the cursor/position immediately by calling stb_textedit_click handler.
4908 if (!recycle_state)
4909 stb_textedit_initialize_state(state->Stb, !is_multiline);
4910
4911 if (!is_multiline)
4912 {
4913 if (flags & ImGuiInputTextFlags_AutoSelectAll)
4914 select_all = true;
4915 if (input_requested_by_nav && (!recycle_state || !(g.NavActivateFlags & ImGuiActivateFlags_TryToPreserveState)))
4916 select_all = true;
4917 if (user_clicked && io.KeyCtrl)
4918 select_all = true;
4919 }
4920
4921 if (flags & ImGuiInputTextFlags_AlwaysOverwrite)
4922 state->Stb->insert_mode = 1; // stb field name is indeed incorrect (see #2863)
4923 }
4924
4925 const bool is_osx = io.ConfigMacOSXBehaviors;
4926 if (init_make_active && g.ActiveId != id)
4927 {
4928 IM_ASSERT(state && state->ID == id);
4929 SetActiveID(id, window);
4930 SetFocusID(id, window);
4931 FocusWindow(window);
4932 if (input_requested_by_nav)
4933 SetNavCursorVisibleAfterMove();
4934 }
4935 if (g.ActiveId == id)
4936 {
4937 // Declare some inputs, the other are registered and polled via Shortcut() routing system.
4938 // FIXME: The reason we don't use Shortcut() is we would need a routing flag to specify multiple mods, or to all mods combination into individual shortcuts.
4939 const ImGuiKey always_owned_keys[] = { ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_Delete, ImGuiKey_Backspace, ImGuiKey_Home, ImGuiKey_End };
4940 for (ImGuiKey key : always_owned_keys)
4941 SetKeyOwner(key, id);
4942 if (user_clicked)
4943 SetKeyOwner(ImGuiKey_MouseLeft, id);
4944 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
4945 if (is_multiline || (flags & ImGuiInputTextFlags_CallbackHistory))
4946 {
4947 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
4948 SetKeyOwner(ImGuiKey_UpArrow, id);
4949 SetKeyOwner(ImGuiKey_DownArrow, id);
4950 }
4951 if (is_multiline)
4952 {
4953 SetKeyOwner(ImGuiKey_PageUp, id);
4954 SetKeyOwner(ImGuiKey_PageDown, id);
4955 }
4956 // FIXME: May be a problem to always steal Alt on OSX, would ideally still allow an uninterrupted Alt down-up to toggle menu
4957 if (is_osx)
4958 SetKeyOwner(ImGuiMod_Alt, id);
4959
4960 // Expose scroll in a manner that is agnostic to us using a child window
4961 if (is_multiline && state != NULL)
4962 state->Scroll.y = draw_window->Scroll.y;
4963
4964 // Read-only mode always ever read from source buffer. Refresh TextLen when active.
4965 if (is_readonly && state != NULL)
4966 state->TextLen = (int)ImStrlen(buf);
4967 if (state != NULL)
4968 state->CursorClamp();
4969 //if (is_readonly && state != NULL)
4970 // state->TextA.clear(); // Uncomment to facilitate debugging, but we otherwise prefer to keep/amortize th allocation.
4971 }
4972 if (state != NULL)
4973 state->TextSrc = is_readonly ? buf : state->TextA.Data;
4974
4975 // 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)
4976 if (g.ActiveId == id && state == NULL)
4977 ClearActiveID();
4978
4979 // Release focus when we click outside
4980 if (g.ActiveId == id && io.MouseClicked[0] && !init_make_active) //-V560
4981 clear_active_id = true;
4982
4983 // Lock the decision of whether we are going to take the path displaying the cursor or selection
4984 bool render_cursor = (g.ActiveId == id) || (state && user_scroll_active);
4985 bool render_selection = state && (state->HasSelection() || select_all) && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
4986 bool value_changed = false;
4987 bool validated = false;
4988
4989 // Select the buffer to render.
4990 const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state;
4991 bool is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0);
4992
4993 // Password pushes a temporary font with only a fallback glyph
4994 if (is_password && !is_displaying_hint)
4995 PushPasswordFont();
4996
4997 if (state != NULL && state->ID == id)
4998 {
4999 state->Flags = flags;
5000
5001 // Word-wrapping: attempt to keep cursor in view while resizing frame/parent (FIXME-WORDWRAP: would be better to preserve same relative offset)
5002 if (is_wordwrap && state->WrapWidth != wrap_width)
5003 {
5004 state->CursorCenterY = true;
5005 state->WrapWidth = wrap_width;
5006 render_cursor = true;
5007 }
5008 }
5009
5010 // Process mouse inputs and character inputs
5011 if (g.ActiveId == id)
5012 {
5013 IM_ASSERT(state != NULL);
5014 state->EditedThisFrame = false;
5015 state->BufCapacity = buf_size;
5016 state->WrapWidth = wrap_width;
5017
5018 // Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget.
5019 // Down the line we should have a cleaner library-wide concept of Selected vs Active.
5020 g.ActiveIdAllowOverlap = !io.MouseDown[0];
5021
5022 // Edit in progress
5023 const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + state->Scroll.x;
5024 const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y) : (g.FontSize * 0.5f));
5025
5026 if (select_all)
5027 {
5028 state->SelectAll();
5029 state->SelectedAllMouseLock = true;
5030 }
5031 else if (hovered && io.MouseClickedCount[0] >= 2 && !io.KeyShift)
5032 {
5033 stb_textedit_click(state, state->Stb, mouse_x, mouse_y);
5034 const int multiclick_count = (io.MouseClickedCount[0] - 2);
5035 if ((multiclick_count % 2) == 0)
5036 {
5037 // Double-click: Select word
5038 // We always use the "Mac" word advance for double-click select vs Ctrl+Right which use the platform dependent variant:
5039 // FIXME: There are likely many ways to improve this behavior, but there's no "right" behavior (depends on use-case, software, OS)
5040 const bool is_bol = (state->Stb->cursor == 0) || ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb->cursor - 1) == '\n';
5041 if (STB_TEXT_HAS_SELECTION(state->Stb) || !is_bol)
5042 state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT);
5043 //state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
5044 if (!STB_TEXT_HAS_SELECTION(state->Stb))
5045 ImStb::stb_textedit_prep_selection_at_cursor(state->Stb);
5046 state->Stb->cursor = ImStb::STB_TEXTEDIT_MOVEWORDRIGHT_MAC(state, state->Stb->cursor);
5047 state->Stb->select_end = state->Stb->cursor;
5048 ImStb::stb_textedit_clamp(state, state->Stb);
5049 }
5050 else
5051 {
5052 // Triple-click: Select line
5053 const bool is_eol = ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb->cursor) == '\n';
5054 state->WrapWidth = 0.0f; // Temporarily disable wrapping so we use real line start.
5055 state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART);
5056 state->OnKeyPressed(STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT);
5057 state->OnKeyPressed(STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT);
5058 state->WrapWidth = wrap_width;
5059 if (!is_eol && is_multiline)
5060 {
5061 ImSwap(state->Stb->select_start, state->Stb->select_end);
5062 state->Stb->cursor = state->Stb->select_end;
5063 }
5064 state->CursorFollow = false;
5065 }
5066 state->CursorAnimReset();
5067 }
5068 else if (io.MouseClicked[0] && !state->SelectedAllMouseLock)
5069 {
5070 if (hovered)
5071 {
5072 if (io.KeyShift)
5073 stb_textedit_drag(state, state->Stb, mouse_x, mouse_y);
5074 else
5075 stb_textedit_click(state, state->Stb, mouse_x, mouse_y);
5076 state->CursorAnimReset();
5077 }
5078 }
5079 else if (io.MouseDown[0] && !state->SelectedAllMouseLock && (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f))
5080 {
5081 stb_textedit_drag(state, state->Stb, mouse_x, mouse_y);
5082 state->CursorAnimReset();
5083 state->CursorFollow = true;
5084 }
5085 if (state->SelectedAllMouseLock && !io.MouseDown[0])
5086 state->SelectedAllMouseLock = false;
5087
5088 // We expect backends to emit a Tab key but some also emit a Tab character which we ignore (#2467, #1336)
5089 // (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)
5090 if ((flags & ImGuiInputTextFlags_AllowTabInput) && !is_readonly)
5091 {
5092 if (Shortcut(ImGuiKey_Tab, ImGuiInputFlags_Repeat, id))
5093 {
5094 unsigned int c = '\t'; // Insert TAB
5095 if (InputTextFilterCharacter(&g, state, &c, callback, callback_user_data))
5096 state->OnCharPressed(c);
5097 }
5098 // FIXME: Implement Shift+Tab
5099 /*
5100 if (Shortcut(ImGuiKey_Tab | ImGuiMod_Shift, ImGuiInputFlags_Repeat, id))
5101 {
5102 }
5103 */
5104 }
5105
5106 // Process regular text input (before we check for Return because using some IME will effectively send a Return?)
5107 // 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.
5108 const bool ignore_char_inputs = (io.KeyCtrl && !io.KeyAlt) || (is_osx && io.KeyCtrl);
5109 if (io.InputQueueCharacters.Size > 0)
5110 {
5111 if (!ignore_char_inputs && !is_readonly && !input_requested_by_nav)
5112 for (int n = 0; n < io.InputQueueCharacters.Size; n++)
5113 {
5114 // Insert character if they pass filtering
5115 unsigned int c = (unsigned int)io.InputQueueCharacters[n];
5116 if (c == '\t') // Skip Tab, see above.
5117 continue;
5118 if (InputTextFilterCharacter(&g, state, &c, callback, callback_user_data))
5119 state->OnCharPressed(c);
5120 }
5121
5122 // Consume characters
5123 io.InputQueueCharacters.resize(0);
5124 }
5125 }
5126
5127 // Process other shortcuts/key-presses
5128 bool revert_edit = false;
5129 if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id)
5130 {
5131 IM_ASSERT(state != NULL);
5132
5133 const int row_count_per_page = ImMax((int)((inner_size.y - style.FramePadding.y) / g.FontSize), 1);
5134 state->Stb->row_count_per_page = row_count_per_page;
5135
5136 const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0);
5137 const bool is_wordmove_key_down = is_osx ? io.KeyAlt : io.KeyCtrl; // OS X style: Text editing cursor movement using Alt instead of Ctrl
5138 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
5139
5140 // 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: former would be handled by InputText)
5141 // Otherwise we could simply assume that we own the keys as we are active.
5142 const ImGuiInputFlags f_repeat = ImGuiInputFlags_Repeat;
5143 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());
5144 const bool is_copy = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_C, 0, id) || Shortcut(ImGuiMod_Ctrl | ImGuiKey_Insert, 0, id)) && !is_password && (!is_multiline || state->HasSelection());
5145 const bool is_paste = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_V, f_repeat, id) || Shortcut(ImGuiMod_Shift | ImGuiKey_Insert, f_repeat, id)) && !is_readonly;
5146 const bool is_undo = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_Z, f_repeat, id)) && !is_readonly && is_undoable;
5147 const bool is_redo = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_Y, f_repeat, id) || Shortcut(ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Z, f_repeat, id)) && !is_readonly && is_undoable;
5148 const bool is_select_all = Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, 0, id);
5149
5150 // 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.
5151 const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0;
5152 const bool is_enter = Shortcut(ImGuiKey_Enter, f_repeat, id) || Shortcut(ImGuiKey_KeypadEnter, f_repeat, id);
5153 const bool is_ctrl_enter = Shortcut(ImGuiMod_Ctrl | ImGuiKey_Enter, f_repeat, id) || Shortcut(ImGuiMod_Ctrl | ImGuiKey_KeypadEnter, f_repeat, id);
5154 const bool is_shift_enter = Shortcut(ImGuiMod_Shift | ImGuiKey_Enter, f_repeat, id) || Shortcut(ImGuiMod_Shift | ImGuiKey_KeypadEnter, f_repeat, id);
5155 const bool is_gamepad_validate = nav_gamepad_active && IsKeyPressed(ImGuiKey_NavGamepadActivate, false);
5156 const bool is_cancel = Shortcut(ImGuiKey_Escape, f_repeat, id) || (nav_gamepad_active && Shortcut(ImGuiKey_NavGamepadCancel, f_repeat, id));
5157
5158 // FIXME: Should use more Shortcut() and reduce IsKeyPressed()+SetKeyOwner(), but requires modifiers combination to be taken account of.
5159 // FIXME-OSX: Missing support for Alt(option)+Right/Left = go to end of line, or next line if already in end of line.
5160 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); }
5161 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); }
5162 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); }
5163 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); }
5164 else if (IsKeyPressed(ImGuiKey_PageUp) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGUP | k_mask); scroll_y -= row_count_per_page * g.FontSize; }
5165 else if (IsKeyPressed(ImGuiKey_PageDown) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGDOWN | k_mask); scroll_y += row_count_per_page * g.FontSize; }
5166 else if (IsKeyPressed(ImGuiKey_Home)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); }
5167 else if (IsKeyPressed(ImGuiKey_End)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); }
5168 else if (IsKeyPressed(ImGuiKey_Delete) && !is_readonly && !is_cut)
5169 {
5170 if (!state->HasSelection())
5171 {
5172 // 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)
5173 if (is_wordmove_key_down)
5174 state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
5175 }
5176 state->OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask);
5177 }
5178 else if (IsKeyPressed(ImGuiKey_Backspace) && !is_readonly)
5179 {
5180 if (!state->HasSelection())
5181 {
5182 if (is_wordmove_key_down)
5183 state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT);
5184 else if (is_osx && io.KeyCtrl && !io.KeyAlt && !io.KeySuper)
5185 state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT);
5186 }
5187 state->OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask);
5188 }
5189 else if (is_enter || is_ctrl_enter || is_shift_enter || is_gamepad_validate)
5190 {
5191 // Determine if we turn Enter into a \n character
5192 bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0;
5193 bool is_new_line = is_multiline && !is_gamepad_validate && (is_shift_enter || (is_enter && !ctrl_enter_for_new_line) || (is_ctrl_enter && ctrl_enter_for_new_line));
5194 if (!is_new_line)
5195 {
5196 validated = clear_active_id = true;
5197 if (io.ConfigInputTextEnterKeepActive && !is_multiline && !is_ctrl_enter && !is_shift_enter)
5198 {
5199 // Queue reactivation, so that e.g. IsItemDeactivatedAfterEdit() will work. (#9001)
5200 state->SelectAll(); // No need to scroll
5201 g.InputTextReactivateId = id; // Mark for reactivation on next frame
5202 }
5203 }
5204 else if (!is_readonly)
5205 {
5206 // Insert new line
5207 unsigned int c = '\n';
5208 if (InputTextFilterCharacter(&g, state, &c, callback, callback_user_data))
5209 state->OnCharPressed(c);
5210 }
5211 }
5212 else if (is_cancel)
5213 {
5214 if (flags & ImGuiInputTextFlags_EscapeClearsAll)
5215 {
5216 if (state->TextA.Data[0] != 0)
5217 {
5218 revert_edit = true;
5219 }
5220 else
5221 {
5222 render_cursor = render_selection = false;
5223 clear_active_id = true;
5224 }
5225 }
5226 else
5227 {
5228 clear_active_id = revert_edit = true;
5229 render_cursor = render_selection = false;
5230 }
5231 }
5232 else if (is_undo || is_redo)
5233 {
5234 state->OnKeyPressed(is_undo ? STB_TEXTEDIT_K_UNDO : STB_TEXTEDIT_K_REDO);
5235 state->ClearSelection();
5236 }
5237 else if (is_select_all)
5238 {
5239 state->SelectAll();
5240 state->CursorFollow = true;
5241 }
5242 else if (is_cut || is_copy)
5243 {
5244 // Cut, Copy
5245 if (g.PlatformIO.Platform_SetClipboardTextFn != NULL)
5246 {
5247 // SetClipboardText() only takes null terminated strings + state->TextSrc may point to read-only user buffer, so we need to make a copy.
5248 const int ib = state->HasSelection() ? ImMin(state->Stb->select_start, state->Stb->select_end) : 0;
5249 const int ie = state->HasSelection() ? ImMax(state->Stb->select_start, state->Stb->select_end) : state->TextLen;
5250 g.TempBuffer.reserve(ie - ib + 1);
5251 memcpy(g.TempBuffer.Data, state->TextSrc + ib, ie - ib);
5252 g.TempBuffer.Data[ie - ib] = 0;
5253 SetClipboardText(g.TempBuffer.Data);
5254 }
5255 if (is_cut)
5256 {
5257 if (!state->HasSelection())
5258 state->SelectAll();
5259 state->CursorFollow = true;
5260 stb_textedit_cut(state, state->Stb);
5261 }
5262 }
5263 else if (is_paste)
5264 {
5265 if (const char* clipboard = GetClipboardText())
5266 {
5267 // Filter pasted buffer
5268 const int clipboard_len = (int)ImStrlen(clipboard);
5269 const char* clipboard_end = clipboard + clipboard_len;
5270 ImVector<char> clipboard_filtered;
5271 clipboard_filtered.reserve(clipboard_len + 1);
5272 for (const char* s = clipboard; *s != 0; )
5273 {
5274 unsigned int c;
5275 int in_len = ImTextCharFromUtf8(&c, s, clipboard_end);
5276 s += in_len;
5277 if (!InputTextFilterCharacter(&g, state, &c, callback, callback_user_data, true))
5278 continue;
5279 char c_utf8[5];
5280 ImTextCharToUtf8(c_utf8, c);
5281 int out_len = (int)ImStrlen(c_utf8);
5282 clipboard_filtered.resize(clipboard_filtered.Size + out_len);
5283 memcpy(clipboard_filtered.Data + clipboard_filtered.Size - out_len, c_utf8, out_len);
5284 }
5285 if (clipboard_filtered.Size > 0) // If everything was filtered, ignore the pasting operation
5286 {
5287 clipboard_filtered.push_back(0);
5288 stb_textedit_paste(state, state->Stb, clipboard_filtered.Data, clipboard_filtered.Size - 1);
5289 state->CursorFollow = true;
5290 }
5291 }
5292 }
5293
5294 // Update render selection flag after events have been handled, so selection highlight can be displayed during the same frame.
5295 render_selection |= state->HasSelection() && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
5296 }
5297
5298 // Process revert and user callbacks
5299 const char* apply_new_text = NULL;
5300 int apply_new_text_length = 0;
5301 if (g.ActiveId == id)
5302 {
5303 IM_ASSERT(state != NULL);
5304 if (revert_edit && !is_readonly)
5305 {
5306 if (flags & ImGuiInputTextFlags_EscapeClearsAll)
5307 {
5308 // Clear input
5309 IM_ASSERT(state->TextA.Data[0] != 0);
5310 apply_new_text = "";
5311 apply_new_text_length = 0;
5312 value_changed = true;
5313 char empty_string = 0;
5314 stb_textedit_replace(state, state->Stb, &empty_string, 0);
5315 }
5316 else if (strcmp(state->TextA.Data, state->TextToRevertTo.Data) != 0)
5317 {
5318 apply_new_text = state->TextToRevertTo.Data;
5319 apply_new_text_length = state->TextToRevertTo.Size - 1;
5320
5321 // Restore initial value. Only return true if restoring to the initial value changes the current buffer contents.
5322 // Push records into the undo stack so we can Ctrl+Z the revert operation itself
5323 value_changed = true;
5324 stb_textedit_replace(state, state->Stb, state->TextToRevertTo.Data, state->TextToRevertTo.Size - 1);
5325 }
5326 }
5327
5328 // User callback
5329 if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackEdit | ImGuiInputTextFlags_CallbackAlways)) != 0)
5330 {
5331 IM_ASSERT(callback != NULL);
5332
5333 // The reason we specify the usage semantic (Completion/History) is that Completion needs to disable keyboard TABBING at the moment.
5334 ImGuiInputTextFlags event_flag = 0;
5335 ImGuiKey event_key = ImGuiKey_None;
5336 if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && Shortcut(ImGuiKey_Tab, 0, id))
5337 {
5338 event_flag = ImGuiInputTextFlags_CallbackCompletion;
5339 event_key = ImGuiKey_Tab;
5340 }
5341 else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_UpArrow))
5342 {
5343 event_flag = ImGuiInputTextFlags_CallbackHistory;
5344 event_key = ImGuiKey_UpArrow;
5345 }
5346 else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_DownArrow))
5347 {
5348 event_flag = ImGuiInputTextFlags_CallbackHistory;
5349 event_key = ImGuiKey_DownArrow;
5350 }
5351 else if ((flags & ImGuiInputTextFlags_CallbackEdit) && state->EditedThisFrame)
5352 {
5353 event_flag = ImGuiInputTextFlags_CallbackEdit;
5354 }
5355 else if (flags & ImGuiInputTextFlags_CallbackAlways)
5356 {
5357 event_flag = ImGuiInputTextFlags_CallbackAlways;
5358 }
5359
5360 if (event_flag)
5361 {
5362 ImGuiInputTextCallbackData callback_data;
5363 callback_data.Ctx = &g;
5364 callback_data.ID = id;
5365 callback_data.Flags = flags;
5366 callback_data.EventFlag = event_flag;
5367 callback_data.EventActivated = (g.ActiveId == state->ID && g.ActiveIdIsJustActivated);
5368 callback_data.UserData = callback_user_data;
5369
5370 // FIXME-OPT: Undo stack reconcile needs a backup of the data until we rework API, see #7925
5371 char* callback_buf = is_readonly ? buf : state->TextA.Data;
5372 IM_ASSERT(callback_buf == state->TextSrc);
5373 state->CallbackTextBackup.resize(state->TextLen + 1);
5374 memcpy(state->CallbackTextBackup.Data, callback_buf, state->TextLen + 1);
5375
5376 callback_data.EventKey = event_key;
5377 callback_data.Buf = callback_buf;
5378 callback_data.BufTextLen = state->TextLen;
5379 callback_data.BufSize = state->BufCapacity;
5380 callback_data.BufDirty = false;
5381 callback_data.CursorPos = state->Stb->cursor;
5382 callback_data.SelectionStart = state->Stb->select_start;
5383 callback_data.SelectionEnd = state->Stb->select_end;
5384
5385 // Call user code
5386 callback(&callback_data);
5387
5388 // Read back what user may have modified
5389 callback_buf = is_readonly ? buf : state->TextA.Data; // Pointer may have been invalidated by a resize callback
5390 IM_ASSERT(callback_data.Buf == callback_buf); // Invalid to modify those fields
5391 IM_ASSERT(callback_data.BufSize == state->BufCapacity);
5392 IM_ASSERT(callback_data.Flags == flags);
5393 if (callback_data.BufDirty || callback_data.CursorPos != state->Stb->cursor)
5394 state->CursorFollow = true;
5395 state->Stb->cursor = ImClamp(callback_data.CursorPos, 0, callback_data.BufTextLen);
5396 state->Stb->select_start = ImClamp(callback_data.SelectionStart, 0, callback_data.BufTextLen);
5397 state->Stb->select_end = ImClamp(callback_data.SelectionEnd, 0, callback_data.BufTextLen);
5398 if (callback_data.BufDirty)
5399 {
5400 // Callback may update buffer and thus set buf_dirty even in read-only mode.
5401 IM_ASSERT(callback_data.BufTextLen == (int)ImStrlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text!
5402 InputTextReconcileUndoState(state, state->CallbackTextBackup.Data, state->CallbackTextBackup.Size - 1, callback_data.Buf, callback_data.BufTextLen);
5403 state->TextLen = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen()
5404 state->EditedBefore = state->EditedThisFrame = true;
5405 state->CursorAnimReset();
5406 }
5407 }
5408 }
5409
5410 // Write back result string if modified.
5411 // FIXME-OPT: Could mark dirty state from the stb_textedit callbacks
5412 if (!is_readonly)
5413 {
5414 if (g.LastItemData.ItemFlags & ImGuiItemFlags_LiveEditOnInputText)
5415 {
5416 // Apply when modified
5417 if (strcmp(state->TextSrc, buf) != 0)
5418 {
5419 apply_new_text = state->TextSrc;
5420 apply_new_text_length = state->TextLen;
5421 value_changed = true;
5422 }
5423 }
5424 else
5425 {
5426 // Apply on validation/deactivation, otherwise cancel out previous apply attempts (e.g. revert)
5427 value_changed = ((validated || clear_active_id || revert_edit) && strcmp(state->TextSrc, buf) != 0);
5428 apply_new_text = value_changed ? state->TextSrc : NULL;
5429 apply_new_text_length = value_changed ? state->TextLen : 0;
5430 }
5431 }
5432 }
5433
5434 // Handle reapplying final data on deactivation (see InputTextDeactivateHook() for details)
5435 // This is used when e.g. losing focus or tabbing out into another InputText() which may already be using the temp buffer.
5436 if (g.InputTextDeactivatedState.ID == id)
5437 {
5438 // The state only exists after an Edit. IsItemDeactivatedAfterEdit() is not valid in every code path (see "widgets_inputtext_status_noliveedit" test).
5439 if ((g.ActiveId != id && IsItemDeactivated()) || (g.ActiveId == id && (flags & ImGuiInputTextFlags_TempInput)))
5440 if (!is_readonly && strcmp(g.InputTextDeactivatedState.TextA.Data, buf) != 0)
5441 {
5442 apply_new_text = g.InputTextDeactivatedState.TextA.Data;
5443 apply_new_text_length = g.InputTextDeactivatedState.TextA.Size - 1;
5444 value_changed = true;
5445 //IMGUI_DEBUG_LOG("InputText(): apply Deactivated data for 0x%08X: \"%.*s\".\n", id, apply_new_text_length, apply_new_text);
5446 }
5447 g.InputTextDeactivatedState.ID = 0;
5448 }
5449
5450 // Write back result to user buffer. This can currently only happen when (g.ActiveId == id) or when just deactivated.
5451 // - As soon as the InputText() is active, our stored in-widget value gets priority over any underlying modification of the user buffer.
5452 // - Make sure we always reapply the live buffer back to the input/user buffer before clearing ActiveId, even thought strictly speaking
5453 // it was not modified on this frame. This allows the user to use InputText() without maintaining any user-side storage.
5454 // (PS: if you use this property together with ImGuiInputTextFlags_CallbackResize, you are at the risk of recreating a temporary
5455 // allocated/string object every frame. Which in the grand scheme of scheme is nothing, but isn't dear imgui vibe).
5456 if (apply_new_text != NULL)
5457 {
5458 IM_ASSERT(apply_new_text_length >= 0);
5459 if (is_resizable)
5460 {
5461 ImGuiInputTextCallbackData callback_data;
5462 callback_data.Ctx = &g;
5463 callback_data.ID = id;
5464 callback_data.Flags = flags;
5465 callback_data.EventFlag = ImGuiInputTextFlags_CallbackResize;
5466 callback_data.EventActivated = (state != NULL && g.ActiveId == state->ID && g.ActiveIdIsJustActivated);
5467 callback_data.Buf = buf;
5468 callback_data.BufTextLen = apply_new_text_length;
5469 callback_data.BufSize = ImMax(buf_size, apply_new_text_length + 1);
5470 callback_data.UserData = callback_user_data;
5471 callback(&callback_data);
5472 buf = callback_data.Buf;
5473 buf_size = callback_data.BufSize;
5474 apply_new_text_length = ImMin(callback_data.BufTextLen, buf_size - 1);
5475 IM_ASSERT(apply_new_text_length <= buf_size);
5476 }
5477 //IMGUI_DEBUG_PRINT("InputText(\"%s\"): apply_new_text length %d\n", label, apply_new_text_length);
5478
5479 // If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size.
5480 ImStrncpy(buf, apply_new_text, ImMin(apply_new_text_length + 1, buf_size));
5481 }
5482
5483 // Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value)
5484 // Otherwise request text input ahead for next frame.
5485 if (g.ActiveId == id && clear_active_id)
5486 {
5487 state->ID = 0; // To avoid InputTextDeactivateHook() unnecessarily running, which wouldn't be harmful but wasteful.
5488 ClearActiveID();
5489 state->ID = id;
5490 }
5491
5492 // Render frame
5493 if (!is_multiline)
5494 {
5495 RenderNavCursor(frame_bb, id);
5496 RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
5497 }
5498
5499 ImVec2 draw_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding;
5500 ImRect clip_rect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + inner_size.x, frame_bb.Min.y + inner_size.y); // Not using frame_bb.Max because we have adjusted size
5501 if (is_multiline)
5502 clip_rect.ClipWith(draw_window->ClipRect);
5503
5504 // Set upper limit of single-line InputTextEx() at 2 million characters strings. The current pathological worst case is a long line
5505 // without any carriage return, which would makes ImFont::RenderText() reserve too many vertices and probably crash. Avoid it altogether.
5506 // Note that we only use this limit on single-line InputText(), so a pathologically large line on a InputTextMultiline() would still crash.
5507 const int buf_display_max_length = 2 * 1024 * 1024;
5508 const char* buf_display = buf_display_from_state ? state->TextA.Data : buf; //-V595
5509 const char* buf_display_end = NULL; // We have specialized paths below for setting the length
5510
5511 // Display hint when contents is empty
5512 // At this point we need to handle the possibility that a callback could have modified the underlying buffer (#8368)
5513 const bool new_is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0);
5514 if (new_is_displaying_hint != is_displaying_hint)
5515 {
5516 if (is_password && !is_displaying_hint)
5517 PopPasswordFont();
5518 is_displaying_hint = new_is_displaying_hint;
5519 if (is_password && !is_displaying_hint)
5520 PushPasswordFont();
5521 }
5522 if (is_displaying_hint)
5523 {
5524 buf_display = hint;
5525 buf_display_end = hint + ImStrlen(hint);
5526 }
5527 else
5528 {
5529 if (render_cursor || render_selection || g.ActiveId == id)
5530 buf_display_end = buf_display + state->TextLen; //-V595
5531 else if (is_multiline && !is_wordwrap)
5532 buf_display_end = NULL; // Inactive multi-line: end of buffer will be output by InputTextLineIndexBuild() special strchr() path.
5533 else
5534 buf_display_end = buf_display + ImStrlen(buf_display);
5535 }
5536
5537 // Calculate visibility
5538 int line_visible_n0 = 0, line_visible_n1 = 1;
5539 if (is_multiline)
5540 CalcClipRectVisibleItemsY(clip_rect, draw_pos, g.FontSize, &line_visible_n0, &line_visible_n1);
5541
5542 // Build line index for easy data access (makes code below simpler and faster)
5543 ImGuiTextIndex* line_index = &g.InputTextLineIndex;
5544 line_index->Offsets.resize(0);
5545 int line_count = 1;
5546 if (is_multiline)
5547 {
5548 // If scrolling is expected to change build full index.
5549 // FIXME-OPT: Could append to index when new value of line_visible_n1 becomes bigger, see second call to CalcClipRectVisibleItemsY() below.
5550 bool will_scroll_y = state && ((state->CursorFollow && render_cursor) || (state->CursorCenterY && (render_cursor || render_selection)));
5551 line_count = InputTextLineIndexBuild(flags, line_index, buf_display, buf_display_end, wrap_width, will_scroll_y ? INT_MAX : line_visible_n1 + 1, buf_display_end ? NULL : &buf_display_end);
5552 }
5553 line_index->EndOffset = (int)(buf_display_end - buf_display);
5554 line_visible_n1 = ImMin(line_visible_n1, line_count);
5555
5556 // Store text height (we don't need width)
5557 float text_size_y = line_count * g.FontSize;
5558 //GetForegroundDrawList()->AddRect(draw_pos + ImVec2(0, line_visible_n0 * g.FontSize), draw_pos + ImVec2(frame_size.x, line_visible_n1 * g.FontSize), IM_COL32(255, 0, 0, 255));
5559
5560 // Calculate blinking cursor position
5561 const ImVec2 cursor_offset = render_cursor && state ? InputTextLineIndexGetPosOffset(g, state, line_index, buf_display, buf_display_end, state->Stb->cursor) : ImVec2(0.0f, 0.0f);
5562 ImVec2 draw_scroll;
5563
5564 // Render text. We currently only render selection when the widget is active or while scrolling.
5565 const ImU32 text_col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
5566 if (render_cursor || render_selection)
5567 {
5568 // Render text (with cursor and selection)
5569 // This is going to be messy. We need to:
5570 // - Display the text (this alone can be more easily clipped)
5571 // - Handle scrolling, highlight selection, display cursor (those all requires some form of 1d->2d cursor position calculation)
5572 // - Measure text height (for scrollbar)
5573 // 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)
5574 // FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8.
5575 IM_ASSERT(state != NULL);
5576 state->LineCount = line_count;
5577
5578 // Scroll
5579 float new_scroll_y = scroll_y;
5580 if (render_cursor && state->CursorFollow)
5581 {
5582 // Horizontal scroll in chunks of quarter width
5583 if (!(flags & ImGuiInputTextFlags_NoHorizontalScroll))
5584 {
5585 const float scroll_increment_x = inner_size.x * 0.25f;
5586 const float visible_width = inner_size.x - style.FramePadding.x;
5587 if (cursor_offset.x < state->Scroll.x)
5588 state->Scroll.x = IM_TRUNC(ImMax(0.0f, cursor_offset.x - scroll_increment_x));
5589 else if (cursor_offset.x - visible_width >= state->Scroll.x)
5590 state->Scroll.x = IM_TRUNC(cursor_offset.x - visible_width + scroll_increment_x);
5591 }
5592 else
5593 {
5594 state->Scroll.x = 0.0f;
5595 }
5596
5597 // Vertical scroll
5598 if (is_multiline)
5599 {
5600 // Test if cursor is vertically visible
5601 if (cursor_offset.y - g.FontSize < scroll_y)
5602 new_scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize);
5603 else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y)
5604 new_scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f;
5605 }
5606 state->CursorFollow = false;
5607 }
5608 if (state->CursorCenterY)
5609 {
5610 if (is_multiline)
5611 new_scroll_y = cursor_offset.y - g.FontSize - (inner_size.y * 0.5f - style.FramePadding.y);
5612 state->CursorCenterY = false;
5613 render_cursor = false;
5614 }
5615 if (new_scroll_y != scroll_y)
5616 {
5617 const float scroll_max_y = ImMax((text_size_y + style.FramePadding.y * 2.0f) - inner_size.y, 0.0f);
5618 scroll_y = ImClamp(new_scroll_y, 0.0f, scroll_max_y);
5619 draw_pos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag
5620 draw_window->Scroll.y = scroll_y;
5621 CalcClipRectVisibleItemsY(clip_rect, draw_pos, g.FontSize, &line_visible_n0, &line_visible_n1);
5622 line_visible_n1 = ImMin(line_visible_n1, line_count);
5623 }
5624
5625 // Draw selection
5626 draw_scroll.x = state->Scroll.x;
5627 if (render_selection)
5628 {
5629 const 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.
5630 const float bg_offy_up = is_multiline ? 0.0f : -1.0f; // FIXME-DPI: those offsets should be part of the style? they don't play so well with multi-line selection.
5631 const float bg_offy_dn = is_multiline ? 0.0f : 2.0f;
5632 const float bg_eol_width = IM_TRUNC(g.FontBaked->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines
5633
5634 const char* text_selected_begin = buf_display + ImMin(state->Stb->select_start, state->Stb->select_end);
5635 const char* text_selected_end = buf_display + ImMax(state->Stb->select_start, state->Stb->select_end);
5636 for (int line_n = line_visible_n0; line_n < line_visible_n1; line_n++)
5637 {
5638 const char* p = line_index->get_line_begin(buf_display, line_n);
5639 const char* p_eol = line_index->get_line_end(buf_display, line_n);
5640 const bool p_eol_is_wrap = (p_eol < buf_display_end && *p_eol != '\n');
5641 if (p_eol_is_wrap)
5642 p_eol++;
5643 const char* line_selected_begin = (text_selected_begin > p) ? text_selected_begin : p;
5644 const char* line_selected_end = (text_selected_end < p_eol) ? text_selected_end : p_eol;
5645
5646 float rect_width = 0.0f;
5647 if (line_selected_begin < line_selected_end)
5648 rect_width += CalcTextSize(line_selected_begin, line_selected_end).x;
5649 if (text_selected_begin <= p_eol && text_selected_end > p_eol && !p_eol_is_wrap)
5650 rect_width += bg_eol_width; // So we can see selected empty lines
5651 if (rect_width == 0.0f)
5652 continue;
5653
5654 ImRect rect;
5655 rect.Min.x = draw_pos.x - draw_scroll.x + CalcTextSize(p, line_selected_begin).x;
5656 rect.Min.y = draw_pos.y - draw_scroll.y + line_n * g.FontSize;
5657 rect.Max.x = rect.Min.x + rect_width;
5658 rect.Max.y = rect.Min.y + bg_offy_dn + g.FontSize;
5659 rect.Min.y += bg_offy_up;
5660 rect.ClipWith(clip_rect);
5661 draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color);
5662 }
5663 }
5664 }
5665
5666 // Find render position for right alignment (single-line only)
5667 if (g.ActiveId != id && (flags & ImGuiInputTextFlags_ElideLeft) && !render_cursor && !render_selection)
5668 draw_pos.x = ImMin(draw_pos.x, frame_bb.Max.x - CalcTextSize(buf_display, NULL).x - style.FramePadding.x);
5669 //draw_scroll.x = state->Scroll.x; // Preserve scroll when inactive?
5670
5671 // Render text
5672 if ((is_multiline || (buf_display_end - buf_display) < buf_display_max_length) && (text_col & IM_COL32_A_MASK) && (line_visible_n0 < line_visible_n1))
5673 g.Font->RenderText(draw_window->DrawList, g.FontSize,
5674 draw_pos - draw_scroll + ImVec2(0.0f, line_visible_n0 * g.FontSize),
5675 text_col, clip_rect.AsVec4(),
5676 line_index->get_line_begin(buf_display, line_visible_n0),
5677 line_index->get_line_end(buf_display, line_visible_n1 - 1),
5678 wrap_width, ImDrawTextFlags_WrapKeepBlanks | ImDrawTextFlags_CpuFineClip);
5679
5680 // Render blinking cursor
5681 if (render_cursor)
5682 {
5683 state->CursorAnim += io.DeltaTime;
5684 bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f;
5685 ImVec2 cursor_screen_pos = ImTrunc(draw_pos + cursor_offset - draw_scroll);
5686 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);
5687 if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect))
5688 draw_window->DrawList->AddLineV(cursor_screen_rect.Min.x, cursor_screen_rect.Min.y, cursor_screen_rect.Max.y, GetColorU32(ImGuiCol_InputTextCursor), style.InputTextCursorSize);
5689
5690 // 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.)
5691 // This is required for some backends (SDL3) to start emitting character/text inputs.
5692 // As per #6341, make sure we don't set that on the deactivating frame.
5693 if (!is_readonly && g.ActiveId == id)
5694 {
5695 ImGuiPlatformImeData* ime_data = &g.PlatformImeData; // (this is a public struct, passed to io.Platform_SetImeDataFn() handler)
5696 ime_data->WantVisible = true;
5697 ime_data->WantTextInput = true;
5698 ime_data->InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize);
5699 ime_data->InputLineHeight = g.FontSize;
5700 ime_data->ViewportId = window->Viewport->ID;
5701 }
5702 }
5703
5704 if (is_password && !is_displaying_hint)
5705 PopPasswordFont();
5706
5707 if (is_multiline)
5708 {
5709 // For focus requests to work on our multiline we need to ensure our child ItemAdd() call specifies the ImGuiItemFlags_Inputable (see #4761, #7870)...
5710 Dummy(ImVec2(0.0f, text_size_y + style.FramePadding.y));
5711 g.NextItemData.ItemFlagsSet |= (ImGuiItemFlags)ImGuiItemFlags_Inputable | ImGuiItemFlags_NoTabStop;
5712 EndChild();
5713 item_data_backup.StatusFlags |= (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredWindow);
5714
5715 // ...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...
5716 // FIXME: This quite messy/tricky, should attempt to get rid of the child window.
5717 EndGroup();
5718 if (g.LastItemData.ID == 0 || g.LastItemData.ID != GetWindowScrollbarID(draw_window, ImGuiAxis_Y))
5719 {
5720 g.LastItemData.ID = id;
5721 g.LastItemData.ItemFlags = item_data_backup.ItemFlags;
5722 g.LastItemData.StatusFlags = item_data_backup.StatusFlags;
5723 }
5724 }
5725 if (state && is_readonly)
5726 state->TextSrc = NULL;
5727
5728 // Log as text
5729 if (g.LogEnabled && (!is_password || is_displaying_hint))
5730 {
5731 LogSetNextTextDecoration("{", "}");
5732 LogRenderedText(&draw_pos, buf_display, buf_display_end);
5733 }
5734
5735 if (label_size.x > 0)
5736 RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label, label_end, false);
5737
5738 if (value_changed)
5739 MarkItemEdited(id);
5740
5741 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
5742 if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0)
5743 return validated;
5744 else
5745 return value_changed;
5746}
5747
5748void ImGui::DebugNodeInputTextState(ImGuiInputTextState* state)
5749{
5750#ifndef IMGUI_DISABLE_DEBUG_TOOLS
5751 ImGuiContext& g = *GImGui;
5752 ImStb::STB_TexteditState* stb_state = state->Stb;
5753 ImStb::StbUndoState* undo_state = &stb_state->undostate;
5754 Text("ID: 0x%08X, ActiveID: 0x%08X", state->ID, g.ActiveId);
5755 DebugLocateItemOnHover(state->ID);
5756 Text("TextLen: %d, Cursor: %d%s, Selection: %d..%d", state->TextLen, stb_state->cursor,
5757 (state->Flags & ImGuiInputTextFlags_WordWrap) ? (state->LastMoveDirectionLR == ImGuiDir_Left ? " (L)" : " (R)") : "",
5758 stb_state->select_start, stb_state->select_end);
5759 Text("BufCapacity: %d, LineCount: %d", state->BufCapacity, state->LineCount);
5760 Text("(Internal Buffer: TextA Size: %d, Capacity: %d)", state->TextA.Size, state->TextA.Capacity);
5761 Text("has_preferred_x: %d (%.2f)", stb_state->has_preferred_x, stb_state->preferred_x);
5762 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);
5763 if (BeginChild("undopoints", ImVec2(0.0f, GetTextLineHeight() * 10), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY)) // Visualize undo state
5764 {
5765 PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
5766 for (int n = 0; n < IMSTB_TEXTEDIT_UNDOSTATECOUNT; n++)
5767 {
5768 ImStb::StbUndoRecord* undo_rec = &undo_state->undo_rec[n];
5769 const char undo_rec_type = (n < undo_state->undo_point) ? 'u' : (n >= undo_state->redo_point) ? 'r' : ' ';
5770 if (undo_rec_type == ' ')
5771 BeginDisabled();
5772 const int buf_preview_len = (undo_rec_type != ' ' && undo_rec->char_storage != -1) ? undo_rec->insert_length : 0;
5773 const char* buf_preview_str = undo_state->undo_char + undo_rec->char_storage;
5774 Text("%c [%02d] where %03d, insert %03d, delete %03d, char_storage %03d \"%.*s\"",
5775 undo_rec_type, n, undo_rec->where, undo_rec->insert_length, undo_rec->delete_length, undo_rec->char_storage, buf_preview_len, buf_preview_str);
5776 if (undo_rec_type == ' ')
5777 EndDisabled();
5778 }
5779 PopStyleVar();
5780 }
5781 EndChild();
5782#else
5783 IM_UNUSED(state);
5784#endif
5785}
5786
5787//-------------------------------------------------------------------------
5788// [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc.
5789//-------------------------------------------------------------------------
5790// - ColorEdit3()
5791// - ColorEdit4()
5792// - ColorPicker3()
5793// - RenderColorRectWithAlphaCheckerboard() [Internal]
5794// - ColorPicker4()
5795// - ColorButton()
5796// - ColorTooltip() [Internal]
5797// - ColorEditOptionsPopup() [Internal]
5798// - ColorPickerOptionsPopup() [Internal]
5799//-------------------------------------------------------------------------
5800
5801bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags)
5802{
5803 return ColorEdit4(label, col, flags | ImGuiColorEditFlags_NoAlpha);
5804}
5805
5806static void ColorEditRestoreH(const float* col, float* H)
5807{
5808 ImGuiContext& g = *GImGui;
5809 IM_ASSERT(g.ColorEditCurrentID != 0);
5810 if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0)))
5811 return;
5812 *H = g.ColorEditSavedHue;
5813}
5814
5815// ColorEdit supports RGB and HSV inputs. In case of RGB input resulting color may have undefined hue and/or saturation.
5816// Since widget displays both RGB and HSV values we must preserve hue and saturation to prevent these values resetting.
5817static void ColorEditRestoreHS(const float* col, float* H, float* S, float* V)
5818{
5819 ImGuiContext& g = *GImGui;
5820 IM_ASSERT(g.ColorEditCurrentID != 0);
5821 if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0)))
5822 return;
5823
5824 // When S == 0, H is undefined.
5825 // When H == 1 it wraps around to 0.
5826 if (*S == 0.0f || (*H == 0.0f && g.ColorEditSavedHue == 1))
5827 *H = g.ColorEditSavedHue;
5828
5829 // When V == 0, S is undefined.
5830 if (*V == 0.0f)
5831 *S = g.ColorEditSavedSat;
5832}
5833
5834// Edit colors components (each component in 0.0f..1.0f range).
5835// See enum ImGuiColorEditFlags_ for available options. e.g. Only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
5836// 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.
5837bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags)
5838{
5839 ImGuiWindow* window = GetCurrentWindow();
5840 if (window->SkipItems)
5841 return false;
5842
5843 ImGuiContext& g = *GImGui;
5844 const ImGuiStyle& style = g.Style;
5845 const float square_sz = GetFrameHeight();
5846 const char* label_display_end = FindRenderedTextEnd(label);
5847 float w_full = CalcItemWidth();
5848 g.NextItemData.ClearFlags();
5849
5850 BeginGroup();
5851 PushID(label);
5852 const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0);
5853 if (set_current_color_edit_id)
5854 g.ColorEditCurrentID = window->IDStack.back();
5855
5856 // If we're not showing any slider there's no point in doing any HSV conversions
5857 const ImGuiColorEditFlags flags_untouched = flags;
5858 if (flags & ImGuiColorEditFlags_NoInputs)
5859 flags = (flags & (~ImGuiColorEditFlags_DisplayMask_)) | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_NoOptions;
5860
5861 // Context menu: display and modify options (before defaults are applied)
5862 if (!(flags & ImGuiColorEditFlags_NoOptions))
5863 ColorEditOptionsPopup(col, flags);
5864
5865 // Read stored options
5866 if (!(flags & ImGuiColorEditFlags_DisplayMask_))
5867 flags |= (g.IO.ConfigColorEditFlags & ImGuiColorEditFlags_DisplayMask_);
5868 if (!(flags & ImGuiColorEditFlags_DataTypeMask_))
5869 flags |= (g.IO.ConfigColorEditFlags & ImGuiColorEditFlags_DataTypeMask_);
5870 if (!(flags & ImGuiColorEditFlags_PickerMask_))
5871 flags |= (g.IO.ConfigColorEditFlags & ImGuiColorEditFlags_PickerMask_);
5872 if (!(flags & ImGuiColorEditFlags_InputMask_))
5873 flags |= (g.IO.ConfigColorEditFlags & ImGuiColorEditFlags_InputMask_);
5874 flags |= (g.IO.ConfigColorEditFlags & ~(ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_));
5875 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check that only 1 is selected
5876 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
5877
5878 const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0;
5879 const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0;
5880 const int components = alpha ? 4 : 3;
5881 const float w_button = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x);
5882 const float w_inputs = ImMax(w_full - w_button, 1.0f);
5883 w_full = w_inputs + w_button;
5884
5885 // Convert to the formats we need
5886 float f[4] = { col[0], col[1], col[2], alpha ? col[3] : 1.0f };
5887 if ((flags & ImGuiColorEditFlags_InputHSV) && (flags & ImGuiColorEditFlags_DisplayRGB))
5888 ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]);
5889 else if ((flags & ImGuiColorEditFlags_InputRGB) && (flags & ImGuiColorEditFlags_DisplayHSV))
5890 {
5891 // Hue is lost when converting from grayscale rgb (saturation=0). Restore it.
5892 ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]);
5893 ColorEditRestoreHS(col, &f[0], &f[1], &f[2]);
5894 }
5895 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]) };
5896
5897 bool value_changed = false;
5898 bool value_changed_as_float = false;
5899
5900 const ImVec2 pos = window->DC.CursorPos;
5901 const float inputs_offset_x = (style.ColorButtonPosition == ImGuiDir_Left) ? w_button : 0.0f;
5902 window->DC.CursorPos.x = pos.x + inputs_offset_x;
5903
5904 if ((flags & (ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5905 {
5906 // RGB/HSV 0..255 Sliders
5907 const float w_items = w_inputs - style.ItemInnerSpacing.x * (components - 1);
5908 const float w_per_component = IM_TRUNC(w_items / components);
5909 const bool draw_color_marker = (flags & (ImGuiColorEditFlags_DisplayHSV | ImGuiColorEditFlags_NoColorMarkers)) == 0;
5910
5911 const bool hide_prefix = draw_color_marker || (w_per_component <= CalcTextSize((flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x);
5912 static const char* ids[4] = { "##X", "##Y", "##Z", "##W" };
5913 static const char* fmt_table_int[3][4] =
5914 {
5915 { "%3d", "%3d", "%3d", "%3d" }, // Short display
5916 { "R:%3d", "G:%3d", "B:%3d", "A:%3d" }, // Long display for RGBA
5917 { "H:%3d", "S:%3d", "V:%3d", "A:%3d" } // Long display for HSVA
5918 };
5919 static const char* fmt_table_float[3][4] =
5920 {
5921 { "%0.3f", "%0.3f", "%0.3f", "%0.3f" }, // Short display
5922 { "R:%0.3f", "G:%0.3f", "B:%0.3f", "A:%0.3f" }, // Long display for RGBA
5923 { "H:%0.3f", "S:%0.3f", "V:%0.3f", "A:%0.3f" } // Long display for HSVA
5924 };
5925 const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_DisplayHSV) ? 2 : 1;
5926 const ImGuiSliderFlags drag_flags = draw_color_marker ? ImGuiSliderFlags_ColorMarkers : ImGuiSliderFlags_None;
5927
5928 float prev_split = 0.0f;
5929 for (int n = 0; n < components; n++)
5930 {
5931 if (n > 0)
5932 SameLine(0, style.ItemInnerSpacing.x);
5933 float next_split = IM_TRUNC(w_items * (n + 1) / components);
5934 SetNextItemWidth(ImMax(next_split - prev_split, 1.0f));
5935 prev_split = next_split;
5936 if (draw_color_marker)
5937 SetNextItemColorMarker(GDefaultRgbaColorMarkers[n]);
5938
5939 // FIXME: When ImGuiColorEditFlags_HDR flag is passed HS values snap in weird ways when SV values go below 0.
5940 if (flags & ImGuiColorEditFlags_Float)
5941 {
5942 value_changed |= DragFloat(ids[n], &f[n], 1.0f / 255.0f, 0.0f, hdr ? 0.0f : 1.0f, fmt_table_float[fmt_idx][n], drag_flags);
5943 value_changed_as_float |= value_changed;
5944 }
5945 else
5946 {
5947 value_changed |= DragInt(ids[n], &i[n], 1.0f, 0, hdr ? 0 : 255, fmt_table_int[fmt_idx][n], drag_flags);
5948 }
5949 if (!(flags & ImGuiColorEditFlags_NoOptions))
5950 OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5951 }
5952 }
5953 else if ((flags & ImGuiColorEditFlags_DisplayHex) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5954 {
5955 // RGB Hexadecimal Input
5956 char buf[64];
5957 if (alpha)
5958 ImFormatString(buf, IM_COUNTOF(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));
5959 else
5960 ImFormatString(buf, IM_COUNTOF(buf), "#%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255));
5961 SetNextItemWidth(w_inputs);
5962 if (InputText("##Text", buf, IM_COUNTOF(buf), ImGuiInputTextFlags_CharsUppercase))
5963 {
5964 value_changed = true;
5965 char* p = buf;
5966 while (*p == '#' || ImCharIsBlankA(*p))
5967 p++;
5968 i[0] = i[1] = i[2] = 0;
5969 i[3] = 0xFF; // alpha default to 255 is not parsed by scanf (e.g. inputting #FFFFFF omitting alpha)
5970 int r;
5971 if (alpha)
5972 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)
5973 else
5974 r = sscanf(p, "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]);
5975 IM_UNUSED(r); // Fixes C6031: Return value ignored: 'sscanf'.
5976 }
5977 if (!(flags & ImGuiColorEditFlags_NoOptions))
5978 OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5979 }
5980
5981 ImGuiWindow* picker_active_window = NULL;
5982 if (!(flags & ImGuiColorEditFlags_NoSmallPreview))
5983 {
5984 const float button_offset_x = ((flags & ImGuiColorEditFlags_NoInputs) || (style.ColorButtonPosition == ImGuiDir_Left)) ? 0.0f : w_inputs + style.ItemInnerSpacing.x;
5985 window->DC.CursorPos = ImVec2(pos.x + button_offset_x, pos.y);
5986
5987 const ImVec4 col_v4(col[0], col[1], col[2], alpha ? col[3] : 1.0f);
5988 if (ColorButton("##ColorButton", col_v4, flags))
5989 {
5990 if (!(flags & ImGuiColorEditFlags_NoPicker))
5991 {
5992 // Store current color and open a picker
5993 g.ColorPickerRef = col_v4;
5994 OpenPopup("picker");
5995 SetNextWindowPos(g.LastItemData.Rect.GetBL() + ImVec2(0.0f, style.ItemSpacing.y));
5996 }
5997 }
5998 if (!(flags & ImGuiColorEditFlags_NoOptions))
5999 OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
6000
6001 if (BeginPopup("picker"))
6002 {
6003 if (g.CurrentWindow->BeginCount == 1)
6004 {
6005 picker_active_window = g.CurrentWindow;
6006 if (label != label_display_end)
6007 {
6008 TextEx(label, label_display_end);
6009 Spacing();
6010 }
6011 ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar;
6012 ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf;
6013 SetNextItemWidth(square_sz * 12.0f); // Use 256 + bar sizes?
6014 value_changed |= ColorPicker4("##picker", col, picker_flags, &g.ColorPickerRef.x);
6015 }
6016 EndPopup();
6017 }
6018 }
6019
6020 if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel))
6021 {
6022 // Position not necessarily next to last submitted button (e.g. if style.ColorButtonPosition == ImGuiDir_Left),
6023 // but we need to use SameLine() to setup baseline correctly. Might want to refactor SameLine() to simplify this.
6024 SameLine(0.0f, style.ItemInnerSpacing.x);
6025 window->DC.CursorPos.x = pos.x + ((flags & ImGuiColorEditFlags_NoInputs) ? w_button : w_full + style.ItemInnerSpacing.x);
6026 TextEx(label, label_display_end);
6027 }
6028
6029 // Convert back
6030 if (value_changed && picker_active_window == NULL)
6031 {
6032 if (!value_changed_as_float)
6033 for (int n = 0; n < 4; n++)
6034 f[n] = i[n] / 255.0f;
6035 if ((flags & ImGuiColorEditFlags_DisplayHSV) && (flags & ImGuiColorEditFlags_InputRGB))
6036 {
6037 g.ColorEditSavedHue = f[0];
6038 g.ColorEditSavedSat = f[1];
6039 ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]);
6040 g.ColorEditSavedID = g.ColorEditCurrentID;
6041 g.ColorEditSavedColor = ColorConvertFloat4ToU32(ImVec4(f[0], f[1], f[2], 0));
6042 }
6043 if ((flags & ImGuiColorEditFlags_DisplayRGB) && (flags & ImGuiColorEditFlags_InputHSV))
6044 ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]);
6045
6046 col[0] = f[0];
6047 col[1] = f[1];
6048 col[2] = f[2];
6049 if (alpha)
6050 col[3] = f[3];
6051 }
6052
6053 if (set_current_color_edit_id)
6054 g.ColorEditCurrentID = 0;
6055 PopID();
6056 EndGroup();
6057
6058 // Drag and Drop Target
6059 // NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test.
6060 if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget())
6061 {
6062 bool accepted_drag_drop = false;
6063 if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F))
6064 {
6065 memcpy((float*)col, payload->Data, sizeof(float) * 3); // Preserve alpha if any //-V512 //-V1086
6066 value_changed = accepted_drag_drop = true;
6067 }
6068 if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F))
6069 {
6070 memcpy((float*)col, payload->Data, sizeof(float) * components);
6071 value_changed = accepted_drag_drop = true;
6072 }
6073
6074 // Drag-drop payloads are always RGB
6075 if (accepted_drag_drop && (flags & ImGuiColorEditFlags_InputHSV))
6076 ColorConvertRGBtoHSV(col[0], col[1], col[2], col[0], col[1], col[2]);
6077 EndDragDropTarget();
6078 }
6079
6080 // When picker is being actively used, use its active id so IsItemActive() will function on ColorEdit4().
6081 if (picker_active_window && g.ActiveId != 0 && g.ActiveIdWindow == picker_active_window)
6082 g.LastItemData.ID = g.ActiveId;
6083
6084 if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
6085 MarkItemEdited(g.LastItemData.ID);
6086
6087 return value_changed;
6088}
6089
6090bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags)
6091{
6092 float col4[4] = { col[0], col[1], col[2], 1.0f };
6093 if (!ColorPicker4(label, col4, flags | ImGuiColorEditFlags_NoAlpha))
6094 return false;
6095 col[0] = col4[0]; col[1] = col4[1]; col[2] = col4[2];
6096 return true;
6097}
6098
6099// Helper for ColorPicker4()
6100static void RenderArrowsForVerticalBar(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w, float alpha)
6101{
6102 ImU32 alpha8 = IM_F32_TO_INT8_SAT(alpha);
6103 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));
6104 ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + half_sz.x, pos.y), half_sz, ImGuiDir_Right, IM_COL32(255,255,255,alpha8));
6105 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));
6106 ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + bar_w - half_sz.x, pos.y), half_sz, ImGuiDir_Left, IM_COL32(255,255,255,alpha8));
6107}
6108
6109// Note: ColorPicker4() only accesses 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
6110// (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.)
6111// 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..)
6112// 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)
6113bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags, const float* ref_col)
6114{
6115 ImGuiContext& g = *GImGui;
6116 ImGuiWindow* window = GetCurrentWindow();
6117 if (window->SkipItems)
6118 return false;
6119
6120 ImDrawList* draw_list = window->DrawList;
6121 ImGuiStyle& style = g.Style;
6122 ImGuiIO& io = g.IO;
6123
6124 const float width = CalcItemWidth();
6125 const bool is_readonly = ((g.NextItemData.ItemFlagsSet | g.CurrentItemFlags) & ImGuiItemFlags_ReadOnly) != 0;
6126 g.NextItemData.ClearFlags();
6127
6128 PushID(label);
6129 const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0);
6130 if (set_current_color_edit_id)
6131 g.ColorEditCurrentID = window->IDStack.back();
6132 BeginGroup();
6133
6134 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
6135 flags |= ImGuiColorEditFlags_NoSmallPreview;
6136
6137 // Context menu: display and store options.
6138 if (!(flags & ImGuiColorEditFlags_NoOptions))
6139 ColorPickerOptionsPopup(col, flags);
6140
6141 // Read stored options
6142 if (!(flags & ImGuiColorEditFlags_PickerMask_))
6143 flags |= ((g.IO.ConfigColorEditFlags & ImGuiColorEditFlags_PickerMask_) ? g.IO.ConfigColorEditFlags : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_PickerMask_;
6144 if (!(flags & ImGuiColorEditFlags_InputMask_))
6145 flags |= ((g.IO.ConfigColorEditFlags & ImGuiColorEditFlags_InputMask_) ? g.IO.ConfigColorEditFlags : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_InputMask_;
6146 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check that only 1 is selected
6147 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
6148 if (!(flags & ImGuiColorEditFlags_NoOptions))
6149 flags |= (g.IO.ConfigColorEditFlags & ImGuiColorEditFlags_AlphaBar);
6150
6151 // Setup
6152 int components = (flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4;
6153 bool alpha_bar = (flags & ImGuiColorEditFlags_AlphaBar) && !(flags & ImGuiColorEditFlags_NoAlpha);
6154 ImVec2 picker_pos = window->DC.CursorPos;
6155 float square_sz = GetFrameHeight();
6156 float bars_width = square_sz; // Arbitrary smallish width of Hue/Alpha picking bars
6157 float sv_picker_size = ImMax(bars_width * 1, width - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box
6158 float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x;
6159 float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x;
6160 float bars_triangles_half_sz = IM_TRUNC(bars_width * 0.20f);
6161
6162 float backup_initial_col[4];
6163 memcpy(backup_initial_col, col, components * sizeof(float));
6164
6165 float wheel_thickness = sv_picker_size * 0.08f;
6166 float wheel_r_outer = sv_picker_size * 0.50f;
6167 float wheel_r_inner = wheel_r_outer - wheel_thickness;
6168 ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width)*0.5f, picker_pos.y + sv_picker_size * 0.5f);
6169
6170 // Note: the triangle is displayed rotated with triangle_pa pointing to Hue, but most coordinates stays unrotated for logic.
6171 const bool triangle_rotate = (flags & ImGuiColorEditFlags_PickerNoRotate) == 0;
6172 float triangle_r = wheel_r_inner - (int)(sv_picker_size * 0.027f);
6173 ImVec2 triangle_pa = triangle_rotate ? ImVec2(triangle_r, 0.0f) : ImVec2(triangle_r * +0.866f, triangle_r * +0.5f); // Hue point.
6174 ImVec2 triangle_pb = triangle_rotate ? ImVec2(triangle_r * -0.5f, triangle_r * -0.866f) : ImVec2(0.0f, -triangle_r); // Black point.
6175 ImVec2 triangle_pc = triangle_rotate ? ImVec2(triangle_r * -0.5f, triangle_r * +0.866f) : ImVec2(triangle_r * -0.866f, triangle_r * +0.5f); // White point.
6176
6177 float H = col[0], S = col[1], V = col[2];
6178 float R = col[0], G = col[1], B = col[2];
6179 if (flags & ImGuiColorEditFlags_InputRGB)
6180 {
6181 // Hue is lost when converting from grayscale rgb (saturation=0). Restore it.
6182 ColorConvertRGBtoHSV(R, G, B, H, S, V);
6183 ColorEditRestoreHS(col, &H, &S, &V);
6184 }
6185 else if (flags & ImGuiColorEditFlags_InputHSV)
6186 {
6187 ColorConvertHSVtoRGB(H, S, V, R, G, B);
6188 }
6189
6190 bool value_changed = false, value_changed_h = false, value_changed_sv = false;
6191
6192 PushItemFlag(ImGuiItemFlags_NoNav, true);
6193 if (flags & ImGuiColorEditFlags_PickerHueWheel)
6194 {
6195 // Hue wheel + SV triangle logic
6196 InvisibleButton("hsv", ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size));
6197 if (IsItemActive() && !is_readonly)
6198 {
6199 ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center;
6200 ImVec2 current_off = g.IO.MousePos - wheel_center;
6201 float initial_dist2 = ImLengthSqr(initial_off);
6202 if (initial_dist2 >= (wheel_r_inner - 1) * (wheel_r_inner - 1) && initial_dist2 <= (wheel_r_outer + 1) * (wheel_r_outer + 1))
6203 {
6204 // Interactive with Hue wheel
6205 H = ImAtan2(current_off.y, current_off.x) / IM_PI * 0.5f;
6206 if (H < 0.0f)
6207 H += 1.0f;
6208 value_changed = value_changed_h = true;
6209 }
6210 float cos_hue_angle = triangle_rotate ? ImCos(-H * 2.0f * IM_PI) : 1.0f;
6211 float sin_hue_angle = triangle_rotate ? ImSin(-H * 2.0f * IM_PI) : 0.0f;
6212 if (ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, ImRotate(initial_off, cos_hue_angle, sin_hue_angle)))
6213 {
6214 // Interacting with SV triangle
6215 ImVec2 current_off_unrotated = ImRotate(current_off, cos_hue_angle, sin_hue_angle);
6216 if (!ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated))
6217 current_off_unrotated = ImTriangleClosestPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated);
6218 float uu, vv, ww;
6219 ImTriangleBarycentricCoords(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated, uu, vv, ww);
6220 V = ImClamp(1.0f - vv, 0.0001f, 1.0f);
6221 S = ImClamp(uu / V, 0.0001f, 1.0f);
6222 value_changed = value_changed_sv = true;
6223 }
6224 }
6225 if (!(flags & ImGuiColorEditFlags_NoOptions))
6226 OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
6227 }
6228 else if (flags & ImGuiColorEditFlags_PickerHueBar)
6229 {
6230 // SV rectangle logic
6231 InvisibleButton("sv", ImVec2(sv_picker_size, sv_picker_size));
6232 if (IsItemActive() && !is_readonly)
6233 {
6234 S = ImSaturate((io.MousePos.x - picker_pos.x) / (sv_picker_size - 1));
6235 V = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
6236 ColorEditRestoreH(col, &H); // Greatly reduces hue jitter and reset to 0 when hue == 255 and color is rapidly modified using SV square.
6237 value_changed = value_changed_sv = true;
6238 }
6239 if (!(flags & ImGuiColorEditFlags_NoOptions))
6240 OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
6241
6242 // Hue bar logic
6243 SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y));
6244 InvisibleButton("hue", ImVec2(bars_width, sv_picker_size));
6245 if (IsItemActive() && !is_readonly)
6246 {
6247 H = ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
6248 value_changed = value_changed_h = true;
6249 }
6250 }
6251
6252 // Alpha bar logic
6253 if (alpha_bar)
6254 {
6255 SetCursorScreenPos(ImVec2(bar1_pos_x, picker_pos.y));
6256 InvisibleButton("alpha", ImVec2(bars_width, sv_picker_size));
6257 if (IsItemActive())
6258 {
6259 col[3] = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
6260 value_changed = true;
6261 }
6262 }
6263 PopItemFlag(); // ImGuiItemFlags_NoNav
6264
6265 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
6266 {
6267 SameLine(0, style.ItemInnerSpacing.x);
6268 BeginGroup();
6269 }
6270
6271 if (!(flags & ImGuiColorEditFlags_NoLabel))
6272 {
6273 const char* label_display_end = FindRenderedTextEnd(label);
6274 if (label != label_display_end)
6275 {
6276 if ((flags & ImGuiColorEditFlags_NoSidePreview))
6277 SameLine(0, style.ItemInnerSpacing.x);
6278 TextEx(label, label_display_end);
6279 }
6280 }
6281
6282 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
6283 {
6284 PushItemFlag(ImGuiItemFlags_NoNavDefaultFocus, true);
6285 ImVec4 col_v4(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6286 if ((flags & ImGuiColorEditFlags_NoLabel))
6287 Text("Current");
6288
6289 ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaMask_ | ImGuiColorEditFlags_NoTooltip;
6290 ColorButton("##current", col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2));
6291 if (ref_col != NULL)
6292 {
6293 Text("Original");
6294 ImVec4 ref_col_v4(ref_col[0], ref_col[1], ref_col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : ref_col[3]);
6295 if (ColorButton("##original", ref_col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2)))
6296 {
6297 memcpy(col, ref_col, components * sizeof(float));
6298 value_changed = true;
6299 }
6300 }
6301 PopItemFlag();
6302 EndGroup();
6303 }
6304
6305 // Convert back color to RGB
6306 if (value_changed_h || value_changed_sv)
6307 {
6308 if (flags & ImGuiColorEditFlags_InputRGB)
6309 {
6310 ColorConvertHSVtoRGB(H, S, V, col[0], col[1], col[2]);
6311 g.ColorEditSavedHue = H;
6312 g.ColorEditSavedSat = S;
6313 g.ColorEditSavedID = g.ColorEditCurrentID;
6314 g.ColorEditSavedColor = ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0));
6315 }
6316 else if (flags & ImGuiColorEditFlags_InputHSV)
6317 {
6318 col[0] = H;
6319 col[1] = S;
6320 col[2] = V;
6321 }
6322 }
6323
6324 // R,G,B and H,S,V slider color editor
6325 bool value_changed_fix_hue_wrap = false;
6326 if ((flags & ImGuiColorEditFlags_NoInputs) == 0)
6327 {
6328 PushItemWidth((alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x);
6329 ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaMask_ | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoSmallPreview;
6330 ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker;
6331 if (flags & ImGuiColorEditFlags_DisplayRGB || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6332 if (ColorEdit4("##rgb", col, sub_flags | ImGuiColorEditFlags_DisplayRGB))
6333 {
6334 // FIXME: Hackily differentiating using the DragInt (ActiveId != 0 && !ActiveIdAllowOverlap) vs. using the InputText or DropTarget.
6335 // 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)
6336 value_changed_fix_hue_wrap = (g.ActiveId != 0 && !g.ActiveIdAllowOverlap);
6337 value_changed = true;
6338 }
6339 if (flags & ImGuiColorEditFlags_DisplayHSV || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6340 value_changed |= ColorEdit4("##hsv", col, sub_flags | ImGuiColorEditFlags_DisplayHSV);
6341 if (flags & ImGuiColorEditFlags_DisplayHex || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6342 value_changed |= ColorEdit4("##hex", col, sub_flags | ImGuiColorEditFlags_DisplayHex);
6343 PopItemWidth();
6344 }
6345
6346 // Try to cancel hue wrap (after ColorEdit4 call), if any
6347 if (value_changed_fix_hue_wrap && (flags & ImGuiColorEditFlags_InputRGB))
6348 {
6349 float new_H, new_S, new_V;
6350 ColorConvertRGBtoHSV(col[0], col[1], col[2], new_H, new_S, new_V);
6351 if (new_H <= 0 && H > 0)
6352 {
6353 if (new_V <= 0 && V != new_V)
6354 ColorConvertHSVtoRGB(H, S, new_V <= 0 ? V * 0.5f : new_V, col[0], col[1], col[2]);
6355 else if (new_S <= 0)
6356 ColorConvertHSVtoRGB(H, new_S <= 0 ? S * 0.5f : new_S, new_V, col[0], col[1], col[2]);
6357 }
6358 }
6359
6360 if (value_changed)
6361 {
6362 if (flags & ImGuiColorEditFlags_InputRGB)
6363 {
6364 R = col[0];
6365 G = col[1];
6366 B = col[2];
6367 ColorConvertRGBtoHSV(R, G, B, H, S, V);
6368 ColorEditRestoreHS(col, &H, &S, &V); // Fix local Hue as display below will use it immediately.
6369 }
6370 else if (flags & ImGuiColorEditFlags_InputHSV)
6371 {
6372 H = col[0];
6373 S = col[1];
6374 V = col[2];
6375 ColorConvertHSVtoRGB(H, S, V, R, G, B);
6376 }
6377 }
6378
6379 const int style_alpha8 = IM_F32_TO_INT8_SAT(style.Alpha);
6380 const ImU32 col_black = IM_COL32(0,0,0,style_alpha8);
6381 const ImU32 col_white = IM_COL32(255,255,255,style_alpha8);
6382 const ImU32 col_midgrey = IM_COL32(128,128,128,style_alpha8);
6383 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) };
6384
6385 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);
6386 ImU32 hue_color32 = ColorConvertFloat4ToU32(hue_color_f);
6387 ImU32 user_col32_striped_of_alpha = ColorConvertFloat4ToU32(ImVec4(R, G, B, style.Alpha)); // Important: this is still including the main rendering/style alpha!!
6388
6389 ImVec2 sv_cursor_pos;
6390
6391 if (flags & ImGuiColorEditFlags_PickerHueWheel)
6392 {
6393 // Render Hue Wheel
6394 const float aeps = 0.5f / wheel_r_outer; // Half a pixel arc length in radians (2pi cancels out).
6395 const int segment_per_arc = ImMax(4, (int)wheel_r_outer / 12);
6396 for (int n = 0; n < 6; n++)
6397 {
6398 const float a0 = (n) /6.0f * 2.0f * IM_PI - aeps;
6399 const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps;
6400 const int vert_start_idx = draw_list->VtxBuffer.Size;
6401 draw_list->PathArcTo(wheel_center, (wheel_r_inner + wheel_r_outer)*0.5f, a0, a1, segment_per_arc);
6402 draw_list->PathStroke(col_white, wheel_thickness);
6403 const int vert_end_idx = draw_list->VtxBuffer.Size;
6404
6405 // Paint colors over existing vertices
6406 ImVec2 gradient_p0(wheel_center.x + ImCos(a0) * wheel_r_inner, wheel_center.y + ImSin(a0) * wheel_r_inner);
6407 ImVec2 gradient_p1(wheel_center.x + ImCos(a1) * wheel_r_inner, wheel_center.y + ImSin(a1) * wheel_r_inner);
6408 ShadeVertsLinearColorGradientKeepAlpha(draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, col_hues[n], col_hues[n + 1]);
6409 }
6410
6411 // Render Cursor + preview on Hue Wheel
6412 float cos_hue_angle = ImCos(H * 2.0f * IM_PI);
6413 float sin_hue_angle = ImSin(H * 2.0f * IM_PI);
6414 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);
6415 float hue_cursor_rad = value_changed_h ? wheel_thickness * 0.65f : wheel_thickness * 0.55f;
6416 int hue_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(hue_cursor_rad); // Lock segment count so the +1 one matches others.
6417 draw_list->AddCircleFilled(hue_cursor_pos, hue_cursor_rad, hue_color32, hue_cursor_segments);
6418 draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad + 1, col_midgrey, hue_cursor_segments);
6419 draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad, col_white, hue_cursor_segments);
6420 if (triangle_rotate == false)
6421 {
6422 cos_hue_angle = 1.0f;
6423 sin_hue_angle = 0.0f;
6424 }
6425
6426 // Render SV triangle (rotated according to hue)
6427 ImVec2 tra = wheel_center + ImRotate(triangle_pa, cos_hue_angle, sin_hue_angle);
6428 ImVec2 trb = wheel_center + ImRotate(triangle_pb, cos_hue_angle, sin_hue_angle);
6429 ImVec2 trc = wheel_center + ImRotate(triangle_pc, cos_hue_angle, sin_hue_angle);
6430 ImVec2 uv_white = GetFontTexUvWhitePixel();
6431 draw_list->PrimReserve(3, 3);
6432 draw_list->PrimVtx(tra, uv_white, hue_color32);
6433 draw_list->PrimVtx(trb, uv_white, col_black);
6434 draw_list->PrimVtx(trc, uv_white, col_white);
6435 draw_list->AddTriangle(tra, trb, trc, col_midgrey, 1.5f);
6436 sv_cursor_pos = ImLerp(ImLerp(trc, tra, ImSaturate(S)), trb, ImSaturate(1 - V));
6437 }
6438 else if (flags & ImGuiColorEditFlags_PickerHueBar)
6439 {
6440 // Render SV Square
6441 draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), col_white, hue_color32, hue_color32, col_white);
6442 draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0, 0, col_black, col_black);
6443 RenderFrameBorder(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0.0f);
6444 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
6445 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);
6446
6447 // Render Hue Bar
6448 for (int i = 0; i < 6; ++i)
6449 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]);
6450 float bar0_line_y = IM_ROUND(picker_pos.y + H * sv_picker_size);
6451 RenderFrameBorder(ImVec2(bar0_pos_x, picker_pos.y), ImVec2(bar0_pos_x + bars_width, picker_pos.y + sv_picker_size), 0.0f);
6452 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);
6453 }
6454
6455 // Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range)
6456 float sv_cursor_rad = value_changed_sv ? wheel_thickness * 0.55f : wheel_thickness * 0.40f;
6457 int sv_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(sv_cursor_rad); // Lock segment count so the +1 one matches others.
6458 draw_list->AddCircleFilled(sv_cursor_pos, sv_cursor_rad, user_col32_striped_of_alpha, sv_cursor_segments);
6459 draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad + 1, col_midgrey, sv_cursor_segments);
6460 draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad, col_white, sv_cursor_segments);
6461
6462 // Render alpha bar
6463 if (alpha_bar)
6464 {
6465 float alpha = ImSaturate(col[3]);
6466 ImRect bar1_bb(bar1_pos_x, picker_pos.y, bar1_pos_x + bars_width, picker_pos.y + sv_picker_size);
6467 RenderColorRectWithAlphaCheckerboard(draw_list, bar1_bb.Min, bar1_bb.Max, 0, bar1_bb.GetWidth() / 2.0f, ImVec2(0.0f, 0.0f));
6468 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);
6469 float bar1_line_y = IM_ROUND(picker_pos.y + (1.0f - alpha) * sv_picker_size);
6470 RenderFrameBorder(bar1_bb.Min, bar1_bb.Max, 0.0f);
6471 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);
6472 }
6473
6474 EndGroup();
6475
6476 if (value_changed && memcmp(backup_initial_col, col, components * sizeof(float)) == 0)
6477 value_changed = false;
6478 if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
6479 MarkItemEdited(g.LastItemData.ID);
6480
6481 if (set_current_color_edit_id)
6482 g.ColorEditCurrentID = 0;
6483 PopID();
6484
6485 return value_changed;
6486}
6487
6488// A little color square. Return true when clicked.
6489// FIXME: May want to display/ignore the alpha component in the color display? Yet show it in the tooltip.
6490// 'desc_id' is not called 'label' because we don't display it next to the button, but only in the tooltip.
6491// Note that 'col' may be encoded in HSV if ImGuiColorEditFlags_InputHSV is set.
6492bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, const ImVec2& size_arg)
6493{
6494 ImGuiWindow* window = GetCurrentWindow();
6495 if (window->SkipItems)
6496 return false;
6497
6498 ImGuiContext& g = *GImGui;
6499 const ImGuiID id = window->GetID(desc_id);
6500 const float default_size = GetFrameHeight();
6501 const ImVec2 size(size_arg.x == 0.0f ? default_size : size_arg.x, size_arg.y == 0.0f ? default_size : size_arg.y);
6502 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
6503 ItemSize(bb, (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f);
6504 if (!ItemAdd(bb, id))
6505 return false;
6506
6507 bool hovered, held;
6508 bool pressed = ButtonBehavior(bb, id, &hovered, &held);
6509
6510 if (flags & (ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaOpaque))
6511 flags &= ~(ImGuiColorEditFlags_AlphaNoBg | ImGuiColorEditFlags_AlphaPreviewHalf);
6512
6513 ImVec4 col_rgb = col;
6514 if (flags & ImGuiColorEditFlags_InputHSV)
6515 ColorConvertHSVtoRGB(col_rgb.x, col_rgb.y, col_rgb.z, col_rgb.x, col_rgb.y, col_rgb.z);
6516
6517 ImVec4 col_rgb_without_alpha(col_rgb.x, col_rgb.y, col_rgb.z, 1.0f);
6518 float grid_step = ImMin(size.x, size.y) / 2.99f;
6519 float rounding = ImMin(g.Style.FrameRounding, grid_step * 0.5f);
6520 ImRect bb_inner = bb;
6521 float off = 0.0f;
6522 if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
6523 {
6524 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.
6525 bb_inner.Expand(off);
6526 }
6527 if ((flags & ImGuiColorEditFlags_AlphaPreviewHalf) && col_rgb.w < 1.0f)
6528 {
6529 float mid_x = IM_ROUND((bb_inner.Min.x + bb_inner.Max.x) * 0.5f);
6530 if ((flags & ImGuiColorEditFlags_AlphaNoBg) == 0)
6531 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);
6532 else
6533 window->DrawList->AddRectFilled(ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), bb_inner.Max, GetColorU32(col_rgb), rounding, ImDrawFlags_RoundCornersRight);
6534 window->DrawList->AddRectFilled(bb_inner.Min, ImVec2(mid_x, bb_inner.Max.y), GetColorU32(col_rgb_without_alpha), rounding, ImDrawFlags_RoundCornersLeft);
6535 }
6536 else
6537 {
6538 // Because GetColorU32() multiplies by the global style Alpha and we don't want to display a checkerboard if the source code had no alpha
6539 ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaOpaque) ? col_rgb_without_alpha : col_rgb;
6540 if (col_source.w < 1.0f && (flags & ImGuiColorEditFlags_AlphaNoBg) == 0)
6541 RenderColorRectWithAlphaCheckerboard(window->DrawList, bb_inner.Min, bb_inner.Max, GetColorU32(col_source), grid_step, ImVec2(off, off), rounding);
6542 else
6543 window->DrawList->AddRectFilled(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), rounding);
6544 }
6545 RenderNavCursor(bb, id);
6546 if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
6547 {
6548 if (g.Style.FrameBorderSize > 0.0f)
6549 RenderFrameBorder(bb.Min, bb.Max, rounding);
6550 else
6551 window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), rounding, 1.0f * (float)(int)g.Style._MainScale); // Color buttons are often in need of some sort of border // FIXME-DPI
6552 }
6553
6554 // Drag and Drop Source
6555 // NB: The ActiveId test is merely an optional micro-optimization, BeginDragDropSource() does the same test.
6556 if (g.ActiveId == id && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropSource())
6557 {
6558 if (flags & ImGuiColorEditFlags_NoAlpha)
6559 SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F, &col_rgb, sizeof(float) * 3, ImGuiCond_Once);
6560 else
6561 SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, &col_rgb, sizeof(float) * 4, ImGuiCond_Once);
6562 ColorButton(desc_id, col, flags);
6563 SameLine();
6564 TextEx("Color");
6565 EndDragDropSource();
6566 }
6567
6568 // Tooltip
6569 if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered && IsItemHovered(ImGuiHoveredFlags_ForTooltip))
6570 ColorTooltip(desc_id, &col.x, flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_AlphaMask_));
6571
6572 return pressed;
6573}
6574
6575#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
6576// This allowed passing 0 to set default, whereas NewFrame() will assert if one of missing. Abide to old logic.
6577void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags)
6578{
6579 ImGuiContext& g = *GImGui;
6580 if ((flags & ImGuiColorEditFlags_DisplayMask_) == 0) { flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DisplayMask_; }
6581 if ((flags & ImGuiColorEditFlags_DataTypeMask_) == 0) { flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DataTypeMask_; }
6582 if ((flags & ImGuiColorEditFlags_PickerMask_) == 0) { flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_PickerMask_; }
6583 if ((flags & ImGuiColorEditFlags_InputMask_) == 0) { flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_InputMask_; }
6584 g.IO.ConfigColorEditFlags = flags;
6585}
6586#endif
6587
6588// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
6589void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags)
6590{
6591 ImGuiContext& g = *GImGui;
6592
6593 if (!BeginTooltipEx(ImGuiTooltipFlags_OverridePrevious, ImGuiWindowFlags_None))
6594 return;
6595 const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text;
6596 if (text_end > text)
6597 {
6598 TextEx(text, text_end);
6599 Separator();
6600 }
6601
6602 ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2);
6603 ImVec4 cf(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6604 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]);
6605 ImGuiColorEditFlags flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_AlphaMask_;
6606 ColorButton("##preview", cf, (flags & flags_to_forward) | ImGuiColorEditFlags_NoTooltip, sz);
6607 SameLine();
6608 if ((flags & ImGuiColorEditFlags_InputRGB) || !(flags & ImGuiColorEditFlags_InputMask_))
6609 {
6610 if (flags & ImGuiColorEditFlags_NoAlpha)
6611 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]);
6612 else
6613 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]);
6614 }
6615 else if (flags & ImGuiColorEditFlags_InputHSV)
6616 {
6617 if (flags & ImGuiColorEditFlags_NoAlpha)
6618 Text("H: %.3f, S: %.3f, V: %.3f", col[0], col[1], col[2]);
6619 else
6620 Text("H: %.3f, S: %.3f, V: %.3f, A: %.3f", col[0], col[1], col[2], col[3]);
6621 }
6622 EndTooltip();
6623}
6624
6625void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags)
6626{
6627 bool allow_opt_inputs = !(flags & ImGuiColorEditFlags_DisplayMask_);
6628 bool allow_opt_datatype = !(flags & ImGuiColorEditFlags_DataTypeMask_);
6629 if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup("context"))
6630 return;
6631
6632 ImGuiContext& g = *GImGui;
6633 PushItemFlag(ImGuiItemFlags_NoMarkEdited, true);
6634 ImGuiColorEditFlags opts = g.IO.ConfigColorEditFlags;
6635 if (allow_opt_inputs)
6636 {
6637 if (RadioButton("RGB", (opts & ImGuiColorEditFlags_DisplayRGB) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayRGB;
6638 if (RadioButton("HSV", (opts & ImGuiColorEditFlags_DisplayHSV) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHSV;
6639 if (RadioButton("Hex", (opts & ImGuiColorEditFlags_DisplayHex) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHex;
6640 }
6641 if (allow_opt_datatype)
6642 {
6643 if (allow_opt_inputs) Separator();
6644 if (RadioButton("0..255", (opts & ImGuiColorEditFlags_Uint8) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Uint8;
6645 if (RadioButton("0.00..1.00", (opts & ImGuiColorEditFlags_Float) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Float;
6646 }
6647
6648 if (allow_opt_inputs || allow_opt_datatype)
6649 Separator();
6650 if (Button("Copy as..", ImVec2(-1, 0)))
6651 OpenPopup("Copy");
6652 if (BeginPopup("Copy"))
6653 {
6654 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]);
6655 char buf[64];
6656 ImFormatString(buf, IM_COUNTOF(buf), "(%.3ff, %.3ff, %.3ff, %.3ff)", col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6657 if (Selectable(buf))
6658 SetClipboardText(buf);
6659 ImFormatString(buf, IM_COUNTOF(buf), "(%d,%d,%d,%d)", cr, cg, cb, ca);
6660 if (Selectable(buf))
6661 SetClipboardText(buf);
6662 ImFormatString(buf, IM_COUNTOF(buf), "#%02X%02X%02X", cr, cg, cb);
6663 if (Selectable(buf))
6664 SetClipboardText(buf);
6665 if (!(flags & ImGuiColorEditFlags_NoAlpha))
6666 {
6667 ImFormatString(buf, IM_COUNTOF(buf), "#%02X%02X%02X%02X", cr, cg, cb, ca);
6668 if (Selectable(buf))
6669 SetClipboardText(buf);
6670 }
6671 EndPopup();
6672 }
6673
6674 g.IO.ConfigColorEditFlags = opts;
6675 PopItemFlag();
6676 EndPopup();
6677}
6678
6679void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags)
6680{
6681 bool allow_opt_picker = !(flags & ImGuiColorEditFlags_PickerMask_);
6682 bool allow_opt_alpha_bar = !(flags & ImGuiColorEditFlags_NoAlpha) && !(flags & ImGuiColorEditFlags_AlphaBar);
6683 if ((!allow_opt_picker && !allow_opt_alpha_bar) || !BeginPopup("context"))
6684 return;
6685
6686 ImGuiContext& g = *GImGui;
6687 PushItemFlag(ImGuiItemFlags_NoMarkEdited, true);
6688 if (allow_opt_picker)
6689 {
6690 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
6691 PushItemWidth(picker_size.x);
6692 for (int picker_type = 0; picker_type < 2; picker_type++)
6693 {
6694 // Draw small/thumbnail version of each picker type (over an invisible button for selection)
6695 if (picker_type > 0) Separator();
6696 PushID(picker_type);
6697 ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoSidePreview | (flags & ImGuiColorEditFlags_NoAlpha);
6698 if (picker_type == 0) picker_flags |= ImGuiColorEditFlags_PickerHueBar;
6699 if (picker_type == 1) picker_flags |= ImGuiColorEditFlags_PickerHueWheel;
6700 ImVec2 backup_pos = GetCursorScreenPos();
6701 if (Selectable("##selectable", false, 0, picker_size)) // By default, Selectable() is closing popup
6702 g.IO.ConfigColorEditFlags = (g.IO.ConfigColorEditFlags & ~ImGuiColorEditFlags_PickerMask_) | (picker_flags & ImGuiColorEditFlags_PickerMask_);
6703 SetCursorScreenPos(backup_pos);
6704 ImVec4 previewing_ref_col;
6705 memcpy(&previewing_ref_col, ref_col, sizeof(float) * ((picker_flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4));
6706 ColorPicker4("##previewing_picker", &previewing_ref_col.x, picker_flags);
6707 PopID();
6708 }
6709 PopItemWidth();
6710 }
6711 if (allow_opt_alpha_bar)
6712 {
6713 if (allow_opt_picker) Separator();
6714 CheckboxFlags("Alpha Bar", &g.IO.ConfigColorEditFlags, ImGuiColorEditFlags_AlphaBar);
6715 }
6716 PopItemFlag();
6717 EndPopup();
6718}
6719
6720//-------------------------------------------------------------------------
6721// [SECTION] Widgets: TreeNode, CollapsingHeader, etc.
6722//-------------------------------------------------------------------------
6723// - TreeNode()
6724// - TreeNodeV()
6725// - TreeNodeEx()
6726// - TreeNodeExV()
6727// - TreeNodeStoreStackData() [Internal]
6728// - TreeNodeBehavior() [Internal]
6729// - TreePush()
6730// - TreePop()
6731// - GetTreeNodeToLabelSpacing()
6732// - SetNextItemOpen()
6733// - CollapsingHeader()
6734//-------------------------------------------------------------------------
6735
6736bool ImGui::TreeNode(const char* str_id, const char* fmt, ...)
6737{
6738 va_list args;
6739 va_start(args, fmt);
6740 bool is_open = TreeNodeExV(str_id, 0, fmt, args);
6741 va_end(args);
6742 return is_open;
6743}
6744
6745bool ImGui::TreeNode(const void* ptr_id, const char* fmt, ...)
6746{
6747 va_list args;
6748 va_start(args, fmt);
6749 bool is_open = TreeNodeExV(ptr_id, 0, fmt, args);
6750 va_end(args);
6751 return is_open;
6752}
6753
6754bool ImGui::TreeNode(const char* label)
6755{
6756 ImGuiWindow* window = GetCurrentWindow();
6757 if (window->SkipItems)
6758 return false;
6759 ImGuiID id = window->GetID(label);
6760 return TreeNodeBehavior(id, ImGuiTreeNodeFlags_None, label, NULL);
6761}
6762
6763bool ImGui::TreeNodeV(const char* str_id, const char* fmt, va_list args)
6764{
6765 return TreeNodeExV(str_id, 0, fmt, args);
6766}
6767
6768bool ImGui::TreeNodeV(const void* ptr_id, const char* fmt, va_list args)
6769{
6770 return TreeNodeExV(ptr_id, 0, fmt, args);
6771}
6772
6773bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags)
6774{
6775 ImGuiWindow* window = GetCurrentWindow();
6776 if (window->SkipItems)
6777 return false;
6778 ImGuiID id = window->GetID(label);
6779 return TreeNodeBehavior(id, flags, label, NULL);
6780}
6781
6782bool ImGui::TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
6783{
6784 va_list args;
6785 va_start(args, fmt);
6786 bool is_open = TreeNodeExV(str_id, flags, fmt, args);
6787 va_end(args);
6788 return is_open;
6789}
6790
6791bool ImGui::TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
6792{
6793 va_list args;
6794 va_start(args, fmt);
6795 bool is_open = TreeNodeExV(ptr_id, flags, fmt, args);
6796 va_end(args);
6797 return is_open;
6798}
6799
6800bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
6801{
6802 ImGuiWindow* window = GetCurrentWindow();
6803 if (window->SkipItems)
6804 return false;
6805
6806 ImGuiID id = window->GetID(str_id);
6807 const char* label, *label_end;
6808 ImFormatStringToTempBufferV(&label, &label_end, fmt, args);
6809 return TreeNodeBehavior(id, flags, label, label_end);
6810}
6811
6812bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
6813{
6814 ImGuiWindow* window = GetCurrentWindow();
6815 if (window->SkipItems)
6816 return false;
6817
6818 ImGuiID id = window->GetID(ptr_id);
6819 const char* label, *label_end;
6820 ImFormatStringToTempBufferV(&label, &label_end, fmt, args);
6821 return TreeNodeBehavior(id, flags, label, label_end);
6822}
6823
6824// The reason those two functions are not yet in public API is because I would like to design a more feature-full and generic API for this.
6825// They are otherwise function (cc: #3823, #9251, #7553, #6754, #5423, #2958, #2079, #1947, #1131, #722)
6826bool ImGui::TreeNodeGetOpen(ImGuiID storage_id)
6827{
6828 ImGuiContext& g = *GImGui;
6829 ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
6830 return storage->GetInt(storage_id, 0) != 0;
6831}
6832
6833void ImGui::TreeNodeSetOpen(ImGuiID storage_id, bool is_open)
6834{
6835 ImGuiContext& g = *GImGui;
6836 ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
6837 storage->SetInt(storage_id, is_open ? 1 : 0);
6838}
6839
6840bool ImGui::TreeNodeUpdateNextOpen(ImGuiID storage_id, ImGuiTreeNodeFlags flags)
6841{
6842 // Leaf node always open a new tree/id scope. If you never use it, add ImGuiTreeNodeFlags_NoTreePushOnOpen.
6843 if (flags & ImGuiTreeNodeFlags_Leaf)
6844 return true;
6845
6846 // We only write to the tree storage if the user clicks, or explicitly use the SetNextItemOpen function
6847 ImGuiContext& g = *GImGui;
6848 ImGuiWindow* window = g.CurrentWindow;
6849 ImGuiStorage* storage = window->DC.StateStorage;
6850
6851 bool is_open;
6852 if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasOpen)
6853 {
6854 if (g.NextItemData.OpenCond & ImGuiCond_Always)
6855 {
6856 is_open = g.NextItemData.OpenVal;
6857 TreeNodeSetOpen(storage_id, is_open);
6858 }
6859 else
6860 {
6861 // We treat ImGuiCond_Once and ImGuiCond_FirstUseEver the same because tree node state are not saved persistently.
6862 const int stored_value = storage->GetInt(storage_id, -1);
6863 if (stored_value == -1)
6864 {
6865 is_open = g.NextItemData.OpenVal;
6866 TreeNodeSetOpen(storage_id, is_open);
6867 }
6868 else
6869 {
6870 is_open = stored_value != 0;
6871 }
6872 }
6873 }
6874 else
6875 {
6876 is_open = storage->GetInt(storage_id, (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0;
6877 }
6878
6879 // When logging is enabled, we automatically expand tree nodes (but *NOT* collapsing headers.. seems like sensible behavior).
6880 // NB- If we are above max depth we still allow manually opened nodes to be logged.
6881 if (g.LogEnabled && !(flags & ImGuiTreeNodeFlags_NoAutoOpenOnLog) && (window->DC.TreeDepth - g.LogDepthRef) < g.LogDepthToExpand)
6882 is_open = true;
6883
6884 return is_open;
6885}
6886
6887// Store ImGuiTreeNodeStackData for just submitted node.
6888// Currently only supports 32 level deep and we are fine with (1 << Depth) overflowing into a zero, easy to increase.
6889static void TreeNodeStoreStackData(ImGuiTreeNodeFlags flags, float x1)
6890{
6891 ImGuiContext& g = *GImGui;
6892 ImGuiWindow* window = g.CurrentWindow;
6893
6894 g.TreeNodeStack.resize(g.TreeNodeStack.Size + 1);
6895 ImGuiTreeNodeStackData* tree_node_data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
6896 tree_node_data->ID = g.LastItemData.ID;
6897 tree_node_data->TreeFlags = flags;
6898 tree_node_data->ItemFlags = g.LastItemData.ItemFlags;
6899 tree_node_data->NavRect = g.LastItemData.NavRect;
6900
6901 // Initially I tried to latch value for GetColorU32(ImGuiCol_TreeLines) but it's not a good trade-off for very large trees.
6902 const bool draw_lines = (flags & (ImGuiTreeNodeFlags_DrawLinesFull | ImGuiTreeNodeFlags_DrawLinesToNodes)) != 0;
6903 tree_node_data->DrawLinesX1 = draw_lines ? (x1 + g.FontSize * 0.5f + g.Style.FramePadding.x) : +FLT_MAX;
6904 tree_node_data->DrawLinesTableColumn = (draw_lines && g.CurrentTable) ? (ImGuiTableColumnIdx)g.CurrentTable->CurrentColumn : -1;
6905 tree_node_data->DrawLinesToNodesY2 = -FLT_MAX;
6906 window->DC.TreeHasStackDataDepthMask |= (1 << window->DC.TreeDepth);
6907 if (flags & ImGuiTreeNodeFlags_DrawLinesToNodes)
6908 window->DC.TreeRecordsClippedNodesY2Mask |= (1 << window->DC.TreeDepth);
6909}
6910
6911bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end)
6912{
6913 ImGuiWindow* window = GetCurrentWindow();
6914 if (window->SkipItems)
6915 return false;
6916
6917 ImGuiContext& g = *GImGui;
6918 const ImGuiStyle& style = g.Style;
6919
6920 // When not framed, we vertically increase height up to typical framed widget height
6921 const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0;
6922 const bool use_frame_padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding));
6923 const ImVec2 padding = use_frame_padding ? style.FramePadding : ImVec2(style.FramePadding.x, ImMin(window->DC.CurrLineTextBaseOffset, style.FramePadding.y));
6924
6925 if (!label_end)
6926 label_end = FindRenderedTextEnd(label);
6927 const ImVec2 label_size = CalcTextSize(label, label_end, false);
6928
6929 const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapsing arrow width + Spacing
6930 const float text_offset_y = use_frame_padding ? ImMax(style.FramePadding.y, window->DC.CurrLineTextBaseOffset) : window->DC.CurrLineTextBaseOffset; // Latch before ItemSize changes it
6931 const float text_width = g.FontSize + label_size.x + padding.x * 2; // Include collapsing arrow
6932
6933 const float frame_height = label_size.y + padding.y * 2;
6934 const bool span_all_columns = (flags & ImGuiTreeNodeFlags_SpanAllColumns) != 0 && (g.CurrentTable != NULL);
6935 const bool span_all_columns_label = (flags & ImGuiTreeNodeFlags_LabelSpanAllColumns) != 0 && (g.CurrentTable != NULL);
6936 ImRect frame_bb;
6937 frame_bb.Min.x = span_all_columns ? window->ParentWorkRect.Min.x : (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x;
6938 frame_bb.Min.y = window->DC.CursorPos.y + (text_offset_y - padding.y);
6939 frame_bb.Max.x = span_all_columns ? window->ParentWorkRect.Max.x : (flags & ImGuiTreeNodeFlags_SpanLabelWidth) ? window->DC.CursorPos.x + text_width + padding.x : window->WorkRect.Max.x;
6940 frame_bb.Max.y = window->DC.CursorPos.y + (text_offset_y - padding.y) + frame_height;
6941 if (display_frame)
6942 {
6943 const float outer_extend = IM_TRUNC(window->WindowPadding.x * 0.5f); // Framed header expand a little outside of current limits
6944 frame_bb.Min.x -= outer_extend;
6945 frame_bb.Max.x += outer_extend;
6946 }
6947
6948 ImVec2 text_pos(window->DC.CursorPos.x + text_offset_x, window->DC.CursorPos.y + text_offset_y);
6949 ItemSize(ImVec2(text_width, frame_height), padding.y);
6950
6951 // For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing
6952 ImRect interact_bb = frame_bb;
6953 if ((flags & (ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_SpanLabelWidth | ImGuiTreeNodeFlags_SpanAllColumns)) == 0)
6954 interact_bb.Max.x = frame_bb.Min.x + text_width + (label_size.x > 0.0f ? style.ItemSpacing.x * 2.0f : 0.0f);
6955
6956 // Compute open and multi-select states before ItemAdd() as it clear NextItem data.
6957 ImGuiID storage_id = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasStorageID) ? g.NextItemData.StorageId : id;
6958 bool is_open = TreeNodeUpdateNextOpen(storage_id, flags);
6959
6960 bool is_visible;
6961 if (span_all_columns || span_all_columns_label)
6962 {
6963 // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable..
6964 const float backup_clip_rect_min_x = window->ClipRect.Min.x;
6965 const float backup_clip_rect_max_x = window->ClipRect.Max.x;
6966 window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
6967 window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
6968 is_visible = ItemAdd(interact_bb, id);
6969 window->ClipRect.Min.x = backup_clip_rect_min_x;
6970 window->ClipRect.Max.x = backup_clip_rect_max_x;
6971 }
6972 else
6973 {
6974 is_visible = ItemAdd(interact_bb, id);
6975 }
6976 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect;
6977 g.LastItemData.DisplayRect = frame_bb;
6978
6979 // If a NavLeft request is happening and ImGuiTreeNodeFlags_NavLeftJumpsToParent enabled:
6980 // Store data for the current depth to allow returning to this node from any child item.
6981 // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop().
6982 // It will become tempting to enable ImGuiTreeNodeFlags_NavLeftJumpsToParent by default or move it to ImGuiStyle.
6983 bool store_tree_node_stack_data = false;
6984 if ((flags & ImGuiTreeNodeFlags_DrawLinesMask_) == 0)
6985 flags |= g.Style.TreeLinesFlags;
6986 const bool draw_tree_lines = (flags & (ImGuiTreeNodeFlags_DrawLinesFull | ImGuiTreeNodeFlags_DrawLinesToNodes)) && (frame_bb.Min.y < window->ClipRect.Max.y) && (g.Style.TreeLinesSize > 0.0f);
6987 if (!(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6988 {
6989 store_tree_node_stack_data = draw_tree_lines;
6990 if ((flags & ImGuiTreeNodeFlags_NavLeftJumpsToParent) && !g.NavIdIsAlive)
6991 if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
6992 store_tree_node_stack_data = true;
6993 }
6994
6995 const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0;
6996 if (!is_visible)
6997 {
6998 if ((flags & ImGuiTreeNodeFlags_DrawLinesToNodes) && (window->DC.TreeRecordsClippedNodesY2Mask & (1 << (window->DC.TreeDepth - 1))))
6999 {
7000 ImGuiTreeNodeStackData* parent_data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
7001 parent_data->DrawLinesToNodesY2 = ImMax(parent_data->DrawLinesToNodesY2, window->DC.CursorPos.y); // Don't need to aim to mid Y position as we are clipped anyway.
7002 if (frame_bb.Min.y >= window->ClipRect.Max.y)
7003 window->DC.TreeRecordsClippedNodesY2Mask &= ~(1 << (window->DC.TreeDepth - 1)); // Done
7004 }
7005 if (is_open && store_tree_node_stack_data)
7006 TreeNodeStoreStackData(flags, text_pos.x - text_offset_x); // Call before TreePushOverrideID()
7007 if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
7008 TreePushOverrideID(id);
7009 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
7010 return is_open;
7011 }
7012
7013 if (span_all_columns || span_all_columns_label)
7014 {
7015 TablePushBackgroundChannel();
7016 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect;
7017 g.LastItemData.ClipRect = window->ClipRect;
7018 }
7019
7020 ImGuiButtonFlags button_flags = ImGuiTreeNodeFlags_None;
7021 if ((flags & ImGuiTreeNodeFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap))
7022 button_flags |= ImGuiButtonFlags_AllowOverlap;
7023 if (!is_leaf)
7024 button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
7025
7026 // We allow clicking on the arrow section with keyboard modifiers held, in order to easily
7027 // allow browsing a tree while preserving selection with code implementing multi-selection patterns.
7028 // When clicking on the rest of the tree node we always disallow keyboard modifiers.
7029 const float arrow_hit_x1 = (text_pos.x - text_offset_x) - style.TouchExtraPadding.x;
7030 const float arrow_hit_x2 = (text_pos.x - text_offset_x) + (g.FontSize + padding.x * 2.0f) + style.TouchExtraPadding.x;
7031 const bool is_mouse_x_over_arrow = (g.IO.MousePos.x >= arrow_hit_x1 && g.IO.MousePos.x < arrow_hit_x2);
7032
7033 const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
7034 if (is_multi_select) // We absolutely need to distinguish open vs select so _OpenOnArrow comes by default
7035 flags |= (flags & ImGuiTreeNodeFlags_OpenOnMask_) == 0 ? ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick : ImGuiTreeNodeFlags_OpenOnArrow;
7036
7037 // Open behaviors can be altered with the _OpenOnArrow and _OnOnDoubleClick flags.
7038 // Some alteration have subtle effects (e.g. toggle on MouseUp vs MouseDown events) due to requirements for multi-selection and drag and drop support.
7039 // - Single-click on label = Toggle on MouseUp (default, when _OpenOnArrow=0)
7040 // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=0)
7041 // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=1)
7042 // - Double-click on label = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1)
7043 // - Double-click on arrow = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1 and _OpenOnArrow=0)
7044 // It is rather standard that arrow click react on Down rather than Up.
7045 // 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.
7046 if (is_mouse_x_over_arrow)
7047 button_flags |= ImGuiButtonFlags_PressedOnClick;
7048 else if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick)
7049 button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick;
7050 else
7051 button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
7052 if (flags & ImGuiTreeNodeFlags_NoNavFocus)
7053 button_flags |= ImGuiButtonFlags_NoNavFocus;
7054
7055 bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0;
7056 const bool was_selected = selected;
7057
7058 // Multi-selection support (header)
7059 if (is_multi_select)
7060 {
7061 // Handle multi-select + alter button flags for it
7062 MultiSelectItemHeader(id, &selected, &button_flags);
7063 if (is_mouse_x_over_arrow)
7064 button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease;
7065 }
7066 else
7067 {
7068 if (window != g.HoveredWindow || !is_mouse_x_over_arrow)
7069 button_flags |= ImGuiButtonFlags_NoKeyModsAllowed;
7070 }
7071
7072 bool hovered, held;
7073 bool pressed = ButtonBehavior(interact_bb, id, &hovered, &held, button_flags);
7074 bool toggled = false;
7075 if (!is_leaf)
7076 {
7077 if (pressed && g.DragDropHoldJustPressedId != id)
7078 {
7079 if ((flags & ImGuiTreeNodeFlags_OpenOnMask_) == 0 || (g.NavActivateId == id && !is_multi_select))
7080 toggled = true; // Single click
7081 if (flags & ImGuiTreeNodeFlags_OpenOnArrow)
7082 toggled |= is_mouse_x_over_arrow && !g.NavHighlightItemUnderNav; // Lightweight equivalent of IsMouseHoveringRect() since ButtonBehavior() already did the job
7083 if ((flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) && g.IO.MouseClickedCount[0] == 2)
7084 toggled = true; // Double click
7085 }
7086 else if (pressed && g.DragDropHoldJustPressedId == id)
7087 {
7088 IM_ASSERT(button_flags & ImGuiButtonFlags_PressedOnDragDropHold);
7089 if (!is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again.
7090 toggled = true;
7091 else
7092 pressed = false; // Cancel press so it doesn't trigger selection.
7093 }
7094
7095 if (g.NavId == id && g.NavMoveDir == ImGuiDir_Left && is_open)
7096 {
7097 toggled = true;
7098 NavClearPreferredPosForAxis(ImGuiAxis_X);
7099 NavMoveRequestCancel();
7100 }
7101 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?
7102 {
7103 toggled = true;
7104 NavClearPreferredPosForAxis(ImGuiAxis_X);
7105 NavMoveRequestCancel();
7106 }
7107
7108 if (toggled)
7109 {
7110 is_open = !is_open;
7111 window->DC.StateStorage->SetInt(storage_id, is_open);
7112 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledOpen;
7113 }
7114 }
7115
7116 // Multi-selection support (footer)
7117 if (is_multi_select)
7118 {
7119 bool pressed_copy = pressed && !toggled;
7120 MultiSelectItemFooter(id, &selected, &pressed_copy);
7121 if (pressed)
7122 SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, interact_bb);
7123 }
7124
7125 if (selected != was_selected)
7126 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
7127
7128 // Render
7129 {
7130 const ImU32 text_col = GetColorU32(ImGuiCol_Text);
7131 ImGuiNavRenderCursorFlags nav_render_cursor_flags = ImGuiNavRenderCursorFlags_Compact;
7132 if (is_multi_select)
7133 nav_render_cursor_flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle
7134 if (display_frame)
7135 {
7136 // Framed type
7137 const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
7138 RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, true, style.FrameRounding);
7139 RenderNavCursor(frame_bb, id, nav_render_cursor_flags);
7140 if (span_all_columns && !span_all_columns_label)
7141 TablePopBackgroundChannel();
7142 if (flags & ImGuiTreeNodeFlags_Bullet)
7143 RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.60f, text_pos.y + g.FontSize * 0.5f), text_col);
7144 else if (!is_leaf)
7145 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);
7146 else // Leaf without bullet, left-adjusted text
7147 text_pos.x -= text_offset_x - padding.x;
7148 if (flags & ImGuiTreeNodeFlags_ClipLabelForTrailingButton)
7149 frame_bb.Max.x -= g.FontSize + style.FramePadding.x;
7150 if (g.LogEnabled)
7151 LogSetNextTextDecoration("###", "###");
7152 }
7153 else
7154 {
7155 // Unframed typed for tree nodes
7156 if (hovered || selected)
7157 {
7158 const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
7159 RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, false, 0.0f);
7160 }
7161 RenderNavCursor(frame_bb, id, nav_render_cursor_flags, 0.0f);
7162 if (span_all_columns && !span_all_columns_label)
7163 TablePopBackgroundChannel();
7164 if (flags & ImGuiTreeNodeFlags_Bullet)
7165 RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.5f, text_pos.y + g.FontSize * 0.5f), text_col);
7166 else if (!is_leaf)
7167 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);
7168 if (g.LogEnabled)
7169 LogSetNextTextDecoration(">", NULL);
7170 }
7171
7172 if (draw_tree_lines)
7173 TreeNodeDrawLineToChildNode(ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y + g.FontSize * 0.5f));
7174
7175 // Label
7176 if (display_frame)
7177 RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size);
7178 else
7179 RenderText(text_pos, label, label_end, false);
7180
7181 if (span_all_columns_label)
7182 TablePopBackgroundChannel();
7183 }
7184
7185 if (is_open && store_tree_node_stack_data)
7186 TreeNodeStoreStackData(flags, text_pos.x - text_offset_x); // Call before TreePushOverrideID()
7187 if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
7188 TreePushOverrideID(id); // Could use TreePush(label) but this avoid computing twice
7189
7190 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
7191 return is_open;
7192}
7193
7194// Draw horizontal line from our parent node
7195// This is only called for visible child nodes so we are not too fussy anymore about performances
7196void ImGui::TreeNodeDrawLineToChildNode(const ImVec2& target_pos)
7197{
7198 ImGuiContext& g = *GImGui;
7199 ImGuiWindow* window = g.CurrentWindow;
7200 if (window->DC.TreeDepth == 0 || (window->DC.TreeHasStackDataDepthMask & (1 << (window->DC.TreeDepth - 1))) == 0)
7201 return;
7202
7203 ImGuiTreeNodeStackData* parent_data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
7204 float x1 = ImTrunc(parent_data->DrawLinesX1);
7205 float x2 = ImTrunc(target_pos.x - g.Style.ItemInnerSpacing.x);
7206 float y = ImTrunc(target_pos.y);
7207 float rounding = (g.Style.TreeLinesRounding > 0.0f) ? ImMin(x2 - x1, g.Style.TreeLinesRounding) : 0.0f;
7208 parent_data->DrawLinesToNodesY2 = ImMax(parent_data->DrawLinesToNodesY2, y - rounding);
7209 if (x1 >= x2)
7210 return;
7211 if (rounding > 0.0f)
7212 {
7213 x1 += 0.5f + rounding;
7214 window->DrawList->PathArcToFast(ImVec2(x1, y - rounding), rounding, 6, 3);
7215 if (x1 < x2)
7216 window->DrawList->PathLineTo(ImVec2(x2, y));
7217 window->DrawList->PathStroke(GetColorU32(ImGuiCol_TreeLines), g.Style.TreeLinesSize);
7218 }
7219 else
7220 {
7221 window->DrawList->AddLineH(x1, x2, y, GetColorU32(ImGuiCol_TreeLines), g.Style.TreeLinesSize);
7222 }
7223}
7224
7225// Draw vertical line of the hierarchy
7226void ImGui::TreeNodeDrawLineToTreePop(const ImGuiTreeNodeStackData* data)
7227{
7228 ImGuiContext& g = *GImGui;
7229 ImGuiWindow* window = g.CurrentWindow;
7230 float y1 = ImMax(data->NavRect.Max.y, window->ClipRect.Min.y);
7231 float y2 = data->DrawLinesToNodesY2;
7232 if (data->TreeFlags & ImGuiTreeNodeFlags_DrawLinesFull)
7233 {
7234 float y2_full = window->DC.CursorPos.y;
7235 if (g.CurrentTable)
7236 y2_full = ImMax(g.CurrentTable->RowPosY2, y2_full);
7237 y2_full = ImTrunc(y2_full - g.Style.ItemSpacing.y - g.FontSize * 0.5f);
7238 if (y2 + (g.Style.ItemSpacing.y + g.Style.TreeLinesRounding) < y2_full) // FIXME: threshold to use ToNodes Y2 instead of Full Y2 when close by ItemSpacing.y
7239 y2 = y2_full;
7240 }
7241 y2 = ImMin(y2, window->ClipRect.Max.y);
7242 if (y2 <= y1)
7243 return;
7244 float x = ImTrunc(data->DrawLinesX1);
7245 if (data->DrawLinesTableColumn != -1)
7246 TablePushColumnChannel(data->DrawLinesTableColumn);
7247 window->DrawList->AddLineV(x, y1, y2, GetColorU32(ImGuiCol_TreeLines), g.Style.TreeLinesSize);
7248 if (data->DrawLinesTableColumn != -1)
7249 TablePopColumnChannel();
7250}
7251
7252void ImGui::TreePush(const char* str_id)
7253{
7254 ImGuiWindow* window = GetCurrentWindow();
7255 Indent();
7256 window->DC.TreeDepth++;
7257 PushID(str_id);
7258}
7259
7260void ImGui::TreePush(const void* ptr_id)
7261{
7262 ImGuiWindow* window = GetCurrentWindow();
7263 Indent();
7264 window->DC.TreeDepth++;
7265 PushID(ptr_id);
7266}
7267
7268void ImGui::TreePushOverrideID(ImGuiID id)
7269{
7270 ImGuiContext& g = *GImGui;
7271 ImGuiWindow* window = g.CurrentWindow;
7272 Indent();
7273 window->DC.TreeDepth++;
7274 PushOverrideID(id);
7275}
7276
7277void ImGui::TreePop()
7278{
7279 ImGuiContext& g = *GImGui;
7280 ImGuiWindow* window = g.CurrentWindow;
7281 Unindent();
7282
7283 window->DC.TreeDepth--;
7284 ImU32 tree_depth_mask = (1 << window->DC.TreeDepth);
7285
7286 if (window->DC.TreeHasStackDataDepthMask & tree_depth_mask)
7287 {
7288 const ImGuiTreeNodeStackData* data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
7289 IM_ASSERT(data->ID == window->IDStack.back());
7290
7291 // Handle Left arrow to move to parent tree node (when ImGuiTreeNodeFlags_NavLeftJumpsToParent is enabled)
7292 if (data->TreeFlags & ImGuiTreeNodeFlags_NavLeftJumpsToParent)
7293 if (g.NavIdIsAlive && g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
7294 NavMoveRequestResolveWithPastTreeNode(&g.NavMoveResultLocal, data);
7295
7296 // Draw hierarchy lines
7297 if (data->DrawLinesX1 != +FLT_MAX && window->DC.CursorPos.y >= window->ClipRect.Min.y)
7298 TreeNodeDrawLineToTreePop(data);
7299
7300 g.TreeNodeStack.pop_back();
7301 window->DC.TreeHasStackDataDepthMask &= ~tree_depth_mask;
7302 window->DC.TreeRecordsClippedNodesY2Mask &= ~tree_depth_mask;
7303 }
7304
7305 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.
7306 PopID();
7307}
7308
7309// Horizontal distance preceding label when using TreeNode() or Bullet()
7310float ImGui::GetTreeNodeToLabelSpacing()
7311{
7312 ImGuiContext& g = *GImGui;
7313 return g.FontSize + (g.Style.FramePadding.x * 2.0f);
7314}
7315
7316// Set next TreeNode/CollapsingHeader open state.
7317void ImGui::SetNextItemOpen(bool is_open, ImGuiCond cond)
7318{
7319 ImGuiContext& g = *GImGui;
7320 if (g.CurrentWindow->SkipItems)
7321 return;
7322 g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasOpen;
7323 g.NextItemData.OpenVal = is_open;
7324 g.NextItemData.OpenCond = (ImU8)(cond ? cond : ImGuiCond_Always);
7325}
7326
7327// Set next TreeNode/CollapsingHeader storage id.
7328void ImGui::SetNextItemStorageID(ImGuiID storage_id)
7329{
7330 ImGuiContext& g = *GImGui;
7331 if (g.CurrentWindow->SkipItems)
7332 return;
7333 g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasStorageID;
7334 g.NextItemData.StorageId = storage_id;
7335}
7336
7337// CollapsingHeader returns true when opened but do not indent nor push into the ID stack (because of the ImGuiTreeNodeFlags_NoTreePushOnOpen flag).
7338// 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().
7339bool ImGui::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags)
7340{
7341 ImGuiWindow* window = GetCurrentWindow();
7342 if (window->SkipItems)
7343 return false;
7344 ImGuiID id = window->GetID(label);
7345 return TreeNodeBehavior(id, flags | ImGuiTreeNodeFlags_CollapsingHeader, label);
7346}
7347
7348// p_visible == NULL : regular collapsing header
7349// 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
7350// p_visible != NULL && *p_visible == false : do not show the header at all
7351// Do not mistake this with the Open state of the header itself, which you can adjust with SetNextItemOpen() or ImGuiTreeNodeFlags_DefaultOpen.
7352bool ImGui::CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags)
7353{
7354 ImGuiWindow* window = GetCurrentWindow();
7355 if (window->SkipItems)
7356 return false;
7357
7358 if (p_visible && !*p_visible)
7359 return false;
7360
7361 ImGuiID id = window->GetID(label);
7362 flags |= ImGuiTreeNodeFlags_CollapsingHeader;
7363 if (p_visible)
7364 flags |= ImGuiTreeNodeFlags_AllowOverlap | (ImGuiTreeNodeFlags)ImGuiTreeNodeFlags_ClipLabelForTrailingButton;
7365 bool is_open = TreeNodeBehavior(id, flags, label);
7366 if (p_visible != NULL)
7367 {
7368 // Create a small overlapping close button
7369 // FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc.
7370 // FIXME: CloseButton can overlap into text, need find a way to clip the text somehow.
7371 ImGuiContext& g = *GImGui;
7372 ImGuiLastItemData last_item_backup = g.LastItemData;
7373 float button_size = g.FontSize;
7374 float button_x = ImMax(g.LastItemData.Rect.Min.x, g.LastItemData.Rect.Max.x - g.Style.FramePadding.x - button_size);
7375 float button_y = g.LastItemData.Rect.Min.y + g.Style.FramePadding.y;
7376 ImGuiID close_button_id = GetIDWithSeed("#CLOSE", NULL, id);
7377 if (CloseButton(close_button_id, ImVec2(button_x, button_y)))
7378 *p_visible = false;
7379 g.LastItemData = last_item_backup;
7380 }
7381
7382 return is_open;
7383}
7384
7385//-------------------------------------------------------------------------
7386// [SECTION] Widgets: Selectable
7387//-------------------------------------------------------------------------
7388// - Selectable()
7389//-------------------------------------------------------------------------
7390
7391// Tip: pass a non-visible label (e.g. "##hello") then you can use the space to draw other text or image.
7392// But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID or use ##unique_id.
7393// With this scheme, ImGuiSelectableFlags_SpanAllColumns and ImGuiSelectableFlags_AllowOverlap are also frequently used flags.
7394// FIXME: Selectable() with (size.x == 0.0f) and (SelectableTextAlign.x > 0.0f) followed by SameLine() is currently not supported.
7395bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
7396{
7397 ImGuiWindow* window = GetCurrentWindow();
7398 if (window->SkipItems)
7399 return false;
7400
7401 ImGuiContext& g = *GImGui;
7402 const ImGuiStyle& style = g.Style;
7403
7404 // Submit label or explicit size to ItemSize(), whereas ItemAdd() will submit a larger/spanning rectangle.
7405 ImGuiID id = window->GetID(label);
7406 const char* label_end = FindRenderedTextEnd(label);
7407 ImVec2 label_size = CalcTextSize(label, label_end, false);
7408 ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y);
7409 ImVec2 pos = window->DC.CursorPos;
7410 pos.y += window->DC.CurrLineTextBaseOffset;
7411 ItemSize(size, 0.0f);
7412
7413 // Fill horizontal space
7414 // We don't support (size < 0.0f) in Selectable() because the ItemSpacing extension would make explicitly right-aligned sizes not visibly match other widgets.
7415 const bool span_all_columns = (flags & ImGuiSelectableFlags_SpanAllColumns) != 0;
7416 const float min_x = span_all_columns ? window->ParentWorkRect.Min.x : pos.x;
7417 const float max_x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x;
7418 if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_SpanAvailWidth))
7419 size.x = ImMax(label_size.x, max_x - min_x);
7420
7421 // Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable.
7422 // FIXME: Not part of layout so not included in clipper calculation, but ItemSize currently doesn't allow offsetting CursorPos.
7423 ImRect bb(min_x, pos.y, min_x + size.x, pos.y + size.y);
7424 if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0)
7425 {
7426 const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x;
7427 const float spacing_y = style.ItemSpacing.y;
7428 const float spacing_L = IM_TRUNC(spacing_x * 0.50f);
7429 const float spacing_U = IM_TRUNC(spacing_y * 0.50f);
7430 bb.Min.x -= spacing_L;
7431 bb.Min.y -= spacing_U;
7432 bb.Max.x += (spacing_x - spacing_L);
7433 bb.Max.y += (spacing_y - spacing_U);
7434 }
7435 //if (g.IO.KeyCtrl) { GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(0, 255, 0, 255)); }
7436
7437 const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0;
7438 const ImGuiItemFlags extra_item_flags = disabled_item ? (ImGuiItemFlags)ImGuiItemFlags_Disabled : ImGuiItemFlags_None;
7439 bool is_visible;
7440 if (span_all_columns)
7441 {
7442 // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable..
7443 const float backup_clip_rect_min_x = window->ClipRect.Min.x;
7444 const float backup_clip_rect_max_x = window->ClipRect.Max.x;
7445 window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
7446 window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
7447 is_visible = ItemAdd(bb, id, NULL, extra_item_flags);
7448 window->ClipRect.Min.x = backup_clip_rect_min_x;
7449 window->ClipRect.Max.x = backup_clip_rect_max_x;
7450 }
7451 else
7452 {
7453 is_visible = ItemAdd(bb, id, NULL, extra_item_flags);
7454 }
7455
7456 const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
7457 if (!is_visible)
7458 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)
7459 return false;
7460
7461 const bool disabled_global = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0;
7462 if (disabled_item && !disabled_global) // Only testing this as an optimization
7463 BeginDisabled();
7464
7465 // FIXME: We can standardize the behavior of those two, we could also keep the fast path of override ClipRect + full push on render only,
7466 // which would be advantageous since most selectable are not selected.
7467 if (span_all_columns)
7468 {
7469 if (g.CurrentTable)
7470 TablePushBackgroundChannel();
7471 else if (window->DC.CurrentColumns)
7472 PushColumnsBackground();
7473 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect;
7474 g.LastItemData.ClipRect = window->ClipRect;
7475 }
7476
7477 // We use NoHoldingActiveID on menus so user can click and _hold_ on a menu then drag to browse child entries
7478 ImGuiButtonFlags button_flags = 0;
7479 if (flags & ImGuiSelectableFlags_NoHoldingActiveID) { button_flags |= ImGuiButtonFlags_NoHoldingActiveId; }
7480 if (flags & ImGuiSelectableFlags_NoSetKeyOwner) { button_flags |= ImGuiButtonFlags_NoSetKeyOwner; }
7481 if (flags & ImGuiSelectableFlags_SelectOnClick) { button_flags |= ImGuiButtonFlags_PressedOnClick; }
7482 if (flags & ImGuiSelectableFlags_SelectOnRelease) { button_flags |= ImGuiButtonFlags_PressedOnRelease; }
7483 if (flags & ImGuiSelectableFlags_AllowDoubleClick) { button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; }
7484 if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap)) { button_flags |= ImGuiButtonFlags_AllowOverlap; }
7485
7486 // Multi-selection support (header)
7487 const bool was_selected = selected;
7488 if (is_multi_select)
7489 {
7490 // Handle multi-select + alter button flags for it
7491 MultiSelectItemHeader(id, &selected, &button_flags);
7492 }
7493
7494 bool hovered, held;
7495 bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags);
7496 bool auto_selected = false;
7497
7498 // Multi-selection support (footer)
7499 if (is_multi_select)
7500 {
7501 MultiSelectItemFooter(id, &selected, &pressed);
7502 }
7503 else
7504 {
7505 // Auto-select when moved into
7506 // - This will be more fully fleshed in the range-select branch
7507 // - This is not exposed as it won't nicely work with some user side handling of shift/control
7508 // - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons
7509 // - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope())
7510 // - (2) usage will fail with clipped items
7511 // The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API.
7512 if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId)
7513 if (g.NavJustMovedToId == id && (g.NavJustMovedToKeyMods & ImGuiMod_Ctrl) == 0)
7514 selected = pressed = auto_selected = true;
7515 }
7516
7517 // Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with keyboard/gamepad
7518 if (pressed || (hovered && (flags & ImGuiSelectableFlags_SetNavIdOnHover)))
7519 {
7520 if (!g.NavHighlightItemUnderNav && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent)
7521 {
7522 SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, WindowRectAbsToRel(window, bb)); // (bb == NavRect)
7523 if (g.IO.ConfigNavCursorVisibleAuto)
7524 g.NavCursorVisible = false;
7525 }
7526 }
7527 if (pressed)
7528 MarkItemEdited(id);
7529
7530 if (selected != was_selected)
7531 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
7532 if (g.ActiveId == id && g.ActiveIdIsJustActivated)
7533 {
7534 g.ActiveIdWasSelected = was_selected;
7535 g.ActiveIdWasSoleSelected = was_selected && (!is_multi_select || g.CurrentMultiSelect->IsSoleOrUnknownSelectionSize);
7536 }
7537
7538 // Render
7539 if (is_visible)
7540 {
7541 const bool highlighted = hovered || (flags & ImGuiSelectableFlags_Highlight);
7542 if (highlighted || selected)
7543 {
7544 // Between 1.91.0 and 1.91.4 we made selected Selectable use an arbitrary lerp between _Header and _HeaderHovered. Removed that now. (#8106)
7545 ImU32 col = GetColorU32((held && highlighted) ? ImGuiCol_HeaderActive : highlighted ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
7546 RenderFrame(bb.Min, bb.Max, col, false, style.SelectableRounding);
7547 }
7548 if (g.NavId == id)
7549 {
7550 ImGuiNavRenderCursorFlags nav_render_cursor_flags = ImGuiNavRenderCursorFlags_Compact;
7551 if (is_multi_select)
7552 nav_render_cursor_flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle
7553 RenderNavCursor(bb, id, nav_render_cursor_flags, style.SelectableRounding);
7554 }
7555 }
7556
7557 if (span_all_columns)
7558 {
7559 if (g.CurrentTable)
7560 TablePopBackgroundChannel();
7561 else if (window->DC.CurrentColumns)
7562 PopColumnsBackground();
7563 }
7564
7565 // Text stays at the submission position. Alignment/clipping extents ignore SpanAllColumns.
7566 if (is_visible)
7567 RenderTextClipped(pos, ImVec2(ImMin(pos.x + size.x, window->WorkRect.Max.x), pos.y + size.y), label, label_end, &label_size, style.SelectableTextAlign, &bb);
7568
7569#ifdef IMGUI_DEBUG_BOXSELECT
7570 if (g.BoxSelectState.UnclipMode) { GetForegroundDrawList()->AddText(pos, IM_COL32(255,255,0,200), label, label_end); }
7571#endif
7572
7573 // Automatically close popups
7574 if (pressed && !auto_selected && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_NoAutoClosePopups) && (g.LastItemData.ItemFlags & ImGuiItemFlags_AutoClosePopups))
7575 CloseCurrentPopup();
7576
7577 if (disabled_item && !disabled_global)
7578 EndDisabled();
7579
7580 // Selectable() always returns a pressed state!
7581 // Users of BeginMultiSelect()/EndMultiSelect() scope: you may call ImGui::IsItemToggledSelection() to retrieve
7582 // selection toggle, only useful if you need that state updated (e.g. for rendering purpose) before reaching EndMultiSelect().
7583 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
7584 return pressed; //-V1020
7585}
7586
7587bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
7588{
7589 if (Selectable(label, *p_selected, flags, size_arg))
7590 {
7591 *p_selected = !*p_selected;
7592 return true;
7593 }
7594 return false;
7595}
7596
7597
7598//-------------------------------------------------------------------------
7599// [SECTION] Widgets: Typing-Select support
7600//-------------------------------------------------------------------------
7601
7602// [Experimental] Currently not exposed in public API.
7603// Consume character inputs and return search request, if any.
7604// This would typically only be called on the focused window or location you want to grab inputs for, e.g.
7605// if (ImGui::IsWindowFocused(...))
7606// if (ImGuiTypingSelectRequest* req = ImGui::GetTypingSelectRequest())
7607// focus_idx = ImGui::TypingSelectFindMatch(req, my_items.size(), [](void*, int n) { return my_items[n]->Name; }, &my_items, -1);
7608// However the code is written in a way where calling it from multiple locations is safe (e.g. to obtain buffer).
7609ImGuiTypingSelectRequest* ImGui::GetTypingSelectRequest(ImGuiTypingSelectFlags flags)
7610{
7611 ImGuiContext& g = *GImGui;
7612 ImGuiTypingSelectState* data = &g.TypingSelectState;
7613 ImGuiTypingSelectRequest* out_request = &data->Request;
7614
7615 // Clear buffer
7616 const float TYPING_SELECT_RESET_TIMER = 1.80f; // FIXME: Potentially move to IO config.
7617 const int TYPING_SELECT_SINGLE_CHAR_COUNT_FOR_LOCK = 4; // Lock single char matching when repeating same char 4 times
7618 if (data->SearchBuffer[0] != 0)
7619 {
7620 bool clear_buffer = false;
7621 clear_buffer |= (g.NavFocusScopeId != data->FocusScope);
7622 clear_buffer |= (data->LastRequestTime + TYPING_SELECT_RESET_TIMER < g.Time);
7623 clear_buffer |= g.NavAnyRequest;
7624 clear_buffer |= g.ActiveId != 0 && g.NavActivateId == 0; // Allow temporary SPACE activation to not interfere
7625 clear_buffer |= IsKeyPressed(ImGuiKey_Escape) || IsKeyPressed(ImGuiKey_Enter);
7626 clear_buffer |= IsKeyPressed(ImGuiKey_Backspace) && (flags & ImGuiTypingSelectFlags_AllowBackspace) == 0;
7627 //if (clear_buffer) { IMGUI_DEBUG_LOG("GetTypingSelectRequest(): Clear SearchBuffer.\n"); }
7628 if (clear_buffer)
7629 data->Clear();
7630 }
7631
7632 // Append to buffer
7633 const int buffer_max_len = IM_COUNTOF(data->SearchBuffer) - 1;
7634 int buffer_len = (int)ImStrlen(data->SearchBuffer);
7635 bool select_request = false;
7636 for (ImWchar w : g.IO.InputQueueCharacters)
7637 {
7638 const int w_len = ImTextCountUtf8BytesFromStr(&w, &w + 1);
7639 if (w < 32 || (buffer_len == 0 && ImCharIsBlankW(w)) || (buffer_len + w_len > buffer_max_len)) // Ignore leading blanks
7640 continue;
7641 char w_buf[5];
7642 ImTextCharToUtf8(w_buf, (unsigned int)w);
7643 if (data->SingleCharModeLock && w_len == out_request->SingleCharSize && memcmp(w_buf, data->SearchBuffer, w_len) == 0)
7644 {
7645 select_request = true; // Same character: don't need to append to buffer.
7646 continue;
7647 }
7648 if (data->SingleCharModeLock)
7649 {
7650 data->Clear(); // Different character: clear
7651 buffer_len = 0;
7652 }
7653 memcpy(data->SearchBuffer + buffer_len, w_buf, w_len + 1); // Append
7654 buffer_len += w_len;
7655 select_request = true;
7656 }
7657 g.IO.InputQueueCharacters.resize(0);
7658
7659 // Handle backspace
7660 if ((flags & ImGuiTypingSelectFlags_AllowBackspace) && IsKeyPressed(ImGuiKey_Backspace, ImGuiInputFlags_Repeat))
7661 {
7662 char* p = (char*)(void*)ImTextFindPreviousUtf8Codepoint(data->SearchBuffer, data->SearchBuffer + buffer_len);
7663 *p = 0;
7664 buffer_len = (int)(p - data->SearchBuffer);
7665 }
7666
7667 // Return request if any
7668 if (buffer_len == 0)
7669 return NULL;
7670 if (select_request)
7671 {
7672 data->FocusScope = g.NavFocusScopeId;
7673 data->LastRequestFrame = g.FrameCount;
7674 data->LastRequestTime = (float)g.Time;
7675 }
7676 out_request->Flags = flags;
7677 out_request->SearchBufferLen = buffer_len;
7678 out_request->SearchBuffer = data->SearchBuffer;
7679 out_request->SelectRequest = (data->LastRequestFrame == g.FrameCount);
7680 out_request->SingleCharMode = false;
7681 out_request->SingleCharSize = 0;
7682
7683 // Calculate if buffer contains the same character repeated.
7684 // - This can be used to implement a special search mode on first character.
7685 // - Performed on UTF-8 codepoint for correctness.
7686 // - SingleCharMode is always set for first input character, because it usually leads to a "next".
7687 if (flags & ImGuiTypingSelectFlags_AllowSingleCharMode)
7688 {
7689 const char* buf_begin = out_request->SearchBuffer;
7690 const char* buf_end = out_request->SearchBuffer + out_request->SearchBufferLen;
7691 const int c0_len = ImTextCountUtf8BytesFromChar(buf_begin, buf_end);
7692 const char* p = buf_begin + c0_len;
7693 for (; p < buf_end; p += c0_len)
7694 if (memcmp(buf_begin, p, (size_t)c0_len) != 0)
7695 break;
7696 const int single_char_count = (p == buf_end) ? (out_request->SearchBufferLen / c0_len) : 0;
7697 out_request->SingleCharMode = (single_char_count > 0 || data->SingleCharModeLock);
7698 out_request->SingleCharSize = (ImS8)c0_len;
7699 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.
7700 }
7701
7702 return out_request;
7703}
7704
7705static int ImStrimatchlen(const char* s1, const char* s1_end, const char* s2)
7706{
7707 int match_len = 0;
7708 while (s1 < s1_end && ImToUpper(*s1++) == ImToUpper(*s2++))
7709 match_len++;
7710 return match_len;
7711}
7712
7713// Default handler for finding a result for typing-select. You may implement your own.
7714// You might want to display a tooltip to visualize the current request SearchBuffer
7715// When SingleCharMode is set:
7716// - it is better to NOT display a tooltip of other on-screen display indicator.
7717// - the index of the currently focused item is required.
7718// if your SetNextItemSelectionUserData() values are indices, you can obtain it from ImGuiMultiSelectIO::NavIdItem, otherwise from g.NavLastValidSelectionUserData.
7719int ImGui::TypingSelectFindMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx)
7720{
7721 if (req == NULL || req->SelectRequest == false) // Support NULL parameter so both calls can be done from same spot.
7722 return -1;
7723 int idx = -1;
7724 if (req->SingleCharMode && (req->Flags & ImGuiTypingSelectFlags_AllowSingleCharMode))
7725 idx = TypingSelectFindNextSingleCharMatch(req, items_count, get_item_name_func, user_data, nav_item_idx);
7726 else
7727 idx = TypingSelectFindBestLeadingMatch(req, items_count, get_item_name_func, user_data);
7728 if (idx != -1)
7729 SetNavCursorVisibleAfterMove();
7730 return idx;
7731}
7732
7733// Special handling when a single character is repeated: perform search on a single letter and goes to next.
7734int ImGui::TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx)
7735{
7736 // FIXME: Assume selection user data is index. Would be extremely practical.
7737 //if (nav_item_idx == -1)
7738 // nav_item_idx = (int)g.NavLastValidSelectionUserData;
7739
7740 int first_match_idx = -1;
7741 bool return_next_match = false;
7742 for (int idx = 0; idx < items_count; idx++)
7743 {
7744 const char* item_name = get_item_name_func(user_data, idx);
7745 if (ImStrimatchlen(req->SearchBuffer, req->SearchBuffer + req->SingleCharSize, item_name) < req->SingleCharSize)
7746 continue;
7747 if (return_next_match) // Return next matching item after current item.
7748 return idx;
7749 if (first_match_idx == -1 && nav_item_idx == -1) // Return first match immediately if we don't have a nav_item_idx value.
7750 return idx;
7751 if (first_match_idx == -1) // Record first match for wrapping.
7752 first_match_idx = idx;
7753 if (nav_item_idx == idx) // Record that we encountering nav_item so we can return next match.
7754 return_next_match = true;
7755 }
7756 return first_match_idx; // First result
7757}
7758
7759int ImGui::TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data)
7760{
7761 int longest_match_idx = -1;
7762 int longest_match_len = 0;
7763 for (int idx = 0; idx < items_count; idx++)
7764 {
7765 const char* item_name = get_item_name_func(user_data, idx);
7766 const int match_len = ImStrimatchlen(req->SearchBuffer, req->SearchBuffer + req->SearchBufferLen, item_name);
7767 if (match_len <= longest_match_len)
7768 continue;
7769 longest_match_idx = idx;
7770 longest_match_len = match_len;
7771 if (match_len == req->SearchBufferLen)
7772 break;
7773 }
7774 return longest_match_idx;
7775}
7776
7777void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data)
7778{
7779#ifndef IMGUI_DISABLE_DEBUG_TOOLS
7780 Text("SearchBuffer = \"%s\"", data->SearchBuffer);
7781 Text("SingleCharMode = %d, Size = %d, Lock = %d", data->Request.SingleCharMode, data->Request.SingleCharSize, data->SingleCharModeLock);
7782 Text("LastRequest = time: %.2f, frame: %d", data->LastRequestTime, data->LastRequestFrame);
7783#else
7784 IM_UNUSED(data);
7785#endif
7786}
7787
7788//-------------------------------------------------------------------------
7789// [SECTION] Widgets: Box-Select support
7790// This has been extracted away from Multi-Select logic in the hope that it could eventually be used elsewhere, but hasn't been yet.
7791//-------------------------------------------------------------------------
7792// Extra logic in MultiSelectItemFooter() and ImGuiListClipper::Step()
7793//-------------------------------------------------------------------------
7794// - BoxSelectPreStartDrag() [Internal]
7795// - BoxSelectActivateDrag() [Internal]
7796// - BoxSelectDeactivateDrag() [Internal]
7797// - BoxSelectScrollWithMouseDrag() [Internal]
7798// - BeginBoxSelect() [Internal]
7799// - EndBoxSelect() [Internal]
7800//-------------------------------------------------------------------------
7801
7802// Call on the initial click.
7803static void BoxSelectPreStartDrag(ImGuiID id, ImGuiSelectionUserData clicked_item)
7804{
7805 ImGuiContext& g = *GImGui;
7806 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7807 bs->ID = id;
7808 bs->IsStarting = true; // Consider starting box-select.
7809 bs->IsStartedFromVoid = (clicked_item == ImGuiSelectionUserData_Invalid);
7810 bs->IsStartedSetNavIdOnce = bs->IsStartedFromVoid;
7811 bs->KeyMods = g.IO.KeyMods;
7812 bs->StartPosRel = bs->EndPosRel = ImGui::WindowPosAbsToRel(g.CurrentWindow, g.IO.MousePos);
7813 bs->ScrollAccum = ImVec2(0.0f, 0.0f);
7814}
7815
7816static void BoxSelectActivateDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window)
7817{
7818 ImGuiContext& g = *GImGui;
7819 IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Activate\n", bs->ID);
7820 bs->IsActive = true;
7821 bs->Window = window;
7822 bs->IsStarting = false;
7823 ImGui::SetActiveID(bs->ID, window);
7824 ImGui::SetActiveIdUsingAllKeyboardKeys();
7825 if (bs->IsStartedFromVoid && (bs->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0)
7826 bs->RequestClear = true;
7827}
7828
7829static void BoxSelectDeactivateDrag(ImGuiBoxSelectState* bs)
7830{
7831 ImGuiContext& g = *GImGui;
7832 bs->IsActive = bs->IsStarting = false;
7833 if (g.ActiveId == bs->ID)
7834 {
7835 IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Deactivate\n", bs->ID);
7836 ImGui::ClearActiveID();
7837 }
7838 bs->ID = 0;
7839}
7840
7841static void BoxSelectScrollWithMouseDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window, const ImRect& inner_r)
7842{
7843 ImGuiContext& g = *GImGui;
7844 IM_ASSERT(bs->Window == window);
7845 for (int n = 0; n < 2; n++) // each axis
7846 {
7847 const float mouse_pos = g.IO.MousePos[n];
7848 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;
7849 const float scroll_curr = window->Scroll[n];
7850 if (dist == 0.0f || (dist < 0.0f && scroll_curr < 0.0f) || (dist > 0.0f && scroll_curr >= window->ScrollMax[n]))
7851 continue;
7852
7853 const float speed_multiplier = ImLinearRemapClamp(g.FontSize, g.FontSize * 5.0f, 1.0f, 4.0f, ImAbs(dist)); // x1 to x4 depending on distance
7854 const float scroll_step = g.FontSize * 35.0f * speed_multiplier * ImSign(dist) * g.IO.DeltaTime;
7855 bs->ScrollAccum[n] += scroll_step;
7856
7857 // Accumulate into a stored value so we can handle high-framerate
7858 const float scroll_step_i = ImFloor(bs->ScrollAccum[n]);
7859 if (scroll_step_i == 0.0f)
7860 continue;
7861 if (n == 0)
7862 ImGui::SetScrollX(window, scroll_curr + scroll_step_i);
7863 else
7864 ImGui::SetScrollY(window, scroll_curr + scroll_step_i);
7865 bs->ScrollAccum[n] -= scroll_step_i;
7866 }
7867}
7868
7869bool ImGui::BeginBoxSelect(const ImRect& scope_rect, ImGuiWindow* window, ImGuiID box_select_id, ImGuiMultiSelectFlags ms_flags)
7870{
7871 ImGuiContext& g = *GImGui;
7872 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7873 KeepAliveID(box_select_id);
7874 if (bs->ID != box_select_id)
7875 return false;
7876
7877 // IsStarting is set by MultiSelectItemFooter() when considering a possible box-select. We validate it here and lock geometry.
7878 bs->UnclipMode = false;
7879 bs->RequestClear = false;
7880 if (bs->IsStarting && IsMouseDragPastThreshold(0))
7881 BoxSelectActivateDrag(bs, window);
7882 else if ((bs->IsStarting || bs->IsActive) && g.IO.MouseDown[0] == false)
7883 BoxSelectDeactivateDrag(bs);
7884 if (!bs->IsActive)
7885 return false;
7886
7887 // Current frame absolute prev/current rectangles are used to toggle selection.
7888 // They are derived from positions relative to scrolling space, so "previous" rectangle is reprojected for current frame coordinates.
7889 ImVec2 start_pos_abs = WindowPosRelToAbs(window, bs->StartPosRel);
7890 ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, bs->EndPosRel); // Clamped already
7891 ImVec2 curr_end_pos_abs = g.IO.MousePos;
7892 if (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) // Box-select scrolling only happens with ScopeWindow
7893 curr_end_pos_abs = ImClamp(curr_end_pos_abs, scope_rect.Min, scope_rect.Max);
7894 bs->BoxSelectRectPrev.Min = ImMin(start_pos_abs, prev_end_pos_abs);
7895 bs->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs);
7896 bs->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs);
7897 bs->BoxSelectRectCurr.Max = ImMax(start_pos_abs, curr_end_pos_abs);
7898 //IMGUI_DEBUG_LOG("StartPosRel (%.2f,%.2f) EndPosRel (%.2f,%.2f) -> (%.2f,%.2f)\n", bs->StartPosRel.x, bs->StartPosRel.y, bs->EndPosRel.x, bs->EndPosRel.y, WindowPosAbsToRel(window, g.IO.MousePos).x, WindowPosAbsToRel(window, g.IO.MousePos).y);
7899
7900 // Box-select 2D mode detects change of the rectangle.
7901 // Storing unclip rects which will be tested by widgets supporting box-select. Always update rectangles when active (even if we don't use them).
7902 // To facilitate understanding this: enable IMGUI_DEBUG_BOXSELECT and visualize all geometry.
7903 if (ms_flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7904 {
7905 // For both sides, compute the area differing between Prev and Curr rectangles.
7906 bs->UnclipRects[0] = bs->UnclipRects[1] = ImRect(+FLT_MAX, +FLT_MAX, -FLT_MAX, -FLT_MAX);
7907 for (int side = 0; side < 2; side++)
7908 {
7909 ImVec2 d_min = (side == 0) ? ImMin(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectPrev.Min) : ImMin(bs->BoxSelectRectCurr.Max, bs->BoxSelectRectPrev.Max);
7910 ImVec2 d_max = (side == 0) ? ImMax(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectPrev.Min) : ImMax(bs->BoxSelectRectCurr.Max, bs->BoxSelectRectPrev.Max);
7911 if (d_min.x != d_max.x)
7912 {
7913 bs->UnclipRects[0].AddX(d_min.x);
7914 bs->UnclipRects[0].AddX(d_max.x);
7915 }
7916 if (d_min.y != d_max.y)
7917 {
7918 bs->UnclipRects[1].AddY(d_min.y);
7919 bs->UnclipRects[1].AddY(d_max.y);
7920 }
7921 }
7922
7923 ImRect box_select_intersection = bs->BoxSelectRectPrev;
7924 box_select_intersection.Add(bs->BoxSelectRectCurr);
7925 if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d)
7926 if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x)
7927 {
7928 bs->UnclipRects[0].AddY(box_select_intersection.Min.y);
7929 bs->UnclipRects[0].AddY(box_select_intersection.Max.y);
7930 }
7931 if (ms_flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7932 if (bs->BoxSelectRectPrev.Min.y != bs->BoxSelectRectCurr.Min.y || bs->BoxSelectRectPrev.Max.y != bs->BoxSelectRectCurr.Max.y)
7933 {
7934 bs->UnclipRects[1].AddX(box_select_intersection.Min.x);
7935 bs->UnclipRects[1].AddX(box_select_intersection.Max.x);
7936 }
7937
7938 // Merge both rectangles into one.
7939 // FIXME-OPT: When UnclipRect.Area() is much larger than the sum of UnclipRects[0]/[1] Areas, widgets should
7940 // ideally first use UnclipRect as a first coarse cull layer + the individual ones as a second validation.
7941 bs->UnclipRect = bs->UnclipRects[0];
7942 bs->UnclipRect.Add(bs->UnclipRects[1]);
7943 if (!bs->UnclipRect.IsInverted() && (!window->ClipRect.Contains(bs->UnclipRect.Min) || !window->ClipRect.Contains(bs->UnclipRect.Max))) // !! Don't use Contains(ImRect)
7944 bs->UnclipMode = true;
7945 if (bs->UnclipMode && g.CurrentTable != NULL)
7946 TableApplyExternalUnclipRect(g.CurrentTable, bs->UnclipRect); // No need submitting both
7947 }
7948
7949#ifdef IMGUI_DEBUG_BOXSELECT
7950 //GetForegroundDrawList()->AddRect(scope_rect.Min, scope_rect.Max, IM_COL32(0, 255, 0, 200), 0.0f, 0, 4.0f);
7951 //GetForegroundDrawList()->AddRect(bs->BoxSelectRectPrev.Min, bs->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f);
7952 //GetForegroundDrawList()->AddRect(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f);
7953 if (ms_flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7954 {
7955 for (ImRect& unclip_r : bs->UnclipRects)
7956 if (!unclip_r.IsInverted())
7957 GetForegroundDrawList()->AddRect(unclip_r.Min, unclip_r.Max, bs->UnclipMode ? IM_COL32(255, 255, 0, 200) : IM_COL32(255, 0, 0, 200), 0.0f, 0, 4.0f);
7958 GetForegroundDrawList()->AddRect(bs->UnclipRect.Min, bs->UnclipRect.Max, bs->UnclipMode ? IM_COL32(255, 255, 0, 200) : IM_COL32(255, 0, 0, 200), 0.0f, 0, 2.0f);
7959 }
7960#endif
7961 return true;
7962}
7963
7964void ImGui::EndBoxSelect(const ImRect& scope_rect, ImGuiMultiSelectFlags ms_flags)
7965{
7966 ImGuiContext& g = *GImGui;
7967 ImGuiWindow* window = g.CurrentWindow;
7968 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7969 IM_ASSERT(bs->IsActive);
7970 bs->UnclipMode = false;
7971
7972 // Render selection rectangle
7973 bs->EndPosRel = WindowPosAbsToRel(window, ImClamp(g.IO.MousePos, scope_rect.Min, scope_rect.Max)); // Clamp stored position according to current scrolling view
7974 ImRect box_select_r = bs->BoxSelectRectCurr;
7975 box_select_r.ClipWith(scope_rect);
7976 ImGuiWindow* draw_window = FindFrontMostVisibleChildWindow(window);
7977 draw_window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling
7978 draw_window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavCursor)); // FIXME-MULTISELECT FIXME-DPI: Styling
7979
7980 // Scroll
7981 const bool enable_scroll = (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms_flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0;
7982 if (enable_scroll)
7983 {
7984 ImRect scroll_r = scope_rect;
7985 scroll_r.Expand(-g.FontSize);
7986 //GetForegroundDrawList()->AddRect(scroll_r.Min, scroll_r.Max, IM_COL32(0, 255, 0, 255));
7987 if (!scroll_r.Contains(g.IO.MousePos))
7988 BoxSelectScrollWithMouseDrag(bs, window, scroll_r);
7989 }
7990}
7991
7992//-------------------------------------------------------------------------
7993// [SECTION] Widgets: Multi-Select support
7994//-------------------------------------------------------------------------
7995// - DebugLogMultiSelectRequests() [Internal]
7996// - CalcScopeRect() [Internal]
7997// - BeginMultiSelect()
7998// - EndMultiSelect()
7999// - SetNextItemSelectionUserData()
8000// - MultiSelectItemHeader() [Internal]
8001// - MultiSelectItemFooter() [Internal]
8002// - DebugNodeMultiSelectState() [Internal]
8003//-------------------------------------------------------------------------
8004
8005static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* io)
8006{
8007 ImGuiContext& g = *GImGui;
8008 IM_UNUSED(function);
8009 for (const ImGuiSelectionRequest& req : io->Requests)
8010 {
8011 if (req.Type == ImGuiSelectionRequestType_SetAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetAll %d (= %s)\n", function, req.Selected, req.Selected ? "SelectAll" : "Clear");
8012 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);
8013 }
8014}
8015
8016static ImRect CalcScopeRect(ImGuiMultiSelectTempData* ms, ImGuiWindow* window)
8017{
8018 if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)
8019 {
8020 // Warning: this depends on CursorMaxPos so it means to be called by EndMultiSelect() only
8021 // This probably doesn't work inside a table as there are ample ambiguities related to exact time of calling BeginMultiSelect()/EndMultiSelect().
8022 return ImRect(ms->ScopeRectMin, ImMax(window->DC.CursorMaxPos, ms->ScopeRectMin));
8023 }
8024 else
8025 {
8026 //// When a table, pull HostClipRect, which allows us to predict ClipRect before first row/layout is performed. (#7970)
8027 ImRect scope_rect = window->InnerClipRect;
8028 //if (g.CurrentTable != NULL)
8029 // scope_rect = g.CurrentTable->HostClipRect;
8030
8031 // Add inner table decoration (#7821) // FIXME: Why not baking in InnerClipRect?
8032 scope_rect.Min = ImMin(scope_rect.Min + ImVec2(window->DecoInnerSizeX1, window->DecoInnerSizeY1), scope_rect.Max);
8033 return scope_rect;
8034 }
8035}
8036
8037// Return ImGuiMultiSelectIO structure.
8038// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect().
8039// Passing 'selection_size' and 'items_count' parameters is currently optional.
8040// - 'selection_size' is useful to disable some shortcut routing: e.g. ImGuiMultiSelectFlags_ClearOnEscape won't claim Escape key when selection_size 0,
8041// allowing a first press to clear selection THEN the second press to leave child window and return to parent.
8042// - '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).
8043// - If they are costly for you to compute (e.g. external intrusive selection without maintaining size), you may avoid them and pass -1.
8044// - 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.
8045ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int selection_size, int items_count)
8046{
8047 ImGuiContext& g = *GImGui;
8048 ImGuiWindow* window = g.CurrentWindow;
8049
8050 if (++g.MultiSelectTempDataStacked > g.MultiSelectTempData.Size)
8051 g.MultiSelectTempData.resize(g.MultiSelectTempDataStacked, ImGuiMultiSelectTempData());
8052 ImGuiMultiSelectTempData* ms = &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1];
8053 IM_STATIC_ASSERT(offsetof(ImGuiMultiSelectTempData, IO) == 0); // Clear() relies on that.
8054 g.CurrentMultiSelect = ms;
8055 if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0)
8056 flags |= ImGuiMultiSelectFlags_ScopeWindow;
8057 if (flags & ImGuiMultiSelectFlags_SingleSelect)
8058 flags &= ~(ImGuiMultiSelectFlags_BoxSelect2d | ImGuiMultiSelectFlags_BoxSelect1d);
8059 if (flags & ImGuiMultiSelectFlags_BoxSelect2d)
8060 flags &= ~ImGuiMultiSelectFlags_BoxSelect1d;
8061
8062 // FIXME: Workaround to the fact we override CursorMaxPos, meaning size measurement are lost. (#8250)
8063 // They should perhaps be stacked properly?
8064 if (ImGuiTable* table = g.CurrentTable)
8065 {
8066 if (!table->IsLayoutLocked)
8067 TableUpdateLayout(table);
8068 else if (table->CurrentColumn != -1)
8069 TableEndCell(table); // This is currently safe to call multiple time. If that properly is lost we can extract the "save measurement" part of it.
8070 }
8071
8072 // FIXME: BeginFocusScope()
8073 const ImGuiID id = window->IDStack.back();
8074 ms->Clear();
8075 ms->FocusScopeId = id;
8076 ms->Flags = flags;
8077 ms->BackupCursorMaxPos = window->DC.CursorMaxPos;
8078 ms->ScopeRectMin = window->DC.CursorPos;
8079 if (flags & ImGuiMultiSelectFlags_ScopeRect)
8080 window->DC.CursorMaxPos = ms->ScopeRectMin; // CalcScopeRect() for ImGuiMultiSelectFlags_ScopeRect will measure in EndMultiSelect().
8081 PushFocusScope(ms->FocusScopeId);
8082 ms->IsFocused = IsInNavFocusRoute(g.CurrentFocusScopeId);
8083 if (flags & ImGuiMultiSelectFlags_ScopeWindow) // Mark parent child window as navigable into, with highlight. Assume user will always submit interactive items.
8084 window->DC.NavLayersActiveMask |= 1 << ImGuiNavLayer_Main;
8085
8086 // Use copy of keyboard mods at the time of the request, otherwise we would requires mods to be held for an extra frame.
8087 ms->KeyMods = g.NavJustMovedToId ? (g.NavJustMovedToIsTabbing ? 0 : g.NavJustMovedToKeyMods) : g.IO.KeyMods;
8088 if (flags & ImGuiMultiSelectFlags_NoRangeSelect)
8089 ms->KeyMods &= ~ImGuiMod_Shift;
8090
8091 // Bind storage
8092 ImGuiMultiSelectState* storage = g.MultiSelectStorage.GetOrAddByKey(id);
8093 storage->ID = id;
8094 storage->LastFrameActive = g.FrameCount;
8095 storage->LastSelectionSize = selection_size;
8096 storage->Window = window;
8097 ms->Storage = storage;
8098
8099 // Output to user
8100 ms->IO.Requests.resize(0);
8101 ms->IO.RangeSrcItem = storage->RangeSrcItem;
8102 ms->IO.NavIdItem = storage->NavIdItem;
8103 ms->IO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false;
8104 ms->IO.ItemsCount = items_count;
8105
8106 // Clear when using Navigation to move within the scope
8107 // (we compare FocusScopeId so it possible to use multiple selections inside a same window)
8108 bool request_clear = false;
8109 bool request_select_all = false;
8110 if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData)
8111 {
8112 if (ms->KeyMods & ImGuiMod_Shift)
8113 ms->IsKeyboardSetRange = true;
8114 if (ms->IsKeyboardSetRange)
8115 IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid); // Not ready -> could clear?
8116 if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0)
8117 request_clear = true;
8118 }
8119 else if (g.NavJustMovedFromFocusScopeId == ms->FocusScopeId)
8120 {
8121 // Also clear on leaving scope (may be optional?)
8122 if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0)
8123 request_clear = true;
8124 }
8125
8126 // Box-select handling: update active state.
8127 ImGuiBoxSelectState* bs = &g.BoxSelectState;
8128 if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
8129 {
8130 ms->BoxSelectId = GetID("##BoxSelect");
8131 if (BeginBoxSelect(CalcScopeRect(ms, window), window, ms->BoxSelectId, flags))
8132 request_clear |= bs->RequestClear;
8133 }
8134
8135 if (ms->IsFocused)
8136 {
8137 // Shortcut: Clear selection (Escape)
8138 // - Only claim shortcut if selection is not empty, allowing further presses on Escape to e.g. leave current child window.
8139 // - Box select also handle Escape and needs to pass an id to bypass ActiveIdUsingAllKeyboardKeys lock.
8140 if (flags & ImGuiMultiSelectFlags_ClearOnEscape)
8141 {
8142 if (selection_size != 0 || bs->IsActive)
8143 if (Shortcut(ImGuiKey_Escape, ImGuiInputFlags_None, bs->IsActive ? bs->ID : 0))
8144 {
8145 request_clear = true;
8146 if (bs->IsActive)
8147 BoxSelectDeactivateDrag(bs);
8148 }
8149 }
8150
8151 // Shortcut: Select all (Ctrl+A)
8152 if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll))
8153 if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A))
8154 request_select_all = true;
8155 }
8156
8157 if (request_clear || request_select_all)
8158 {
8159 MultiSelectAddSetAll(ms, request_select_all);
8160 if (!request_select_all)
8161 storage->LastSelectionSize = 0;
8162 }
8163 ms->LoopRequestSetAll = request_select_all ? 1 : request_clear ? 0 : -1;
8164 ms->IsSoleOrUnknownSelectionSize = (storage->LastSelectionSize == 1) || (storage->LastSelectionSize == -1);
8165 //ms->PrevSubmittedItem = ImGuiSelectionUserData_Invalid;
8166
8167 if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)
8168 DebugLogMultiSelectRequests("BeginMultiSelect", &ms->IO);
8169
8170 return &ms->IO;
8171}
8172
8173// Return updated ImGuiMultiSelectIO structure.
8174// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect().
8175ImGuiMultiSelectIO* ImGui::EndMultiSelect()
8176{
8177 ImGuiContext& g = *GImGui;
8178 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
8179 ImGuiMultiSelectState* storage = ms->Storage;
8180 ImGuiWindow* window = g.CurrentWindow;
8181 IM_ASSERT_USER_ERROR(ms->FocusScopeId == g.CurrentFocusScopeId, "EndMultiSelect() FocusScope mismatch!");
8182 IM_ASSERT(g.CurrentMultiSelect != NULL && storage->Window == g.CurrentWindow);
8183 IM_ASSERT(g.MultiSelectTempDataStacked > 0 && &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] == g.CurrentMultiSelect);
8184
8185 ImRect scope_rect = CalcScopeRect(ms, window);
8186 if (ms->IsFocused)
8187 {
8188 // We currently don't allow user code to modify RangeSrcItem by writing to BeginIO's version, but that would be an easy change here.
8189 if (ms->IO.RangeSrcReset || (ms->RangeSrcPassedBy == false && ms->IO.RangeSrcItem != ImGuiSelectionUserData_Invalid)) // Can't read storage->RangeSrcItem here -> we want the state at beginning of the scope (see tests for easy failure)
8190 {
8191 IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset RangeSrcItem.\n"); // Will set be to NavId.
8192 storage->RangeSrcItem = ImGuiSelectionUserData_Invalid;
8193 }
8194 if (ms->NavIdPassedBy == false && storage->NavIdItem != ImGuiSelectionUserData_Invalid)
8195 {
8196 IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset NavIdItem.\n");
8197 storage->NavIdItem = ImGuiSelectionUserData_Invalid;
8198 storage->NavIdSelected = -1;
8199 }
8200
8201 if ((ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) && GetBoxSelectState(ms->BoxSelectId))
8202 EndBoxSelect(scope_rect, ms->Flags);
8203 }
8204
8205 if (ms->IsEndIO == false)
8206 ms->IO.Requests.resize(0);
8207
8208 // Clear selection when clicking void?
8209 // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection!
8210 // The InnerRect test is necessary for non-child/decorated windows.
8211 bool scope_hovered = window->InnerRect.Contains(g.IO.MousePos) && IsWindowHovered(ImGuiHoveredFlags_ChildWindows);
8212 if (scope_hovered && (ms->Flags & ImGuiMultiSelectFlags_ScopeRect))
8213 scope_hovered &= scope_rect.Contains(g.IO.MousePos);
8214 if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0)
8215 {
8216 if (ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
8217 {
8218 if (!g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && g.IO.MouseClickedCount[0] == 1)
8219 {
8220 BoxSelectPreStartDrag(ms->BoxSelectId, ImGuiSelectionUserData_Invalid);
8221 FocusWindow(window, ImGuiFocusRequestFlags_UnlessBelowModal);
8222 SetHoveredID(ms->BoxSelectId);
8223 if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)
8224 SetNavID(0, ImGuiNavLayer_Main, ms->FocusScopeId, ImRect(g.IO.MousePos, g.IO.MousePos)); // Automatically switch FocusScope for initial click from void to box-select.
8225 }
8226 }
8227
8228 if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid)
8229 if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None)
8230 MultiSelectAddSetAll(ms, false);
8231 }
8232
8233 // Courtesy nav wrapping helper flag
8234 if (ms->Flags & ImGuiMultiSelectFlags_NavWrapX)
8235 {
8236 IM_ASSERT(ms->Flags & ImGuiMultiSelectFlags_ScopeWindow); // Only supported at window scope
8237 NavMoveRequestTryWrapping(GetCurrentWindow(), ImGuiNavMoveFlags_WrapX);
8238 }
8239
8240 // Unwind
8241 if (ImGuiTable* table = g.CurrentTable)
8242 if (table->IsInsideRow)
8243 TableEndRow(table);
8244 window->DC.CursorMaxPos = ImMax(ms->BackupCursorMaxPos, window->DC.CursorMaxPos);
8245 PopFocusScope();
8246
8247 if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)
8248 DebugLogMultiSelectRequests("EndMultiSelect", &ms->IO);
8249
8250 ms->FocusScopeId = 0;
8251 ms->Flags = ImGuiMultiSelectFlags_None;
8252 g.CurrentMultiSelect = (--g.MultiSelectTempDataStacked > 0) ? &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] : NULL;
8253
8254 return &ms->IO;
8255}
8256
8257void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data)
8258{
8259 // Note that flags will be cleared by ItemAdd(), so it's only useful for Navigation code!
8260 // This designed so widgets can also cheaply set this before calling ItemAdd(), so we are not tied to MultiSelect api.
8261 ImGuiContext& g = *GImGui;
8262 g.NextItemData.SelectionUserData = selection_user_data;
8263 g.NextItemData.FocusScopeId = g.CurrentFocusScopeId;
8264
8265 if (ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect)
8266 {
8267 // Auto updating RangeSrcPassedBy for cases were clipper is not used (done before ItemAdd() clipping)
8268 g.NextItemData.ItemFlagsSet |= ImGuiItemFlags_HasSelectionUserData | ImGuiItemFlags_IsMultiSelect;
8269 if (ms->IO.RangeSrcItem == selection_user_data)
8270 ms->RangeSrcPassedBy = true;
8271 //ms->PrevSubmittedItem = ms->CurrSubmittedItem; // Can't rely on previous g.NextItemData.SelectionUserData because NextItemData is not restored on nested multi-select.
8272 //ms->CurrSubmittedItem = selection_user_data;
8273 }
8274 else
8275 {
8276 g.NextItemData.ItemFlagsSet |= ImGuiItemFlags_HasSelectionUserData;
8277 }
8278}
8279
8280// In charge of:
8281// - Applying SetAll for submitted items.
8282// - Applying SetRange for submitted items and record end points.
8283// - Altering button behavior flags to facilitate use with drag and drop.
8284void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags)
8285{
8286 ImGuiContext& g = *GImGui;
8287 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
8288
8289 bool selected = *p_selected;
8290 if (ms->IsFocused)
8291 {
8292 ImGuiMultiSelectState* storage = ms->Storage;
8293 ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData;
8294 IM_ASSERT(g.NextItemData.FocusScopeId == g.CurrentFocusScopeId && "Forgot to call SetNextItemSelectionUserData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope");
8295
8296 // Apply SetAll (Clear/SelectAll) requests requested by BeginMultiSelect().
8297 // This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper.
8298 // If you are using a clipper you need to process the SetAll request after calling BeginMultiSelect()
8299 if (ms->LoopRequestSetAll != -1)
8300 selected = (ms->LoopRequestSetAll == 1);
8301
8302 // 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)
8303 // For this to work, we need someone to set 'RangeSrcPassedBy = true' at some point (either clipper either SetNextItemSelectionUserData() function)
8304 if (ms->IsKeyboardSetRange)
8305 {
8306 IM_ASSERT(id != 0 && (ms->KeyMods & ImGuiMod_Shift) != 0);
8307 const bool is_range_dst = (ms->RangeDstPassedBy == false) && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped.
8308 if (is_range_dst)
8309 ms->RangeDstPassedBy = true;
8310 if (is_range_dst && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) // If we don't have RangeSrc, assign RangeSrc = RangeDst
8311 {
8312 storage->RangeSrcItem = item_data;
8313 storage->RangeSelected = selected ? 1 : 0;
8314 }
8315 const bool is_range_src = storage->RangeSrcItem == item_data;
8316 if (is_range_src || is_range_dst || ms->RangeSrcPassedBy != ms->RangeDstPassedBy)
8317 {
8318 // Apply range-select value to visible items
8319 IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid && storage->RangeSelected != -1);
8320 selected = (storage->RangeSelected != 0);
8321 }
8322 else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0 && (ms->Flags & ImGuiMultiSelectFlags_NoAutoClear) == 0)
8323 {
8324 // Clear other items
8325 selected = false;
8326 }
8327 }
8328 *p_selected = selected;
8329 }
8330
8331 // Alter button behavior flags
8332 // To handle drag and drop of multiple items we need to avoid clearing selection on click.
8333 // 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.
8334 if (p_button_flags != NULL)
8335 {
8336 ImGuiButtonFlags button_flags = *p_button_flags;
8337 button_flags |= ImGuiButtonFlags_NoHoveredOnFocus;
8338 button_flags &= ~(ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnClickRelease);
8339 if (ms->Flags & ImGuiMultiSelectFlags_SelectOnClickAlways)
8340 button_flags |= ImGuiButtonFlags_PressedOnClick;
8341 else if (ms->Flags & ImGuiMultiSelectFlags_SelectOnClickRelease)
8342 button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
8343 else // ImGuiMultiSelectFlags_SelectOnAuto
8344 button_flags |= (!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) ? ImGuiButtonFlags_PressedOnClick : ImGuiButtonFlags_PressedOnClickRelease;
8345 *p_button_flags = button_flags;
8346 }
8347}
8348
8349// In charge of:
8350// - Auto-select on navigation.
8351// - Box-select toggle handling.
8352// - Right-click handling.
8353// - Altering selection based on Ctrl/Shift modifiers, both for keyboard and mouse.
8354// - Record current selection state for RangeSrc
8355// This is all rather complex, best to run and refer to "widgets_multiselect_xxx" tests in imgui_test_suite.
8356void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed, ImGuiMultiSelectFlags extra_flags)
8357{
8358 ImGuiContext& g = *GImGui;
8359 ImGuiWindow* window = g.CurrentWindow;
8360
8361 bool selected = *p_selected;
8362 bool pressed = *p_pressed;
8363 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
8364 ImGuiMultiSelectState* storage = ms->Storage;
8365 if (pressed)
8366 ms->IsFocused = true;
8367
8368 bool hovered = false;
8369 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect)
8370 hovered = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
8371 if (!ms->IsFocused && !hovered)
8372 return;
8373
8374 ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData;
8375
8376 ImGuiMultiSelectFlags flags = ms->Flags | extra_flags;
8377 const bool is_singleselect = (flags & ImGuiMultiSelectFlags_SingleSelect) != 0;
8378 bool is_ctrl = (ms->KeyMods & ImGuiMod_Ctrl) != 0;
8379 bool is_shift = (ms->KeyMods & ImGuiMod_Shift) != 0;
8380
8381 bool apply_to_range_src = false;
8382
8383 if (g.NavId == id && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid)
8384 apply_to_range_src = true;
8385 if (ms->IsEndIO == false)
8386 {
8387 ms->IO.Requests.resize(0);
8388 ms->IsEndIO = true;
8389 }
8390
8391 // Auto-select as you navigate a list
8392 if (g.NavJustMovedToId == id)
8393 {
8394 if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
8395 {
8396 if (is_ctrl && is_shift)
8397 pressed = true;
8398 else if (!is_ctrl)
8399 selected = pressed = true;
8400 }
8401 else
8402 {
8403 // With NoAutoSelect, using Shift+keyboard performs a write/copy
8404 if (is_shift)
8405 pressed = true;
8406 else if (!is_ctrl)
8407 apply_to_range_src = true; // Since if (pressed) {} main block is not running we update this
8408 }
8409 }
8410
8411 if (apply_to_range_src)
8412 {
8413 storage->RangeSrcItem = item_data;
8414 storage->RangeSelected = selected; // Will be updated at the end of this function anyway.
8415 }
8416
8417 // Box-select toggle handling
8418 if (ms->BoxSelectId != 0)
8419 if (ImGuiBoxSelectState* bs = GetBoxSelectState(ms->BoxSelectId))
8420 {
8421 ImRect item_rect = g.LastItemData.Rect;
8422 if (!window->DC.NavIsScrollPushableX) // FIXME: Rename to be more generic.
8423 if (ImGuiTable* table = g.CurrentTable)
8424 if (table->CurrentColumn != -1)
8425 {
8426 // FIXME: We cannot solely use current ClipRect as it includes HostClipRect.
8427 // However we account for ClipRect being larger than current column (e.g. when using SpanAllColumns)
8428 // A more generic version would be nice, but window->WorkRect.Min/Max exclude CellPadding. (#7994, #9383)
8429 ImGuiTableColumn* column = &table->Columns[table->CurrentColumn];
8430 float clip_min_x = (g.LastItemData.ItemFlags & ImGuiItemStatusFlags_HasClipRect) ? g.LastItemData.ClipRect.Min.x : window->ClipRect.Min.x;
8431 float clip_max_x = (g.LastItemData.ItemFlags & ImGuiItemStatusFlags_HasClipRect) ? g.LastItemData.ClipRect.Max.x : window->ClipRect.Max.x;
8432 if (clip_min_x != clip_max_x) // When zero sized we expect that bounds have been clamped and thus are unreliable
8433 {
8434 item_rect.Min.x = ImMax(item_rect.Min.x, ImMin(column->MinX, clip_min_x));
8435 item_rect.Max.x = ImMin(item_rect.Max.x, ImMax(column->MaxX, clip_max_x));
8436 }
8437 else
8438 {
8439 item_rect.Min.x = ImMax(item_rect.Min.x, column->MinX);
8440 item_rect.Max.x = ImMin(item_rect.Max.x, column->MaxX);
8441 }
8442 //GetForegroundDrawList()->AddRect(item_rect.Min, item_rect.Max, IM_COL32(255, 0, 255, 255));
8443 }
8444 const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(item_rect);
8445 const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(item_rect);
8446 if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr))
8447 {
8448 if (storage->LastSelectionSize <= 0 && bs->IsStartedSetNavIdOnce)
8449 {
8450 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)
8451 bs->IsStartedSetNavIdOnce = false;
8452 }
8453 else
8454 {
8455 selected = !selected;
8456 MultiSelectAddSetRange(ms, selected, +1, item_data, item_data);
8457#ifdef IMGUI_DEBUG_BOXSELECT
8458 GetForegroundDrawList()->AddRectFilled(g.LastItemData.Rect.Min, g.LastItemData.Rect.Max, selected ? IM_COL32(0, 255, 0, 200) : IM_COL32(255, 0, 0, 200));
8459#endif
8460 }
8461 storage->LastSelectionSize = ImMax(storage->LastSelectionSize + 1, 1);
8462 }
8463 }
8464
8465 // Right-click handling.
8466 // FIXME-MULTISELECT: Maybe should be moved to Selectable()? Also see #5816, #8200, #9015
8467 if (hovered && IsMouseClicked(1) && (flags & (ImGuiMultiSelectFlags_NoAutoSelect | ImGuiMultiSelectFlags_NoSelectOnRightClick)) == 0)
8468 {
8469 if (g.ActiveId != 0 && g.ActiveId != id)
8470 ClearActiveID();
8471 SetFocusID(id, window);
8472 if (!pressed && !selected)
8473 {
8474 pressed = true;
8475 is_ctrl = is_shift = false;
8476 }
8477 }
8478
8479 // Unlike Space, Enter doesn't alter selection (but can still return a press) unless current item is not selected.
8480 // The later, "unless current item is not select", may become optional? It seems like a better default if Enter doesn't necessarily open something
8481 // (unlike e.g. Windows explorer). For use case where Enter always open something, we might decide to make this optional?
8482 const bool enter_pressed = pressed && (g.NavActivateId == id) && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput);
8483
8484 // Alter selection
8485 if (pressed && (!enter_pressed || !selected))
8486 {
8487 // Box-select
8488 ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse;
8489 if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
8490 if (!g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1)
8491 BoxSelectPreStartDrag(ms->BoxSelectId, item_data);
8492
8493 //----------------------------------------------------------------------------------------
8494 // ACTION | Begin | Pressed/Activated | End
8495 //----------------------------------------------------------------------------------------
8496 // Keys Navigated: | Clear | Src=item, Sel=1 SetRange 1
8497 // Keys Navigated: Ctrl | n/a | n/a
8498 // Keys Navigated: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1
8499 // Keys Navigated: Ctrl+Shift | n/a | Dst=item, Sel=Src => Clear + SetRange Src-Dst
8500 // Keys Activated: | n/a | Src=item, Sel=1 => Clear + SetRange 1
8501 // Keys Activated: Ctrl | n/a | Src=item, Sel=!Sel => SetSange 1
8502 // Keys Activated: Shift | n/a | Dst=item, Sel=1 => Clear + SetSange 1
8503 //----------------------------------------------------------------------------------------
8504 // Mouse Pressed: | n/a | Src=item, Sel=1, => Clear + SetRange 1
8505 // Mouse Pressed: Ctrl | n/a | Src=item, Sel=!Sel => SetRange 1
8506 // Mouse Pressed: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1
8507 // Mouse Pressed: Ctrl+Shift | n/a | Dst=item, Sel=!Sel => SetRange Src-Dst
8508 //----------------------------------------------------------------------------------------
8509
8510 if ((flags & ImGuiMultiSelectFlags_NoAutoClear) == 0)
8511 {
8512 bool request_clear = false;
8513 if (is_singleselect)
8514 request_clear = true;
8515 else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl)
8516 request_clear = (flags & ImGuiMultiSelectFlags_NoAutoClearOnReselect) ? !selected : true;
8517 else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl)
8518 request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again.
8519 if (request_clear)
8520 MultiSelectAddSetAll(ms, false);
8521 }
8522
8523 int range_direction;
8524 bool range_selected;
8525 if (is_shift && !is_singleselect)
8526 {
8527 //IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue);
8528 if (storage->RangeSrcItem == ImGuiSelectionUserData_Invalid)
8529 storage->RangeSrcItem = item_data;
8530 if ((flags & ImGuiMultiSelectFlags_CheckboxMode_) == 0)
8531 {
8532 // Shift+Arrow always select
8533 // Ctrl+Shift+Arrow copy source selection state (already stored by BeginMultiSelect() in storage->RangeSelected)
8534 range_selected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true;
8535 }
8536 else
8537 {
8538 // Shift+Arrow copy source selection state
8539 // Shift+Click always copy from target selection state
8540 if (ms->IsKeyboardSetRange)
8541 range_selected = (storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true;
8542 else
8543 range_selected = !selected;
8544 }
8545 range_direction = ms->RangeSrcPassedBy ? +1 : -1;
8546 }
8547 else
8548 {
8549 // Ctrl inverts selection, otherwise always select
8550 if ((flags & ImGuiMultiSelectFlags_CheckboxMode_) == 0)
8551 selected = is_ctrl ? !selected : true;
8552 else
8553 selected = !selected;
8554 storage->RangeSrcItem = item_data;
8555 range_selected = selected;
8556 range_direction = +1;
8557 }
8558 MultiSelectAddSetRange(ms, range_selected, range_direction, storage->RangeSrcItem, item_data);
8559 }
8560
8561 // Update/store the selection state of the Source item (used by Ctrl+Shift, when Source is unselected we perform a range unselect)
8562 if (storage->RangeSrcItem == item_data)
8563 storage->RangeSelected = selected ? 1 : 0;
8564
8565 // Update/store the selection state of focused item
8566 if (g.NavId == id)
8567 {
8568 storage->NavIdItem = item_data;
8569 storage->NavIdSelected = selected ? 1 : 0;
8570 }
8571 if (storage->NavIdItem == item_data)
8572 ms->NavIdPassedBy = true;
8573
8574 *p_selected = selected;
8575 *p_pressed = pressed;
8576}
8577
8578void ImGui::MultiSelectAddSetAll(ImGuiMultiSelectTempData* ms, bool selected)
8579{
8580 ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, selected, 0, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid };
8581 ms->IO.Requests.resize(0); // Can always clear previous requests
8582 ms->IO.Requests.push_back(req); // Add new request
8583}
8584
8585void ImGui::MultiSelectAddSetRange(ImGuiMultiSelectTempData* ms, bool selected, int range_dir, ImGuiSelectionUserData first_item, ImGuiSelectionUserData last_item)
8586{
8587 // Merge contiguous spans into same request (unless NoRangeSelect is set which guarantees single-item ranges)
8588 // FIXME-OPT: Disabled on 2026/04/09 as this would break with any form of coarse clipping that we don't know about (e.g. TableNextColumn() return value).
8589 // The low-hanging fruit would be to know that ImGuiSelectionUserData are sequential indices, in which case we can trivially compare PrevSubmittedItem + RangeDir == FirstItem.
8590 // User can always perform this merge if required.
8591#if 0
8592 if (ms->IO.Requests.Size > 0 && first_item == last_item && (ms->Flags & ImGuiMultiSelectFlags_NoRangeSelect) == 0)
8593 {
8594 ImGuiSelectionRequest* prev = &ms->IO.Requests.Data[ms->IO.Requests.Size - 1];
8595 if (prev->Type == ImGuiSelectionRequestType_SetRange && prev->RangeLastItem == ms->PrevSubmittedItem && prev->Selected == selected)
8596 {
8597 prev->RangeLastItem = last_item;
8598 return;
8599 }
8600 }
8601#endif
8602
8603 ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, (ImS8)range_dir, (range_dir > 0) ? first_item : last_item, (range_dir > 0) ? last_item : first_item };
8604 ms->IO.Requests.push_back(req); // Add new request
8605}
8606
8607void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage)
8608{
8609#ifndef IMGUI_DISABLE_DEBUG_TOOLS
8610 const bool is_active = (storage->LastFrameActive >= GetFrameCount() - 2); // Note that fully clipped early out scrolling tables will appear as inactive here.
8611 if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); }
8612 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*");
8613 if (!is_active) { PopStyleColor(); }
8614 if (!open)
8615 return;
8616 Text("RangeSrcItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), RangeSelected = %d", storage->RangeSrcItem, storage->RangeSrcItem, storage->RangeSelected);
8617 Text("NavIdItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), NavIdSelected = %d", storage->NavIdItem, storage->NavIdItem, storage->NavIdSelected);
8618 Text("LastSelectionSize = %d", storage->LastSelectionSize); // Provided by user
8619 TreePop();
8620#else
8621 IM_UNUSED(storage);
8622#endif
8623}
8624
8625//-------------------------------------------------------------------------
8626// [SECTION] Widgets: Multi-Select helpers
8627//-------------------------------------------------------------------------
8628// - ImGuiSelectionBasicStorage
8629// - ImGuiSelectionExternalStorage
8630//-------------------------------------------------------------------------
8631
8632ImGuiSelectionBasicStorage::ImGuiSelectionBasicStorage()
8633{
8634 Size = 0;
8635 PreserveOrder = false;
8636 UserData = NULL;
8637 AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; };
8638 _SelectionOrder = 1; // Always >0
8639}
8640
8641void ImGuiSelectionBasicStorage::Clear()
8642{
8643 Size = 0;
8644 _SelectionOrder = 1; // Always >0
8645 _Storage.Data.resize(0);
8646}
8647
8648void ImGuiSelectionBasicStorage::Swap(ImGuiSelectionBasicStorage& r)
8649{
8650 ImSwap(Size, r.Size);
8651 ImSwap(_SelectionOrder, r._SelectionOrder);
8652 _Storage.Data.swap(r._Storage.Data);
8653}
8654
8655bool ImGuiSelectionBasicStorage::Contains(ImGuiID id) const
8656{
8657 return _Storage.GetInt(id, 0) != 0;
8658}
8659
8660static int IMGUI_CDECL PairComparerByValueInt(const void* lhs, const void* rhs)
8661{
8662 int lhs_v = ((const ImGuiStoragePair*)lhs)->val_i;
8663 int rhs_v = ((const ImGuiStoragePair*)rhs)->val_i;
8664 return (lhs_v > rhs_v ? +1 : lhs_v < rhs_v ? -1 : 0);
8665}
8666
8667// GetNextSelectedItem() is an abstraction allowing us to change our underlying actual storage system without impacting user.
8668// (e.g. store unselected vs compact down, compact down on demand, use raw ImVector<ImGuiID> instead of ImGuiStorage...)
8669bool ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it, ImGuiID* out_id)
8670{
8671 ImGuiStoragePair* it = (ImGuiStoragePair*)*opaque_it;
8672 ImGuiStoragePair* it_end = _Storage.Data.Data + _Storage.Data.Size;
8673 if (PreserveOrder && it == NULL && it_end != NULL)
8674 ImQsort(_Storage.Data.Data, (size_t)_Storage.Data.Size, sizeof(ImGuiStoragePair), PairComparerByValueInt); // ~ImGuiStorage::BuildSortByValueInt()
8675 if (it == NULL)
8676 it = _Storage.Data.Data;
8677 IM_ASSERT(it >= _Storage.Data.Data && it <= it_end);
8678 if (it != it_end)
8679 while (it->val_i == 0 && it < it_end)
8680 it++;
8681 const bool has_more = (it != it_end);
8682 *opaque_it = has_more ? (void**)(it + 1) : (void**)(it);
8683 *out_id = has_more ? it->key : 0;
8684 if (PreserveOrder && !has_more)
8685 _Storage.BuildSortByKey();
8686 return has_more;
8687}
8688
8689void ImGuiSelectionBasicStorage::SetItemSelected(ImGuiID id, bool selected)
8690{
8691 int* p_int = _Storage.GetIntRef(id, 0);
8692 if (selected && *p_int == 0) { *p_int = _SelectionOrder++; Size++; }
8693 else if (!selected && *p_int != 0) { *p_int = 0; Size--; }
8694}
8695
8696// Optimized for batch edits (with same value of 'selected')
8697static void ImGuiSelectionBasicStorage_BatchSetItemSelected(ImGuiSelectionBasicStorage* selection, ImGuiID id, bool selected, int size_before_amends, int selection_order)
8698{
8699 ImGuiStorage* storage = &selection->_Storage;
8700 ImGuiStoragePair* it = ImLowerBound(storage->Data.Data, storage->Data.Data + size_before_amends, id);
8701 const bool is_contained = (it != storage->Data.Data + size_before_amends) && (it->key == id);
8702 if (selected == (is_contained && it->val_i != 0))
8703 return;
8704 if (selected && !is_contained)
8705 storage->Data.push_back(ImGuiStoragePair(id, selection_order)); // Push unsorted at end of vector, will be sorted in SelectionMultiAmendsFinish()
8706 else if (is_contained)
8707 it->val_i = selected ? selection_order : 0; // Modify in-place.
8708 selection->Size += selected ? +1 : -1;
8709}
8710
8711static void ImGuiSelectionBasicStorage_BatchFinish(ImGuiSelectionBasicStorage* selection, bool selected, int size_before_amends)
8712{
8713 ImGuiStorage* storage = &selection->_Storage;
8714 if (selected && selection->Size != size_before_amends)
8715 storage->BuildSortByKey(); // When done selecting: sort everything
8716}
8717
8718// Apply requests coming from BeginMultiSelect() and EndMultiSelect().
8719// - Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen.
8720// - Honoring SetRange requests requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem.
8721// - In this demo we often submit indices to SetNextItemSelectionUserData() + store the same indices in persistent selection.
8722// - Your code may do differently. If you store pointers or objects ID in ImGuiSelectionUserData you may need to perform
8723// a lookup in order to have some way to iterate/interpolate between two items.
8724// - A full-featured application is likely to allow search/filtering which is likely to lead to using indices
8725// and constructing a view index <> object id/ptr data structure anyway.
8726// WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THIS UNNECESSARY 'ImGuiSelectionBasicStorage' INDIRECTION LOGIC.
8727// Notice that with the simplest adapter (using indices everywhere), all functions return their parameters.
8728// The most simple implementation (using indices everywhere) would look like:
8729// for (ImGuiSelectionRequest& req : ms_io->Requests)
8730// {
8731// if (req.Type == ImGuiSelectionRequestType_SetAll) { Clear(); if (req.Selected) { for (int n = 0; n < items_count; n++) { SetItemSelected(n, true); } }
8732// if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { SetItemSelected(n, ms_io->Selected); } }
8733// }
8734void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
8735{
8736 // For convenience we obtain ItemsCount as passed to BeginMultiSelect(), which is optional.
8737 // It makes sense when using ImGuiSelectionBasicStorage to simply pass your items count to BeginMultiSelect().
8738 // Other scheme may handle SetAll differently.
8739 IM_ASSERT(ms_io->ItemsCount != -1 && "Missing value for items_count in BeginMultiSelect() call!");
8740 IM_ASSERT(AdapterIndexToStorageId != NULL);
8741
8742 // This is optimized/specialized to cope with very large selections (e.g. 100k+ items)
8743 // - A simpler version could call SetItemSelected() directly instead of ImGuiSelectionBasicStorage_BatchSetItemSelected() + ImGuiSelectionBasicStorage_BatchFinish().
8744 // - Optimized select can append unsorted, then sort in a second pass. Optimized unselect can clear in-place then compact in a second pass.
8745 // - 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.
8746 // - 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
8747 // left and right: it affects coarse clipping + can emit multiple SetRange with 1 item each.
8748 // FIXME-OPT: For each block of consecutive SetRange request:
8749 // - add all requests to a sorted list, store ID, selected, offset in ImGuiStorage.
8750 // - rewrite sorted storage a single time.
8751 for (ImGuiSelectionRequest& req : ms_io->Requests)
8752 {
8753 if (req.Type == ImGuiSelectionRequestType_SetAll)
8754 {
8755 Clear();
8756 if (req.Selected)
8757 {
8758 _Storage.Data.reserve(ms_io->ItemsCount);
8759 const int size_before_amends = _Storage.Data.Size;
8760 for (int idx = 0; idx < ms_io->ItemsCount; idx++, _SelectionOrder++)
8761 ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, _SelectionOrder);
8762 ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends);
8763 }
8764 }
8765 else if (req.Type == ImGuiSelectionRequestType_SetRange)
8766 {
8767 const int selection_changes = (int)req.RangeLastItem - (int)req.RangeFirstItem + 1;
8768 //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);
8769 if (selection_changes == 1 || (selection_changes < Size / 100))
8770 {
8771 // Multiple sorted insertion + copy likely to be faster.
8772 // Technically we could do a single copy with a little more work (sort sequential SetRange requests)
8773 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++)
8774 SetItemSelected(GetStorageIdFromIndex(idx), req.Selected);
8775 }
8776 else
8777 {
8778 // Append insertion + single sort likely be faster.
8779 // Use req.RangeDirection to set order field so that Shift+Clicking from 1 to 5 is different than Shift+Clicking from 5 to 1
8780 const int size_before_amends = _Storage.Data.Size;
8781 int selection_order = _SelectionOrder + ((req.RangeDirection < 0) ? selection_changes - 1 : 0);
8782 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++, selection_order += req.RangeDirection)
8783 ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, selection_order);
8784 if (req.Selected)
8785 _SelectionOrder += selection_changes;
8786 ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends);
8787 }
8788 }
8789 }
8790}
8791
8792//-------------------------------------------------------------------------
8793
8794ImGuiSelectionExternalStorage::ImGuiSelectionExternalStorage()
8795{
8796 UserData = NULL;
8797 AdapterSetItemSelected = NULL;
8798}
8799
8800// Apply requests coming from BeginMultiSelect() and EndMultiSelect().
8801// We also pull 'ms_io->ItemsCount' as passed for BeginMultiSelect() for consistency with ImGuiSelectionBasicStorage
8802// This makes no assumption about underlying storage.
8803void ImGuiSelectionExternalStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
8804{
8805 IM_ASSERT(AdapterSetItemSelected);
8806 for (ImGuiSelectionRequest& req : ms_io->Requests)
8807 {
8808 if (req.Type == ImGuiSelectionRequestType_SetAll)
8809 for (int idx = 0; idx < ms_io->ItemsCount; idx++)
8810 AdapterSetItemSelected(this, idx, req.Selected);
8811 if (req.Type == ImGuiSelectionRequestType_SetRange)
8812 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++)
8813 AdapterSetItemSelected(this, idx, req.Selected);
8814 }
8815}
8816
8817//-------------------------------------------------------------------------
8818// [SECTION] Widgets: ListBox
8819//-------------------------------------------------------------------------
8820// - BeginListBox()
8821// - EndListBox()
8822// - ListBox()
8823//-------------------------------------------------------------------------
8824
8825// This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label.
8826// This handle some subtleties with capturing info from the label.
8827// If you don't need a label you can pretty much directly use ImGui::BeginChild() with ImGuiChildFlags_FrameStyle.
8828// Tip: To have a list filling the entire window width, use size.x = -FLT_MIN and pass an non-visible label e.g. "##empty"
8829// 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.5f * item_height).
8830bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg)
8831{
8832 ImGuiContext& g = *GImGui;
8833 ImGuiWindow* window = GetCurrentWindow();
8834 if (window->SkipItems)
8835 return false;
8836
8837 const ImGuiStyle& style = g.Style;
8838 const ImGuiID id = GetID(label);
8839 const char* label_end = FindRenderedTextEnd(label);
8840 const ImVec2 label_size = CalcTextSize(label, label_end, false);
8841
8842 // Size default to hold ~7.25 items.
8843 // Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar.
8844 ImVec2 size = ImTrunc(CalcItemSize(size_arg, CalcItemWidth(), GetTextLineHeightWithSpacing() * 7.25f + style.FramePadding.y * 2.0f));
8845 ImVec2 frame_size = ImVec2(size.x, ImMax(size.y, label_size.y));
8846 ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
8847 ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
8848 g.NextItemData.ClearFlags();
8849
8850 if (!IsRectVisible(bb.Min, bb.Max))
8851 {
8852 ItemSize(bb.GetSize(), style.FramePadding.y);
8853 ItemAdd(bb, 0, &frame_bb);
8854 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
8855 return false;
8856 }
8857
8858 // FIXME-OPT: We could omit the BeginGroup() if label_size.x == 0.0f but would need to omit the EndGroup() as well.
8859 BeginGroup();
8860 if (label_size.x > 0.0f)
8861 {
8862 ImVec2 label_pos = ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y);
8863 RenderText(label_pos, label, label_end, false);
8864 window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, label_pos + label_size);
8865 AlignTextToFramePadding();
8866 }
8867
8868 BeginChild(id, frame_bb.GetSize(), ImGuiChildFlags_FrameStyle);
8869 return true;
8870}
8871
8872void ImGui::EndListBox()
8873{
8874 ImGuiContext& g = *GImGui;
8875 ImGuiWindow* window = g.CurrentWindow;
8876 IM_ASSERT((window->Flags & ImGuiWindowFlags_ChildWindow) && "Mismatched BeginListBox/EndListBox calls. Did you test the return value of BeginListBox?");
8877 IM_UNUSED(window);
8878
8879 EndChild();
8880 EndGroup(); // This is only required to be able to do IsItemXXX query on the whole ListBox including label
8881}
8882
8883bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items)
8884{
8885 const bool value_changed = ListBox(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_items);
8886 return value_changed;
8887}
8888
8889// This is merely a helper around BeginListBox(), EndListBox().
8890// Considering using those directly to submit custom data or store selection differently.
8891bool 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)
8892{
8893 ImGuiContext& g = *GImGui;
8894
8895 // Calculate size from "height_in_items"
8896 if (height_in_items < 0)
8897 height_in_items = ImMin(items_count, 7);
8898 float height_in_items_f = height_in_items + 0.25f;
8899 ImVec2 size(0.0f, ImTrunc(GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f));
8900
8901 if (!BeginListBox(label, size))
8902 return false;
8903
8904 // Assume all items have even height (= 1 line of text). If you need items of different height,
8905 // you can create a custom version of ListBox() in your code without using the clipper.
8906 bool value_changed = false;
8907 ImGuiListClipper clipper;
8908 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.
8909 clipper.IncludeItemByIndex(*current_item);
8910 while (clipper.Step())
8911 for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
8912 {
8913 const char* item_text = getter(user_data, i);
8914 if (item_text == NULL)
8915 item_text = "*Unknown item*";
8916
8917 PushID(i);
8918 const bool item_selected = (i == *current_item);
8919 if (Selectable(item_text, item_selected))
8920 {
8921 *current_item = i;
8922 value_changed = true;
8923 }
8924 if (item_selected)
8925 SetItemDefaultFocus();
8926 PopID();
8927 }
8928 EndListBox();
8929
8930 if (value_changed)
8931 MarkItemEdited(g.LastItemData.ID);
8932
8933 return value_changed;
8934}
8935
8936//-------------------------------------------------------------------------
8937// [SECTION] Widgets: PlotLines, PlotHistogram
8938//-------------------------------------------------------------------------
8939// - PlotEx() [Internal]
8940// - PlotLines()
8941// - PlotHistogram()
8942//-------------------------------------------------------------------------
8943// Plot/Graph widgets are not very good.
8944// Consider using ImPlot (https://github.com/epezent/implot) which is much better!
8945//-------------------------------------------------------------------------
8946
8947int 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)
8948{
8949 ImGuiContext& g = *GImGui;
8950 ImGuiWindow* window = GetCurrentWindow();
8951 if (window->SkipItems)
8952 return -1;
8953
8954 const ImGuiStyle& style = g.Style;
8955 const ImGuiID id = window->GetID(label);
8956
8957 const char* label_end = FindRenderedTextEnd(label);
8958 const ImVec2 label_size = CalcTextSize(label, label_end, false);
8959 const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), label_size.y + style.FramePadding.y * 2.0f);
8960
8961 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
8962 const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding);
8963 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));
8964 ItemSize(total_bb, style.FramePadding.y);
8965 if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_NoNav))
8966 return -1;
8967 bool hovered;
8968 ButtonBehavior(frame_bb, id, &hovered, NULL);
8969
8970 // Determine scale from values if not specified
8971 if (scale_min == FLT_MAX || scale_max == FLT_MAX)
8972 {
8973 float v_min = FLT_MAX;
8974 float v_max = -FLT_MAX;
8975 for (int i = 0; i < values_count; i++)
8976 {
8977 const float v = values_getter(data, i);
8978 if (v != v) // Ignore NaN values
8979 continue;
8980 v_min = ImMin(v_min, v);
8981 v_max = ImMax(v_max, v);
8982 }
8983 if (scale_min == FLT_MAX)
8984 scale_min = v_min;
8985 if (scale_max == FLT_MAX)
8986 scale_max = v_max;
8987 }
8988
8989 RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
8990
8991 const int values_count_min = (plot_type == ImGuiPlotType_Lines) ? 2 : 1;
8992 int idx_hovered = -1;
8993 if (values_count >= values_count_min)
8994 {
8995 int res_w = ImMin((int)frame_size.x, values_count) + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
8996 int item_count = values_count + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
8997
8998 // Tooltip on hover
8999 if (hovered && inner_bb.Contains(g.IO.MousePos))
9000 {
9001 const float t = ImClamp((g.IO.MousePos.x - inner_bb.Min.x) / (inner_bb.Max.x - inner_bb.Min.x), 0.0f, 0.999f);
9002 const int v_idx = (int)(t * item_count);
9003 IM_ASSERT(v_idx >= 0 && v_idx < values_count);
9004
9005 const float v0 = values_getter(data, (v_idx + values_offset) % values_count);
9006 const float v1 = values_getter(data, (v_idx + 1 + values_offset) % values_count);
9007 if (plot_type == ImGuiPlotType_Lines)
9008 SetTooltip("%d: %8.4g\n%d: %8.4g", v_idx, v0, v_idx + 1, v1);
9009 else if (plot_type == ImGuiPlotType_Histogram)
9010 SetTooltip("%d: %8.4g", v_idx, v0);
9011 idx_hovered = v_idx;
9012 }
9013
9014 const float t_step = 1.0f / (float)res_w;
9015 const float inv_scale = (scale_min == scale_max) ? 0.0f : (1.0f / (scale_max - scale_min));
9016
9017 float v0 = values_getter(data, (0 + values_offset) % values_count);
9018 float t0 = 0.0f;
9019 ImVec2 tp0 = ImVec2( t0, 1.0f - ImSaturate((v0 - scale_min) * inv_scale) ); // Point in the normalized space of our target rectangle
9020 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
9021
9022 const ImU32 col_base = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLines : ImGuiCol_PlotHistogram);
9023 const ImU32 col_hovered = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLinesHovered : ImGuiCol_PlotHistogramHovered);
9024
9025 for (int n = 0; n < res_w; n++)
9026 {
9027 const float t1 = t0 + t_step;
9028 const int v1_idx = (int)(t0 * item_count + 0.5f);
9029 IM_ASSERT(v1_idx >= 0 && v1_idx < values_count);
9030 const float v1 = values_getter(data, (v1_idx + values_offset + 1) % values_count);
9031 const ImVec2 tp1 = ImVec2( t1, 1.0f - ImSaturate((v1 - scale_min) * inv_scale) );
9032
9033 // 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.
9034 ImVec2 pos0 = ImLerp(inner_bb.Min, inner_bb.Max, tp0);
9035 ImVec2 pos1 = ImLerp(inner_bb.Min, inner_bb.Max, (plot_type == ImGuiPlotType_Lines) ? tp1 : ImVec2(tp1.x, histogram_zero_line_t));
9036 if (plot_type == ImGuiPlotType_Lines)
9037 {
9038 window->DrawList->AddLine(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base);
9039 }
9040 else if (plot_type == ImGuiPlotType_Histogram)
9041 {
9042 if (pos1.x >= pos0.x + 2.0f)
9043 pos1.x -= 1.0f;
9044 window->DrawList->AddRectFilled(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base);
9045 }
9046
9047 t0 = t1;
9048 tp0 = tp1;
9049 }
9050 }
9051
9052 // Text overlay
9053 if (overlay_text)
9054 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));
9055
9056 if (label_size.x > 0.0f)
9057 RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), label, label_end, false);
9058
9059 // Return hovered index or -1 if none are hovered.
9060 // 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().
9061 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
9062 return idx_hovered;
9063}
9064
9065struct ImGuiPlotArrayGetterData
9066{
9067 const float* Values;
9068 int Stride;
9069
9070 ImGuiPlotArrayGetterData(const float* values, int stride) { Values = values; Stride = stride; }
9071};
9072
9073static float Plot_ArrayGetter(void* data, int idx)
9074{
9075 ImGuiPlotArrayGetterData* plot_data = (ImGuiPlotArrayGetterData*)data;
9076 const float v = *(const float*)(const void*)((const unsigned char*)plot_data->Values + (size_t)idx * plot_data->Stride);
9077 return v;
9078}
9079
9080void 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)
9081{
9082 ImGuiPlotArrayGetterData data(values, stride);
9083 PlotEx(ImGuiPlotType_Lines, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
9084}
9085
9086void 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)
9087{
9088 PlotEx(ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
9089}
9090
9091// Plot Histogram (the data provided _is_ histogram data. it doesn't compute the histogram of your data)
9092void 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)
9093{
9094 ImGuiPlotArrayGetterData data(values, stride);
9095 PlotEx(ImGuiPlotType_Histogram, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
9096}
9097
9098void 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)
9099{
9100 PlotEx(ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
9101}
9102
9103//-------------------------------------------------------------------------
9104// [SECTION] Widgets: Value helpers
9105// Those is not very useful, legacy API.
9106//-------------------------------------------------------------------------
9107// - Value()
9108//-------------------------------------------------------------------------
9109
9110void ImGui::Value(const char* prefix, bool b)
9111{
9112 Text("%s: %s", prefix, (b ? "true" : "false"));
9113}
9114
9115void ImGui::Value(const char* prefix, int v)
9116{
9117 Text("%s: %d", prefix, v);
9118}
9119
9120void ImGui::Value(const char* prefix, unsigned int v)
9121{
9122 Text("%s: %d", prefix, v);
9123}
9124
9125void ImGui::Value(const char* prefix, float v, const char* float_format)
9126{
9127 if (float_format)
9128 {
9129 char fmt[64];
9130 ImFormatString(fmt, IM_COUNTOF(fmt), "%%s: %s", float_format);
9131 Text(fmt, prefix, v);
9132 }
9133 else
9134 {
9135 Text("%s: %.3f", prefix, v);
9136 }
9137}
9138
9139//-------------------------------------------------------------------------
9140// [SECTION] MenuItem, BeginMenu, EndMenu, etc.
9141//-------------------------------------------------------------------------
9142// - ImGuiMenuColumns [Internal]
9143// - BeginMenuBar()
9144// - EndMenuBar()
9145// - BeginMainMenuBar()
9146// - EndMainMenuBar()
9147// - BeginMenu()
9148// - EndMenu()
9149// - MenuItemEx() [Internal]
9150// - MenuItem()
9151//-------------------------------------------------------------------------
9152
9153// Helpers for internal use
9154void ImGuiMenuColumns::Update(float spacing, bool window_reappearing)
9155{
9156 if (window_reappearing)
9157 memset(Widths, 0, sizeof(Widths));
9158 Spacing = (ImU16)spacing;
9159 CalcNextTotalWidth(true);
9160 memset(Widths, 0, sizeof(Widths));
9161 TotalWidth = NextTotalWidth;
9162 NextTotalWidth = 0;
9163}
9164
9165void ImGuiMenuColumns::CalcNextTotalWidth(bool update_offsets)
9166{
9167 ImU16 offset = 0;
9168 bool want_spacing = false;
9169 for (int i = 0; i < IM_COUNTOF(Widths); i++)
9170 {
9171 ImU16 width = Widths[i];
9172 if (want_spacing && width > 0)
9173 offset += Spacing;
9174 want_spacing |= (width > 0);
9175 if (update_offsets)
9176 {
9177 if (i == 1) { OffsetLabel = offset; }
9178 if (i == 2) { OffsetShortcut = offset; }
9179 if (i == 3) { OffsetMark = offset; }
9180 }
9181 offset += width;
9182 }
9183 NextTotalWidth = offset;
9184}
9185
9186float ImGuiMenuColumns::DeclColumns(float w_icon, float w_label, float w_shortcut, float w_mark)
9187{
9188 Widths[0] = ImMax(Widths[0], (ImU16)w_icon);
9189 Widths[1] = ImMax(Widths[1], (ImU16)w_label);
9190 Widths[2] = ImMax(Widths[2], (ImU16)w_shortcut);
9191 Widths[3] = ImMax(Widths[3], (ImU16)w_mark);
9192 CalcNextTotalWidth(false);
9193 return (float)ImMax(TotalWidth, NextTotalWidth);
9194}
9195
9196// FIXME: Provided a rectangle perhaps e.g. a BeginMenuBarEx() could be used anywhere..
9197// Currently the main responsibility of this function being to setup clip-rect + horizontal layout + menu navigation layer.
9198// 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.
9199// Then later the same system could be used for multiple menu-bars, scrollbars, side-bars.
9200bool ImGui::BeginMenuBar()
9201{
9202 ImGuiWindow* window = GetCurrentWindow();
9203 if (window->SkipItems)
9204 return false;
9205 if (!(window->Flags & ImGuiWindowFlags_MenuBar))
9206 return false;
9207
9208 IM_ASSERT(!window->DC.MenuBarAppending);
9209 BeginGroup(); // Backup position on layer 0 // FIXME: Misleading to use a group for that backup/restore
9210 PushID("##MenuBar");
9211
9212 // 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.
9213 // 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.
9214 const float border_top = ImMax(window->WindowBorderSize * 0.5f - window->TitleBarHeight, 0.0f);
9215 ImRect bar_rect = window->MenuBarRect();
9216 ImRect clip_rect(IM_ROUND(bar_rect.Min.x + window->WindowBorderSize * 0.5f), IM_ROUND(bar_rect.Min.y + border_top), IM_ROUND(ImMax(bar_rect.Min.x, bar_rect.Max.x - ImMax(window->WindowRounding, window->WindowBorderSize * 0.5f))), IM_ROUND(bar_rect.Max.y));
9217 clip_rect.ClipWith(window->OuterRectClipped);
9218 PushClipRect(clip_rect.Min, clip_rect.Max, false);
9219
9220 // 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).
9221 window->DC.CursorPos = window->DC.CursorMaxPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffset.x, bar_rect.Min.y + window->DC.MenuBarOffset.y);
9222 window->DC.LayoutType = ImGuiLayoutType_Horizontal;
9223 window->DC.IsSameLine = false;
9224 window->DC.NavLayerCurrent = ImGuiNavLayer_Menu;
9225 window->DC.MenuBarAppending = true;
9226 AlignTextToFramePadding();
9227 return true;
9228}
9229
9230void ImGui::EndMenuBar()
9231{
9232 ImGuiWindow* window = GetCurrentWindow();
9233 if (window->SkipItems)
9234 return;
9235 ImGuiContext& g = *GImGui;
9236
9237 IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'"
9238 IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar);
9239 IM_ASSERT(window->DC.MenuBarAppending);
9240
9241 // Nav: When a move request within one of our child menu failed, capture the request to navigate among our siblings.
9242 if (NavMoveRequestButNoResultYet() && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) && (g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu))
9243 {
9244 // Try to find out if the request is for one of our child menu
9245 ImGuiWindow* nav_earliest_child = g.NavWindow;
9246 while (nav_earliest_child->ParentWindow && (nav_earliest_child->ParentWindow->Flags & ImGuiWindowFlags_ChildMenu))
9247 nav_earliest_child = nav_earliest_child->ParentWindow;
9248 if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0)
9249 {
9250 // To do so we claim focus back, restore NavId and then process the movement request for yet another frame.
9251 // 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)
9252 const ImGuiNavLayer layer = ImGuiNavLayer_Menu;
9253 IM_ASSERT(window->DC.NavLayersActiveMaskNext & (1 << layer)); // Sanity check (FIXME: Seems unnecessary)
9254 FocusWindow(window);
9255 SetNavID(window->NavLastIds[layer], layer, 0, window->NavRectRel[layer]);
9256 // FIXME-NAV: How to deal with this when not using g.IO.ConfigNavCursorVisibleAuto?
9257 if (g.NavCursorVisible)
9258 {
9259 g.NavCursorVisible = false; // Hide nav cursor for the current frame so we don't see the intermediary selection. Will be set again
9260 g.NavCursorHideFrames = 2;
9261 }
9262 g.NavHighlightItemUnderNav = g.NavMousePosDirty = true;
9263 NavMoveRequestForward(g.NavMoveDir, g.NavMoveClipDir, g.NavMoveFlags, g.NavMoveScrollFlags); // Repeat
9264 }
9265 }
9266 else
9267 {
9268 NavMoveRequestTryWrapping(window, ImGuiNavMoveFlags_WrapX);
9269 }
9270
9271 PopClipRect();
9272 PopID();
9273 IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'"
9274 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.
9275
9276 // FIXME: Extremely confusing, cleanup by (a) working on WorkRect stack system (b) not using a Group confusingly here.
9277 ImGuiGroupData& group_data = g.GroupStack.back();
9278 group_data.EmitItem = false;
9279 ImVec2 restore_cursor_max_pos = group_data.BackupCursorMaxPos;
9280 window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, window->DC.CursorMaxPos.x - window->Scroll.x); // Convert ideal extents for scrolling layer equivalent.
9281 EndGroup(); // Restore position on layer 0 // FIXME: Misleading to use a group for that backup/restore
9282 window->DC.LayoutType = ImGuiLayoutType_Vertical;
9283 window->DC.IsSameLine = false;
9284 window->DC.NavLayerCurrent = ImGuiNavLayer_Main;
9285 window->DC.MenuBarAppending = false;
9286 window->DC.CursorMaxPos = restore_cursor_max_pos;
9287}
9288
9289// Important: calling order matters!
9290// FIXME: Somehow overlapping with docking tech.
9291// FIXME: The "rect-cut" aspect of this could be formalized into a lower-level helper (rect-cut: https://halt.software/dead-simple-layouts)
9292bool ImGui::BeginViewportSideBar(const char* name, ImGuiViewport* viewport_p, ImGuiDir dir, float axis_size, ImGuiWindowFlags window_flags)
9293{
9294 IM_ASSERT(dir != ImGuiDir_None);
9295
9296 ImGuiWindow* bar_window = FindWindowByName(name);
9297 if (bar_window == NULL || bar_window->BeginCount == 0)
9298 {
9299 // Calculate and set window size/position
9300 ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)(viewport_p ? viewport_p : GetMainViewport());
9301 ImRect avail_rect = viewport->GetBuildWorkRect();
9302 ImGuiAxis axis = (dir == ImGuiDir_Up || dir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X;
9303 ImVec2 pos = avail_rect.Min;
9304 if (dir == ImGuiDir_Right || dir == ImGuiDir_Down)
9305 pos[axis] = avail_rect.Max[axis] - axis_size;
9306 ImVec2 size = avail_rect.GetSize();
9307 size[axis] = axis_size;
9308 SetNextWindowPos(pos);
9309 SetNextWindowSize(size);
9310
9311 // Report our size into work area (for next frame) using actual window size
9312 if (dir == ImGuiDir_Up || dir == ImGuiDir_Left)
9313 viewport->BuildWorkInsetMin[axis] += axis_size;
9314 else if (dir == ImGuiDir_Down || dir == ImGuiDir_Right)
9315 viewport->BuildWorkInsetMax[axis] += axis_size;
9316 }
9317
9318 window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
9319 PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
9320 PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0)); // Lift normal size constraint
9321 bool is_open = Begin(name, NULL, window_flags);
9322 PopStyleVar(2);
9323
9324 return is_open;
9325}
9326
9327bool ImGui::BeginMainMenuBar()
9328{
9329 ImGuiContext& g = *GImGui;
9330 ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport();
9331
9332 // For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set.
9333 // FIXME: This could be generalized as an opt-in way to clamp window->DC.CursorStartPos to avoid SafeArea?
9334 // FIXME: Consider removing support for safe area down the line... it's messy. Nowadays consoles have support for TV calibration in OS settings.
9335 g.NextWindowData.MenuBarOffsetMinVal = ImVec2(g.Style.DisplaySafeAreaPadding.x, ImMax(g.Style.DisplaySafeAreaPadding.y - g.Style.FramePadding.y, 0.0f));
9336 ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar;
9337 float height = GetFrameHeight();
9338 bool is_open = BeginViewportSideBar("##MainMenuBar", viewport, ImGuiDir_Up, height, window_flags);
9339 g.NextWindowData.MenuBarOffsetMinVal = ImVec2(0.0f, 0.0f);
9340 if (!is_open)
9341 {
9342 End();
9343 return false;
9344 }
9345
9346 // Temporarily disable _NoSavedSettings, in the off-chance that tables or child windows submitted within the menu-bar may want to use settings. (#8356)
9347 g.CurrentWindow->Flags &= ~ImGuiWindowFlags_NoSavedSettings;
9348 BeginMenuBar();
9349 return is_open;
9350}
9351
9352void ImGui::EndMainMenuBar()
9353{
9354 ImGuiContext& g = *GImGui;
9355 IM_ASSERT_USER_ERROR_RET(g.CurrentWindow->DC.MenuBarAppending, "Calling EndMainMenuBar() not from a menu-bar!"); // Not technically testing that it is the main menu bar
9356
9357 EndMenuBar();
9358 g.CurrentWindow->Flags |= ImGuiWindowFlags_NoSavedSettings; // Restore _NoSavedSettings (#8356)
9359
9360 // When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window
9361 // FIXME: With this strategy we won't be able to restore a NULL focus.
9362 if (g.CurrentWindow == g.NavWindow && g.NavLayer == ImGuiNavLayer_Main && !g.NavAnyRequest && g.ActiveId == 0)
9363 FocusTopMostWindowUnderOne(g.NavWindow, NULL, NULL, ImGuiFocusRequestFlags_UnlessBelowModal | ImGuiFocusRequestFlags_RestoreFocusedChild);
9364
9365 End();
9366}
9367
9368static bool IsRootOfOpenMenuSet()
9369{
9370 ImGuiContext& g = *GImGui;
9371 ImGuiWindow* window = g.CurrentWindow;
9372 if ((g.OpenPopupStack.Size <= g.BeginPopupStack.Size) || (window->Flags & ImGuiWindowFlags_ChildMenu))
9373 return false;
9374
9375 // Initially we used 'upper_popup->OpenParentId == window->IDStack.back()' to differentiate multiple menu sets from each others
9376 // (e.g. inside menu bar vs loose menu items) based on parent ID.
9377 // This would however prevent the use of e.g. PushID() user code submitting menus.
9378 // Previously this worked between popup and a first child menu because the first child menu always had the _ChildWindow flag,
9379 // making hovering on parent popup possible while first child menu was focused - but this was generally a bug with other side effects.
9380 // Instead we don't treat Popup specifically (in order to consistently support menu features in them), maybe the first child menu of a Popup
9381 // doesn't have the _ChildWindow flag, and we rely on this IsRootOfOpenMenuSet() check to allow hovering between root window/popup and first child menu.
9382 // 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.
9383 // 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
9384 // 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
9385 // it likely won't be a problem anyone runs into.
9386 const ImGuiPopupData* upper_popup = &g.OpenPopupStack[g.BeginPopupStack.Size];
9387 if (window->DC.NavLayerCurrent != upper_popup->ParentNavLayer)
9388 return false;
9389 return upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu) && ImGui::IsWindowChildOf(upper_popup->Window, window, true);
9390}
9391
9392bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
9393{
9394 ImGuiWindow* window = GetCurrentWindow();
9395 if (window->SkipItems)
9396 return false;
9397
9398 ImGuiContext& g = *GImGui;
9399 ImGuiStyle& style = g.Style;
9400 const ImGuiID id = window->GetID(label);
9401 bool menu_is_open = IsPopupOpen(id, ImGuiPopupFlags_None);
9402
9403 // 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)
9404 // 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.
9405 ImGuiWindowFlags window_flags = ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus;
9406 if (window->Flags & ImGuiWindowFlags_ChildMenu)
9407 window_flags |= ImGuiWindowFlags_ChildWindow;
9408
9409 // If a menu with same the ID was already submitted, we will append to it, matching the behavior of Begin().
9410 // 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.
9411 // If somehow this is ever becoming a problem we can switch to use e.g. ImGuiStorage mapping key to last frame used.
9412 if (g.MenusIdSubmittedThisFrame.contains(id))
9413 {
9414 if (menu_is_open)
9415 menu_is_open = BeginPopupMenuEx(id, label, window_flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display)
9416 else
9417 g.NextWindowData.ClearFlags(); // we behave like Begin() and need to consume those values
9418 return menu_is_open;
9419 }
9420
9421 // Tag menu as used. Next time BeginMenu() with same ID is called it will append to existing menu
9422 g.MenusIdSubmittedThisFrame.push_back(id);
9423
9424 const char* label_end = FindRenderedTextEnd(label);
9425 ImVec2 label_size = CalcTextSize(label, label_end, false);
9426
9427 // 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)
9428 // This is only done for items for the menu set and not the full parent window.
9429 const bool menuset_is_open = IsRootOfOpenMenuSet();
9430 if (menuset_is_open)
9431 g.NextItemData.ItemFlagsSet |= ImGuiItemFlags_NoWindowHoverableCheck;
9432
9433 // The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu,
9434 // However the final position is going to be different! It is chosen by FindBestWindowPosForPopup().
9435 // e.g. Menus tend to overlap each other horizontally to amplify relative Z-ordering.
9436 ImVec2 popup_pos;
9437 ImVec2 pos = window->DC.CursorPos;
9438 PushID(label);
9439 if (!enabled)
9440 BeginDisabled();
9441
9442 bool pressed;
9443
9444 const float backup_rounding = style.SelectableRounding;
9445 style.SelectableRounding = style.MenuItemRounding;
9446 const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoAutoClosePopups | (ImGuiSelectableFlags)ImGuiSelectableFlags_SelectOnClick;
9447 ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
9448 if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
9449 {
9450 // Menu inside a horizontal menu bar
9451 // Selectable extend their highlight by half ItemSpacing in each direction.
9452 // For ChildMenu, the popup position will be overwritten by the call to FindBestWindowPosForPopup() in Begin()
9453 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f);
9454 PushStyleVarX(ImGuiStyleVar_ItemSpacing, style.ItemSpacing.x * 2.0f);
9455 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, pos.y + window->DC.CurrLineTextBaseOffset);
9456 pressed = Selectable("", menu_is_open, selectable_flags, label_size);
9457 LogSetNextTextDecoration("[", "]");
9458 RenderText(text_pos, label, label_end, false);
9459 PopStyleVar();
9460 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().
9461 popup_pos = ImVec2(pos.x - 1.0f - IM_TRUNC(style.ItemSpacing.x * 0.5f), text_pos.y - style.FramePadding.y + window->MenuBarHeight);
9462 }
9463 else
9464 {
9465 // Menu inside a regular/vertical menu
9466 // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
9467 // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.)
9468 float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f;
9469 float checkmark_w = IM_TRUNC(g.FontSize * 1.20f);
9470 float min_w = offsets->DeclColumns(icon_w, label_size.x, 0.0f, checkmark_w); // Feedback to next frame
9471 float extra_w = ImMax(0.0f, GetContentRegionAvail().x - min_w);
9472 ImVec2 text_pos(window->DC.CursorPos.x, pos.y + window->DC.CurrLineTextBaseOffset);
9473 pressed = Selectable("", menu_is_open, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y));
9474 LogSetNextTextDecoration("", ">");
9475 RenderText(ImVec2(text_pos.x + offsets->OffsetLabel, text_pos.y), label, label_end, false);
9476 if (icon_w > 0.0f)
9477 RenderText(ImVec2(text_pos.x + offsets->OffsetIcon, text_pos.y), icon);
9478 RenderArrow(window->DrawList, ImVec2(text_pos.x + offsets->OffsetMark + extra_w + g.FontSize * 0.30f, text_pos.y), GetColorU32(ImGuiCol_Text), ImGuiDir_Right);
9479 popup_pos = ImVec2(pos.x, text_pos.y - style.WindowPadding.y);
9480 }
9481 style.SelectableRounding = backup_rounding;
9482 if (!enabled)
9483 EndDisabled();
9484
9485 // Once dragged, release ActiveId + key ownership. This is to allow the idiom of mouse down a menu, dragging elsewhere, up on some other MenuItem(). (#8233, #9394)
9486 // Could move logic into lower-level ImGuiButtonFlags_AutoReleaseActiveId + ImGuiButtonFlags_AutoReleaseKeyOwner? Easier once we get rid of the Selectable() middle-man here.
9487 if (g.ActiveId == id && g.HoveredId != id && g.ActiveIdSource == ImGuiInputSource_Mouse && IsMouseDragging(0))
9488 {
9489 ClearActiveID();
9490 SetKeyOwner(ImGuiKey_MouseLeft, ImGuiKeyOwner_NoOwner);
9491 }
9492
9493 const bool hovered = (g.HoveredId == id) && enabled && !g.NavHighlightItemUnderNav;
9494
9495 bool want_open = false;
9496 bool want_open_nav_init = false;
9497 bool want_close = false;
9498 if (window->DC.LayoutType == ImGuiLayoutType_Vertical) // (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu))
9499 {
9500 // Close menu when not hovering it anymore unless we are moving roughly in the direction of the menu
9501 // Implement http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown to avoid using timers, so menus feels more reactive.
9502 bool moving_toward_child_menu = false;
9503 ImGuiPopupData* child_popup = (g.BeginPopupStack.Size < g.OpenPopupStack.Size) ? &g.OpenPopupStack[g.BeginPopupStack.Size] : NULL; // Popup candidate (testing below)
9504 ImGuiWindow* child_menu_window = (child_popup && child_popup->Window && child_popup->Window->ParentWindow == window) ? child_popup->Window : NULL;
9505 if (g.HoveredWindow == window && child_menu_window != NULL)
9506 {
9507 const float ref_unit = g.FontSize; // FIXME-DPI
9508 const float child_dir = (window->Pos.x < child_menu_window->Pos.x) ? 1.0f : -1.0f;
9509 const ImRect next_window_rect = child_menu_window->Rect();
9510 ImVec2 ta = (g.IO.MousePos - g.IO.MouseDelta);
9511 ImVec2 tb = (child_dir > 0.0f) ? next_window_rect.GetTL() : next_window_rect.GetTR();
9512 ImVec2 tc = (child_dir > 0.0f) ? next_window_rect.GetBL() : next_window_rect.GetBR();
9513 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.
9514 ta.x += child_dir * -0.5f;
9515 tb.x += child_dir * ref_unit;
9516 tc.x += child_dir * ref_unit;
9517 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
9518 tc.y = ta.y + ImMin((tc.y + pad_farmost_h) - ta.y, +ref_unit * 8.0f);
9519 moving_toward_child_menu = ImTriangleContainsPoint(ta, tb, tc, g.IO.MousePos);
9520 //GetForegroundDrawList()->AddTriangleFilled(ta, tb, tc, moving_toward_child_menu ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); // [DEBUG]
9521 }
9522
9523 // 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)
9524 // 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.
9525 // (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.)
9526 if (menu_is_open && !hovered && g.HoveredWindow == window && !moving_toward_child_menu && !g.NavHighlightItemUnderNav && g.ActiveId == 0)
9527 want_close = true;
9528
9529 // Open
9530 // (note: at this point 'hovered' actually includes the NavDisableMouseHover == false test)
9531 if (!menu_is_open && pressed) // Click/activate to open
9532 want_open = true;
9533 else if (!menu_is_open && hovered && !moving_toward_child_menu) // Hover to open
9534 want_open = true;
9535 else if (!menu_is_open && hovered && g.HoveredIdTimer >= 0.30f && g.MouseStationaryTimer >= 0.30f) // Hover to open (timer fallback)
9536 want_open = true;
9537 if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open
9538 {
9539 want_open = want_open_nav_init = true;
9540 NavMoveRequestCancel();
9541 SetNavCursorVisibleAfterMove();
9542 }
9543 }
9544 else
9545 {
9546 // Menu bar
9547 if (menu_is_open && pressed && menuset_is_open) // Click an open menu again to close it
9548 {
9549 want_close = true;
9550 want_open = menu_is_open = false;
9551 }
9552 else if (pressed || (hovered && menuset_is_open && !menu_is_open)) // First click to open, then hover to open others
9553 {
9554 want_open = true;
9555 }
9556 else if (g.NavId == id && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open
9557 {
9558 want_open = true;
9559 NavMoveRequestCancel();
9560 }
9561 }
9562
9563 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.. }'
9564 want_close = true;
9565 if (want_close && IsPopupOpen(id, ImGuiPopupFlags_None))
9566 ClosePopupToLevel(g.BeginPopupStack.Size, true);
9567
9568 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Openable | (menu_is_open ? ImGuiItemStatusFlags_Opened : 0));
9569 PopID();
9570
9571 if (g.ActiveId == id && want_open)
9572 g.ActiveIdNoClearOnFocusLoss = true;
9573
9574 if (want_open && !menu_is_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size)
9575 {
9576 // 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.
9577 OpenPopup(label);
9578 }
9579 else if (want_open)
9580 {
9581 menu_is_open = true;
9582 OpenPopup(label, ImGuiPopupFlags_NoReopen);// | (want_open_nav_init ? ImGuiPopupFlags_NoReopenAlwaysNavInit : 0));
9583 }
9584
9585 if (menu_is_open)
9586 {
9587 ImGuiLastItemData last_item_in_parent = g.LastItemData;
9588 SetNextWindowPos(popup_pos, ImGuiCond_Always); // Note: misleading: the value will serve as reference for FindBestWindowPosForPopup(), not actual pos.
9589 PushStyleVar(ImGuiStyleVar_ChildRounding, style.PopupRounding); // First level will use _PopupRounding, subsequent will use _ChildRounding
9590 menu_is_open = BeginPopupMenuEx(id, label, window_flags); // menu_is_open may be 'false' when the popup is completely clipped (e.g. zero size display)
9591 PopStyleVar();
9592 if (menu_is_open)
9593 {
9594 // Implement what ImGuiPopupFlags_NoReopenAlwaysNavInit would do:
9595 // Perform an init request in the case the popup was already open (via a previous mouse hover)
9596 if (want_open && want_open_nav_init && !g.NavInitRequest)
9597 {
9598 FocusWindow(g.CurrentWindow, ImGuiFocusRequestFlags_UnlessBelowModal);
9599 NavInitWindow(g.CurrentWindow, false);
9600 }
9601
9602 // Restore LastItemData so IsItemXXXX functions can work after BeginMenu()/EndMenu()
9603 // (This fixes using IsItemClicked() and IsItemHovered(), but IsItemHovered() also relies on its support for ImGuiItemFlags_NoWindowHoverableCheck)
9604 g.LastItemData = last_item_in_parent;
9605 if (g.HoveredWindow == window)
9606 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow;
9607 }
9608 }
9609 else
9610 {
9611 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
9612 }
9613
9614 return menu_is_open;
9615}
9616
9617bool ImGui::BeginMenu(const char* label, bool enabled)
9618{
9619 return BeginMenuEx(label, NULL, enabled);
9620}
9621
9622void ImGui::EndMenu()
9623{
9624 // Nav: When a left move request our menu failed, close ourselves.
9625 ImGuiContext& g = *GImGui;
9626 ImGuiWindow* window = g.CurrentWindow;
9627 IM_ASSERT_USER_ERROR_RET((window->Flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_ChildMenu)) == (ImGuiWindowFlags_Popup | ImGuiWindowFlags_ChildMenu), "Calling EndMenu() in wrong window!");
9628
9629 ImGuiWindow* parent_window = window->ParentWindow; // Should always be != NULL is we passed assert.
9630 if (window->BeginCount == window->BeginCountPreviousFrame)
9631 if (g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet())
9632 if (g.NavWindow && (g.NavWindow->RootWindowForNav == window) && parent_window->DC.LayoutType == ImGuiLayoutType_Vertical)
9633 {
9634 ClosePopupToLevel(g.BeginPopupStack.Size - 1, true);
9635 NavMoveRequestCancel();
9636 }
9637
9638 EndPopup();
9639}
9640
9641bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut, bool selected, bool enabled)
9642{
9643 ImGuiWindow* window = GetCurrentWindow();
9644 if (window->SkipItems)
9645 return false;
9646
9647 ImGuiContext& g = *GImGui;
9648 ImGuiStyle& style = g.Style;
9649 ImVec2 pos = window->DC.CursorPos;
9650 const char* label_end = FindRenderedTextEnd(label);
9651 ImVec2 label_size = CalcTextSize(label, label_end, false);
9652
9653 // See BeginMenuEx() for comments about this.
9654 const bool menuset_is_open = IsRootOfOpenMenuSet();
9655 if (menuset_is_open)
9656 g.NextItemData.ItemFlagsSet |= ImGuiItemFlags_NoWindowHoverableCheck;
9657
9658 // We've been using the equivalent of ImGuiSelectableFlags_SetNavIdOnHover on all Selectable() since early Nav system days (commit 43ee5d73),
9659 // 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.
9660 bool pressed;
9661 PushID(label);
9662 if (!enabled)
9663 BeginDisabled();
9664
9665 // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
9666 const float backup_rounding = style.SelectableRounding;
9667 style.SelectableRounding = style.MenuItemRounding;
9668 const ImGuiSelectableFlags selectable_flags = (ImGuiSelectableFlags)ImGuiSelectableFlags_SelectOnRelease | (ImGuiSelectableFlags)ImGuiSelectableFlags_SetNavIdOnHover;
9669 ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
9670 if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
9671 {
9672 // Mimic the exact layout spacing of BeginMenu() to allow MenuItem() inside a menu bar, which is a little misleading but may be useful
9673 // Note that in this situation: we don't render the shortcut, we render a highlight instead of the selected tick mark.
9674 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f);
9675 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
9676 PushStyleVarX(ImGuiStyleVar_ItemSpacing, style.ItemSpacing.x * 2.0f);
9677 pressed = Selectable("", selected, selectable_flags, ImVec2(label_size.x, 0.0f));
9678 PopStyleVar();
9679 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
9680 RenderText(text_pos, label, label_end, false);
9681 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().
9682 }
9683 else
9684 {
9685 // Menu item inside a vertical menu
9686 // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
9687 // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.)
9688 float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f;
9689 float shortcut_w = (shortcut && shortcut[0]) ? CalcTextSize(shortcut, NULL).x : 0.0f;
9690 float checkmark_w = IM_TRUNC(g.FontSize * 1.20f); // FIXME: Always accounted for, even in a menu with none, because 'selected' has no neutral setting.
9691 float min_w = offsets->DeclColumns(icon_w, label_size.x, shortcut_w, checkmark_w); // Feedback for next frame
9692 float stretch_w = ImMax(0.0f, GetContentRegionAvail().x - min_w);
9693 ImVec2 text_pos(pos.x, pos.y + window->DC.CurrLineTextBaseOffset);
9694 pressed = Selectable("", false, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y));
9695 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
9696 {
9697 RenderText(text_pos + ImVec2(offsets->OffsetLabel, 0.0f), label, label_end, false);
9698 if (icon_w > 0.0f)
9699 RenderText(text_pos + ImVec2(offsets->OffsetIcon, 0.0f), icon);
9700 if (shortcut_w > 0.0f)
9701 {
9702 PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]);
9703 LogSetNextTextDecoration("(", ")");
9704 RenderText(text_pos + ImVec2(offsets->OffsetShortcut + stretch_w, 0.0f), shortcut, NULL, false);
9705 PopStyleColor();
9706 }
9707 if (selected)
9708 RenderCheckMark(window->DrawList, text_pos + ImVec2(offsets->OffsetMark + stretch_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), GetColorU32(ImGuiCol_Text), g.FontSize * 0.866f);
9709 }
9710 }
9711 style.SelectableRounding = backup_rounding;
9712
9713 // Once dragged, release ActiveId + key ownership. This is to allow the idiom of mouse down a menu, dragging elsewhere, up on some other MenuItem(). (#8233, #9394)
9714 // Could move logic into lower-level ImGuiButtonFlags_AutoReleaseActiveId + ImGuiButtonFlags_AutoReleaseKeyOwner? Easier once we get rid of the Selectable() middle-man here.
9715 const ImGuiID id = g.LastItemData.ID;
9716 if (g.ActiveId == id && g.HoveredId != id && g.ActiveIdSource == ImGuiInputSource_Mouse && IsMouseDragging(0))
9717 {
9718 ClearActiveID();
9719 SetKeyOwner(ImGuiKey_MouseLeft, ImGuiKeyOwner_NoOwner);
9720 }
9721
9722
9723 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0));
9724 if (!enabled)
9725 EndDisabled();
9726 PopID();
9727
9728 return pressed;
9729}
9730
9731bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled)
9732{
9733 return MenuItemEx(label, NULL, shortcut, selected, enabled);
9734}
9735
9736bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled)
9737{
9738 if (MenuItemEx(label, NULL, shortcut, p_selected ? *p_selected : false, enabled))
9739 {
9740 if (p_selected)
9741 *p_selected = !*p_selected;
9742 return true;
9743 }
9744 return false;
9745}
9746
9747//-------------------------------------------------------------------------
9748// [SECTION] Widgets: BeginTabBar, EndTabBar, etc.
9749//-------------------------------------------------------------------------
9750// - BeginTabBar()
9751// - BeginTabBarEx() [Internal]
9752// - EndTabBar()
9753// - TabBarLayout() [Internal]
9754// - TabBarCalcTabID() [Internal]
9755// - TabBarCalcMaxTabWidth() [Internal]
9756// - TabBarFindTabById() [Internal]
9757// - TabBarFindTabByOrder() [Internal]
9758// - TabBarGetCurrentTab() [Internal]
9759// - TabBarGetTabName() [Internal]
9760// - TabBarRemoveTab() [Internal]
9761// - TabBarCloseTab() [Internal]
9762// - TabBarScrollClamp() [Internal]
9763// - TabBarScrollToTab() [Internal]
9764// - TabBarQueueFocus() [Internal]
9765// - TabBarQueueReorder() [Internal]
9766// - TabBarProcessReorderFromMousePos() [Internal]
9767// - TabBarProcessReorder() [Internal]
9768// - TabBarScrollingButtons() [Internal]
9769// - TabBarTabListPopupButton() [Internal]
9770//-------------------------------------------------------------------------
9771
9772struct ImGuiTabBarSection
9773{
9774 int TabCount; // Number of tabs in this section.
9775 float Width; // Sum of width of tabs in this section (after shrinking down)
9776 float WidthAfterShrinkMinWidth;
9777 float Spacing; // Horizontal spacing at the end of the section.
9778
9779 ImGuiTabBarSection() { memset((void*)this, 0, sizeof(*this)); }
9780};
9781
9782namespace ImGui
9783{
9784 static void TabBarLayout(ImGuiTabBar* tab_bar);
9785 static ImU32 TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window);
9786 static float TabBarCalcMaxTabWidth();
9787 static float TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling);
9788 static void TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections);
9789 static ImGuiTabItem* TabBarScrollingButtons(ImGuiTabBar* tab_bar);
9790 static ImGuiTabItem* TabBarTabListPopupButton(ImGuiTabBar* tab_bar);
9791}
9792
9793ImGuiTabBar::ImGuiTabBar()
9794{
9795 memset((void*)this, 0, sizeof(*this));
9796 CurrFrameVisible = PrevFrameVisible = -1;
9797 LastTabItemIdx = -1;
9798}
9799
9800static inline int TabItemGetSectionIdx(const ImGuiTabItem* tab)
9801{
9802 return (tab->Flags & ImGuiTabItemFlags_Leading) ? 0 : (tab->Flags & ImGuiTabItemFlags_Trailing) ? 2 : 1;
9803}
9804
9805static int IMGUI_CDECL TabItemComparerBySection(const void* lhs, const void* rhs)
9806{
9807 const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
9808 const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
9809 const int a_section = TabItemGetSectionIdx(a);
9810 const int b_section = TabItemGetSectionIdx(b);
9811 if (a_section != b_section)
9812 return a_section - b_section;
9813 return (int)(a->IndexDuringLayout - b->IndexDuringLayout);
9814}
9815
9816static int IMGUI_CDECL TabItemComparerByBeginOrder(const void* lhs, const void* rhs)
9817{
9818 const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
9819 const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
9820 return (int)(a->BeginOrder - b->BeginOrder);
9821}
9822
9823static ImGuiTabBar* GetTabBarFromTabBarRef(const ImGuiPtrOrIndex& ref)
9824{
9825 ImGuiContext& g = *GImGui;
9826 return ref.Ptr ? (ImGuiTabBar*)ref.Ptr : g.TabBars.GetByIndex(ref.Index);
9827}
9828
9829static ImGuiPtrOrIndex GetTabBarRefFromTabBar(ImGuiTabBar* tab_bar)
9830{
9831 ImGuiContext& g = *GImGui;
9832 if (g.TabBars.Contains(tab_bar))
9833 return ImGuiPtrOrIndex(g.TabBars.GetIndex(tab_bar));
9834 return ImGuiPtrOrIndex(tab_bar);
9835}
9836
9837ImGuiTabBar* ImGui::TabBarFindByID(ImGuiID id)
9838{
9839 ImGuiContext& g = *GImGui;
9840 return g.TabBars.GetByKey(id);
9841}
9842
9843// Remove TabBar data (currently only used by TestEngine)
9844void ImGui::TabBarRemove(ImGuiTabBar* tab_bar)
9845{
9846 ImGuiContext& g = *GImGui;
9847 g.TabBars.Remove(tab_bar->ID, tab_bar);
9848}
9849
9850bool ImGui::BeginTabBar(const char* str_id, ImGuiTabBarFlags flags)
9851{
9852 ImGuiContext& g = *GImGui;
9853 ImGuiWindow* window = g.CurrentWindow;
9854 if (window->SkipItems)
9855 return false;
9856
9857 ImGuiID id = window->GetID(str_id);
9858 ImGuiTabBar* tab_bar = g.TabBars.GetOrAddByKey(id);
9859 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);
9860 tab_bar->ID = id;
9861 tab_bar->SeparatorMinX = tab_bar_bb.Min.x - IM_TRUNC(window->WindowPadding.x * 0.5f);
9862 tab_bar->SeparatorMaxX = tab_bar_bb.Max.x + IM_TRUNC(window->WindowPadding.x * 0.5f);
9863 //if (g.NavWindow && IsWindowChildOf(g.NavWindow, window, false, false))
9864 flags |= ImGuiTabBarFlags_IsFocused;
9865 return BeginTabBarEx(tab_bar, tab_bar_bb, flags);
9866}
9867
9868bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImGuiTabBarFlags flags)
9869{
9870 ImGuiContext& g = *GImGui;
9871 ImGuiWindow* window = g.CurrentWindow;
9872 if (window->SkipItems)
9873 return false;
9874
9875 IM_ASSERT(tab_bar->ID != 0);
9876 if ((flags & ImGuiTabBarFlags_DockNode) == 0)
9877 PushOverrideID(tab_bar->ID);
9878
9879 // Add to stack
9880 g.CurrentTabBarStack.push_back(GetTabBarRefFromTabBar(tab_bar));
9881 g.CurrentTabBar = tab_bar;
9882 tab_bar->Window = window;
9883
9884 // Append with multiple BeginTabBar()/EndTabBar() pairs.
9885 tab_bar->BackupCursorPos = window->DC.CursorPos;
9886 if (tab_bar->CurrFrameVisible == g.FrameCount)
9887 {
9888 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
9889 tab_bar->BeginCount++;
9890 return true;
9891 }
9892
9893 // Ensure correct ordering when toggling ImGuiTabBarFlags_Reorderable flag, or when a new tab was added while being not reorderable
9894 if ((flags & ImGuiTabBarFlags_Reorderable) != (tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (tab_bar->TabsAddedNew && !(flags & ImGuiTabBarFlags_Reorderable)))
9895 ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerByBeginOrder);
9896 tab_bar->TabsAddedNew = false;
9897
9898 // Flags
9899 if ((flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0)
9900 flags |= ImGuiTabBarFlags_FittingPolicyDefault_;
9901
9902 tab_bar->Flags = flags;
9903 tab_bar->BarRect = tab_bar_bb;
9904 tab_bar->WantLayout = true; // Layout will be done on the first call to ItemTab()
9905 tab_bar->PrevFrameVisible = tab_bar->CurrFrameVisible;
9906 tab_bar->CurrFrameVisible = g.FrameCount;
9907 tab_bar->PrevTabsContentsHeight = tab_bar->CurrTabsContentsHeight;
9908 tab_bar->CurrTabsContentsHeight = 0.0f;
9909 tab_bar->ItemSpacingY = g.Style.ItemSpacing.y;
9910 tab_bar->FramePadding = g.Style.FramePadding;
9911 tab_bar->TabsActiveCount = 0;
9912 tab_bar->LastTabItemIdx = -1;
9913 tab_bar->BeginCount = 1;
9914
9915 // Set cursor pos in a way which only be used in the off-chance the user erroneously submits item before BeginTabItem(): items will overlap
9916 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
9917
9918 // Draw separator
9919 // (it would be misleading to draw this in EndTabBar() suggesting that it may be drawn over tabs, as tab bar are appendable)
9920 const ImU32 col = GetColorU32((flags & ImGuiTabBarFlags_IsFocused) ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected);
9921 if (g.Style.TabBarBorderSize > 0.0f)
9922 {
9923 const float y = tab_bar->BarRect.Max.y;
9924 window->DrawList->AddRectFilled(ImVec2(tab_bar->SeparatorMinX, y - g.Style.TabBarBorderSize), ImVec2(tab_bar->SeparatorMaxX, y), col);
9925 }
9926 return true;
9927}
9928
9929void ImGui::EndTabBar()
9930{
9931 ImGuiContext& g = *GImGui;
9932 ImGuiWindow* window = g.CurrentWindow;
9933 if (window->SkipItems)
9934 return;
9935
9936 ImGuiTabBar* tab_bar = g.CurrentTabBar;
9937 IM_ASSERT_USER_ERROR_RET(tab_bar != NULL, "Mismatched BeginTabBar()/EndTabBar()!");
9938
9939 // Fallback in case no TabItem have been submitted
9940 if (tab_bar->WantLayout)
9941 TabBarLayout(tab_bar);
9942
9943 // Restore the last visible height if no tab is visible, this reduce vertical flicker/movement when a tabs gets removed without calling SetTabItemClosed().
9944 const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
9945 if (tab_bar->VisibleTabWasSubmitted || tab_bar->VisibleTabId == 0 || tab_bar_appearing)
9946 {
9947 tab_bar->CurrTabsContentsHeight = ImMax(window->DC.CursorPos.y - tab_bar->BarRect.Max.y, tab_bar->CurrTabsContentsHeight);
9948 window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->CurrTabsContentsHeight;
9949 }
9950 else
9951 {
9952 window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->PrevTabsContentsHeight;
9953 }
9954 if (tab_bar->BeginCount > 1)
9955 window->DC.CursorPos = tab_bar->BackupCursorPos;
9956
9957 tab_bar->LastTabItemIdx = -1;
9958 if ((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0)
9959 PopID();
9960
9961 g.CurrentTabBarStack.pop_back();
9962 g.CurrentTabBar = g.CurrentTabBarStack.empty() ? NULL : GetTabBarFromTabBarRef(g.CurrentTabBarStack.back());
9963}
9964
9965// Scrolling happens only in the central section (leading/trailing sections are not scrolling)
9966static float TabBarCalcScrollableWidth(ImGuiTabBar* tab_bar, ImGuiTabBarSection* sections)
9967{
9968 return tab_bar->BarRect.GetWidth() - sections[0].Width - sections[2].Width - sections[1].Spacing;
9969}
9970
9971// This is called only once a frame before by the first call to ItemTab()
9972// The reason we're not calling it in BeginTabBar() is to leave a chance to the user to call the SetTabItemClosed() functions.
9973static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar)
9974{
9975 ImGuiContext& g = *GImGui;
9976 tab_bar->WantLayout = false;
9977
9978 // Track selected tab when resizing our parent down
9979 const bool scroll_to_selected_tab = (tab_bar->BarRectPrevWidth > tab_bar->BarRect.GetWidth());
9980 tab_bar->BarRectPrevWidth = tab_bar->BarRect.GetWidth();
9981
9982 // Garbage collect by compacting list
9983 // Detect if we need to sort out tab list (e.g. in rare case where a tab changed section)
9984 int tab_dst_n = 0;
9985 bool need_sort_by_section = false;
9986 ImGuiTabBarSection sections[3]; // Layout sections: Leading, Central, Trailing
9987 for (int tab_src_n = 0; tab_src_n < tab_bar->Tabs.Size; tab_src_n++)
9988 {
9989 ImGuiTabItem* tab = &tab_bar->Tabs[tab_src_n];
9990 if (tab->LastFrameVisible < tab_bar->PrevFrameVisible || tab->WantClose)
9991 {
9992 // Remove tab
9993 if (tab_bar->VisibleTabId == tab->ID) { tab_bar->VisibleTabId = 0; }
9994 if (tab_bar->SelectedTabId == tab->ID) { tab_bar->SelectedTabId = 0; }
9995 if (tab_bar->NextSelectedTabId == tab->ID) { tab_bar->NextSelectedTabId = 0; }
9996 continue;
9997 }
9998 if (tab_dst_n != tab_src_n)
9999 tab_bar->Tabs[tab_dst_n] = tab_bar->Tabs[tab_src_n];
10000
10001 tab = &tab_bar->Tabs[tab_dst_n];
10002 tab->IndexDuringLayout = (ImS16)tab_dst_n;
10003
10004 // We will need sorting if tabs have changed section (e.g. moved from one of Leading/Central/Trailing to another)
10005 int curr_tab_section_n = TabItemGetSectionIdx(tab);
10006 if (tab_dst_n > 0)
10007 {
10008 ImGuiTabItem* prev_tab = &tab_bar->Tabs[tab_dst_n - 1];
10009 int prev_tab_section_n = TabItemGetSectionIdx(prev_tab);
10010 if (curr_tab_section_n == 0 && prev_tab_section_n != 0)
10011 need_sort_by_section = true;
10012 if (prev_tab_section_n == 2 && curr_tab_section_n != 2)
10013 need_sort_by_section = true;
10014 }
10015
10016 sections[curr_tab_section_n].TabCount++;
10017 tab_dst_n++;
10018 }
10019 if (tab_bar->Tabs.Size != tab_dst_n)
10020 tab_bar->Tabs.resize(tab_dst_n);
10021
10022 if (need_sort_by_section)
10023 ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerBySection);
10024
10025 // Calculate spacing between sections
10026 const float tab_spacing = g.Style.ItemInnerSpacing.x;
10027 sections[0].Spacing = sections[0].TabCount > 0 && (sections[1].TabCount + sections[2].TabCount) > 0 ? tab_spacing : 0.0f;
10028 sections[1].Spacing = sections[1].TabCount > 0 && sections[2].TabCount > 0 ? tab_spacing : 0.0f;
10029
10030 // Setup next selected tab
10031 ImGuiID scroll_to_tab_id = 0;
10032 if (tab_bar->NextScrollToTabId)
10033 {
10034 scroll_to_tab_id = tab_bar->NextScrollToTabId;
10035 tab_bar->NextScrollToTabId = 0;
10036 }
10037 if (tab_bar->NextSelectedTabId)
10038 {
10039 tab_bar->SelectedTabId = tab_bar->NextSelectedTabId;
10040 tab_bar->NextSelectedTabId = 0;
10041 scroll_to_tab_id = tab_bar->SelectedTabId;
10042 }
10043
10044 // Process order change request (we could probably process it when requested but it's just saner to do it in a single spot).
10045 if (tab_bar->ReorderRequestTabId != 0)
10046 {
10047 if (TabBarProcessReorder(tab_bar))
10048 if (tab_bar->ReorderRequestTabId == tab_bar->SelectedTabId)
10049 scroll_to_tab_id = tab_bar->ReorderRequestTabId;
10050 tab_bar->ReorderRequestTabId = 0;
10051 }
10052
10053 // Tab List Popup (will alter tab_bar->BarRect and therefore the available width!)
10054 const bool tab_list_popup_button = (tab_bar->Flags & ImGuiTabBarFlags_TabListPopupButton) != 0;
10055 if (tab_list_popup_button)
10056 if (ImGuiTabItem* tab_to_select = TabBarTabListPopupButton(tab_bar)) // NB: Will alter BarRect.Min.x!
10057 scroll_to_tab_id = tab_bar->SelectedTabId = tab_to_select->ID;
10058
10059 // Leading/Trailing tabs will be shrink only if central one aren't visible anymore, so layout the shrink data as: leading, trailing, central
10060 // (whereas our tabs are stored as: leading, central, trailing)
10061 int shrink_buffer_indexes[3] = { 0, sections[0].TabCount + sections[2].TabCount, sections[0].TabCount };
10062 g.ShrinkWidthBuffer.resize(tab_bar->Tabs.Size);
10063
10064 // Minimum shrink width
10065 const float shrink_min_width = (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyMixed) ? g.Style.TabMinWidthShrink : 1.0f;
10066
10067 // Compute ideal tabs widths + store them into shrink buffer
10068 ImGuiTabItem* most_recently_selected_tab = NULL;
10069 int curr_section_n = -1;
10070 bool found_selected_tab_id = false;
10071 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
10072 {
10073 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
10074 IM_ASSERT(tab->LastFrameVisible >= tab_bar->PrevFrameVisible);
10075
10076 if ((most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) && !(tab->Flags & ImGuiTabItemFlags_Button))
10077 most_recently_selected_tab = tab;
10078 if (tab->ID == tab_bar->SelectedTabId)
10079 found_selected_tab_id = true;
10080 if (scroll_to_tab_id == 0 && g.NavJustMovedToId == tab->ID)
10081 scroll_to_tab_id = tab->ID;
10082
10083 // Refresh tab width immediately, otherwise changes of style e.g. style.FramePadding.x would noticeably lag in the tab bar.
10084 // Additionally, when using TabBarAddTab() to manipulate tab bar order we occasionally insert new tabs that don't have a width yet,
10085 // and we cannot wait for the next BeginTabItem() call. We cannot compute this width within TabBarAddTab() because font size depends on the active window.
10086 const char* tab_name = TabBarGetTabName(tab_bar, tab);
10087 const bool has_close_button_or_unsaved_marker = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) == 0 || (tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
10088 tab->ContentWidth = (tab->RequestedWidth >= 0.0f) ? tab->RequestedWidth : TabItemCalcSize(tab_name, has_close_button_or_unsaved_marker).x;
10089 if ((tab->Flags & ImGuiTabItemFlags_Button) == 0)
10090 tab->ContentWidth = ImMax(tab->ContentWidth, g.Style.TabMinWidthBase);
10091
10092 int section_n = TabItemGetSectionIdx(tab);
10093 ImGuiTabBarSection* section = &sections[section_n];
10094 section->Width += tab->ContentWidth + (section_n == curr_section_n ? tab_spacing : 0.0f);
10095 section->WidthAfterShrinkMinWidth += ImMin(tab->ContentWidth, shrink_min_width) + (section_n == curr_section_n ? tab_spacing : 0.0f);
10096 curr_section_n = section_n;
10097
10098 // Store data so we can build an array sorted by width if we need to shrink tabs down
10099 IM_MSVC_WARNING_SUPPRESS(6385);
10100 ImGuiShrinkWidthItem* shrink_width_item = &g.ShrinkWidthBuffer[shrink_buffer_indexes[section_n]++];
10101 shrink_width_item->Index = tab_n;
10102 shrink_width_item->Width = shrink_width_item->InitialWidth = tab->ContentWidth;
10103 tab->Width = ImMax(tab->ContentWidth, 1.0f);
10104 }
10105
10106 // Compute total ideal width (used for e.g. auto-resizing a window)
10107 float width_all_tabs_after_min_width_shrink = 0.0f;
10108 tab_bar->WidthAllTabsIdeal = 0.0f;
10109 for (int section_n = 0; section_n < 3; section_n++)
10110 {
10111 tab_bar->WidthAllTabsIdeal += sections[section_n].Width + sections[section_n].Spacing;
10112 width_all_tabs_after_min_width_shrink += sections[section_n].WidthAfterShrinkMinWidth + sections[section_n].Spacing;
10113 }
10114
10115 // Horizontal scrolling buttons
10116 // Important: note that TabBarScrollButtons() will alter BarRect.Max.x.
10117 const bool can_scroll = (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll) || (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyMixed);
10118 const float width_all_tabs_to_use_for_scroll = (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll) ? tab_bar->WidthAllTabs : width_all_tabs_after_min_width_shrink;
10119 tab_bar->ScrollButtonEnabled = ((width_all_tabs_to_use_for_scroll > tab_bar->BarRect.GetWidth() && tab_bar->Tabs.Size > 1) && !(tab_bar->Flags & ImGuiTabBarFlags_NoTabListScrollingButtons) && can_scroll);
10120 if (tab_bar->ScrollButtonEnabled)
10121 if (ImGuiTabItem* scroll_and_select_tab = TabBarScrollingButtons(tab_bar))
10122 {
10123 scroll_to_tab_id = scroll_and_select_tab->ID;
10124 if ((scroll_and_select_tab->Flags & ImGuiTabItemFlags_Button) == 0)
10125 tab_bar->SelectedTabId = scroll_to_tab_id;
10126 }
10127 if (scroll_to_tab_id == 0 && scroll_to_selected_tab)
10128 scroll_to_tab_id = tab_bar->SelectedTabId;
10129
10130 // Shrink widths if full tabs don't fit in their allocated space
10131 float section_0_w = sections[0].Width + sections[0].Spacing;
10132 float section_1_w = sections[1].Width + sections[1].Spacing;
10133 float section_2_w = sections[2].Width + sections[2].Spacing;
10134 bool central_section_is_visible = (section_0_w + section_2_w) < tab_bar->BarRect.GetWidth();
10135 float width_excess;
10136 if (central_section_is_visible)
10137 width_excess = ImMax(section_1_w - (tab_bar->BarRect.GetWidth() - section_0_w - section_2_w), 0.0f); // Excess used to shrink central section
10138 else
10139 width_excess = (section_0_w + section_2_w) - tab_bar->BarRect.GetWidth(); // Excess used to shrink leading/trailing section
10140
10141 // With ImGuiTabBarFlags_FittingPolicyScroll policy, we will only shrink leading/trailing if the central section is not visible anymore
10142 const bool can_shrink = (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyShrink) || (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyMixed);
10143 if (width_excess >= 1.0f && (can_shrink || !central_section_is_visible))
10144 {
10145 int shrink_data_count = (central_section_is_visible ? sections[1].TabCount : sections[0].TabCount + sections[2].TabCount);
10146 int shrink_data_offset = (central_section_is_visible ? sections[0].TabCount + sections[2].TabCount : 0);
10147 ShrinkWidths(g.ShrinkWidthBuffer.Data + shrink_data_offset, shrink_data_count, width_excess, shrink_min_width);
10148
10149 // Apply shrunk values into tabs and sections
10150 for (int tab_n = shrink_data_offset; tab_n < shrink_data_offset + shrink_data_count; tab_n++)
10151 {
10152 ImGuiTabItem* tab = &tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index];
10153 float shrinked_width = IM_TRUNC(g.ShrinkWidthBuffer[tab_n].Width);
10154 if (shrinked_width < 0.0f)
10155 continue;
10156
10157 shrinked_width = ImMax(1.0f, shrinked_width);
10158 int section_n = TabItemGetSectionIdx(tab);
10159 sections[section_n].Width -= (tab->Width - shrinked_width);
10160 tab->Width = shrinked_width;
10161 }
10162 }
10163
10164 // Layout all active tabs
10165 int section_tab_index = 0;
10166 float tab_offset = 0.0f;
10167 tab_bar->WidthAllTabs = 0.0f;
10168 for (int section_n = 0; section_n < 3; section_n++)
10169 {
10170 ImGuiTabBarSection* section = &sections[section_n];
10171 if (section_n == 2)
10172 tab_offset = ImMin(ImMax(0.0f, tab_bar->BarRect.GetWidth() - section->Width), tab_offset);
10173
10174 for (int tab_n = 0; tab_n < section->TabCount; tab_n++)
10175 {
10176 ImGuiTabItem* tab = &tab_bar->Tabs[section_tab_index + tab_n];
10177 tab->Offset = tab_offset;
10178 tab->NameOffset = -1;
10179 tab_offset += tab->Width + (tab_n < section->TabCount - 1 ? g.Style.ItemInnerSpacing.x : 0.0f);
10180 }
10181 tab_bar->WidthAllTabs += ImMax(section->Width + section->Spacing, 0.0f);
10182 tab_offset += section->Spacing;
10183 section_tab_index += section->TabCount;
10184 }
10185
10186 // Clear name buffers
10187 tab_bar->TabsNames.Buf.resize(0);
10188
10189 // If we have lost the selected tab, select the next most recently active one
10190 const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
10191 if (found_selected_tab_id == false && !tab_bar_appearing)
10192 tab_bar->SelectedTabId = 0;
10193 if (tab_bar->SelectedTabId == 0 && tab_bar->NextSelectedTabId == 0 && most_recently_selected_tab != NULL)
10194 scroll_to_tab_id = tab_bar->SelectedTabId = most_recently_selected_tab->ID;
10195
10196 // Lock in visible tab
10197 tab_bar->VisibleTabId = tab_bar->SelectedTabId;
10198 tab_bar->VisibleTabWasSubmitted = false;
10199
10200 // Apply request requests
10201 if (scroll_to_tab_id != 0)
10202 TabBarScrollToTab(tab_bar, scroll_to_tab_id, sections);
10203 else if (tab_bar->ScrollButtonEnabled && IsMouseHoveringRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max, true) && IsWindowContentHoverable(g.CurrentWindow))
10204 {
10205 const float wheel = g.IO.MouseWheelRequestAxisSwap ? g.IO.MouseWheel : g.IO.MouseWheelH;
10206 const ImGuiKey wheel_key = g.IO.MouseWheelRequestAxisSwap ? ImGuiKey_MouseWheelY : ImGuiKey_MouseWheelX;
10207 if (TestKeyOwner(wheel_key, tab_bar->ID) && wheel != 0.0f)
10208 {
10209 const float scroll_step = wheel * TabBarCalcScrollableWidth(tab_bar, sections) / 3.0f;
10210 tab_bar->ScrollingTargetDistToVisibility = 0.0f;
10211 tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, tab_bar->ScrollingTarget - scroll_step);
10212 }
10213 SetKeyOwner(wheel_key, tab_bar->ID);
10214 }
10215
10216 // Update scrolling
10217 tab_bar->ScrollingAnim = TabBarScrollClamp(tab_bar, tab_bar->ScrollingAnim);
10218 tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, tab_bar->ScrollingTarget);
10219 if (tab_bar->ScrollingAnim != tab_bar->ScrollingTarget)
10220 {
10221 // Scrolling speed adjust itself so we can always reach our target in 1/3 seconds.
10222 // Teleport if we are aiming far off the visible line
10223 tab_bar->ScrollingSpeed = ImMax(tab_bar->ScrollingSpeed, 70.0f * g.FontSize);
10224 tab_bar->ScrollingSpeed = ImMax(tab_bar->ScrollingSpeed, ImFabs(tab_bar->ScrollingTarget - tab_bar->ScrollingAnim) / 0.3f);
10225 const bool teleport = (tab_bar->PrevFrameVisible + 1 < g.FrameCount) || (tab_bar->ScrollingTargetDistToVisibility > 10.0f * g.FontSize);
10226 tab_bar->ScrollingAnim = teleport ? tab_bar->ScrollingTarget : ImLinearSweep(tab_bar->ScrollingAnim, tab_bar->ScrollingTarget, g.IO.DeltaTime * tab_bar->ScrollingSpeed);
10227 }
10228 else
10229 {
10230 tab_bar->ScrollingSpeed = 0.0f;
10231 }
10232 tab_bar->ScrollingRectMinX = tab_bar->BarRect.Min.x + sections[0].Width + sections[0].Spacing;
10233 tab_bar->ScrollingRectMaxX = tab_bar->BarRect.Max.x - sections[2].Width - sections[1].Spacing;
10234
10235 // Actual layout in host window (we don't do it in BeginTabBar() so as not to waste an extra frame)
10236 ImGuiWindow* window = g.CurrentWindow;
10237 window->DC.CursorPos = tab_bar->BarRect.Min;
10238 ItemSize(ImVec2(tab_bar->WidthAllTabs, tab_bar->BarRect.GetHeight()), tab_bar->FramePadding.y);
10239 window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, tab_bar->BarRect.Min.x + tab_bar->WidthAllTabsIdeal);
10240}
10241
10242// Dockable windows uses Name/ID in the global namespace. Non-dockable items use the ID stack.
10243static ImU32 ImGui::TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window)
10244{
10245 IM_ASSERT(docked_window == NULL); // master branch only
10246 IM_UNUSED(docked_window);
10247 if (tab_bar->Flags & ImGuiTabBarFlags_DockNode)
10248 {
10249 ImGuiID id = ImHashStr(label);
10250 KeepAliveID(id);
10251 return id;
10252 }
10253 else
10254 {
10255 ImGuiWindow* window = GImGui->CurrentWindow;
10256 return window->GetID(label);
10257 }
10258}
10259
10260static float ImGui::TabBarCalcMaxTabWidth()
10261{
10262 ImGuiContext& g = *GImGui;
10263 return g.FontSize * 20.0f;
10264}
10265
10266ImGuiTabItem* ImGui::TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id)
10267{
10268 if (tab_id != 0)
10269 for (int n = 0; n < tab_bar->Tabs.Size; n++)
10270 if (tab_bar->Tabs[n].ID == tab_id)
10271 return &tab_bar->Tabs[n];
10272 return NULL;
10273}
10274
10275// Order = visible order, not submission order! (which is tab->BeginOrder)
10276ImGuiTabItem* ImGui::TabBarFindTabByOrder(ImGuiTabBar* tab_bar, int order)
10277{
10278 if (order < 0 || order >= tab_bar->Tabs.Size)
10279 return NULL;
10280 return &tab_bar->Tabs[order];
10281}
10282
10283ImGuiTabItem* ImGui::TabBarGetCurrentTab(ImGuiTabBar* tab_bar)
10284{
10285 if (tab_bar->LastTabItemIdx < 0 || tab_bar->LastTabItemIdx >= tab_bar->Tabs.Size)
10286 return NULL;
10287 return &tab_bar->Tabs[tab_bar->LastTabItemIdx];
10288}
10289
10290const char* ImGui::TabBarGetTabName(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
10291{
10292 if (tab->NameOffset == -1)
10293 return "N/A";
10294 IM_ASSERT(tab->NameOffset < tab_bar->TabsNames.Buf.Size);
10295 return tab_bar->TabsNames.Buf.Data + tab->NameOffset;
10296}
10297
10298// The *TabId fields are already set by the docking system _before_ the actual TabItem was created, so we clear them regardless.
10299void ImGui::TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id)
10300{
10301 if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
10302 tab_bar->Tabs.erase(tab);
10303 if (tab_bar->VisibleTabId == tab_id) { tab_bar->VisibleTabId = 0; }
10304 if (tab_bar->SelectedTabId == tab_id) { tab_bar->SelectedTabId = 0; }
10305 if (tab_bar->NextSelectedTabId == tab_id) { tab_bar->NextSelectedTabId = 0; }
10306}
10307
10308// Called on manual closure attempt
10309void ImGui::TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
10310{
10311 if (tab->Flags & ImGuiTabItemFlags_Button)
10312 return; // A button appended with TabItemButton().
10313
10314 if ((tab->Flags & (ImGuiTabItemFlags_UnsavedDocument | ImGuiTabItemFlags_NoAssumedClosure)) == 0)
10315 {
10316 // This will remove a frame of lag for selecting another tab on closure.
10317 // 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
10318 tab->WantClose = true;
10319 if (tab_bar->VisibleTabId == tab->ID)
10320 {
10321 tab->LastFrameVisible = -1;
10322 tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = 0;
10323 }
10324 }
10325 else
10326 {
10327 // Actually select before expecting closure attempt (on an UnsavedDocument tab user is expect to e.g. show a popup)
10328 if (tab_bar->VisibleTabId != tab->ID)
10329 TabBarQueueFocus(tab_bar, tab);
10330 }
10331}
10332
10333static float ImGui::TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling)
10334{
10335 scrolling = ImMin(scrolling, tab_bar->WidthAllTabs - tab_bar->BarRect.GetWidth());
10336 return ImMax(scrolling, 0.0f);
10337}
10338
10339// Note: we may scroll to tab that are not selected! e.g. using keyboard arrow keys
10340static void ImGui::TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections)
10341{
10342 ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id);
10343 if (tab == NULL)
10344 return;
10345 if (tab->Flags & ImGuiTabItemFlags_SectionMask_)
10346 return;
10347
10348 ImGuiContext& g = *GImGui;
10349 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)
10350 int order = TabBarGetTabOrder(tab_bar, tab);
10351
10352 // Scrolling happens only in the central section (leading/trailing sections are not scrolling)
10353 float scrollable_width = TabBarCalcScrollableWidth(tab_bar, sections);
10354
10355 // We make all tabs positions all relative Sections[0].Width to make code simpler
10356 float tab_x1 = tab->Offset - sections[0].Width + (order > sections[0].TabCount - 1 ? -margin : 0.0f);
10357 float tab_x2 = tab->Offset - sections[0].Width + tab->Width + (order + 1 < tab_bar->Tabs.Size - sections[2].TabCount ? margin : 1.0f);
10358 tab_bar->ScrollingTargetDistToVisibility = 0.0f;
10359 if (tab_bar->ScrollingTarget > tab_x1 || (tab_x2 - tab_x1 >= scrollable_width))
10360 {
10361 // Scroll to the left
10362 tab_bar->ScrollingTargetDistToVisibility = ImMax(tab_bar->ScrollingAnim - tab_x2, 0.0f);
10363 tab_bar->ScrollingTarget = tab_x1;
10364 }
10365 else if (tab_bar->ScrollingTarget < tab_x2 - scrollable_width)
10366 {
10367 // Scroll to the right
10368 tab_bar->ScrollingTargetDistToVisibility = ImMax((tab_x1 - scrollable_width) - tab_bar->ScrollingAnim, 0.0f);
10369 tab_bar->ScrollingTarget = tab_x2 - scrollable_width;
10370 }
10371}
10372
10373void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
10374{
10375 tab_bar->NextSelectedTabId = tab->ID;
10376}
10377
10378void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, const char* tab_name)
10379{
10380 IM_ASSERT((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0); // Only supported for manual/explicit tab bars
10381 ImGuiID tab_id = TabBarCalcTabID(tab_bar, tab_name, NULL);
10382 tab_bar->NextSelectedTabId = tab_id;
10383}
10384
10385void ImGui::TabBarQueueReorder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab, int offset)
10386{
10387 IM_ASSERT(offset != 0);
10388 IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
10389 tab_bar->ReorderRequestTabId = tab->ID;
10390 tab_bar->ReorderRequestOffset = (ImS16)offset;
10391}
10392
10393void ImGui::TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, ImGuiTabItem* src_tab, ImVec2 mouse_pos)
10394{
10395 ImGuiContext& g = *GImGui;
10396 IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
10397 if ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) == 0)
10398 return;
10399
10400 const float tab_spacing = g.Style.ItemInnerSpacing.x;
10401 const bool is_central_section = (src_tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
10402 const float bar_offset = tab_bar->BarRect.Min.x - (is_central_section ? tab_bar->ScrollingTarget : 0);
10403
10404 // Count number of contiguous tabs we are crossing over
10405 const int dir = (bar_offset + src_tab->Offset) > mouse_pos.x ? -1 : +1;
10406 const int src_idx = tab_bar->Tabs.index_from_ptr(src_tab);
10407 int dst_idx = src_idx;
10408 for (int i = src_idx; i >= 0 && i < tab_bar->Tabs.Size; i += dir)
10409 {
10410 // Reordered tabs must share the same section
10411 const ImGuiTabItem* dst_tab = &tab_bar->Tabs[i];
10412 if (dst_tab->Flags & ImGuiTabItemFlags_NoReorder)
10413 break;
10414 if ((dst_tab->Flags & ImGuiTabItemFlags_SectionMask_) != (src_tab->Flags & ImGuiTabItemFlags_SectionMask_))
10415 break;
10416 dst_idx = i;
10417
10418 // Include spacing after tab, so when mouse cursor is between tabs we would not continue checking further tabs that are not hovered.
10419 const float x1 = bar_offset + dst_tab->Offset - tab_spacing;
10420 const float x2 = bar_offset + dst_tab->Offset + dst_tab->Width + tab_spacing;
10421 //GetForegroundDrawList()->AddRect(ImVec2(x1, tab_bar->BarRect.Min.y), ImVec2(x2, tab_bar->BarRect.Max.y), IM_COL32(255, 0, 0, 255));
10422 if ((dir < 0 && mouse_pos.x > x1) || (dir > 0 && mouse_pos.x < x2))
10423 break;
10424 }
10425
10426 if (dst_idx != src_idx)
10427 TabBarQueueReorder(tab_bar, src_tab, dst_idx - src_idx);
10428}
10429
10430bool ImGui::TabBarProcessReorder(ImGuiTabBar* tab_bar)
10431{
10432 ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_bar->ReorderRequestTabId);
10433 if (tab1 == NULL || (tab1->Flags & ImGuiTabItemFlags_NoReorder))
10434 return false;
10435
10436 //IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_Reorderable); // <- this may happen when using debug tools
10437 int tab2_order = TabBarGetTabOrder(tab_bar, tab1) + tab_bar->ReorderRequestOffset;
10438 if (tab2_order < 0 || tab2_order >= tab_bar->Tabs.Size)
10439 return false;
10440
10441 // Reordered tabs must share the same section
10442 // (Note: TabBarQueueReorderFromMousePos() also has a similar test but since we allow direct calls to TabBarQueueReorder() we do it here too)
10443 ImGuiTabItem* tab2 = &tab_bar->Tabs[tab2_order];
10444 if (tab2->Flags & ImGuiTabItemFlags_NoReorder)
10445 return false;
10446 if ((tab1->Flags & ImGuiTabItemFlags_SectionMask_) != (tab2->Flags & ImGuiTabItemFlags_SectionMask_))
10447 return false;
10448
10449 ImGuiTabItem item_tmp = *tab1;
10450 ImGuiTabItem* src_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 + 1 : tab2;
10451 ImGuiTabItem* dst_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 : tab2 + 1;
10452 const int move_count = (tab_bar->ReorderRequestOffset > 0) ? tab_bar->ReorderRequestOffset : -tab_bar->ReorderRequestOffset;
10453 memmove(dst_tab, src_tab, move_count * sizeof(ImGuiTabItem));
10454 *tab2 = item_tmp;
10455
10456 if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings)
10457 MarkIniSettingsDirty();
10458 return true;
10459}
10460
10461static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar)
10462{
10463 ImGuiContext& g = *GImGui;
10464 ImGuiWindow* window = g.CurrentWindow;
10465
10466 const ImVec2 arrow_button_size(g.FontSize - 2.0f, g.FontSize + g.Style.FramePadding.y * 2.0f);
10467 const float scrolling_buttons_width = arrow_button_size.x * 2.0f;
10468
10469 const ImVec2 backup_cursor_pos = window->DC.CursorPos;
10470 //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));
10471
10472 int select_dir = 0;
10473 ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
10474 arrow_col.w *= 0.5f;
10475
10476 PushStyleColor(ImGuiCol_Text, arrow_col);
10477 PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
10478 PushItemFlag(ImGuiItemFlags_ButtonRepeat | ImGuiItemFlags_NoNav, true);
10479 const float backup_repeat_delay = g.IO.KeyRepeatDelay;
10480 const float backup_repeat_rate = g.IO.KeyRepeatRate;
10481 g.IO.KeyRepeatDelay = 0.250f;
10482 g.IO.KeyRepeatRate = 0.200f;
10483 float x = ImMax(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.x - scrolling_buttons_width);
10484 window->DC.CursorPos = ImVec2(x, tab_bar->BarRect.Min.y);
10485 if (ArrowButtonEx("##<", ImGuiDir_Left, arrow_button_size, ImGuiButtonFlags_PressedOnClick))
10486 select_dir = -1;
10487 window->DC.CursorPos = ImVec2(x + arrow_button_size.x, tab_bar->BarRect.Min.y);
10488 if (ArrowButtonEx("##>", ImGuiDir_Right, arrow_button_size, ImGuiButtonFlags_PressedOnClick))
10489 select_dir = +1;
10490 PopItemFlag();
10491 PopStyleColor(2);
10492 g.IO.KeyRepeatRate = backup_repeat_rate;
10493 g.IO.KeyRepeatDelay = backup_repeat_delay;
10494
10495 ImGuiTabItem* tab_to_scroll_to = NULL;
10496 if (select_dir != 0)
10497 if (ImGuiTabItem* tab_item = TabBarFindTabByID(tab_bar, tab_bar->SelectedTabId))
10498 {
10499 int selected_order = TabBarGetTabOrder(tab_bar, tab_item);
10500 int target_order = selected_order + select_dir;
10501
10502 // Skip tab item buttons until another tab item is found or end is reached
10503 while (tab_to_scroll_to == NULL)
10504 {
10505 // If we are at the end of the list, still scroll to make our tab visible
10506 tab_to_scroll_to = &tab_bar->Tabs[(target_order >= 0 && target_order < tab_bar->Tabs.Size) ? target_order : selected_order];
10507
10508 // Cross through buttons
10509 // (even if first/last item is a button, return it so we can update the scroll)
10510 if (tab_to_scroll_to->Flags & ImGuiTabItemFlags_Button)
10511 {
10512 target_order += select_dir;
10513 selected_order += select_dir;
10514 tab_to_scroll_to = (target_order < 0 || target_order >= tab_bar->Tabs.Size) ? tab_to_scroll_to : NULL;
10515 }
10516 }
10517 }
10518 window->DC.CursorPos = backup_cursor_pos;
10519 tab_bar->BarRect.Max.x -= scrolling_buttons_width + 1.0f;
10520
10521 return tab_to_scroll_to;
10522}
10523
10524static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar)
10525{
10526 ImGuiContext& g = *GImGui;
10527 ImGuiWindow* window = g.CurrentWindow;
10528
10529 // We use g.Style.FramePadding.y to match the square ArrowButton size
10530 const float tab_list_popup_button_width = g.FontSize + g.Style.FramePadding.y;
10531 const ImVec2 backup_cursor_pos = window->DC.CursorPos;
10532 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x - g.Style.FramePadding.y, tab_bar->BarRect.Min.y);
10533 tab_bar->BarRect.Min.x += tab_list_popup_button_width;
10534
10535 ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
10536 arrow_col.w *= 0.5f;
10537 PushStyleColor(ImGuiCol_Text, arrow_col);
10538 PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
10539 bool open = BeginCombo("##v", NULL, ImGuiComboFlags_NoPreview | ImGuiComboFlags_HeightLargest);
10540 PopStyleColor(2);
10541
10542 ImGuiTabItem* tab_to_select = NULL;
10543 if (open)
10544 {
10545 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
10546 {
10547 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
10548 if (tab->Flags & ImGuiTabItemFlags_Button)
10549 continue;
10550
10551 const char* tab_name = TabBarGetTabName(tab_bar, tab);
10552 if (Selectable(tab_name, tab_bar->SelectedTabId == tab->ID))
10553 tab_to_select = tab;
10554 }
10555 EndCombo();
10556 }
10557
10558 window->DC.CursorPos = backup_cursor_pos;
10559 return tab_to_select;
10560}
10561
10562//-------------------------------------------------------------------------
10563// [SECTION] Widgets: BeginTabItem, EndTabItem, etc.
10564//-------------------------------------------------------------------------
10565// - BeginTabItem()
10566// - EndTabItem()
10567// - TabItemButton()
10568// - TabItemEx() [Internal]
10569// - SetTabItemClosed()
10570// - TabItemCalcSize() [Internal]
10571// - TabItemBackground() [Internal]
10572// - TabItemLabelAndCloseButton() [Internal]
10573//-------------------------------------------------------------------------
10574
10575bool ImGui::BeginTabItem(const char* label, bool* p_open, ImGuiTabItemFlags flags)
10576{
10577 ImGuiContext& g = *GImGui;
10578 ImGuiWindow* window = g.CurrentWindow;
10579 if (window->SkipItems)
10580 return false;
10581
10582 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10583 IM_ASSERT_USER_ERROR_RETV(tab_bar != NULL, false, "Needs to be called between BeginTabBar() and EndTabBar()!");
10584 IM_ASSERT((flags & ImGuiTabItemFlags_Button) == 0); // BeginTabItem() Can't be used with button flags, use TabItemButton() instead!
10585
10586 bool ret = TabItemEx(tab_bar, label, p_open, flags, NULL);
10587 if (ret && !(flags & ImGuiTabItemFlags_NoPushId))
10588 {
10589 ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
10590 PushOverrideID(tab->ID); // We already hashed 'label' so push into the ID stack directly instead of doing another hash through PushID(label)
10591 }
10592 return ret;
10593}
10594
10595void ImGui::EndTabItem()
10596{
10597 ImGuiContext& g = *GImGui;
10598 ImGuiWindow* window = g.CurrentWindow;
10599 if (window->SkipItems)
10600 return;
10601
10602 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10603 IM_ASSERT_USER_ERROR_RET(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
10604 IM_ASSERT(tab_bar->LastTabItemIdx >= 0);
10605 ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
10606 if (!(tab->Flags & ImGuiTabItemFlags_NoPushId))
10607 PopID();
10608}
10609
10610bool ImGui::TabItemButton(const char* label, ImGuiTabItemFlags flags)
10611{
10612 ImGuiContext& g = *GImGui;
10613 ImGuiWindow* window = g.CurrentWindow;
10614 if (window->SkipItems)
10615 return false;
10616
10617 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10618 IM_ASSERT_USER_ERROR_RETV(tab_bar != NULL, false, "Needs to be called between BeginTabBar() and EndTabBar()!");
10619 return TabItemEx(tab_bar, label, NULL, flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder, NULL);
10620}
10621
10622void ImGui::TabItemSpacing(const char* str_id, ImGuiTabItemFlags flags, float width)
10623{
10624 ImGuiContext& g = *GImGui;
10625 ImGuiWindow* window = g.CurrentWindow;
10626 if (window->SkipItems)
10627 return;
10628
10629 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10630 IM_ASSERT_USER_ERROR_RET(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
10631 SetNextItemWidth(width);
10632 TabItemEx(tab_bar, str_id, NULL, flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder | ImGuiTabItemFlags_Invisible, NULL);
10633}
10634
10635bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags, ImGuiWindow* docked_window)
10636{
10637 // Layout whole tab bar if not already done
10638 ImGuiContext& g = *GImGui;
10639 if (tab_bar->WantLayout)
10640 {
10641 ImGuiNextItemData backup_next_item_data = g.NextItemData;
10642 TabBarLayout(tab_bar);
10643 g.NextItemData = backup_next_item_data;
10644 }
10645 ImGuiWindow* window = g.CurrentWindow;
10646 if (window->SkipItems)
10647 return false;
10648
10649 const ImGuiStyle& style = g.Style;
10650 const ImGuiID id = TabBarCalcTabID(tab_bar, label, docked_window);
10651
10652 // If the user called us with *p_open == false, we early out and don't render.
10653 // 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.
10654 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
10655 if (p_open && !*p_open)
10656 {
10657 ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav);
10658 return false;
10659 }
10660
10661 IM_ASSERT(!p_open || !(flags & ImGuiTabItemFlags_Button));
10662 IM_ASSERT((flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)) != (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)); // Can't use both Leading and Trailing
10663
10664 // Store into ImGuiTabItemFlags_NoCloseButton, also honor ImGuiTabItemFlags_NoCloseButton passed by user (although not documented)
10665 if (flags & ImGuiTabItemFlags_NoCloseButton)
10666 p_open = NULL;
10667 else if (p_open == NULL)
10668 flags |= ImGuiTabItemFlags_NoCloseButton;
10669
10670 // Acquire tab data
10671 ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, id);
10672 bool tab_is_new = false;
10673 if (tab == NULL)
10674 {
10675 tab_bar->Tabs.push_back(ImGuiTabItem());
10676 tab = &tab_bar->Tabs.back();
10677 tab->ID = id;
10678 tab_bar->TabsAddedNew = tab_is_new = true;
10679 }
10680 tab_bar->LastTabItemIdx = (ImS16)tab_bar->Tabs.index_from_ptr(tab);
10681
10682 // Calculate tab contents size
10683 ImVec2 size = TabItemCalcSize(label, (p_open != NULL) || (flags & ImGuiTabItemFlags_UnsavedDocument));
10684 tab->RequestedWidth = -1.0f;
10685 if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasWidth)
10686 size.x = tab->RequestedWidth = g.NextItemData.Width;
10687 if (tab_is_new)
10688 tab->Width = ImMax(1.0f, size.x);
10689 tab->ContentWidth = size.x;
10690 tab->BeginOrder = tab_bar->TabsActiveCount++;
10691
10692 const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
10693 const bool tab_bar_focused = (tab_bar->Flags & ImGuiTabBarFlags_IsFocused) != 0;
10694 const bool tab_appearing = (tab->LastFrameVisible + 1 < g.FrameCount);
10695 const bool tab_just_unsaved = (flags & ImGuiTabItemFlags_UnsavedDocument) && !(tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
10696 const bool is_tab_button = (flags & ImGuiTabItemFlags_Button) != 0;
10697 tab->LastFrameVisible = g.FrameCount;
10698 tab->Flags = flags;
10699
10700 // Append name _WITH_ the zero-terminator
10701 if (docked_window != NULL)
10702 {
10703 IM_ASSERT(docked_window == NULL); // master branch only
10704 }
10705 else
10706 {
10707 tab->NameOffset = (ImS32)tab_bar->TabsNames.size();
10708 tab_bar->TabsNames.append(label, label + ImStrlen(label) + 1);
10709 }
10710
10711 // Update selected tab
10712 if (!is_tab_button)
10713 {
10714 if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0)
10715 if (!tab_bar_appearing || tab_bar->SelectedTabId == 0)
10716 TabBarQueueFocus(tab_bar, tab); // New tabs gets activated
10717 if ((flags & ImGuiTabItemFlags_SetSelected) && (tab_bar->SelectedTabId != id)) // _SetSelected can only be passed on explicit tab bar
10718 TabBarQueueFocus(tab_bar, tab);
10719 }
10720
10721 // Lock visibility
10722 // (Note: tab_contents_visible != tab_selected... because Ctrl+Tab operations may preview some tabs without selecting them!)
10723 bool tab_contents_visible = (tab_bar->VisibleTabId == id);
10724 if (tab_contents_visible)
10725 tab_bar->VisibleTabWasSubmitted = true;
10726
10727 // On the very first frame of a tab bar we let first tab contents be visible to minimize appearing glitches
10728 if (!tab_contents_visible && tab_bar->SelectedTabId == 0 && tab_bar_appearing)
10729 if (tab_bar->Tabs.Size == 1 && !(tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs))
10730 tab_contents_visible = true;
10731
10732 // Note that tab_is_new is not necessarily the same as tab_appearing! When a tab bar stops being submitted
10733 // and then gets submitted again, the tabs will have 'tab_appearing=true' but 'tab_is_new=false'.
10734 if (tab_appearing && (!tab_bar_appearing || tab_is_new))
10735 {
10736 ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav);
10737 if (is_tab_button)
10738 return false;
10739 return tab_contents_visible;
10740 }
10741
10742 if (tab_bar->SelectedTabId == id)
10743 tab->LastFrameSelected = g.FrameCount;
10744
10745 // Backup current layout position
10746 const ImVec2 backup_main_cursor_pos = window->DC.CursorPos;
10747
10748 // Layout
10749 const bool is_central_section = (tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
10750 size.x = tab->Width;
10751 if (is_central_section)
10752 window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_TRUNC(tab->Offset - tab_bar->ScrollingAnim), 0.0f);
10753 else
10754 window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(tab->Offset, 0.0f);
10755 ImVec2 pos = window->DC.CursorPos;
10756 ImRect bb(pos, pos + size);
10757
10758 // 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)
10759 const bool want_clip_rect = is_central_section && (bb.Min.x < tab_bar->ScrollingRectMinX || bb.Max.x > tab_bar->ScrollingRectMaxX);
10760 if (want_clip_rect)
10761 PushClipRect(ImVec2(ImClamp(bb.Min.x, tab_bar->ScrollingRectMinX, tab_bar->ScrollingRectMaxX), bb.Min.y - 1), ImVec2(tab_bar->ScrollingRectMaxX, bb.Max.y), true);
10762
10763 ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos;
10764 ItemSize(bb.GetSize(), style.FramePadding.y);
10765 window->DC.CursorMaxPos = backup_cursor_max_pos;
10766
10767 if (!ItemAdd(bb, id))
10768 {
10769 if (want_clip_rect)
10770 PopClipRect();
10771 window->DC.CursorPos = backup_main_cursor_pos;
10772 return tab_contents_visible;
10773 }
10774
10775 // Click to Select a tab
10776 // Allow the close button to overlap
10777 ImGuiButtonFlags button_flags = ((ImGuiButtonFlags)(is_tab_button ? ImGuiButtonFlags_PressedOnClickRelease : ImGuiButtonFlags_PressedOnClick) | ImGuiButtonFlags_AllowOverlap);
10778 if (g.DragDropActive)
10779 button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
10780 bool hovered, held, pressed;
10781 if (flags & ImGuiTabItemFlags_Invisible)
10782 hovered = held = pressed = false;
10783 else
10784 pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags);
10785 if (pressed && !is_tab_button)
10786 TabBarQueueFocus(tab_bar, tab);
10787
10788 // Drag and drop: re-order tabs
10789 if (held && !tab_appearing && IsMouseDragging(0))
10790 {
10791 if (!g.DragDropActive && (tab_bar->Flags & ImGuiTabBarFlags_Reorderable))
10792 {
10793 // While moving a tab it will jump on the other side of the mouse, so we also test for MouseDelta.x
10794 if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < bb.Min.x)
10795 {
10796 TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos);
10797 }
10798 else if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > bb.Max.x)
10799 {
10800 TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos);
10801 }
10802 }
10803 }
10804
10805#if 0
10806 if (hovered && g.HoveredIdNotActiveTimer > TOOLTIP_DELAY && bb.GetWidth() < tab->ContentWidth)
10807 {
10808 // Enlarge tab display when hovering
10809 bb.Max.x = bb.Min.x + IM_TRUNC(ImLerp(bb.GetWidth(), tab->ContentWidth, ImSaturate((g.HoveredIdNotActiveTimer - 0.40f) * 6.0f)));
10810 display_draw_list = GetForegroundDrawList(window);
10811 TabItemBackground(display_draw_list, bb, flags, GetColorU32(ImGuiCol_TitleBgActive));
10812 }
10813#endif
10814
10815 // Render tab shape
10816 const bool is_visible = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) && !(flags & ImGuiTabItemFlags_Invisible);
10817 if (is_visible)
10818 {
10819 ImDrawList* display_draw_list = window->DrawList;
10820 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));
10821 TabItemBackground(display_draw_list, bb, flags, tab_col);
10822 if (tab_contents_visible && (tab_bar->Flags & ImGuiTabBarFlags_DrawSelectedOverline) && style.TabBarOverlineSize > 0.0f)
10823 {
10824 // Might be moved to TabItemBackground() ?
10825 ImVec2 tl = bb.GetTL() + ImVec2(0, 1.0f * g.CurrentDpiScale);
10826 ImVec2 tr = bb.GetTR() + ImVec2(0, 1.0f * g.CurrentDpiScale);
10827 ImU32 overline_col = GetColorU32(tab_bar_focused ? ImGuiCol_TabSelectedOverline : ImGuiCol_TabDimmedSelectedOverline);
10828 if (style.TabRounding > 0.0f)
10829 {
10830 float rounding = style.TabRounding;
10831 display_draw_list->PathArcToFast(tl + ImVec2(+rounding, +rounding), rounding, 7, 9);
10832 display_draw_list->PathArcToFast(tr + ImVec2(-rounding, +rounding), rounding, 9, 11);
10833 display_draw_list->PathStroke(overline_col, style.TabBarOverlineSize);
10834 }
10835 else
10836 {
10837 display_draw_list->AddLine(tl - ImVec2(0.5f, 0.5f), tr - ImVec2(0.5f, 0.5f), overline_col, style.TabBarOverlineSize);
10838 }
10839 }
10840 RenderNavCursor(bb, id);
10841
10842 // Select with right mouse button. This is so the common idiom for context menu automatically highlight the current widget.
10843 const bool hovered_unblocked = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
10844 if (tab_bar->SelectedTabId != tab->ID && hovered_unblocked && (IsMouseClicked(1) || IsMouseReleased(1)) && !is_tab_button)
10845 TabBarQueueFocus(tab_bar, tab);
10846
10847 if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)
10848 flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton;
10849
10850 // Render tab label, process close button
10851 const ImGuiID close_button_id = p_open ? GetIDWithSeed("#CLOSE", NULL, id) : 0;
10852 bool just_closed;
10853 bool text_clipped;
10854 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);
10855 if (just_closed && p_open != NULL)
10856 {
10857 *p_open = false;
10858 TabBarCloseTab(tab_bar, tab);
10859 }
10860
10861 // Tooltip
10862 // (Won't work over the close button because ItemOverlap systems messes up with HoveredIdTimer-> seems ok)
10863 // (We test IsItemHovered() to discard e.g. when another item is active or drag and drop over the tab bar, which g.HoveredId ignores)
10864 // FIXME: This is a mess.
10865 // FIXME: We may want disabled tab to still display the tooltip?
10866 if (text_clipped && g.HoveredId == id && !held)
10867 if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip) && !(tab->Flags & ImGuiTabItemFlags_NoTooltip))
10868 SetItemTooltip("%.*s", (int)(FindRenderedTextEnd(label) - label), label);
10869 }
10870
10871 // Restore main window position so user can draw there
10872 if (want_clip_rect)
10873 PopClipRect();
10874 window->DC.CursorPos = backup_main_cursor_pos;
10875
10876 IM_ASSERT(!is_tab_button || !(tab_bar->SelectedTabId == tab->ID && is_tab_button)); // TabItemButton should not be selected
10877 if (is_tab_button)
10878 return pressed;
10879 return tab_contents_visible;
10880}
10881
10882// [Public] This is call is 100% optional but it allows to remove some one-frame glitches when a tab has been unexpectedly removed.
10883// To use it to need to call the function SetTabItemClosed() between BeginTabBar() and EndTabBar().
10884// Tabs closed by the close button will automatically be flagged to avoid this issue.
10885void ImGui::SetTabItemClosed(const char* label)
10886{
10887 ImGuiContext& g = *GImGui;
10888 bool is_within_manual_tab_bar = g.CurrentTabBar && !(g.CurrentTabBar->Flags & ImGuiTabBarFlags_DockNode);
10889 if (is_within_manual_tab_bar)
10890 {
10891 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10892 ImGuiID tab_id = TabBarCalcTabID(tab_bar, label, NULL);
10893 if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
10894 tab->WantClose = true; // Will be processed by next call to TabBarLayout()
10895 }
10896}
10897
10898ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button_or_unsaved_marker)
10899{
10900 ImGuiContext& g = *GImGui;
10901 ImVec2 label_size = CalcTextSize(label, NULL, true);
10902 ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x, label_size.y + g.Style.FramePadding.y * 2.0f);
10903 if (has_close_button_or_unsaved_marker)
10904 size.x += g.Style.FramePadding.x + (g.Style.ItemInnerSpacing.x + g.FontSize); // We use Y intentionally to fit the close button circle.
10905 else
10906 size.x += g.Style.FramePadding.x + 1.0f;
10907 return ImVec2(ImMin(size.x, TabBarCalcMaxTabWidth()), size.y);
10908}
10909
10910ImVec2 ImGui::TabItemCalcSize(ImGuiWindow*)
10911{
10912 IM_ASSERT(0); // This function exists to facilitate merge with 'docking' branch.
10913 return ImVec2(0.0f, 0.0f);
10914}
10915
10916void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImU32 col)
10917{
10918 // 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.
10919 ImGuiContext& g = *GImGui;
10920 const float width = bb.GetWidth();
10921 IM_UNUSED(flags);
10922 IM_ASSERT(width > 0.0f);
10923 const float rounding = ImMax(0.0f, ImMin((flags & ImGuiTabItemFlags_Button) ? g.Style.FrameRounding : g.Style.TabRounding, width * 0.5f - 1.0f));
10924 const float y1 = bb.Min.y + 1.0f; // Leave a bit of room in title bars.
10925 const float y2 = bb.Max.y - g.Style.TabBarBorderSize;
10926 draw_list->AddRectFilled(bb.Min, ImVec2(bb.Max.x, y2), col, rounding, ImDrawFlags_RoundCornersTop);
10927 if (g.Style.TabBorderSize > 0.0f)
10928 {
10929 draw_list->PathLineTo(ImVec2(bb.Min.x + 0.5f, y2));
10930 draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding + 0.5f, y1 + rounding + 0.5f), rounding, 6, 9);
10931 draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding - 0.5f, y1 + rounding + 0.5f), rounding, 9, 12);
10932 draw_list->PathLineTo(ImVec2(bb.Max.x - 0.5f, y2));
10933 draw_list->PathStroke(GetColorU32(ImGuiCol_Border), g.Style.TabBorderSize);
10934 }
10935}
10936
10937// Render text label (with custom clipping) + Unsaved Document marker + Close Button logic
10938// We tend to lock style.FramePadding for a given tab-bar, hence the 'frame_padding' parameter.
10939void 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)
10940{
10941 ImGuiContext& g = *GImGui;
10942 const char* label_end = FindRenderedTextEnd(label);
10943 ImVec2 label_size = CalcTextSize(label, label_end, false);
10944
10945 if (out_just_closed)
10946 *out_just_closed = false;
10947 if (out_text_clipped)
10948 *out_text_clipped = false;
10949
10950 if (bb.GetWidth() <= 1.0f)
10951 return;
10952
10953 // In Style V2 we'll have full override of all colors per state (e.g. focused, selected)
10954 // But right now if you want to alter text color of tabs this is what you need to do.
10955#if 0
10956 const float backup_alpha = g.Style.Alpha;
10957 if (!is_contents_visible)
10958 g.Style.Alpha *= 0.7f;
10959#endif
10960
10961 // Render text label (with clipping + alpha gradient) + unsaved marker
10962 ImRect text_ellipsis_clip_bb(bb.Min.x + frame_padding.x, bb.Min.y + frame_padding.y, bb.Max.x - frame_padding.x, bb.Max.y);
10963
10964 // Return clipped state ignoring the close button
10965 if (out_text_clipped)
10966 {
10967 *out_text_clipped = (text_ellipsis_clip_bb.Min.x + label_size.x) > text_ellipsis_clip_bb.Max.x;
10968 //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));
10969 }
10970
10971 const float button_sz = g.FontSize;
10972 const ImVec2 button_pos(ImMax(bb.Min.x, bb.Max.x - frame_padding.x - button_sz), bb.Min.y + frame_padding.y);
10973
10974 // Close Button & Unsaved Marker
10975 // We are relying on a subtle and confusing distinction between 'hovered' and 'g.HoveredId' which happens because we are using ImGuiButtonFlags_AllowOverlapMode + SetItemAllowOverlap()
10976 // 'hovered' will be true when hovering the Tab but NOT when hovering the close button
10977 // 'g.HoveredId==id' will be true when hovering the Tab including when hovering the close button
10978 // 'g.ActiveId==close_button_id' will be true when we are holding on the close button, in which case both hovered booleans are false
10979 bool close_button_pressed = false;
10980 bool close_button_visible = false;
10981 bool is_hovered = g.HoveredId == tab_id || g.HoveredId == close_button_id || g.ActiveId == tab_id || g.ActiveId == close_button_id; // Any interaction account for this too.
10982
10983 if (close_button_id != 0)
10984 {
10985 if (is_contents_visible)
10986 close_button_visible = (g.Style.TabCloseButtonMinWidthSelected < 0.0f) ? true : (is_hovered && bb.GetWidth() >= ImMax(button_sz, g.Style.TabCloseButtonMinWidthSelected));
10987 else
10988 close_button_visible = (g.Style.TabCloseButtonMinWidthUnselected < 0.0f) ? true : (is_hovered && bb.GetWidth() >= ImMax(button_sz, g.Style.TabCloseButtonMinWidthUnselected));
10989 }
10990
10991 // When tabs/document is unsaved, the unsaved marker takes priority over the close button.
10992 const bool unsaved_marker_visible = (flags & ImGuiTabItemFlags_UnsavedDocument) != 0 && (button_pos.x + button_sz <= bb.Max.x) && (!close_button_visible || !is_hovered);
10993 if (unsaved_marker_visible)
10994 {
10995 ImVec2 bullet_pos = button_pos + ImVec2(button_sz, button_sz) * 0.5f;
10996 RenderBullet(draw_list, bullet_pos, GetColorU32(ImGuiCol_UnsavedMarker));
10997 }
10998 else if (close_button_visible)
10999 {
11000 ImGuiLastItemData last_item_backup = g.LastItemData;
11001 if (CloseButton(close_button_id, button_pos))
11002 close_button_pressed = true;
11003 g.LastItemData = last_item_backup;
11004
11005 // Close with middle mouse button
11006 if (is_hovered && !(flags & ImGuiTabItemFlags_NoCloseWithMiddleMouseButton) && IsMouseClicked(2))
11007 close_button_pressed = true;
11008 }
11009
11010 // This is all rather complicated
11011 // (the main idea is that because the close button only appears on hover, we don't want it to alter the ellipsis position)
11012 // 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..
11013 float ellipsis_max_x = text_ellipsis_clip_bb.Max.x;
11014 if (close_button_visible || unsaved_marker_visible)
11015 {
11016 const bool visible_without_hover = unsaved_marker_visible || (is_contents_visible ? g.Style.TabCloseButtonMinWidthSelected : g.Style.TabCloseButtonMinWidthUnselected) < 0.0f;
11017 if (visible_without_hover)
11018 {
11019 text_ellipsis_clip_bb.Max.x -= button_sz * 0.90f;
11020 ellipsis_max_x -= button_sz * 0.90f;
11021 }
11022 else
11023 {
11024 text_ellipsis_clip_bb.Max.x -= button_sz * 1.00f;
11025 }
11026 }
11027 LogSetNextTextDecoration("/", "\\");
11028 RenderTextEllipsis(draw_list, text_ellipsis_clip_bb.Min, text_ellipsis_clip_bb.Max, ellipsis_max_x, label, label_end, &label_size);
11029
11030#if 0
11031 if (!is_contents_visible)
11032 g.Style.Alpha = backup_alpha;
11033#endif
11034
11035 if (out_just_closed)
11036 *out_just_closed = close_button_pressed;
11037}
11038
11039
11040#endif // #ifndef IMGUI_DISABLE