1// dear imgui, v1.92.2b
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#endif
96
97//-------------------------------------------------------------------------
98// Data
99//-------------------------------------------------------------------------
100
101// Widgets
102static const float DRAGDROP_HOLD_TO_OPEN_TIMER = 0.70f; // Time for drag-hold to activate items accepting the ImGuiButtonFlags_PressedOnDragDropHold button behavior.
103static 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.
104
105// Those MIN/MAX values are not define because we need to point to them
106static const signed char IM_S8_MIN = -128;
107static const signed char IM_S8_MAX = 127;
108static const unsigned char IM_U8_MIN = 0;
109static const unsigned char IM_U8_MAX = 0xFF;
110static const signed short IM_S16_MIN = -32768;
111static const signed short IM_S16_MAX = 32767;
112static const unsigned short IM_U16_MIN = 0;
113static const unsigned short IM_U16_MAX = 0xFFFF;
114static const ImS32 IM_S32_MIN = INT_MIN; // (-2147483647 - 1), (0x80000000);
115static const ImS32 IM_S32_MAX = INT_MAX; // (2147483647), (0x7FFFFFFF)
116static const ImU32 IM_U32_MIN = 0;
117static const ImU32 IM_U32_MAX = UINT_MAX; // (0xFFFFFFFF)
118#ifdef LLONG_MIN
119static const ImS64 IM_S64_MIN = LLONG_MIN; // (-9223372036854775807ll - 1ll);
120static const ImS64 IM_S64_MAX = LLONG_MAX; // (9223372036854775807ll);
121#else
122static const ImS64 IM_S64_MIN = -9223372036854775807LL - 1;
123static const ImS64 IM_S64_MAX = 9223372036854775807LL;
124#endif
125static const ImU64 IM_U64_MIN = 0;
126#ifdef ULLONG_MAX
127static const ImU64 IM_U64_MAX = ULLONG_MAX; // (0xFFFFFFFFFFFFFFFFull);
128#else
129static const ImU64 IM_U64_MAX = (2ULL * 9223372036854775807LL + 1);
130#endif
131
132//-------------------------------------------------------------------------
133// [SECTION] Forward Declarations
134//-------------------------------------------------------------------------
135
136// For InputTextEx()
137static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard = false);
138static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end);
139static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end, const char** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false);
140
141//-------------------------------------------------------------------------
142// [SECTION] Widgets: Text, etc.
143//-------------------------------------------------------------------------
144// - TextEx() [Internal]
145// - TextUnformatted()
146// - Text()
147// - TextV()
148// - TextColored()
149// - TextColoredV()
150// - TextDisabled()
151// - TextDisabledV()
152// - TextWrapped()
153// - TextWrappedV()
154// - LabelText()
155// - LabelTextV()
156// - BulletText()
157// - BulletTextV()
158//-------------------------------------------------------------------------
159
160void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags)
161{
162 ImGuiWindow* window = GetCurrentWindow();
163 if (window->SkipItems)
164 return;
165 ImGuiContext& g = *GImGui;
166
167 // Accept null ranges
168 if (text == text_end)
169 text = text_end = "";
170
171 // Calculate length
172 const char* text_begin = text;
173 if (text_end == NULL)
174 text_end = text + ImStrlen(s: text); // FIXME-OPT
175
176 const ImVec2 text_pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
177 const float wrap_pos_x = window->DC.TextWrapPos;
178 const bool wrap_enabled = (wrap_pos_x >= 0.0f);
179 if (text_end - text <= 2000 || wrap_enabled)
180 {
181 // Common case
182 const float wrap_width = wrap_enabled ? CalcWrapWidthForPos(pos: window->DC.CursorPos, wrap_pos_x) : 0.0f;
183 const ImVec2 text_size = CalcTextSize(text: text_begin, text_end, hide_text_after_double_hash: false, wrap_width);
184
185 ImRect bb(text_pos, text_pos + text_size);
186 ItemSize(size: text_size, text_baseline_y: 0.0f);
187 if (!ItemAdd(bb, id: 0))
188 return;
189
190 // Render (we don't hide text after ## in this end-user function)
191 RenderTextWrapped(pos: bb.Min, text: text_begin, text_end, wrap_width);
192 }
193 else
194 {
195 // Long text!
196 // Perform manual coarse clipping to optimize for long multi-line text
197 // - From this point we will only compute the width of lines that are visible. Optimization only available when word-wrapping is disabled.
198 // - 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.
199 // - We use memchr(), pay attention that well optimized versions of those str/mem functions are much faster than a casually written loop.
200 const char* line = text;
201 const float line_height = GetTextLineHeight();
202 ImVec2 text_size(0, 0);
203
204 // Lines to skip (can't skip when logging text)
205 ImVec2 pos = text_pos;
206 if (!g.LogEnabled)
207 {
208 int lines_skippable = (int)((window->ClipRect.Min.y - text_pos.y) / line_height);
209 if (lines_skippable > 0)
210 {
211 int lines_skipped = 0;
212 while (line < text_end && lines_skipped < lines_skippable)
213 {
214 const char* line_end = (const char*)ImMemchr(s: line, c: '\n', n: text_end - line);
215 if (!line_end)
216 line_end = text_end;
217 if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0)
218 text_size.x = ImMax(lhs: text_size.x, rhs: CalcTextSize(text: line, text_end: line_end).x);
219 line = line_end + 1;
220 lines_skipped++;
221 }
222 pos.y += lines_skipped * line_height;
223 }
224 }
225
226 // Lines to render
227 if (line < text_end)
228 {
229 ImRect line_rect(pos, pos + ImVec2(FLT_MAX, line_height));
230 while (line < text_end)
231 {
232 if (IsClippedEx(bb: line_rect, id: 0))
233 break;
234
235 const char* line_end = (const char*)ImMemchr(s: line, c: '\n', n: text_end - line);
236 if (!line_end)
237 line_end = text_end;
238 text_size.x = ImMax(lhs: text_size.x, rhs: CalcTextSize(text: line, text_end: line_end).x);
239 RenderText(pos, text: line, text_end: line_end, hide_text_after_hash: false);
240 line = line_end + 1;
241 line_rect.Min.y += line_height;
242 line_rect.Max.y += line_height;
243 pos.y += line_height;
244 }
245
246 // Count remaining lines
247 int lines_skipped = 0;
248 while (line < text_end)
249 {
250 const char* line_end = (const char*)ImMemchr(s: line, c: '\n', n: text_end - line);
251 if (!line_end)
252 line_end = text_end;
253 if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0)
254 text_size.x = ImMax(lhs: text_size.x, rhs: CalcTextSize(text: line, text_end: line_end).x);
255 line = line_end + 1;
256 lines_skipped++;
257 }
258 pos.y += lines_skipped * line_height;
259 }
260 text_size.y = (pos - text_pos).y;
261
262 ImRect bb(text_pos, text_pos + text_size);
263 ItemSize(size: text_size, text_baseline_y: 0.0f);
264 ItemAdd(bb, id: 0);
265 }
266}
267
268void ImGui::TextUnformatted(const char* text, const char* text_end)
269{
270 TextEx(text, text_end, flags: ImGuiTextFlags_NoWidthForLargeClippedText);
271}
272
273void ImGui::Text(const char* fmt, ...)
274{
275 va_list args;
276 va_start(args, fmt);
277 TextV(fmt, args);
278 va_end(args);
279}
280
281void ImGui::TextV(const char* fmt, va_list args)
282{
283 ImGuiWindow* window = GetCurrentWindow();
284 if (window->SkipItems)
285 return;
286
287 const char* text, *text_end;
288 ImFormatStringToTempBufferV(out_buf: &text, out_buf_end: &text_end, fmt, args);
289 TextEx(text, text_end, flags: ImGuiTextFlags_NoWidthForLargeClippedText);
290}
291
292void ImGui::TextColored(const ImVec4& col, const char* fmt, ...)
293{
294 va_list args;
295 va_start(args, fmt);
296 TextColoredV(col, fmt, args);
297 va_end(args);
298}
299
300void ImGui::TextColoredV(const ImVec4& col, const char* fmt, va_list args)
301{
302 PushStyleColor(idx: ImGuiCol_Text, col);
303 TextV(fmt, args);
304 PopStyleColor();
305}
306
307void ImGui::TextDisabled(const char* fmt, ...)
308{
309 va_list args;
310 va_start(args, fmt);
311 TextDisabledV(fmt, args);
312 va_end(args);
313}
314
315void ImGui::TextDisabledV(const char* fmt, va_list args)
316{
317 ImGuiContext& g = *GImGui;
318 PushStyleColor(idx: ImGuiCol_Text, col: g.Style.Colors[ImGuiCol_TextDisabled]);
319 TextV(fmt, args);
320 PopStyleColor();
321}
322
323void ImGui::TextWrapped(const char* fmt, ...)
324{
325 va_list args;
326 va_start(args, fmt);
327 TextWrappedV(fmt, args);
328 va_end(args);
329}
330
331void ImGui::TextWrappedV(const char* fmt, va_list args)
332{
333 ImGuiContext& g = *GImGui;
334 const bool need_backup = (g.CurrentWindow->DC.TextWrapPos < 0.0f); // Keep existing wrap position if one is already set
335 if (need_backup)
336 PushTextWrapPos(wrap_local_pos_x: 0.0f);
337 TextV(fmt, args);
338 if (need_backup)
339 PopTextWrapPos();
340}
341
342void ImGui::TextAligned(float align_x, float size_x, const char* fmt, ...)
343{
344 va_list args;
345 va_start(args, fmt);
346 TextAlignedV(align_x, size_x, fmt, args);
347 va_end(args);
348}
349
350// align_x: 0.0f = left, 0.5f = center, 1.0f = right.
351// size_x : 0.0f = shortcut for GetContentRegionAvail().x
352// FIXME-WIP: Works but API is likely to be reworked. This is designed for 1 item on the line. (#7024)
353void ImGui::TextAlignedV(float align_x, float size_x, const char* fmt, va_list args)
354{
355 ImGuiWindow* window = GetCurrentWindow();
356 if (window->SkipItems)
357 return;
358
359 const char* text, *text_end;
360 ImFormatStringToTempBufferV(out_buf: &text, out_buf_end: &text_end, fmt, args);
361 const ImVec2 text_size = CalcTextSize(text, text_end);
362 size_x = CalcItemSize(size: ImVec2(size_x, 0.0f), default_w: 0.0f, default_h: text_size.y).x;
363
364 ImVec2 pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
365 ImVec2 pos_max(pos.x + size_x, window->ClipRect.Max.y);
366 ImVec2 size(ImMin(lhs: size_x, rhs: text_size.x), text_size.y);
367 window->DC.CursorMaxPos.x = ImMax(lhs: window->DC.CursorMaxPos.x, rhs: pos.x + text_size.x);
368 window->DC.IdealMaxPos.x = ImMax(lhs: window->DC.IdealMaxPos.x, rhs: pos.x + text_size.x);
369 if (align_x > 0.0f && text_size.x < size_x)
370 pos.x += ImTrunc(f: (size_x - text_size.x) * align_x);
371 RenderTextEllipsis(draw_list: window->DrawList, pos_min: pos, pos_max, ellipsis_max_x: pos_max.x, text, text_end, text_size_if_known: &text_size);
372
373 const ImVec2 backup_max_pos = window->DC.CursorMaxPos;
374 ItemSize(size);
375 ItemAdd(bb: ImRect(pos, pos + size), id: 0);
376 window->DC.CursorMaxPos.x = backup_max_pos.x; // Cancel out extending content size because right-aligned text would otherwise mess it up.
377
378 if (size_x < text_size.x && IsItemHovered(flags: ImGuiHoveredFlags_NoNavOverride | ImGuiHoveredFlags_AllowWhenDisabled | ImGuiHoveredFlags_ForTooltip))
379 SetTooltip("%.*s", (int)(text_end - text), text);
380}
381
382void ImGui::LabelText(const char* label, const char* fmt, ...)
383{
384 va_list args;
385 va_start(args, fmt);
386 LabelTextV(label, fmt, args);
387 va_end(args);
388}
389
390// Add a label+text combo aligned to other label+value widgets
391void ImGui::LabelTextV(const char* label, const char* fmt, va_list args)
392{
393 ImGuiWindow* window = GetCurrentWindow();
394 if (window->SkipItems)
395 return;
396
397 ImGuiContext& g = *GImGui;
398 const ImGuiStyle& style = g.Style;
399 const float w = CalcItemWidth();
400
401 const char* value_text_begin, *value_text_end;
402 ImFormatStringToTempBufferV(out_buf: &value_text_begin, out_buf_end: &value_text_end, fmt, args);
403 const ImVec2 value_size = CalcTextSize(text: value_text_begin, text_end: value_text_end, hide_text_after_double_hash: false);
404 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
405
406 const ImVec2 pos = window->DC.CursorPos;
407 const ImRect value_bb(pos, pos + ImVec2(w, value_size.y + style.FramePadding.y * 2));
408 const ImRect total_bb(pos, pos + ImVec2(w + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), ImMax(lhs: value_size.y, rhs: label_size.y) + style.FramePadding.y * 2));
409 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
410 if (!ItemAdd(bb: total_bb, id: 0))
411 return;
412
413 // Render
414 RenderTextClipped(pos_min: value_bb.Min + style.FramePadding, pos_max: value_bb.Max, text: value_text_begin, text_end: value_text_end, text_size_if_known: &value_size, align: ImVec2(0.0f, 0.0f));
415 if (label_size.x > 0.0f)
416 RenderText(pos: ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), text: label);
417}
418
419void ImGui::BulletText(const char* fmt, ...)
420{
421 va_list args;
422 va_start(args, fmt);
423 BulletTextV(fmt, args);
424 va_end(args);
425}
426
427// Text with a little bullet aligned to the typical tree node.
428void ImGui::BulletTextV(const char* fmt, va_list args)
429{
430 ImGuiWindow* window = GetCurrentWindow();
431 if (window->SkipItems)
432 return;
433
434 ImGuiContext& g = *GImGui;
435 const ImGuiStyle& style = g.Style;
436
437 const char* text_begin, *text_end;
438 ImFormatStringToTempBufferV(out_buf: &text_begin, out_buf_end: &text_end, fmt, args);
439 const ImVec2 label_size = CalcTextSize(text: text_begin, text_end, hide_text_after_double_hash: false);
440 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
441 ImVec2 pos = window->DC.CursorPos;
442 pos.y += window->DC.CurrLineTextBaseOffset;
443 ItemSize(size: total_size, text_baseline_y: 0.0f);
444 const ImRect bb(pos, pos + total_size);
445 if (!ItemAdd(bb, id: 0))
446 return;
447
448 // Render
449 ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
450 RenderBullet(draw_list: window->DrawList, pos: bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, g.FontSize * 0.5f), col: text_col);
451 RenderText(pos: bb.Min + ImVec2(g.FontSize + style.FramePadding.x * 2, 0.0f), text: text_begin, text_end, hide_text_after_hash: false);
452}
453
454//-------------------------------------------------------------------------
455// [SECTION] Widgets: Main
456//-------------------------------------------------------------------------
457// - ButtonBehavior() [Internal]
458// - Button()
459// - SmallButton()
460// - InvisibleButton()
461// - ArrowButton()
462// - CloseButton() [Internal]
463// - CollapseButton() [Internal]
464// - GetWindowScrollbarID() [Internal]
465// - GetWindowScrollbarRect() [Internal]
466// - Scrollbar() [Internal]
467// - ScrollbarEx() [Internal]
468// - Image()
469// - ImageButton()
470// - Checkbox()
471// - CheckboxFlagsT() [Internal]
472// - CheckboxFlags()
473// - RadioButton()
474// - ProgressBar()
475// - Bullet()
476// - Hyperlink()
477//-------------------------------------------------------------------------
478
479// The ButtonBehavior() function is key to many interactions and used by many/most widgets.
480// Because we handle so many cases (keyboard/gamepad navigation, drag and drop) and many specific behavior (via ImGuiButtonFlags_),
481// this code is a little complex.
482// By far the most common path is interacting with the Mouse using the default ImGuiButtonFlags_PressedOnClickRelease button behavior.
483// See the series of events below and the corresponding state reported by dear imgui:
484//------------------------------------------------------------------------------------------------------------------------------------------------
485// with PressedOnClickRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
486// Frame N+0 (mouse is outside bb) - - - - - -
487// Frame N+1 (mouse moves inside bb) - true - - - -
488// Frame N+2 (mouse button is down) - true true true - true
489// Frame N+3 (mouse button is down) - true true - - -
490// Frame N+4 (mouse moves outside bb) - - true - - -
491// Frame N+5 (mouse moves inside bb) - true true - - -
492// Frame N+6 (mouse button is released) true true - - true -
493// Frame N+7 (mouse button is released) - true - - - -
494// Frame N+8 (mouse moves outside bb) - - - - - -
495//------------------------------------------------------------------------------------------------------------------------------------------------
496// with PressedOnClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
497// Frame N+2 (mouse button is down) true true true true - true
498// Frame N+3 (mouse button is down) - true true - - -
499// Frame N+6 (mouse button is released) - true - - true -
500// Frame N+7 (mouse button is released) - true - - - -
501//------------------------------------------------------------------------------------------------------------------------------------------------
502// with PressedOnRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
503// Frame N+2 (mouse button is down) - true - - - true
504// Frame N+3 (mouse button is down) - true - - - -
505// Frame N+6 (mouse button is released) true true - - - -
506// Frame N+7 (mouse button is released) - true - - - -
507//------------------------------------------------------------------------------------------------------------------------------------------------
508// with PressedOnDoubleClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
509// Frame N+0 (mouse button is down) - true - - - true
510// Frame N+1 (mouse button is down) - true - - - -
511// Frame N+2 (mouse button is released) - true - - - -
512// Frame N+3 (mouse button is released) - true - - - -
513// Frame N+4 (mouse button is down) true true true true - true
514// Frame N+5 (mouse button is down) - true true - - -
515// Frame N+6 (mouse button is released) - true - - true -
516// Frame N+7 (mouse button is released) - true - - - -
517//------------------------------------------------------------------------------------------------------------------------------------------------
518// Note that some combinations are supported,
519// - PressedOnDragDropHold can generally be associated with any flag.
520// - PressedOnDoubleClick can be associated by PressedOnClickRelease/PressedOnRelease, in which case the second release event won't be reported.
521//------------------------------------------------------------------------------------------------------------------------------------------------
522// The behavior of the return-value changes when ImGuiItemFlags_ButtonRepeat is set:
523// Repeat+ Repeat+ Repeat+ Repeat+
524// PressedOnClickRelease PressedOnClick PressedOnRelease PressedOnDoubleClick
525//-------------------------------------------------------------------------------------------------------------------------------------------------
526// Frame N+0 (mouse button is down) - true - true
527// ... - - - -
528// Frame N + RepeatDelay true true - true
529// ... - - - -
530// Frame N + RepeatDelay + RepeatRate*N true true - true
531//-------------------------------------------------------------------------------------------------------------------------------------------------
532
533// - FIXME: For refactor we could output flags, incl mouse hovered vs nav keyboard vs nav triggered etc.
534// And better standardize how widgets use 'GetColor32((held && hovered) ? ... : hovered ? ...)' vs 'GetColor32(held ? ... : hovered ? ...);'
535// For mouse feedback we typically prefer the 'held && hovered' test, but for nav feedback not always. Outputting hovered=true on Activation may be misleading.
536// - Since v1.91.2 (Sept 2024) we included io.ConfigDebugHighlightIdConflicts feature.
537// One idiom which was previously valid which will now emit a warning is when using multiple overlaid ButtonBehavior()
538// with same ID and different MouseButton (see #8030). You can fix it by:
539// (1) switching to use a single ButtonBehavior() with multiple _MouseButton flags.
540// or (2) surrounding those calls with PushItemFlag(ImGuiItemFlags_AllowDuplicateId, true); ... PopItemFlag()
541bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags)
542{
543 ImGuiContext& g = *GImGui;
544 ImGuiWindow* window = GetCurrentWindow();
545
546 // Default behavior inherited from item flags
547 // Note that _both_ ButtonFlags and ItemFlags are valid sources, so copy one into the item_flags and only check that.
548 ImGuiItemFlags item_flags = (g.LastItemData.ID == id ? g.LastItemData.ItemFlags : g.CurrentItemFlags);
549 if (flags & ImGuiButtonFlags_AllowOverlap)
550 item_flags |= ImGuiItemFlags_AllowOverlap;
551 if (item_flags & ImGuiItemFlags_NoFocus)
552 flags |= ImGuiButtonFlags_NoFocus | ImGuiButtonFlags_NoNavFocus;
553
554 // Default only reacts to left mouse button
555 if ((flags & ImGuiButtonFlags_MouseButtonMask_) == 0)
556 flags |= ImGuiButtonFlags_MouseButtonLeft;
557
558 // Default behavior requires click + release inside bounding box
559 if ((flags & ImGuiButtonFlags_PressedOnMask_) == 0)
560 flags |= (item_flags & ImGuiItemFlags_ButtonRepeat) ? ImGuiButtonFlags_PressedOnClick : ImGuiButtonFlags_PressedOnDefault_;
561
562 ImGuiWindow* backup_hovered_window = g.HoveredWindow;
563 const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredWindow && g.HoveredWindow->RootWindowDockTree == window->RootWindowDockTree;
564 if (flatten_hovered_children)
565 g.HoveredWindow = window;
566
567#ifdef IMGUI_ENABLE_TEST_ENGINE
568 // Alternate registration spot, for when caller didn't use ItemAdd()
569 if (g.LastItemData.ID != id)
570 IMGUI_TEST_ENGINE_ITEM_ADD(id, bb, NULL);
571#endif
572
573 bool pressed = false;
574 bool hovered = ItemHoverable(bb, id, item_flags);
575
576 // Special mode for Drag and Drop used by openables (tree nodes, tabs etc.)
577 // where holding the button pressed for a long time while drag a payload item triggers the button.
578 if (g.DragDropActive && (flags & ImGuiButtonFlags_PressedOnDragDropHold) && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers))
579 if (IsItemHovered(flags: ImGuiHoveredFlags_AllowWhenBlockedByActiveItem))
580 {
581 hovered = true;
582 SetHoveredID(id);
583 if (g.HoveredIdTimer - g.IO.DeltaTime <= DRAGDROP_HOLD_TO_OPEN_TIMER && g.HoveredIdTimer >= DRAGDROP_HOLD_TO_OPEN_TIMER)
584 {
585 pressed = true;
586 g.DragDropHoldJustPressedId = id;
587 FocusWindow(window);
588 }
589 }
590
591 if (flatten_hovered_children)
592 g.HoveredWindow = backup_hovered_window;
593
594 // Mouse handling
595 const ImGuiID test_owner_id = (flags & ImGuiButtonFlags_NoTestKeyOwner) ? ImGuiKeyOwner_Any : id;
596 if (hovered)
597 {
598 IM_ASSERT(id != 0); // Lazily check inside rare path.
599
600 // Poll mouse buttons
601 // - 'mouse_button_clicked' is generally carried into ActiveIdMouseButton when setting ActiveId.
602 // - Technically we only need some values in one code path, but since this is gated by hovered test this is fine.
603 int mouse_button_clicked = -1;
604 int mouse_button_released = -1;
605 for (int button = 0; button < 3; button++)
606 if (flags & (ImGuiButtonFlags_MouseButtonLeft << button)) // Handle ImGuiButtonFlags_MouseButtonRight and ImGuiButtonFlags_MouseButtonMiddle here.
607 {
608 if (IsMouseClicked(button, flags: ImGuiInputFlags_None, owner_id: test_owner_id) && mouse_button_clicked == -1) { mouse_button_clicked = button; }
609 if (IsMouseReleased(button, owner_id: test_owner_id) && mouse_button_released == -1) { mouse_button_released = button; }
610 }
611
612 // Process initial action
613 const bool mods_ok = !(flags & ImGuiButtonFlags_NoKeyModsAllowed) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt);
614 if (mods_ok)
615 {
616 if (mouse_button_clicked != -1 && g.ActiveId != id)
617 {
618 if (!(flags & ImGuiButtonFlags_NoSetKeyOwner))
619 SetKeyOwner(key: MouseButtonToKey(button: mouse_button_clicked), owner_id: id);
620 if (flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClickReleaseAnywhere))
621 {
622 SetActiveID(id, window);
623 g.ActiveIdMouseButton = mouse_button_clicked;
624 if (!(flags & ImGuiButtonFlags_NoNavFocus))
625 {
626 SetFocusID(id, window);
627 FocusWindow(window);
628 }
629 else if (!(flags & ImGuiButtonFlags_NoFocus))
630 {
631 FocusWindow(window, flags: ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child
632 }
633 }
634 if ((flags & ImGuiButtonFlags_PressedOnClick) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseClickedCount[mouse_button_clicked] == 2))
635 {
636 pressed = true;
637 if (flags & ImGuiButtonFlags_NoHoldingActiveId)
638 ClearActiveID();
639 else
640 SetActiveID(id, window); // Hold on ID
641 g.ActiveIdMouseButton = mouse_button_clicked;
642 if (!(flags & ImGuiButtonFlags_NoNavFocus))
643 {
644 SetFocusID(id, window);
645 FocusWindow(window);
646 }
647 else if (!(flags & ImGuiButtonFlags_NoFocus))
648 {
649 FocusWindow(window, flags: ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child
650 }
651 }
652 }
653 if (flags & ImGuiButtonFlags_PressedOnRelease)
654 {
655 if (mouse_button_released != -1)
656 {
657 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
658 if (!has_repeated_at_least_once)
659 pressed = true;
660 if (!(flags & ImGuiButtonFlags_NoNavFocus))
661 SetFocusID(id, window); // FIXME: Lack of FocusWindow() call here is inconsistent with other paths. Research why.
662 ClearActiveID();
663 }
664 }
665
666 // 'Repeat' mode acts when held regardless of _PressedOn flags (see table above).
667 // Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings.
668 if (g.ActiveId == id && (item_flags & ImGuiItemFlags_ButtonRepeat))
669 if (g.IO.MouseDownDuration[g.ActiveIdMouseButton] > 0.0f && IsMouseClicked(button: g.ActiveIdMouseButton, flags: ImGuiInputFlags_Repeat, owner_id: test_owner_id))
670 pressed = true;
671 }
672
673 if (pressed && g.IO.ConfigNavCursorVisibleAuto)
674 g.NavCursorVisible = false;
675 }
676
677 // Keyboard/Gamepad navigation handling
678 // We report navigated and navigation-activated items as hovered but we don't set g.HoveredId to not interfere with mouse.
679 if (g.NavId == id && g.NavCursorVisible && g.NavHighlightItemUnderNav)
680 if (!(flags & ImGuiButtonFlags_NoHoveredOnFocus))
681 hovered = true;
682 if (g.NavActivateDownId == id)
683 {
684 bool nav_activated_by_code = (g.NavActivateId == id);
685 bool nav_activated_by_inputs = (g.NavActivatePressedId == id);
686 if (!nav_activated_by_inputs && (item_flags & ImGuiItemFlags_ButtonRepeat))
687 {
688 // Avoid pressing multiple keys from triggering excessive amount of repeat events
689 const ImGuiKeyData* key1 = GetKeyData(key: ImGuiKey_Space);
690 const ImGuiKeyData* key2 = GetKeyData(key: ImGuiKey_Enter);
691 const ImGuiKeyData* key3 = GetKeyData(ImGuiKey_NavGamepadActivate);
692 const float t1 = ImMax(lhs: ImMax(lhs: key1->DownDuration, rhs: key2->DownDuration), rhs: key3->DownDuration);
693 nav_activated_by_inputs = CalcTypematicRepeatAmount(t0: t1 - g.IO.DeltaTime, t1, repeat_delay: g.IO.KeyRepeatDelay, repeat_rate: g.IO.KeyRepeatRate) > 0;
694 }
695 if (nav_activated_by_code || nav_activated_by_inputs)
696 {
697 // Set active id so it can be queried by user via IsItemActive(), equivalent of holding the mouse button.
698 pressed = true;
699 SetActiveID(id, window);
700 g.ActiveIdSource = g.NavInputSource;
701 if (!(flags & ImGuiButtonFlags_NoNavFocus) && !(g.NavActivateFlags & ImGuiActivateFlags_FromShortcut))
702 SetFocusID(id, window);
703 if (g.NavActivateFlags & ImGuiActivateFlags_FromShortcut)
704 g.ActiveIdFromShortcut = true;
705 }
706 }
707
708 // Process while held
709 bool held = false;
710 if (g.ActiveId == id)
711 {
712 if (g.ActiveIdSource == ImGuiInputSource_Mouse)
713 {
714 if (g.ActiveIdIsJustActivated)
715 g.ActiveIdClickOffset = g.IO.MousePos - bb.Min;
716
717 const int mouse_button = g.ActiveIdMouseButton;
718 if (mouse_button == -1)
719 {
720 // Fallback for the rare situation were g.ActiveId was set programmatically or from another widget (e.g. #6304).
721 ClearActiveID();
722 }
723 else if (IsMouseDown(button: mouse_button, owner_id: test_owner_id))
724 {
725 held = true;
726 }
727 else
728 {
729 bool release_in = hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease) != 0;
730 bool release_anywhere = (flags & ImGuiButtonFlags_PressedOnClickReleaseAnywhere) != 0;
731 if ((release_in || release_anywhere) && !g.DragDropActive)
732 {
733 // Report as pressed when releasing the mouse (this is the most common path)
734 bool is_double_click_release = (flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseReleased[mouse_button] && g.IO.MouseClickedLastCount[mouse_button] == 2;
735 bool is_repeating_already = (item_flags & ImGuiItemFlags_ButtonRepeat) && g.IO.MouseDownDurationPrev[mouse_button] >= g.IO.KeyRepeatDelay; // Repeat mode trumps <on release>
736 bool is_button_avail_or_owned = TestKeyOwner(key: MouseButtonToKey(button: mouse_button), owner_id: test_owner_id);
737 if (!is_double_click_release && !is_repeating_already && is_button_avail_or_owned)
738 pressed = true;
739 }
740 ClearActiveID();
741 }
742 if (!(flags & ImGuiButtonFlags_NoNavFocus) && g.IO.ConfigNavCursorVisibleAuto)
743 g.NavCursorVisible = false;
744 }
745 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
746 {
747 // When activated using Nav, we hold on the ActiveID until activation button is released
748 if (g.NavActivateDownId == id)
749 held = true; // hovered == true not true as we are already likely hovered on direct activation.
750 else
751 ClearActiveID();
752 }
753 if (pressed)
754 g.ActiveIdHasBeenPressedBefore = true;
755 }
756
757 // Activation highlight (this may be a remote activation)
758 if (g.NavHighlightActivatedId == id)
759 hovered = true;
760
761 if (out_hovered) *out_hovered = hovered;
762 if (out_held) *out_held = held;
763
764 return pressed;
765}
766
767bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags)
768{
769 ImGuiWindow* window = GetCurrentWindow();
770 if (window->SkipItems)
771 return false;
772
773 ImGuiContext& g = *GImGui;
774 const ImGuiStyle& style = g.Style;
775 const ImGuiID id = window->GetID(str: label);
776 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
777
778 ImVec2 pos = window->DC.CursorPos;
779 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)
780 pos.y += window->DC.CurrLineTextBaseOffset - style.FramePadding.y;
781 ImVec2 size = CalcItemSize(size: size_arg, default_w: label_size.x + style.FramePadding.x * 2.0f, default_h: label_size.y + style.FramePadding.y * 2.0f);
782
783 const ImRect bb(pos, pos + size);
784 ItemSize(size, text_baseline_y: style.FramePadding.y);
785 if (!ItemAdd(bb, id))
786 return false;
787
788 bool hovered, held;
789 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags);
790
791 // Render
792 const ImU32 col = GetColorU32(idx: (held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
793 RenderNavCursor(bb, id);
794 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: col, borders: true, rounding: style.FrameRounding);
795
796 if (g.LogEnabled)
797 LogSetNextTextDecoration(prefix: "[", suffix: "]");
798 RenderTextClipped(pos_min: bb.Min + style.FramePadding, pos_max: bb.Max - style.FramePadding, text: label, NULL, text_size_if_known: &label_size, align: style.ButtonTextAlign, clip_rect: &bb);
799
800 // Automatically close popups
801 //if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup))
802 // CloseCurrentPopup();
803
804 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
805 return pressed;
806}
807
808bool ImGui::Button(const char* label, const ImVec2& size_arg)
809{
810 return ButtonEx(label, size_arg, flags: ImGuiButtonFlags_None);
811}
812
813// Small buttons fits within text without additional vertical spacing.
814bool ImGui::SmallButton(const char* label)
815{
816 ImGuiContext& g = *GImGui;
817 float backup_padding_y = g.Style.FramePadding.y;
818 g.Style.FramePadding.y = 0.0f;
819 bool pressed = ButtonEx(label, size_arg: ImVec2(0, 0), flags: ImGuiButtonFlags_AlignTextBaseLine);
820 g.Style.FramePadding.y = backup_padding_y;
821 return pressed;
822}
823
824// Tip: use ImGui::PushID()/PopID() to push indices or pointers in the ID stack.
825// 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)
826bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg, ImGuiButtonFlags flags)
827{
828 ImGuiContext& g = *GImGui;
829 ImGuiWindow* window = GetCurrentWindow();
830 if (window->SkipItems)
831 return false;
832
833 // Cannot use zero-size for InvisibleButton(). Unlike Button() there is not way to fallback using the label size.
834 IM_ASSERT(size_arg.x != 0.0f && size_arg.y != 0.0f);
835
836 const ImGuiID id = window->GetID(str: str_id);
837 ImVec2 size = CalcItemSize(size: size_arg, default_w: 0.0f, default_h: 0.0f);
838 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
839 ItemSize(size);
840 if (!ItemAdd(bb, id, NULL, extra_flags: (flags & ImGuiButtonFlags_EnableNav) ? ImGuiItemFlags_None : ImGuiItemFlags_NoNav))
841 return false;
842
843 bool hovered, held;
844 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags);
845 RenderNavCursor(bb, id);
846
847 IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
848 return pressed;
849}
850
851bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiButtonFlags flags)
852{
853 ImGuiContext& g = *GImGui;
854 ImGuiWindow* window = GetCurrentWindow();
855 if (window->SkipItems)
856 return false;
857
858 const ImGuiID id = window->GetID(str: str_id);
859 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
860 const float default_size = GetFrameHeight();
861 ItemSize(size, text_baseline_y: (size.y >= default_size) ? g.Style.FramePadding.y : -1.0f);
862 if (!ItemAdd(bb, id))
863 return false;
864
865 bool hovered, held;
866 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags);
867
868 // Render
869 const ImU32 bg_col = GetColorU32(idx: (held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
870 const ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
871 RenderNavCursor(bb, id);
872 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: bg_col, borders: true, rounding: g.Style.FrameRounding);
873 RenderArrow(draw_list: window->DrawList, pos: bb.Min + ImVec2(ImMax(lhs: 0.0f, rhs: (size.x - g.FontSize) * 0.5f), ImMax(lhs: 0.0f, rhs: (size.y - g.FontSize) * 0.5f)), col: text_col, dir);
874
875 IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
876 return pressed;
877}
878
879bool ImGui::ArrowButton(const char* str_id, ImGuiDir dir)
880{
881 float sz = GetFrameHeight();
882 return ArrowButtonEx(str_id, dir, size: ImVec2(sz, sz), flags: ImGuiButtonFlags_None);
883}
884
885// Button to close a window
886bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos)
887{
888 ImGuiContext& g = *GImGui;
889 ImGuiWindow* window = g.CurrentWindow;
890
891 // 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)
892 // 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?
893 const ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize));
894 ImRect bb_interact = bb;
895 const float area_to_visible_ratio = window->OuterRectClipped.GetArea() / bb.GetArea();
896 if (area_to_visible_ratio < 1.5f)
897 bb_interact.Expand(amount: ImTrunc(v: bb_interact.GetSize() * -0.25f));
898
899 // Tweak 2: We intentionally allow interaction when clipped so that a mechanical Alt,Right,Activate sequence can always close a window.
900 // (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).
901 bool is_clipped = !ItemAdd(bb: bb_interact, id);
902
903 bool hovered, held;
904 bool pressed = ButtonBehavior(bb: bb_interact, id, out_hovered: &hovered, out_held: &held);
905 if (is_clipped)
906 return pressed;
907
908 // Render
909 ImU32 bg_col = GetColorU32(idx: held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered);
910 if (hovered)
911 window->DrawList->AddRectFilled(p_min: bb.Min, p_max: bb.Max, col: bg_col);
912 RenderNavCursor(bb, id, flags: ImGuiNavRenderCursorFlags_Compact);
913 const ImU32 cross_col = GetColorU32(idx: ImGuiCol_Text);
914 const ImVec2 cross_center = bb.GetCenter() - ImVec2(0.5f, 0.5f);
915 const float cross_extent = g.FontSize * 0.5f * 0.7071f - 1.0f;
916 const float cross_thickness = 1.0f; // FIXME-DPI
917 window->DrawList->AddLine(p1: cross_center + ImVec2(+cross_extent, +cross_extent), p2: cross_center + ImVec2(-cross_extent, -cross_extent), col: cross_col, thickness: cross_thickness);
918 window->DrawList->AddLine(p1: cross_center + ImVec2(+cross_extent, -cross_extent), p2: cross_center + ImVec2(-cross_extent, +cross_extent), col: cross_col, thickness: cross_thickness);
919
920 return pressed;
921}
922
923// The Collapse button also functions as a Dock Menu button.
924bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos, ImGuiDockNode* dock_node)
925{
926 ImGuiContext& g = *GImGui;
927 ImGuiWindow* window = g.CurrentWindow;
928
929 ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize));
930 bool is_clipped = !ItemAdd(bb, id);
931 bool hovered, held;
932 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags: ImGuiButtonFlags_None);
933 if (is_clipped)
934 return pressed;
935
936 // Render
937 //bool is_dock_menu = (window->DockNodeAsHost && !window->Collapsed);
938 ImU32 bg_col = GetColorU32(idx: (held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
939 ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
940 if (hovered || held)
941 window->DrawList->AddRectFilled(p_min: bb.Min, p_max: bb.Max, col: bg_col);
942 RenderNavCursor(bb, id, flags: ImGuiNavRenderCursorFlags_Compact);
943
944 if (dock_node)
945 RenderArrowDockMenu(draw_list: window->DrawList, p_min: bb.Min, sz: g.FontSize, col: text_col);
946 else
947 RenderArrow(draw_list: window->DrawList, pos: bb.Min, col: text_col, dir: window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, scale: 1.0f);
948
949 // Switch to moving the window after mouse is moved beyond the initial drag threshold
950 if (IsItemActive() && IsMouseDragging(button: 0))
951 StartMouseMovingWindowOrNode(window, node: dock_node, undock: true); // Undock from window/collapse menu button
952
953 return pressed;
954}
955
956ImGuiID ImGui::GetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis)
957{
958 return window->GetID(str: axis == ImGuiAxis_X ? "#SCROLLX" : "#SCROLLY");
959}
960
961// Return scrollbar rectangle, must only be called for corresponding axis if window->ScrollbarX/Y is set.
962ImRect ImGui::GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis)
963{
964 ImGuiContext& g = *GImGui;
965 const ImRect outer_rect = window->Rect();
966 const ImRect inner_rect = window->InnerRect;
967 const float scrollbar_size = window->ScrollbarSizes[axis ^ 1]; // (ScrollbarSizes.x = width of Y scrollbar; ScrollbarSizes.y = height of X scrollbar)
968 IM_ASSERT(scrollbar_size >= 0.0f);
969 const float border_size = IM_ROUND(window->WindowBorderSize * 0.5f);
970 const float border_top = (window->Flags & ImGuiWindowFlags_MenuBar) ? IM_ROUND(g.Style.FrameBorderSize * 0.5f) : 0.0f;
971 if (axis == ImGuiAxis_X)
972 return ImRect(inner_rect.Min.x + border_size, ImMax(lhs: outer_rect.Min.y + border_size, rhs: outer_rect.Max.y - border_size - scrollbar_size), inner_rect.Max.x - border_size, outer_rect.Max.y - border_size);
973 else
974 return ImRect(ImMax(lhs: outer_rect.Min.x, rhs: 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);
975}
976
977void ImGui::Scrollbar(ImGuiAxis axis)
978{
979 ImGuiContext& g = *GImGui;
980 ImGuiWindow* window = g.CurrentWindow;
981 const ImGuiID id = GetWindowScrollbarID(window, axis);
982
983 // Calculate scrollbar bounding box
984 ImRect bb = GetWindowScrollbarRect(window, axis);
985 ImDrawFlags rounding_corners = ImDrawFlags_RoundCornersNone;
986 if (axis == ImGuiAxis_X)
987 {
988 rounding_corners |= ImDrawFlags_RoundCornersBottomLeft;
989 if (!window->ScrollbarY)
990 rounding_corners |= ImDrawFlags_RoundCornersBottomRight;
991 }
992 else
993 {
994 if ((window->Flags & ImGuiWindowFlags_NoTitleBar) && !(window->Flags & ImGuiWindowFlags_MenuBar))
995 rounding_corners |= ImDrawFlags_RoundCornersTopRight;
996 if (!window->ScrollbarX)
997 rounding_corners |= ImDrawFlags_RoundCornersBottomRight;
998 }
999 float size_visible = window->InnerRect.Max[axis] - window->InnerRect.Min[axis];
1000 float size_contents = window->ContentSize[axis] + window->WindowPadding[axis] * 2.0f;
1001 ImS64 scroll = (ImS64)window->Scroll[axis];
1002 ScrollbarEx(bb, id, axis, p_scroll_v: &scroll, avail_v: (ImS64)size_visible, contents_v: (ImS64)size_contents, draw_rounding_flags: rounding_corners);
1003 window->Scroll[axis] = (float)scroll;
1004}
1005
1006// Vertical/Horizontal scrollbar
1007// The entire piece of code below is rather confusing because:
1008// - We handle absolute seeking (when first clicking outside the grab) and relative manipulation (afterward or when clicking inside the grab)
1009// - We store values as normalized ratio and in a form that allows the window content to change while we are holding on a scrollbar
1010// - We handle both horizontal and vertical scrollbars, which makes the terminology not ideal.
1011// Still, the code should probably be made simpler..
1012bool 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)
1013{
1014 ImGuiContext& g = *GImGui;
1015 ImGuiWindow* window = g.CurrentWindow;
1016 if (window->SkipItems)
1017 return false;
1018
1019 const float bb_frame_width = bb_frame.GetWidth();
1020 const float bb_frame_height = bb_frame.GetHeight();
1021 if (bb_frame_width <= 0.0f || bb_frame_height <= 0.0f)
1022 return false;
1023
1024 // 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)
1025 float alpha = 1.0f;
1026 if ((axis == ImGuiAxis_Y) && bb_frame_height < bb_frame_width)
1027 alpha = ImSaturate(f: bb_frame_height / ImMax(lhs: bb_frame_width * 2.0f, rhs: 1.0f));
1028 if (alpha <= 0.0f)
1029 return false;
1030
1031 const ImGuiStyle& style = g.Style;
1032 const bool allow_interaction = (alpha >= 1.0f);
1033
1034 ImRect bb = bb_frame;
1035 bb.Expand(amount: ImVec2(-ImClamp(IM_TRUNC((bb_frame_width - 2.0f) * 0.5f), mn: 0.0f, mx: 3.0f), -ImClamp(IM_TRUNC((bb_frame_height - 2.0f) * 0.5f), mn: 0.0f, mx: 3.0f)));
1036
1037 // V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar)
1038 const float scrollbar_size_v = (axis == ImGuiAxis_X) ? bb.GetWidth() : bb.GetHeight();
1039
1040 // Calculate the height of our grabbable box. It generally represent the amount visible (vs the total scrollable amount)
1041 // But we maintain a minimum size in pixel to allow for the user to still aim inside.
1042 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.
1043 const ImS64 win_size_v = ImMax(lhs: ImMax(lhs: size_contents_v, rhs: size_visible_v), rhs: (ImS64)1);
1044 const float grab_h_minsize = ImMin(lhs: bb.GetSize()[axis], rhs: style.GrabMinSize);
1045 const float grab_h_pixels = ImClamp(v: scrollbar_size_v * ((float)size_visible_v / (float)win_size_v), mn: grab_h_minsize, mx: scrollbar_size_v);
1046 const float grab_h_norm = grab_h_pixels / scrollbar_size_v;
1047
1048 // Handle input right away. None of the code of Begin() is relying on scrolling position before calling Scrollbar().
1049 bool held = false;
1050 bool hovered = false;
1051 ItemAdd(bb: bb_frame, id, NULL, extra_flags: ImGuiItemFlags_NoNav);
1052 ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags: ImGuiButtonFlags_NoNavFocus);
1053
1054 const ImS64 scroll_max = ImMax(lhs: (ImS64)1, rhs: size_contents_v - size_visible_v);
1055 float scroll_ratio = ImSaturate(f: (float)*p_scroll_v / (float)scroll_max);
1056 float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; // Grab position in normalized space
1057 if (held && allow_interaction && grab_h_norm < 1.0f)
1058 {
1059 const float scrollbar_pos_v = bb.Min[axis];
1060 const float mouse_pos_v = g.IO.MousePos[axis];
1061
1062 // Click position in scrollbar normalized space (0.0f->1.0f)
1063 const float clicked_v_norm = ImSaturate(f: (mouse_pos_v - scrollbar_pos_v) / scrollbar_size_v);
1064
1065 const int held_dir = (clicked_v_norm < grab_v_norm) ? -1 : (clicked_v_norm > grab_v_norm + grab_h_norm) ? +1 : 0;
1066 if (g.ActiveIdIsJustActivated)
1067 {
1068 // On initial click when held_dir == 0 (clicked over grab): calculate the distance between mouse and the center of the grab
1069 const bool scroll_to_clicked_location = (g.IO.ConfigScrollbarScrollByPage == false || g.IO.KeyShift || held_dir == 0);
1070 g.ScrollbarSeekMode = scroll_to_clicked_location ? 0 : (short)held_dir;
1071 g.ScrollbarClickDeltaToGrabCenter = (held_dir == 0 && !g.IO.KeyShift) ? clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f : 0.0f;
1072 }
1073
1074 // Apply scroll (p_scroll_v will generally point on one member of window->Scroll)
1075 // 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
1076 if (g.ScrollbarSeekMode == 0)
1077 {
1078 // Absolute seeking
1079 const float scroll_v_norm = ImSaturate(f: (clicked_v_norm - g.ScrollbarClickDeltaToGrabCenter - grab_h_norm * 0.5f) / (1.0f - grab_h_norm));
1080 *p_scroll_v = (ImS64)(scroll_v_norm * scroll_max);
1081 }
1082 else
1083 {
1084 // Page by page
1085 if (IsMouseClicked(button: ImGuiMouseButton_Left, flags: ImGuiInputFlags_Repeat) && held_dir == g.ScrollbarSeekMode)
1086 {
1087 float page_dir = (g.ScrollbarSeekMode > 0.0f) ? +1.0f : -1.0f;
1088 *p_scroll_v = ImClamp(v: *p_scroll_v + (ImS64)(page_dir * size_visible_v), mn: (ImS64)0, mx: scroll_max);
1089 }
1090 }
1091
1092 // Update values for rendering
1093 scroll_ratio = ImSaturate(f: (float)*p_scroll_v / (float)scroll_max);
1094 grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v;
1095
1096 // Update distance to grab now that we have seek'ed and saturated
1097 //if (seek_absolute)
1098 // g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f;
1099 }
1100
1101 // Render
1102 const ImU32 bg_col = GetColorU32(idx: ImGuiCol_ScrollbarBg);
1103 const ImU32 grab_col = GetColorU32(idx: held ? ImGuiCol_ScrollbarGrabActive : hovered ? ImGuiCol_ScrollbarGrabHovered : ImGuiCol_ScrollbarGrab, alpha_mul: alpha);
1104 window->DrawList->AddRectFilled(p_min: bb_frame.Min, p_max: bb_frame.Max, col: bg_col, rounding: window->WindowRounding, flags: draw_rounding_flags);
1105 ImRect grab_rect;
1106 if (axis == ImGuiAxis_X)
1107 grab_rect = ImRect(ImLerp(a: bb.Min.x, b: bb.Max.x, t: grab_v_norm), bb.Min.y, ImLerp(a: bb.Min.x, b: bb.Max.x, t: grab_v_norm) + grab_h_pixels, bb.Max.y);
1108 else
1109 grab_rect = ImRect(bb.Min.x, ImLerp(a: bb.Min.y, b: bb.Max.y, t: grab_v_norm), bb.Max.x, ImLerp(a: bb.Min.y, b: bb.Max.y, t: grab_v_norm) + grab_h_pixels);
1110 window->DrawList->AddRectFilled(p_min: grab_rect.Min, p_max: grab_rect.Max, col: grab_col, rounding: style.ScrollbarRounding);
1111
1112 return held;
1113}
1114
1115// - Read about ImTextureID/ImTextureRef here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples
1116// - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above.
1117void ImGui::ImageWithBg(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col)
1118{
1119 ImGuiContext& g = *GImGui;
1120 ImGuiWindow* window = GetCurrentWindow();
1121 if (window->SkipItems)
1122 return;
1123
1124 const ImVec2 padding(g.Style.ImageBorderSize, g.Style.ImageBorderSize);
1125 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f);
1126 ItemSize(bb);
1127 if (!ItemAdd(bb, id: 0))
1128 return;
1129
1130 // Render
1131 if (g.Style.ImageBorderSize > 0.0f)
1132 window->DrawList->AddRect(p_min: bb.Min, p_max: bb.Max, col: GetColorU32(idx: ImGuiCol_Border), rounding: 0.0f, flags: ImDrawFlags_None, thickness: g.Style.ImageBorderSize);
1133 if (bg_col.w > 0.0f)
1134 window->DrawList->AddRectFilled(p_min: bb.Min + padding, p_max: bb.Max - padding, col: GetColorU32(col: bg_col));
1135 window->DrawList->AddImage(tex_ref, p_min: bb.Min + padding, p_max: bb.Max - padding, uv_min: uv0, uv_max: uv1, col: GetColorU32(col: tint_col));
1136}
1137
1138void ImGui::Image(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1)
1139{
1140 ImageWithBg(tex_ref, image_size, uv0, uv1);
1141}
1142
1143// 1.91.9 (February 2025) removed 'tint_col' and 'border_col' parameters, made border size not depend on color value. (#8131, #8238)
1144#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1145void ImGui::Image(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col)
1146{
1147 ImGuiContext& g = *GImGui;
1148 PushStyleVar(idx: ImGuiStyleVar_ImageBorderSize, val: (border_col.w > 0.0f) ? ImMax(lhs: 1.0f, rhs: g.Style.ImageBorderSize) : 0.0f); // Preserve legacy behavior where border is always visible when border_col's Alpha is >0.0f
1149 PushStyleColor(idx: ImGuiCol_Border, col: border_col);
1150 ImageWithBg(tex_ref, image_size, uv0, uv1, bg_col: ImVec4(0, 0, 0, 0), tint_col);
1151 PopStyleColor();
1152 PopStyleVar();
1153}
1154#endif
1155
1156bool 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)
1157{
1158 ImGuiContext& g = *GImGui;
1159 ImGuiWindow* window = GetCurrentWindow();
1160 if (window->SkipItems)
1161 return false;
1162
1163 const ImVec2 padding = g.Style.FramePadding;
1164 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f);
1165 ItemSize(bb);
1166 if (!ItemAdd(bb, id))
1167 return false;
1168
1169 bool hovered, held;
1170 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags);
1171
1172 // Render
1173 const ImU32 col = GetColorU32(idx: (held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1174 RenderNavCursor(bb, id);
1175 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: col, borders: true, rounding: ImClamp(v: (float)ImMin(lhs: padding.x, rhs: padding.y), mn: 0.0f, mx: g.Style.FrameRounding));
1176 if (bg_col.w > 0.0f)
1177 window->DrawList->AddRectFilled(p_min: bb.Min + padding, p_max: bb.Max - padding, col: GetColorU32(col: bg_col));
1178 window->DrawList->AddImage(tex_ref, p_min: bb.Min + padding, p_max: bb.Max - padding, uv_min: uv0, uv_max: uv1, col: GetColorU32(col: tint_col));
1179
1180 return pressed;
1181}
1182
1183// - ImageButton() adds style.FramePadding*2.0f to provided size. This is in order to facilitate fitting an image in a button.
1184// - ImageButton() draws a background based on regular Button() color + optionally an inner background if specified. (#8165) // FIXME: Maybe that's not the best design?
1185bool 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)
1186{
1187 ImGuiContext& g = *GImGui;
1188 ImGuiWindow* window = g.CurrentWindow;
1189 if (window->SkipItems)
1190 return false;
1191
1192 return ImageButtonEx(id: window->GetID(str: str_id), tex_ref, image_size, uv0, uv1, bg_col, tint_col);
1193}
1194
1195#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1196// Legacy API obsoleted in 1.89. Two differences with new ImageButton()
1197// - old ImageButton() used ImTextureID as item id (created issue with multiple buttons with same image, transient texture id values, opaque computation of ID)
1198// - new ImageButton() requires an explicit 'const char* str_id'
1199// - old ImageButton() had frame_padding' override argument.
1200// - new ImageButton() always use style.FramePadding.
1201/*
1202bool 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)
1203{
1204 // Default to using texture ID as ID. User can still push string/integer prefixes.
1205 PushID((ImTextureID)(intptr_t)user_texture_id);
1206 if (frame_padding >= 0)
1207 PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2((float)frame_padding, (float)frame_padding));
1208 bool ret = ImageButton("", user_texture_id, size, uv0, uv1, bg_col, tint_col);
1209 if (frame_padding >= 0)
1210 PopStyleVar();
1211 PopID();
1212 return ret;
1213}
1214*/
1215#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1216
1217bool ImGui::Checkbox(const char* label, bool* v)
1218{
1219 ImGuiWindow* window = GetCurrentWindow();
1220 if (window->SkipItems)
1221 return false;
1222
1223 ImGuiContext& g = *GImGui;
1224 const ImGuiStyle& style = g.Style;
1225 const ImGuiID id = window->GetID(str: label);
1226 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
1227
1228 const float square_sz = GetFrameHeight();
1229 const ImVec2 pos = window->DC.CursorPos;
1230 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));
1231 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
1232 const bool is_visible = ItemAdd(bb: total_bb, id);
1233 const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
1234 if (!is_visible)
1235 if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(r: total_bb)) // Extra layer of "no logic clip" for box-select support
1236 {
1237 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1238 return false;
1239 }
1240
1241 // Range-Selection/Multi-selection support (header)
1242 bool checked = *v;
1243 if (is_multi_select)
1244 MultiSelectItemHeader(id, p_selected: &checked, NULL);
1245
1246 bool hovered, held;
1247 bool pressed = ButtonBehavior(bb: total_bb, id, out_hovered: &hovered, out_held: &held);
1248
1249 // Range-Selection/Multi-selection support (footer)
1250 if (is_multi_select)
1251 MultiSelectItemFooter(id, p_selected: &checked, p_pressed: &pressed);
1252 else if (pressed)
1253 checked = !checked;
1254
1255 if (*v != checked)
1256 {
1257 *v = checked;
1258 pressed = true; // return value
1259 MarkItemEdited(id);
1260 }
1261
1262 const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1263 const bool mixed_value = (g.LastItemData.ItemFlags & ImGuiItemFlags_MixedValue) != 0;
1264 if (is_visible)
1265 {
1266 RenderNavCursor(bb: total_bb, id);
1267 RenderFrame(p_min: check_bb.Min, p_max: check_bb.Max, fill_col: GetColorU32(idx: (held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), borders: true, rounding: style.FrameRounding);
1268 ImU32 check_col = GetColorU32(idx: ImGuiCol_CheckMark);
1269 if (mixed_value)
1270 {
1271 // Undocumented tristate/mixed/indeterminate checkbox (#2644)
1272 // This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all widgets (not just checkbox)
1273 ImVec2 pad(ImMax(lhs: 1.0f, IM_TRUNC(square_sz / 3.6f)), ImMax(lhs: 1.0f, IM_TRUNC(square_sz / 3.6f)));
1274 window->DrawList->AddRectFilled(p_min: check_bb.Min + pad, p_max: check_bb.Max - pad, col: check_col, rounding: style.FrameRounding);
1275 }
1276 else if (*v)
1277 {
1278 const float pad = ImMax(lhs: 1.0f, IM_TRUNC(square_sz / 6.0f));
1279 RenderCheckMark(draw_list: window->DrawList, pos: check_bb.Min + ImVec2(pad, pad), col: check_col, sz: square_sz - pad * 2.0f);
1280 }
1281 }
1282 const ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1283 if (g.LogEnabled)
1284 LogRenderedText(ref_pos: &label_pos, text: mixed_value ? "[~]" : *v ? "[x]" : "[ ]");
1285 if (is_visible && label_size.x > 0.0f)
1286 RenderText(pos: label_pos, text: label);
1287
1288 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1289 return pressed;
1290}
1291
1292template<typename T>
1293bool ImGui::CheckboxFlagsT(const char* label, T* flags, T flags_value)
1294{
1295 bool all_on = (*flags & flags_value) == flags_value;
1296 bool any_on = (*flags & flags_value) != 0;
1297 bool pressed;
1298 if (!all_on && any_on)
1299 {
1300 ImGuiContext& g = *GImGui;
1301 g.NextItemData.ItemFlags |= ImGuiItemFlags_MixedValue;
1302 pressed = Checkbox(label, v: &all_on);
1303 }
1304 else
1305 {
1306 pressed = Checkbox(label, v: &all_on);
1307
1308 }
1309 if (pressed)
1310 {
1311 if (all_on)
1312 *flags |= flags_value;
1313 else
1314 *flags &= ~flags_value;
1315 }
1316 return pressed;
1317}
1318
1319bool ImGui::CheckboxFlags(const char* label, int* flags, int flags_value)
1320{
1321 return CheckboxFlagsT(label, flags, flags_value);
1322}
1323
1324bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value)
1325{
1326 return CheckboxFlagsT(label, flags, flags_value);
1327}
1328
1329bool ImGui::CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value)
1330{
1331 return CheckboxFlagsT(label, flags, flags_value);
1332}
1333
1334bool ImGui::CheckboxFlags(const char* label, ImU64* flags, ImU64 flags_value)
1335{
1336 return CheckboxFlagsT(label, flags, flags_value);
1337}
1338
1339bool ImGui::RadioButton(const char* label, bool active)
1340{
1341 ImGuiWindow* window = GetCurrentWindow();
1342 if (window->SkipItems)
1343 return false;
1344
1345 ImGuiContext& g = *GImGui;
1346 const ImGuiStyle& style = g.Style;
1347 const ImGuiID id = window->GetID(str: label);
1348 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
1349
1350 const float square_sz = GetFrameHeight();
1351 const ImVec2 pos = window->DC.CursorPos;
1352 const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1353 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));
1354 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
1355 if (!ItemAdd(bb: total_bb, id))
1356 return false;
1357
1358 ImVec2 center = check_bb.GetCenter();
1359 center.x = IM_ROUND(center.x);
1360 center.y = IM_ROUND(center.y);
1361 const float radius = (square_sz - 1.0f) * 0.5f;
1362
1363 bool hovered, held;
1364 bool pressed = ButtonBehavior(bb: total_bb, id, out_hovered: &hovered, out_held: &held);
1365 if (pressed)
1366 MarkItemEdited(id);
1367
1368 RenderNavCursor(bb: total_bb, id);
1369 const int num_segment = window->DrawList->_CalcCircleAutoSegmentCount(radius);
1370 window->DrawList->AddCircleFilled(center, radius, col: GetColorU32(idx: (held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), num_segments: num_segment);
1371 if (active)
1372 {
1373 const float pad = ImMax(lhs: 1.0f, IM_TRUNC(square_sz / 6.0f));
1374 window->DrawList->AddCircleFilled(center, radius: radius - pad, col: GetColorU32(idx: ImGuiCol_CheckMark));
1375 }
1376
1377 if (style.FrameBorderSize > 0.0f)
1378 {
1379 window->DrawList->AddCircle(center: center + ImVec2(1, 1), radius, col: GetColorU32(idx: ImGuiCol_BorderShadow), num_segments: num_segment, thickness: style.FrameBorderSize);
1380 window->DrawList->AddCircle(center, radius, col: GetColorU32(idx: ImGuiCol_Border), num_segments: num_segment, thickness: style.FrameBorderSize);
1381 }
1382
1383 ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1384 if (g.LogEnabled)
1385 LogRenderedText(ref_pos: &label_pos, text: active ? "(x)" : "( )");
1386 if (label_size.x > 0.0f)
1387 RenderText(pos: label_pos, text: label);
1388
1389 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
1390 return pressed;
1391}
1392
1393// 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..
1394bool ImGui::RadioButton(const char* label, int* v, int v_button)
1395{
1396 const bool pressed = RadioButton(label, active: *v == v_button);
1397 if (pressed)
1398 *v = v_button;
1399 return pressed;
1400}
1401
1402// size_arg (for each axis) < 0.0f: align to end, 0.0f: auto, > 0.0f: specified size
1403void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* overlay)
1404{
1405 ImGuiWindow* window = GetCurrentWindow();
1406 if (window->SkipItems)
1407 return;
1408
1409 ImGuiContext& g = *GImGui;
1410 const ImGuiStyle& style = g.Style;
1411
1412 ImVec2 pos = window->DC.CursorPos;
1413 ImVec2 size = CalcItemSize(size: size_arg, default_w: CalcItemWidth(), default_h: g.FontSize + style.FramePadding.y * 2.0f);
1414 ImRect bb(pos, pos + size);
1415 ItemSize(size, text_baseline_y: style.FramePadding.y);
1416 if (!ItemAdd(bb, id: 0))
1417 return;
1418
1419 // Fraction < 0.0f will display an indeterminate progress bar animation
1420 // The value must be animated along with time, so e.g. passing '-1.0f * ImGui::GetTime()' as fraction works.
1421 const bool is_indeterminate = (fraction < 0.0f);
1422 if (!is_indeterminate)
1423 fraction = ImSaturate(f: fraction);
1424
1425 // Out of courtesy we accept a NaN fraction without crashing
1426 float fill_n0 = 0.0f;
1427 float fill_n1 = (fraction == fraction) ? fraction : 0.0f;
1428
1429 if (is_indeterminate)
1430 {
1431 const float fill_width_n = 0.2f;
1432 fill_n0 = ImFmod(-fraction, 1.0f) * (1.0f + fill_width_n) - fill_width_n;
1433 fill_n1 = ImSaturate(f: fill_n0 + fill_width_n);
1434 fill_n0 = ImSaturate(f: fill_n0);
1435 }
1436
1437 // Render
1438 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: GetColorU32(idx: ImGuiCol_FrameBg), borders: true, rounding: style.FrameRounding);
1439 bb.Expand(amount: ImVec2(-style.FrameBorderSize, -style.FrameBorderSize));
1440 RenderRectFilledRangeH(draw_list: window->DrawList, rect: bb, col: GetColorU32(idx: ImGuiCol_PlotHistogram), x_start_norm: fill_n0, x_end_norm: fill_n1, rounding: style.FrameRounding);
1441
1442 // Default displaying the fraction as percentage string, but user can override it
1443 // Don't display text for indeterminate bars by default
1444 char overlay_buf[32];
1445 if (!is_indeterminate || overlay != NULL)
1446 {
1447 if (!overlay)
1448 {
1449 ImFormatString(buf: overlay_buf, IM_ARRAYSIZE(overlay_buf), fmt: "%.0f%%", fraction * 100 + 0.01f);
1450 overlay = overlay_buf;
1451 }
1452
1453 ImVec2 overlay_size = CalcTextSize(text: overlay, NULL);
1454 if (overlay_size.x > 0.0f)
1455 {
1456 float text_x = is_indeterminate ? (bb.Min.x + bb.Max.x - overlay_size.x) * 0.5f : ImLerp(a: bb.Min.x, b: bb.Max.x, t: fill_n1) + style.ItemSpacing.x;
1457 RenderTextClipped(pos_min: ImVec2(ImClamp(v: text_x, mn: bb.Min.x, mx: bb.Max.x - overlay_size.x - style.ItemInnerSpacing.x), bb.Min.y), pos_max: bb.Max, text: overlay, NULL, text_size_if_known: &overlay_size, align: ImVec2(0.0f, 0.5f), clip_rect: &bb);
1458 }
1459 }
1460}
1461
1462void ImGui::Bullet()
1463{
1464 ImGuiWindow* window = GetCurrentWindow();
1465 if (window->SkipItems)
1466 return;
1467
1468 ImGuiContext& g = *GImGui;
1469 const ImGuiStyle& style = g.Style;
1470 const float line_height = ImMax(lhs: ImMin(lhs: window->DC.CurrLineSize.y, rhs: g.FontSize + style.FramePadding.y * 2), rhs: g.FontSize);
1471 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize, line_height));
1472 ItemSize(bb);
1473 if (!ItemAdd(bb, id: 0))
1474 {
1475 SameLine(offset_from_start_x: 0, spacing: style.FramePadding.x * 2);
1476 return;
1477 }
1478
1479 // Render and stay on same line
1480 ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
1481 RenderBullet(draw_list: window->DrawList, pos: bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, line_height * 0.5f), col: text_col);
1482 SameLine(offset_from_start_x: 0, spacing: style.FramePadding.x * 2.0f);
1483}
1484
1485// This is provided as a convenience for being an often requested feature.
1486// FIXME-STYLE: we delayed adding as there is a larger plan to revamp the styling system.
1487// Because of this we currently don't provide many styling options for this widget
1488// (e.g. hovered/active colors are automatically inferred from a single color).
1489bool ImGui::TextLink(const char* label)
1490{
1491 ImGuiWindow* window = GetCurrentWindow();
1492 if (window->SkipItems)
1493 return false;
1494
1495 ImGuiContext& g = *GImGui;
1496 const ImGuiID id = window->GetID(str: label);
1497 const char* label_end = FindRenderedTextEnd(text: label);
1498
1499 ImVec2 pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
1500 ImVec2 size = CalcTextSize(text: label, text_end: label_end, hide_text_after_double_hash: true);
1501 ImRect bb(pos, pos + size);
1502 ItemSize(size, text_baseline_y: 0.0f);
1503 if (!ItemAdd(bb, id))
1504 return false;
1505
1506 bool hovered, held;
1507 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held);
1508 RenderNavCursor(bb, id);
1509
1510 if (hovered)
1511 SetMouseCursor(ImGuiMouseCursor_Hand);
1512
1513 ImVec4 text_colf = g.Style.Colors[ImGuiCol_TextLink];
1514 ImVec4 line_colf = text_colf;
1515 {
1516 // FIXME-STYLE: Read comments above. This widget is NOT written in the same style as some earlier widgets,
1517 // as we are currently experimenting/planning a different styling system.
1518 float h, s, v;
1519 ColorConvertRGBtoHSV(r: text_colf.x, g: text_colf.y, b: text_colf.z, out_h&: h, out_s&: s, out_v&: v);
1520 if (held || hovered)
1521 {
1522 v = ImSaturate(f: v + (held ? 0.4f : 0.3f));
1523 h = ImFmod(h + 0.02f, 1.0f);
1524 }
1525 ColorConvertHSVtoRGB(h, s, v, out_r&: text_colf.x, out_g&: text_colf.y, out_b&: text_colf.z);
1526 v = ImSaturate(f: v - 0.20f);
1527 ColorConvertHSVtoRGB(h, s, v, out_r&: line_colf.x, out_g&: line_colf.y, out_b&: line_colf.z);
1528 }
1529
1530 float line_y = bb.Max.y + ImFloor(f: g.FontBaked->Descent * g.FontBakedScale * 0.20f);
1531 window->DrawList->AddLine(p1: ImVec2(bb.Min.x, line_y), p2: ImVec2(bb.Max.x, line_y), col: GetColorU32(col: line_colf)); // FIXME-TEXT: Underline mode // FIXME-DPI
1532
1533 PushStyleColor(idx: ImGuiCol_Text, col: GetColorU32(col: text_colf));
1534 RenderText(pos: bb.Min, text: label, text_end: label_end);
1535 PopStyleColor();
1536
1537 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
1538 return pressed;
1539}
1540
1541bool ImGui::TextLinkOpenURL(const char* label, const char* url)
1542{
1543 ImGuiContext& g = *GImGui;
1544 if (url == NULL)
1545 url = label;
1546 bool pressed = TextLink(label);
1547 if (pressed && g.PlatformIO.Platform_OpenInShellFn != NULL)
1548 g.PlatformIO.Platform_OpenInShellFn(&g, url);
1549 SetItemTooltip(LocalizeGetMsg(key: ImGuiLocKey_OpenLink_s), url); // It is more reassuring for user to _always_ display URL when we same as label
1550 if (BeginPopupContextItem())
1551 {
1552 if (MenuItem(label: LocalizeGetMsg(key: ImGuiLocKey_CopyLink)))
1553 SetClipboardText(url);
1554 EndPopup();
1555 }
1556 return pressed;
1557}
1558
1559//-------------------------------------------------------------------------
1560// [SECTION] Widgets: Low-level Layout helpers
1561//-------------------------------------------------------------------------
1562// - Spacing()
1563// - Dummy()
1564// - NewLine()
1565// - AlignTextToFramePadding()
1566// - SeparatorEx() [Internal]
1567// - Separator()
1568// - SplitterBehavior() [Internal]
1569// - ShrinkWidths() [Internal]
1570//-------------------------------------------------------------------------
1571
1572void ImGui::Spacing()
1573{
1574 ImGuiWindow* window = GetCurrentWindow();
1575 if (window->SkipItems)
1576 return;
1577 ItemSize(size: ImVec2(0, 0));
1578}
1579
1580void ImGui::Dummy(const ImVec2& size)
1581{
1582 ImGuiWindow* window = GetCurrentWindow();
1583 if (window->SkipItems)
1584 return;
1585
1586 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
1587 ItemSize(size);
1588 ItemAdd(bb, id: 0);
1589}
1590
1591void ImGui::NewLine()
1592{
1593 ImGuiWindow* window = GetCurrentWindow();
1594 if (window->SkipItems)
1595 return;
1596
1597 ImGuiContext& g = *GImGui;
1598 const ImGuiLayoutType backup_layout_type = window->DC.LayoutType;
1599 window->DC.LayoutType = ImGuiLayoutType_Vertical;
1600 window->DC.IsSameLine = false;
1601 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.
1602 ItemSize(size: ImVec2(0, 0));
1603 else
1604 ItemSize(size: ImVec2(0.0f, g.FontSize));
1605 window->DC.LayoutType = backup_layout_type;
1606}
1607
1608void ImGui::AlignTextToFramePadding()
1609{
1610 ImGuiWindow* window = GetCurrentWindow();
1611 if (window->SkipItems)
1612 return;
1613
1614 ImGuiContext& g = *GImGui;
1615 window->DC.CurrLineSize.y = ImMax(lhs: window->DC.CurrLineSize.y, rhs: g.FontSize + g.Style.FramePadding.y * 2);
1616 window->DC.CurrLineTextBaseOffset = ImMax(lhs: window->DC.CurrLineTextBaseOffset, rhs: g.Style.FramePadding.y);
1617}
1618
1619// Horizontal/vertical separating line
1620// FIXME: Surprisingly, this seemingly trivial widget is a victim of many different legacy/tricky layout issues.
1621// Note how thickness == 1.0f is handled specifically as not moving CursorPos by 'thickness', but other values are.
1622void ImGui::SeparatorEx(ImGuiSeparatorFlags flags, float thickness)
1623{
1624 ImGuiWindow* window = GetCurrentWindow();
1625 if (window->SkipItems)
1626 return;
1627
1628 ImGuiContext& g = *GImGui;
1629 IM_ASSERT(ImIsPowerOfTwo(flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical))); // Check that only 1 option is selected
1630 IM_ASSERT(thickness > 0.0f);
1631
1632 if (flags & ImGuiSeparatorFlags_Vertical)
1633 {
1634 // Vertical separator, for menu bars (use current line height).
1635 float y1 = window->DC.CursorPos.y;
1636 float y2 = window->DC.CursorPos.y + window->DC.CurrLineSize.y;
1637 const ImRect bb(ImVec2(window->DC.CursorPos.x, y1), ImVec2(window->DC.CursorPos.x + thickness, y2));
1638 ItemSize(size: ImVec2(thickness, 0.0f));
1639 if (!ItemAdd(bb, id: 0))
1640 return;
1641
1642 // Draw
1643 window->DrawList->AddRectFilled(p_min: bb.Min, p_max: bb.Max, col: GetColorU32(idx: ImGuiCol_Separator));
1644 if (g.LogEnabled)
1645 LogText(fmt: " |");
1646 }
1647 else if (flags & ImGuiSeparatorFlags_Horizontal)
1648 {
1649 // Horizontal Separator
1650 float x1 = window->DC.CursorPos.x;
1651 float x2 = window->WorkRect.Max.x;
1652
1653 // Preserve legacy behavior inside Columns()
1654 // Before Tables API happened, we relied on Separator() to span all columns of a Columns() set.
1655 // We currently don't need to provide the same feature for tables because tables naturally have border features.
1656 ImGuiOldColumns* columns = (flags & ImGuiSeparatorFlags_SpanAllColumns) ? window->DC.CurrentColumns : NULL;
1657 if (columns)
1658 {
1659 x1 = window->Pos.x + window->DC.Indent.x; // Used to be Pos.x before 2023/10/03
1660 x2 = window->Pos.x + window->Size.x;
1661 PushColumnsBackground();
1662 }
1663
1664 // We don't provide our width to the layout so that it doesn't get feed back into AutoFit
1665 // FIXME: This prevents ->CursorMaxPos based bounding box evaluation from working (e.g. TableEndCell)
1666 const float thickness_for_layout = (thickness == 1.0f) ? 0.0f : thickness; // FIXME: See 1.70/1.71 Separator() change: makes legacy 1-px separator not affect layout yet. Should change.
1667 const ImRect bb(ImVec2(x1, window->DC.CursorPos.y), ImVec2(x2, window->DC.CursorPos.y + thickness));
1668 ItemSize(size: ImVec2(0.0f, thickness_for_layout));
1669
1670 if (ItemAdd(bb, id: 0))
1671 {
1672 // Draw
1673 window->DrawList->AddRectFilled(p_min: bb.Min, p_max: bb.Max, col: GetColorU32(idx: ImGuiCol_Separator));
1674 if (g.LogEnabled)
1675 LogRenderedText(ref_pos: &bb.Min, text: "--------------------------------\n");
1676
1677 }
1678 if (columns)
1679 {
1680 PopColumnsBackground();
1681 columns->LineMinY = window->DC.CursorPos.y;
1682 }
1683 }
1684}
1685
1686void ImGui::Separator()
1687{
1688 ImGuiContext& g = *GImGui;
1689 ImGuiWindow* window = g.CurrentWindow;
1690 if (window->SkipItems)
1691 return;
1692
1693 // Those flags should eventually be configurable by the user
1694 // FIXME: We cannot g.Style.SeparatorTextBorderSize for thickness as it relates to SeparatorText() which is a decorated separator, not defaulting to 1.0f.
1695 ImGuiSeparatorFlags flags = (window->DC.LayoutType == ImGuiLayoutType_Horizontal) ? ImGuiSeparatorFlags_Vertical : ImGuiSeparatorFlags_Horizontal;
1696
1697 // Only applies to legacy Columns() api as they relied on Separator() a lot.
1698 if (window->DC.CurrentColumns)
1699 flags |= ImGuiSeparatorFlags_SpanAllColumns;
1700
1701 SeparatorEx(flags, thickness: 1.0f);
1702}
1703
1704void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end, float extra_w)
1705{
1706 ImGuiContext& g = *GImGui;
1707 ImGuiWindow* window = g.CurrentWindow;
1708 ImGuiStyle& style = g.Style;
1709
1710 const ImVec2 label_size = CalcTextSize(text: label, text_end: label_end, hide_text_after_double_hash: false);
1711 const ImVec2 pos = window->DC.CursorPos;
1712 const ImVec2 padding = style.SeparatorTextPadding;
1713
1714 const float separator_thickness = style.SeparatorTextBorderSize;
1715 const ImVec2 min_size(label_size.x + extra_w + padding.x * 2.0f, ImMax(lhs: label_size.y + padding.y * 2.0f, rhs: separator_thickness));
1716 const ImRect bb(pos, ImVec2(window->WorkRect.Max.x, pos.y + min_size.y));
1717 const float text_baseline_y = ImTrunc(f: (bb.GetHeight() - label_size.y) * style.SeparatorTextAlign.y + 0.99999f); //ImMax(padding.y, ImTrunc((style.SeparatorTextSize - label_size.y) * 0.5f));
1718 ItemSize(size: min_size, text_baseline_y);
1719 if (!ItemAdd(bb, id))
1720 return;
1721
1722 const float sep1_x1 = pos.x;
1723 const float sep2_x2 = bb.Max.x;
1724 const float seps_y = ImTrunc(f: (bb.Min.y + bb.Max.y) * 0.5f + 0.99999f);
1725
1726 const float label_avail_w = ImMax(lhs: 0.0f, rhs: sep2_x2 - sep1_x1 - padding.x * 2.0f);
1727 const ImVec2 label_pos(pos.x + padding.x + ImMax(lhs: 0.0f, rhs: (label_avail_w - label_size.x - extra_w) * style.SeparatorTextAlign.x), pos.y + text_baseline_y); // FIXME-ALIGN
1728
1729 // This allows using SameLine() to position something in the 'extra_w'
1730 window->DC.CursorPosPrevLine.x = label_pos.x + label_size.x;
1731
1732 const ImU32 separator_col = GetColorU32(idx: ImGuiCol_Separator);
1733 if (label_size.x > 0.0f)
1734 {
1735 const float sep1_x2 = label_pos.x - style.ItemSpacing.x;
1736 const float sep2_x1 = label_pos.x + label_size.x + extra_w + style.ItemSpacing.x;
1737 if (sep1_x2 > sep1_x1 && separator_thickness > 0.0f)
1738 window->DrawList->AddLine(p1: ImVec2(sep1_x1, seps_y), p2: ImVec2(sep1_x2, seps_y), col: separator_col, thickness: separator_thickness);
1739 if (sep2_x2 > sep2_x1 && separator_thickness > 0.0f)
1740 window->DrawList->AddLine(p1: ImVec2(sep2_x1, seps_y), p2: ImVec2(sep2_x2, seps_y), col: separator_col, thickness: separator_thickness);
1741 if (g.LogEnabled)
1742 LogSetNextTextDecoration(prefix: "---", NULL);
1743 RenderTextEllipsis(draw_list: window->DrawList, pos_min: label_pos, pos_max: ImVec2(bb.Max.x, bb.Max.y + style.ItemSpacing.y), ellipsis_max_x: bb.Max.x, text: label, text_end: label_end, text_size_if_known: &label_size);
1744 }
1745 else
1746 {
1747 if (g.LogEnabled)
1748 LogText(fmt: "---");
1749 if (separator_thickness > 0.0f)
1750 window->DrawList->AddLine(p1: ImVec2(sep1_x1, seps_y), p2: ImVec2(sep2_x2, seps_y), col: separator_col, thickness: separator_thickness);
1751 }
1752}
1753
1754void ImGui::SeparatorText(const char* label)
1755{
1756 ImGuiWindow* window = GetCurrentWindow();
1757 if (window->SkipItems)
1758 return;
1759
1760 // The SeparatorText() vs SeparatorTextEx() distinction is designed to be considerate that we may want:
1761 // - allow separator-text to be draggable items (would require a stable ID + a noticeable highlight)
1762 // - this high-level entry point to allow formatting? (which in turns may require ID separate from formatted string)
1763 // - because of this we probably can't turn 'const char* label' into 'const char* fmt, ...'
1764 // Otherwise, we can decide that users wanting to drag this would layout a dedicated drag-item,
1765 // and then we can turn this into a format function.
1766 SeparatorTextEx(id: 0, label, label_end: FindRenderedTextEnd(text: label), extra_w: 0.0f);
1767}
1768
1769// 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.
1770bool 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)
1771{
1772 ImGuiContext& g = *GImGui;
1773 ImGuiWindow* window = g.CurrentWindow;
1774
1775 if (!ItemAdd(bb, id, NULL, extra_flags: ImGuiItemFlags_NoNav))
1776 return false;
1777
1778 // FIXME: AFAIK the only leftover reason for passing ImGuiButtonFlags_AllowOverlap here is
1779 // to allow caller of SplitterBehavior() to call SetItemAllowOverlap() after the item.
1780 // Nowadays we would instead want to use SetNextItemAllowOverlap() before the item.
1781 ImGuiButtonFlags button_flags = ImGuiButtonFlags_FlattenChildren;
1782#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1783 button_flags |= ImGuiButtonFlags_AllowOverlap;
1784#endif
1785
1786 bool hovered, held;
1787 ImRect bb_interact = bb;
1788 bb_interact.Expand(amount: axis == ImGuiAxis_Y ? ImVec2(0.0f, hover_extend) : ImVec2(hover_extend, 0.0f));
1789 ButtonBehavior(bb: bb_interact, id, out_hovered: &hovered, out_held: &held, flags: button_flags);
1790 if (hovered)
1791 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; // for IsItemHovered(), because bb_interact is larger than bb
1792
1793 if (held || (hovered && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= hover_visibility_delay))
1794 SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW);
1795
1796 ImRect bb_render = bb;
1797 if (held)
1798 {
1799 float mouse_delta = (g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min)[axis];
1800
1801 // Minimum pane size
1802 float size_1_maximum_delta = ImMax(lhs: 0.0f, rhs: *size1 - min_size1);
1803 float size_2_maximum_delta = ImMax(lhs: 0.0f, rhs: *size2 - min_size2);
1804 if (mouse_delta < -size_1_maximum_delta)
1805 mouse_delta = -size_1_maximum_delta;
1806 if (mouse_delta > size_2_maximum_delta)
1807 mouse_delta = size_2_maximum_delta;
1808
1809 // Apply resize
1810 if (mouse_delta != 0.0f)
1811 {
1812 *size1 = ImMax(lhs: *size1 + mouse_delta, rhs: min_size1);
1813 *size2 = ImMax(lhs: *size2 - mouse_delta, rhs: min_size2);
1814 bb_render.Translate(d: (axis == ImGuiAxis_X) ? ImVec2(mouse_delta, 0.0f) : ImVec2(0.0f, mouse_delta));
1815 MarkItemEdited(id);
1816 }
1817 }
1818
1819 // Render at new position
1820 if (bg_col & IM_COL32_A_MASK)
1821 window->DrawList->AddRectFilled(p_min: bb_render.Min, p_max: bb_render.Max, col: bg_col, rounding: 0.0f);
1822 const ImU32 col = GetColorU32(idx: held ? ImGuiCol_SeparatorActive : (hovered && g.HoveredIdTimer >= hover_visibility_delay) ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator);
1823 window->DrawList->AddRectFilled(p_min: bb_render.Min, p_max: bb_render.Max, col, rounding: 0.0f);
1824
1825 return held;
1826}
1827
1828static int IMGUI_CDECL ShrinkWidthItemComparer(const void* lhs, const void* rhs)
1829{
1830 const ImGuiShrinkWidthItem* a = (const ImGuiShrinkWidthItem*)lhs;
1831 const ImGuiShrinkWidthItem* b = (const ImGuiShrinkWidthItem*)rhs;
1832 if (int d = (int)(b->Width - a->Width))
1833 return d;
1834 return (b->Index - a->Index);
1835}
1836
1837// Shrink excess width from a set of item, by removing width from the larger items first.
1838// Set items Width to -1.0f to disable shrinking this item.
1839void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess, float width_min)
1840{
1841 if (count == 1)
1842 {
1843 if (items[0].Width >= 0.0f)
1844 items[0].Width = ImMax(lhs: items[0].Width - width_excess, rhs: width_min);
1845 return;
1846 }
1847 ImQsort(base: items, count: (size_t)count, size_of_element: sizeof(ImGuiShrinkWidthItem), compare_func: ShrinkWidthItemComparer); // Sort largest first, smallest last.
1848 int count_same_width = 1;
1849 while (width_excess > 0.001f && count_same_width < count)
1850 {
1851 while (count_same_width < count && items[0].Width <= items[count_same_width].Width)
1852 count_same_width++;
1853 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);
1854 max_width_to_remove_per_item = ImMin(lhs: items[0].Width - width_min, rhs: max_width_to_remove_per_item);
1855 if (max_width_to_remove_per_item <= 0.0f)
1856 break;
1857 float base_width_to_remove_per_item = ImMin(lhs: width_excess / count_same_width, rhs: max_width_to_remove_per_item);
1858 for (int item_n = 0; item_n < count_same_width; item_n++)
1859 {
1860 float width_to_remove_for_this_item = ImMin(lhs: base_width_to_remove_per_item, rhs: items[item_n].Width - width_min);
1861 items[item_n].Width -= width_to_remove_for_this_item;
1862 width_excess -= width_to_remove_for_this_item;
1863 }
1864 }
1865
1866 // Round width and redistribute remainder
1867 // 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.
1868 width_excess = 0.0f;
1869 for (int n = 0; n < count; n++)
1870 {
1871 float width_rounded = ImTrunc(f: items[n].Width);
1872 width_excess += items[n].Width - width_rounded;
1873 items[n].Width = width_rounded;
1874 }
1875 while (width_excess > 0.0f)
1876 for (int n = 0; n < count && width_excess > 0.0f; n++)
1877 {
1878 float width_to_add = ImMin(lhs: items[n].InitialWidth - items[n].Width, rhs: 1.0f);
1879 items[n].Width += width_to_add;
1880 width_excess -= width_to_add;
1881 }
1882}
1883
1884//-------------------------------------------------------------------------
1885// [SECTION] Widgets: ComboBox
1886//-------------------------------------------------------------------------
1887// - CalcMaxPopupHeightFromItemCount() [Internal]
1888// - BeginCombo()
1889// - BeginComboPopup() [Internal]
1890// - EndCombo()
1891// - BeginComboPreview() [Internal]
1892// - EndComboPreview() [Internal]
1893// - Combo()
1894//-------------------------------------------------------------------------
1895
1896static float CalcMaxPopupHeightFromItemCount(int items_count)
1897{
1898 ImGuiContext& g = *GImGui;
1899 if (items_count <= 0)
1900 return FLT_MAX;
1901 return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2);
1902}
1903
1904bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags)
1905{
1906 ImGuiContext& g = *GImGui;
1907 ImGuiWindow* window = GetCurrentWindow();
1908
1909 ImGuiNextWindowDataFlags backup_next_window_data_flags = g.NextWindowData.HasFlags;
1910 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
1911 if (window->SkipItems)
1912 return false;
1913
1914 const ImGuiStyle& style = g.Style;
1915 const ImGuiID id = window->GetID(str: label);
1916 IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together
1917 if (flags & ImGuiComboFlags_WidthFitPreview)
1918 IM_ASSERT((flags & (ImGuiComboFlags_NoPreview | (ImGuiComboFlags)ImGuiComboFlags_CustomPreview)) == 0);
1919
1920 const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight();
1921 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
1922 const float preview_width = ((flags & ImGuiComboFlags_WidthFitPreview) && (preview_value != NULL)) ? CalcTextSize(text: preview_value, NULL, hide_text_after_double_hash: true).x : 0.0f;
1923 const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : ((flags & ImGuiComboFlags_WidthFitPreview) ? (arrow_size + preview_width + style.FramePadding.x * 2.0f) : CalcItemWidth());
1924 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
1925 const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
1926 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
1927 if (!ItemAdd(bb: total_bb, id, nav_bb: &bb))
1928 return false;
1929
1930 // Open on click
1931 bool hovered, held;
1932 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held);
1933 const ImGuiID popup_id = ImHashStr(data: "##ComboPopup", data_size: 0, seed: id);
1934 bool popup_open = IsPopupOpen(id: popup_id, popup_flags: ImGuiPopupFlags_None);
1935 if (pressed && !popup_open)
1936 {
1937 OpenPopupEx(id: popup_id, popup_flags: ImGuiPopupFlags_None);
1938 popup_open = true;
1939 }
1940
1941 // Render shape
1942 const ImU32 frame_col = GetColorU32(idx: hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
1943 const float value_x2 = ImMax(lhs: bb.Min.x, rhs: bb.Max.x - arrow_size);
1944 RenderNavCursor(bb, id);
1945 if (!(flags & ImGuiComboFlags_NoPreview))
1946 window->DrawList->AddRectFilled(p_min: bb.Min, p_max: ImVec2(value_x2, bb.Max.y), col: frame_col, rounding: style.FrameRounding, flags: (flags & ImGuiComboFlags_NoArrowButton) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersLeft);
1947 if (!(flags & ImGuiComboFlags_NoArrowButton))
1948 {
1949 ImU32 bg_col = GetColorU32(idx: (popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1950 ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
1951 window->DrawList->AddRectFilled(p_min: ImVec2(value_x2, bb.Min.y), p_max: bb.Max, col: bg_col, rounding: style.FrameRounding, flags: (w <= arrow_size) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersRight);
1952 if (value_x2 + arrow_size - style.FramePadding.x <= bb.Max.x)
1953 RenderArrow(draw_list: window->DrawList, pos: ImVec2(value_x2 + style.FramePadding.y, bb.Min.y + style.FramePadding.y), col: text_col, dir: ImGuiDir_Down, scale: 1.0f);
1954 }
1955 RenderFrameBorder(p_min: bb.Min, p_max: bb.Max, rounding: style.FrameRounding);
1956
1957 // Custom preview
1958 if (flags & ImGuiComboFlags_CustomPreview)
1959 {
1960 g.ComboPreviewData.PreviewRect = ImRect(bb.Min.x, bb.Min.y, value_x2, bb.Max.y);
1961 IM_ASSERT(preview_value == NULL || preview_value[0] == 0);
1962 preview_value = NULL;
1963 }
1964
1965 // Render preview and label
1966 if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview))
1967 {
1968 if (g.LogEnabled)
1969 LogSetNextTextDecoration(prefix: "{", suffix: "}");
1970 RenderTextClipped(pos_min: bb.Min + style.FramePadding, pos_max: ImVec2(value_x2, bb.Max.y), text: preview_value, NULL, NULL);
1971 }
1972 if (label_size.x > 0)
1973 RenderText(pos: ImVec2(bb.Max.x + style.ItemInnerSpacing.x, bb.Min.y + style.FramePadding.y), text: label);
1974
1975 if (!popup_open)
1976 return false;
1977
1978 g.NextWindowData.HasFlags = backup_next_window_data_flags;
1979 return BeginComboPopup(popup_id, bb, flags);
1980}
1981
1982bool ImGui::BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags flags)
1983{
1984 ImGuiContext& g = *GImGui;
1985 if (!IsPopupOpen(id: popup_id, popup_flags: ImGuiPopupFlags_None))
1986 {
1987 g.NextWindowData.ClearFlags();
1988 return false;
1989 }
1990
1991 // Set popup size
1992 float w = bb.GetWidth();
1993 if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSizeConstraint)
1994 {
1995 g.NextWindowData.SizeConstraintRect.Min.x = ImMax(lhs: g.NextWindowData.SizeConstraintRect.Min.x, rhs: w);
1996 }
1997 else
1998 {
1999 if ((flags & ImGuiComboFlags_HeightMask_) == 0)
2000 flags |= ImGuiComboFlags_HeightRegular;
2001 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one
2002 int popup_max_height_in_items = -1;
2003 if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8;
2004 else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4;
2005 else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20;
2006 ImVec2 constraint_min(0.0f, 0.0f), constraint_max(FLT_MAX, FLT_MAX);
2007 if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.x <= 0.0f) // Don't apply constraints if user specified a size
2008 constraint_min.x = w;
2009 if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.y <= 0.0f)
2010 constraint_max.y = CalcMaxPopupHeightFromItemCount(items_count: popup_max_height_in_items);
2011 SetNextWindowSizeConstraints(size_min: constraint_min, size_max: constraint_max);
2012 }
2013
2014 // This is essentially a specialized version of BeginPopupEx()
2015 char name[16];
2016 ImFormatString(buf: name, IM_ARRAYSIZE(name), fmt: "##Combo_%02d", g.BeginComboDepth); // Recycle windows based on depth
2017
2018 // Set position given a custom constraint (peak into expected window size so we can position it)
2019 // FIXME: This might be easier to express with an hypothetical SetNextWindowPosConstraints() function?
2020 // FIXME: This might be moved to Begin() or at least around the same spot where Tooltips and other Popups are calling FindBestWindowPosForPopupEx()?
2021 if (ImGuiWindow* popup_window = FindWindowByName(name))
2022 if (popup_window->WasActive)
2023 {
2024 // Always override 'AutoPosLastDirection' to not leave a chance for a past value to affect us.
2025 ImVec2 size_expected = CalcWindowNextAutoFitSize(window: popup_window);
2026 popup_window->AutoPosLastDirection = (flags & ImGuiComboFlags_PopupAlignLeft) ? ImGuiDir_Left : ImGuiDir_Down; // Left = "Below, Toward Left", Down = "Below, Toward Right (default)"
2027 ImRect r_outer = GetPopupAllowedExtentRect(window: popup_window);
2028 ImVec2 pos = FindBestWindowPosForPopupEx(ref_pos: bb.GetBL(), size: size_expected, last_dir: &popup_window->AutoPosLastDirection, r_outer, r_avoid: bb, policy: ImGuiPopupPositionPolicy_ComboBox);
2029 SetNextWindowPos(pos);
2030 }
2031
2032 // We don't use BeginPopupEx() solely because we have a custom name string, which we could make an argument to BeginPopupEx()
2033 ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove;
2034 PushStyleVarX(idx: ImGuiStyleVar_WindowPadding, val_x: g.Style.FramePadding.x); // Horizontally align ourselves with the framed text
2035 bool ret = Begin(name, NULL, flags: window_flags);
2036 PopStyleVar();
2037 if (!ret)
2038 {
2039 EndPopup();
2040 IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above
2041 return false;
2042 }
2043 g.BeginComboDepth++;
2044 return true;
2045}
2046
2047void ImGui::EndCombo()
2048{
2049 ImGuiContext& g = *GImGui;
2050 EndPopup();
2051 g.BeginComboDepth--;
2052}
2053
2054// Call directly after the BeginCombo/EndCombo block. The preview is designed to only host non-interactive elements
2055// (Experimental, see GitHub issues: #1658, #4168)
2056bool ImGui::BeginComboPreview()
2057{
2058 ImGuiContext& g = *GImGui;
2059 ImGuiWindow* window = g.CurrentWindow;
2060 ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
2061
2062 if (window->SkipItems || !(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible))
2063 return false;
2064 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?
2065 if (!window->ClipRect.Overlaps(r: preview_data->PreviewRect)) // Narrower test (optional)
2066 return false;
2067
2068 // FIXME: This could be contained in a PushWorkRect() api
2069 preview_data->BackupCursorPos = window->DC.CursorPos;
2070 preview_data->BackupCursorMaxPos = window->DC.CursorMaxPos;
2071 preview_data->BackupCursorPosPrevLine = window->DC.CursorPosPrevLine;
2072 preview_data->BackupPrevLineTextBaseOffset = window->DC.PrevLineTextBaseOffset;
2073 preview_data->BackupLayout = window->DC.LayoutType;
2074 window->DC.CursorPos = preview_data->PreviewRect.Min + g.Style.FramePadding;
2075 window->DC.CursorMaxPos = window->DC.CursorPos;
2076 window->DC.LayoutType = ImGuiLayoutType_Horizontal;
2077 window->DC.IsSameLine = false;
2078 PushClipRect(clip_rect_min: preview_data->PreviewRect.Min, clip_rect_max: preview_data->PreviewRect.Max, intersect_with_current_clip_rect: true);
2079
2080 return true;
2081}
2082
2083void ImGui::EndComboPreview()
2084{
2085 ImGuiContext& g = *GImGui;
2086 ImGuiWindow* window = g.CurrentWindow;
2087 ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
2088
2089 // FIXME: Using CursorMaxPos approximation instead of correct AABB which we will store in ImDrawCmd in the future
2090 ImDrawList* draw_list = window->DrawList;
2091 if (window->DC.CursorMaxPos.x < preview_data->PreviewRect.Max.x && window->DC.CursorMaxPos.y < preview_data->PreviewRect.Max.y)
2092 if (draw_list->CmdBuffer.Size > 1) // Unlikely case that the PushClipRect() didn't create a command
2093 {
2094 draw_list->_CmdHeader.ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 2].ClipRect;
2095 draw_list->_TryMergeDrawCmds();
2096 }
2097 PopClipRect();
2098 window->DC.CursorPos = preview_data->BackupCursorPos;
2099 window->DC.CursorMaxPos = ImMax(lhs: window->DC.CursorMaxPos, rhs: preview_data->BackupCursorMaxPos);
2100 window->DC.CursorPosPrevLine = preview_data->BackupCursorPosPrevLine;
2101 window->DC.PrevLineTextBaseOffset = preview_data->BackupPrevLineTextBaseOffset;
2102 window->DC.LayoutType = preview_data->BackupLayout;
2103 window->DC.IsSameLine = false;
2104 preview_data->PreviewRect = ImRect();
2105}
2106
2107// Getter for the old Combo() API: const char*[]
2108static const char* Items_ArrayGetter(void* data, int idx)
2109{
2110 const char* const* items = (const char* const*)data;
2111 return items[idx];
2112}
2113
2114// Getter for the old Combo() API: "item1\0item2\0item3\0"
2115static const char* Items_SingleStringGetter(void* data, int idx)
2116{
2117 const char* items_separated_by_zeros = (const char*)data;
2118 int items_count = 0;
2119 const char* p = items_separated_by_zeros;
2120 while (*p)
2121 {
2122 if (idx == items_count)
2123 break;
2124 p += ImStrlen(s: p) + 1;
2125 items_count++;
2126 }
2127 return *p ? p : NULL;
2128}
2129
2130// Old API, prefer using BeginCombo() nowadays if you can.
2131bool 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)
2132{
2133 ImGuiContext& g = *GImGui;
2134
2135 // Call the getter to obtain the preview string which is a parameter to BeginCombo()
2136 const char* preview_value = NULL;
2137 if (*current_item >= 0 && *current_item < items_count)
2138 preview_value = getter(user_data, *current_item);
2139
2140 // 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.
2141 if (popup_max_height_in_items != -1 && !(g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSizeConstraint))
2142 SetNextWindowSizeConstraints(size_min: ImVec2(0, 0), size_max: ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(items_count: popup_max_height_in_items)));
2143
2144 if (!BeginCombo(label, preview_value, flags: ImGuiComboFlags_None))
2145 return false;
2146
2147 // Display items
2148 bool value_changed = false;
2149 ImGuiListClipper clipper;
2150 clipper.Begin(items_count);
2151 clipper.IncludeItemByIndex(item_index: *current_item);
2152 while (clipper.Step())
2153 for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
2154 {
2155 const char* item_text = getter(user_data, i);
2156 if (item_text == NULL)
2157 item_text = "*Unknown item*";
2158
2159 PushID(int_id: i);
2160 const bool item_selected = (i == *current_item);
2161 if (Selectable(label: item_text, selected: item_selected) && *current_item != i)
2162 {
2163 value_changed = true;
2164 *current_item = i;
2165 }
2166 if (item_selected)
2167 SetItemDefaultFocus();
2168 PopID();
2169 }
2170
2171 EndCombo();
2172 if (value_changed)
2173 MarkItemEdited(id: g.LastItemData.ID);
2174
2175 return value_changed;
2176}
2177
2178// Combo box helper allowing to pass an array of strings.
2179bool ImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items)
2180{
2181 const bool value_changed = Combo(label, current_item, getter: Items_ArrayGetter, user_data: (void*)items, items_count, popup_max_height_in_items: height_in_items);
2182 return value_changed;
2183}
2184
2185// Combo box helper allowing to pass all items in a single string literal holding multiple zero-terminated items "item1\0item2\0"
2186bool ImGui::Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int height_in_items)
2187{
2188 int items_count = 0;
2189 const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open
2190 while (*p)
2191 {
2192 p += ImStrlen(s: p) + 1;
2193 items_count++;
2194 }
2195 bool value_changed = Combo(label, current_item, getter: Items_SingleStringGetter, user_data: (void*)items_separated_by_zeros, items_count, popup_max_height_in_items: height_in_items);
2196 return value_changed;
2197}
2198
2199#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
2200
2201struct ImGuiGetNameFromIndexOldToNewCallbackData { void* UserData; bool (*OldCallback)(void*, int, const char**); };
2202static const char* ImGuiGetNameFromIndexOldToNewCallback(void* user_data, int idx)
2203{
2204 ImGuiGetNameFromIndexOldToNewCallbackData* data = (ImGuiGetNameFromIndexOldToNewCallbackData*)user_data;
2205 const char* s = NULL;
2206 data->OldCallback(data->UserData, idx, &s);
2207 return s;
2208}
2209
2210bool ImGui::ListBox(const char* label, int* current_item, bool (*old_getter)(void*, int, const char**), void* user_data, int items_count, int height_in_items)
2211{
2212 ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { .UserData: user_data, .OldCallback: old_getter };
2213 return ListBox(label, current_item, getter: ImGuiGetNameFromIndexOldToNewCallback, user_data: &old_to_new_data, items_count, height_in_items);
2214}
2215bool ImGui::Combo(const char* label, int* current_item, bool (*old_getter)(void*, int, const char**), void* user_data, int items_count, int popup_max_height_in_items)
2216{
2217 ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { .UserData: user_data, .OldCallback: old_getter };
2218 return Combo(label, current_item, getter: ImGuiGetNameFromIndexOldToNewCallback, user_data: &old_to_new_data, items_count, popup_max_height_in_items);
2219}
2220
2221#endif
2222
2223//-------------------------------------------------------------------------
2224// [SECTION] Data Type and Data Formatting Helpers [Internal]
2225//-------------------------------------------------------------------------
2226// - DataTypeGetInfo()
2227// - DataTypeFormatString()
2228// - DataTypeApplyOp()
2229// - DataTypeApplyFromText()
2230// - DataTypeCompare()
2231// - DataTypeClamp()
2232// - GetMinimumStepAtDecimalPrecision
2233// - RoundScalarWithFormat<>()
2234//-------------------------------------------------------------------------
2235
2236static const ImGuiDataTypeInfo GDataTypeInfo[] =
2237{
2238 { .Size: sizeof(char), .Name: "S8", .PrintFmt: "%d", .ScanFmt: "%d" }, // ImGuiDataType_S8
2239 { .Size: sizeof(unsigned char), .Name: "U8", .PrintFmt: "%u", .ScanFmt: "%u" },
2240 { .Size: sizeof(short), .Name: "S16", .PrintFmt: "%d", .ScanFmt: "%d" }, // ImGuiDataType_S16
2241 { .Size: sizeof(unsigned short), .Name: "U16", .PrintFmt: "%u", .ScanFmt: "%u" },
2242 { .Size: sizeof(int), .Name: "S32", .PrintFmt: "%d", .ScanFmt: "%d" }, // ImGuiDataType_S32
2243 { .Size: sizeof(unsigned int), .Name: "U32", .PrintFmt: "%u", .ScanFmt: "%u" },
2244#ifdef _MSC_VER
2245 { sizeof(ImS64), "S64", "%I64d","%I64d" }, // ImGuiDataType_S64
2246 { sizeof(ImU64), "U64", "%I64u","%I64u" },
2247#else
2248 { .Size: sizeof(ImS64), .Name: "S64", .PrintFmt: "%lld", .ScanFmt: "%lld" }, // ImGuiDataType_S64
2249 { .Size: sizeof(ImU64), .Name: "U64", .PrintFmt: "%llu", .ScanFmt: "%llu" },
2250#endif
2251 { .Size: sizeof(float), .Name: "float", .PrintFmt: "%.3f",.ScanFmt: "%f" }, // ImGuiDataType_Float (float are promoted to double in va_arg)
2252 { .Size: sizeof(double), .Name: "double",.PrintFmt: "%f", .ScanFmt: "%lf" }, // ImGuiDataType_Double
2253 { .Size: sizeof(bool), .Name: "bool", .PrintFmt: "%d", .ScanFmt: "%d" }, // ImGuiDataType_Bool
2254 { .Size: 0, .Name: "char*",.PrintFmt: "%s", .ScanFmt: "%s" }, // ImGuiDataType_String
2255};
2256IM_STATIC_ASSERT(IM_ARRAYSIZE(GDataTypeInfo) == ImGuiDataType_COUNT);
2257
2258const ImGuiDataTypeInfo* ImGui::DataTypeGetInfo(ImGuiDataType data_type)
2259{
2260 IM_ASSERT(data_type >= 0 && data_type < ImGuiDataType_COUNT);
2261 return &GDataTypeInfo[data_type];
2262}
2263
2264int ImGui::DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* p_data, const char* format)
2265{
2266 // Signedness doesn't matter when pushing integer arguments
2267 if (data_type == ImGuiDataType_S32 || data_type == ImGuiDataType_U32)
2268 return ImFormatString(buf, buf_size, fmt: format, *(const ImU32*)p_data);
2269 if (data_type == ImGuiDataType_S64 || data_type == ImGuiDataType_U64)
2270 return ImFormatString(buf, buf_size, fmt: format, *(const ImU64*)p_data);
2271 if (data_type == ImGuiDataType_Float)
2272 return ImFormatString(buf, buf_size, fmt: format, *(const float*)p_data);
2273 if (data_type == ImGuiDataType_Double)
2274 return ImFormatString(buf, buf_size, fmt: format, *(const double*)p_data);
2275 if (data_type == ImGuiDataType_S8)
2276 return ImFormatString(buf, buf_size, fmt: format, *(const ImS8*)p_data);
2277 if (data_type == ImGuiDataType_U8)
2278 return ImFormatString(buf, buf_size, fmt: format, *(const ImU8*)p_data);
2279 if (data_type == ImGuiDataType_S16)
2280 return ImFormatString(buf, buf_size, fmt: format, *(const ImS16*)p_data);
2281 if (data_type == ImGuiDataType_U16)
2282 return ImFormatString(buf, buf_size, fmt: format, *(const ImU16*)p_data);
2283 IM_ASSERT(0);
2284 return 0;
2285}
2286
2287void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg1, const void* arg2)
2288{
2289 IM_ASSERT(op == '+' || op == '-');
2290 switch (data_type)
2291 {
2292 case ImGuiDataType_S8:
2293 if (op == '+') { *(ImS8*)output = ImAddClampOverflow(a: *(const ImS8*)arg1, b: *(const ImS8*)arg2, mn: IM_S8_MIN, mx: IM_S8_MAX); }
2294 if (op == '-') { *(ImS8*)output = ImSubClampOverflow(a: *(const ImS8*)arg1, b: *(const ImS8*)arg2, mn: IM_S8_MIN, mx: IM_S8_MAX); }
2295 return;
2296 case ImGuiDataType_U8:
2297 if (op == '+') { *(ImU8*)output = ImAddClampOverflow(a: *(const ImU8*)arg1, b: *(const ImU8*)arg2, mn: IM_U8_MIN, mx: IM_U8_MAX); }
2298 if (op == '-') { *(ImU8*)output = ImSubClampOverflow(a: *(const ImU8*)arg1, b: *(const ImU8*)arg2, mn: IM_U8_MIN, mx: IM_U8_MAX); }
2299 return;
2300 case ImGuiDataType_S16:
2301 if (op == '+') { *(ImS16*)output = ImAddClampOverflow(a: *(const ImS16*)arg1, b: *(const ImS16*)arg2, mn: IM_S16_MIN, mx: IM_S16_MAX); }
2302 if (op == '-') { *(ImS16*)output = ImSubClampOverflow(a: *(const ImS16*)arg1, b: *(const ImS16*)arg2, mn: IM_S16_MIN, mx: IM_S16_MAX); }
2303 return;
2304 case ImGuiDataType_U16:
2305 if (op == '+') { *(ImU16*)output = ImAddClampOverflow(a: *(const ImU16*)arg1, b: *(const ImU16*)arg2, mn: IM_U16_MIN, mx: IM_U16_MAX); }
2306 if (op == '-') { *(ImU16*)output = ImSubClampOverflow(a: *(const ImU16*)arg1, b: *(const ImU16*)arg2, mn: IM_U16_MIN, mx: IM_U16_MAX); }
2307 return;
2308 case ImGuiDataType_S32:
2309 if (op == '+') { *(ImS32*)output = ImAddClampOverflow(a: *(const ImS32*)arg1, b: *(const ImS32*)arg2, mn: IM_S32_MIN, mx: IM_S32_MAX); }
2310 if (op == '-') { *(ImS32*)output = ImSubClampOverflow(a: *(const ImS32*)arg1, b: *(const ImS32*)arg2, mn: IM_S32_MIN, mx: IM_S32_MAX); }
2311 return;
2312 case ImGuiDataType_U32:
2313 if (op == '+') { *(ImU32*)output = ImAddClampOverflow(a: *(const ImU32*)arg1, b: *(const ImU32*)arg2, mn: IM_U32_MIN, mx: IM_U32_MAX); }
2314 if (op == '-') { *(ImU32*)output = ImSubClampOverflow(a: *(const ImU32*)arg1, b: *(const ImU32*)arg2, mn: IM_U32_MIN, mx: IM_U32_MAX); }
2315 return;
2316 case ImGuiDataType_S64:
2317 if (op == '+') { *(ImS64*)output = ImAddClampOverflow(a: *(const ImS64*)arg1, b: *(const ImS64*)arg2, mn: IM_S64_MIN, mx: IM_S64_MAX); }
2318 if (op == '-') { *(ImS64*)output = ImSubClampOverflow(a: *(const ImS64*)arg1, b: *(const ImS64*)arg2, mn: IM_S64_MIN, mx: IM_S64_MAX); }
2319 return;
2320 case ImGuiDataType_U64:
2321 if (op == '+') { *(ImU64*)output = ImAddClampOverflow(a: *(const ImU64*)arg1, b: *(const ImU64*)arg2, mn: IM_U64_MIN, mx: IM_U64_MAX); }
2322 if (op == '-') { *(ImU64*)output = ImSubClampOverflow(a: *(const ImU64*)arg1, b: *(const ImU64*)arg2, mn: IM_U64_MIN, mx: IM_U64_MAX); }
2323 return;
2324 case ImGuiDataType_Float:
2325 if (op == '+') { *(float*)output = *(const float*)arg1 + *(const float*)arg2; }
2326 if (op == '-') { *(float*)output = *(const float*)arg1 - *(const float*)arg2; }
2327 return;
2328 case ImGuiDataType_Double:
2329 if (op == '+') { *(double*)output = *(const double*)arg1 + *(const double*)arg2; }
2330 if (op == '-') { *(double*)output = *(const double*)arg1 - *(const double*)arg2; }
2331 return;
2332 case ImGuiDataType_COUNT: break;
2333 }
2334 IM_ASSERT(0);
2335}
2336
2337// User can input math operators (e.g. +100) to edit a numerical values.
2338// NB: This is _not_ a full expression evaluator. We should probably add one and replace this dumb mess..
2339bool ImGui::DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format, void* p_data_when_empty)
2340{
2341 // Copy the value in an opaque buffer so we can compare at the end of the function if it changed at all.
2342 const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
2343 ImGuiDataTypeStorage data_backup;
2344 memcpy(dest: &data_backup, src: p_data, n: type_info->Size);
2345
2346 while (ImCharIsBlankA(c: *buf))
2347 buf++;
2348 if (!buf[0])
2349 {
2350 if (p_data_when_empty != NULL)
2351 {
2352 memcpy(dest: p_data, src: p_data_when_empty, n: type_info->Size);
2353 return memcmp(s1: &data_backup, s2: p_data, n: type_info->Size) != 0;
2354 }
2355 return false;
2356 }
2357
2358 // Sanitize format
2359 // - 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
2360 // - In theory could treat empty format as using default, but this would only cover rare/bizarre case of using InputScalar() + integer + format string without %.
2361 char format_sanitized[32];
2362 if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double)
2363 format = type_info->ScanFmt;
2364 else
2365 format = ImParseFormatSanitizeForScanning(fmt_in: format, fmt_out: format_sanitized, IM_ARRAYSIZE(format_sanitized));
2366
2367 // Small types need a 32-bit buffer to receive the result from scanf()
2368 int v32 = 0;
2369 if (sscanf(s: buf, format: format, type_info->Size >= 4 ? p_data : &v32) < 1)
2370 return false;
2371 if (type_info->Size < 4)
2372 {
2373 if (data_type == ImGuiDataType_S8)
2374 *(ImS8*)p_data = (ImS8)ImClamp(v: v32, mn: (int)IM_S8_MIN, mx: (int)IM_S8_MAX);
2375 else if (data_type == ImGuiDataType_U8)
2376 *(ImU8*)p_data = (ImU8)ImClamp(v: v32, mn: (int)IM_U8_MIN, mx: (int)IM_U8_MAX);
2377 else if (data_type == ImGuiDataType_S16)
2378 *(ImS16*)p_data = (ImS16)ImClamp(v: v32, mn: (int)IM_S16_MIN, mx: (int)IM_S16_MAX);
2379 else if (data_type == ImGuiDataType_U16)
2380 *(ImU16*)p_data = (ImU16)ImClamp(v: v32, mn: (int)IM_U16_MIN, mx: (int)IM_U16_MAX);
2381 else
2382 IM_ASSERT(0);
2383 }
2384
2385 return memcmp(s1: &data_backup, s2: p_data, n: type_info->Size) != 0;
2386}
2387
2388template<typename T>
2389static int DataTypeCompareT(const T* lhs, const T* rhs)
2390{
2391 if (*lhs < *rhs) return -1;
2392 if (*lhs > *rhs) return +1;
2393 return 0;
2394}
2395
2396int ImGui::DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2)
2397{
2398 switch (data_type)
2399 {
2400 case ImGuiDataType_S8: return DataTypeCompareT<ImS8 >(lhs: (const ImS8* )arg_1, rhs: (const ImS8* )arg_2);
2401 case ImGuiDataType_U8: return DataTypeCompareT<ImU8 >(lhs: (const ImU8* )arg_1, rhs: (const ImU8* )arg_2);
2402 case ImGuiDataType_S16: return DataTypeCompareT<ImS16 >(lhs: (const ImS16* )arg_1, rhs: (const ImS16* )arg_2);
2403 case ImGuiDataType_U16: return DataTypeCompareT<ImU16 >(lhs: (const ImU16* )arg_1, rhs: (const ImU16* )arg_2);
2404 case ImGuiDataType_S32: return DataTypeCompareT<ImS32 >(lhs: (const ImS32* )arg_1, rhs: (const ImS32* )arg_2);
2405 case ImGuiDataType_U32: return DataTypeCompareT<ImU32 >(lhs: (const ImU32* )arg_1, rhs: (const ImU32* )arg_2);
2406 case ImGuiDataType_S64: return DataTypeCompareT<ImS64 >(lhs: (const ImS64* )arg_1, rhs: (const ImS64* )arg_2);
2407 case ImGuiDataType_U64: return DataTypeCompareT<ImU64 >(lhs: (const ImU64* )arg_1, rhs: (const ImU64* )arg_2);
2408 case ImGuiDataType_Float: return DataTypeCompareT<float >(lhs: (const float* )arg_1, rhs: (const float* )arg_2);
2409 case ImGuiDataType_Double: return DataTypeCompareT<double>(lhs: (const double*)arg_1, rhs: (const double*)arg_2);
2410 case ImGuiDataType_COUNT: break;
2411 }
2412 IM_ASSERT(0);
2413 return 0;
2414}
2415
2416template<typename T>
2417static bool DataTypeClampT(T* v, const T* v_min, const T* v_max)
2418{
2419 // Clamp, both sides are optional, return true if modified
2420 if (v_min && *v < *v_min) { *v = *v_min; return true; }
2421 if (v_max && *v > *v_max) { *v = *v_max; return true; }
2422 return false;
2423}
2424
2425bool ImGui::DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max)
2426{
2427 switch (data_type)
2428 {
2429 case ImGuiDataType_S8: return DataTypeClampT<ImS8 >(v: (ImS8* )p_data, v_min: (const ImS8* )p_min, v_max: (const ImS8* )p_max);
2430 case ImGuiDataType_U8: return DataTypeClampT<ImU8 >(v: (ImU8* )p_data, v_min: (const ImU8* )p_min, v_max: (const ImU8* )p_max);
2431 case ImGuiDataType_S16: return DataTypeClampT<ImS16 >(v: (ImS16* )p_data, v_min: (const ImS16* )p_min, v_max: (const ImS16* )p_max);
2432 case ImGuiDataType_U16: return DataTypeClampT<ImU16 >(v: (ImU16* )p_data, v_min: (const ImU16* )p_min, v_max: (const ImU16* )p_max);
2433 case ImGuiDataType_S32: return DataTypeClampT<ImS32 >(v: (ImS32* )p_data, v_min: (const ImS32* )p_min, v_max: (const ImS32* )p_max);
2434 case ImGuiDataType_U32: return DataTypeClampT<ImU32 >(v: (ImU32* )p_data, v_min: (const ImU32* )p_min, v_max: (const ImU32* )p_max);
2435 case ImGuiDataType_S64: return DataTypeClampT<ImS64 >(v: (ImS64* )p_data, v_min: (const ImS64* )p_min, v_max: (const ImS64* )p_max);
2436 case ImGuiDataType_U64: return DataTypeClampT<ImU64 >(v: (ImU64* )p_data, v_min: (const ImU64* )p_min, v_max: (const ImU64* )p_max);
2437 case ImGuiDataType_Float: return DataTypeClampT<float >(v: (float* )p_data, v_min: (const float* )p_min, v_max: (const float* )p_max);
2438 case ImGuiDataType_Double: return DataTypeClampT<double>(v: (double*)p_data, v_min: (const double*)p_min, v_max: (const double*)p_max);
2439 case ImGuiDataType_COUNT: break;
2440 }
2441 IM_ASSERT(0);
2442 return false;
2443}
2444
2445bool ImGui::DataTypeIsZero(ImGuiDataType data_type, const void* p_data)
2446{
2447 ImGuiContext& g = *GImGui;
2448 return DataTypeCompare(data_type, arg_1: p_data, arg_2: &g.DataTypeZeroValue) == 0;
2449}
2450
2451static float GetMinimumStepAtDecimalPrecision(int decimal_precision)
2452{
2453 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 };
2454 if (decimal_precision < 0)
2455 return FLT_MIN;
2456 return (decimal_precision < IM_ARRAYSIZE(min_steps)) ? min_steps[decimal_precision] : ImPow(x: 10.0f, y: (float)-decimal_precision);
2457}
2458
2459template<typename TYPE>
2460TYPE ImGui::RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, TYPE v)
2461{
2462 IM_UNUSED(data_type);
2463 IM_ASSERT(data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double);
2464 const char* fmt_start = ImParseFormatFindStart(format);
2465 if (fmt_start[0] != '%' || fmt_start[1] == '%') // Don't apply if the value is not visible in the format string
2466 return v;
2467
2468 // Sanitize format
2469 char fmt_sanitized[32];
2470 ImParseFormatSanitizeForPrinting(fmt_in: fmt_start, fmt_out: fmt_sanitized, IM_ARRAYSIZE(fmt_sanitized));
2471 fmt_start = fmt_sanitized;
2472
2473 // Format value with our rounding, and read back
2474 char v_str[64];
2475 ImFormatString(v_str, IM_ARRAYSIZE(v_str), fmt_start, v);
2476 const char* p = v_str;
2477 while (*p == ' ')
2478 p++;
2479 v = (TYPE)ImAtof(p);
2480
2481 return v;
2482}
2483
2484//-------------------------------------------------------------------------
2485// [SECTION] Widgets: DragScalar, DragFloat, DragInt, etc.
2486//-------------------------------------------------------------------------
2487// - DragBehaviorT<>() [Internal]
2488// - DragBehavior() [Internal]
2489// - DragScalar()
2490// - DragScalarN()
2491// - DragFloat()
2492// - DragFloat2()
2493// - DragFloat3()
2494// - DragFloat4()
2495// - DragFloatRange2()
2496// - DragInt()
2497// - DragInt2()
2498// - DragInt3()
2499// - DragInt4()
2500// - DragIntRange2()
2501//-------------------------------------------------------------------------
2502
2503// This is called by DragBehavior() when the widget is active (held by mouse or being manipulated with Nav controls)
2504template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2505bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags)
2506{
2507 ImGuiContext& g = *GImGui;
2508 const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
2509 const bool is_bounded = (v_min < v_max) || ((v_min == v_max) && (v_min != 0.0f || (flags & ImGuiSliderFlags_ClampZeroRange)));
2510 const bool is_wrapped = is_bounded && (flags & ImGuiSliderFlags_WrapAround);
2511 const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
2512 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2513
2514 // Default tweak speed
2515 if (v_speed == 0.0f && is_bounded && (v_max - v_min < FLT_MAX))
2516 v_speed = (float)((v_max - v_min) * g.DragSpeedDefaultRatio);
2517
2518 // Inputs accumulates into g.DragCurrentAccum, which is flushed into the current value as soon as it makes a difference with our precision settings
2519 float adjust_delta = 0.0f;
2520 if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid() && IsMouseDragPastThreshold(button: 0, lock_threshold: g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2521 {
2522 adjust_delta = g.IO.MouseDelta[axis];
2523 if (g.IO.KeyAlt && !(flags & ImGuiSliderFlags_NoSpeedTweaks))
2524 adjust_delta *= 1.0f / 100.0f;
2525 if (g.IO.KeyShift && !(flags & ImGuiSliderFlags_NoSpeedTweaks))
2526 adjust_delta *= 10.0f;
2527 }
2528 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
2529 {
2530 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, default_value: 3) : 0;
2531 const bool tweak_slow = IsKeyDown(key: (g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
2532 const bool tweak_fast = IsKeyDown(key: (g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
2533 const float tweak_factor = (flags & ImGuiSliderFlags_NoSpeedTweaks) ? 1.0f : tweak_slow ? 1.0f / 10.0f : tweak_fast ? 10.0f : 1.0f;
2534 adjust_delta = GetNavTweakPressedAmount(axis) * tweak_factor;
2535 v_speed = ImMax(lhs: v_speed, rhs: GetMinimumStepAtDecimalPrecision(decimal_precision));
2536 }
2537 adjust_delta *= v_speed;
2538
2539 // For vertical drag we currently assume that Up=higher value (like we do with vertical sliders). This may become a parameter.
2540 if (axis == ImGuiAxis_Y)
2541 adjust_delta = -adjust_delta;
2542
2543 // For logarithmic use our range is effectively 0..1 so scale the delta into that range
2544 if (is_logarithmic && (v_max - v_min < FLT_MAX) && ((v_max - v_min) > 0.000001f)) // Epsilon to avoid /0
2545 adjust_delta /= (float)(v_max - v_min);
2546
2547 // Clear current value on activation
2548 // 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.
2549 const bool is_just_activated = g.ActiveIdIsJustActivated;
2550 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));
2551 if (is_just_activated || is_already_past_limits_and_pushing_outward)
2552 {
2553 g.DragCurrentAccum = 0.0f;
2554 g.DragCurrentAccumDirty = false;
2555 }
2556 else if (adjust_delta != 0.0f)
2557 {
2558 g.DragCurrentAccum += adjust_delta;
2559 g.DragCurrentAccumDirty = true;
2560 }
2561
2562 if (!g.DragCurrentAccumDirty)
2563 return false;
2564
2565 TYPE v_cur = *v;
2566 FLOATTYPE v_old_ref_for_accum_remainder = (FLOATTYPE)0.0f;
2567
2568 float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
2569 const float zero_deadzone_halfsize = 0.0f; // Drag widgets have no deadzone (as it doesn't make sense)
2570 if (is_logarithmic)
2571 {
2572 // 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.
2573 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, default_value: 3) : 1;
2574 logarithmic_zero_epsilon = ImPow(x: 0.1f, y: (float)decimal_precision);
2575
2576 // Convert to parametric space, apply delta, convert back
2577 float v_old_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2578 float v_new_parametric = v_old_parametric + g.DragCurrentAccum;
2579 v_cur = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new_parametric, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2580 v_old_ref_for_accum_remainder = v_old_parametric;
2581 }
2582 else
2583 {
2584 v_cur += (SIGNEDTYPE)g.DragCurrentAccum;
2585 }
2586
2587 // Round to user desired precision based on format string
2588 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
2589 v_cur = RoundScalarWithFormatT<TYPE>(format, data_type, v_cur);
2590
2591 // Preserve remainder after rounding has been applied. This also allow slow tweaking of values.
2592 g.DragCurrentAccumDirty = false;
2593 if (is_logarithmic)
2594 {
2595 // Convert to parametric space, apply delta, convert back
2596 float v_new_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2597 g.DragCurrentAccum -= (float)(v_new_parametric - v_old_ref_for_accum_remainder);
2598 }
2599 else
2600 {
2601 g.DragCurrentAccum -= (float)((SIGNEDTYPE)v_cur - (SIGNEDTYPE)*v);
2602 }
2603
2604 // Lose zero sign for float/double
2605 if (v_cur == (TYPE)-0)
2606 v_cur = (TYPE)0;
2607
2608 if (*v != v_cur && is_bounded)
2609 {
2610 if (is_wrapped)
2611 {
2612 // Wrap values
2613 if (v_cur < v_min)
2614 v_cur += v_max - v_min + (is_floating_point ? 0 : 1);
2615 if (v_cur > v_max)
2616 v_cur -= v_max - v_min + (is_floating_point ? 0 : 1);
2617 }
2618 else
2619 {
2620 // Clamp values + handle overflow/wrap-around for integer types.
2621 if (v_cur < v_min || (v_cur > *v && adjust_delta < 0.0f && !is_floating_point))
2622 v_cur = v_min;
2623 if (v_cur > v_max || (v_cur < *v && adjust_delta > 0.0f && !is_floating_point))
2624 v_cur = v_max;
2625 }
2626 }
2627
2628 // Apply result
2629 if (*v == v_cur)
2630 return false;
2631 *v = v_cur;
2632 return true;
2633}
2634
2635bool 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)
2636{
2637 // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
2638 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.");
2639
2640 ImGuiContext& g = *GImGui;
2641 if (g.ActiveId == id)
2642 {
2643 // Those are the things we can do easily outside the DragBehaviorT<> template, saves code generation.
2644 if (g.ActiveIdSource == ImGuiInputSource_Mouse && !g.IO.MouseDown[0])
2645 ClearActiveID();
2646 else if ((g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) && g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
2647 ClearActiveID();
2648 }
2649 if (g.ActiveId != id)
2650 return false;
2651 if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
2652 return false;
2653
2654 switch (data_type)
2655 {
2656 case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = DragBehaviorT<ImS32, ImS32, float>(data_type: ImGuiDataType_S32, v: &v32, v_speed, v_min: p_min ? *(const ImS8*) p_min : IM_S8_MIN, v_max: p_max ? *(const ImS8*)p_max : IM_S8_MAX, format, flags); if (r) *(ImS8*)p_v = (ImS8)v32; return r; }
2657 case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)p_v; bool r = DragBehaviorT<ImU32, ImS32, float>(data_type: ImGuiDataType_U32, v: &v32, v_speed, v_min: p_min ? *(const ImU8*) p_min : IM_U8_MIN, v_max: p_max ? *(const ImU8*)p_max : IM_U8_MAX, format, flags); if (r) *(ImU8*)p_v = (ImU8)v32; return r; }
2658 case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)p_v; bool r = DragBehaviorT<ImS32, ImS32, float>(data_type: ImGuiDataType_S32, v: &v32, v_speed, v_min: p_min ? *(const ImS16*)p_min : IM_S16_MIN, v_max: p_max ? *(const ImS16*)p_max : IM_S16_MAX, format, flags); if (r) *(ImS16*)p_v = (ImS16)v32; return r; }
2659 case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)p_v; bool r = DragBehaviorT<ImU32, ImS32, float>(data_type: ImGuiDataType_U32, v: &v32, v_speed, v_min: p_min ? *(const ImU16*)p_min : IM_U16_MIN, v_max: p_max ? *(const ImU16*)p_max : IM_U16_MAX, format, flags); if (r) *(ImU16*)p_v = (ImU16)v32; return r; }
2660 case ImGuiDataType_S32: return DragBehaviorT<ImS32, ImS32, float >(data_type, v: (ImS32*)p_v, v_speed, v_min: p_min ? *(const ImS32* )p_min : IM_S32_MIN, v_max: p_max ? *(const ImS32* )p_max : IM_S32_MAX, format, flags);
2661 case ImGuiDataType_U32: return DragBehaviorT<ImU32, ImS32, float >(data_type, v: (ImU32*)p_v, v_speed, v_min: p_min ? *(const ImU32* )p_min : IM_U32_MIN, v_max: p_max ? *(const ImU32* )p_max : IM_U32_MAX, format, flags);
2662 case ImGuiDataType_S64: return DragBehaviorT<ImS64, ImS64, double>(data_type, v: (ImS64*)p_v, v_speed, v_min: p_min ? *(const ImS64* )p_min : IM_S64_MIN, v_max: p_max ? *(const ImS64* )p_max : IM_S64_MAX, format, flags);
2663 case ImGuiDataType_U64: return DragBehaviorT<ImU64, ImS64, double>(data_type, v: (ImU64*)p_v, v_speed, v_min: p_min ? *(const ImU64* )p_min : IM_U64_MIN, v_max: p_max ? *(const ImU64* )p_max : IM_U64_MAX, format, flags);
2664 case ImGuiDataType_Float: return DragBehaviorT<float, float, float >(data_type, v: (float*)p_v, v_speed, v_min: p_min ? *(const float* )p_min : -FLT_MAX, v_max: p_max ? *(const float* )p_max : FLT_MAX, format, flags);
2665 case ImGuiDataType_Double: return DragBehaviorT<double,double,double>(data_type, v: (double*)p_v, v_speed, v_min: p_min ? *(const double*)p_min : -DBL_MAX, v_max: p_max ? *(const double*)p_max : DBL_MAX, format, flags);
2666 case ImGuiDataType_COUNT: break;
2667 }
2668 IM_ASSERT(0);
2669 return false;
2670}
2671
2672// 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.
2673// Read code of e.g. DragFloat(), DragInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
2674bool 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)
2675{
2676 ImGuiWindow* window = GetCurrentWindow();
2677 if (window->SkipItems)
2678 return false;
2679
2680 ImGuiContext& g = *GImGui;
2681 const ImGuiStyle& style = g.Style;
2682 const ImGuiID id = window->GetID(str: label);
2683 const float w = CalcItemWidth();
2684
2685 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
2686 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
2687 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));
2688
2689 const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
2690 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
2691 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
2692 return false;
2693
2694 // Default format string when passing NULL
2695 if (format == NULL)
2696 format = DataTypeGetInfo(data_type)->PrintFmt;
2697
2698 const bool hovered = ItemHoverable(bb: frame_bb, id, item_flags: g.LastItemData.ItemFlags);
2699 bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
2700 if (!temp_input_is_active)
2701 {
2702 // Tabbing or CTRL+click on Drag turns it into an InputText
2703 const bool clicked = hovered && IsMouseClicked(button: 0, flags: ImGuiInputFlags_None, owner_id: id);
2704 const bool double_clicked = (hovered && g.IO.MouseClickedCount[0] == 2 && TestKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id));
2705 const bool make_active = (clicked || double_clicked || g.NavActivateId == id);
2706 if (make_active && (clicked || double_clicked))
2707 SetKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id);
2708 if (make_active && temp_input_allowed)
2709 if ((clicked && g.IO.KeyCtrl) || double_clicked || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
2710 temp_input_is_active = true;
2711
2712 // (Optional) simple click (without moving) turns Drag into an InputText
2713 if (g.IO.ConfigDragClickToInputText && temp_input_allowed && !temp_input_is_active)
2714 if (g.ActiveId == id && hovered && g.IO.MouseReleased[0] && !IsMouseDragPastThreshold(button: 0, lock_threshold: g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2715 {
2716 g.NavActivateId = id;
2717 g.NavActivateFlags = ImGuiActivateFlags_PreferInput;
2718 temp_input_is_active = true;
2719 }
2720
2721 // Store initial value (not used by main lib but available as a convenience but some mods e.g. to revert)
2722 if (make_active)
2723 memcpy(dest: &g.ActiveIdValueOnActivation, src: p_data, n: DataTypeGetInfo(data_type)->Size);
2724
2725 if (make_active && !temp_input_is_active)
2726 {
2727 SetActiveID(id, window);
2728 SetFocusID(id, window);
2729 FocusWindow(window);
2730 g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
2731 }
2732 }
2733
2734 if (temp_input_is_active)
2735 {
2736 // Only clamp CTRL+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp)
2737 bool clamp_enabled = false;
2738 if ((flags & ImGuiSliderFlags_ClampOnInput) && (p_min != NULL || p_max != NULL))
2739 {
2740 const int clamp_range_dir = (p_min != NULL && p_max != NULL) ? DataTypeCompare(data_type, arg_1: p_min, arg_2: p_max) : 0; // -1 when *p_min < *p_max, == 0 when *p_min == *p_max
2741 if (p_min == NULL || p_max == NULL || clamp_range_dir < 0)
2742 clamp_enabled = true;
2743 else if (clamp_range_dir == 0)
2744 clamp_enabled = DataTypeIsZero(data_type, p_data: p_min) ? ((flags & ImGuiSliderFlags_ClampZeroRange) != 0) : true;
2745 }
2746 return TempInputScalar(bb: frame_bb, id, label, data_type, p_data, format, p_clamp_min: clamp_enabled ? p_min : NULL, p_clamp_max: clamp_enabled ? p_max : NULL);
2747 }
2748
2749 // Draw frame
2750 const ImU32 frame_col = GetColorU32(idx: g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
2751 RenderNavCursor(bb: frame_bb, id);
2752 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: frame_col, borders: true, rounding: style.FrameRounding);
2753
2754 // Drag behavior
2755 const bool value_changed = DragBehavior(id, data_type, p_v: p_data, v_speed, p_min, p_max, format, flags);
2756 if (value_changed)
2757 MarkItemEdited(id);
2758
2759 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
2760 char value_buf[64];
2761 const char* value_buf_end = value_buf + DataTypeFormatString(buf: value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
2762 if (g.LogEnabled)
2763 LogSetNextTextDecoration(prefix: "{", suffix: "}");
2764 RenderTextClipped(pos_min: frame_bb.Min, pos_max: frame_bb.Max, text: value_buf, text_end: value_buf_end, NULL, align: ImVec2(0.5f, 0.5f));
2765
2766 if (label_size.x > 0.0f)
2767 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), text: label);
2768
2769 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
2770 return value_changed;
2771}
2772
2773bool 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)
2774{
2775 ImGuiWindow* window = GetCurrentWindow();
2776 if (window->SkipItems)
2777 return false;
2778
2779 ImGuiContext& g = *GImGui;
2780 bool value_changed = false;
2781 BeginGroup();
2782 PushID(str_id: label);
2783 PushMultiItemsWidths(components, width_full: CalcItemWidth());
2784 size_t type_size = GDataTypeInfo[data_type].Size;
2785 for (int i = 0; i < components; i++)
2786 {
2787 PushID(int_id: i);
2788 if (i > 0)
2789 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2790 value_changed |= DragScalar(label: "", data_type, p_data, v_speed, p_min, p_max, format, flags);
2791 PopID();
2792 PopItemWidth();
2793 p_data = (void*)((char*)p_data + type_size);
2794 }
2795 PopID();
2796
2797 const char* label_end = FindRenderedTextEnd(text: label);
2798 if (label != label_end)
2799 {
2800 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2801 TextEx(text: label, text_end: label_end);
2802 }
2803
2804 EndGroup();
2805 return value_changed;
2806}
2807
2808bool ImGui::DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2809{
2810 return DragScalar(label, data_type: ImGuiDataType_Float, p_data: v, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2811}
2812
2813bool ImGui::DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2814{
2815 return DragScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 2, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2816}
2817
2818bool ImGui::DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2819{
2820 return DragScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 3, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2821}
2822
2823bool ImGui::DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2824{
2825 return DragScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 4, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2826}
2827
2828// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2829bool 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)
2830{
2831 ImGuiWindow* window = GetCurrentWindow();
2832 if (window->SkipItems)
2833 return false;
2834
2835 ImGuiContext& g = *GImGui;
2836 PushID(str_id: label);
2837 BeginGroup();
2838 PushMultiItemsWidths(components: 2, width_full: CalcItemWidth());
2839
2840 float min_min = (v_min >= v_max) ? -FLT_MAX : v_min;
2841 float min_max = (v_min >= v_max) ? *v_current_max : ImMin(lhs: v_max, rhs: *v_current_max);
2842 ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2843 bool value_changed = DragScalar(label: "##min", data_type: ImGuiDataType_Float, p_data: v_current_min, v_speed, p_min: &min_min, p_max: &min_max, format, flags: min_flags);
2844 PopItemWidth();
2845 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2846
2847 float max_min = (v_min >= v_max) ? *v_current_min : ImMax(lhs: v_min, rhs: *v_current_min);
2848 float max_max = (v_min >= v_max) ? FLT_MAX : v_max;
2849 ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2850 value_changed |= DragScalar(label: "##max", data_type: ImGuiDataType_Float, p_data: v_current_max, v_speed, p_min: &max_min, p_max: &max_max, format: format_max ? format_max : format, flags: max_flags);
2851 PopItemWidth();
2852 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2853
2854 TextEx(text: label, text_end: FindRenderedTextEnd(text: label));
2855 EndGroup();
2856 PopID();
2857
2858 return value_changed;
2859}
2860
2861// NB: v_speed is float to allow adjusting the drag speed with more precision
2862bool ImGui::DragInt(const char* label, int* v, float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2863{
2864 return DragScalar(label, data_type: ImGuiDataType_S32, p_data: v, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2865}
2866
2867bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2868{
2869 return DragScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 2, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2870}
2871
2872bool ImGui::DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2873{
2874 return DragScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 3, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2875}
2876
2877bool ImGui::DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2878{
2879 return DragScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 4, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2880}
2881
2882// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2883bool 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)
2884{
2885 ImGuiWindow* window = GetCurrentWindow();
2886 if (window->SkipItems)
2887 return false;
2888
2889 ImGuiContext& g = *GImGui;
2890 PushID(str_id: label);
2891 BeginGroup();
2892 PushMultiItemsWidths(components: 2, width_full: CalcItemWidth());
2893
2894 int min_min = (v_min >= v_max) ? INT_MIN : v_min;
2895 int min_max = (v_min >= v_max) ? *v_current_max : ImMin(lhs: v_max, rhs: *v_current_max);
2896 ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2897 bool value_changed = DragInt(label: "##min", v: v_current_min, v_speed, v_min: min_min, v_max: min_max, format, flags: min_flags);
2898 PopItemWidth();
2899 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2900
2901 int max_min = (v_min >= v_max) ? *v_current_min : ImMax(lhs: v_min, rhs: *v_current_min);
2902 int max_max = (v_min >= v_max) ? INT_MAX : v_max;
2903 ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2904 value_changed |= DragInt(label: "##max", v: v_current_max, v_speed, v_min: max_min, v_max: max_max, format: format_max ? format_max : format, flags: max_flags);
2905 PopItemWidth();
2906 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2907
2908 TextEx(text: label, text_end: FindRenderedTextEnd(text: label));
2909 EndGroup();
2910 PopID();
2911
2912 return value_changed;
2913}
2914
2915//-------------------------------------------------------------------------
2916// [SECTION] Widgets: SliderScalar, SliderFloat, SliderInt, etc.
2917//-------------------------------------------------------------------------
2918// - ScaleRatioFromValueT<> [Internal]
2919// - ScaleValueFromRatioT<> [Internal]
2920// - SliderBehaviorT<>() [Internal]
2921// - SliderBehavior() [Internal]
2922// - SliderScalar()
2923// - SliderScalarN()
2924// - SliderFloat()
2925// - SliderFloat2()
2926// - SliderFloat3()
2927// - SliderFloat4()
2928// - SliderAngle()
2929// - SliderInt()
2930// - SliderInt2()
2931// - SliderInt3()
2932// - SliderInt4()
2933// - VSliderScalar()
2934// - VSliderFloat()
2935// - VSliderInt()
2936//-------------------------------------------------------------------------
2937
2938// Convert a value v in the output space of a slider into a parametric position on the slider itself (the logical opposite of ScaleValueFromRatioT)
2939template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2940float ImGui::ScaleRatioFromValueT(ImGuiDataType data_type, TYPE v, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize)
2941{
2942 if (v_min == v_max)
2943 return 0.0f;
2944 IM_UNUSED(data_type);
2945
2946 const TYPE v_clamped = (v_min < v_max) ? ImClamp(v, v_min, v_max) : ImClamp(v, v_max, v_min);
2947 if (is_logarithmic)
2948 {
2949 bool flipped = v_max < v_min;
2950
2951 if (flipped) // Handle the case where the range is backwards
2952 ImSwap(v_min, v_max);
2953
2954 // Fudge min/max to avoid getting close to log(0)
2955 FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
2956 FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
2957
2958 // Awkward special cases - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
2959 if ((v_min == 0.0f) && (v_max < 0.0f))
2960 v_min_fudged = -logarithmic_zero_epsilon;
2961 else if ((v_max == 0.0f) && (v_min < 0.0f))
2962 v_max_fudged = -logarithmic_zero_epsilon;
2963
2964 float result;
2965 if (v_clamped <= v_min_fudged)
2966 result = 0.0f; // Workaround for values that are in-range but below our fudge
2967 else if (v_clamped >= v_max_fudged)
2968 result = 1.0f; // Workaround for values that are in-range but above our fudge
2969 else if ((v_min * v_max) < 0.0f) // Range crosses zero, so split into two portions
2970 {
2971 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)
2972 float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
2973 float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
2974 if (v == 0.0f)
2975 result = zero_point_center; // Special case for exactly zero
2976 else if (v < 0.0f)
2977 result = (1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(-v_min_fudged / logarithmic_zero_epsilon))) * zero_point_snap_L;
2978 else
2979 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));
2980 }
2981 else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
2982 result = 1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / -v_max_fudged) / ImLog(-v_min_fudged / -v_max_fudged));
2983 else
2984 result = (float)(ImLog((FLOATTYPE)v_clamped / v_min_fudged) / ImLog(v_max_fudged / v_min_fudged));
2985
2986 return flipped ? (1.0f - result) : result;
2987 }
2988 else
2989 {
2990 // Linear slider
2991 return (float)((FLOATTYPE)(SIGNEDTYPE)(v_clamped - v_min) / (FLOATTYPE)(SIGNEDTYPE)(v_max - v_min));
2992 }
2993}
2994
2995// Convert a parametric position on a slider into a value v in the output space (the logical opposite of ScaleRatioFromValueT)
2996template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2997TYPE ImGui::ScaleValueFromRatioT(ImGuiDataType data_type, float t, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize)
2998{
2999 // We special-case the extents because otherwise our logarithmic fudging can lead to "mathematically correct"
3000 // but non-intuitive behaviors like a fully-left slider not actually reaching the minimum value. Also generally simpler.
3001 if (t <= 0.0f || v_min == v_max)
3002 return v_min;
3003 if (t >= 1.0f)
3004 return v_max;
3005
3006 TYPE result = (TYPE)0;
3007 if (is_logarithmic)
3008 {
3009 // Fudge min/max to avoid getting silly results close to zero
3010 FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
3011 FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
3012
3013 const bool flipped = v_max < v_min; // Check if range is "backwards"
3014 if (flipped)
3015 ImSwap(v_min_fudged, v_max_fudged);
3016
3017 // Awkward special case - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
3018 if ((v_max == 0.0f) && (v_min < 0.0f))
3019 v_max_fudged = -logarithmic_zero_epsilon;
3020
3021 float t_with_flip = flipped ? (1.0f - t) : t; // t, but flipped if necessary to account for us flipping the range
3022
3023 if ((v_min * v_max) < 0.0f) // Range crosses zero, so we have to do this in two parts
3024 {
3025 float zero_point_center = (-(float)ImMin(v_min, v_max)) / ImAbs(x: (float)v_max - (float)v_min); // The zero point in parametric space
3026 float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
3027 float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
3028 if (t_with_flip >= zero_point_snap_L && t_with_flip <= zero_point_snap_R)
3029 result = (TYPE)0.0f; // Special case to make getting exactly zero possible (the epsilon prevents it otherwise)
3030 else if (t_with_flip < zero_point_center)
3031 result = (TYPE)-(logarithmic_zero_epsilon * ImPow(-v_min_fudged / logarithmic_zero_epsilon, (FLOATTYPE)(1.0f - (t_with_flip / zero_point_snap_L))));
3032 else
3033 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))));
3034 }
3035 else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
3036 result = (TYPE)-(-v_max_fudged * ImPow(-v_min_fudged / -v_max_fudged, (FLOATTYPE)(1.0f - t_with_flip)));
3037 else
3038 result = (TYPE)(v_min_fudged * ImPow(v_max_fudged / v_min_fudged, (FLOATTYPE)t_with_flip));
3039 }
3040 else
3041 {
3042 // Linear slider
3043 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
3044 if (is_floating_point)
3045 {
3046 result = ImLerp(v_min, v_max, t);
3047 }
3048 else if (t < 1.0)
3049 {
3050 // - For integer values we want the clicking position to match the grab box so we round above
3051 // This code is carefully tuned to work with large values (e.g. high ranges of U64) while preserving this property..
3052 // - 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
3053 // range is going to be imprecise anyway, with this check we at least make the edge values matches expected limits.
3054 FLOATTYPE v_new_off_f = (SIGNEDTYPE)(v_max - v_min) * t;
3055 result = (TYPE)((SIGNEDTYPE)v_min + (SIGNEDTYPE)(v_new_off_f + (FLOATTYPE)(v_min > v_max ? -0.5 : 0.5)));
3056 }
3057 }
3058
3059 return result;
3060}
3061
3062// FIXME: Try to move more of the code into shared SliderBehavior()
3063template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
3064bool 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)
3065{
3066 ImGuiContext& g = *GImGui;
3067 const ImGuiStyle& style = g.Style;
3068
3069 const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
3070 const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
3071 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
3072 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.
3073
3074 // Calculate bounds
3075 const float grab_padding = 2.0f; // FIXME: Should be part of style.
3076 const float slider_sz = (bb.Max[axis] - bb.Min[axis]) - grab_padding * 2.0f;
3077 float grab_sz = style.GrabMinSize;
3078 if (!is_floating_point && v_range_f >= 0.0f) // v_range_f < 0 may happen on integer overflows
3079 grab_sz = ImMax(lhs: slider_sz / (v_range_f + 1), rhs: style.GrabMinSize); // For integer sliders: if possible have the grab size represent 1 unit
3080 grab_sz = ImMin(lhs: grab_sz, rhs: slider_sz);
3081 const float slider_usable_sz = slider_sz - grab_sz;
3082 const float slider_usable_pos_min = bb.Min[axis] + grab_padding + grab_sz * 0.5f;
3083 const float slider_usable_pos_max = bb.Max[axis] - grab_padding - grab_sz * 0.5f;
3084
3085 float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
3086 float zero_deadzone_halfsize = 0.0f; // Only valid when is_logarithmic is true
3087 if (is_logarithmic)
3088 {
3089 // 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.
3090 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, default_value: 3) : 1;
3091 logarithmic_zero_epsilon = ImPow(x: 0.1f, y: (float)decimal_precision);
3092 zero_deadzone_halfsize = (style.LogSliderDeadzone * 0.5f) / ImMax(lhs: slider_usable_sz, rhs: 1.0f);
3093 }
3094
3095 // Process interacting with the slider
3096 bool value_changed = false;
3097 if (g.ActiveId == id)
3098 {
3099 bool set_new_value = false;
3100 float clicked_t = 0.0f;
3101 if (g.ActiveIdSource == ImGuiInputSource_Mouse)
3102 {
3103 if (!g.IO.MouseDown[0])
3104 {
3105 ClearActiveID();
3106 }
3107 else
3108 {
3109 const float mouse_abs_pos = g.IO.MousePos[axis];
3110 if (g.ActiveIdIsJustActivated)
3111 {
3112 float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3113 if (axis == ImGuiAxis_Y)
3114 grab_t = 1.0f - grab_t;
3115 const float grab_pos = ImLerp(a: slider_usable_pos_min, b: slider_usable_pos_max, t: grab_t);
3116 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.
3117 g.SliderGrabClickOffset = (clicked_around_grab && is_floating_point) ? mouse_abs_pos - grab_pos : 0.0f;
3118 }
3119 if (slider_usable_sz > 0.0f)
3120 clicked_t = ImSaturate(f: (mouse_abs_pos - g.SliderGrabClickOffset - slider_usable_pos_min) / slider_usable_sz);
3121 if (axis == ImGuiAxis_Y)
3122 clicked_t = 1.0f - clicked_t;
3123 set_new_value = true;
3124 }
3125 }
3126 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
3127 {
3128 if (g.ActiveIdIsJustActivated)
3129 {
3130 g.SliderCurrentAccum = 0.0f; // Reset any stored nav delta upon activation
3131 g.SliderCurrentAccumDirty = false;
3132 }
3133
3134 float input_delta = (axis == ImGuiAxis_X) ? GetNavTweakPressedAmount(axis) : -GetNavTweakPressedAmount(axis);
3135 if (input_delta != 0.0f)
3136 {
3137 const bool tweak_slow = IsKeyDown(key: (g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
3138 const bool tweak_fast = IsKeyDown(key: (g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
3139 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, default_value: 3) : 0;
3140 if (decimal_precision > 0)
3141 {
3142 input_delta /= 100.0f; // Keyboard/Gamepad tweak speeds in % of slider bounds
3143 if (tweak_slow)
3144 input_delta /= 10.0f;
3145 }
3146 else
3147 {
3148 if ((v_range_f >= -100.0f && v_range_f <= 100.0f && v_range_f != 0.0f) || tweak_slow)
3149 input_delta = ((input_delta < 0.0f) ? -1.0f : +1.0f) / v_range_f; // Keyboard/Gamepad tweak speeds in integer steps
3150 else
3151 input_delta /= 100.0f;
3152 }
3153 if (tweak_fast)
3154 input_delta *= 10.0f;
3155
3156 g.SliderCurrentAccum += input_delta;
3157 g.SliderCurrentAccumDirty = true;
3158 }
3159
3160 float delta = g.SliderCurrentAccum;
3161 if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
3162 {
3163 ClearActiveID();
3164 }
3165 else if (g.SliderCurrentAccumDirty)
3166 {
3167 clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3168
3169 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
3170 {
3171 set_new_value = false;
3172 g.SliderCurrentAccum = 0.0f; // If pushing up against the limits, don't continue to accumulate
3173 }
3174 else
3175 {
3176 set_new_value = true;
3177 float old_clicked_t = clicked_t;
3178 clicked_t = ImSaturate(f: clicked_t + delta);
3179
3180 // Calculate what our "new" clicked_t will be, and thus how far we actually moved the slider, and subtract this from the accumulator
3181 TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3182 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
3183 v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
3184 float new_clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3185
3186 if (delta > 0)
3187 g.SliderCurrentAccum -= ImMin(lhs: new_clicked_t - old_clicked_t, rhs: delta);
3188 else
3189 g.SliderCurrentAccum -= ImMax(lhs: new_clicked_t - old_clicked_t, rhs: delta);
3190 }
3191
3192 g.SliderCurrentAccumDirty = false;
3193 }
3194 }
3195
3196 if (set_new_value)
3197 if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
3198 set_new_value = false;
3199
3200 if (set_new_value)
3201 {
3202 TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3203
3204 // Round to user desired precision based on format string
3205 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
3206 v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
3207
3208 // Apply result
3209 if (*v != v_new)
3210 {
3211 *v = v_new;
3212 value_changed = true;
3213 }
3214 }
3215 }
3216
3217 if (slider_sz < 1.0f)
3218 {
3219 *out_grab_bb = ImRect(bb.Min, bb.Min);
3220 }
3221 else
3222 {
3223 // Output grab position so it can be displayed by the caller
3224 float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3225 if (axis == ImGuiAxis_Y)
3226 grab_t = 1.0f - grab_t;
3227 const float grab_pos = ImLerp(a: slider_usable_pos_min, b: slider_usable_pos_max, t: grab_t);
3228 if (axis == ImGuiAxis_X)
3229 *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);
3230 else
3231 *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);
3232 }
3233
3234 return value_changed;
3235}
3236
3237// For 32-bit and larger types, slider bounds are limited to half the natural type range.
3238// 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.
3239// It would be possible to lift that limitation with some work but it doesn't seem to be worth it for sliders.
3240bool 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)
3241{
3242 // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
3243 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.");
3244 IM_ASSERT((flags & ImGuiSliderFlags_WrapAround) == 0); // Not supported by SliderXXX(), only by DragXXX()
3245
3246 switch (data_type)
3247 {
3248 case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = SliderBehaviorT<ImS32, ImS32, float>(bb, id, data_type: ImGuiDataType_S32, v: &v32, v_min: *(const ImS8*)p_min, v_max: *(const ImS8*)p_max, format, flags, out_grab_bb); if (r) *(ImS8*)p_v = (ImS8)v32; return r; }
3249 case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)p_v; bool r = SliderBehaviorT<ImU32, ImS32, float>(bb, id, data_type: ImGuiDataType_U32, v: &v32, v_min: *(const ImU8*)p_min, v_max: *(const ImU8*)p_max, format, flags, out_grab_bb); if (r) *(ImU8*)p_v = (ImU8)v32; return r; }
3250 case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)p_v; bool r = SliderBehaviorT<ImS32, ImS32, float>(bb, id, data_type: ImGuiDataType_S32, v: &v32, v_min: *(const ImS16*)p_min, v_max: *(const ImS16*)p_max, format, flags, out_grab_bb); if (r) *(ImS16*)p_v = (ImS16)v32; return r; }
3251 case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)p_v; bool r = SliderBehaviorT<ImU32, ImS32, float>(bb, id, data_type: ImGuiDataType_U32, v: &v32, v_min: *(const ImU16*)p_min, v_max: *(const ImU16*)p_max, format, flags, out_grab_bb); if (r) *(ImU16*)p_v = (ImU16)v32; return r; }
3252 case ImGuiDataType_S32:
3253 IM_ASSERT(*(const ImS32*)p_min >= IM_S32_MIN / 2 && *(const ImS32*)p_max <= IM_S32_MAX / 2);
3254 return SliderBehaviorT<ImS32, ImS32, float >(bb, id, data_type, v: (ImS32*)p_v, v_min: *(const ImS32*)p_min, v_max: *(const ImS32*)p_max, format, flags, out_grab_bb);
3255 case ImGuiDataType_U32:
3256 IM_ASSERT(*(const ImU32*)p_max <= IM_U32_MAX / 2);
3257 return SliderBehaviorT<ImU32, ImS32, float >(bb, id, data_type, v: (ImU32*)p_v, v_min: *(const ImU32*)p_min, v_max: *(const ImU32*)p_max, format, flags, out_grab_bb);
3258 case ImGuiDataType_S64:
3259 IM_ASSERT(*(const ImS64*)p_min >= IM_S64_MIN / 2 && *(const ImS64*)p_max <= IM_S64_MAX / 2);
3260 return SliderBehaviorT<ImS64, ImS64, double>(bb, id, data_type, v: (ImS64*)p_v, v_min: *(const ImS64*)p_min, v_max: *(const ImS64*)p_max, format, flags, out_grab_bb);
3261 case ImGuiDataType_U64:
3262 IM_ASSERT(*(const ImU64*)p_max <= IM_U64_MAX / 2);
3263 return SliderBehaviorT<ImU64, ImS64, double>(bb, id, data_type, v: (ImU64*)p_v, v_min: *(const ImU64*)p_min, v_max: *(const ImU64*)p_max, format, flags, out_grab_bb);
3264 case ImGuiDataType_Float:
3265 IM_ASSERT(*(const float*)p_min >= -FLT_MAX / 2.0f && *(const float*)p_max <= FLT_MAX / 2.0f);
3266 return SliderBehaviorT<float, float, float >(bb, id, data_type, v: (float*)p_v, v_min: *(const float*)p_min, v_max: *(const float*)p_max, format, flags, out_grab_bb);
3267 case ImGuiDataType_Double:
3268 IM_ASSERT(*(const double*)p_min >= -DBL_MAX / 2.0f && *(const double*)p_max <= DBL_MAX / 2.0f);
3269 return SliderBehaviorT<double, double, double>(bb, id, data_type, v: (double*)p_v, v_min: *(const double*)p_min, v_max: *(const double*)p_max, format, flags, out_grab_bb);
3270 case ImGuiDataType_COUNT: break;
3271 }
3272 IM_ASSERT(0);
3273 return false;
3274}
3275
3276// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a slider, they are all required.
3277// Read code of e.g. SliderFloat(), SliderInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3278bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags)
3279{
3280 ImGuiWindow* window = GetCurrentWindow();
3281 if (window->SkipItems)
3282 return false;
3283
3284 ImGuiContext& g = *GImGui;
3285 const ImGuiStyle& style = g.Style;
3286 const ImGuiID id = window->GetID(str: label);
3287 const float w = CalcItemWidth();
3288
3289 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
3290 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
3291 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));
3292
3293 const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
3294 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
3295 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
3296 return false;
3297
3298 // Default format string when passing NULL
3299 if (format == NULL)
3300 format = DataTypeGetInfo(data_type)->PrintFmt;
3301
3302 const bool hovered = ItemHoverable(bb: frame_bb, id, item_flags: g.LastItemData.ItemFlags);
3303 bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
3304 if (!temp_input_is_active)
3305 {
3306 // Tabbing or CTRL+click on Slider turns it into an input box
3307 const bool clicked = hovered && IsMouseClicked(button: 0, flags: ImGuiInputFlags_None, owner_id: id);
3308 const bool make_active = (clicked || g.NavActivateId == id);
3309 if (make_active && clicked)
3310 SetKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id);
3311 if (make_active && temp_input_allowed)
3312 if ((clicked && g.IO.KeyCtrl) || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
3313 temp_input_is_active = true;
3314
3315 // Store initial value (not used by main lib but available as a convenience but some mods e.g. to revert)
3316 if (make_active)
3317 memcpy(dest: &g.ActiveIdValueOnActivation, src: p_data, n: DataTypeGetInfo(data_type)->Size);
3318
3319 if (make_active && !temp_input_is_active)
3320 {
3321 SetActiveID(id, window);
3322 SetFocusID(id, window);
3323 FocusWindow(window);
3324 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
3325 }
3326 }
3327
3328 if (temp_input_is_active)
3329 {
3330 // Only clamp CTRL+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp)
3331 const bool clamp_enabled = (flags & ImGuiSliderFlags_ClampOnInput) != 0;
3332 return TempInputScalar(bb: frame_bb, id, label, data_type, p_data, format, p_clamp_min: clamp_enabled ? p_min : NULL, p_clamp_max: clamp_enabled ? p_max : NULL);
3333 }
3334
3335 // Draw frame
3336 const ImU32 frame_col = GetColorU32(idx: g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3337 RenderNavCursor(bb: frame_bb, id);
3338 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: frame_col, borders: true, rounding: g.Style.FrameRounding);
3339
3340 // Slider behavior
3341 ImRect grab_bb;
3342 const bool value_changed = SliderBehavior(bb: frame_bb, id, data_type, p_v: p_data, p_min, p_max, format, flags, out_grab_bb: &grab_bb);
3343 if (value_changed)
3344 MarkItemEdited(id);
3345
3346 // Render grab
3347 if (grab_bb.Max.x > grab_bb.Min.x)
3348 window->DrawList->AddRectFilled(p_min: grab_bb.Min, p_max: grab_bb.Max, col: GetColorU32(idx: g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), rounding: style.GrabRounding);
3349
3350 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3351 char value_buf[64];
3352 const char* value_buf_end = value_buf + DataTypeFormatString(buf: value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
3353 if (g.LogEnabled)
3354 LogSetNextTextDecoration(prefix: "{", suffix: "}");
3355 RenderTextClipped(pos_min: frame_bb.Min, pos_max: frame_bb.Max, text: value_buf, text_end: value_buf_end, NULL, align: ImVec2(0.5f, 0.5f));
3356
3357 if (label_size.x > 0.0f)
3358 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), text: label);
3359
3360 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
3361 return value_changed;
3362}
3363
3364// Add multiple sliders on 1 line for compact edition of multiple components
3365bool 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)
3366{
3367 ImGuiWindow* window = GetCurrentWindow();
3368 if (window->SkipItems)
3369 return false;
3370
3371 ImGuiContext& g = *GImGui;
3372 bool value_changed = false;
3373 BeginGroup();
3374 PushID(str_id: label);
3375 PushMultiItemsWidths(components, width_full: CalcItemWidth());
3376 size_t type_size = GDataTypeInfo[data_type].Size;
3377 for (int i = 0; i < components; i++)
3378 {
3379 PushID(int_id: i);
3380 if (i > 0)
3381 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
3382 value_changed |= SliderScalar(label: "", data_type, p_data: v, p_min: v_min, p_max: v_max, format, flags);
3383 PopID();
3384 PopItemWidth();
3385 v = (void*)((char*)v + type_size);
3386 }
3387 PopID();
3388
3389 const char* label_end = FindRenderedTextEnd(text: label);
3390 if (label != label_end)
3391 {
3392 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
3393 TextEx(text: label, text_end: label_end);
3394 }
3395
3396 EndGroup();
3397 return value_changed;
3398}
3399
3400bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3401{
3402 return SliderScalar(label, data_type: ImGuiDataType_Float, p_data: v, p_min: &v_min, p_max: &v_max, format, flags);
3403}
3404
3405bool ImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3406{
3407 return SliderScalarN(label, data_type: ImGuiDataType_Float, v, components: 2, v_min: &v_min, v_max: &v_max, format, flags);
3408}
3409
3410bool ImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3411{
3412 return SliderScalarN(label, data_type: ImGuiDataType_Float, v, components: 3, v_min: &v_min, v_max: &v_max, format, flags);
3413}
3414
3415bool ImGui::SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3416{
3417 return SliderScalarN(label, data_type: ImGuiDataType_Float, v, components: 4, v_min: &v_min, v_max: &v_max, format, flags);
3418}
3419
3420bool ImGui::SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max, const char* format, ImGuiSliderFlags flags)
3421{
3422 if (format == NULL)
3423 format = "%.0f deg";
3424 float v_deg = (*v_rad) * 360.0f / (2 * IM_PI);
3425 bool value_changed = SliderFloat(label, v: &v_deg, v_min: v_degrees_min, v_max: v_degrees_max, format, flags);
3426 if (value_changed)
3427 *v_rad = v_deg * (2 * IM_PI) / 360.0f;
3428 return value_changed;
3429}
3430
3431bool ImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3432{
3433 return SliderScalar(label, data_type: ImGuiDataType_S32, p_data: v, p_min: &v_min, p_max: &v_max, format, flags);
3434}
3435
3436bool ImGui::SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3437{
3438 return SliderScalarN(label, data_type: ImGuiDataType_S32, v, components: 2, v_min: &v_min, v_max: &v_max, format, flags);
3439}
3440
3441bool ImGui::SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3442{
3443 return SliderScalarN(label, data_type: ImGuiDataType_S32, v, components: 3, v_min: &v_min, v_max: &v_max, format, flags);
3444}
3445
3446bool ImGui::SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3447{
3448 return SliderScalarN(label, data_type: ImGuiDataType_S32, v, components: 4, v_min: &v_min, v_max: &v_max, format, flags);
3449}
3450
3451bool 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)
3452{
3453 ImGuiWindow* window = GetCurrentWindow();
3454 if (window->SkipItems)
3455 return false;
3456
3457 ImGuiContext& g = *GImGui;
3458 const ImGuiStyle& style = g.Style;
3459 const ImGuiID id = window->GetID(str: label);
3460
3461 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
3462 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size);
3463 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));
3464
3465 ItemSize(bb, text_baseline_y: style.FramePadding.y);
3466 if (!ItemAdd(bb: frame_bb, id))
3467 return false;
3468
3469 // Default format string when passing NULL
3470 if (format == NULL)
3471 format = DataTypeGetInfo(data_type)->PrintFmt;
3472
3473 const bool hovered = ItemHoverable(bb: frame_bb, id, item_flags: g.LastItemData.ItemFlags);
3474 const bool clicked = hovered && IsMouseClicked(button: 0, flags: ImGuiInputFlags_None, owner_id: id);
3475 if (clicked || g.NavActivateId == id)
3476 {
3477 if (clicked)
3478 SetKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id);
3479 SetActiveID(id, window);
3480 SetFocusID(id, window);
3481 FocusWindow(window);
3482 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
3483 }
3484
3485 // Draw frame
3486 const ImU32 frame_col = GetColorU32(idx: g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3487 RenderNavCursor(bb: frame_bb, id);
3488 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: frame_col, borders: true, rounding: g.Style.FrameRounding);
3489
3490 // Slider behavior
3491 ImRect grab_bb;
3492 const bool value_changed = SliderBehavior(bb: frame_bb, id, data_type, p_v: p_data, p_min, p_max, format, flags: flags | ImGuiSliderFlags_Vertical, out_grab_bb: &grab_bb);
3493 if (value_changed)
3494 MarkItemEdited(id);
3495
3496 // Render grab
3497 if (grab_bb.Max.y > grab_bb.Min.y)
3498 window->DrawList->AddRectFilled(p_min: grab_bb.Min, p_max: grab_bb.Max, col: GetColorU32(idx: g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), rounding: style.GrabRounding);
3499
3500 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3501 // For the vertical slider we allow centered text to overlap the frame padding
3502 char value_buf[64];
3503 const char* value_buf_end = value_buf + DataTypeFormatString(buf: value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
3504 RenderTextClipped(pos_min: ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), pos_max: frame_bb.Max, text: value_buf, text_end: value_buf_end, NULL, align: ImVec2(0.5f, 0.0f));
3505 if (label_size.x > 0.0f)
3506 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), text: label);
3507
3508 return value_changed;
3509}
3510
3511bool ImGui::VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3512{
3513 return VSliderScalar(label, size, data_type: ImGuiDataType_Float, p_data: v, p_min: &v_min, p_max: &v_max, format, flags);
3514}
3515
3516bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3517{
3518 return VSliderScalar(label, size, data_type: ImGuiDataType_S32, p_data: v, p_min: &v_min, p_max: &v_max, format, flags);
3519}
3520
3521//-------------------------------------------------------------------------
3522// [SECTION] Widgets: InputScalar, InputFloat, InputInt, etc.
3523//-------------------------------------------------------------------------
3524// - ImParseFormatFindStart() [Internal]
3525// - ImParseFormatFindEnd() [Internal]
3526// - ImParseFormatTrimDecorations() [Internal]
3527// - ImParseFormatSanitizeForPrinting() [Internal]
3528// - ImParseFormatSanitizeForScanning() [Internal]
3529// - ImParseFormatPrecision() [Internal]
3530// - TempInputTextScalar() [Internal]
3531// - InputScalar()
3532// - InputScalarN()
3533// - InputFloat()
3534// - InputFloat2()
3535// - InputFloat3()
3536// - InputFloat4()
3537// - InputInt()
3538// - InputInt2()
3539// - InputInt3()
3540// - InputInt4()
3541// - InputDouble()
3542//-------------------------------------------------------------------------
3543
3544// We don't use strchr() because our strings are usually very short and often start with '%'
3545const char* ImParseFormatFindStart(const char* fmt)
3546{
3547 while (char c = fmt[0])
3548 {
3549 if (c == '%' && fmt[1] != '%')
3550 return fmt;
3551 else if (c == '%')
3552 fmt++;
3553 fmt++;
3554 }
3555 return fmt;
3556}
3557
3558const char* ImParseFormatFindEnd(const char* fmt)
3559{
3560 // Printf/scanf types modifiers: I/L/h/j/l/t/w/z. Other uppercase letters qualify as types aka end of the format.
3561 if (fmt[0] != '%')
3562 return fmt;
3563 const unsigned int ignored_uppercase_mask = (1 << ('I'-'A')) | (1 << ('L'-'A'));
3564 const unsigned int ignored_lowercase_mask = (1 << ('h'-'a')) | (1 << ('j'-'a')) | (1 << ('l'-'a')) | (1 << ('t'-'a')) | (1 << ('w'-'a')) | (1 << ('z'-'a'));
3565 for (char c; (c = *fmt) != 0; fmt++)
3566 {
3567 if (c >= 'A' && c <= 'Z' && ((1 << (c - 'A')) & ignored_uppercase_mask) == 0)
3568 return fmt + 1;
3569 if (c >= 'a' && c <= 'z' && ((1 << (c - 'a')) & ignored_lowercase_mask) == 0)
3570 return fmt + 1;
3571 }
3572 return fmt;
3573}
3574
3575// Extract the format out of a format string with leading or trailing decorations
3576// fmt = "blah blah" -> return ""
3577// fmt = "%.3f" -> return fmt
3578// fmt = "hello %.3f" -> return fmt + 6
3579// fmt = "%.3f hello" -> return buf written with "%.3f"
3580const char* ImParseFormatTrimDecorations(const char* fmt, char* buf, size_t buf_size)
3581{
3582 const char* fmt_start = ImParseFormatFindStart(fmt);
3583 if (fmt_start[0] != '%')
3584 return "";
3585 const char* fmt_end = ImParseFormatFindEnd(fmt: fmt_start);
3586 if (fmt_end[0] == 0) // If we only have leading decoration, we don't need to copy the data.
3587 return fmt_start;
3588 ImStrncpy(dst: buf, src: fmt_start, count: ImMin(lhs: (size_t)(fmt_end - fmt_start) + 1, rhs: buf_size));
3589 return buf;
3590}
3591
3592// Sanitize format
3593// - Zero terminate so extra characters after format (e.g. "%f123") don't confuse atof/atoi
3594// - stb_sprintf.h supports several new modifiers which format numbers in a way that also makes them incompatible atof/atoi.
3595void ImParseFormatSanitizeForPrinting(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3596{
3597 const char* fmt_end = ImParseFormatFindEnd(fmt: fmt_in);
3598 IM_UNUSED(fmt_out_size);
3599 IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3600 while (fmt_in < fmt_end)
3601 {
3602 char c = *fmt_in++;
3603 if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3604 *(fmt_out++) = c;
3605 }
3606 *fmt_out = 0; // Zero-terminate
3607}
3608
3609// - 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"
3610const char* ImParseFormatSanitizeForScanning(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3611{
3612 const char* fmt_end = ImParseFormatFindEnd(fmt: fmt_in);
3613 const char* fmt_out_begin = fmt_out;
3614 IM_UNUSED(fmt_out_size);
3615 IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3616 bool has_type = false;
3617 while (fmt_in < fmt_end)
3618 {
3619 char c = *fmt_in++;
3620 if (!has_type && ((c >= '0' && c <= '9') || c == '.' || c == '+' || c == '#'))
3621 continue;
3622 has_type |= ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')); // Stop skipping digits
3623 if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3624 *(fmt_out++) = c;
3625 }
3626 *fmt_out = 0; // Zero-terminate
3627 return fmt_out_begin;
3628}
3629
3630template<typename TYPE>
3631static const char* ImAtoi(const char* src, TYPE* output)
3632{
3633 int negative = 0;
3634 if (*src == '-') { negative = 1; src++; }
3635 if (*src == '+') { src++; }
3636 TYPE v = 0;
3637 while (*src >= '0' && *src <= '9')
3638 v = (v * 10) + (*src++ - '0');
3639 *output = negative ? -v : v;
3640 return src;
3641}
3642
3643// Parse display precision back from the display format string
3644// 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.
3645int ImParseFormatPrecision(const char* fmt, int default_precision)
3646{
3647 fmt = ImParseFormatFindStart(fmt);
3648 if (fmt[0] != '%')
3649 return default_precision;
3650 fmt++;
3651 while (*fmt >= '0' && *fmt <= '9')
3652 fmt++;
3653 int precision = INT_MAX;
3654 if (*fmt == '.')
3655 {
3656 fmt = ImAtoi<int>(src: fmt + 1, output: &precision);
3657 if (precision < 0 || precision > 99)
3658 precision = default_precision;
3659 }
3660 if (*fmt == 'e' || *fmt == 'E') // Maximum precision with scientific notation
3661 precision = -1;
3662 if ((*fmt == 'g' || *fmt == 'G') && precision == INT_MAX)
3663 precision = -1;
3664 return (precision == INT_MAX) ? default_precision : precision;
3665}
3666
3667// Create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets)
3668// FIXME: Facilitate using this in variety of other situations.
3669// FIXME: Among other things, setting ImGuiItemFlags_AllowDuplicateId in LastItemData is currently correct but
3670// the expected relationship between TempInputXXX functions and LastItemData is a little fishy.
3671bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags)
3672{
3673 // On the first frame, g.TempInputTextId == 0, then on subsequent frames it becomes == id.
3674 // We clear ActiveID on the first frame to allow the InputText() taking it back.
3675 ImGuiContext& g = *GImGui;
3676 const bool init = (g.TempInputId != id);
3677 if (init)
3678 ClearActiveID();
3679
3680 g.CurrentWindow->DC.CursorPos = bb.Min;
3681 g.LastItemData.ItemFlags |= ImGuiItemFlags_AllowDuplicateId;
3682 bool value_changed = InputTextEx(label, NULL, buf, buf_size, size_arg: bb.GetSize(), flags: flags | ImGuiInputTextFlags_MergedItem);
3683 if (init)
3684 {
3685 // First frame we started displaying the InputText widget, we expect it to take the active id.
3686 IM_ASSERT(g.ActiveId == id);
3687 g.TempInputId = g.ActiveId;
3688 }
3689 return value_changed;
3690}
3691
3692// Note that Drag/Slider functions are only forwarding the min/max values clamping values if the ImGuiSliderFlags_AlwaysClamp flag is set!
3693// This is intended: this way we allow CTRL+Click manual input to set a value out of bounds, for maximum flexibility.
3694// However this may not be ideal for all uses, as some user code may break on out of bound values.
3695bool 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)
3696{
3697 // FIXME: May need to clarify display behavior if format doesn't contain %.
3698 // "%d" -> "%d" / "There are %d items" -> "%d" / "items" -> "%d" (fallback). Also see #6405
3699 ImGuiContext& g = *GImGui;
3700 const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
3701 char fmt_buf[32];
3702 char data_buf[32];
3703 format = ImParseFormatTrimDecorations(fmt: format, buf: fmt_buf, IM_ARRAYSIZE(fmt_buf));
3704 if (format[0] == 0)
3705 format = type_info->PrintFmt;
3706 DataTypeFormatString(buf: data_buf, IM_ARRAYSIZE(data_buf), data_type, p_data, format);
3707 ImStrTrimBlanks(str: data_buf);
3708
3709 ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
3710 g.LastItemData.ItemFlags |= ImGuiItemFlags_NoMarkEdited; // Because TempInputText() uses ImGuiInputTextFlags_MergedItem it doesn't submit a new item, so we poke LastItemData.
3711 bool value_changed = false;
3712 if (TempInputText(bb, id, label, buf: data_buf, IM_ARRAYSIZE(data_buf), flags))
3713 {
3714 // Backup old value
3715 size_t data_type_size = type_info->Size;
3716 ImGuiDataTypeStorage data_backup;
3717 memcpy(dest: &data_backup, src: p_data, n: data_type_size);
3718
3719 // Apply new value (or operations) then clamp
3720 DataTypeApplyFromText(buf: data_buf, data_type, p_data, format, NULL);
3721 if (p_clamp_min || p_clamp_max)
3722 {
3723 if (p_clamp_min && p_clamp_max && DataTypeCompare(data_type, arg_1: p_clamp_min, arg_2: p_clamp_max) > 0)
3724 ImSwap(a&: p_clamp_min, b&: p_clamp_max);
3725 DataTypeClamp(data_type, p_data, p_min: p_clamp_min, p_max: p_clamp_max);
3726 }
3727
3728 // Only mark as edited if new value is different
3729 g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited;
3730 value_changed = memcmp(s1: &data_backup, s2: p_data, n: data_type_size) != 0;
3731 if (value_changed)
3732 MarkItemEdited(id);
3733 }
3734 return value_changed;
3735}
3736
3737void ImGui::SetNextItemRefVal(ImGuiDataType data_type, void* p_data)
3738{
3739 ImGuiContext& g = *GImGui;
3740 g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasRefVal;
3741 memcpy(dest: &g.NextItemData.RefVal, src: p_data, n: DataTypeGetInfo(data_type)->Size);
3742}
3743
3744// 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.
3745// Read code of e.g. InputFloat(), InputInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3746bool 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)
3747{
3748 ImGuiWindow* window = GetCurrentWindow();
3749 if (window->SkipItems)
3750 return false;
3751
3752 ImGuiContext& g = *GImGui;
3753 ImGuiStyle& style = g.Style;
3754 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()!
3755
3756 if (format == NULL)
3757 format = DataTypeGetInfo(data_type)->PrintFmt;
3758
3759 void* p_data_default = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasRefVal) ? &g.NextItemData.RefVal : &g.DataTypeZeroValue;
3760
3761 char buf[64];
3762 if ((flags & ImGuiInputTextFlags_DisplayEmptyRefVal) && DataTypeCompare(data_type, arg_1: p_data, arg_2: p_data_default) == 0)
3763 buf[0] = 0;
3764 else
3765 DataTypeFormatString(buf, IM_ARRAYSIZE(buf), data_type, p_data, format);
3766
3767 // Disable the MarkItemEdited() call in InputText but keep ImGuiItemStatusFlags_Edited.
3768 // We call MarkItemEdited() ourselves by comparing the actual data rather than the string.
3769 g.NextItemData.ItemFlags |= ImGuiItemFlags_NoMarkEdited;
3770 flags |= ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
3771
3772 bool value_changed = false;
3773 if (p_step == NULL)
3774 {
3775 if (InputText(label, buf, IM_ARRAYSIZE(buf), flags))
3776 value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, p_data_when_empty: (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL);
3777 }
3778 else
3779 {
3780 const float button_size = GetFrameHeight();
3781
3782 BeginGroup(); // The only purpose of the group here is to allow the caller to query item data e.g. IsItemActive()
3783 PushID(str_id: label);
3784 SetNextItemWidth(ImMax(lhs: 1.0f, rhs: CalcItemWidth() - (button_size + style.ItemInnerSpacing.x) * 2));
3785 if (InputText(label: "", buf, IM_ARRAYSIZE(buf), flags)) // PushId(label) + "" gives us the expected ID from outside point of view
3786 value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, p_data_when_empty: (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL);
3787 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
3788
3789 // Step buttons
3790 const ImVec2 backup_frame_padding = style.FramePadding;
3791 style.FramePadding.x = style.FramePadding.y;
3792 if (flags & ImGuiInputTextFlags_ReadOnly)
3793 BeginDisabled();
3794 PushItemFlag(option: ImGuiItemFlags_ButtonRepeat, enabled: true);
3795 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
3796 if (ButtonEx(label: "-", size_arg: ImVec2(button_size, button_size)))
3797 {
3798 DataTypeApplyOp(data_type, op: '-', output: p_data, arg1: p_data, arg2: g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3799 value_changed = true;
3800 }
3801 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
3802 if (ButtonEx(label: "+", size_arg: ImVec2(button_size, button_size)))
3803 {
3804 DataTypeApplyOp(data_type, op: '+', output: p_data, arg1: p_data, arg2: g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3805 value_changed = true;
3806 }
3807 PopItemFlag();
3808 if (flags & ImGuiInputTextFlags_ReadOnly)
3809 EndDisabled();
3810
3811 const char* label_end = FindRenderedTextEnd(text: label);
3812 if (label != label_end)
3813 {
3814 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
3815 TextEx(text: label, text_end: label_end);
3816 }
3817 style.FramePadding = backup_frame_padding;
3818
3819 PopID();
3820 EndGroup();
3821 }
3822
3823 g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited;
3824 if (value_changed)
3825 MarkItemEdited(id: g.LastItemData.ID);
3826
3827 return value_changed;
3828}
3829
3830bool 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)
3831{
3832 ImGuiWindow* window = GetCurrentWindow();
3833 if (window->SkipItems)
3834 return false;
3835
3836 ImGuiContext& g = *GImGui;
3837 bool value_changed = false;
3838 BeginGroup();
3839 PushID(str_id: label);
3840 PushMultiItemsWidths(components, width_full: CalcItemWidth());
3841 size_t type_size = GDataTypeInfo[data_type].Size;
3842 for (int i = 0; i < components; i++)
3843 {
3844 PushID(int_id: i);
3845 if (i > 0)
3846 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
3847 value_changed |= InputScalar(label: "", data_type, p_data, p_step, p_step_fast, format, flags);
3848 PopID();
3849 PopItemWidth();
3850 p_data = (void*)((char*)p_data + type_size);
3851 }
3852 PopID();
3853
3854 const char* label_end = FindRenderedTextEnd(text: label);
3855 if (label != label_end)
3856 {
3857 SameLine(offset_from_start_x: 0.0f, spacing: g.Style.ItemInnerSpacing.x);
3858 TextEx(text: label, text_end: label_end);
3859 }
3860
3861 EndGroup();
3862 return value_changed;
3863}
3864
3865bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, const char* format, ImGuiInputTextFlags flags)
3866{
3867 return InputScalar(label, data_type: ImGuiDataType_Float, p_data: (void*)v, p_step: (void*)(step > 0.0f ? &step : NULL), p_step_fast: (void*)(step_fast > 0.0f ? &step_fast : NULL), format, flags);
3868}
3869
3870bool ImGui::InputFloat2(const char* label, float v[2], const char* format, ImGuiInputTextFlags flags)
3871{
3872 return InputScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 2, NULL, NULL, format, flags);
3873}
3874
3875bool ImGui::InputFloat3(const char* label, float v[3], const char* format, ImGuiInputTextFlags flags)
3876{
3877 return InputScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 3, NULL, NULL, format, flags);
3878}
3879
3880bool ImGui::InputFloat4(const char* label, float v[4], const char* format, ImGuiInputTextFlags flags)
3881{
3882 return InputScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 4, NULL, NULL, format, flags);
3883}
3884
3885bool ImGui::InputInt(const char* label, int* v, int step, int step_fast, ImGuiInputTextFlags flags)
3886{
3887 // 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.
3888 const char* format = (flags & ImGuiInputTextFlags_CharsHexadecimal) ? "%08X" : "%d";
3889 return InputScalar(label, data_type: ImGuiDataType_S32, p_data: (void*)v, p_step: (void*)(step > 0 ? &step : NULL), p_step_fast: (void*)(step_fast > 0 ? &step_fast : NULL), format, flags);
3890}
3891
3892bool ImGui::InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags)
3893{
3894 return InputScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 2, NULL, NULL, format: "%d", flags);
3895}
3896
3897bool ImGui::InputInt3(const char* label, int v[3], ImGuiInputTextFlags flags)
3898{
3899 return InputScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 3, NULL, NULL, format: "%d", flags);
3900}
3901
3902bool ImGui::InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags)
3903{
3904 return InputScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 4, NULL, NULL, format: "%d", flags);
3905}
3906
3907bool ImGui::InputDouble(const char* label, double* v, double step, double step_fast, const char* format, ImGuiInputTextFlags flags)
3908{
3909 return InputScalar(label, data_type: ImGuiDataType_Double, p_data: (void*)v, p_step: (void*)(step > 0.0 ? &step : NULL), p_step_fast: (void*)(step_fast > 0.0 ? &step_fast : NULL), format, flags);
3910}
3911
3912//-------------------------------------------------------------------------
3913// [SECTION] Widgets: InputText, InputTextMultiline, InputTextWithHint
3914//-------------------------------------------------------------------------
3915// - imstb_textedit.h include
3916// - InputText()
3917// - InputTextWithHint()
3918// - InputTextMultiline()
3919// - InputTextGetCharInfo() [Internal]
3920// - InputTextReindexLines() [Internal]
3921// - InputTextReindexLinesRange() [Internal]
3922// - InputTextEx() [Internal]
3923// - DebugNodeInputTextState() [Internal]
3924//-------------------------------------------------------------------------
3925
3926namespace ImStb
3927{
3928#include "imstb_textedit.h"
3929}
3930
3931bool ImGui::InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3932{
3933 IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline()
3934 return InputTextEx(label, NULL, buf, buf_size: (int)buf_size, size_arg: ImVec2(0, 0), flags, callback, user_data);
3935}
3936
3937bool ImGui::InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3938{
3939 return InputTextEx(label, NULL, buf, buf_size: (int)buf_size, size_arg: size, flags: flags | ImGuiInputTextFlags_Multiline, callback, user_data);
3940}
3941
3942bool ImGui::InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3943{
3944 IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() or InputTextEx() manually if you need multi-line + hint.
3945 return InputTextEx(label, hint, buf, buf_size: (int)buf_size, size_arg: ImVec2(0, 0), flags, callback, user_data);
3946}
3947
3948// This is only used in the path where the multiline widget is inactive.
3949static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end)
3950{
3951 int line_count = 0;
3952 const char* s = text_begin;
3953 while (true)
3954 {
3955 const char* s_eol = strchr(s: s, c: '\n');
3956 line_count++;
3957 if (s_eol == NULL)
3958 {
3959 s = s + ImStrlen(s: s);
3960 break;
3961 }
3962 s = s_eol + 1;
3963 }
3964 *out_text_end = s;
3965 return line_count;
3966}
3967
3968// FIXME: Ideally we'd share code with ImFont::CalcTextSizeA()
3969static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end, const char** remaining, ImVec2* out_offset, bool stop_on_new_line)
3970{
3971 ImGuiContext& g = *ctx;
3972 //ImFont* font = g.Font;
3973 ImFontBaked* baked = g.FontBaked;
3974 const float line_height = g.FontSize;
3975 const float scale = line_height / baked->Size;
3976
3977 ImVec2 text_size = ImVec2(0, 0);
3978 float line_width = 0.0f;
3979
3980 const char* s = text_begin;
3981 while (s < text_end)
3982 {
3983 unsigned int c = (unsigned int)*s;
3984 if (c < 0x80)
3985 s += 1;
3986 else
3987 s += ImTextCharFromUtf8(out_char: &c, in_text: s, in_text_end: text_end);
3988
3989 if (c == '\n')
3990 {
3991 text_size.x = ImMax(lhs: text_size.x, rhs: line_width);
3992 text_size.y += line_height;
3993 line_width = 0.0f;
3994 if (stop_on_new_line)
3995 break;
3996 continue;
3997 }
3998 if (c == '\r')
3999 continue;
4000
4001 line_width += baked->GetCharAdvance(c: (ImWchar)c) * scale;
4002 }
4003
4004 if (text_size.x < line_width)
4005 text_size.x = line_width;
4006
4007 if (out_offset)
4008 *out_offset = ImVec2(line_width, text_size.y + line_height); // offset allow for the possibility of sitting after a trailing \n
4009
4010 if (line_width > 0 || text_size.y == 0.0f) // whereas size.y will ignore the trailing \n
4011 text_size.y += line_height;
4012
4013 if (remaining)
4014 *remaining = s;
4015
4016 return text_size;
4017}
4018
4019// 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)
4020// With our UTF-8 use of stb_textedit:
4021// - 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.
4022// - 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).
4023// - ...but we don't use that feature.
4024namespace ImStb
4025{
4026static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->TextLen; }
4027static char STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { IM_ASSERT(idx <= obj->TextLen); return obj->TextSrc[idx]; }
4028static float STB_TEXTEDIT_GETWIDTH(ImGuiInputTextState* obj, int line_start_idx, int char_idx) { unsigned int c; ImTextCharFromUtf8(out_char: &c, in_text: obj->TextSrc + line_start_idx + char_idx, in_text_end: obj->TextSrc + obj->TextLen); if ((ImWchar)c == '\n') return IMSTB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *obj->Ctx; return g.FontBaked->GetCharAdvance(c: (ImWchar)c) * g.FontBakedScale; }
4029static char STB_TEXTEDIT_NEWLINE = '\n';
4030static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx)
4031{
4032 const char* text = obj->TextSrc;
4033 const char* text_remaining = NULL;
4034 const ImVec2 size = InputTextCalcTextSize(ctx: obj->Ctx, text_begin: text + line_start_idx, text_end: text + obj->TextLen, remaining: &text_remaining, NULL, stop_on_new_line: true);
4035 r->x0 = 0.0f;
4036 r->x1 = size.x;
4037 r->baseline_y_delta = size.y;
4038 r->ymin = 0.0f;
4039 r->ymax = size.y;
4040 r->num_chars = (int)(text_remaining - (text + line_start_idx));
4041}
4042
4043#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL
4044#define IMSTB_TEXTEDIT_GETPREVCHARINDEX IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL
4045
4046static int IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx)
4047{
4048 if (idx >= obj->TextLen)
4049 return obj->TextLen + 1;
4050 unsigned int c;
4051 return idx + ImTextCharFromUtf8(out_char: &c, in_text: obj->TextSrc + idx, in_text_end: obj->TextSrc + obj->TextLen);
4052}
4053
4054static int IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx)
4055{
4056 if (idx <= 0)
4057 return -1;
4058 const char* p = ImTextFindPreviousUtf8Codepoint(in_text_start: obj->TextSrc, in_text_curr: obj->TextSrc + idx);
4059 return (int)(p - obj->TextSrc);
4060}
4061
4062static bool ImCharIsSeparatorW(unsigned int c)
4063{
4064 static const unsigned int separator_list[] =
4065 {
4066 ',', 0x3001, '.', 0x3002, ';', 0xFF1B, '(', 0xFF08, ')', 0xFF09, '{', 0xFF5B, '}', 0xFF5D,
4067 '[', 0x300C, ']', 0x300D, '|', 0xFF5C, '!', 0xFF01, '\\', 0xFFE5, '/', 0x30FB, 0xFF0F,
4068 '\n', '\r',
4069 };
4070 for (unsigned int separator : separator_list)
4071 if (c == separator)
4072 return true;
4073 return false;
4074}
4075
4076static int is_word_boundary_from_right(ImGuiInputTextState* obj, int idx)
4077{
4078 // 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.
4079 if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
4080 return 0;
4081
4082 const char* curr_p = obj->TextSrc + idx;
4083 const char* prev_p = ImTextFindPreviousUtf8Codepoint(in_text_start: obj->TextSrc, in_text_curr: curr_p);
4084 unsigned int curr_c; ImTextCharFromUtf8(out_char: &curr_c, in_text: curr_p, in_text_end: obj->TextSrc + obj->TextLen);
4085 unsigned int prev_c; ImTextCharFromUtf8(out_char: &prev_c, in_text: prev_p, in_text_end: obj->TextSrc + obj->TextLen);
4086
4087 bool prev_white = ImCharIsBlankW(c: prev_c);
4088 bool prev_separ = ImCharIsSeparatorW(c: prev_c);
4089 bool curr_white = ImCharIsBlankW(c: curr_c);
4090 bool curr_separ = ImCharIsSeparatorW(c: curr_c);
4091 return ((prev_white || prev_separ) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ);
4092}
4093static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx)
4094{
4095 if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
4096 return 0;
4097
4098 const char* curr_p = obj->TextSrc + idx;
4099 const char* prev_p = ImTextFindPreviousUtf8Codepoint(in_text_start: obj->TextSrc, in_text_curr: curr_p);
4100 unsigned int prev_c; ImTextCharFromUtf8(out_char: &prev_c, in_text: curr_p, in_text_end: obj->TextSrc + obj->TextLen);
4101 unsigned int curr_c; ImTextCharFromUtf8(out_char: &curr_c, in_text: prev_p, in_text_end: obj->TextSrc + obj->TextLen);
4102
4103 bool prev_white = ImCharIsBlankW(c: prev_c);
4104 bool prev_separ = ImCharIsSeparatorW(c: prev_c);
4105 bool curr_white = ImCharIsBlankW(c: curr_c);
4106 bool curr_separ = ImCharIsSeparatorW(c: curr_c);
4107 return ((prev_white) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ);
4108}
4109static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(ImGuiInputTextState* obj, int idx)
4110{
4111 idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx);
4112 while (idx >= 0 && !is_word_boundary_from_right(obj, idx))
4113 idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx);
4114 return idx < 0 ? 0 : idx;
4115}
4116static int STB_TEXTEDIT_MOVEWORDRIGHT_MAC(ImGuiInputTextState* obj, int idx)
4117{
4118 int len = obj->TextLen;
4119 idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4120 while (idx < len && !is_word_boundary_from_left(obj, idx))
4121 idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4122 return idx > len ? len : idx;
4123}
4124static int STB_TEXTEDIT_MOVEWORDRIGHT_WIN(ImGuiInputTextState* obj, int idx)
4125{
4126 idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4127 int len = obj->TextLen;
4128 while (idx < len && !is_word_boundary_from_right(obj, idx))
4129 idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4130 return idx > len ? len : idx;
4131}
4132static 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); }
4133#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h
4134#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL
4135
4136static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n)
4137{
4138 // Offset remaining text (+ copy zero terminator)
4139 IM_ASSERT(obj->TextSrc == obj->TextA.Data);
4140 char* dst = obj->TextA.Data + pos;
4141 char* src = obj->TextA.Data + pos + n;
4142 memmove(dest: dst, src: src, n: obj->TextLen - n - pos + 1);
4143 obj->Edited = true;
4144 obj->TextLen -= n;
4145}
4146
4147static bool STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const char* new_text, int new_text_len)
4148{
4149 const bool is_resizable = (obj->Flags & ImGuiInputTextFlags_CallbackResize) != 0;
4150 const int text_len = obj->TextLen;
4151 IM_ASSERT(pos <= text_len);
4152
4153 if (!is_resizable && (new_text_len + obj->TextLen + 1 > obj->BufCapacity))
4154 return false;
4155
4156 // Grow internal buffer if needed
4157 IM_ASSERT(obj->TextSrc == obj->TextA.Data);
4158 if (new_text_len + text_len + 1 > obj->TextA.Size)
4159 {
4160 if (!is_resizable)
4161 return false;
4162 obj->TextA.resize(new_size: text_len + ImClamp(v: new_text_len, mn: 32, mx: ImMax(lhs: 256, rhs: new_text_len)) + 1);
4163 obj->TextSrc = obj->TextA.Data;
4164 }
4165
4166 char* text = obj->TextA.Data;
4167 if (pos != text_len)
4168 memmove(dest: text + pos + new_text_len, src: text + pos, n: (size_t)(text_len - pos));
4169 memcpy(dest: text + pos, src: new_text, n: (size_t)new_text_len);
4170
4171 obj->Edited = true;
4172 obj->TextLen += new_text_len;
4173 obj->TextA[obj->TextLen] = '\0';
4174
4175 return true;
4176}
4177
4178// 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)
4179#define STB_TEXTEDIT_K_LEFT 0x200000 // keyboard input to move cursor left
4180#define STB_TEXTEDIT_K_RIGHT 0x200001 // keyboard input to move cursor right
4181#define STB_TEXTEDIT_K_UP 0x200002 // keyboard input to move cursor up
4182#define STB_TEXTEDIT_K_DOWN 0x200003 // keyboard input to move cursor down
4183#define STB_TEXTEDIT_K_LINESTART 0x200004 // keyboard input to move cursor to start of line
4184#define STB_TEXTEDIT_K_LINEEND 0x200005 // keyboard input to move cursor to end of line
4185#define STB_TEXTEDIT_K_TEXTSTART 0x200006 // keyboard input to move cursor to start of text
4186#define STB_TEXTEDIT_K_TEXTEND 0x200007 // keyboard input to move cursor to end of text
4187#define STB_TEXTEDIT_K_DELETE 0x200008 // keyboard input to delete selection or character under cursor
4188#define STB_TEXTEDIT_K_BACKSPACE 0x200009 // keyboard input to delete selection or character left of cursor
4189#define STB_TEXTEDIT_K_UNDO 0x20000A // keyboard input to perform undo
4190#define STB_TEXTEDIT_K_REDO 0x20000B // keyboard input to perform redo
4191#define STB_TEXTEDIT_K_WORDLEFT 0x20000C // keyboard input to move cursor left one word
4192#define STB_TEXTEDIT_K_WORDRIGHT 0x20000D // keyboard input to move cursor right one word
4193#define STB_TEXTEDIT_K_PGUP 0x20000E // keyboard input to move cursor up a page
4194#define STB_TEXTEDIT_K_PGDOWN 0x20000F // keyboard input to move cursor down a page
4195#define STB_TEXTEDIT_K_SHIFT 0x400000
4196
4197#define IMSTB_TEXTEDIT_IMPLEMENTATION
4198#define IMSTB_TEXTEDIT_memmove memmove
4199#include "imstb_textedit.h"
4200
4201// stb_textedit internally allows for a single undo record to do addition and deletion, but somehow, calling
4202// the stb_textedit_paste() function creates two separate records, so we perform it manually. (FIXME: Report to nothings/stb?)
4203static void stb_textedit_replace(ImGuiInputTextState* str, STB_TexteditState* state, const IMSTB_TEXTEDIT_CHARTYPE* text, int text_len)
4204{
4205 stb_text_makeundo_replace(str, state, where: 0, old_length: str->TextLen, new_length: text_len);
4206 ImStb::STB_TEXTEDIT_DELETECHARS(obj: str, pos: 0, n: str->TextLen);
4207 state->cursor = state->select_start = state->select_end = 0;
4208 if (text_len <= 0)
4209 return;
4210 if (ImStb::STB_TEXTEDIT_INSERTCHARS(obj: str, pos: 0, new_text: text, new_text_len: text_len))
4211 {
4212 state->cursor = state->select_start = state->select_end = text_len;
4213 state->has_preferred_x = 0;
4214 return;
4215 }
4216 IM_ASSERT(0); // Failed to insert character, normally shouldn't happen because of how we currently use stb_textedit_replace()
4217}
4218
4219} // namespace ImStb
4220
4221// We added an extra indirection where 'Stb' is heap-allocated, in order facilitate the work of bindings generators.
4222ImGuiInputTextState::ImGuiInputTextState()
4223{
4224 memset(s: this, c: 0, n: sizeof(*this));
4225 Stb = IM_NEW(ImStbTexteditState);
4226 memset(s: Stb, c: 0, n: sizeof(*Stb));
4227}
4228
4229ImGuiInputTextState::~ImGuiInputTextState()
4230{
4231 IM_DELETE(p: Stb);
4232}
4233
4234void ImGuiInputTextState::OnKeyPressed(int key)
4235{
4236 stb_textedit_key(str: this, state: Stb, key);
4237 CursorFollow = true;
4238 CursorAnimReset();
4239}
4240
4241void ImGuiInputTextState::OnCharPressed(unsigned int c)
4242{
4243 // Convert the key to a UTF8 byte sequence.
4244 // The changes we had to make to stb_textedit_key made it very much UTF-8 specific which is not too great.
4245 char utf8[5];
4246 ImTextCharToUtf8(out_buf: utf8, c);
4247 stb_textedit_text(str: this, state: Stb, text: utf8, text_len: (int)ImStrlen(s: utf8));
4248 CursorFollow = true;
4249 CursorAnimReset();
4250}
4251
4252// Those functions are not inlined in imgui_internal.h, allowing us to hide ImStbTexteditState from that header.
4253void ImGuiInputTextState::CursorAnimReset() { CursorAnim = -0.30f; } // After a user-input the cursor stays on for a while without blinking
4254void ImGuiInputTextState::CursorClamp() { Stb->cursor = ImMin(lhs: Stb->cursor, rhs: TextLen); Stb->select_start = ImMin(lhs: Stb->select_start, rhs: TextLen); Stb->select_end = ImMin(lhs: Stb->select_end, rhs: TextLen); }
4255bool ImGuiInputTextState::HasSelection() const { return Stb->select_start != Stb->select_end; }
4256void ImGuiInputTextState::ClearSelection() { Stb->select_start = Stb->select_end = Stb->cursor; }
4257int ImGuiInputTextState::GetCursorPos() const { return Stb->cursor; }
4258int ImGuiInputTextState::GetSelectionStart() const { return Stb->select_start; }
4259int ImGuiInputTextState::GetSelectionEnd() const { return Stb->select_end; }
4260void ImGuiInputTextState::SelectAll() { Stb->select_start = 0; Stb->cursor = Stb->select_end = TextLen; Stb->has_preferred_x = 0; }
4261void ImGuiInputTextState::ReloadUserBufAndSelectAll() { WantReloadUserBuf = true; ReloadSelectionStart = 0; ReloadSelectionEnd = INT_MAX; }
4262void ImGuiInputTextState::ReloadUserBufAndKeepSelection() { WantReloadUserBuf = true; ReloadSelectionStart = Stb->select_start; ReloadSelectionEnd = Stb->select_end; }
4263void ImGuiInputTextState::ReloadUserBufAndMoveToEnd() { WantReloadUserBuf = true; ReloadSelectionStart = ReloadSelectionEnd = INT_MAX; }
4264
4265ImGuiInputTextCallbackData::ImGuiInputTextCallbackData()
4266{
4267 memset(s: this, c: 0, n: sizeof(*this));
4268}
4269
4270// Public API to manipulate UTF-8 text from within a callback.
4271// FIXME: The existence of this rarely exercised code path is a bit of a nuisance.
4272// Historically they existed because STB_TEXTEDIT_INSERTCHARS() etc. worked on our ImWchar
4273// buffer, but nowadays they both work on UTF-8 data. Should aim to merge both.
4274void ImGuiInputTextCallbackData::DeleteChars(int pos, int bytes_count)
4275{
4276 IM_ASSERT(pos + bytes_count <= BufTextLen);
4277 char* dst = Buf + pos;
4278 const char* src = Buf + pos + bytes_count;
4279 memmove(dest: dst, src: src, n: BufTextLen - bytes_count - pos + 1);
4280
4281 if (CursorPos >= pos + bytes_count)
4282 CursorPos -= bytes_count;
4283 else if (CursorPos >= pos)
4284 CursorPos = pos;
4285 SelectionStart = SelectionEnd = CursorPos;
4286 BufDirty = true;
4287 BufTextLen -= bytes_count;
4288}
4289
4290void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, const char* new_text_end)
4291{
4292 // Accept null ranges
4293 if (new_text == new_text_end)
4294 return;
4295
4296 ImGuiContext& g = *Ctx;
4297 ImGuiInputTextState* obj = &g.InputTextState;
4298 IM_ASSERT(obj->ID != 0 && g.ActiveId == obj->ID);
4299
4300 // Grow internal buffer if needed
4301 const bool is_resizable = (Flags & ImGuiInputTextFlags_CallbackResize) != 0;
4302 const int new_text_len = new_text_end ? (int)(new_text_end - new_text) : (int)ImStrlen(s: new_text);
4303 if (new_text_len + BufTextLen + 1 > obj->TextA.Size && (Flags & ImGuiInputTextFlags_ReadOnly) == 0)
4304 {
4305 if (!is_resizable)
4306 return;
4307
4308 IM_ASSERT(Buf == obj->TextA.Data);
4309 int new_buf_size = BufTextLen + ImClamp(v: new_text_len * 4, mn: 32, mx: ImMax(lhs: 256, rhs: new_text_len)) + 1;
4310 obj->TextA.resize(new_size: new_buf_size + 1);
4311 obj->TextSrc = obj->TextA.Data;
4312 Buf = obj->TextA.Data;
4313 BufSize = obj->BufCapacity = new_buf_size;
4314 }
4315
4316 if (BufTextLen != pos)
4317 memmove(dest: Buf + pos + new_text_len, src: Buf + pos, n: (size_t)(BufTextLen - pos));
4318 memcpy(dest: Buf + pos, src: new_text, n: (size_t)new_text_len * sizeof(char));
4319 Buf[BufTextLen + new_text_len] = '\0';
4320
4321 if (CursorPos >= pos)
4322 CursorPos += new_text_len;
4323 SelectionStart = SelectionEnd = CursorPos;
4324 BufDirty = true;
4325 BufTextLen += new_text_len;
4326}
4327
4328void ImGui::PushPasswordFont()
4329{
4330 ImGuiContext& g = *GImGui;
4331 ImFontBaked* backup = &g.InputTextPasswordFontBackupBaked;
4332 IM_ASSERT(backup->IndexAdvanceX.Size == 0 && backup->IndexLookup.Size == 0);
4333 ImFontGlyph* glyph = g.FontBaked->FindGlyph(c: '*');
4334 g.InputTextPasswordFontBackupFlags = g.Font->Flags;
4335 backup->FallbackGlyphIndex = g.FontBaked->FallbackGlyphIndex;
4336 backup->FallbackAdvanceX = g.FontBaked->FallbackAdvanceX;
4337 backup->IndexLookup.swap(rhs&: g.FontBaked->IndexLookup);
4338 backup->IndexAdvanceX.swap(rhs&: g.FontBaked->IndexAdvanceX);
4339 g.Font->Flags |= ImFontFlags_NoLoadGlyphs;
4340 g.FontBaked->FallbackGlyphIndex = g.FontBaked->Glyphs.index_from_ptr(it: glyph);
4341 g.FontBaked->FallbackAdvanceX = glyph->AdvanceX;
4342}
4343
4344void ImGui::PopPasswordFont()
4345{
4346 ImGuiContext& g = *GImGui;
4347 ImFontBaked* backup = &g.InputTextPasswordFontBackupBaked;
4348 g.Font->Flags = g.InputTextPasswordFontBackupFlags;
4349 g.FontBaked->FallbackGlyphIndex = backup->FallbackGlyphIndex;
4350 g.FontBaked->FallbackAdvanceX = backup->FallbackAdvanceX;
4351 g.FontBaked->IndexLookup.swap(rhs&: backup->IndexLookup);
4352 g.FontBaked->IndexAdvanceX.swap(rhs&: backup->IndexAdvanceX);
4353 IM_ASSERT(backup->IndexAdvanceX.Size == 0 && backup->IndexLookup.Size == 0);
4354}
4355
4356// Return false to discard a character.
4357static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard)
4358{
4359 unsigned int c = *p_char;
4360
4361 // Filter non-printable (NB: isprint is unreliable! see #2467)
4362 bool apply_named_filters = true;
4363 if (c < 0x20)
4364 {
4365 bool pass = false;
4366 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)
4367 if (c == '\n' && input_source_is_clipboard && (flags & ImGuiInputTextFlags_Multiline) == 0) // In single line mode, replace \n with a space
4368 {
4369 c = *p_char = ' ';
4370 pass = true;
4371 }
4372 pass |= (c == '\n') && (flags & ImGuiInputTextFlags_Multiline) != 0;
4373 pass |= (c == '\t') && (flags & ImGuiInputTextFlags_AllowTabInput) != 0;
4374 if (!pass)
4375 return false;
4376 apply_named_filters = false; // Override named filters below so newline and tabs can still be inserted.
4377 }
4378
4379 if (input_source_is_clipboard == false)
4380 {
4381 // We ignore Ascii representation of delete (emitted from Backspace on OSX, see #2578, #2817)
4382 if (c == 127)
4383 return false;
4384
4385 // Filter private Unicode range. GLFW on OSX seems to send private characters for special keys like arrow keys (FIXME)
4386 if (c >= 0xE000 && c <= 0xF8FF)
4387 return false;
4388 }
4389
4390 // Filter Unicode ranges we are not handling in this build
4391 if (c > IM_UNICODE_CODEPOINT_MAX)
4392 return false;
4393
4394 // Generic named filters
4395 if (apply_named_filters && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint)))
4396 {
4397 // 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 '.'.
4398 // The standard mandate that programs starts in the "C" locale where the decimal point is '.'.
4399 // 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.
4400 // Change the default decimal_point with:
4401 // ImGui::GetPlatformIO()->Platform_LocaleDecimalPoint = *localeconv()->decimal_point;
4402 // 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.
4403 ImGuiContext& g = *ctx;
4404 const unsigned c_decimal_point = (unsigned int)g.PlatformIO.Platform_LocaleDecimalPoint;
4405 if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint))
4406 if (c == '.' || c == ',')
4407 c = c_decimal_point;
4408
4409 // Full-width -> half-width conversion for numeric fields (https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
4410 // While this is mostly convenient, this has the side-effect for uninformed users accidentally inputting full-width characters that they may
4411 // scratch their head as to why it works in numerical fields vs in generic text fields it would require support in the font.
4412 if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | ImGuiInputTextFlags_CharsHexadecimal))
4413 if (c >= 0xFF01 && c <= 0xFF5E)
4414 c = c - 0xFF01 + 0x21;
4415
4416 // Allow 0-9 . - + * /
4417 if (flags & ImGuiInputTextFlags_CharsDecimal)
4418 if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/'))
4419 return false;
4420
4421 // Allow 0-9 . - + * / e E
4422 if (flags & ImGuiInputTextFlags_CharsScientific)
4423 if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/') && (c != 'e') && (c != 'E'))
4424 return false;
4425
4426 // Allow 0-9 a-F A-F
4427 if (flags & ImGuiInputTextFlags_CharsHexadecimal)
4428 if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
4429 return false;
4430
4431 // Turn a-z into A-Z
4432 if (flags & ImGuiInputTextFlags_CharsUppercase)
4433 if (c >= 'a' && c <= 'z')
4434 c += (unsigned int)('A' - 'a');
4435
4436 if (flags & ImGuiInputTextFlags_CharsNoBlank)
4437 if (ImCharIsBlankW(c))
4438 return false;
4439
4440 *p_char = c;
4441 }
4442
4443 // Custom callback filter
4444 if (flags & ImGuiInputTextFlags_CallbackCharFilter)
4445 {
4446 ImGuiContext& g = *GImGui;
4447 ImGuiInputTextCallbackData callback_data;
4448 callback_data.Ctx = &g;
4449 callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter;
4450 callback_data.EventChar = (ImWchar)c;
4451 callback_data.Flags = flags;
4452 callback_data.UserData = user_data;
4453 if (callback(&callback_data) != 0)
4454 return false;
4455 *p_char = callback_data.EventChar;
4456 if (!callback_data.EventChar)
4457 return false;
4458 }
4459
4460 return true;
4461}
4462
4463// Find the shortest single replacement we can make to get from old_buf to new_buf
4464// Note that this doesn't directly alter state->TextA, state->TextLen. They are expected to be made valid separately.
4465// 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.
4466static void InputTextReconcileUndoState(ImGuiInputTextState* state, const char* old_buf, int old_length, const char* new_buf, int new_length)
4467{
4468 const int shorter_length = ImMin(lhs: old_length, rhs: new_length);
4469 int first_diff;
4470 for (first_diff = 0; first_diff < shorter_length; first_diff++)
4471 if (old_buf[first_diff] != new_buf[first_diff])
4472 break;
4473 if (first_diff == old_length && first_diff == new_length)
4474 return;
4475
4476 int old_last_diff = old_length - 1;
4477 int new_last_diff = new_length - 1;
4478 for (; old_last_diff >= first_diff && new_last_diff >= first_diff; old_last_diff--, new_last_diff--)
4479 if (old_buf[old_last_diff] != new_buf[new_last_diff])
4480 break;
4481
4482 const int insert_len = new_last_diff - first_diff + 1;
4483 const int delete_len = old_last_diff - first_diff + 1;
4484 if (insert_len > 0 || delete_len > 0)
4485 if (IMSTB_TEXTEDIT_CHARTYPE* p = stb_text_createundo(state: &state->Stb->undostate, pos: first_diff, insert_len: delete_len, delete_len: insert_len))
4486 for (int i = 0; i < delete_len; i++)
4487 p[i] = old_buf[first_diff + i];
4488}
4489
4490// As InputText() retain textual data and we currently provide a path for user to not retain it (via local variables)
4491// we need some form of hook to reapply data back to user buffer on deactivation frame. (#4714)
4492// It would be more desirable that we discourage users from taking advantage of the "user not retaining data" trick,
4493// but that more likely be attractive when we do have _NoLiveEdit flag available.
4494void ImGui::InputTextDeactivateHook(ImGuiID id)
4495{
4496 ImGuiContext& g = *GImGui;
4497 ImGuiInputTextState* state = &g.InputTextState;
4498 if (id == 0 || state->ID != id)
4499 return;
4500 g.InputTextDeactivatedState.ID = state->ID;
4501 if (state->Flags & ImGuiInputTextFlags_ReadOnly)
4502 {
4503 g.InputTextDeactivatedState.TextA.resize(new_size: 0); // In theory this data won't be used, but clear to be neat.
4504 }
4505 else
4506 {
4507 IM_ASSERT(state->TextA.Data != 0);
4508 IM_ASSERT(state->TextA[state->TextLen] == 0);
4509 g.InputTextDeactivatedState.TextA.resize(new_size: state->TextLen + 1);
4510 memcpy(dest: g.InputTextDeactivatedState.TextA.Data, src: state->TextA.Data, n: state->TextLen + 1);
4511 }
4512}
4513
4514// Edit a string of text
4515// - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!".
4516// This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match
4517// Note that in std::string world, capacity() would omit 1 byte used by the zero-terminator.
4518// - 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.
4519// - If you want to use ImGui::InputText() with std::string, see misc/cpp/imgui_stdlib.h
4520// (FIXME: Rather confusing and messy function, among the worse part of our codebase, expecting to rewrite a V2 at some point.. Partly because we are
4521// doing UTF8 > U16 > UTF8 conversions on the go to easily interface with stb_textedit. Ideally should stay in UTF-8 all the time. See https://github.com/nothings/stb/issues/188)
4522bool 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)
4523{
4524 ImGuiWindow* window = GetCurrentWindow();
4525 if (window->SkipItems)
4526 return false;
4527
4528 IM_ASSERT(buf != NULL && buf_size >= 0);
4529 IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys)
4530 IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key)
4531 IM_ASSERT(!((flags & ImGuiInputTextFlags_ElideLeft) && (flags & ImGuiInputTextFlags_Multiline))); // Multiline will not work with left-trimming
4532
4533 ImGuiContext& g = *GImGui;
4534 ImGuiIO& io = g.IO;
4535 const ImGuiStyle& style = g.Style;
4536
4537 const bool RENDER_SELECTION_WHEN_INACTIVE = false;
4538 const bool is_multiline = (flags & ImGuiInputTextFlags_Multiline) != 0;
4539
4540 if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope (including the scrollbar)
4541 BeginGroup();
4542 const ImGuiID id = window->GetID(str: label);
4543 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
4544 const ImVec2 frame_size = CalcItemSize(size: size_arg, default_w: CalcItemWidth(), default_h: (is_multiline ? g.FontSize * 8.0f : label_size.y) + style.FramePadding.y * 2.0f); // Arbitrary default of 8 lines high for multi-line
4545 const ImVec2 total_size = ImVec2(frame_size.x + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), frame_size.y);
4546
4547 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
4548 const ImRect total_bb(frame_bb.Min, frame_bb.Min + total_size);
4549
4550 ImGuiWindow* draw_window = window;
4551 ImVec2 inner_size = frame_size;
4552 ImGuiLastItemData item_data_backup;
4553 if (is_multiline)
4554 {
4555 ImVec2 backup_pos = window->DC.CursorPos;
4556 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
4557 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: ImGuiItemFlags_Inputable))
4558 {
4559 EndGroup();
4560 return false;
4561 }
4562 item_data_backup = g.LastItemData;
4563 window->DC.CursorPos = backup_pos;
4564
4565 // Prevent NavActivation from Tabbing when our widget accepts Tab inputs: this allows cycling through widgets without stopping.
4566 if (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_FromTabbing) && (flags & ImGuiInputTextFlags_AllowTabInput))
4567 g.NavActivateId = 0;
4568
4569 // Prevent NavActivate reactivating in BeginChild() when we are already active.
4570 const ImGuiID backup_activate_id = g.NavActivateId;
4571 if (g.ActiveId == id) // Prevent reactivation
4572 g.NavActivateId = 0;
4573
4574 // We reproduce the contents of BeginChildFrame() in order to provide 'label' so our window internal data are easier to read/debug.
4575 PushStyleColor(idx: ImGuiCol_ChildBg, col: style.Colors[ImGuiCol_FrameBg]);
4576 PushStyleVar(idx: ImGuiStyleVar_ChildRounding, val: style.FrameRounding);
4577 PushStyleVar(idx: ImGuiStyleVar_ChildBorderSize, val: style.FrameBorderSize);
4578 PushStyleVar(idx: ImGuiStyleVar_WindowPadding, val: ImVec2(0, 0)); // Ensure no clip rect so mouse hover can reach FramePadding edges
4579 bool child_visible = BeginChildEx(name: label, id, size_arg: frame_bb.GetSize(), child_flags: ImGuiChildFlags_Borders, window_flags: ImGuiWindowFlags_NoMove);
4580 g.NavActivateId = backup_activate_id;
4581 PopStyleVar(count: 3);
4582 PopStyleColor();
4583 if (!child_visible)
4584 {
4585 EndChild();
4586 EndGroup();
4587 return false;
4588 }
4589 draw_window = g.CurrentWindow; // Child window
4590 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.
4591 draw_window->DC.CursorPos += style.FramePadding;
4592 inner_size.x -= draw_window->ScrollbarSizes.x;
4593 }
4594 else
4595 {
4596 // Support for internal ImGuiInputTextFlags_MergedItem flag, which could be redesigned as an ItemFlags if needed (with test performed in ItemAdd)
4597 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
4598 if (!(flags & ImGuiInputTextFlags_MergedItem))
4599 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: ImGuiItemFlags_Inputable))
4600 return false;
4601 }
4602
4603 // Ensure mouse cursor is set even after switching to keyboard/gamepad mode. May generalize further? (#6417)
4604 bool hovered = ItemHoverable(bb: frame_bb, id, item_flags: g.LastItemData.ItemFlags | ImGuiItemFlags_NoNavDisableMouseHover);
4605 if (hovered)
4606 SetMouseCursor(ImGuiMouseCursor_TextInput);
4607 if (hovered && g.NavHighlightItemUnderNav)
4608 hovered = false;
4609
4610 // We are only allowed to access the state if we are already the active widget.
4611 ImGuiInputTextState* state = GetInputTextState(id);
4612
4613 if (g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly)
4614 flags |= ImGuiInputTextFlags_ReadOnly;
4615 const bool is_readonly = (flags & ImGuiInputTextFlags_ReadOnly) != 0;
4616 const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0;
4617 const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0;
4618 const bool is_resizable = (flags & ImGuiInputTextFlags_CallbackResize) != 0;
4619 if (is_resizable)
4620 IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag!
4621
4622 const bool input_requested_by_nav = (g.ActiveId != id) && ((g.NavActivateId == id) && ((g.NavActivateFlags & ImGuiActivateFlags_PreferInput) || (g.NavInputSource == ImGuiInputSource_Keyboard)));
4623
4624 const bool user_clicked = hovered && io.MouseClicked[0];
4625 const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == GetWindowScrollbarID(window: draw_window, axis: ImGuiAxis_Y);
4626 const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == GetWindowScrollbarID(window: draw_window, axis: ImGuiAxis_Y);
4627 bool clear_active_id = false;
4628 bool select_all = false;
4629
4630 float scroll_y = is_multiline ? draw_window->Scroll.y : FLT_MAX;
4631
4632 const bool init_reload_from_user_buf = (state != NULL && state->WantReloadUserBuf);
4633 const bool init_changed_specs = (state != NULL && state->Stb->single_line != !is_multiline); // state != NULL means its our state.
4634 const bool init_make_active = (user_clicked || user_scroll_finish || input_requested_by_nav);
4635 const bool init_state = (init_make_active || user_scroll_active);
4636 if (init_reload_from_user_buf)
4637 {
4638 int new_len = (int)ImStrlen(s: buf);
4639 IM_ASSERT(new_len + 1 <= buf_size && "Is your input buffer properly zero-terminated?");
4640 state->WantReloadUserBuf = false;
4641 InputTextReconcileUndoState(state, old_buf: state->TextA.Data, old_length: state->TextLen, new_buf: buf, new_length: new_len);
4642 state->TextA.resize(new_size: buf_size + 1); // we use +1 to make sure that .Data is always pointing to at least an empty string.
4643 state->TextLen = new_len;
4644 memcpy(dest: state->TextA.Data, src: buf, n: state->TextLen + 1);
4645 state->Stb->select_start = state->ReloadSelectionStart;
4646 state->Stb->cursor = state->Stb->select_end = state->ReloadSelectionEnd;
4647 state->CursorClamp();
4648 }
4649 else if ((init_state && g.ActiveId != id) || init_changed_specs)
4650 {
4651 // Access state even if we don't own it yet.
4652 state = &g.InputTextState;
4653 state->CursorAnimReset();
4654
4655 // 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)
4656 InputTextDeactivateHook(id: state->ID);
4657
4658 // Take a copy of the initial buffer value.
4659 // From the moment we focused we are normally ignoring the content of 'buf' (unless we are in read-only mode)
4660 const int buf_len = (int)ImStrlen(s: buf);
4661 IM_ASSERT(buf_len + 1 <= buf_size && "Is your input buffer properly zero-terminated?");
4662 state->TextToRevertTo.resize(new_size: buf_len + 1); // UTF-8. we use +1 to make sure that .Data is always pointing to at least an empty string.
4663 memcpy(dest: state->TextToRevertTo.Data, src: buf, n: buf_len + 1);
4664
4665 // Preserve cursor position and undo/redo stack if we come back to same widget
4666 // FIXME: Since we reworked this on 2022/06, may want to differentiate recycle_cursor vs recycle_undostate?
4667 bool recycle_state = (state->ID == id && !init_changed_specs);
4668 if (recycle_state && (state->TextLen != buf_len || (state->TextA.Data == NULL || strncmp(s1: state->TextA.Data, s2: buf, n: buf_len) != 0)))
4669 recycle_state = false;
4670
4671 // Start edition
4672 state->ID = id;
4673 state->TextLen = buf_len;
4674 if (!is_readonly)
4675 {
4676 state->TextA.resize(new_size: buf_size + 1); // we use +1 to make sure that .Data is always pointing to at least an empty string.
4677 memcpy(dest: state->TextA.Data, src: buf, n: state->TextLen + 1);
4678 }
4679
4680 // Find initial scroll position for right alignment
4681 state->Scroll = ImVec2(0.0f, 0.0f);
4682 if (flags & ImGuiInputTextFlags_ElideLeft)
4683 state->Scroll.x += ImMax(lhs: 0.0f, rhs: CalcTextSize(text: buf).x - frame_size.x + style.FramePadding.x * 2.0f);
4684
4685 // Recycle existing cursor/selection/undo stack but clamp position
4686 // Note a single mouse click will override the cursor/position immediately by calling stb_textedit_click handler.
4687 if (recycle_state)
4688 state->CursorClamp();
4689 else
4690 stb_textedit_initialize_state(state: state->Stb, is_single_line: !is_multiline);
4691
4692 if (!is_multiline)
4693 {
4694 if (flags & ImGuiInputTextFlags_AutoSelectAll)
4695 select_all = true;
4696 if (input_requested_by_nav && (!recycle_state || !(g.NavActivateFlags & ImGuiActivateFlags_TryToPreserveState)))
4697 select_all = true;
4698 if (user_clicked && io.KeyCtrl)
4699 select_all = true;
4700 }
4701
4702 if (flags & ImGuiInputTextFlags_AlwaysOverwrite)
4703 state->Stb->insert_mode = 1; // stb field name is indeed incorrect (see #2863)
4704 }
4705
4706 const bool is_osx = io.ConfigMacOSXBehaviors;
4707 if (g.ActiveId != id && init_make_active)
4708 {
4709 IM_ASSERT(state && state->ID == id);
4710 SetActiveID(id, window);
4711 SetFocusID(id, window);
4712 FocusWindow(window);
4713 }
4714 if (g.ActiveId == id)
4715 {
4716 // Declare some inputs, the other are registered and polled via Shortcut() routing system.
4717 // 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.
4718 const ImGuiKey always_owned_keys[] = { ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_Enter, ImGuiKey_KeypadEnter, ImGuiKey_Delete, ImGuiKey_Backspace, ImGuiKey_Home, ImGuiKey_End };
4719 for (ImGuiKey key : always_owned_keys)
4720 SetKeyOwner(key, owner_id: id);
4721 if (user_clicked)
4722 SetKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id);
4723 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
4724 if (is_multiline || (flags & ImGuiInputTextFlags_CallbackHistory))
4725 {
4726 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
4727 SetKeyOwner(key: ImGuiKey_UpArrow, owner_id: id);
4728 SetKeyOwner(key: ImGuiKey_DownArrow, owner_id: id);
4729 }
4730 if (is_multiline)
4731 {
4732 SetKeyOwner(key: ImGuiKey_PageUp, owner_id: id);
4733 SetKeyOwner(key: ImGuiKey_PageDown, owner_id: id);
4734 }
4735 // FIXME: May be a problem to always steal Alt on OSX, would ideally still allow an uninterrupted Alt down-up to toggle menu
4736 if (is_osx)
4737 SetKeyOwner(key: ImGuiMod_Alt, owner_id: id);
4738
4739 // Expose scroll in a manner that is agnostic to us using a child window
4740 if (is_multiline && state != NULL)
4741 state->Scroll.y = draw_window->Scroll.y;
4742
4743 // Read-only mode always ever read from source buffer. Refresh TextLen when active.
4744 if (is_readonly && state != NULL)
4745 state->TextLen = (int)ImStrlen(s: buf);
4746 //if (is_readonly && state != NULL)
4747 // state->TextA.clear(); // Uncomment to facilitate debugging, but we otherwise prefer to keep/amortize th allocation.
4748 }
4749 if (state != NULL)
4750 state->TextSrc = is_readonly ? buf : state->TextA.Data;
4751
4752 // 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)
4753 if (g.ActiveId == id && state == NULL)
4754 ClearActiveID();
4755
4756 // Release focus when we click outside
4757 if (g.ActiveId == id && io.MouseClicked[0] && !init_state && !init_make_active) //-V560
4758 clear_active_id = true;
4759
4760 // Lock the decision of whether we are going to take the path displaying the cursor or selection
4761 bool render_cursor = (g.ActiveId == id) || (state && user_scroll_active);
4762 bool render_selection = state && (state->HasSelection() || select_all) && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
4763 bool value_changed = false;
4764 bool validated = false;
4765
4766 // Select the buffer to render.
4767 const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state;
4768 bool is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0);
4769
4770 // Password pushes a temporary font with only a fallback glyph
4771 if (is_password && !is_displaying_hint)
4772 PushPasswordFont();
4773
4774 // Process mouse inputs and character inputs
4775 if (g.ActiveId == id)
4776 {
4777 IM_ASSERT(state != NULL);
4778 state->Edited = false;
4779 state->BufCapacity = buf_size;
4780 state->Flags = flags;
4781
4782 // Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget.
4783 // Down the line we should have a cleaner library-wide concept of Selected vs Active.
4784 g.ActiveIdAllowOverlap = !io.MouseDown[0];
4785
4786 // Edit in progress
4787 const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + state->Scroll.x;
4788 const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y) : (g.FontSize * 0.5f));
4789
4790 if (select_all)
4791 {
4792 state->SelectAll();
4793 state->SelectedAllMouseLock = true;
4794 }
4795 else if (hovered && io.MouseClickedCount[0] >= 2 && !io.KeyShift)
4796 {
4797 stb_textedit_click(str: state, state: state->Stb, x: mouse_x, y: mouse_y);
4798 const int multiclick_count = (io.MouseClickedCount[0] - 2);
4799 if ((multiclick_count % 2) == 0)
4800 {
4801 // Double-click: Select word
4802 // We always use the "Mac" word advance for double-click select vs CTRL+Right which use the platform dependent variant:
4803 // FIXME: There are likely many ways to improve this behavior, but there's no "right" behavior (depends on use-case, software, OS)
4804 const bool is_bol = (state->Stb->cursor == 0) || ImStb::STB_TEXTEDIT_GETCHAR(obj: state, idx: state->Stb->cursor - 1) == '\n';
4805 if (STB_TEXT_HAS_SELECTION(state->Stb) || !is_bol)
4806 state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT);
4807 //state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
4808 if (!STB_TEXT_HAS_SELECTION(state->Stb))
4809 ImStb::stb_textedit_prep_selection_at_cursor(state: state->Stb);
4810 state->Stb->cursor = ImStb::STB_TEXTEDIT_MOVEWORDRIGHT_MAC(obj: state, idx: state->Stb->cursor);
4811 state->Stb->select_end = state->Stb->cursor;
4812 ImStb::stb_textedit_clamp(str: state, state: state->Stb);
4813 }
4814 else
4815 {
4816 // Triple-click: Select line
4817 const bool is_eol = ImStb::STB_TEXTEDIT_GETCHAR(obj: state, idx: state->Stb->cursor) == '\n';
4818 state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART);
4819 state->OnKeyPressed(STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT);
4820 state->OnKeyPressed(STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT);
4821 if (!is_eol && is_multiline)
4822 {
4823 ImSwap(a&: state->Stb->select_start, b&: state->Stb->select_end);
4824 state->Stb->cursor = state->Stb->select_end;
4825 }
4826 state->CursorFollow = false;
4827 }
4828 state->CursorAnimReset();
4829 }
4830 else if (io.MouseClicked[0] && !state->SelectedAllMouseLock)
4831 {
4832 if (hovered)
4833 {
4834 if (io.KeyShift)
4835 stb_textedit_drag(str: state, state: state->Stb, x: mouse_x, y: mouse_y);
4836 else
4837 stb_textedit_click(str: state, state: state->Stb, x: mouse_x, y: mouse_y);
4838 state->CursorAnimReset();
4839 }
4840 }
4841 else if (io.MouseDown[0] && !state->SelectedAllMouseLock && (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f))
4842 {
4843 stb_textedit_drag(str: state, state: state->Stb, x: mouse_x, y: mouse_y);
4844 state->CursorAnimReset();
4845 state->CursorFollow = true;
4846 }
4847 if (state->SelectedAllMouseLock && !io.MouseDown[0])
4848 state->SelectedAllMouseLock = false;
4849
4850 // We expect backends to emit a Tab key but some also emit a Tab character which we ignore (#2467, #1336)
4851 // (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)
4852 if ((flags & ImGuiInputTextFlags_AllowTabInput) && !is_readonly)
4853 {
4854 if (Shortcut(key_chord: ImGuiKey_Tab, flags: ImGuiInputFlags_Repeat, owner_id: id))
4855 {
4856 unsigned int c = '\t'; // Insert TAB
4857 if (InputTextFilterCharacter(ctx: &g, p_char: &c, flags, callback, user_data: callback_user_data))
4858 state->OnCharPressed(c);
4859 }
4860 // FIXME: Implement Shift+Tab
4861 /*
4862 if (Shortcut(ImGuiKey_Tab | ImGuiMod_Shift, ImGuiInputFlags_Repeat, id))
4863 {
4864 }
4865 */
4866 }
4867
4868 // Process regular text input (before we check for Return because using some IME will effectively send a Return?)
4869 // 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.
4870 const bool ignore_char_inputs = (io.KeyCtrl && !io.KeyAlt) || (is_osx && io.KeyCtrl);
4871 if (io.InputQueueCharacters.Size > 0)
4872 {
4873 if (!ignore_char_inputs && !is_readonly && !input_requested_by_nav)
4874 for (int n = 0; n < io.InputQueueCharacters.Size; n++)
4875 {
4876 // Insert character if they pass filtering
4877 unsigned int c = (unsigned int)io.InputQueueCharacters[n];
4878 if (c == '\t') // Skip Tab, see above.
4879 continue;
4880 if (InputTextFilterCharacter(ctx: &g, p_char: &c, flags, callback, user_data: callback_user_data))
4881 state->OnCharPressed(c);
4882 }
4883
4884 // Consume characters
4885 io.InputQueueCharacters.resize(new_size: 0);
4886 }
4887 }
4888
4889 // Process other shortcuts/key-presses
4890 bool revert_edit = false;
4891 if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id)
4892 {
4893 IM_ASSERT(state != NULL);
4894
4895 const int row_count_per_page = ImMax(lhs: (int)((inner_size.y - style.FramePadding.y) / g.FontSize), rhs: 1);
4896 state->Stb->row_count_per_page = row_count_per_page;
4897
4898 const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0);
4899 const bool is_wordmove_key_down = is_osx ? io.KeyAlt : io.KeyCtrl; // OS X style: Text editing cursor movement using Alt instead of Ctrl
4900 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
4901
4902 // 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)
4903 // Otherwise we could simply assume that we own the keys as we are active.
4904 const ImGuiInputFlags f_repeat = ImGuiInputFlags_Repeat;
4905 const bool is_cut = (Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiKey_X, flags: f_repeat, owner_id: id) || Shortcut(key_chord: ImGuiMod_Shift | ImGuiKey_Delete, flags: f_repeat, owner_id: id)) && !is_readonly && !is_password && (!is_multiline || state->HasSelection());
4906 const bool is_copy = (Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiKey_C, flags: 0, owner_id: id) || Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiKey_Insert, flags: 0, owner_id: id)) && !is_password && (!is_multiline || state->HasSelection());
4907 const bool is_paste = (Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiKey_V, flags: f_repeat, owner_id: id) || Shortcut(key_chord: ImGuiMod_Shift | ImGuiKey_Insert, flags: f_repeat, owner_id: id)) && !is_readonly;
4908 const bool is_undo = (Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiKey_Z, flags: f_repeat, owner_id: id)) && !is_readonly && is_undoable;
4909 const bool is_redo = (Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiKey_Y, flags: f_repeat, owner_id: id) || Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Z, flags: f_repeat, owner_id: id)) && !is_readonly && is_undoable;
4910 const bool is_select_all = Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiKey_A, flags: 0, owner_id: id);
4911
4912 // 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.
4913 const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0;
4914 const bool is_enter_pressed = IsKeyPressed(key: ImGuiKey_Enter, repeat: true) || IsKeyPressed(key: ImGuiKey_KeypadEnter, repeat: true);
4915 const bool is_gamepad_validate = nav_gamepad_active && (IsKeyPressed(ImGuiKey_NavGamepadActivate, repeat: false) || IsKeyPressed(ImGuiKey_NavGamepadInput, repeat: false));
4916 const bool is_cancel = Shortcut(key_chord: ImGuiKey_Escape, flags: f_repeat, owner_id: id) || (nav_gamepad_active && Shortcut(ImGuiKey_NavGamepadCancel, flags: f_repeat, owner_id: id));
4917
4918 // FIXME: Should use more Shortcut() and reduce IsKeyPressed()+SetKeyOwner(), but requires modifiers combination to be taken account of.
4919 // FIXME-OSX: Missing support for Alt(option)+Right/Left = go to end of line, or next line if already in end of line.
4920 if (IsKeyPressed(key: ImGuiKey_LeftArrow)) { state->OnKeyPressed(key: (is_startend_key_down ? STB_TEXTEDIT_K_LINESTART : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDLEFT : STB_TEXTEDIT_K_LEFT) | k_mask); }
4921 else if (IsKeyPressed(key: ImGuiKey_RightArrow)) { state->OnKeyPressed(key: (is_startend_key_down ? STB_TEXTEDIT_K_LINEEND : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDRIGHT : STB_TEXTEDIT_K_RIGHT) | k_mask); }
4922 else if (IsKeyPressed(key: ImGuiKey_UpArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(window: draw_window, scroll_y: ImMax(lhs: draw_window->Scroll.y - g.FontSize, rhs: 0.0f)); else state->OnKeyPressed(key: (is_startend_key_down ? STB_TEXTEDIT_K_TEXTSTART : STB_TEXTEDIT_K_UP) | k_mask); }
4923 else if (IsKeyPressed(key: ImGuiKey_DownArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(window: draw_window, scroll_y: ImMin(lhs: draw_window->Scroll.y + g.FontSize, rhs: GetScrollMaxY())); else state->OnKeyPressed(key: (is_startend_key_down ? STB_TEXTEDIT_K_TEXTEND : STB_TEXTEDIT_K_DOWN) | k_mask); }
4924 else if (IsKeyPressed(key: ImGuiKey_PageUp) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGUP | k_mask); scroll_y -= row_count_per_page * g.FontSize; }
4925 else if (IsKeyPressed(key: ImGuiKey_PageDown) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGDOWN | k_mask); scroll_y += row_count_per_page * g.FontSize; }
4926 else if (IsKeyPressed(key: ImGuiKey_Home)) { state->OnKeyPressed(key: io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); }
4927 else if (IsKeyPressed(key: ImGuiKey_End)) { state->OnKeyPressed(key: io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); }
4928 else if (IsKeyPressed(key: ImGuiKey_Delete) && !is_readonly && !is_cut)
4929 {
4930 if (!state->HasSelection())
4931 {
4932 // 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)
4933 if (is_wordmove_key_down)
4934 state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
4935 }
4936 state->OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask);
4937 }
4938 else if (IsKeyPressed(key: ImGuiKey_Backspace) && !is_readonly)
4939 {
4940 if (!state->HasSelection())
4941 {
4942 if (is_wordmove_key_down)
4943 state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT);
4944 else if (is_osx && io.KeyCtrl && !io.KeyAlt && !io.KeySuper)
4945 state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT);
4946 }
4947 state->OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask);
4948 }
4949 else if (is_enter_pressed || is_gamepad_validate)
4950 {
4951 // Determine if we turn Enter into a \n character
4952 bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0;
4953 if (!is_multiline || is_gamepad_validate || (ctrl_enter_for_new_line != io.KeyCtrl))
4954 {
4955 validated = true;
4956 if (io.ConfigInputTextEnterKeepActive && !is_multiline)
4957 state->SelectAll(); // No need to scroll
4958 else
4959 clear_active_id = true;
4960 }
4961 else if (!is_readonly)
4962 {
4963 unsigned int c = '\n'; // Insert new line
4964 if (InputTextFilterCharacter(ctx: &g, p_char: &c, flags, callback, user_data: callback_user_data))
4965 state->OnCharPressed(c);
4966 }
4967 }
4968 else if (is_cancel)
4969 {
4970 if (flags & ImGuiInputTextFlags_EscapeClearsAll)
4971 {
4972 if (buf[0] != 0)
4973 {
4974 revert_edit = true;
4975 }
4976 else
4977 {
4978 render_cursor = render_selection = false;
4979 clear_active_id = true;
4980 }
4981 }
4982 else
4983 {
4984 clear_active_id = revert_edit = true;
4985 render_cursor = render_selection = false;
4986 }
4987 }
4988 else if (is_undo || is_redo)
4989 {
4990 state->OnKeyPressed(key: is_undo ? STB_TEXTEDIT_K_UNDO : STB_TEXTEDIT_K_REDO);
4991 state->ClearSelection();
4992 }
4993 else if (is_select_all)
4994 {
4995 state->SelectAll();
4996 state->CursorFollow = true;
4997 }
4998 else if (is_cut || is_copy)
4999 {
5000 // Cut, Copy
5001 if (g.PlatformIO.Platform_SetClipboardTextFn != NULL)
5002 {
5003 // SetClipboardText() only takes null terminated strings + state->TextSrc may point to read-only user buffer, so we need to make a copy.
5004 const int ib = state->HasSelection() ? ImMin(lhs: state->Stb->select_start, rhs: state->Stb->select_end) : 0;
5005 const int ie = state->HasSelection() ? ImMax(lhs: state->Stb->select_start, rhs: state->Stb->select_end) : state->TextLen;
5006 g.TempBuffer.reserve(new_capacity: ie - ib + 1);
5007 memcpy(dest: g.TempBuffer.Data, src: state->TextSrc + ib, n: ie - ib);
5008 g.TempBuffer.Data[ie - ib] = 0;
5009 SetClipboardText(g.TempBuffer.Data);
5010 }
5011 if (is_cut)
5012 {
5013 if (!state->HasSelection())
5014 state->SelectAll();
5015 state->CursorFollow = true;
5016 stb_textedit_cut(str: state, state: state->Stb);
5017 }
5018 }
5019 else if (is_paste)
5020 {
5021 if (const char* clipboard = GetClipboardText())
5022 {
5023 // Filter pasted buffer
5024 const int clipboard_len = (int)ImStrlen(s: clipboard);
5025 ImVector<char> clipboard_filtered;
5026 clipboard_filtered.reserve(new_capacity: clipboard_len + 1);
5027 for (const char* s = clipboard; *s != 0; )
5028 {
5029 unsigned int c;
5030 int in_len = ImTextCharFromUtf8(out_char: &c, in_text: s, NULL);
5031 s += in_len;
5032 if (!InputTextFilterCharacter(ctx: &g, p_char: &c, flags, callback, user_data: callback_user_data, input_source_is_clipboard: true))
5033 continue;
5034 char c_utf8[5];
5035 ImTextCharToUtf8(out_buf: c_utf8, c);
5036 int out_len = (int)ImStrlen(s: c_utf8);
5037 clipboard_filtered.resize(new_size: clipboard_filtered.Size + out_len);
5038 memcpy(dest: clipboard_filtered.Data + clipboard_filtered.Size - out_len, src: c_utf8, n: out_len);
5039 }
5040 if (clipboard_filtered.Size > 0) // If everything was filtered, ignore the pasting operation
5041 {
5042 clipboard_filtered.push_back(v: 0);
5043 stb_textedit_paste(str: state, state: state->Stb, ctext: clipboard_filtered.Data, len: clipboard_filtered.Size - 1);
5044 state->CursorFollow = true;
5045 }
5046 }
5047 }
5048
5049 // Update render selection flag after events have been handled, so selection highlight can be displayed during the same frame.
5050 render_selection |= state->HasSelection() && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
5051 }
5052
5053 // Process callbacks and apply result back to user's buffer.
5054 const char* apply_new_text = NULL;
5055 int apply_new_text_length = 0;
5056 if (g.ActiveId == id)
5057 {
5058 IM_ASSERT(state != NULL);
5059 if (revert_edit && !is_readonly)
5060 {
5061 if (flags & ImGuiInputTextFlags_EscapeClearsAll)
5062 {
5063 // Clear input
5064 IM_ASSERT(buf[0] != 0);
5065 apply_new_text = "";
5066 apply_new_text_length = 0;
5067 value_changed = true;
5068 IMSTB_TEXTEDIT_CHARTYPE empty_string;
5069 stb_textedit_replace(str: state, state: state->Stb, text: &empty_string, text_len: 0);
5070 }
5071 else if (strcmp(s1: buf, s2: state->TextToRevertTo.Data) != 0)
5072 {
5073 apply_new_text = state->TextToRevertTo.Data;
5074 apply_new_text_length = state->TextToRevertTo.Size - 1;
5075
5076 // Restore initial value. Only return true if restoring to the initial value changes the current buffer contents.
5077 // Push records into the undo stack so we can CTRL+Z the revert operation itself
5078 value_changed = true;
5079 stb_textedit_replace(str: state, state: state->Stb, text: state->TextToRevertTo.Data, text_len: state->TextToRevertTo.Size - 1);
5080 }
5081 }
5082
5083 // FIXME-OPT: We always reapply the live buffer back to the input buffer before clearing ActiveId,
5084 // even though strictly speaking it wasn't modified on this frame. Should mark dirty state from the stb_textedit callbacks.
5085 // If we do that, need to ensure that as special case, 'validated == true' also writes back.
5086 // This also allows the user to use InputText() without maintaining any user-side storage.
5087 // (please note that if you use this property along ImGuiInputTextFlags_CallbackResize you can end up with your temporary string object
5088 // unnecessarily allocating once a frame, either store your string data, either if you don't then don't use ImGuiInputTextFlags_CallbackResize).
5089 const bool apply_edit_back_to_user_buffer = true;// !revert_edit || (validated && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0);
5090 if (apply_edit_back_to_user_buffer)
5091 {
5092 // Apply current edited text immediately.
5093 // Note that as soon as the input box is active, the in-widget value gets priority over any underlying modification of the input buffer
5094
5095 // User callback
5096 if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackEdit | ImGuiInputTextFlags_CallbackAlways)) != 0)
5097 {
5098 IM_ASSERT(callback != NULL);
5099
5100 // The reason we specify the usage semantic (Completion/History) is that Completion needs to disable keyboard TABBING at the moment.
5101 ImGuiInputTextFlags event_flag = 0;
5102 ImGuiKey event_key = ImGuiKey_None;
5103 if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && Shortcut(key_chord: ImGuiKey_Tab, flags: 0, owner_id: id))
5104 {
5105 event_flag = ImGuiInputTextFlags_CallbackCompletion;
5106 event_key = ImGuiKey_Tab;
5107 }
5108 else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(key: ImGuiKey_UpArrow))
5109 {
5110 event_flag = ImGuiInputTextFlags_CallbackHistory;
5111 event_key = ImGuiKey_UpArrow;
5112 }
5113 else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(key: ImGuiKey_DownArrow))
5114 {
5115 event_flag = ImGuiInputTextFlags_CallbackHistory;
5116 event_key = ImGuiKey_DownArrow;
5117 }
5118 else if ((flags & ImGuiInputTextFlags_CallbackEdit) && state->Edited)
5119 {
5120 event_flag = ImGuiInputTextFlags_CallbackEdit;
5121 }
5122 else if (flags & ImGuiInputTextFlags_CallbackAlways)
5123 {
5124 event_flag = ImGuiInputTextFlags_CallbackAlways;
5125 }
5126
5127 if (event_flag)
5128 {
5129 ImGuiInputTextCallbackData callback_data;
5130 callback_data.Ctx = &g;
5131 callback_data.EventFlag = event_flag;
5132 callback_data.Flags = flags;
5133 callback_data.UserData = callback_user_data;
5134
5135 // FIXME-OPT: Undo stack reconcile needs a backup of the data until we rework API, see #7925
5136 char* callback_buf = is_readonly ? buf : state->TextA.Data;
5137 IM_ASSERT(callback_buf == state->TextSrc);
5138 state->CallbackTextBackup.resize(new_size: state->TextLen + 1);
5139 memcpy(dest: state->CallbackTextBackup.Data, src: callback_buf, n: state->TextLen + 1);
5140
5141 callback_data.EventKey = event_key;
5142 callback_data.Buf = callback_buf;
5143 callback_data.BufTextLen = state->TextLen;
5144 callback_data.BufSize = state->BufCapacity;
5145 callback_data.BufDirty = false;
5146
5147 const int utf8_cursor_pos = callback_data.CursorPos = state->Stb->cursor;
5148 const int utf8_selection_start = callback_data.SelectionStart = state->Stb->select_start;
5149 const int utf8_selection_end = callback_data.SelectionEnd = state->Stb->select_end;
5150
5151 // Call user code
5152 callback(&callback_data);
5153
5154 // Read back what user may have modified
5155 callback_buf = is_readonly ? buf : state->TextA.Data; // Pointer may have been invalidated by a resize callback
5156 IM_ASSERT(callback_data.Buf == callback_buf); // Invalid to modify those fields
5157 IM_ASSERT(callback_data.BufSize == state->BufCapacity);
5158 IM_ASSERT(callback_data.Flags == flags);
5159 const bool buf_dirty = callback_data.BufDirty;
5160 if (callback_data.CursorPos != utf8_cursor_pos || buf_dirty) { state->Stb->cursor = callback_data.CursorPos; state->CursorFollow = true; }
5161 if (callback_data.SelectionStart != utf8_selection_start || buf_dirty) { state->Stb->select_start = (callback_data.SelectionStart == callback_data.CursorPos) ? state->Stb->cursor : callback_data.SelectionStart; }
5162 if (callback_data.SelectionEnd != utf8_selection_end || buf_dirty) { state->Stb->select_end = (callback_data.SelectionEnd == callback_data.SelectionStart) ? state->Stb->select_start : callback_data.SelectionEnd; }
5163 if (buf_dirty)
5164 {
5165 // Callback may update buffer and thus set buf_dirty even in read-only mode.
5166 IM_ASSERT(callback_data.BufTextLen == (int)ImStrlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text!
5167 InputTextReconcileUndoState(state, old_buf: state->CallbackTextBackup.Data, old_length: state->CallbackTextBackup.Size - 1, new_buf: callback_data.Buf, new_length: callback_data.BufTextLen);
5168 state->TextLen = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen()
5169 state->CursorAnimReset();
5170 }
5171 }
5172 }
5173
5174 // Will copy result string if modified
5175 if (!is_readonly && strcmp(s1: state->TextSrc, s2: buf) != 0)
5176 {
5177 apply_new_text = state->TextSrc;
5178 apply_new_text_length = state->TextLen;
5179 value_changed = true;
5180 }
5181 }
5182 }
5183
5184 // Handle reapplying final data on deactivation (see InputTextDeactivateHook() for details)
5185 if (g.InputTextDeactivatedState.ID == id)
5186 {
5187 if (g.ActiveId != id && IsItemDeactivatedAfterEdit() && !is_readonly && strcmp(s1: g.InputTextDeactivatedState.TextA.Data, s2: buf) != 0)
5188 {
5189 apply_new_text = g.InputTextDeactivatedState.TextA.Data;
5190 apply_new_text_length = g.InputTextDeactivatedState.TextA.Size - 1;
5191 value_changed = true;
5192 //IMGUI_DEBUG_LOG("InputText(): apply Deactivated data for 0x%08X: \"%.*s\".\n", id, apply_new_text_length, apply_new_text);
5193 }
5194 g.InputTextDeactivatedState.ID = 0;
5195 }
5196
5197 // Copy result to user buffer. This can currently only happen when (g.ActiveId == id)
5198 if (apply_new_text != NULL)
5199 {
5200 //// We cannot test for 'backup_current_text_length != apply_new_text_length' here because we have no guarantee that the size
5201 //// of our owned buffer matches the size of the string object held by the user, and by design we allow InputText() to be used
5202 //// without any storage on user's side.
5203 IM_ASSERT(apply_new_text_length >= 0);
5204 if (is_resizable)
5205 {
5206 ImGuiInputTextCallbackData callback_data;
5207 callback_data.Ctx = &g;
5208 callback_data.EventFlag = ImGuiInputTextFlags_CallbackResize;
5209 callback_data.Flags = flags;
5210 callback_data.Buf = buf;
5211 callback_data.BufTextLen = apply_new_text_length;
5212 callback_data.BufSize = ImMax(lhs: buf_size, rhs: apply_new_text_length + 1);
5213 callback_data.UserData = callback_user_data;
5214 callback(&callback_data);
5215 buf = callback_data.Buf;
5216 buf_size = callback_data.BufSize;
5217 apply_new_text_length = ImMin(lhs: callback_data.BufTextLen, rhs: buf_size - 1);
5218 IM_ASSERT(apply_new_text_length <= buf_size);
5219 }
5220 //IMGUI_DEBUG_PRINT("InputText(\"%s\"): apply_new_text length %d\n", label, apply_new_text_length);
5221
5222 // If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size.
5223 ImStrncpy(dst: buf, src: apply_new_text, count: ImMin(lhs: apply_new_text_length + 1, rhs: buf_size));
5224 }
5225
5226 // Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value)
5227 // Otherwise request text input ahead for next frame.
5228 if (g.ActiveId == id && clear_active_id)
5229 ClearActiveID();
5230
5231 // Render frame
5232 if (!is_multiline)
5233 {
5234 RenderNavCursor(bb: frame_bb, id);
5235 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: GetColorU32(idx: ImGuiCol_FrameBg), borders: true, rounding: style.FrameRounding);
5236 }
5237
5238 const ImVec4 clip_rect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + inner_size.x, frame_bb.Min.y + inner_size.y); // Not using frame_bb.Max because we have adjusted size
5239 ImVec2 draw_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding;
5240 ImVec2 text_size(0.0f, 0.0f);
5241
5242 // Set upper limit of single-line InputTextEx() at 2 million characters strings. The current pathological worst case is a long line
5243 // without any carriage return, which would makes ImFont::RenderText() reserve too many vertices and probably crash. Avoid it altogether.
5244 // Note that we only use this limit on single-line InputText(), so a pathologically large line on a InputTextMultiline() would still crash.
5245 const int buf_display_max_length = 2 * 1024 * 1024;
5246 const char* buf_display = buf_display_from_state ? state->TextA.Data : buf; //-V595
5247 const char* buf_display_end = NULL; // We have specialized paths below for setting the length
5248
5249 // Display hint when contents is empty
5250 // At this point we need to handle the possibility that a callback could have modified the underlying buffer (#8368)
5251 const bool new_is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0);
5252 if (new_is_displaying_hint != is_displaying_hint)
5253 {
5254 if (is_password && !is_displaying_hint)
5255 PopPasswordFont();
5256 is_displaying_hint = new_is_displaying_hint;
5257 if (is_password && !is_displaying_hint)
5258 PushPasswordFont();
5259 }
5260 if (is_displaying_hint)
5261 {
5262 buf_display = hint;
5263 buf_display_end = hint + ImStrlen(s: hint);
5264 }
5265
5266 // Render text. We currently only render selection when the widget is active or while scrolling.
5267 // FIXME: We could remove the '&& render_cursor' to keep rendering selection when inactive.
5268 if (render_cursor || render_selection)
5269 {
5270 IM_ASSERT(state != NULL);
5271 if (!is_displaying_hint)
5272 buf_display_end = buf_display + state->TextLen;
5273
5274 // Render text (with cursor and selection)
5275 // This is going to be messy. We need to:
5276 // - Display the text (this alone can be more easily clipped)
5277 // - Handle scrolling, highlight selection, display cursor (those all requires some form of 1d->2d cursor position calculation)
5278 // - Measure text height (for scrollbar)
5279 // 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)
5280 // FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8.
5281 const char* text_begin = buf_display;
5282 const char* text_end = text_begin + state->TextLen;
5283 ImVec2 cursor_offset, select_start_offset;
5284
5285 {
5286 // Find lines numbers straddling cursor and selection min position
5287 int cursor_line_no = render_cursor ? -1 : -1000;
5288 int selmin_line_no = render_selection ? -1 : -1000;
5289 const char* cursor_ptr = render_cursor ? text_begin + state->Stb->cursor : NULL;
5290 const char* selmin_ptr = render_selection ? text_begin + ImMin(lhs: state->Stb->select_start, rhs: state->Stb->select_end) : NULL;
5291
5292 // Count lines and find line number for cursor and selection ends
5293 int line_count = 1;
5294 if (is_multiline)
5295 {
5296 for (const char* s = text_begin; (s = (const char*)ImMemchr(s: s, c: '\n', n: (size_t)(text_end - s))) != NULL; s++)
5297 {
5298 if (cursor_line_no == -1 && s >= cursor_ptr) { cursor_line_no = line_count; }
5299 if (selmin_line_no == -1 && s >= selmin_ptr) { selmin_line_no = line_count; }
5300 line_count++;
5301 }
5302 }
5303 if (cursor_line_no == -1)
5304 cursor_line_no = line_count;
5305 if (selmin_line_no == -1)
5306 selmin_line_no = line_count;
5307
5308 // Calculate 2d position by finding the beginning of the line and measuring distance
5309 cursor_offset.x = InputTextCalcTextSize(ctx: &g, text_begin: ImStrbol(buf_mid_line: cursor_ptr, buf_begin: text_begin), text_end: cursor_ptr).x;
5310 cursor_offset.y = cursor_line_no * g.FontSize;
5311 if (selmin_line_no >= 0)
5312 {
5313 select_start_offset.x = InputTextCalcTextSize(ctx: &g, text_begin: ImStrbol(buf_mid_line: selmin_ptr, buf_begin: text_begin), text_end: selmin_ptr).x;
5314 select_start_offset.y = selmin_line_no * g.FontSize;
5315 }
5316
5317 // Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224)
5318 if (is_multiline)
5319 text_size = ImVec2(inner_size.x, line_count * g.FontSize);
5320 }
5321
5322 // Scroll
5323 if (render_cursor && state->CursorFollow)
5324 {
5325 // Horizontal scroll in chunks of quarter width
5326 if (!(flags & ImGuiInputTextFlags_NoHorizontalScroll))
5327 {
5328 const float scroll_increment_x = inner_size.x * 0.25f;
5329 const float visible_width = inner_size.x - style.FramePadding.x;
5330 if (cursor_offset.x < state->Scroll.x)
5331 state->Scroll.x = IM_TRUNC(ImMax(0.0f, cursor_offset.x - scroll_increment_x));
5332 else if (cursor_offset.x - visible_width >= state->Scroll.x)
5333 state->Scroll.x = IM_TRUNC(cursor_offset.x - visible_width + scroll_increment_x);
5334 }
5335 else
5336 {
5337 state->Scroll.y = 0.0f;
5338 }
5339
5340 // Vertical scroll
5341 if (is_multiline)
5342 {
5343 // Test if cursor is vertically visible
5344 if (cursor_offset.y - g.FontSize < scroll_y)
5345 scroll_y = ImMax(lhs: 0.0f, rhs: cursor_offset.y - g.FontSize);
5346 else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y)
5347 scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f;
5348 const float scroll_max_y = ImMax(lhs: (text_size.y + style.FramePadding.y * 2.0f) - inner_size.y, rhs: 0.0f);
5349 scroll_y = ImClamp(v: scroll_y, mn: 0.0f, mx: scroll_max_y);
5350 draw_pos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag
5351 draw_window->Scroll.y = scroll_y;
5352 }
5353
5354 state->CursorFollow = false;
5355 }
5356
5357 // Draw selection
5358 const ImVec2 draw_scroll = ImVec2(state->Scroll.x, 0.0f);
5359 if (render_selection)
5360 {
5361 const char* text_selected_begin = text_begin + ImMin(lhs: state->Stb->select_start, rhs: state->Stb->select_end);
5362 const char* text_selected_end = text_begin + ImMax(lhs: state->Stb->select_start, rhs: state->Stb->select_end);
5363
5364 ImU32 bg_color = GetColorU32(idx: ImGuiCol_TextSelectedBg, alpha_mul: 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.
5365 float bg_offy_up = is_multiline ? 0.0f : -1.0f; // FIXME: those offsets should be part of the style? they don't play so well with multi-line selection.
5366 float bg_offy_dn = is_multiline ? 0.0f : 2.0f;
5367 ImVec2 rect_pos = draw_pos + select_start_offset - draw_scroll;
5368 for (const char* p = text_selected_begin; p < text_selected_end; )
5369 {
5370 if (rect_pos.y > clip_rect.w + g.FontSize)
5371 break;
5372 if (rect_pos.y < clip_rect.y)
5373 {
5374 p = (const char*)ImMemchr(s: (void*)p, c: '\n', n: text_selected_end - p);
5375 p = p ? p + 1 : text_selected_end;
5376 }
5377 else
5378 {
5379 ImVec2 rect_size = InputTextCalcTextSize(ctx: &g, text_begin: p, text_end: text_selected_end, remaining: &p, NULL, stop_on_new_line: true);
5380 if (rect_size.x <= 0.0f) rect_size.x = IM_TRUNC(g.FontBaked->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines
5381 ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_size.x, bg_offy_dn));
5382 rect.ClipWith(r: clip_rect);
5383 if (rect.Overlaps(r: clip_rect))
5384 draw_window->DrawList->AddRectFilled(p_min: rect.Min, p_max: rect.Max, col: bg_color);
5385 rect_pos.x = draw_pos.x - draw_scroll.x;
5386 }
5387 rect_pos.y += g.FontSize;
5388 }
5389 }
5390
5391 // We test for 'buf_display_max_length' as a way to avoid some pathological cases (e.g. single-line 1 MB string) which would make ImDrawList crash.
5392 // FIXME-OPT: Multiline could submit a smaller amount of contents to AddText() since we already iterated through it.
5393 if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
5394 {
5395 ImU32 col = GetColorU32(idx: is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
5396 draw_window->DrawList->AddText(font: g.Font, font_size: g.FontSize, pos: draw_pos - draw_scroll, col, text_begin: buf_display, text_end: buf_display_end, wrap_width: 0.0f, cpu_fine_clip_rect: is_multiline ? NULL : &clip_rect);
5397 }
5398
5399 // Draw blinking cursor
5400 if (render_cursor)
5401 {
5402 state->CursorAnim += io.DeltaTime;
5403 bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f;
5404 ImVec2 cursor_screen_pos = ImTrunc(v: draw_pos + cursor_offset - draw_scroll);
5405 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);
5406 if (cursor_is_visible && cursor_screen_rect.Overlaps(r: clip_rect))
5407 draw_window->DrawList->AddLine(p1: cursor_screen_rect.Min, p2: cursor_screen_rect.GetBL(), col: GetColorU32(idx: ImGuiCol_InputTextCursor), thickness: 1.0f); // FIXME-DPI: Cursor thickness (#7031)
5408
5409 // 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.)
5410 // This is required for some backends (SDL3) to start emitting character/text inputs.
5411 // As per #6341, make sure we don't set that on the deactivating frame.
5412 if (!is_readonly && g.ActiveId == id)
5413 {
5414 ImGuiPlatformImeData* ime_data = &g.PlatformImeData; // (this is a public struct, passed to io.Platform_SetImeDataFn() handler)
5415 ime_data->WantVisible = true;
5416 ime_data->WantTextInput = true;
5417 ime_data->InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize);
5418 ime_data->InputLineHeight = g.FontSize;
5419 ime_data->ViewportId = window->Viewport->ID;
5420 }
5421 }
5422 }
5423 else
5424 {
5425 // Render text only (no selection, no cursor)
5426 if (is_multiline)
5427 text_size = ImVec2(inner_size.x, InputTextCalcTextLenAndLineCount(text_begin: buf_display, out_text_end: &buf_display_end) * g.FontSize); // We don't need width
5428 else if (!is_displaying_hint && g.ActiveId == id)
5429 buf_display_end = buf_display + state->TextLen;
5430 else if (!is_displaying_hint)
5431 buf_display_end = buf_display + ImStrlen(s: buf_display);
5432
5433 if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
5434 {
5435 // Find render position for right alignment
5436 if (flags & ImGuiInputTextFlags_ElideLeft)
5437 draw_pos.x = ImMin(lhs: draw_pos.x, rhs: frame_bb.Max.x - CalcTextSize(text: buf_display, NULL).x - style.FramePadding.x);
5438
5439 const ImVec2 draw_scroll = /*state ? ImVec2(state->Scroll.x, 0.0f) :*/ ImVec2(0.0f, 0.0f); // Preserve scroll when inactive?
5440 ImU32 col = GetColorU32(idx: is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
5441 draw_window->DrawList->AddText(font: g.Font, font_size: g.FontSize, pos: draw_pos - draw_scroll, col, text_begin: buf_display, text_end: buf_display_end, wrap_width: 0.0f, cpu_fine_clip_rect: is_multiline ? NULL : &clip_rect);
5442 }
5443 }
5444
5445 if (is_password && !is_displaying_hint)
5446 PopPasswordFont();
5447
5448 if (is_multiline)
5449 {
5450 // For focus requests to work on our multiline we need to ensure our child ItemAdd() call specifies the ImGuiItemFlags_Inputable (see #4761, #7870)...
5451 Dummy(size: ImVec2(text_size.x, text_size.y + style.FramePadding.y));
5452 g.NextItemData.ItemFlags |= (ImGuiItemFlags)ImGuiItemFlags_Inputable | ImGuiItemFlags_NoTabStop;
5453 EndChild();
5454 item_data_backup.StatusFlags |= (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredWindow);
5455
5456 // ...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...
5457 // FIXME: This quite messy/tricky, should attempt to get rid of the child window.
5458 EndGroup();
5459 if (g.LastItemData.ID == 0 || g.LastItemData.ID != GetWindowScrollbarID(window: draw_window, axis: ImGuiAxis_Y))
5460 {
5461 g.LastItemData.ID = id;
5462 g.LastItemData.ItemFlags = item_data_backup.ItemFlags;
5463 g.LastItemData.StatusFlags = item_data_backup.StatusFlags;
5464 }
5465 }
5466 if (state)
5467 state->TextSrc = NULL;
5468
5469 // Log as text
5470 if (g.LogEnabled && (!is_password || is_displaying_hint))
5471 {
5472 LogSetNextTextDecoration(prefix: "{", suffix: "}");
5473 LogRenderedText(ref_pos: &draw_pos, text: buf_display, text_end: buf_display_end);
5474 }
5475
5476 if (label_size.x > 0)
5477 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), text: label);
5478
5479 if (value_changed)
5480 MarkItemEdited(id);
5481
5482 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
5483 if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0)
5484 return validated;
5485 else
5486 return value_changed;
5487}
5488
5489void ImGui::DebugNodeInputTextState(ImGuiInputTextState* state)
5490{
5491#ifndef IMGUI_DISABLE_DEBUG_TOOLS
5492 ImGuiContext& g = *GImGui;
5493 ImStb::STB_TexteditState* stb_state = state->Stb;
5494 ImStb::StbUndoState* undo_state = &stb_state->undostate;
5495 Text(fmt: "ID: 0x%08X, ActiveID: 0x%08X", state->ID, g.ActiveId);
5496 DebugLocateItemOnHover(target_id: state->ID);
5497 Text(fmt: "CurLenA: %d, Cursor: %d, Selection: %d..%d", state->TextLen, stb_state->cursor, stb_state->select_start, stb_state->select_end);
5498 Text(fmt: "BufCapacity: %d", state->BufCapacity);
5499 Text(fmt: "(Internal Buffer: TextA Size: %d, Capacity: %d)", state->TextA.Size, state->TextA.Capacity);
5500 Text(fmt: "has_preferred_x: %d (%.2f)", stb_state->has_preferred_x, stb_state->preferred_x);
5501 Text(fmt: "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);
5502 if (BeginChild(str_id: "undopoints", size: ImVec2(0.0f, GetTextLineHeight() * 10), child_flags: ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY)) // Visualize undo state
5503 {
5504 PushStyleVar(idx: ImGuiStyleVar_ItemSpacing, val: ImVec2(0, 0));
5505 for (int n = 0; n < IMSTB_TEXTEDIT_UNDOSTATECOUNT; n++)
5506 {
5507 ImStb::StbUndoRecord* undo_rec = &undo_state->undo_rec[n];
5508 const char undo_rec_type = (n < undo_state->undo_point) ? 'u' : (n >= undo_state->redo_point) ? 'r' : ' ';
5509 if (undo_rec_type == ' ')
5510 BeginDisabled();
5511 const int buf_preview_len = (undo_rec_type != ' ' && undo_rec->char_storage != -1) ? undo_rec->insert_length : 0;
5512 const char* buf_preview_str = undo_state->undo_char + undo_rec->char_storage;
5513 Text(fmt: "%c [%02d] where %03d, insert %03d, delete %03d, char_storage %03d \"%.*s\"",
5514 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);
5515 if (undo_rec_type == ' ')
5516 EndDisabled();
5517 }
5518 PopStyleVar();
5519 }
5520 EndChild();
5521#else
5522 IM_UNUSED(state);
5523#endif
5524}
5525
5526//-------------------------------------------------------------------------
5527// [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc.
5528//-------------------------------------------------------------------------
5529// - ColorEdit3()
5530// - ColorEdit4()
5531// - ColorPicker3()
5532// - RenderColorRectWithAlphaCheckerboard() [Internal]
5533// - ColorPicker4()
5534// - ColorButton()
5535// - SetColorEditOptions()
5536// - ColorTooltip() [Internal]
5537// - ColorEditOptionsPopup() [Internal]
5538// - ColorPickerOptionsPopup() [Internal]
5539//-------------------------------------------------------------------------
5540
5541bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags)
5542{
5543 return ColorEdit4(label, col, flags: flags | ImGuiColorEditFlags_NoAlpha);
5544}
5545
5546static void ColorEditRestoreH(const float* col, float* H)
5547{
5548 ImGuiContext& g = *GImGui;
5549 IM_ASSERT(g.ColorEditCurrentID != 0);
5550 if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(in: ImVec4(col[0], col[1], col[2], 0)))
5551 return;
5552 *H = g.ColorEditSavedHue;
5553}
5554
5555// ColorEdit supports RGB and HSV inputs. In case of RGB input resulting color may have undefined hue and/or saturation.
5556// Since widget displays both RGB and HSV values we must preserve hue and saturation to prevent these values resetting.
5557static void ColorEditRestoreHS(const float* col, float* H, float* S, float* V)
5558{
5559 ImGuiContext& g = *GImGui;
5560 IM_ASSERT(g.ColorEditCurrentID != 0);
5561 if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(in: ImVec4(col[0], col[1], col[2], 0)))
5562 return;
5563
5564 // When S == 0, H is undefined.
5565 // When H == 1 it wraps around to 0.
5566 if (*S == 0.0f || (*H == 0.0f && g.ColorEditSavedHue == 1))
5567 *H = g.ColorEditSavedHue;
5568
5569 // When V == 0, S is undefined.
5570 if (*V == 0.0f)
5571 *S = g.ColorEditSavedSat;
5572}
5573
5574// Edit colors components (each component in 0.0f..1.0f range).
5575// See enum ImGuiColorEditFlags_ for available options. e.g. Only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
5576// 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.
5577bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags)
5578{
5579 ImGuiWindow* window = GetCurrentWindow();
5580 if (window->SkipItems)
5581 return false;
5582
5583 ImGuiContext& g = *GImGui;
5584 const ImGuiStyle& style = g.Style;
5585 const float square_sz = GetFrameHeight();
5586 const char* label_display_end = FindRenderedTextEnd(text: label);
5587 float w_full = CalcItemWidth();
5588 g.NextItemData.ClearFlags();
5589
5590 BeginGroup();
5591 PushID(str_id: label);
5592 const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0);
5593 if (set_current_color_edit_id)
5594 g.ColorEditCurrentID = window->IDStack.back();
5595
5596 // If we're not showing any slider there's no point in doing any HSV conversions
5597 const ImGuiColorEditFlags flags_untouched = flags;
5598 if (flags & ImGuiColorEditFlags_NoInputs)
5599 flags = (flags & (~ImGuiColorEditFlags_DisplayMask_)) | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_NoOptions;
5600
5601 // Context menu: display and modify options (before defaults are applied)
5602 if (!(flags & ImGuiColorEditFlags_NoOptions))
5603 ColorEditOptionsPopup(col, flags);
5604
5605 // Read stored options
5606 if (!(flags & ImGuiColorEditFlags_DisplayMask_))
5607 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DisplayMask_);
5608 if (!(flags & ImGuiColorEditFlags_DataTypeMask_))
5609 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DataTypeMask_);
5610 if (!(flags & ImGuiColorEditFlags_PickerMask_))
5611 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_);
5612 if (!(flags & ImGuiColorEditFlags_InputMask_))
5613 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_InputMask_);
5614 flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_));
5615 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check that only 1 is selected
5616 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
5617
5618 const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0;
5619 const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0;
5620 const int components = alpha ? 4 : 3;
5621 const float w_button = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x);
5622 const float w_inputs = ImMax(lhs: w_full - w_button, rhs: 1.0f);
5623 w_full = w_inputs + w_button;
5624
5625 // Convert to the formats we need
5626 float f[4] = { col[0], col[1], col[2], alpha ? col[3] : 1.0f };
5627 if ((flags & ImGuiColorEditFlags_InputHSV) && (flags & ImGuiColorEditFlags_DisplayRGB))
5628 ColorConvertHSVtoRGB(h: f[0], s: f[1], v: f[2], out_r&: f[0], out_g&: f[1], out_b&: f[2]);
5629 else if ((flags & ImGuiColorEditFlags_InputRGB) && (flags & ImGuiColorEditFlags_DisplayHSV))
5630 {
5631 // Hue is lost when converting from grayscale rgb (saturation=0). Restore it.
5632 ColorConvertRGBtoHSV(r: f[0], g: f[1], b: f[2], out_h&: f[0], out_s&: f[1], out_v&: f[2]);
5633 ColorEditRestoreHS(col, H: &f[0], S: &f[1], V: &f[2]);
5634 }
5635 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]) };
5636
5637 bool value_changed = false;
5638 bool value_changed_as_float = false;
5639
5640 const ImVec2 pos = window->DC.CursorPos;
5641 const float inputs_offset_x = (style.ColorButtonPosition == ImGuiDir_Left) ? w_button : 0.0f;
5642 window->DC.CursorPos.x = pos.x + inputs_offset_x;
5643
5644 if ((flags & (ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5645 {
5646 // RGB/HSV 0..255 Sliders
5647 const float w_items = w_inputs - style.ItemInnerSpacing.x * (components - 1);
5648
5649 const bool hide_prefix = (IM_TRUNC(w_items / components) <= CalcTextSize(text: (flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x);
5650 static const char* ids[4] = { "##X", "##Y", "##Z", "##W" };
5651 static const char* fmt_table_int[3][4] =
5652 {
5653 { "%3d", "%3d", "%3d", "%3d" }, // Short display
5654 { "R:%3d", "G:%3d", "B:%3d", "A:%3d" }, // Long display for RGBA
5655 { "H:%3d", "S:%3d", "V:%3d", "A:%3d" } // Long display for HSVA
5656 };
5657 static const char* fmt_table_float[3][4] =
5658 {
5659 { "%0.3f", "%0.3f", "%0.3f", "%0.3f" }, // Short display
5660 { "R:%0.3f", "G:%0.3f", "B:%0.3f", "A:%0.3f" }, // Long display for RGBA
5661 { "H:%0.3f", "S:%0.3f", "V:%0.3f", "A:%0.3f" } // Long display for HSVA
5662 };
5663 const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_DisplayHSV) ? 2 : 1;
5664
5665 float prev_split = 0.0f;
5666 for (int n = 0; n < components; n++)
5667 {
5668 if (n > 0)
5669 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
5670 float next_split = IM_TRUNC(w_items * (n + 1) / components);
5671 SetNextItemWidth(ImMax(lhs: next_split - prev_split, rhs: 1.0f));
5672 prev_split = next_split;
5673
5674 // FIXME: When ImGuiColorEditFlags_HDR flag is passed HS values snap in weird ways when SV values go below 0.
5675 if (flags & ImGuiColorEditFlags_Float)
5676 {
5677 value_changed |= DragFloat(label: ids[n], v: &f[n], v_speed: 1.0f / 255.0f, v_min: 0.0f, v_max: hdr ? 0.0f : 1.0f, format: fmt_table_float[fmt_idx][n]);
5678 value_changed_as_float |= value_changed;
5679 }
5680 else
5681 {
5682 value_changed |= DragInt(label: ids[n], v: &i[n], v_speed: 1.0f, v_min: 0, v_max: hdr ? 0 : 255, format: fmt_table_int[fmt_idx][n]);
5683 }
5684 if (!(flags & ImGuiColorEditFlags_NoOptions))
5685 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5686 }
5687 }
5688 else if ((flags & ImGuiColorEditFlags_DisplayHex) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5689 {
5690 // RGB Hexadecimal Input
5691 char buf[64];
5692 if (alpha)
5693 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "#%02X%02X%02X%02X", ImClamp(v: i[0], mn: 0, mx: 255), ImClamp(v: i[1], mn: 0, mx: 255), ImClamp(v: i[2], mn: 0, mx: 255), ImClamp(v: i[3], mn: 0, mx: 255));
5694 else
5695 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "#%02X%02X%02X", ImClamp(v: i[0], mn: 0, mx: 255), ImClamp(v: i[1], mn: 0, mx: 255), ImClamp(v: i[2], mn: 0, mx: 255));
5696 SetNextItemWidth(w_inputs);
5697 if (InputText(label: "##Text", buf, IM_ARRAYSIZE(buf), flags: ImGuiInputTextFlags_CharsUppercase))
5698 {
5699 value_changed = true;
5700 char* p = buf;
5701 while (*p == '#' || ImCharIsBlankA(c: *p))
5702 p++;
5703 i[0] = i[1] = i[2] = 0;
5704 i[3] = 0xFF; // alpha default to 255 is not parsed by scanf (e.g. inputting #FFFFFF omitting alpha)
5705 int r;
5706 if (alpha)
5707 r = sscanf(s: p, format: "%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)
5708 else
5709 r = sscanf(s: p, format: "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]);
5710 IM_UNUSED(r); // Fixes C6031: Return value ignored: 'sscanf'.
5711 }
5712 if (!(flags & ImGuiColorEditFlags_NoOptions))
5713 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5714 }
5715
5716 ImGuiWindow* picker_active_window = NULL;
5717 if (!(flags & ImGuiColorEditFlags_NoSmallPreview))
5718 {
5719 const float button_offset_x = ((flags & ImGuiColorEditFlags_NoInputs) || (style.ColorButtonPosition == ImGuiDir_Left)) ? 0.0f : w_inputs + style.ItemInnerSpacing.x;
5720 window->DC.CursorPos = ImVec2(pos.x + button_offset_x, pos.y);
5721
5722 const ImVec4 col_v4(col[0], col[1], col[2], alpha ? col[3] : 1.0f);
5723 if (ColorButton(desc_id: "##ColorButton", col: col_v4, flags))
5724 {
5725 if (!(flags & ImGuiColorEditFlags_NoPicker))
5726 {
5727 // Store current color and open a picker
5728 g.ColorPickerRef = col_v4;
5729 OpenPopup(str_id: "picker");
5730 SetNextWindowPos(pos: g.LastItemData.Rect.GetBL() + ImVec2(0.0f, style.ItemSpacing.y));
5731 }
5732 }
5733 if (!(flags & ImGuiColorEditFlags_NoOptions))
5734 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5735
5736 if (BeginPopup(str_id: "picker"))
5737 {
5738 if (g.CurrentWindow->BeginCount == 1)
5739 {
5740 picker_active_window = g.CurrentWindow;
5741 if (label != label_display_end)
5742 {
5743 TextEx(text: label, text_end: label_display_end);
5744 Spacing();
5745 }
5746 ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar;
5747 ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf;
5748 SetNextItemWidth(square_sz * 12.0f); // Use 256 + bar sizes?
5749 value_changed |= ColorPicker4(label: "##picker", col, flags: picker_flags, ref_col: &g.ColorPickerRef.x);
5750 }
5751 EndPopup();
5752 }
5753 }
5754
5755 if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel))
5756 {
5757 // Position not necessarily next to last submitted button (e.g. if style.ColorButtonPosition == ImGuiDir_Left),
5758 // but we need to use SameLine() to setup baseline correctly. Might want to refactor SameLine() to simplify this.
5759 SameLine(offset_from_start_x: 0.0f, spacing: style.ItemInnerSpacing.x);
5760 window->DC.CursorPos.x = pos.x + ((flags & ImGuiColorEditFlags_NoInputs) ? w_button : w_full + style.ItemInnerSpacing.x);
5761 TextEx(text: label, text_end: label_display_end);
5762 }
5763
5764 // Convert back
5765 if (value_changed && picker_active_window == NULL)
5766 {
5767 if (!value_changed_as_float)
5768 for (int n = 0; n < 4; n++)
5769 f[n] = i[n] / 255.0f;
5770 if ((flags & ImGuiColorEditFlags_DisplayHSV) && (flags & ImGuiColorEditFlags_InputRGB))
5771 {
5772 g.ColorEditSavedHue = f[0];
5773 g.ColorEditSavedSat = f[1];
5774 ColorConvertHSVtoRGB(h: f[0], s: f[1], v: f[2], out_r&: f[0], out_g&: f[1], out_b&: f[2]);
5775 g.ColorEditSavedID = g.ColorEditCurrentID;
5776 g.ColorEditSavedColor = ColorConvertFloat4ToU32(in: ImVec4(f[0], f[1], f[2], 0));
5777 }
5778 if ((flags & ImGuiColorEditFlags_DisplayRGB) && (flags & ImGuiColorEditFlags_InputHSV))
5779 ColorConvertRGBtoHSV(r: f[0], g: f[1], b: f[2], out_h&: f[0], out_s&: f[1], out_v&: f[2]);
5780
5781 col[0] = f[0];
5782 col[1] = f[1];
5783 col[2] = f[2];
5784 if (alpha)
5785 col[3] = f[3];
5786 }
5787
5788 if (set_current_color_edit_id)
5789 g.ColorEditCurrentID = 0;
5790 PopID();
5791 EndGroup();
5792
5793 // Drag and Drop Target
5794 // NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test.
5795 if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget())
5796 {
5797 bool accepted_drag_drop = false;
5798 if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F))
5799 {
5800 memcpy(dest: (float*)col, src: payload->Data, n: sizeof(float) * 3); // Preserve alpha if any //-V512 //-V1086
5801 value_changed = accepted_drag_drop = true;
5802 }
5803 if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F))
5804 {
5805 memcpy(dest: (float*)col, src: payload->Data, n: sizeof(float) * components);
5806 value_changed = accepted_drag_drop = true;
5807 }
5808
5809 // Drag-drop payloads are always RGB
5810 if (accepted_drag_drop && (flags & ImGuiColorEditFlags_InputHSV))
5811 ColorConvertRGBtoHSV(r: col[0], g: col[1], b: col[2], out_h&: col[0], out_s&: col[1], out_v&: col[2]);
5812 EndDragDropTarget();
5813 }
5814
5815 // When picker is being actively used, use its active id so IsItemActive() will function on ColorEdit4().
5816 if (picker_active_window && g.ActiveId != 0 && g.ActiveIdWindow == picker_active_window)
5817 g.LastItemData.ID = g.ActiveId;
5818
5819 if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
5820 MarkItemEdited(id: g.LastItemData.ID);
5821
5822 return value_changed;
5823}
5824
5825bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags)
5826{
5827 float col4[4] = { col[0], col[1], col[2], 1.0f };
5828 if (!ColorPicker4(label, col: col4, flags: flags | ImGuiColorEditFlags_NoAlpha))
5829 return false;
5830 col[0] = col4[0]; col[1] = col4[1]; col[2] = col4[2];
5831 return true;
5832}
5833
5834// Helper for ColorPicker4()
5835static void RenderArrowsForVerticalBar(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w, float alpha)
5836{
5837 ImU32 alpha8 = IM_F32_TO_INT8_SAT(alpha);
5838 ImGui::RenderArrowPointingAt(draw_list, pos: ImVec2(pos.x + half_sz.x + 1, pos.y), half_sz: ImVec2(half_sz.x + 2, half_sz.y + 1), direction: ImGuiDir_Right, IM_COL32(0,0,0,alpha8));
5839 ImGui::RenderArrowPointingAt(draw_list, pos: ImVec2(pos.x + half_sz.x, pos.y), half_sz, direction: ImGuiDir_Right, IM_COL32(255,255,255,alpha8));
5840 ImGui::RenderArrowPointingAt(draw_list, pos: ImVec2(pos.x + bar_w - half_sz.x - 1, pos.y), half_sz: ImVec2(half_sz.x + 2, half_sz.y + 1), direction: ImGuiDir_Left, IM_COL32(0,0,0,alpha8));
5841 ImGui::RenderArrowPointingAt(draw_list, pos: ImVec2(pos.x + bar_w - half_sz.x, pos.y), half_sz, direction: ImGuiDir_Left, IM_COL32(255,255,255,alpha8));
5842}
5843
5844// Note: ColorPicker4() only accesses 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
5845// (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.)
5846// 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..)
5847// 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)
5848bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags, const float* ref_col)
5849{
5850 ImGuiContext& g = *GImGui;
5851 ImGuiWindow* window = GetCurrentWindow();
5852 if (window->SkipItems)
5853 return false;
5854
5855 ImDrawList* draw_list = window->DrawList;
5856 ImGuiStyle& style = g.Style;
5857 ImGuiIO& io = g.IO;
5858
5859 const float width = CalcItemWidth();
5860 const bool is_readonly = ((g.NextItemData.ItemFlags | g.CurrentItemFlags) & ImGuiItemFlags_ReadOnly) != 0;
5861 g.NextItemData.ClearFlags();
5862
5863 PushID(str_id: label);
5864 const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0);
5865 if (set_current_color_edit_id)
5866 g.ColorEditCurrentID = window->IDStack.back();
5867 BeginGroup();
5868
5869 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5870 flags |= ImGuiColorEditFlags_NoSmallPreview;
5871
5872 // Context menu: display and store options.
5873 if (!(flags & ImGuiColorEditFlags_NoOptions))
5874 ColorPickerOptionsPopup(ref_col: col, flags);
5875
5876 // Read stored options
5877 if (!(flags & ImGuiColorEditFlags_PickerMask_))
5878 flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_PickerMask_;
5879 if (!(flags & ImGuiColorEditFlags_InputMask_))
5880 flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_InputMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_InputMask_;
5881 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check that only 1 is selected
5882 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
5883 if (!(flags & ImGuiColorEditFlags_NoOptions))
5884 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_AlphaBar);
5885
5886 // Setup
5887 int components = (flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4;
5888 bool alpha_bar = (flags & ImGuiColorEditFlags_AlphaBar) && !(flags & ImGuiColorEditFlags_NoAlpha);
5889 ImVec2 picker_pos = window->DC.CursorPos;
5890 float square_sz = GetFrameHeight();
5891 float bars_width = square_sz; // Arbitrary smallish width of Hue/Alpha picking bars
5892 float sv_picker_size = ImMax(lhs: bars_width * 1, rhs: width - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box
5893 float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x;
5894 float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x;
5895 float bars_triangles_half_sz = IM_TRUNC(bars_width * 0.20f);
5896
5897 float backup_initial_col[4];
5898 memcpy(dest: backup_initial_col, src: col, n: components * sizeof(float));
5899
5900 float wheel_thickness = sv_picker_size * 0.08f;
5901 float wheel_r_outer = sv_picker_size * 0.50f;
5902 float wheel_r_inner = wheel_r_outer - wheel_thickness;
5903 ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width)*0.5f, picker_pos.y + sv_picker_size * 0.5f);
5904
5905 // Note: the triangle is displayed rotated with triangle_pa pointing to Hue, but most coordinates stays unrotated for logic.
5906 float triangle_r = wheel_r_inner - (int)(sv_picker_size * 0.027f);
5907 ImVec2 triangle_pa = ImVec2(triangle_r, 0.0f); // Hue point.
5908 ImVec2 triangle_pb = ImVec2(triangle_r * -0.5f, triangle_r * -0.866025f); // Black point.
5909 ImVec2 triangle_pc = ImVec2(triangle_r * -0.5f, triangle_r * +0.866025f); // White point.
5910
5911 float H = col[0], S = col[1], V = col[2];
5912 float R = col[0], G = col[1], B = col[2];
5913 if (flags & ImGuiColorEditFlags_InputRGB)
5914 {
5915 // Hue is lost when converting from grayscale rgb (saturation=0). Restore it.
5916 ColorConvertRGBtoHSV(r: R, g: G, b: B, out_h&: H, out_s&: S, out_v&: V);
5917 ColorEditRestoreHS(col, H: &H, S: &S, V: &V);
5918 }
5919 else if (flags & ImGuiColorEditFlags_InputHSV)
5920 {
5921 ColorConvertHSVtoRGB(h: H, s: S, v: V, out_r&: R, out_g&: G, out_b&: B);
5922 }
5923
5924 bool value_changed = false, value_changed_h = false, value_changed_sv = false;
5925
5926 PushItemFlag(option: ImGuiItemFlags_NoNav, enabled: true);
5927 if (flags & ImGuiColorEditFlags_PickerHueWheel)
5928 {
5929 // Hue wheel + SV triangle logic
5930 InvisibleButton(str_id: "hsv", size_arg: ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size));
5931 if (IsItemActive() && !is_readonly)
5932 {
5933 ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center;
5934 ImVec2 current_off = g.IO.MousePos - wheel_center;
5935 float initial_dist2 = ImLengthSqr(lhs: initial_off);
5936 if (initial_dist2 >= (wheel_r_inner - 1) * (wheel_r_inner - 1) && initial_dist2 <= (wheel_r_outer + 1) * (wheel_r_outer + 1))
5937 {
5938 // Interactive with Hue wheel
5939 H = ImAtan2(current_off.y, current_off.x) / IM_PI * 0.5f;
5940 if (H < 0.0f)
5941 H += 1.0f;
5942 value_changed = value_changed_h = true;
5943 }
5944 float cos_hue_angle = ImCos(-H * 2.0f * IM_PI);
5945 float sin_hue_angle = ImSin(-H * 2.0f * IM_PI);
5946 if (ImTriangleContainsPoint(a: triangle_pa, b: triangle_pb, c: triangle_pc, p: ImRotate(v: initial_off, cos_a: cos_hue_angle, sin_a: sin_hue_angle)))
5947 {
5948 // Interacting with SV triangle
5949 ImVec2 current_off_unrotated = ImRotate(v: current_off, cos_a: cos_hue_angle, sin_a: sin_hue_angle);
5950 if (!ImTriangleContainsPoint(a: triangle_pa, b: triangle_pb, c: triangle_pc, p: current_off_unrotated))
5951 current_off_unrotated = ImTriangleClosestPoint(a: triangle_pa, b: triangle_pb, c: triangle_pc, p: current_off_unrotated);
5952 float uu, vv, ww;
5953 ImTriangleBarycentricCoords(a: triangle_pa, b: triangle_pb, c: triangle_pc, p: current_off_unrotated, out_u&: uu, out_v&: vv, out_w&: ww);
5954 V = ImClamp(v: 1.0f - vv, mn: 0.0001f, mx: 1.0f);
5955 S = ImClamp(v: uu / V, mn: 0.0001f, mx: 1.0f);
5956 value_changed = value_changed_sv = true;
5957 }
5958 }
5959 if (!(flags & ImGuiColorEditFlags_NoOptions))
5960 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5961 }
5962 else if (flags & ImGuiColorEditFlags_PickerHueBar)
5963 {
5964 // SV rectangle logic
5965 InvisibleButton(str_id: "sv", size_arg: ImVec2(sv_picker_size, sv_picker_size));
5966 if (IsItemActive() && !is_readonly)
5967 {
5968 S = ImSaturate(f: (io.MousePos.x - picker_pos.x) / (sv_picker_size - 1));
5969 V = 1.0f - ImSaturate(f: (io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5970 ColorEditRestoreH(col, H: &H); // Greatly reduces hue jitter and reset to 0 when hue == 255 and color is rapidly modified using SV square.
5971 value_changed = value_changed_sv = true;
5972 }
5973 if (!(flags & ImGuiColorEditFlags_NoOptions))
5974 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5975
5976 // Hue bar logic
5977 SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y));
5978 InvisibleButton(str_id: "hue", size_arg: ImVec2(bars_width, sv_picker_size));
5979 if (IsItemActive() && !is_readonly)
5980 {
5981 H = ImSaturate(f: (io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5982 value_changed = value_changed_h = true;
5983 }
5984 }
5985
5986 // Alpha bar logic
5987 if (alpha_bar)
5988 {
5989 SetCursorScreenPos(ImVec2(bar1_pos_x, picker_pos.y));
5990 InvisibleButton(str_id: "alpha", size_arg: ImVec2(bars_width, sv_picker_size));
5991 if (IsItemActive())
5992 {
5993 col[3] = 1.0f - ImSaturate(f: (io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5994 value_changed = true;
5995 }
5996 }
5997 PopItemFlag(); // ImGuiItemFlags_NoNav
5998
5999 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
6000 {
6001 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
6002 BeginGroup();
6003 }
6004
6005 if (!(flags & ImGuiColorEditFlags_NoLabel))
6006 {
6007 const char* label_display_end = FindRenderedTextEnd(text: label);
6008 if (label != label_display_end)
6009 {
6010 if ((flags & ImGuiColorEditFlags_NoSidePreview))
6011 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
6012 TextEx(text: label, text_end: label_display_end);
6013 }
6014 }
6015
6016 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
6017 {
6018 PushItemFlag(option: ImGuiItemFlags_NoNavDefaultFocus, enabled: true);
6019 ImVec4 col_v4(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6020 if ((flags & ImGuiColorEditFlags_NoLabel))
6021 Text(fmt: "Current");
6022
6023 ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaMask_ | ImGuiColorEditFlags_NoTooltip;
6024 ColorButton(desc_id: "##current", col: col_v4, flags: (flags & sub_flags_to_forward), size: ImVec2(square_sz * 3, square_sz * 2));
6025 if (ref_col != NULL)
6026 {
6027 Text(fmt: "Original");
6028 ImVec4 ref_col_v4(ref_col[0], ref_col[1], ref_col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : ref_col[3]);
6029 if (ColorButton(desc_id: "##original", col: ref_col_v4, flags: (flags & sub_flags_to_forward), size: ImVec2(square_sz * 3, square_sz * 2)))
6030 {
6031 memcpy(dest: col, src: ref_col, n: components * sizeof(float));
6032 value_changed = true;
6033 }
6034 }
6035 PopItemFlag();
6036 EndGroup();
6037 }
6038
6039 // Convert back color to RGB
6040 if (value_changed_h || value_changed_sv)
6041 {
6042 if (flags & ImGuiColorEditFlags_InputRGB)
6043 {
6044 ColorConvertHSVtoRGB(h: H, s: S, v: V, out_r&: col[0], out_g&: col[1], out_b&: col[2]);
6045 g.ColorEditSavedHue = H;
6046 g.ColorEditSavedSat = S;
6047 g.ColorEditSavedID = g.ColorEditCurrentID;
6048 g.ColorEditSavedColor = ColorConvertFloat4ToU32(in: ImVec4(col[0], col[1], col[2], 0));
6049 }
6050 else if (flags & ImGuiColorEditFlags_InputHSV)
6051 {
6052 col[0] = H;
6053 col[1] = S;
6054 col[2] = V;
6055 }
6056 }
6057
6058 // R,G,B and H,S,V slider color editor
6059 bool value_changed_fix_hue_wrap = false;
6060 if ((flags & ImGuiColorEditFlags_NoInputs) == 0)
6061 {
6062 PushItemWidth(item_width: (alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x);
6063 ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaMask_ | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoSmallPreview;
6064 ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker;
6065 if (flags & ImGuiColorEditFlags_DisplayRGB || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6066 if (ColorEdit4(label: "##rgb", col, flags: sub_flags | ImGuiColorEditFlags_DisplayRGB))
6067 {
6068 // FIXME: Hackily differentiating using the DragInt (ActiveId != 0 && !ActiveIdAllowOverlap) vs. using the InputText or DropTarget.
6069 // 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)
6070 value_changed_fix_hue_wrap = (g.ActiveId != 0 && !g.ActiveIdAllowOverlap);
6071 value_changed = true;
6072 }
6073 if (flags & ImGuiColorEditFlags_DisplayHSV || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6074 value_changed |= ColorEdit4(label: "##hsv", col, flags: sub_flags | ImGuiColorEditFlags_DisplayHSV);
6075 if (flags & ImGuiColorEditFlags_DisplayHex || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6076 value_changed |= ColorEdit4(label: "##hex", col, flags: sub_flags | ImGuiColorEditFlags_DisplayHex);
6077 PopItemWidth();
6078 }
6079
6080 // Try to cancel hue wrap (after ColorEdit4 call), if any
6081 if (value_changed_fix_hue_wrap && (flags & ImGuiColorEditFlags_InputRGB))
6082 {
6083 float new_H, new_S, new_V;
6084 ColorConvertRGBtoHSV(r: col[0], g: col[1], b: col[2], out_h&: new_H, out_s&: new_S, out_v&: new_V);
6085 if (new_H <= 0 && H > 0)
6086 {
6087 if (new_V <= 0 && V != new_V)
6088 ColorConvertHSVtoRGB(h: H, s: S, v: new_V <= 0 ? V * 0.5f : new_V, out_r&: col[0], out_g&: col[1], out_b&: col[2]);
6089 else if (new_S <= 0)
6090 ColorConvertHSVtoRGB(h: H, s: new_S <= 0 ? S * 0.5f : new_S, v: new_V, out_r&: col[0], out_g&: col[1], out_b&: col[2]);
6091 }
6092 }
6093
6094 if (value_changed)
6095 {
6096 if (flags & ImGuiColorEditFlags_InputRGB)
6097 {
6098 R = col[0];
6099 G = col[1];
6100 B = col[2];
6101 ColorConvertRGBtoHSV(r: R, g: G, b: B, out_h&: H, out_s&: S, out_v&: V);
6102 ColorEditRestoreHS(col, H: &H, S: &S, V: &V); // Fix local Hue as display below will use it immediately.
6103 }
6104 else if (flags & ImGuiColorEditFlags_InputHSV)
6105 {
6106 H = col[0];
6107 S = col[1];
6108 V = col[2];
6109 ColorConvertHSVtoRGB(h: H, s: S, v: V, out_r&: R, out_g&: G, out_b&: B);
6110 }
6111 }
6112
6113 const int style_alpha8 = IM_F32_TO_INT8_SAT(style.Alpha);
6114 const ImU32 col_black = IM_COL32(0,0,0,style_alpha8);
6115 const ImU32 col_white = IM_COL32(255,255,255,style_alpha8);
6116 const ImU32 col_midgrey = IM_COL32(128,128,128,style_alpha8);
6117 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) };
6118
6119 ImVec4 hue_color_f(1, 1, 1, style.Alpha); ColorConvertHSVtoRGB(h: H, s: 1, v: 1, out_r&: hue_color_f.x, out_g&: hue_color_f.y, out_b&: hue_color_f.z);
6120 ImU32 hue_color32 = ColorConvertFloat4ToU32(in: hue_color_f);
6121 ImU32 user_col32_striped_of_alpha = ColorConvertFloat4ToU32(in: ImVec4(R, G, B, style.Alpha)); // Important: this is still including the main rendering/style alpha!!
6122
6123 ImVec2 sv_cursor_pos;
6124
6125 if (flags & ImGuiColorEditFlags_PickerHueWheel)
6126 {
6127 // Render Hue Wheel
6128 const float aeps = 0.5f / wheel_r_outer; // Half a pixel arc length in radians (2pi cancels out).
6129 const int segment_per_arc = ImMax(lhs: 4, rhs: (int)wheel_r_outer / 12);
6130 for (int n = 0; n < 6; n++)
6131 {
6132 const float a0 = (n) /6.0f * 2.0f * IM_PI - aeps;
6133 const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps;
6134 const int vert_start_idx = draw_list->VtxBuffer.Size;
6135 draw_list->PathArcTo(center: wheel_center, radius: (wheel_r_inner + wheel_r_outer)*0.5f, a_min: a0, a_max: a1, num_segments: segment_per_arc);
6136 draw_list->PathStroke(col: col_white, flags: 0, thickness: wheel_thickness);
6137 const int vert_end_idx = draw_list->VtxBuffer.Size;
6138
6139 // Paint colors over existing vertices
6140 ImVec2 gradient_p0(wheel_center.x + ImCos(a0) * wheel_r_inner, wheel_center.y + ImSin(a0) * wheel_r_inner);
6141 ImVec2 gradient_p1(wheel_center.x + ImCos(a1) * wheel_r_inner, wheel_center.y + ImSin(a1) * wheel_r_inner);
6142 ShadeVertsLinearColorGradientKeepAlpha(draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, col0: col_hues[n], col1: col_hues[n + 1]);
6143 }
6144
6145 // Render Cursor + preview on Hue Wheel
6146 float cos_hue_angle = ImCos(H * 2.0f * IM_PI);
6147 float sin_hue_angle = ImSin(H * 2.0f * IM_PI);
6148 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);
6149 float hue_cursor_rad = value_changed_h ? wheel_thickness * 0.65f : wheel_thickness * 0.55f;
6150 int hue_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(radius: hue_cursor_rad); // Lock segment count so the +1 one matches others.
6151 draw_list->AddCircleFilled(center: hue_cursor_pos, radius: hue_cursor_rad, col: hue_color32, num_segments: hue_cursor_segments);
6152 draw_list->AddCircle(center: hue_cursor_pos, radius: hue_cursor_rad + 1, col: col_midgrey, num_segments: hue_cursor_segments);
6153 draw_list->AddCircle(center: hue_cursor_pos, radius: hue_cursor_rad, col: col_white, num_segments: hue_cursor_segments);
6154
6155 // Render SV triangle (rotated according to hue)
6156 ImVec2 tra = wheel_center + ImRotate(v: triangle_pa, cos_a: cos_hue_angle, sin_a: sin_hue_angle);
6157 ImVec2 trb = wheel_center + ImRotate(v: triangle_pb, cos_a: cos_hue_angle, sin_a: sin_hue_angle);
6158 ImVec2 trc = wheel_center + ImRotate(v: triangle_pc, cos_a: cos_hue_angle, sin_a: sin_hue_angle);
6159 ImVec2 uv_white = GetFontTexUvWhitePixel();
6160 draw_list->PrimReserve(idx_count: 3, vtx_count: 3);
6161 draw_list->PrimVtx(pos: tra, uv: uv_white, col: hue_color32);
6162 draw_list->PrimVtx(pos: trb, uv: uv_white, col: col_black);
6163 draw_list->PrimVtx(pos: trc, uv: uv_white, col: col_white);
6164 draw_list->AddTriangle(p1: tra, p2: trb, p3: trc, col: col_midgrey, thickness: 1.5f);
6165 sv_cursor_pos = ImLerp(a: ImLerp(a: trc, b: tra, t: ImSaturate(f: S)), b: trb, t: ImSaturate(f: 1 - V));
6166 }
6167 else if (flags & ImGuiColorEditFlags_PickerHueBar)
6168 {
6169 // Render SV Square
6170 draw_list->AddRectFilledMultiColor(p_min: picker_pos, p_max: picker_pos + ImVec2(sv_picker_size, sv_picker_size), col_upr_left: col_white, col_upr_right: hue_color32, col_bot_right: hue_color32, col_bot_left: col_white);
6171 draw_list->AddRectFilledMultiColor(p_min: picker_pos, p_max: picker_pos + ImVec2(sv_picker_size, sv_picker_size), col_upr_left: 0, col_upr_right: 0, col_bot_right: col_black, col_bot_left: col_black);
6172 RenderFrameBorder(p_min: picker_pos, p_max: picker_pos + ImVec2(sv_picker_size, sv_picker_size), rounding: 0.0f);
6173 sv_cursor_pos.x = ImClamp(IM_ROUND(picker_pos.x + ImSaturate(S) * sv_picker_size), mn: picker_pos.x + 2, mx: picker_pos.x + sv_picker_size - 2); // Sneakily prevent the circle to stick out too much
6174 sv_cursor_pos.y = ImClamp(IM_ROUND(picker_pos.y + ImSaturate(1 - V) * sv_picker_size), mn: picker_pos.y + 2, mx: picker_pos.y + sv_picker_size - 2);
6175
6176 // Render Hue Bar
6177 for (int i = 0; i < 6; ++i)
6178 draw_list->AddRectFilledMultiColor(p_min: ImVec2(bar0_pos_x, picker_pos.y + i * (sv_picker_size / 6)), p_max: ImVec2(bar0_pos_x + bars_width, picker_pos.y + (i + 1) * (sv_picker_size / 6)), col_upr_left: col_hues[i], col_upr_right: col_hues[i], col_bot_right: col_hues[i + 1], col_bot_left: col_hues[i + 1]);
6179 float bar0_line_y = IM_ROUND(picker_pos.y + H * sv_picker_size);
6180 RenderFrameBorder(p_min: ImVec2(bar0_pos_x, picker_pos.y), p_max: ImVec2(bar0_pos_x + bars_width, picker_pos.y + sv_picker_size), rounding: 0.0f);
6181 RenderArrowsForVerticalBar(draw_list, pos: ImVec2(bar0_pos_x - 1, bar0_line_y), half_sz: ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bar_w: bars_width + 2.0f, alpha: style.Alpha);
6182 }
6183
6184 // Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range)
6185 float sv_cursor_rad = value_changed_sv ? wheel_thickness * 0.55f : wheel_thickness * 0.40f;
6186 int sv_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(radius: sv_cursor_rad); // Lock segment count so the +1 one matches others.
6187 draw_list->AddCircleFilled(center: sv_cursor_pos, radius: sv_cursor_rad, col: user_col32_striped_of_alpha, num_segments: sv_cursor_segments);
6188 draw_list->AddCircle(center: sv_cursor_pos, radius: sv_cursor_rad + 1, col: col_midgrey, num_segments: sv_cursor_segments);
6189 draw_list->AddCircle(center: sv_cursor_pos, radius: sv_cursor_rad, col: col_white, num_segments: sv_cursor_segments);
6190
6191 // Render alpha bar
6192 if (alpha_bar)
6193 {
6194 float alpha = ImSaturate(f: col[3]);
6195 ImRect bar1_bb(bar1_pos_x, picker_pos.y, bar1_pos_x + bars_width, picker_pos.y + sv_picker_size);
6196 RenderColorRectWithAlphaCheckerboard(draw_list, p_min: bar1_bb.Min, p_max: bar1_bb.Max, fill_col: 0, grid_step: bar1_bb.GetWidth() / 2.0f, grid_off: ImVec2(0.0f, 0.0f));
6197 draw_list->AddRectFilledMultiColor(p_min: bar1_bb.Min, p_max: bar1_bb.Max, col_upr_left: user_col32_striped_of_alpha, col_upr_right: user_col32_striped_of_alpha, col_bot_right: user_col32_striped_of_alpha & ~IM_COL32_A_MASK, col_bot_left: user_col32_striped_of_alpha & ~IM_COL32_A_MASK);
6198 float bar1_line_y = IM_ROUND(picker_pos.y + (1.0f - alpha) * sv_picker_size);
6199 RenderFrameBorder(p_min: bar1_bb.Min, p_max: bar1_bb.Max, rounding: 0.0f);
6200 RenderArrowsForVerticalBar(draw_list, pos: ImVec2(bar1_pos_x - 1, bar1_line_y), half_sz: ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bar_w: bars_width + 2.0f, alpha: style.Alpha);
6201 }
6202
6203 EndGroup();
6204
6205 if (value_changed && memcmp(s1: backup_initial_col, s2: col, n: components * sizeof(float)) == 0)
6206 value_changed = false;
6207 if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
6208 MarkItemEdited(id: g.LastItemData.ID);
6209
6210 if (set_current_color_edit_id)
6211 g.ColorEditCurrentID = 0;
6212 PopID();
6213
6214 return value_changed;
6215}
6216
6217// A little color square. Return true when clicked.
6218// FIXME: May want to display/ignore the alpha component in the color display? Yet show it in the tooltip.
6219// 'desc_id' is not called 'label' because we don't display it next to the button, but only in the tooltip.
6220// Note that 'col' may be encoded in HSV if ImGuiColorEditFlags_InputHSV is set.
6221bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, const ImVec2& size_arg)
6222{
6223 ImGuiWindow* window = GetCurrentWindow();
6224 if (window->SkipItems)
6225 return false;
6226
6227 ImGuiContext& g = *GImGui;
6228 const ImGuiID id = window->GetID(str: desc_id);
6229 const float default_size = GetFrameHeight();
6230 const ImVec2 size(size_arg.x == 0.0f ? default_size : size_arg.x, size_arg.y == 0.0f ? default_size : size_arg.y);
6231 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
6232 ItemSize(bb, text_baseline_y: (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f);
6233 if (!ItemAdd(bb, id))
6234 return false;
6235
6236 bool hovered, held;
6237 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held);
6238
6239 if (flags & (ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaOpaque))
6240 flags &= ~(ImGuiColorEditFlags_AlphaNoBg | ImGuiColorEditFlags_AlphaPreviewHalf);
6241
6242 ImVec4 col_rgb = col;
6243 if (flags & ImGuiColorEditFlags_InputHSV)
6244 ColorConvertHSVtoRGB(h: col_rgb.x, s: col_rgb.y, v: col_rgb.z, out_r&: col_rgb.x, out_g&: col_rgb.y, out_b&: col_rgb.z);
6245
6246 ImVec4 col_rgb_without_alpha(col_rgb.x, col_rgb.y, col_rgb.z, 1.0f);
6247 float grid_step = ImMin(lhs: size.x, rhs: size.y) / 2.99f;
6248 float rounding = ImMin(lhs: g.Style.FrameRounding, rhs: grid_step * 0.5f);
6249 ImRect bb_inner = bb;
6250 float off = 0.0f;
6251 if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
6252 {
6253 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.
6254 bb_inner.Expand(amount: off);
6255 }
6256 if ((flags & ImGuiColorEditFlags_AlphaPreviewHalf) && col_rgb.w < 1.0f)
6257 {
6258 float mid_x = IM_ROUND((bb_inner.Min.x + bb_inner.Max.x) * 0.5f);
6259 if ((flags & ImGuiColorEditFlags_AlphaNoBg) == 0)
6260 RenderColorRectWithAlphaCheckerboard(draw_list: window->DrawList, p_min: ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), p_max: bb_inner.Max, fill_col: GetColorU32(col: col_rgb), grid_step, grid_off: ImVec2(-grid_step + off, off), rounding, flags: ImDrawFlags_RoundCornersRight);
6261 else
6262 window->DrawList->AddRectFilled(p_min: ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), p_max: bb_inner.Max, col: GetColorU32(col: col_rgb), rounding, flags: ImDrawFlags_RoundCornersRight);
6263 window->DrawList->AddRectFilled(p_min: bb_inner.Min, p_max: ImVec2(mid_x, bb_inner.Max.y), col: GetColorU32(col: col_rgb_without_alpha), rounding, flags: ImDrawFlags_RoundCornersLeft);
6264 }
6265 else
6266 {
6267 // Because GetColorU32() multiplies by the global style Alpha and we don't want to display a checkerboard if the source code had no alpha
6268 ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaOpaque) ? col_rgb_without_alpha : col_rgb;
6269 if (col_source.w < 1.0f && (flags & ImGuiColorEditFlags_AlphaNoBg) == 0)
6270 RenderColorRectWithAlphaCheckerboard(draw_list: window->DrawList, p_min: bb_inner.Min, p_max: bb_inner.Max, fill_col: GetColorU32(col: col_source), grid_step, grid_off: ImVec2(off, off), rounding);
6271 else
6272 window->DrawList->AddRectFilled(p_min: bb_inner.Min, p_max: bb_inner.Max, col: GetColorU32(col: col_source), rounding);
6273 }
6274 RenderNavCursor(bb, id);
6275 if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
6276 {
6277 if (g.Style.FrameBorderSize > 0.0f)
6278 RenderFrameBorder(p_min: bb.Min, p_max: bb.Max, rounding);
6279 else
6280 window->DrawList->AddRect(p_min: bb.Min, p_max: bb.Max, col: GetColorU32(idx: ImGuiCol_FrameBg), rounding); // Color buttons are often in need of some sort of border // FIXME-DPI
6281 }
6282
6283 // Drag and Drop Source
6284 // NB: The ActiveId test is merely an optional micro-optimization, BeginDragDropSource() does the same test.
6285 if (g.ActiveId == id && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropSource())
6286 {
6287 if (flags & ImGuiColorEditFlags_NoAlpha)
6288 SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F, data: &col_rgb, sz: sizeof(float) * 3, cond: ImGuiCond_Once);
6289 else
6290 SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, data: &col_rgb, sz: sizeof(float) * 4, cond: ImGuiCond_Once);
6291 ColorButton(desc_id, col, flags);
6292 SameLine();
6293 TextEx(text: "Color");
6294 EndDragDropSource();
6295 }
6296
6297 // Tooltip
6298 if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered && IsItemHovered(flags: ImGuiHoveredFlags_ForTooltip))
6299 ColorTooltip(text: desc_id, col: &col.x, flags: flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_AlphaMask_));
6300
6301 return pressed;
6302}
6303
6304// Initialize/override default color options
6305void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags)
6306{
6307 ImGuiContext& g = *GImGui;
6308 if ((flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6309 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DisplayMask_;
6310 if ((flags & ImGuiColorEditFlags_DataTypeMask_) == 0)
6311 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DataTypeMask_;
6312 if ((flags & ImGuiColorEditFlags_PickerMask_) == 0)
6313 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_PickerMask_;
6314 if ((flags & ImGuiColorEditFlags_InputMask_) == 0)
6315 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_InputMask_;
6316 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check only 1 option is selected
6317 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DataTypeMask_)); // Check only 1 option is selected
6318 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check only 1 option is selected
6319 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check only 1 option is selected
6320 g.ColorEditOptions = flags;
6321}
6322
6323// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
6324void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags)
6325{
6326 ImGuiContext& g = *GImGui;
6327
6328 if (!BeginTooltipEx(tooltip_flags: ImGuiTooltipFlags_OverridePrevious, extra_window_flags: ImGuiWindowFlags_None))
6329 return;
6330 const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text;
6331 if (text_end > text)
6332 {
6333 TextEx(text, text_end);
6334 Separator();
6335 }
6336
6337 ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2);
6338 ImVec4 cf(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6339 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]);
6340 ImGuiColorEditFlags flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_AlphaMask_;
6341 ColorButton(desc_id: "##preview", col: cf, flags: (flags & flags_to_forward) | ImGuiColorEditFlags_NoTooltip, size_arg: sz);
6342 SameLine();
6343 if ((flags & ImGuiColorEditFlags_InputRGB) || !(flags & ImGuiColorEditFlags_InputMask_))
6344 {
6345 if (flags & ImGuiColorEditFlags_NoAlpha)
6346 Text(fmt: "#%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]);
6347 else
6348 Text(fmt: "#%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]);
6349 }
6350 else if (flags & ImGuiColorEditFlags_InputHSV)
6351 {
6352 if (flags & ImGuiColorEditFlags_NoAlpha)
6353 Text(fmt: "H: %.3f, S: %.3f, V: %.3f", col[0], col[1], col[2]);
6354 else
6355 Text(fmt: "H: %.3f, S: %.3f, V: %.3f, A: %.3f", col[0], col[1], col[2], col[3]);
6356 }
6357 EndTooltip();
6358}
6359
6360void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags)
6361{
6362 bool allow_opt_inputs = !(flags & ImGuiColorEditFlags_DisplayMask_);
6363 bool allow_opt_datatype = !(flags & ImGuiColorEditFlags_DataTypeMask_);
6364 if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup(str_id: "context"))
6365 return;
6366
6367 ImGuiContext& g = *GImGui;
6368 PushItemFlag(option: ImGuiItemFlags_NoMarkEdited, enabled: true);
6369 ImGuiColorEditFlags opts = g.ColorEditOptions;
6370 if (allow_opt_inputs)
6371 {
6372 if (RadioButton(label: "RGB", active: (opts & ImGuiColorEditFlags_DisplayRGB) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayRGB;
6373 if (RadioButton(label: "HSV", active: (opts & ImGuiColorEditFlags_DisplayHSV) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHSV;
6374 if (RadioButton(label: "Hex", active: (opts & ImGuiColorEditFlags_DisplayHex) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHex;
6375 }
6376 if (allow_opt_datatype)
6377 {
6378 if (allow_opt_inputs) Separator();
6379 if (RadioButton(label: "0..255", active: (opts & ImGuiColorEditFlags_Uint8) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Uint8;
6380 if (RadioButton(label: "0.00..1.00", active: (opts & ImGuiColorEditFlags_Float) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Float;
6381 }
6382
6383 if (allow_opt_inputs || allow_opt_datatype)
6384 Separator();
6385 if (Button(label: "Copy as..", size_arg: ImVec2(-1, 0)))
6386 OpenPopup(str_id: "Copy");
6387 if (BeginPopup(str_id: "Copy"))
6388 {
6389 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]);
6390 char buf[64];
6391 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "(%.3ff, %.3ff, %.3ff, %.3ff)", col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6392 if (Selectable(label: buf))
6393 SetClipboardText(buf);
6394 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "(%d,%d,%d,%d)", cr, cg, cb, ca);
6395 if (Selectable(label: buf))
6396 SetClipboardText(buf);
6397 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "#%02X%02X%02X", cr, cg, cb);
6398 if (Selectable(label: buf))
6399 SetClipboardText(buf);
6400 if (!(flags & ImGuiColorEditFlags_NoAlpha))
6401 {
6402 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "#%02X%02X%02X%02X", cr, cg, cb, ca);
6403 if (Selectable(label: buf))
6404 SetClipboardText(buf);
6405 }
6406 EndPopup();
6407 }
6408
6409 g.ColorEditOptions = opts;
6410 PopItemFlag();
6411 EndPopup();
6412}
6413
6414void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags)
6415{
6416 bool allow_opt_picker = !(flags & ImGuiColorEditFlags_PickerMask_);
6417 bool allow_opt_alpha_bar = !(flags & ImGuiColorEditFlags_NoAlpha) && !(flags & ImGuiColorEditFlags_AlphaBar);
6418 if ((!allow_opt_picker && !allow_opt_alpha_bar) || !BeginPopup(str_id: "context"))
6419 return;
6420
6421 ImGuiContext& g = *GImGui;
6422 PushItemFlag(option: ImGuiItemFlags_NoMarkEdited, enabled: true);
6423 if (allow_opt_picker)
6424 {
6425 ImVec2 picker_size(g.FontSize * 8, ImMax(lhs: g.FontSize * 8 - (GetFrameHeight() + g.Style.ItemInnerSpacing.x), rhs: 1.0f)); // FIXME: Picker size copied from main picker function
6426 PushItemWidth(item_width: picker_size.x);
6427 for (int picker_type = 0; picker_type < 2; picker_type++)
6428 {
6429 // Draw small/thumbnail version of each picker type (over an invisible button for selection)
6430 if (picker_type > 0) Separator();
6431 PushID(int_id: picker_type);
6432 ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoSidePreview | (flags & ImGuiColorEditFlags_NoAlpha);
6433 if (picker_type == 0) picker_flags |= ImGuiColorEditFlags_PickerHueBar;
6434 if (picker_type == 1) picker_flags |= ImGuiColorEditFlags_PickerHueWheel;
6435 ImVec2 backup_pos = GetCursorScreenPos();
6436 if (Selectable(label: "##selectable", selected: false, flags: 0, size: picker_size)) // By default, Selectable() is closing popup
6437 g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags_PickerMask_) | (picker_flags & ImGuiColorEditFlags_PickerMask_);
6438 SetCursorScreenPos(backup_pos);
6439 ImVec4 previewing_ref_col;
6440 memcpy(dest: &previewing_ref_col, src: ref_col, n: sizeof(float) * ((picker_flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4));
6441 ColorPicker4(label: "##previewing_picker", col: &previewing_ref_col.x, flags: picker_flags);
6442 PopID();
6443 }
6444 PopItemWidth();
6445 }
6446 if (allow_opt_alpha_bar)
6447 {
6448 if (allow_opt_picker) Separator();
6449 CheckboxFlags(label: "Alpha Bar", flags: &g.ColorEditOptions, flags_value: ImGuiColorEditFlags_AlphaBar);
6450 }
6451 PopItemFlag();
6452 EndPopup();
6453}
6454
6455//-------------------------------------------------------------------------
6456// [SECTION] Widgets: TreeNode, CollapsingHeader, etc.
6457//-------------------------------------------------------------------------
6458// - TreeNode()
6459// - TreeNodeV()
6460// - TreeNodeEx()
6461// - TreeNodeExV()
6462// - TreeNodeStoreStackData() [Internal]
6463// - TreeNodeBehavior() [Internal]
6464// - TreePush()
6465// - TreePop()
6466// - GetTreeNodeToLabelSpacing()
6467// - SetNextItemOpen()
6468// - CollapsingHeader()
6469//-------------------------------------------------------------------------
6470
6471bool ImGui::TreeNode(const char* str_id, const char* fmt, ...)
6472{
6473 va_list args;
6474 va_start(args, fmt);
6475 bool is_open = TreeNodeExV(str_id, flags: 0, fmt, args);
6476 va_end(args);
6477 return is_open;
6478}
6479
6480bool ImGui::TreeNode(const void* ptr_id, const char* fmt, ...)
6481{
6482 va_list args;
6483 va_start(args, fmt);
6484 bool is_open = TreeNodeExV(ptr_id, flags: 0, fmt, args);
6485 va_end(args);
6486 return is_open;
6487}
6488
6489bool ImGui::TreeNode(const char* label)
6490{
6491 ImGuiWindow* window = GetCurrentWindow();
6492 if (window->SkipItems)
6493 return false;
6494 ImGuiID id = window->GetID(str: label);
6495 return TreeNodeBehavior(id, flags: ImGuiTreeNodeFlags_None, label, NULL);
6496}
6497
6498bool ImGui::TreeNodeV(const char* str_id, const char* fmt, va_list args)
6499{
6500 return TreeNodeExV(str_id, flags: 0, fmt, args);
6501}
6502
6503bool ImGui::TreeNodeV(const void* ptr_id, const char* fmt, va_list args)
6504{
6505 return TreeNodeExV(ptr_id, flags: 0, fmt, args);
6506}
6507
6508bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags)
6509{
6510 ImGuiWindow* window = GetCurrentWindow();
6511 if (window->SkipItems)
6512 return false;
6513 ImGuiID id = window->GetID(str: label);
6514 return TreeNodeBehavior(id, flags, label, NULL);
6515}
6516
6517bool ImGui::TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
6518{
6519 va_list args;
6520 va_start(args, fmt);
6521 bool is_open = TreeNodeExV(str_id, flags, fmt, args);
6522 va_end(args);
6523 return is_open;
6524}
6525
6526bool ImGui::TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
6527{
6528 va_list args;
6529 va_start(args, fmt);
6530 bool is_open = TreeNodeExV(ptr_id, flags, fmt, args);
6531 va_end(args);
6532 return is_open;
6533}
6534
6535bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
6536{
6537 ImGuiWindow* window = GetCurrentWindow();
6538 if (window->SkipItems)
6539 return false;
6540
6541 ImGuiID id = window->GetID(str: str_id);
6542 const char* label, *label_end;
6543 ImFormatStringToTempBufferV(out_buf: &label, out_buf_end: &label_end, fmt, args);
6544 return TreeNodeBehavior(id, flags, label, label_end);
6545}
6546
6547bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
6548{
6549 ImGuiWindow* window = GetCurrentWindow();
6550 if (window->SkipItems)
6551 return false;
6552
6553 ImGuiID id = window->GetID(ptr: ptr_id);
6554 const char* label, *label_end;
6555 ImFormatStringToTempBufferV(out_buf: &label, out_buf_end: &label_end, fmt, args);
6556 return TreeNodeBehavior(id, flags, label, label_end);
6557}
6558
6559bool ImGui::TreeNodeGetOpen(ImGuiID storage_id)
6560{
6561 ImGuiContext& g = *GImGui;
6562 ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
6563 return storage->GetInt(key: storage_id, default_val: 0) != 0;
6564}
6565
6566void ImGui::TreeNodeSetOpen(ImGuiID storage_id, bool open)
6567{
6568 ImGuiContext& g = *GImGui;
6569 ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
6570 storage->SetInt(key: storage_id, val: open ? 1 : 0);
6571}
6572
6573bool ImGui::TreeNodeUpdateNextOpen(ImGuiID storage_id, ImGuiTreeNodeFlags flags)
6574{
6575 if (flags & ImGuiTreeNodeFlags_Leaf)
6576 return true;
6577
6578 // We only write to the tree storage if the user clicks, or explicitly use the SetNextItemOpen function
6579 ImGuiContext& g = *GImGui;
6580 ImGuiWindow* window = g.CurrentWindow;
6581 ImGuiStorage* storage = window->DC.StateStorage;
6582
6583 bool is_open;
6584 if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasOpen)
6585 {
6586 if (g.NextItemData.OpenCond & ImGuiCond_Always)
6587 {
6588 is_open = g.NextItemData.OpenVal;
6589 TreeNodeSetOpen(storage_id, open: is_open);
6590 }
6591 else
6592 {
6593 // We treat ImGuiCond_Once and ImGuiCond_FirstUseEver the same because tree node state are not saved persistently.
6594 const int stored_value = storage->GetInt(key: storage_id, default_val: -1);
6595 if (stored_value == -1)
6596 {
6597 is_open = g.NextItemData.OpenVal;
6598 TreeNodeSetOpen(storage_id, open: is_open);
6599 }
6600 else
6601 {
6602 is_open = stored_value != 0;
6603 }
6604 }
6605 }
6606 else
6607 {
6608 is_open = storage->GetInt(key: storage_id, default_val: (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0;
6609 }
6610
6611 // When logging is enabled, we automatically expand tree nodes (but *NOT* collapsing headers.. seems like sensible behavior).
6612 // NB- If we are above max depth we still allow manually opened nodes to be logged.
6613 if (g.LogEnabled && !(flags & ImGuiTreeNodeFlags_NoAutoOpenOnLog) && (window->DC.TreeDepth - g.LogDepthRef) < g.LogDepthToExpand)
6614 is_open = true;
6615
6616 return is_open;
6617}
6618
6619// Store ImGuiTreeNodeStackData for just submitted node.
6620// Currently only supports 32 level deep and we are fine with (1 << Depth) overflowing into a zero, easy to increase.
6621static void TreeNodeStoreStackData(ImGuiTreeNodeFlags flags, float x1)
6622{
6623 ImGuiContext& g = *GImGui;
6624 ImGuiWindow* window = g.CurrentWindow;
6625
6626 g.TreeNodeStack.resize(new_size: g.TreeNodeStack.Size + 1);
6627 ImGuiTreeNodeStackData* tree_node_data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
6628 tree_node_data->ID = g.LastItemData.ID;
6629 tree_node_data->TreeFlags = flags;
6630 tree_node_data->ItemFlags = g.LastItemData.ItemFlags;
6631 tree_node_data->NavRect = g.LastItemData.NavRect;
6632
6633 // Initially I tried to latch value for GetColorU32(ImGuiCol_TreeLines) but it's not a good trade-off for very large trees.
6634 const bool draw_lines = (flags & (ImGuiTreeNodeFlags_DrawLinesFull | ImGuiTreeNodeFlags_DrawLinesToNodes)) != 0;
6635 tree_node_data->DrawLinesX1 = draw_lines ? (x1 + g.FontSize * 0.5f + g.Style.FramePadding.x) : +FLT_MAX;
6636 tree_node_data->DrawLinesTableColumn = (draw_lines && g.CurrentTable) ? (ImGuiTableColumnIdx)g.CurrentTable->CurrentColumn : -1;
6637 tree_node_data->DrawLinesToNodesY2 = -FLT_MAX;
6638 window->DC.TreeHasStackDataDepthMask |= (1 << window->DC.TreeDepth);
6639 if (flags & ImGuiTreeNodeFlags_DrawLinesToNodes)
6640 window->DC.TreeRecordsClippedNodesY2Mask |= (1 << window->DC.TreeDepth);
6641}
6642
6643// When using public API, currently 'id == storage_id' is always true, but we separate the values to facilitate advanced user code doing storage queries outside of UI loop.
6644bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end)
6645{
6646 ImGuiWindow* window = GetCurrentWindow();
6647 if (window->SkipItems)
6648 return false;
6649
6650 ImGuiContext& g = *GImGui;
6651 const ImGuiStyle& style = g.Style;
6652 const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0;
6653 const ImVec2 padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, ImMin(lhs: window->DC.CurrLineTextBaseOffset, rhs: style.FramePadding.y));
6654
6655 if (!label_end)
6656 label_end = FindRenderedTextEnd(text: label);
6657 const ImVec2 label_size = CalcTextSize(text: label, text_end: label_end, hide_text_after_double_hash: false);
6658
6659 const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapsing arrow width + Spacing
6660 const float text_offset_y = ImMax(lhs: padding.y, rhs: window->DC.CurrLineTextBaseOffset); // Latch before ItemSize changes it
6661 const float text_width = g.FontSize + label_size.x + padding.x * 2; // Include collapsing arrow
6662
6663 // We vertically grow up to current line height up the typical widget height.
6664 const float frame_height = ImMax(lhs: ImMin(lhs: window->DC.CurrLineSize.y, rhs: g.FontSize + style.FramePadding.y * 2), rhs: label_size.y + padding.y * 2);
6665 const bool span_all_columns = (flags & ImGuiTreeNodeFlags_SpanAllColumns) != 0 && (g.CurrentTable != NULL);
6666 const bool span_all_columns_label = (flags & ImGuiTreeNodeFlags_LabelSpanAllColumns) != 0 && (g.CurrentTable != NULL);
6667 ImRect frame_bb;
6668 frame_bb.Min.x = span_all_columns ? window->ParentWorkRect.Min.x : (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x;
6669 frame_bb.Min.y = window->DC.CursorPos.y;
6670 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;
6671 frame_bb.Max.y = window->DC.CursorPos.y + frame_height;
6672 if (display_frame)
6673 {
6674 const float outer_extend = IM_TRUNC(window->WindowPadding.x * 0.5f); // Framed header expand a little outside of current limits
6675 frame_bb.Min.x -= outer_extend;
6676 frame_bb.Max.x += outer_extend;
6677 }
6678
6679 ImVec2 text_pos(window->DC.CursorPos.x + text_offset_x, window->DC.CursorPos.y + text_offset_y);
6680 ItemSize(size: ImVec2(text_width, frame_height), text_baseline_y: padding.y);
6681
6682 // For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing
6683 ImRect interact_bb = frame_bb;
6684 if ((flags & (ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_SpanLabelWidth | ImGuiTreeNodeFlags_SpanAllColumns)) == 0)
6685 interact_bb.Max.x = frame_bb.Min.x + text_width + (label_size.x > 0.0f ? style.ItemSpacing.x * 2.0f : 0.0f);
6686
6687 // Compute open and multi-select states before ItemAdd() as it clear NextItem data.
6688 ImGuiID storage_id = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasStorageID) ? g.NextItemData.StorageId : id;
6689 bool is_open = TreeNodeUpdateNextOpen(storage_id, flags);
6690
6691 bool is_visible;
6692 if (span_all_columns || span_all_columns_label)
6693 {
6694 // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable..
6695 const float backup_clip_rect_min_x = window->ClipRect.Min.x;
6696 const float backup_clip_rect_max_x = window->ClipRect.Max.x;
6697 window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
6698 window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
6699 is_visible = ItemAdd(bb: interact_bb, id);
6700 window->ClipRect.Min.x = backup_clip_rect_min_x;
6701 window->ClipRect.Max.x = backup_clip_rect_max_x;
6702 }
6703 else
6704 {
6705 is_visible = ItemAdd(bb: interact_bb, id);
6706 }
6707 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect;
6708 g.LastItemData.DisplayRect = frame_bb;
6709
6710 // If a NavLeft request is happening and ImGuiTreeNodeFlags_NavLeftJumpsToParent enabled:
6711 // Store data for the current depth to allow returning to this node from any child item.
6712 // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop().
6713 // It will become tempting to enable ImGuiTreeNodeFlags_NavLeftJumpsToParent by default or move it to ImGuiStyle.
6714 bool store_tree_node_stack_data = false;
6715 if ((flags & ImGuiTreeNodeFlags_DrawLinesMask_) == 0)
6716 flags |= g.Style.TreeLinesFlags;
6717 const bool draw_tree_lines = (flags & (ImGuiTreeNodeFlags_DrawLinesFull | ImGuiTreeNodeFlags_DrawLinesToNodes)) && (frame_bb.Min.y < window->ClipRect.Max.y) && (g.Style.TreeLinesSize > 0.0f);
6718 if (!(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6719 {
6720 store_tree_node_stack_data = draw_tree_lines;
6721 if ((flags & ImGuiTreeNodeFlags_NavLeftJumpsToParent) && !g.NavIdIsAlive)
6722 if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
6723 store_tree_node_stack_data = true;
6724 }
6725
6726 const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0;
6727 if (!is_visible)
6728 {
6729 if ((flags & ImGuiTreeNodeFlags_DrawLinesToNodes) && (window->DC.TreeRecordsClippedNodesY2Mask & (1 << (window->DC.TreeDepth - 1))))
6730 {
6731 ImGuiTreeNodeStackData* parent_data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
6732 parent_data->DrawLinesToNodesY2 = ImMax(lhs: parent_data->DrawLinesToNodesY2, rhs: window->DC.CursorPos.y); // Don't need to aim to mid Y position as we are clipped anyway.
6733 if (frame_bb.Min.y >= window->ClipRect.Max.y)
6734 window->DC.TreeRecordsClippedNodesY2Mask &= ~(1 << (window->DC.TreeDepth - 1)); // Done
6735 }
6736 if (is_open && store_tree_node_stack_data)
6737 TreeNodeStoreStackData(flags, x1: text_pos.x - text_offset_x); // Call before TreePushOverrideID()
6738 if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6739 TreePushOverrideID(id);
6740 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
6741 return is_open;
6742 }
6743
6744 if (span_all_columns || span_all_columns_label)
6745 {
6746 TablePushBackgroundChannel();
6747 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect;
6748 g.LastItemData.ClipRect = window->ClipRect;
6749 }
6750
6751 ImGuiButtonFlags button_flags = ImGuiTreeNodeFlags_None;
6752 if ((flags & ImGuiTreeNodeFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap))
6753 button_flags |= ImGuiButtonFlags_AllowOverlap;
6754 if (!is_leaf)
6755 button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
6756
6757 // We allow clicking on the arrow section with keyboard modifiers held, in order to easily
6758 // allow browsing a tree while preserving selection with code implementing multi-selection patterns.
6759 // When clicking on the rest of the tree node we always disallow keyboard modifiers.
6760 const float arrow_hit_x1 = (text_pos.x - text_offset_x) - style.TouchExtraPadding.x;
6761 const float arrow_hit_x2 = (text_pos.x - text_offset_x) + (g.FontSize + padding.x * 2.0f) + style.TouchExtraPadding.x;
6762 const bool is_mouse_x_over_arrow = (g.IO.MousePos.x >= arrow_hit_x1 && g.IO.MousePos.x < arrow_hit_x2);
6763
6764 const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
6765 if (is_multi_select) // We absolutely need to distinguish open vs select so _OpenOnArrow comes by default
6766 flags |= (flags & ImGuiTreeNodeFlags_OpenOnMask_) == 0 ? ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick : ImGuiTreeNodeFlags_OpenOnArrow;
6767
6768 // Open behaviors can be altered with the _OpenOnArrow and _OnOnDoubleClick flags.
6769 // Some alteration have subtle effects (e.g. toggle on MouseUp vs MouseDown events) due to requirements for multi-selection and drag and drop support.
6770 // - Single-click on label = Toggle on MouseUp (default, when _OpenOnArrow=0)
6771 // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=0)
6772 // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=1)
6773 // - Double-click on label = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1)
6774 // - Double-click on arrow = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1 and _OpenOnArrow=0)
6775 // It is rather standard that arrow click react on Down rather than Up.
6776 // 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.
6777 if (is_mouse_x_over_arrow)
6778 button_flags |= ImGuiButtonFlags_PressedOnClick;
6779 else if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick)
6780 button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick;
6781 else
6782 button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
6783 if (flags & ImGuiTreeNodeFlags_NoNavFocus)
6784 button_flags |= ImGuiButtonFlags_NoNavFocus;
6785
6786 bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0;
6787 const bool was_selected = selected;
6788
6789 // Multi-selection support (header)
6790 if (is_multi_select)
6791 {
6792 // Handle multi-select + alter button flags for it
6793 MultiSelectItemHeader(id, p_selected: &selected, p_button_flags: &button_flags);
6794 if (is_mouse_x_over_arrow)
6795 button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease;
6796 }
6797 else
6798 {
6799 if (window != g.HoveredWindow || !is_mouse_x_over_arrow)
6800 button_flags |= ImGuiButtonFlags_NoKeyModsAllowed;
6801 }
6802
6803 bool hovered, held;
6804 bool pressed = ButtonBehavior(bb: interact_bb, id, out_hovered: &hovered, out_held: &held, flags: button_flags);
6805 bool toggled = false;
6806 if (!is_leaf)
6807 {
6808 if (pressed && g.DragDropHoldJustPressedId != id)
6809 {
6810 if ((flags & ImGuiTreeNodeFlags_OpenOnMask_) == 0 || (g.NavActivateId == id && !is_multi_select))
6811 toggled = true; // Single click
6812 if (flags & ImGuiTreeNodeFlags_OpenOnArrow)
6813 toggled |= is_mouse_x_over_arrow && !g.NavHighlightItemUnderNav; // Lightweight equivalent of IsMouseHoveringRect() since ButtonBehavior() already did the job
6814 if ((flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) && g.IO.MouseClickedCount[0] == 2)
6815 toggled = true; // Double click
6816 }
6817 else if (pressed && g.DragDropHoldJustPressedId == id)
6818 {
6819 IM_ASSERT(button_flags & ImGuiButtonFlags_PressedOnDragDropHold);
6820 if (!is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again.
6821 toggled = true;
6822 else
6823 pressed = false; // Cancel press so it doesn't trigger selection.
6824 }
6825
6826 if (g.NavId == id && g.NavMoveDir == ImGuiDir_Left && is_open)
6827 {
6828 toggled = true;
6829 NavClearPreferredPosForAxis(axis: ImGuiAxis_X);
6830 NavMoveRequestCancel();
6831 }
6832 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?
6833 {
6834 toggled = true;
6835 NavClearPreferredPosForAxis(axis: ImGuiAxis_X);
6836 NavMoveRequestCancel();
6837 }
6838
6839 if (toggled)
6840 {
6841 is_open = !is_open;
6842 window->DC.StateStorage->SetInt(key: storage_id, val: is_open);
6843 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledOpen;
6844 }
6845 }
6846
6847 // Multi-selection support (footer)
6848 if (is_multi_select)
6849 {
6850 bool pressed_copy = pressed && !toggled;
6851 MultiSelectItemFooter(id, p_selected: &selected, p_pressed: &pressed_copy);
6852 if (pressed)
6853 SetNavID(id, nav_layer: window->DC.NavLayerCurrent, focus_scope_id: g.CurrentFocusScopeId, rect_rel: interact_bb);
6854 }
6855
6856 if (selected != was_selected)
6857 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
6858
6859 // Render
6860 {
6861 const ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
6862 ImGuiNavRenderCursorFlags nav_render_cursor_flags = ImGuiNavRenderCursorFlags_Compact;
6863 if (is_multi_select)
6864 nav_render_cursor_flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle
6865 if (display_frame)
6866 {
6867 // Framed type
6868 const ImU32 bg_col = GetColorU32(idx: (held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
6869 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: bg_col, borders: true, rounding: style.FrameRounding);
6870 RenderNavCursor(bb: frame_bb, id, flags: nav_render_cursor_flags);
6871 if (span_all_columns && !span_all_columns_label)
6872 TablePopBackgroundChannel();
6873 if (flags & ImGuiTreeNodeFlags_Bullet)
6874 RenderBullet(draw_list: window->DrawList, pos: ImVec2(text_pos.x - text_offset_x * 0.60f, text_pos.y + g.FontSize * 0.5f), col: text_col);
6875 else if (!is_leaf)
6876 RenderArrow(draw_list: window->DrawList, pos: ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y), col: text_col, dir: is_open ? ((flags & ImGuiTreeNodeFlags_UpsideDownArrow) ? ImGuiDir_Up : ImGuiDir_Down) : ImGuiDir_Right, scale: 1.0f);
6877 else // Leaf without bullet, left-adjusted text
6878 text_pos.x -= text_offset_x - padding.x;
6879 if (flags & ImGuiTreeNodeFlags_ClipLabelForTrailingButton)
6880 frame_bb.Max.x -= g.FontSize + style.FramePadding.x;
6881 if (g.LogEnabled)
6882 LogSetNextTextDecoration(prefix: "###", suffix: "###");
6883 }
6884 else
6885 {
6886 // Unframed typed for tree nodes
6887 if (hovered || selected)
6888 {
6889 const ImU32 bg_col = GetColorU32(idx: (held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
6890 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: bg_col, borders: false);
6891 }
6892 RenderNavCursor(bb: frame_bb, id, flags: nav_render_cursor_flags);
6893 if (span_all_columns && !span_all_columns_label)
6894 TablePopBackgroundChannel();
6895 if (flags & ImGuiTreeNodeFlags_Bullet)
6896 RenderBullet(draw_list: window->DrawList, pos: ImVec2(text_pos.x - text_offset_x * 0.5f, text_pos.y + g.FontSize * 0.5f), col: text_col);
6897 else if (!is_leaf)
6898 RenderArrow(draw_list: window->DrawList, pos: ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y + g.FontSize * 0.15f), col: text_col, dir: is_open ? ((flags & ImGuiTreeNodeFlags_UpsideDownArrow) ? ImGuiDir_Up : ImGuiDir_Down) : ImGuiDir_Right, scale: 0.70f);
6899 if (g.LogEnabled)
6900 LogSetNextTextDecoration(prefix: ">", NULL);
6901 }
6902
6903 if (draw_tree_lines)
6904 TreeNodeDrawLineToChildNode(target_pos: ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y + g.FontSize * 0.5f));
6905
6906 // Label
6907 if (display_frame)
6908 RenderTextClipped(pos_min: text_pos, pos_max: frame_bb.Max, text: label, text_end: label_end, text_size_if_known: &label_size);
6909 else
6910 RenderText(pos: text_pos, text: label, text_end: label_end, hide_text_after_hash: false);
6911
6912 if (span_all_columns_label)
6913 TablePopBackgroundChannel();
6914 }
6915
6916 if (is_open && store_tree_node_stack_data)
6917 TreeNodeStoreStackData(flags, x1: text_pos.x - text_offset_x); // Call before TreePushOverrideID()
6918 if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6919 TreePushOverrideID(id); // Could use TreePush(label) but this avoid computing twice
6920
6921 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
6922 return is_open;
6923}
6924
6925// Draw horizontal line from our parent node
6926// This is only called for visible child nodes so we are not too fussy anymore about performances
6927void ImGui::TreeNodeDrawLineToChildNode(const ImVec2& target_pos)
6928{
6929 ImGuiContext& g = *GImGui;
6930 ImGuiWindow* window = g.CurrentWindow;
6931 if (window->DC.TreeDepth == 0 || (window->DC.TreeHasStackDataDepthMask & (1 << (window->DC.TreeDepth - 1))) == 0)
6932 return;
6933
6934 ImGuiTreeNodeStackData* parent_data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
6935 float x1 = ImTrunc(f: parent_data->DrawLinesX1);
6936 float x2 = ImTrunc(f: target_pos.x - g.Style.ItemInnerSpacing.x);
6937 float y = ImTrunc(f: target_pos.y);
6938 float rounding = (g.Style.TreeLinesRounding > 0.0f) ? ImMin(lhs: x2 - x1, rhs: g.Style.TreeLinesRounding) : 0.0f;
6939 parent_data->DrawLinesToNodesY2 = ImMax(lhs: parent_data->DrawLinesToNodesY2, rhs: y - rounding);
6940 if (x1 >= x2)
6941 return;
6942 if (rounding > 0.0f)
6943 {
6944 x1 += 0.5f + rounding;
6945 window->DrawList->PathArcToFast(center: ImVec2(x1, y - rounding), radius: rounding, a_min_of_12: 6, a_max_of_12: 3);
6946 if (x1 < x2)
6947 window->DrawList->PathLineTo(pos: ImVec2(x2, y));
6948 window->DrawList->PathStroke(col: GetColorU32(idx: ImGuiCol_TreeLines), flags: ImDrawFlags_None, thickness: g.Style.TreeLinesSize);
6949 }
6950 else
6951 {
6952 window->DrawList->AddLine(p1: ImVec2(x1, y), p2: ImVec2(x2, y), col: GetColorU32(idx: ImGuiCol_TreeLines), thickness: g.Style.TreeLinesSize);
6953 }
6954}
6955
6956// Draw vertical line of the hierarchy
6957void ImGui::TreeNodeDrawLineToTreePop(const ImGuiTreeNodeStackData* data)
6958{
6959 ImGuiContext& g = *GImGui;
6960 ImGuiWindow* window = g.CurrentWindow;
6961 float y1 = ImMax(lhs: data->NavRect.Max.y, rhs: window->ClipRect.Min.y);
6962 float y2 = data->DrawLinesToNodesY2;
6963 if (data->TreeFlags & ImGuiTreeNodeFlags_DrawLinesFull)
6964 {
6965 float y2_full = window->DC.CursorPos.y;
6966 if (g.CurrentTable)
6967 y2_full = ImMax(lhs: g.CurrentTable->RowPosY2, rhs: y2_full);
6968 y2_full = ImTrunc(f: y2_full - g.Style.ItemSpacing.y - g.FontSize * 0.5f);
6969 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
6970 y2 = y2_full;
6971 }
6972 y2 = ImMin(lhs: y2, rhs: window->ClipRect.Max.y);
6973 if (y2 <= y1)
6974 return;
6975 float x = ImTrunc(f: data->DrawLinesX1);
6976 if (data->DrawLinesTableColumn != -1)
6977 TablePushColumnChannel(column_n: data->DrawLinesTableColumn);
6978 window->DrawList->AddLine(p1: ImVec2(x, y1), p2: ImVec2(x, y2), col: GetColorU32(idx: ImGuiCol_TreeLines), thickness: g.Style.TreeLinesSize);
6979 if (data->DrawLinesTableColumn != -1)
6980 TablePopColumnChannel();
6981}
6982
6983void ImGui::TreePush(const char* str_id)
6984{
6985 ImGuiWindow* window = GetCurrentWindow();
6986 Indent();
6987 window->DC.TreeDepth++;
6988 PushID(str_id);
6989}
6990
6991void ImGui::TreePush(const void* ptr_id)
6992{
6993 ImGuiWindow* window = GetCurrentWindow();
6994 Indent();
6995 window->DC.TreeDepth++;
6996 PushID(ptr_id);
6997}
6998
6999void ImGui::TreePushOverrideID(ImGuiID id)
7000{
7001 ImGuiContext& g = *GImGui;
7002 ImGuiWindow* window = g.CurrentWindow;
7003 Indent();
7004 window->DC.TreeDepth++;
7005 PushOverrideID(id);
7006}
7007
7008void ImGui::TreePop()
7009{
7010 ImGuiContext& g = *GImGui;
7011 ImGuiWindow* window = g.CurrentWindow;
7012 Unindent();
7013
7014 window->DC.TreeDepth--;
7015 ImU32 tree_depth_mask = (1 << window->DC.TreeDepth);
7016
7017 if (window->DC.TreeHasStackDataDepthMask & tree_depth_mask)
7018 {
7019 const ImGuiTreeNodeStackData* data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
7020 IM_ASSERT(data->ID == window->IDStack.back());
7021
7022 // Handle Left arrow to move to parent tree node (when ImGuiTreeNodeFlags_NavLeftJumpsToParent is enabled)
7023 if (data->TreeFlags & ImGuiTreeNodeFlags_NavLeftJumpsToParent)
7024 if (g.NavIdIsAlive && g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
7025 NavMoveRequestResolveWithPastTreeNode(result: &g.NavMoveResultLocal, tree_node_data: data);
7026
7027 // Draw hierarchy lines
7028 if (data->DrawLinesX1 != +FLT_MAX && window->DC.CursorPos.y >= window->ClipRect.Min.y)
7029 TreeNodeDrawLineToTreePop(data);
7030
7031 g.TreeNodeStack.pop_back();
7032 window->DC.TreeHasStackDataDepthMask &= ~tree_depth_mask;
7033 window->DC.TreeRecordsClippedNodesY2Mask &= ~tree_depth_mask;
7034 }
7035
7036 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.
7037 PopID();
7038}
7039
7040// Horizontal distance preceding label when using TreeNode() or Bullet()
7041float ImGui::GetTreeNodeToLabelSpacing()
7042{
7043 ImGuiContext& g = *GImGui;
7044 return g.FontSize + (g.Style.FramePadding.x * 2.0f);
7045}
7046
7047// Set next TreeNode/CollapsingHeader open state.
7048void ImGui::SetNextItemOpen(bool is_open, ImGuiCond cond)
7049{
7050 ImGuiContext& g = *GImGui;
7051 if (g.CurrentWindow->SkipItems)
7052 return;
7053 g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasOpen;
7054 g.NextItemData.OpenVal = is_open;
7055 g.NextItemData.OpenCond = (ImU8)(cond ? cond : ImGuiCond_Always);
7056}
7057
7058// Set next TreeNode/CollapsingHeader storage id.
7059void ImGui::SetNextItemStorageID(ImGuiID storage_id)
7060{
7061 ImGuiContext& g = *GImGui;
7062 if (g.CurrentWindow->SkipItems)
7063 return;
7064 g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasStorageID;
7065 g.NextItemData.StorageId = storage_id;
7066}
7067
7068// CollapsingHeader returns true when opened but do not indent nor push into the ID stack (because of the ImGuiTreeNodeFlags_NoTreePushOnOpen flag).
7069// 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().
7070bool ImGui::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags)
7071{
7072 ImGuiWindow* window = GetCurrentWindow();
7073 if (window->SkipItems)
7074 return false;
7075 ImGuiID id = window->GetID(str: label);
7076 return TreeNodeBehavior(id, flags: flags | ImGuiTreeNodeFlags_CollapsingHeader, label);
7077}
7078
7079// p_visible == NULL : regular collapsing header
7080// 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
7081// p_visible != NULL && *p_visible == false : do not show the header at all
7082// Do not mistake this with the Open state of the header itself, which you can adjust with SetNextItemOpen() or ImGuiTreeNodeFlags_DefaultOpen.
7083bool ImGui::CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags)
7084{
7085 ImGuiWindow* window = GetCurrentWindow();
7086 if (window->SkipItems)
7087 return false;
7088
7089 if (p_visible && !*p_visible)
7090 return false;
7091
7092 ImGuiID id = window->GetID(str: label);
7093 flags |= ImGuiTreeNodeFlags_CollapsingHeader;
7094 if (p_visible)
7095 flags |= ImGuiTreeNodeFlags_AllowOverlap | (ImGuiTreeNodeFlags)ImGuiTreeNodeFlags_ClipLabelForTrailingButton;
7096 bool is_open = TreeNodeBehavior(id, flags, label);
7097 if (p_visible != NULL)
7098 {
7099 // Create a small overlapping close button
7100 // FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc.
7101 // FIXME: CloseButton can overlap into text, need find a way to clip the text somehow.
7102 ImGuiContext& g = *GImGui;
7103 ImGuiLastItemData last_item_backup = g.LastItemData;
7104 float button_size = g.FontSize;
7105 float button_x = ImMax(lhs: g.LastItemData.Rect.Min.x, rhs: g.LastItemData.Rect.Max.x - g.Style.FramePadding.x - button_size);
7106 float button_y = g.LastItemData.Rect.Min.y + g.Style.FramePadding.y;
7107 ImGuiID close_button_id = GetIDWithSeed(str_id_begin: "#CLOSE", NULL, seed: id);
7108 if (CloseButton(id: close_button_id, pos: ImVec2(button_x, button_y)))
7109 *p_visible = false;
7110 g.LastItemData = last_item_backup;
7111 }
7112
7113 return is_open;
7114}
7115
7116//-------------------------------------------------------------------------
7117// [SECTION] Widgets: Selectable
7118//-------------------------------------------------------------------------
7119// - Selectable()
7120//-------------------------------------------------------------------------
7121
7122// Tip: pass a non-visible label (e.g. "##hello") then you can use the space to draw other text or image.
7123// But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID or use ##unique_id.
7124// With this scheme, ImGuiSelectableFlags_SpanAllColumns and ImGuiSelectableFlags_AllowOverlap are also frequently used flags.
7125// FIXME: Selectable() with (size.x == 0.0f) and (SelectableTextAlign.x > 0.0f) followed by SameLine() is currently not supported.
7126bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
7127{
7128 ImGuiWindow* window = GetCurrentWindow();
7129 if (window->SkipItems)
7130 return false;
7131
7132 ImGuiContext& g = *GImGui;
7133 const ImGuiStyle& style = g.Style;
7134
7135 // Submit label or explicit size to ItemSize(), whereas ItemAdd() will submit a larger/spanning rectangle.
7136 ImGuiID id = window->GetID(str: label);
7137 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
7138 ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y);
7139 ImVec2 pos = window->DC.CursorPos;
7140 pos.y += window->DC.CurrLineTextBaseOffset;
7141 ItemSize(size, text_baseline_y: 0.0f);
7142
7143 // Fill horizontal space
7144 // We don't support (size < 0.0f) in Selectable() because the ItemSpacing extension would make explicitly right-aligned sizes not visibly match other widgets.
7145 const bool span_all_columns = (flags & ImGuiSelectableFlags_SpanAllColumns) != 0;
7146 const float min_x = span_all_columns ? window->ParentWorkRect.Min.x : pos.x;
7147 const float max_x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x;
7148 if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_SpanAvailWidth))
7149 size.x = ImMax(lhs: label_size.x, rhs: max_x - min_x);
7150
7151 // Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable.
7152 // FIXME: Not part of layout so not included in clipper calculation, but ItemSize currently doesn't allow offsetting CursorPos.
7153 ImRect bb(min_x, pos.y, min_x + size.x, pos.y + size.y);
7154 if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0)
7155 {
7156 const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x;
7157 const float spacing_y = style.ItemSpacing.y;
7158 const float spacing_L = IM_TRUNC(spacing_x * 0.50f);
7159 const float spacing_U = IM_TRUNC(spacing_y * 0.50f);
7160 bb.Min.x -= spacing_L;
7161 bb.Min.y -= spacing_U;
7162 bb.Max.x += (spacing_x - spacing_L);
7163 bb.Max.y += (spacing_y - spacing_U);
7164 }
7165 //if (g.IO.KeyCtrl) { GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(0, 255, 0, 255)); }
7166
7167 const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0;
7168 const ImGuiItemFlags extra_item_flags = disabled_item ? (ImGuiItemFlags)ImGuiItemFlags_Disabled : ImGuiItemFlags_None;
7169 bool is_visible;
7170 if (span_all_columns)
7171 {
7172 // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable..
7173 const float backup_clip_rect_min_x = window->ClipRect.Min.x;
7174 const float backup_clip_rect_max_x = window->ClipRect.Max.x;
7175 window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
7176 window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
7177 is_visible = ItemAdd(bb, id, NULL, extra_flags: extra_item_flags);
7178 window->ClipRect.Min.x = backup_clip_rect_min_x;
7179 window->ClipRect.Max.x = backup_clip_rect_max_x;
7180 }
7181 else
7182 {
7183 is_visible = ItemAdd(bb, id, NULL, extra_flags: extra_item_flags);
7184 }
7185
7186 const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
7187 if (!is_visible)
7188 if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(r: bb)) // Extra layer of "no logic clip" for box-select support (would be more overhead to add to ItemAdd)
7189 return false;
7190
7191 const bool disabled_global = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0;
7192 if (disabled_item && !disabled_global) // Only testing this as an optimization
7193 BeginDisabled();
7194
7195 // FIXME: We can standardize the behavior of those two, we could also keep the fast path of override ClipRect + full push on render only,
7196 // which would be advantageous since most selectable are not selected.
7197 if (span_all_columns)
7198 {
7199 if (g.CurrentTable)
7200 TablePushBackgroundChannel();
7201 else if (window->DC.CurrentColumns)
7202 PushColumnsBackground();
7203 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect;
7204 g.LastItemData.ClipRect = window->ClipRect;
7205 }
7206
7207 // We use NoHoldingActiveID on menus so user can click and _hold_ on a menu then drag to browse child entries
7208 ImGuiButtonFlags button_flags = 0;
7209 if (flags & ImGuiSelectableFlags_NoHoldingActiveID) { button_flags |= ImGuiButtonFlags_NoHoldingActiveId; }
7210 if (flags & ImGuiSelectableFlags_NoSetKeyOwner) { button_flags |= ImGuiButtonFlags_NoSetKeyOwner; }
7211 if (flags & ImGuiSelectableFlags_SelectOnClick) { button_flags |= ImGuiButtonFlags_PressedOnClick; }
7212 if (flags & ImGuiSelectableFlags_SelectOnRelease) { button_flags |= ImGuiButtonFlags_PressedOnRelease; }
7213 if (flags & ImGuiSelectableFlags_AllowDoubleClick) { button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; }
7214 if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap)) { button_flags |= ImGuiButtonFlags_AllowOverlap; }
7215
7216 // Multi-selection support (header)
7217 const bool was_selected = selected;
7218 if (is_multi_select)
7219 {
7220 // Handle multi-select + alter button flags for it
7221 MultiSelectItemHeader(id, p_selected: &selected, p_button_flags: &button_flags);
7222 }
7223
7224 bool hovered, held;
7225 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags: button_flags);
7226
7227 // Multi-selection support (footer)
7228 if (is_multi_select)
7229 {
7230 MultiSelectItemFooter(id, p_selected: &selected, p_pressed: &pressed);
7231 }
7232 else
7233 {
7234 // Auto-select when moved into
7235 // - This will be more fully fleshed in the range-select branch
7236 // - This is not exposed as it won't nicely work with some user side handling of shift/control
7237 // - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons
7238 // - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope())
7239 // - (2) usage will fail with clipped items
7240 // The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API.
7241 if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId)
7242 if (g.NavJustMovedToId == id)
7243 selected = pressed = true;
7244 }
7245
7246 // Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with keyboard/gamepad
7247 if (pressed || (hovered && (flags & ImGuiSelectableFlags_SetNavIdOnHover)))
7248 {
7249 if (!g.NavHighlightItemUnderNav && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent)
7250 {
7251 SetNavID(id, nav_layer: window->DC.NavLayerCurrent, focus_scope_id: g.CurrentFocusScopeId, rect_rel: WindowRectAbsToRel(window, r: bb)); // (bb == NavRect)
7252 if (g.IO.ConfigNavCursorVisibleAuto)
7253 g.NavCursorVisible = false;
7254 }
7255 }
7256 if (pressed)
7257 MarkItemEdited(id);
7258
7259 if (selected != was_selected)
7260 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
7261
7262 // Render
7263 if (is_visible)
7264 {
7265 const bool highlighted = hovered || (flags & ImGuiSelectableFlags_Highlight);
7266 if (highlighted || selected)
7267 {
7268 // Between 1.91.0 and 1.91.4 we made selected Selectable use an arbitrary lerp between _Header and _HeaderHovered. Removed that now. (#8106)
7269 ImU32 col = GetColorU32(idx: (held && highlighted) ? ImGuiCol_HeaderActive : highlighted ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
7270 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: col, borders: false, rounding: 0.0f);
7271 }
7272 if (g.NavId == id)
7273 {
7274 ImGuiNavRenderCursorFlags nav_render_cursor_flags = ImGuiNavRenderCursorFlags_Compact | ImGuiNavRenderCursorFlags_NoRounding;
7275 if (is_multi_select)
7276 nav_render_cursor_flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle
7277 RenderNavCursor(bb, id, flags: nav_render_cursor_flags);
7278 }
7279 }
7280
7281 if (span_all_columns)
7282 {
7283 if (g.CurrentTable)
7284 TablePopBackgroundChannel();
7285 else if (window->DC.CurrentColumns)
7286 PopColumnsBackground();
7287 }
7288
7289 // Text stays at the submission position. Alignment/clipping extents ignore SpanAllColumns.
7290 if (is_visible)
7291 RenderTextClipped(pos_min: pos, pos_max: ImVec2(ImMin(lhs: pos.x + size.x, rhs: window->WorkRect.Max.x), pos.y + size.y), text: label, NULL, text_size_if_known: &label_size, align: style.SelectableTextAlign, clip_rect: &bb);
7292
7293 // Automatically close popups
7294 if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_NoAutoClosePopups) && (g.LastItemData.ItemFlags & ImGuiItemFlags_AutoClosePopups))
7295 CloseCurrentPopup();
7296
7297 if (disabled_item && !disabled_global)
7298 EndDisabled();
7299
7300 // Selectable() always returns a pressed state!
7301 // Users of BeginMultiSelect()/EndMultiSelect() scope: you may call ImGui::IsItemToggledSelection() to retrieve
7302 // selection toggle, only useful if you need that state updated (e.g. for rendering purpose) before reaching EndMultiSelect().
7303 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
7304 return pressed; //-V1020
7305}
7306
7307bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
7308{
7309 if (Selectable(label, selected: *p_selected, flags, size_arg))
7310 {
7311 *p_selected = !*p_selected;
7312 return true;
7313 }
7314 return false;
7315}
7316
7317
7318//-------------------------------------------------------------------------
7319// [SECTION] Widgets: Typing-Select support
7320//-------------------------------------------------------------------------
7321
7322// [Experimental] Currently not exposed in public API.
7323// Consume character inputs and return search request, if any.
7324// This would typically only be called on the focused window or location you want to grab inputs for, e.g.
7325// if (ImGui::IsWindowFocused(...))
7326// if (ImGuiTypingSelectRequest* req = ImGui::GetTypingSelectRequest())
7327// focus_idx = ImGui::TypingSelectFindMatch(req, my_items.size(), [](void*, int n) { return my_items[n]->Name; }, &my_items, -1);
7328// However the code is written in a way where calling it from multiple locations is safe (e.g. to obtain buffer).
7329ImGuiTypingSelectRequest* ImGui::GetTypingSelectRequest(ImGuiTypingSelectFlags flags)
7330{
7331 ImGuiContext& g = *GImGui;
7332 ImGuiTypingSelectState* data = &g.TypingSelectState;
7333 ImGuiTypingSelectRequest* out_request = &data->Request;
7334
7335 // Clear buffer
7336 const float TYPING_SELECT_RESET_TIMER = 1.80f; // FIXME: Potentially move to IO config.
7337 const int TYPING_SELECT_SINGLE_CHAR_COUNT_FOR_LOCK = 4; // Lock single char matching when repeating same char 4 times
7338 if (data->SearchBuffer[0] != 0)
7339 {
7340 bool clear_buffer = false;
7341 clear_buffer |= (g.NavFocusScopeId != data->FocusScope);
7342 clear_buffer |= (data->LastRequestTime + TYPING_SELECT_RESET_TIMER < g.Time);
7343 clear_buffer |= g.NavAnyRequest;
7344 clear_buffer |= g.ActiveId != 0 && g.NavActivateId == 0; // Allow temporary SPACE activation to not interfere
7345 clear_buffer |= IsKeyPressed(key: ImGuiKey_Escape) || IsKeyPressed(key: ImGuiKey_Enter);
7346 clear_buffer |= IsKeyPressed(key: ImGuiKey_Backspace) && (flags & ImGuiTypingSelectFlags_AllowBackspace) == 0;
7347 //if (clear_buffer) { IMGUI_DEBUG_LOG("GetTypingSelectRequest(): Clear SearchBuffer.\n"); }
7348 if (clear_buffer)
7349 data->Clear();
7350 }
7351
7352 // Append to buffer
7353 const int buffer_max_len = IM_ARRAYSIZE(data->SearchBuffer) - 1;
7354 int buffer_len = (int)ImStrlen(s: data->SearchBuffer);
7355 bool select_request = false;
7356 for (ImWchar w : g.IO.InputQueueCharacters)
7357 {
7358 const int w_len = ImTextCountUtf8BytesFromStr(in_text: &w, in_text_end: &w + 1);
7359 if (w < 32 || (buffer_len == 0 && ImCharIsBlankW(c: w)) || (buffer_len + w_len > buffer_max_len)) // Ignore leading blanks
7360 continue;
7361 char w_buf[5];
7362 ImTextCharToUtf8(out_buf: w_buf, c: (unsigned int)w);
7363 if (data->SingleCharModeLock && w_len == out_request->SingleCharSize && memcmp(s1: w_buf, s2: data->SearchBuffer, n: w_len) == 0)
7364 {
7365 select_request = true; // Same character: don't need to append to buffer.
7366 continue;
7367 }
7368 if (data->SingleCharModeLock)
7369 {
7370 data->Clear(); // Different character: clear
7371 buffer_len = 0;
7372 }
7373 memcpy(dest: data->SearchBuffer + buffer_len, src: w_buf, n: w_len + 1); // Append
7374 buffer_len += w_len;
7375 select_request = true;
7376 }
7377 g.IO.InputQueueCharacters.resize(new_size: 0);
7378
7379 // Handle backspace
7380 if ((flags & ImGuiTypingSelectFlags_AllowBackspace) && IsKeyPressed(key: ImGuiKey_Backspace, flags: ImGuiInputFlags_Repeat))
7381 {
7382 char* p = (char*)(void*)ImTextFindPreviousUtf8Codepoint(in_text_start: data->SearchBuffer, in_text_curr: data->SearchBuffer + buffer_len);
7383 *p = 0;
7384 buffer_len = (int)(p - data->SearchBuffer);
7385 }
7386
7387 // Return request if any
7388 if (buffer_len == 0)
7389 return NULL;
7390 if (select_request)
7391 {
7392 data->FocusScope = g.NavFocusScopeId;
7393 data->LastRequestFrame = g.FrameCount;
7394 data->LastRequestTime = (float)g.Time;
7395 }
7396 out_request->Flags = flags;
7397 out_request->SearchBufferLen = buffer_len;
7398 out_request->SearchBuffer = data->SearchBuffer;
7399 out_request->SelectRequest = (data->LastRequestFrame == g.FrameCount);
7400 out_request->SingleCharMode = false;
7401 out_request->SingleCharSize = 0;
7402
7403 // Calculate if buffer contains the same character repeated.
7404 // - This can be used to implement a special search mode on first character.
7405 // - Performed on UTF-8 codepoint for correctness.
7406 // - SingleCharMode is always set for first input character, because it usually leads to a "next".
7407 if (flags & ImGuiTypingSelectFlags_AllowSingleCharMode)
7408 {
7409 const char* buf_begin = out_request->SearchBuffer;
7410 const char* buf_end = out_request->SearchBuffer + out_request->SearchBufferLen;
7411 const int c0_len = ImTextCountUtf8BytesFromChar(in_text: buf_begin, in_text_end: buf_end);
7412 const char* p = buf_begin + c0_len;
7413 for (; p < buf_end; p += c0_len)
7414 if (memcmp(s1: buf_begin, s2: p, n: (size_t)c0_len) != 0)
7415 break;
7416 const int single_char_count = (p == buf_end) ? (out_request->SearchBufferLen / c0_len) : 0;
7417 out_request->SingleCharMode = (single_char_count > 0 || data->SingleCharModeLock);
7418 out_request->SingleCharSize = (ImS8)c0_len;
7419 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.
7420 }
7421
7422 return out_request;
7423}
7424
7425static int ImStrimatchlen(const char* s1, const char* s1_end, const char* s2)
7426{
7427 int match_len = 0;
7428 while (s1 < s1_end && ImToUpper(c: *s1++) == ImToUpper(c: *s2++))
7429 match_len++;
7430 return match_len;
7431}
7432
7433// Default handler for finding a result for typing-select. You may implement your own.
7434// You might want to display a tooltip to visualize the current request SearchBuffer
7435// When SingleCharMode is set:
7436// - it is better to NOT display a tooltip of other on-screen display indicator.
7437// - the index of the currently focused item is required.
7438// if your SetNextItemSelectionUserData() values are indices, you can obtain it from ImGuiMultiSelectIO::NavIdItem, otherwise from g.NavLastValidSelectionUserData.
7439int ImGui::TypingSelectFindMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx)
7440{
7441 if (req == NULL || req->SelectRequest == false) // Support NULL parameter so both calls can be done from same spot.
7442 return -1;
7443 int idx = -1;
7444 if (req->SingleCharMode && (req->Flags & ImGuiTypingSelectFlags_AllowSingleCharMode))
7445 idx = TypingSelectFindNextSingleCharMatch(req, items_count, get_item_name_func, user_data, nav_item_idx);
7446 else
7447 idx = TypingSelectFindBestLeadingMatch(req, items_count, get_item_name_func, user_data);
7448 if (idx != -1)
7449 SetNavCursorVisibleAfterMove();
7450 return idx;
7451}
7452
7453// Special handling when a single character is repeated: perform search on a single letter and goes to next.
7454int ImGui::TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx)
7455{
7456 // FIXME: Assume selection user data is index. Would be extremely practical.
7457 //if (nav_item_idx == -1)
7458 // nav_item_idx = (int)g.NavLastValidSelectionUserData;
7459
7460 int first_match_idx = -1;
7461 bool return_next_match = false;
7462 for (int idx = 0; idx < items_count; idx++)
7463 {
7464 const char* item_name = get_item_name_func(user_data, idx);
7465 if (ImStrimatchlen(s1: req->SearchBuffer, s1_end: req->SearchBuffer + req->SingleCharSize, s2: item_name) < req->SingleCharSize)
7466 continue;
7467 if (return_next_match) // Return next matching item after current item.
7468 return idx;
7469 if (first_match_idx == -1 && nav_item_idx == -1) // Return first match immediately if we don't have a nav_item_idx value.
7470 return idx;
7471 if (first_match_idx == -1) // Record first match for wrapping.
7472 first_match_idx = idx;
7473 if (nav_item_idx == idx) // Record that we encountering nav_item so we can return next match.
7474 return_next_match = true;
7475 }
7476 return first_match_idx; // First result
7477}
7478
7479int ImGui::TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data)
7480{
7481 int longest_match_idx = -1;
7482 int longest_match_len = 0;
7483 for (int idx = 0; idx < items_count; idx++)
7484 {
7485 const char* item_name = get_item_name_func(user_data, idx);
7486 const int match_len = ImStrimatchlen(s1: req->SearchBuffer, s1_end: req->SearchBuffer + req->SearchBufferLen, s2: item_name);
7487 if (match_len <= longest_match_len)
7488 continue;
7489 longest_match_idx = idx;
7490 longest_match_len = match_len;
7491 if (match_len == req->SearchBufferLen)
7492 break;
7493 }
7494 return longest_match_idx;
7495}
7496
7497void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data)
7498{
7499#ifndef IMGUI_DISABLE_DEBUG_TOOLS
7500 Text(fmt: "SearchBuffer = \"%s\"", data->SearchBuffer);
7501 Text(fmt: "SingleCharMode = %d, Size = %d, Lock = %d", data->Request.SingleCharMode, data->Request.SingleCharSize, data->SingleCharModeLock);
7502 Text(fmt: "LastRequest = time: %.2f, frame: %d", data->LastRequestTime, data->LastRequestFrame);
7503#else
7504 IM_UNUSED(data);
7505#endif
7506}
7507
7508//-------------------------------------------------------------------------
7509// [SECTION] Widgets: Box-Select support
7510// This has been extracted away from Multi-Select logic in the hope that it could eventually be used elsewhere, but hasn't been yet.
7511//-------------------------------------------------------------------------
7512// Extra logic in MultiSelectItemFooter() and ImGuiListClipper::Step()
7513//-------------------------------------------------------------------------
7514// - BoxSelectPreStartDrag() [Internal]
7515// - BoxSelectActivateDrag() [Internal]
7516// - BoxSelectDeactivateDrag() [Internal]
7517// - BoxSelectScrollWithMouseDrag() [Internal]
7518// - BeginBoxSelect() [Internal]
7519// - EndBoxSelect() [Internal]
7520//-------------------------------------------------------------------------
7521
7522// Call on the initial click.
7523static void BoxSelectPreStartDrag(ImGuiID id, ImGuiSelectionUserData clicked_item)
7524{
7525 ImGuiContext& g = *GImGui;
7526 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7527 bs->ID = id;
7528 bs->IsStarting = true; // Consider starting box-select.
7529 bs->IsStartedFromVoid = (clicked_item == ImGuiSelectionUserData_Invalid);
7530 bs->IsStartedSetNavIdOnce = bs->IsStartedFromVoid;
7531 bs->KeyMods = g.IO.KeyMods;
7532 bs->StartPosRel = bs->EndPosRel = ImGui::WindowPosAbsToRel(window: g.CurrentWindow, p: g.IO.MousePos);
7533 bs->ScrollAccum = ImVec2(0.0f, 0.0f);
7534}
7535
7536static void BoxSelectActivateDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window)
7537{
7538 ImGuiContext& g = *GImGui;
7539 IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Activate\n", bs->ID);
7540 bs->IsActive = true;
7541 bs->Window = window;
7542 bs->IsStarting = false;
7543 ImGui::SetActiveID(id: bs->ID, window);
7544 ImGui::SetActiveIdUsingAllKeyboardKeys();
7545 if (bs->IsStartedFromVoid && (bs->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0)
7546 bs->RequestClear = true;
7547}
7548
7549static void BoxSelectDeactivateDrag(ImGuiBoxSelectState* bs)
7550{
7551 ImGuiContext& g = *GImGui;
7552 bs->IsActive = bs->IsStarting = false;
7553 if (g.ActiveId == bs->ID)
7554 {
7555 IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Deactivate\n", bs->ID);
7556 ImGui::ClearActiveID();
7557 }
7558 bs->ID = 0;
7559}
7560
7561static void BoxSelectScrollWithMouseDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window, const ImRect& inner_r)
7562{
7563 ImGuiContext& g = *GImGui;
7564 IM_ASSERT(bs->Window == window);
7565 for (int n = 0; n < 2; n++) // each axis
7566 {
7567 const float mouse_pos = g.IO.MousePos[n];
7568 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;
7569 const float scroll_curr = window->Scroll[n];
7570 if (dist == 0.0f || (dist < 0.0f && scroll_curr < 0.0f) || (dist > 0.0f && scroll_curr >= window->ScrollMax[n]))
7571 continue;
7572
7573 const float speed_multiplier = ImLinearRemapClamp(s0: g.FontSize, s1: g.FontSize * 5.0f, d0: 1.0f, d1: 4.0f, x: ImAbs(x: dist)); // x1 to x4 depending on distance
7574 const float scroll_step = g.FontSize * 35.0f * speed_multiplier * ImSign(x: dist) * g.IO.DeltaTime;
7575 bs->ScrollAccum[n] += scroll_step;
7576
7577 // Accumulate into a stored value so we can handle high-framerate
7578 const float scroll_step_i = ImFloor(f: bs->ScrollAccum[n]);
7579 if (scroll_step_i == 0.0f)
7580 continue;
7581 if (n == 0)
7582 ImGui::SetScrollX(window, scroll_x: scroll_curr + scroll_step_i);
7583 else
7584 ImGui::SetScrollY(window, scroll_y: scroll_curr + scroll_step_i);
7585 bs->ScrollAccum[n] -= scroll_step_i;
7586 }
7587}
7588
7589bool ImGui::BeginBoxSelect(const ImRect& scope_rect, ImGuiWindow* window, ImGuiID box_select_id, ImGuiMultiSelectFlags ms_flags)
7590{
7591 ImGuiContext& g = *GImGui;
7592 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7593 KeepAliveID(id: box_select_id);
7594 if (bs->ID != box_select_id)
7595 return false;
7596
7597 // IsStarting is set by MultiSelectItemFooter() when considering a possible box-select. We validate it here and lock geometry.
7598 bs->UnclipMode = false;
7599 bs->RequestClear = false;
7600 if (bs->IsStarting && IsMouseDragPastThreshold(button: 0))
7601 BoxSelectActivateDrag(bs, window);
7602 else if ((bs->IsStarting || bs->IsActive) && g.IO.MouseDown[0] == false)
7603 BoxSelectDeactivateDrag(bs);
7604 if (!bs->IsActive)
7605 return false;
7606
7607 // Current frame absolute prev/current rectangles are used to toggle selection.
7608 // They are derived from positions relative to scrolling space.
7609 ImVec2 start_pos_abs = WindowPosRelToAbs(window, p: bs->StartPosRel);
7610 ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, p: bs->EndPosRel); // Clamped already
7611 ImVec2 curr_end_pos_abs = g.IO.MousePos;
7612 if (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) // Box-select scrolling only happens with ScopeWindow
7613 curr_end_pos_abs = ImClamp(v: curr_end_pos_abs, mn: scope_rect.Min, mx: scope_rect.Max);
7614 bs->BoxSelectRectPrev.Min = ImMin(lhs: start_pos_abs, rhs: prev_end_pos_abs);
7615 bs->BoxSelectRectPrev.Max = ImMax(lhs: start_pos_abs, rhs: prev_end_pos_abs);
7616 bs->BoxSelectRectCurr.Min = ImMin(lhs: start_pos_abs, rhs: curr_end_pos_abs);
7617 bs->BoxSelectRectCurr.Max = ImMax(lhs: start_pos_abs, rhs: curr_end_pos_abs);
7618
7619 // Box-select 2D mode detects horizontal changes (vertical ones are already picked by Clipper)
7620 // Storing an extra rect used by widgets supporting box-select.
7621 if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d)
7622 if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x)
7623 {
7624 bs->UnclipMode = true;
7625 bs->UnclipRect = bs->BoxSelectRectPrev; // FIXME-OPT: UnclipRect x coordinates could be intersection of Prev and Curr rect on X axis.
7626 bs->UnclipRect.Add(r: bs->BoxSelectRectCurr);
7627 }
7628
7629 //GetForegroundDrawList()->AddRect(bs->UnclipRect.Min, bs->UnclipRect.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f);
7630 //GetForegroundDrawList()->AddRect(bs->BoxSelectRectPrev.Min, bs->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f);
7631 //GetForegroundDrawList()->AddRect(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f);
7632 return true;
7633}
7634
7635void ImGui::EndBoxSelect(const ImRect& scope_rect, ImGuiMultiSelectFlags ms_flags)
7636{
7637 ImGuiContext& g = *GImGui;
7638 ImGuiWindow* window = g.CurrentWindow;
7639 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7640 IM_ASSERT(bs->IsActive);
7641 bs->UnclipMode = false;
7642
7643 // Render selection rectangle
7644 bs->EndPosRel = WindowPosAbsToRel(window, p: ImClamp(v: g.IO.MousePos, mn: scope_rect.Min, mx: scope_rect.Max)); // Clamp stored position according to current scrolling view
7645 ImRect box_select_r = bs->BoxSelectRectCurr;
7646 box_select_r.ClipWith(r: scope_rect);
7647 window->DrawList->AddRectFilled(p_min: box_select_r.Min, p_max: box_select_r.Max, col: GetColorU32(idx: ImGuiCol_SeparatorHovered, alpha_mul: 0.30f)); // FIXME-MULTISELECT: Styling
7648 window->DrawList->AddRect(p_min: box_select_r.Min, p_max: box_select_r.Max, col: GetColorU32(idx: ImGuiCol_NavCursor)); // FIXME-MULTISELECT FIXME-DPI: Styling
7649
7650 // Scroll
7651 const bool enable_scroll = (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms_flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0;
7652 if (enable_scroll)
7653 {
7654 ImRect scroll_r = scope_rect;
7655 scroll_r.Expand(amount: -g.FontSize);
7656 //GetForegroundDrawList()->AddRect(scroll_r.Min, scroll_r.Max, IM_COL32(0, 255, 0, 255));
7657 if (!scroll_r.Contains(p: g.IO.MousePos))
7658 BoxSelectScrollWithMouseDrag(bs, window, inner_r: scroll_r);
7659 }
7660}
7661
7662//-------------------------------------------------------------------------
7663// [SECTION] Widgets: Multi-Select support
7664//-------------------------------------------------------------------------
7665// - DebugLogMultiSelectRequests() [Internal]
7666// - CalcScopeRect() [Internal]
7667// - BeginMultiSelect()
7668// - EndMultiSelect()
7669// - SetNextItemSelectionUserData()
7670// - MultiSelectItemHeader() [Internal]
7671// - MultiSelectItemFooter() [Internal]
7672// - DebugNodeMultiSelectState() [Internal]
7673//-------------------------------------------------------------------------
7674
7675static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* io)
7676{
7677 ImGuiContext& g = *GImGui;
7678 IM_UNUSED(function);
7679 for (const ImGuiSelectionRequest& req : io->Requests)
7680 {
7681 if (req.Type == ImGuiSelectionRequestType_SetAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetAll %d (= %s)\n", function, req.Selected, req.Selected ? "SelectAll" : "Clear");
7682 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);
7683 }
7684}
7685
7686static ImRect CalcScopeRect(ImGuiMultiSelectTempData* ms, ImGuiWindow* window)
7687{
7688 ImGuiContext& g = *GImGui;
7689 if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)
7690 {
7691 // Warning: this depends on CursorMaxPos so it means to be called by EndMultiSelect() only
7692 return ImRect(ms->ScopeRectMin, ImMax(lhs: window->DC.CursorMaxPos, rhs: ms->ScopeRectMin));
7693 }
7694 else
7695 {
7696 // When a table, pull HostClipRect, which allows us to predict ClipRect before first row/layout is performed. (#7970)
7697 ImRect scope_rect = window->InnerClipRect;
7698 if (g.CurrentTable != NULL)
7699 scope_rect = g.CurrentTable->HostClipRect;
7700
7701 // Add inner table decoration (#7821) // FIXME: Why not baking in InnerClipRect?
7702 scope_rect.Min = ImMin(lhs: scope_rect.Min + ImVec2(window->DecoInnerSizeX1, window->DecoInnerSizeY1), rhs: scope_rect.Max);
7703 return scope_rect;
7704 }
7705}
7706
7707// Return ImGuiMultiSelectIO structure.
7708// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect().
7709// Passing 'selection_size' and 'items_count' parameters is currently optional.
7710// - 'selection_size' is useful to disable some shortcut routing: e.g. ImGuiMultiSelectFlags_ClearOnEscape won't claim Escape key when selection_size 0,
7711// allowing a first press to clear selection THEN the second press to leave child window and return to parent.
7712// - '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).
7713// - If they are costly for you to compute (e.g. external intrusive selection without maintaining size), you may avoid them and pass -1.
7714// - 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.
7715ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int selection_size, int items_count)
7716{
7717 ImGuiContext& g = *GImGui;
7718 ImGuiWindow* window = g.CurrentWindow;
7719
7720 if (++g.MultiSelectTempDataStacked > g.MultiSelectTempData.Size)
7721 g.MultiSelectTempData.resize(new_size: g.MultiSelectTempDataStacked, v: ImGuiMultiSelectTempData());
7722 ImGuiMultiSelectTempData* ms = &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1];
7723 IM_STATIC_ASSERT(offsetof(ImGuiMultiSelectTempData, IO) == 0); // Clear() relies on that.
7724 g.CurrentMultiSelect = ms;
7725 if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0)
7726 flags |= ImGuiMultiSelectFlags_ScopeWindow;
7727 if (flags & ImGuiMultiSelectFlags_SingleSelect)
7728 flags &= ~(ImGuiMultiSelectFlags_BoxSelect2d | ImGuiMultiSelectFlags_BoxSelect1d);
7729 if (flags & ImGuiMultiSelectFlags_BoxSelect2d)
7730 flags &= ~ImGuiMultiSelectFlags_BoxSelect1d;
7731
7732 // FIXME: Workaround to the fact we override CursorMaxPos, meaning size measurement are lost. (#8250)
7733 // They should perhaps be stacked properly?
7734 if (ImGuiTable* table = g.CurrentTable)
7735 if (table->CurrentColumn != -1)
7736 TableEndCell(table); // This is currently safe to call multiple time. If that properly is lost we can extract the "save measurement" part of it.
7737
7738 // FIXME: BeginFocusScope()
7739 const ImGuiID id = window->IDStack.back();
7740 ms->Clear();
7741 ms->FocusScopeId = id;
7742 ms->Flags = flags;
7743 ms->IsFocused = (ms->FocusScopeId == g.NavFocusScopeId);
7744 ms->BackupCursorMaxPos = window->DC.CursorMaxPos;
7745 ms->ScopeRectMin = window->DC.CursorMaxPos = window->DC.CursorPos;
7746 PushFocusScope(id: ms->FocusScopeId);
7747 if (flags & ImGuiMultiSelectFlags_ScopeWindow) // Mark parent child window as navigable into, with highlight. Assume user will always submit interactive items.
7748 window->DC.NavLayersActiveMask |= 1 << ImGuiNavLayer_Main;
7749
7750 // Use copy of keyboard mods at the time of the request, otherwise we would requires mods to be held for an extra frame.
7751 ms->KeyMods = g.NavJustMovedToId ? (g.NavJustMovedToIsTabbing ? 0 : g.NavJustMovedToKeyMods) : g.IO.KeyMods;
7752 if (flags & ImGuiMultiSelectFlags_NoRangeSelect)
7753 ms->KeyMods &= ~ImGuiMod_Shift;
7754
7755 // Bind storage
7756 ImGuiMultiSelectState* storage = g.MultiSelectStorage.GetOrAddByKey(key: id);
7757 storage->ID = id;
7758 storage->LastFrameActive = g.FrameCount;
7759 storage->LastSelectionSize = selection_size;
7760 storage->Window = window;
7761 ms->Storage = storage;
7762
7763 // Output to user
7764 ms->IO.Requests.resize(new_size: 0);
7765 ms->IO.RangeSrcItem = storage->RangeSrcItem;
7766 ms->IO.NavIdItem = storage->NavIdItem;
7767 ms->IO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false;
7768 ms->IO.ItemsCount = items_count;
7769
7770 // Clear when using Navigation to move within the scope
7771 // (we compare FocusScopeId so it possible to use multiple selections inside a same window)
7772 bool request_clear = false;
7773 bool request_select_all = false;
7774 if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData)
7775 {
7776 if (ms->KeyMods & ImGuiMod_Shift)
7777 ms->IsKeyboardSetRange = true;
7778 if (ms->IsKeyboardSetRange)
7779 IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid); // Not ready -> could clear?
7780 if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0)
7781 request_clear = true;
7782 }
7783 else if (g.NavJustMovedFromFocusScopeId == ms->FocusScopeId)
7784 {
7785 // Also clear on leaving scope (may be optional?)
7786 if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0)
7787 request_clear = true;
7788 }
7789
7790 // Box-select handling: update active state.
7791 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7792 if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7793 {
7794 ms->BoxSelectId = GetID(str_id: "##BoxSelect");
7795 if (BeginBoxSelect(scope_rect: CalcScopeRect(ms, window), window, box_select_id: ms->BoxSelectId, ms_flags: flags))
7796 request_clear |= bs->RequestClear;
7797 }
7798
7799 if (ms->IsFocused)
7800 {
7801 // Shortcut: Clear selection (Escape)
7802 // - Only claim shortcut if selection is not empty, allowing further presses on Escape to e.g. leave current child window.
7803 // - Box select also handle Escape and needs to pass an id to bypass ActiveIdUsingAllKeyboardKeys lock.
7804 if (flags & ImGuiMultiSelectFlags_ClearOnEscape)
7805 {
7806 if (selection_size != 0 || bs->IsActive)
7807 if (Shortcut(key_chord: ImGuiKey_Escape, flags: ImGuiInputFlags_None, owner_id: bs->IsActive ? bs->ID : 0))
7808 {
7809 request_clear = true;
7810 if (bs->IsActive)
7811 BoxSelectDeactivateDrag(bs);
7812 }
7813 }
7814
7815 // Shortcut: Select all (CTRL+A)
7816 if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll))
7817 if (Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiKey_A))
7818 request_select_all = true;
7819 }
7820
7821 if (request_clear || request_select_all)
7822 {
7823 MultiSelectAddSetAll(ms, selected: request_select_all);
7824 if (!request_select_all)
7825 storage->LastSelectionSize = 0;
7826 }
7827 ms->LoopRequestSetAll = request_select_all ? 1 : request_clear ? 0 : -1;
7828 ms->LastSubmittedItem = ImGuiSelectionUserData_Invalid;
7829
7830 if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)
7831 DebugLogMultiSelectRequests(function: "BeginMultiSelect", io: &ms->IO);
7832
7833 return &ms->IO;
7834}
7835
7836// Return updated ImGuiMultiSelectIO structure.
7837// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect().
7838ImGuiMultiSelectIO* ImGui::EndMultiSelect()
7839{
7840 ImGuiContext& g = *GImGui;
7841 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
7842 ImGuiMultiSelectState* storage = ms->Storage;
7843 ImGuiWindow* window = g.CurrentWindow;
7844 IM_ASSERT_USER_ERROR(ms->FocusScopeId == g.CurrentFocusScopeId, "EndMultiSelect() FocusScope mismatch!");
7845 IM_ASSERT(g.CurrentMultiSelect != NULL && storage->Window == g.CurrentWindow);
7846 IM_ASSERT(g.MultiSelectTempDataStacked > 0 && &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] == g.CurrentMultiSelect);
7847
7848 ImRect scope_rect = CalcScopeRect(ms, window);
7849 if (ms->IsFocused)
7850 {
7851 // We currently don't allow user code to modify RangeSrcItem by writing to BeginIO's version, but that would be an easy change here.
7852 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)
7853 {
7854 IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset RangeSrcItem.\n"); // Will set be to NavId.
7855 storage->RangeSrcItem = ImGuiSelectionUserData_Invalid;
7856 }
7857 if (ms->NavIdPassedBy == false && storage->NavIdItem != ImGuiSelectionUserData_Invalid)
7858 {
7859 IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset NavIdItem.\n");
7860 storage->NavIdItem = ImGuiSelectionUserData_Invalid;
7861 storage->NavIdSelected = -1;
7862 }
7863
7864 if ((ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) && GetBoxSelectState(id: ms->BoxSelectId))
7865 EndBoxSelect(scope_rect, ms_flags: ms->Flags);
7866 }
7867
7868 if (ms->IsEndIO == false)
7869 ms->IO.Requests.resize(new_size: 0);
7870
7871 // Clear selection when clicking void?
7872 // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection!
7873 // The InnerRect test is necessary for non-child/decorated windows.
7874 bool scope_hovered = IsWindowHovered() && window->InnerRect.Contains(p: g.IO.MousePos);
7875 if (scope_hovered && (ms->Flags & ImGuiMultiSelectFlags_ScopeRect))
7876 scope_hovered &= scope_rect.Contains(p: g.IO.MousePos);
7877 if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0)
7878 {
7879 if (ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7880 {
7881 if (!g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && g.IO.MouseClickedCount[0] == 1)
7882 {
7883 BoxSelectPreStartDrag(id: ms->BoxSelectId, ImGuiSelectionUserData_Invalid);
7884 FocusWindow(window, flags: ImGuiFocusRequestFlags_UnlessBelowModal);
7885 SetHoveredID(ms->BoxSelectId);
7886 if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)
7887 SetNavID(id: 0, nav_layer: ImGuiNavLayer_Main, focus_scope_id: ms->FocusScopeId, rect_rel: ImRect(g.IO.MousePos, g.IO.MousePos)); // Automatically switch FocusScope for initial click from void to box-select.
7888 }
7889 }
7890
7891 if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid)
7892 if (IsMouseReleased(button: 0) && IsMouseDragPastThreshold(button: 0) == false && g.IO.KeyMods == ImGuiMod_None)
7893 MultiSelectAddSetAll(ms, selected: false);
7894 }
7895
7896 // Courtesy nav wrapping helper flag
7897 if (ms->Flags & ImGuiMultiSelectFlags_NavWrapX)
7898 {
7899 IM_ASSERT(ms->Flags & ImGuiMultiSelectFlags_ScopeWindow); // Only supported at window scope
7900 ImGui::NavMoveRequestTryWrapping(window: ImGui::GetCurrentWindow(), move_flags: ImGuiNavMoveFlags_WrapX);
7901 }
7902
7903 // Unwind
7904 window->DC.CursorMaxPos = ImMax(lhs: ms->BackupCursorMaxPos, rhs: window->DC.CursorMaxPos);
7905 PopFocusScope();
7906
7907 if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)
7908 DebugLogMultiSelectRequests(function: "EndMultiSelect", io: &ms->IO);
7909
7910 ms->FocusScopeId = 0;
7911 ms->Flags = ImGuiMultiSelectFlags_None;
7912 g.CurrentMultiSelect = (--g.MultiSelectTempDataStacked > 0) ? &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] : NULL;
7913
7914 return &ms->IO;
7915}
7916
7917void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data)
7918{
7919 // Note that flags will be cleared by ItemAdd(), so it's only useful for Navigation code!
7920 // This designed so widgets can also cheaply set this before calling ItemAdd(), so we are not tied to MultiSelect api.
7921 ImGuiContext& g = *GImGui;
7922 g.NextItemData.SelectionUserData = selection_user_data;
7923 g.NextItemData.FocusScopeId = g.CurrentFocusScopeId;
7924
7925 if (ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect)
7926 {
7927 // Auto updating RangeSrcPassedBy for cases were clipper is not used (done before ItemAdd() clipping)
7928 g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData | ImGuiItemFlags_IsMultiSelect;
7929 if (ms->IO.RangeSrcItem == selection_user_data)
7930 ms->RangeSrcPassedBy = true;
7931 }
7932 else
7933 {
7934 g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData;
7935 }
7936}
7937
7938// In charge of:
7939// - Applying SetAll for submitted items.
7940// - Applying SetRange for submitted items and record end points.
7941// - Altering button behavior flags to facilitate use with drag and drop.
7942void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags)
7943{
7944 ImGuiContext& g = *GImGui;
7945 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
7946
7947 bool selected = *p_selected;
7948 if (ms->IsFocused)
7949 {
7950 ImGuiMultiSelectState* storage = ms->Storage;
7951 ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData;
7952 IM_ASSERT(g.NextItemData.FocusScopeId == g.CurrentFocusScopeId && "Forgot to call SetNextItemSelectionUserData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope");
7953
7954 // Apply SetAll (Clear/SelectAll) requests requested by BeginMultiSelect().
7955 // This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper.
7956 // If you are using a clipper you need to process the SetAll request after calling BeginMultiSelect()
7957 if (ms->LoopRequestSetAll != -1)
7958 selected = (ms->LoopRequestSetAll == 1);
7959
7960 // 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)
7961 // For this to work, we need someone to set 'RangeSrcPassedBy = true' at some point (either clipper either SetNextItemSelectionUserData() function)
7962 if (ms->IsKeyboardSetRange)
7963 {
7964 IM_ASSERT(id != 0 && (ms->KeyMods & ImGuiMod_Shift) != 0);
7965 const bool is_range_dst = (ms->RangeDstPassedBy == false) && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped.
7966 if (is_range_dst)
7967 ms->RangeDstPassedBy = true;
7968 if (is_range_dst && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) // If we don't have RangeSrc, assign RangeSrc = RangeDst
7969 {
7970 storage->RangeSrcItem = item_data;
7971 storage->RangeSelected = selected ? 1 : 0;
7972 }
7973 const bool is_range_src = storage->RangeSrcItem == item_data;
7974 if (is_range_src || is_range_dst || ms->RangeSrcPassedBy != ms->RangeDstPassedBy)
7975 {
7976 // Apply range-select value to visible items
7977 IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid && storage->RangeSelected != -1);
7978 selected = (storage->RangeSelected != 0);
7979 }
7980 else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0 && (ms->Flags & ImGuiMultiSelectFlags_NoAutoClear) == 0)
7981 {
7982 // Clear other items
7983 selected = false;
7984 }
7985 }
7986 *p_selected = selected;
7987 }
7988
7989 // Alter button behavior flags
7990 // To handle drag and drop of multiple items we need to avoid clearing selection on click.
7991 // 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.
7992 if (p_button_flags != NULL)
7993 {
7994 ImGuiButtonFlags button_flags = *p_button_flags;
7995 button_flags |= ImGuiButtonFlags_NoHoveredOnFocus;
7996 if ((!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) && !(ms->Flags & ImGuiMultiSelectFlags_SelectOnClickRelease))
7997 button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease;
7998 else
7999 button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
8000 *p_button_flags = button_flags;
8001 }
8002}
8003
8004// In charge of:
8005// - Auto-select on navigation.
8006// - Box-select toggle handling.
8007// - Right-click handling.
8008// - Altering selection based on Ctrl/Shift modifiers, both for keyboard and mouse.
8009// - Record current selection state for RangeSrc
8010// This is all rather complex, best to run and refer to "widgets_multiselect_xxx" tests in imgui_test_suite.
8011void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed)
8012{
8013 ImGuiContext& g = *GImGui;
8014 ImGuiWindow* window = g.CurrentWindow;
8015
8016 bool selected = *p_selected;
8017 bool pressed = *p_pressed;
8018 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
8019 ImGuiMultiSelectState* storage = ms->Storage;
8020 if (pressed)
8021 ms->IsFocused = true;
8022
8023 bool hovered = false;
8024 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect)
8025 hovered = IsItemHovered(flags: ImGuiHoveredFlags_AllowWhenBlockedByPopup);
8026 if (!ms->IsFocused && !hovered)
8027 return;
8028
8029 ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData;
8030
8031 ImGuiMultiSelectFlags flags = ms->Flags;
8032 const bool is_singleselect = (flags & ImGuiMultiSelectFlags_SingleSelect) != 0;
8033 bool is_ctrl = (ms->KeyMods & ImGuiMod_Ctrl) != 0;
8034 bool is_shift = (ms->KeyMods & ImGuiMod_Shift) != 0;
8035
8036 bool apply_to_range_src = false;
8037
8038 if (g.NavId == id && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid)
8039 apply_to_range_src = true;
8040 if (ms->IsEndIO == false)
8041 {
8042 ms->IO.Requests.resize(new_size: 0);
8043 ms->IsEndIO = true;
8044 }
8045
8046 // Auto-select as you navigate a list
8047 if (g.NavJustMovedToId == id)
8048 {
8049 if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
8050 {
8051 if (is_ctrl && is_shift)
8052 pressed = true;
8053 else if (!is_ctrl)
8054 selected = pressed = true;
8055 }
8056 else
8057 {
8058 // With NoAutoSelect, using Shift+keyboard performs a write/copy
8059 if (is_shift)
8060 pressed = true;
8061 else if (!is_ctrl)
8062 apply_to_range_src = true; // Since if (pressed) {} main block is not running we update this
8063 }
8064 }
8065
8066 if (apply_to_range_src)
8067 {
8068 storage->RangeSrcItem = item_data;
8069 storage->RangeSelected = selected; // Will be updated at the end of this function anyway.
8070 }
8071
8072 // Box-select toggle handling
8073 if (ms->BoxSelectId != 0)
8074 if (ImGuiBoxSelectState* bs = GetBoxSelectState(id: ms->BoxSelectId))
8075 {
8076 const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(r: g.LastItemData.Rect);
8077 const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(r: g.LastItemData.Rect);
8078 if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr))
8079 {
8080 if (storage->LastSelectionSize <= 0 && bs->IsStartedSetNavIdOnce)
8081 {
8082 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)
8083 bs->IsStartedSetNavIdOnce = false;
8084 }
8085 else
8086 {
8087 selected = !selected;
8088 MultiSelectAddSetRange(ms, selected, range_dir: +1, first_item: item_data, last_item: item_data);
8089 }
8090 storage->LastSelectionSize = ImMax(lhs: storage->LastSelectionSize + 1, rhs: 1);
8091 }
8092 }
8093
8094 // Right-click handling.
8095 // FIXME-MULTISELECT: Currently filtered out by ImGuiMultiSelectFlags_NoAutoSelect but maybe should be moved to Selectable(). See https://github.com/ocornut/imgui/pull/5816
8096 if (hovered && IsMouseClicked(button: 1) && (flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
8097 {
8098 if (g.ActiveId != 0 && g.ActiveId != id)
8099 ClearActiveID();
8100 SetFocusID(id, window);
8101 if (!pressed && !selected)
8102 {
8103 pressed = true;
8104 is_ctrl = is_shift = false;
8105 }
8106 }
8107
8108 // Unlike Space, Enter doesn't alter selection (but can still return a press) unless current item is not selected.
8109 // The later, "unless current item is not select", may become optional? It seems like a better default if Enter doesn't necessarily open something
8110 // (unlike e.g. Windows explorer). For use case where Enter always open something, we might decide to make this optional?
8111 const bool enter_pressed = pressed && (g.NavActivateId == id) && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput);
8112
8113 // Alter selection
8114 if (pressed && (!enter_pressed || !selected))
8115 {
8116 // Box-select
8117 ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse;
8118 if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
8119 if (selected == false && !g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1)
8120 BoxSelectPreStartDrag(id: ms->BoxSelectId, clicked_item: item_data);
8121
8122 //----------------------------------------------------------------------------------------
8123 // ACTION | Begin | Pressed/Activated | End
8124 //----------------------------------------------------------------------------------------
8125 // Keys Navigated: | Clear | Src=item, Sel=1 SetRange 1
8126 // Keys Navigated: Ctrl | n/a | n/a
8127 // Keys Navigated: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1
8128 // Keys Navigated: Ctrl+Shift | n/a | Dst=item, Sel=Src => Clear + SetRange Src-Dst
8129 // Keys Activated: | n/a | Src=item, Sel=1 => Clear + SetRange 1
8130 // Keys Activated: Ctrl | n/a | Src=item, Sel=!Sel => SetSange 1
8131 // Keys Activated: Shift | n/a | Dst=item, Sel=1 => Clear + SetSange 1
8132 //----------------------------------------------------------------------------------------
8133 // Mouse Pressed: | n/a | Src=item, Sel=1, => Clear + SetRange 1
8134 // Mouse Pressed: Ctrl | n/a | Src=item, Sel=!Sel => SetRange 1
8135 // Mouse Pressed: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1
8136 // Mouse Pressed: Ctrl+Shift | n/a | Dst=item, Sel=!Sel => SetRange Src-Dst
8137 //----------------------------------------------------------------------------------------
8138
8139 if ((flags & ImGuiMultiSelectFlags_NoAutoClear) == 0)
8140 {
8141 bool request_clear = false;
8142 if (is_singleselect)
8143 request_clear = true;
8144 else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl)
8145 request_clear = (flags & ImGuiMultiSelectFlags_NoAutoClearOnReselect) ? !selected : true;
8146 else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl)
8147 request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again.
8148 if (request_clear)
8149 MultiSelectAddSetAll(ms, selected: false);
8150 }
8151
8152 int range_direction;
8153 bool range_selected;
8154 if (is_shift && !is_singleselect)
8155 {
8156 //IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue);
8157 if (storage->RangeSrcItem == ImGuiSelectionUserData_Invalid)
8158 storage->RangeSrcItem = item_data;
8159 if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
8160 {
8161 // Shift+Arrow always select
8162 // Ctrl+Shift+Arrow copy source selection state (already stored by BeginMultiSelect() in storage->RangeSelected)
8163 range_selected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true;
8164 }
8165 else
8166 {
8167 // Shift+Arrow copy source selection state
8168 // Shift+Click always copy from target selection state
8169 if (ms->IsKeyboardSetRange)
8170 range_selected = (storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true;
8171 else
8172 range_selected = !selected;
8173 }
8174 range_direction = ms->RangeSrcPassedBy ? +1 : -1;
8175 }
8176 else
8177 {
8178 // Ctrl inverts selection, otherwise always select
8179 if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
8180 selected = is_ctrl ? !selected : true;
8181 else
8182 selected = !selected;
8183 storage->RangeSrcItem = item_data;
8184 range_selected = selected;
8185 range_direction = +1;
8186 }
8187 MultiSelectAddSetRange(ms, selected: range_selected, range_dir: range_direction, first_item: storage->RangeSrcItem, last_item: item_data);
8188 }
8189
8190 // Update/store the selection state of the Source item (used by CTRL+SHIFT, when Source is unselected we perform a range unselect)
8191 if (storage->RangeSrcItem == item_data)
8192 storage->RangeSelected = selected ? 1 : 0;
8193
8194 // Update/store the selection state of focused item
8195 if (g.NavId == id)
8196 {
8197 storage->NavIdItem = item_data;
8198 storage->NavIdSelected = selected ? 1 : 0;
8199 }
8200 if (storage->NavIdItem == item_data)
8201 ms->NavIdPassedBy = true;
8202 ms->LastSubmittedItem = item_data;
8203
8204 *p_selected = selected;
8205 *p_pressed = pressed;
8206}
8207
8208void ImGui::MultiSelectAddSetAll(ImGuiMultiSelectTempData* ms, bool selected)
8209{
8210 ImGuiSelectionRequest req = { .Type: ImGuiSelectionRequestType_SetAll, .Selected: selected, .RangeDirection: 0, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid };
8211 ms->IO.Requests.resize(new_size: 0); // Can always clear previous requests
8212 ms->IO.Requests.push_back(v: req); // Add new request
8213}
8214
8215void ImGui::MultiSelectAddSetRange(ImGuiMultiSelectTempData* ms, bool selected, int range_dir, ImGuiSelectionUserData first_item, ImGuiSelectionUserData last_item)
8216{
8217 // Merge contiguous spans into same request (unless NoRangeSelect is set which guarantees single-item ranges)
8218 if (ms->IO.Requests.Size > 0 && first_item == last_item && (ms->Flags & ImGuiMultiSelectFlags_NoRangeSelect) == 0)
8219 {
8220 ImGuiSelectionRequest* prev = &ms->IO.Requests.Data[ms->IO.Requests.Size - 1];
8221 if (prev->Type == ImGuiSelectionRequestType_SetRange && prev->RangeLastItem == ms->LastSubmittedItem && prev->Selected == selected)
8222 {
8223 prev->RangeLastItem = last_item;
8224 return;
8225 }
8226 }
8227
8228 ImGuiSelectionRequest req = { .Type: ImGuiSelectionRequestType_SetRange, .Selected: selected, .RangeDirection: (ImS8)range_dir, .RangeFirstItem: (range_dir > 0) ? first_item : last_item, .RangeLastItem: (range_dir > 0) ? last_item : first_item };
8229 ms->IO.Requests.push_back(v: req); // Add new request
8230}
8231
8232void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage)
8233{
8234#ifndef IMGUI_DISABLE_DEBUG_TOOLS
8235 const bool is_active = (storage->LastFrameActive >= GetFrameCount() - 2); // Note that fully clipped early out scrolling tables will appear as inactive here.
8236 if (!is_active) { PushStyleColor(idx: ImGuiCol_Text, col: GetStyleColorVec4(idx: ImGuiCol_TextDisabled)); }
8237 bool open = TreeNode(ptr_id: (void*)(intptr_t)storage->ID, fmt: "MultiSelect 0x%08X in '%s'%s", storage->ID, storage->Window ? storage->Window->Name : "N/A", is_active ? "" : " *Inactive*");
8238 if (!is_active) { PopStyleColor(); }
8239 if (!open)
8240 return;
8241 Text(fmt: "RangeSrcItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), RangeSelected = %d", storage->RangeSrcItem, storage->RangeSrcItem, storage->RangeSelected);
8242 Text(fmt: "NavIdItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), NavIdSelected = %d", storage->NavIdItem, storage->NavIdItem, storage->NavIdSelected);
8243 Text(fmt: "LastSelectionSize = %d", storage->LastSelectionSize); // Provided by user
8244 TreePop();
8245#else
8246 IM_UNUSED(storage);
8247#endif
8248}
8249
8250//-------------------------------------------------------------------------
8251// [SECTION] Widgets: Multi-Select helpers
8252//-------------------------------------------------------------------------
8253// - ImGuiSelectionBasicStorage
8254// - ImGuiSelectionExternalStorage
8255//-------------------------------------------------------------------------
8256
8257ImGuiSelectionBasicStorage::ImGuiSelectionBasicStorage()
8258{
8259 Size = 0;
8260 PreserveOrder = false;
8261 UserData = NULL;
8262 AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; };
8263 _SelectionOrder = 1; // Always >0
8264}
8265
8266void ImGuiSelectionBasicStorage::Clear()
8267{
8268 Size = 0;
8269 _SelectionOrder = 1; // Always >0
8270 _Storage.Data.resize(new_size: 0);
8271}
8272
8273void ImGuiSelectionBasicStorage::Swap(ImGuiSelectionBasicStorage& r)
8274{
8275 ImSwap(a&: Size, b&: r.Size);
8276 ImSwap(a&: _SelectionOrder, b&: r._SelectionOrder);
8277 _Storage.Data.swap(rhs&: r._Storage.Data);
8278}
8279
8280bool ImGuiSelectionBasicStorage::Contains(ImGuiID id) const
8281{
8282 return _Storage.GetInt(key: id, default_val: 0) != 0;
8283}
8284
8285static int IMGUI_CDECL PairComparerByValueInt(const void* lhs, const void* rhs)
8286{
8287 int lhs_v = ((const ImGuiStoragePair*)lhs)->val_i;
8288 int rhs_v = ((const ImGuiStoragePair*)rhs)->val_i;
8289 return (lhs_v > rhs_v ? +1 : lhs_v < rhs_v ? -1 : 0);
8290}
8291
8292// GetNextSelectedItem() is an abstraction allowing us to change our underlying actual storage system without impacting user.
8293// (e.g. store unselected vs compact down, compact down on demand, use raw ImVector<ImGuiID> instead of ImGuiStorage...)
8294bool ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it, ImGuiID* out_id)
8295{
8296 ImGuiStoragePair* it = (ImGuiStoragePair*)*opaque_it;
8297 ImGuiStoragePair* it_end = _Storage.Data.Data + _Storage.Data.Size;
8298 if (PreserveOrder && it == NULL && it_end != NULL)
8299 ImQsort(base: _Storage.Data.Data, count: (size_t)_Storage.Data.Size, size_of_element: sizeof(ImGuiStoragePair), compare_func: PairComparerByValueInt); // ~ImGuiStorage::BuildSortByValueInt()
8300 if (it == NULL)
8301 it = _Storage.Data.Data;
8302 IM_ASSERT(it >= _Storage.Data.Data && it <= it_end);
8303 if (it != it_end)
8304 while (it->val_i == 0 && it < it_end)
8305 it++;
8306 const bool has_more = (it != it_end);
8307 *opaque_it = has_more ? (void**)(it + 1) : (void**)(it);
8308 *out_id = has_more ? it->key : 0;
8309 if (PreserveOrder && !has_more)
8310 _Storage.BuildSortByKey();
8311 return has_more;
8312}
8313
8314void ImGuiSelectionBasicStorage::SetItemSelected(ImGuiID id, bool selected)
8315{
8316 int* p_int = _Storage.GetIntRef(key: id, default_val: 0);
8317 if (selected && *p_int == 0) { *p_int = _SelectionOrder++; Size++; }
8318 else if (!selected && *p_int != 0) { *p_int = 0; Size--; }
8319}
8320
8321// Optimized for batch edits (with same value of 'selected')
8322static void ImGuiSelectionBasicStorage_BatchSetItemSelected(ImGuiSelectionBasicStorage* selection, ImGuiID id, bool selected, int size_before_amends, int selection_order)
8323{
8324 ImGuiStorage* storage = &selection->_Storage;
8325 ImGuiStoragePair* it = ImLowerBound(in_begin: storage->Data.Data, in_end: storage->Data.Data + size_before_amends, key: id);
8326 const bool is_contained = (it != storage->Data.Data + size_before_amends) && (it->key == id);
8327 if (selected == (is_contained && it->val_i != 0))
8328 return;
8329 if (selected && !is_contained)
8330 storage->Data.push_back(v: ImGuiStoragePair(id, selection_order)); // Push unsorted at end of vector, will be sorted in SelectionMultiAmendsFinish()
8331 else if (is_contained)
8332 it->val_i = selected ? selection_order : 0; // Modify in-place.
8333 selection->Size += selected ? +1 : -1;
8334}
8335
8336static void ImGuiSelectionBasicStorage_BatchFinish(ImGuiSelectionBasicStorage* selection, bool selected, int size_before_amends)
8337{
8338 ImGuiStorage* storage = &selection->_Storage;
8339 if (selected && selection->Size != size_before_amends)
8340 storage->BuildSortByKey(); // When done selecting: sort everything
8341}
8342
8343// Apply requests coming from BeginMultiSelect() and EndMultiSelect().
8344// - Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen.
8345// - Honoring SetRange requests requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem.
8346// - In this demo we often submit indices to SetNextItemSelectionUserData() + store the same indices in persistent selection.
8347// - Your code may do differently. If you store pointers or objects ID in ImGuiSelectionUserData you may need to perform
8348// a lookup in order to have some way to iterate/interpolate between two items.
8349// - A full-featured application is likely to allow search/filtering which is likely to lead to using indices
8350// and constructing a view index <> object id/ptr data structure anyway.
8351// WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THIS UNNECESSARY 'ImGuiSelectionBasicStorage' INDIRECTION LOGIC.
8352// Notice that with the simplest adapter (using indices everywhere), all functions return their parameters.
8353// The most simple implementation (using indices everywhere) would look like:
8354// for (ImGuiSelectionRequest& req : ms_io->Requests)
8355// {
8356// if (req.Type == ImGuiSelectionRequestType_SetAll) { Clear(); if (req.Selected) { for (int n = 0; n < items_count; n++) { SetItemSelected(n, true); } }
8357// if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { SetItemSelected(n, ms_io->Selected); } }
8358// }
8359void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
8360{
8361 // For convenience we obtain ItemsCount as passed to BeginMultiSelect(), which is optional.
8362 // It makes sense when using ImGuiSelectionBasicStorage to simply pass your items count to BeginMultiSelect().
8363 // Other scheme may handle SetAll differently.
8364 IM_ASSERT(ms_io->ItemsCount != -1 && "Missing value for items_count in BeginMultiSelect() call!");
8365 IM_ASSERT(AdapterIndexToStorageId != NULL);
8366
8367 // This is optimized/specialized to cope with very large selections (e.g. 100k+ items)
8368 // - A simpler version could call SetItemSelected() directly instead of ImGuiSelectionBasicStorage_BatchSetItemSelected() + ImGuiSelectionBasicStorage_BatchFinish().
8369 // - Optimized select can append unsorted, then sort in a second pass. Optimized unselect can clear in-place then compact in a second pass.
8370 // - 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.
8371 // - 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
8372 // left and right: it affects coarse clipping + can emit multiple SetRange with 1 item each.)
8373 // FIXME-OPT: For each block of consecutive SetRange request:
8374 // - add all requests to a sorted list, store ID, selected, offset in ImGuiStorage.
8375 // - rewrite sorted storage a single time.
8376 for (ImGuiSelectionRequest& req : ms_io->Requests)
8377 {
8378 if (req.Type == ImGuiSelectionRequestType_SetAll)
8379 {
8380 Clear();
8381 if (req.Selected)
8382 {
8383 _Storage.Data.reserve(new_capacity: ms_io->ItemsCount);
8384 const int size_before_amends = _Storage.Data.Size;
8385 for (int idx = 0; idx < ms_io->ItemsCount; idx++, _SelectionOrder++)
8386 ImGuiSelectionBasicStorage_BatchSetItemSelected(selection: this, id: GetStorageIdFromIndex(idx), selected: req.Selected, size_before_amends, selection_order: _SelectionOrder);
8387 ImGuiSelectionBasicStorage_BatchFinish(selection: this, selected: req.Selected, size_before_amends);
8388 }
8389 }
8390 else if (req.Type == ImGuiSelectionRequestType_SetRange)
8391 {
8392 const int selection_changes = (int)req.RangeLastItem - (int)req.RangeFirstItem + 1;
8393 //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);
8394 if (selection_changes == 1 || (selection_changes < Size / 100))
8395 {
8396 // Multiple sorted insertion + copy likely to be faster.
8397 // Technically we could do a single copy with a little more work (sort sequential SetRange requests)
8398 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++)
8399 SetItemSelected(id: GetStorageIdFromIndex(idx), selected: req.Selected);
8400 }
8401 else
8402 {
8403 // Append insertion + single sort likely be faster.
8404 // Use req.RangeDirection to set order field so that shift+clicking from 1 to 5 is different than shift+clicking from 5 to 1
8405 const int size_before_amends = _Storage.Data.Size;
8406 int selection_order = _SelectionOrder + ((req.RangeDirection < 0) ? selection_changes - 1 : 0);
8407 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++, selection_order += req.RangeDirection)
8408 ImGuiSelectionBasicStorage_BatchSetItemSelected(selection: this, id: GetStorageIdFromIndex(idx), selected: req.Selected, size_before_amends, selection_order);
8409 if (req.Selected)
8410 _SelectionOrder += selection_changes;
8411 ImGuiSelectionBasicStorage_BatchFinish(selection: this, selected: req.Selected, size_before_amends);
8412 }
8413 }
8414 }
8415}
8416
8417//-------------------------------------------------------------------------
8418
8419ImGuiSelectionExternalStorage::ImGuiSelectionExternalStorage()
8420{
8421 UserData = NULL;
8422 AdapterSetItemSelected = NULL;
8423}
8424
8425// Apply requests coming from BeginMultiSelect() and EndMultiSelect().
8426// We also pull 'ms_io->ItemsCount' as passed for BeginMultiSelect() for consistency with ImGuiSelectionBasicStorage
8427// This makes no assumption about underlying storage.
8428void ImGuiSelectionExternalStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
8429{
8430 IM_ASSERT(AdapterSetItemSelected);
8431 for (ImGuiSelectionRequest& req : ms_io->Requests)
8432 {
8433 if (req.Type == ImGuiSelectionRequestType_SetAll)
8434 for (int idx = 0; idx < ms_io->ItemsCount; idx++)
8435 AdapterSetItemSelected(this, idx, req.Selected);
8436 if (req.Type == ImGuiSelectionRequestType_SetRange)
8437 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++)
8438 AdapterSetItemSelected(this, idx, req.Selected);
8439 }
8440}
8441
8442//-------------------------------------------------------------------------
8443// [SECTION] Widgets: ListBox
8444//-------------------------------------------------------------------------
8445// - BeginListBox()
8446// - EndListBox()
8447// - ListBox()
8448//-------------------------------------------------------------------------
8449
8450// This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label.
8451// This handle some subtleties with capturing info from the label.
8452// If you don't need a label you can pretty much directly use ImGui::BeginChild() with ImGuiChildFlags_FrameStyle.
8453// Tip: To have a list filling the entire window width, use size.x = -FLT_MIN and pass an non-visible label e.g. "##empty"
8454// 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).
8455bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg)
8456{
8457 ImGuiContext& g = *GImGui;
8458 ImGuiWindow* window = GetCurrentWindow();
8459 if (window->SkipItems)
8460 return false;
8461
8462 const ImGuiStyle& style = g.Style;
8463 const ImGuiID id = GetID(str_id: label);
8464 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
8465
8466 // Size default to hold ~7.25 items.
8467 // Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar.
8468 ImVec2 size = ImTrunc(v: CalcItemSize(size: size_arg, default_w: CalcItemWidth(), default_h: GetTextLineHeightWithSpacing() * 7.25f + style.FramePadding.y * 2.0f));
8469 ImVec2 frame_size = ImVec2(size.x, ImMax(lhs: size.y, rhs: label_size.y));
8470 ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
8471 ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
8472 g.NextItemData.ClearFlags();
8473
8474 if (!IsRectVisible(rect_min: bb.Min, rect_max: bb.Max))
8475 {
8476 ItemSize(size: bb.GetSize(), text_baseline_y: style.FramePadding.y);
8477 ItemAdd(bb, id: 0, nav_bb: &frame_bb);
8478 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
8479 return false;
8480 }
8481
8482 // FIXME-OPT: We could omit the BeginGroup() if label_size.x == 0.0f but would need to omit the EndGroup() as well.
8483 BeginGroup();
8484 if (label_size.x > 0.0f)
8485 {
8486 ImVec2 label_pos = ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y);
8487 RenderText(pos: label_pos, text: label);
8488 window->DC.CursorMaxPos = ImMax(lhs: window->DC.CursorMaxPos, rhs: label_pos + label_size);
8489 AlignTextToFramePadding();
8490 }
8491
8492 BeginChild(id, size: frame_bb.GetSize(), child_flags: ImGuiChildFlags_FrameStyle);
8493 return true;
8494}
8495
8496void ImGui::EndListBox()
8497{
8498 ImGuiContext& g = *GImGui;
8499 ImGuiWindow* window = g.CurrentWindow;
8500 IM_ASSERT((window->Flags & ImGuiWindowFlags_ChildWindow) && "Mismatched BeginListBox/EndListBox calls. Did you test the return value of BeginListBox?");
8501 IM_UNUSED(window);
8502
8503 EndChild();
8504 EndGroup(); // This is only required to be able to do IsItemXXX query on the whole ListBox including label
8505}
8506
8507bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items)
8508{
8509 const bool value_changed = ListBox(label, current_item, getter: Items_ArrayGetter, user_data: (void*)items, items_count, height_in_items: height_items);
8510 return value_changed;
8511}
8512
8513// This is merely a helper around BeginListBox(), EndListBox().
8514// Considering using those directly to submit custom data or store selection differently.
8515bool 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)
8516{
8517 ImGuiContext& g = *GImGui;
8518
8519 // Calculate size from "height_in_items"
8520 if (height_in_items < 0)
8521 height_in_items = ImMin(lhs: items_count, rhs: 7);
8522 float height_in_items_f = height_in_items + 0.25f;
8523 ImVec2 size(0.0f, ImTrunc(f: GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f));
8524
8525 if (!BeginListBox(label, size_arg: size))
8526 return false;
8527
8528 // Assume all items have even height (= 1 line of text). If you need items of different height,
8529 // you can create a custom version of ListBox() in your code without using the clipper.
8530 bool value_changed = false;
8531 ImGuiListClipper clipper;
8532 clipper.Begin(items_count, items_height: GetTextLineHeightWithSpacing()); // We know exactly our line height here so we pass it as a minor optimization, but generally you don't need to.
8533 clipper.IncludeItemByIndex(item_index: *current_item);
8534 while (clipper.Step())
8535 for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
8536 {
8537 const char* item_text = getter(user_data, i);
8538 if (item_text == NULL)
8539 item_text = "*Unknown item*";
8540
8541 PushID(int_id: i);
8542 const bool item_selected = (i == *current_item);
8543 if (Selectable(label: item_text, selected: item_selected))
8544 {
8545 *current_item = i;
8546 value_changed = true;
8547 }
8548 if (item_selected)
8549 SetItemDefaultFocus();
8550 PopID();
8551 }
8552 EndListBox();
8553
8554 if (value_changed)
8555 MarkItemEdited(id: g.LastItemData.ID);
8556
8557 return value_changed;
8558}
8559
8560//-------------------------------------------------------------------------
8561// [SECTION] Widgets: PlotLines, PlotHistogram
8562//-------------------------------------------------------------------------
8563// - PlotEx() [Internal]
8564// - PlotLines()
8565// - PlotHistogram()
8566//-------------------------------------------------------------------------
8567// Plot/Graph widgets are not very good.
8568// Consider writing your own, or using a third-party one, see:
8569// - ImPlot https://github.com/epezent/implot
8570// - others https://github.com/ocornut/imgui/wiki/Useful-Extensions
8571//-------------------------------------------------------------------------
8572
8573int 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)
8574{
8575 ImGuiContext& g = *GImGui;
8576 ImGuiWindow* window = GetCurrentWindow();
8577 if (window->SkipItems)
8578 return -1;
8579
8580 const ImGuiStyle& style = g.Style;
8581 const ImGuiID id = window->GetID(str: label);
8582
8583 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
8584 const ImVec2 frame_size = CalcItemSize(size: size_arg, default_w: CalcItemWidth(), default_h: label_size.y + style.FramePadding.y * 2.0f);
8585
8586 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
8587 const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding);
8588 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));
8589 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
8590 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: ImGuiItemFlags_NoNav))
8591 return -1;
8592 bool hovered;
8593 ButtonBehavior(bb: frame_bb, id, out_hovered: &hovered, NULL);
8594
8595 // Determine scale from values if not specified
8596 if (scale_min == FLT_MAX || scale_max == FLT_MAX)
8597 {
8598 float v_min = FLT_MAX;
8599 float v_max = -FLT_MAX;
8600 for (int i = 0; i < values_count; i++)
8601 {
8602 const float v = values_getter(data, i);
8603 if (v != v) // Ignore NaN values
8604 continue;
8605 v_min = ImMin(lhs: v_min, rhs: v);
8606 v_max = ImMax(lhs: v_max, rhs: v);
8607 }
8608 if (scale_min == FLT_MAX)
8609 scale_min = v_min;
8610 if (scale_max == FLT_MAX)
8611 scale_max = v_max;
8612 }
8613
8614 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: GetColorU32(idx: ImGuiCol_FrameBg), borders: true, rounding: style.FrameRounding);
8615
8616 const int values_count_min = (plot_type == ImGuiPlotType_Lines) ? 2 : 1;
8617 int idx_hovered = -1;
8618 if (values_count >= values_count_min)
8619 {
8620 int res_w = ImMin(lhs: (int)frame_size.x, rhs: values_count) + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
8621 int item_count = values_count + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
8622
8623 // Tooltip on hover
8624 if (hovered && inner_bb.Contains(p: g.IO.MousePos))
8625 {
8626 const float t = ImClamp(v: (g.IO.MousePos.x - inner_bb.Min.x) / (inner_bb.Max.x - inner_bb.Min.x), mn: 0.0f, mx: 0.9999f);
8627 const int v_idx = (int)(t * item_count);
8628 IM_ASSERT(v_idx >= 0 && v_idx < values_count);
8629
8630 const float v0 = values_getter(data, (v_idx + values_offset) % values_count);
8631 const float v1 = values_getter(data, (v_idx + 1 + values_offset) % values_count);
8632 if (plot_type == ImGuiPlotType_Lines)
8633 SetTooltip("%d: %8.4g\n%d: %8.4g", v_idx, v0, v_idx + 1, v1);
8634 else if (plot_type == ImGuiPlotType_Histogram)
8635 SetTooltip("%d: %8.4g", v_idx, v0);
8636 idx_hovered = v_idx;
8637 }
8638
8639 const float t_step = 1.0f / (float)res_w;
8640 const float inv_scale = (scale_min == scale_max) ? 0.0f : (1.0f / (scale_max - scale_min));
8641
8642 float v0 = values_getter(data, (0 + values_offset) % values_count);
8643 float t0 = 0.0f;
8644 ImVec2 tp0 = ImVec2( t0, 1.0f - ImSaturate(f: (v0 - scale_min) * inv_scale) ); // Point in the normalized space of our target rectangle
8645 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
8646
8647 const ImU32 col_base = GetColorU32(idx: (plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLines : ImGuiCol_PlotHistogram);
8648 const ImU32 col_hovered = GetColorU32(idx: (plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLinesHovered : ImGuiCol_PlotHistogramHovered);
8649
8650 for (int n = 0; n < res_w; n++)
8651 {
8652 const float t1 = t0 + t_step;
8653 const int v1_idx = (int)(t0 * item_count + 0.5f);
8654 IM_ASSERT(v1_idx >= 0 && v1_idx < values_count);
8655 const float v1 = values_getter(data, (v1_idx + values_offset + 1) % values_count);
8656 const ImVec2 tp1 = ImVec2( t1, 1.0f - ImSaturate(f: (v1 - scale_min) * inv_scale) );
8657
8658 // 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.
8659 ImVec2 pos0 = ImLerp(a: inner_bb.Min, b: inner_bb.Max, t: tp0);
8660 ImVec2 pos1 = ImLerp(a: inner_bb.Min, b: inner_bb.Max, t: (plot_type == ImGuiPlotType_Lines) ? tp1 : ImVec2(tp1.x, histogram_zero_line_t));
8661 if (plot_type == ImGuiPlotType_Lines)
8662 {
8663 window->DrawList->AddLine(p1: pos0, p2: pos1, col: idx_hovered == v1_idx ? col_hovered : col_base);
8664 }
8665 else if (plot_type == ImGuiPlotType_Histogram)
8666 {
8667 if (pos1.x >= pos0.x + 2.0f)
8668 pos1.x -= 1.0f;
8669 window->DrawList->AddRectFilled(p_min: pos0, p_max: pos1, col: idx_hovered == v1_idx ? col_hovered : col_base);
8670 }
8671
8672 t0 = t1;
8673 tp0 = tp1;
8674 }
8675 }
8676
8677 // Text overlay
8678 if (overlay_text)
8679 RenderTextClipped(pos_min: ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), pos_max: frame_bb.Max, text: overlay_text, NULL, NULL, align: ImVec2(0.5f, 0.0f));
8680
8681 if (label_size.x > 0.0f)
8682 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), text: label);
8683
8684 // Return hovered index or -1 if none are hovered.
8685 // 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().
8686 return idx_hovered;
8687}
8688
8689struct ImGuiPlotArrayGetterData
8690{
8691 const float* Values;
8692 int Stride;
8693
8694 ImGuiPlotArrayGetterData(const float* values, int stride) { Values = values; Stride = stride; }
8695};
8696
8697static float Plot_ArrayGetter(void* data, int idx)
8698{
8699 ImGuiPlotArrayGetterData* plot_data = (ImGuiPlotArrayGetterData*)data;
8700 const float v = *(const float*)(const void*)((const unsigned char*)plot_data->Values + (size_t)idx * plot_data->Stride);
8701 return v;
8702}
8703
8704void 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)
8705{
8706 ImGuiPlotArrayGetterData data(values, stride);
8707 PlotEx(plot_type: ImGuiPlotType_Lines, label, values_getter: &Plot_ArrayGetter, data: (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, size_arg: graph_size);
8708}
8709
8710void 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)
8711{
8712 PlotEx(plot_type: ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, size_arg: graph_size);
8713}
8714
8715void 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)
8716{
8717 ImGuiPlotArrayGetterData data(values, stride);
8718 PlotEx(plot_type: ImGuiPlotType_Histogram, label, values_getter: &Plot_ArrayGetter, data: (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, size_arg: graph_size);
8719}
8720
8721void 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)
8722{
8723 PlotEx(plot_type: ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, size_arg: graph_size);
8724}
8725
8726//-------------------------------------------------------------------------
8727// [SECTION] Widgets: Value helpers
8728// Those is not very useful, legacy API.
8729//-------------------------------------------------------------------------
8730// - Value()
8731//-------------------------------------------------------------------------
8732
8733void ImGui::Value(const char* prefix, bool b)
8734{
8735 Text(fmt: "%s: %s", prefix, (b ? "true" : "false"));
8736}
8737
8738void ImGui::Value(const char* prefix, int v)
8739{
8740 Text(fmt: "%s: %d", prefix, v);
8741}
8742
8743void ImGui::Value(const char* prefix, unsigned int v)
8744{
8745 Text(fmt: "%s: %d", prefix, v);
8746}
8747
8748void ImGui::Value(const char* prefix, float v, const char* float_format)
8749{
8750 if (float_format)
8751 {
8752 char fmt[64];
8753 ImFormatString(buf: fmt, IM_ARRAYSIZE(fmt), fmt: "%%s: %s", float_format);
8754 Text(fmt, prefix, v);
8755 }
8756 else
8757 {
8758 Text(fmt: "%s: %.3f", prefix, v);
8759 }
8760}
8761
8762//-------------------------------------------------------------------------
8763// [SECTION] MenuItem, BeginMenu, EndMenu, etc.
8764//-------------------------------------------------------------------------
8765// - ImGuiMenuColumns [Internal]
8766// - BeginMenuBar()
8767// - EndMenuBar()
8768// - BeginMainMenuBar()
8769// - EndMainMenuBar()
8770// - BeginMenu()
8771// - EndMenu()
8772// - MenuItemEx() [Internal]
8773// - MenuItem()
8774//-------------------------------------------------------------------------
8775
8776// Helpers for internal use
8777void ImGuiMenuColumns::Update(float spacing, bool window_reappearing)
8778{
8779 if (window_reappearing)
8780 memset(s: Widths, c: 0, n: sizeof(Widths));
8781 Spacing = (ImU16)spacing;
8782 CalcNextTotalWidth(update_offsets: true);
8783 memset(s: Widths, c: 0, n: sizeof(Widths));
8784 TotalWidth = NextTotalWidth;
8785 NextTotalWidth = 0;
8786}
8787
8788void ImGuiMenuColumns::CalcNextTotalWidth(bool update_offsets)
8789{
8790 ImU16 offset = 0;
8791 bool want_spacing = false;
8792 for (int i = 0; i < IM_ARRAYSIZE(Widths); i++)
8793 {
8794 ImU16 width = Widths[i];
8795 if (want_spacing && width > 0)
8796 offset += Spacing;
8797 want_spacing |= (width > 0);
8798 if (update_offsets)
8799 {
8800 if (i == 1) { OffsetLabel = offset; }
8801 if (i == 2) { OffsetShortcut = offset; }
8802 if (i == 3) { OffsetMark = offset; }
8803 }
8804 offset += width;
8805 }
8806 NextTotalWidth = offset;
8807}
8808
8809float ImGuiMenuColumns::DeclColumns(float w_icon, float w_label, float w_shortcut, float w_mark)
8810{
8811 Widths[0] = ImMax(lhs: Widths[0], rhs: (ImU16)w_icon);
8812 Widths[1] = ImMax(lhs: Widths[1], rhs: (ImU16)w_label);
8813 Widths[2] = ImMax(lhs: Widths[2], rhs: (ImU16)w_shortcut);
8814 Widths[3] = ImMax(lhs: Widths[3], rhs: (ImU16)w_mark);
8815 CalcNextTotalWidth(update_offsets: false);
8816 return (float)ImMax(lhs: TotalWidth, rhs: NextTotalWidth);
8817}
8818
8819// FIXME: Provided a rectangle perhaps e.g. a BeginMenuBarEx() could be used anywhere..
8820// Currently the main responsibility of this function being to setup clip-rect + horizontal layout + menu navigation layer.
8821// 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.
8822// Then later the same system could be used for multiple menu-bars, scrollbars, side-bars.
8823bool ImGui::BeginMenuBar()
8824{
8825 ImGuiWindow* window = GetCurrentWindow();
8826 if (window->SkipItems)
8827 return false;
8828 if (!(window->Flags & ImGuiWindowFlags_MenuBar))
8829 return false;
8830
8831 IM_ASSERT(!window->DC.MenuBarAppending);
8832 BeginGroup(); // Backup position on layer 0 // FIXME: Misleading to use a group for that backup/restore
8833 PushID(str_id: "##MenuBar");
8834
8835 // 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.
8836 // 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.
8837 const float border_top = ImMax(IM_ROUND(window->WindowBorderSize * 0.5f - window->TitleBarHeight), rhs: 0.0f);
8838 const float border_half = IM_ROUND(window->WindowBorderSize * 0.5f);
8839 ImRect bar_rect = window->MenuBarRect();
8840 ImRect clip_rect(ImFloor(f: bar_rect.Min.x + border_half), ImFloor(f: bar_rect.Min.y + border_top), ImFloor(f: ImMax(lhs: bar_rect.Min.x, rhs: bar_rect.Max.x - ImMax(lhs: window->WindowRounding, rhs: border_half))), ImFloor(f: bar_rect.Max.y));
8841 clip_rect.ClipWith(r: window->OuterRectClipped);
8842 PushClipRect(clip_rect_min: clip_rect.Min, clip_rect_max: clip_rect.Max, intersect_with_current_clip_rect: false);
8843
8844 // 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).
8845 window->DC.CursorPos = window->DC.CursorMaxPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffset.x, bar_rect.Min.y + window->DC.MenuBarOffset.y);
8846 window->DC.LayoutType = ImGuiLayoutType_Horizontal;
8847 window->DC.IsSameLine = false;
8848 window->DC.NavLayerCurrent = ImGuiNavLayer_Menu;
8849 window->DC.MenuBarAppending = true;
8850 AlignTextToFramePadding();
8851 return true;
8852}
8853
8854void ImGui::EndMenuBar()
8855{
8856 ImGuiWindow* window = GetCurrentWindow();
8857 if (window->SkipItems)
8858 return;
8859 ImGuiContext& g = *GImGui;
8860
8861 IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'"
8862 IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar);
8863 IM_ASSERT(window->DC.MenuBarAppending);
8864
8865 // Nav: When a move request within one of our child menu failed, capture the request to navigate among our siblings.
8866 if (NavMoveRequestButNoResultYet() && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) && (g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu))
8867 {
8868 // Try to find out if the request is for one of our child menu
8869 ImGuiWindow* nav_earliest_child = g.NavWindow;
8870 while (nav_earliest_child->ParentWindow && (nav_earliest_child->ParentWindow->Flags & ImGuiWindowFlags_ChildMenu))
8871 nav_earliest_child = nav_earliest_child->ParentWindow;
8872 if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0)
8873 {
8874 // To do so we claim focus back, restore NavId and then process the movement request for yet another frame.
8875 // 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)
8876 const ImGuiNavLayer layer = ImGuiNavLayer_Menu;
8877 IM_ASSERT(window->DC.NavLayersActiveMaskNext & (1 << layer)); // Sanity check (FIXME: Seems unnecessary)
8878 FocusWindow(window);
8879 SetNavID(id: window->NavLastIds[layer], nav_layer: layer, focus_scope_id: 0, rect_rel: window->NavRectRel[layer]);
8880 // FIXME-NAV: How to deal with this when not using g.IO.ConfigNavCursorVisibleAuto?
8881 if (g.NavCursorVisible)
8882 {
8883 g.NavCursorVisible = false; // Hide nav cursor for the current frame so we don't see the intermediary selection. Will be set again
8884 g.NavCursorHideFrames = 2;
8885 }
8886 g.NavHighlightItemUnderNav = g.NavMousePosDirty = true;
8887 NavMoveRequestForward(move_dir: g.NavMoveDir, clip_dir: g.NavMoveClipDir, move_flags: g.NavMoveFlags, scroll_flags: g.NavMoveScrollFlags); // Repeat
8888 }
8889 }
8890
8891 PopClipRect();
8892 PopID();
8893 IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'"
8894 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.
8895
8896 // FIXME: Extremely confusing, cleanup by (a) working on WorkRect stack system (b) not using a Group confusingly here.
8897 ImGuiGroupData& group_data = g.GroupStack.back();
8898 group_data.EmitItem = false;
8899 ImVec2 restore_cursor_max_pos = group_data.BackupCursorMaxPos;
8900 window->DC.IdealMaxPos.x = ImMax(lhs: window->DC.IdealMaxPos.x, rhs: window->DC.CursorMaxPos.x - window->Scroll.x); // Convert ideal extents for scrolling layer equivalent.
8901 EndGroup(); // Restore position on layer 0 // FIXME: Misleading to use a group for that backup/restore
8902 window->DC.LayoutType = ImGuiLayoutType_Vertical;
8903 window->DC.IsSameLine = false;
8904 window->DC.NavLayerCurrent = ImGuiNavLayer_Main;
8905 window->DC.MenuBarAppending = false;
8906 window->DC.CursorMaxPos = restore_cursor_max_pos;
8907}
8908
8909// Important: calling order matters!
8910// FIXME: Somehow overlapping with docking tech.
8911// FIXME: The "rect-cut" aspect of this could be formalized into a lower-level helper (rect-cut: https://halt.software/dead-simple-layouts)
8912bool ImGui::BeginViewportSideBar(const char* name, ImGuiViewport* viewport_p, ImGuiDir dir, float axis_size, ImGuiWindowFlags window_flags)
8913{
8914 IM_ASSERT(dir != ImGuiDir_None);
8915
8916 ImGuiWindow* bar_window = FindWindowByName(name);
8917 ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)(viewport_p ? viewport_p : GetMainViewport());
8918 if (bar_window == NULL || bar_window->BeginCount == 0)
8919 {
8920 // Calculate and set window size/position
8921 ImRect avail_rect = viewport->GetBuildWorkRect();
8922 ImGuiAxis axis = (dir == ImGuiDir_Up || dir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X;
8923 ImVec2 pos = avail_rect.Min;
8924 if (dir == ImGuiDir_Right || dir == ImGuiDir_Down)
8925 pos[axis] = avail_rect.Max[axis] - axis_size;
8926 ImVec2 size = avail_rect.GetSize();
8927 size[axis] = axis_size;
8928 SetNextWindowPos(pos);
8929 SetNextWindowSize(size);
8930
8931 // Report our size into work area (for next frame) using actual window size
8932 if (dir == ImGuiDir_Up || dir == ImGuiDir_Left)
8933 viewport->BuildWorkInsetMin[axis] += axis_size;
8934 else if (dir == ImGuiDir_Down || dir == ImGuiDir_Right)
8935 viewport->BuildWorkInsetMax[axis] += axis_size;
8936 }
8937
8938 window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking;
8939 SetNextWindowViewport(viewport->ID); // Enforce viewport so we don't create our own viewport when ImGuiConfigFlags_ViewportsNoMerge is set.
8940 PushStyleVar(idx: ImGuiStyleVar_WindowRounding, val: 0.0f);
8941 PushStyleVar(idx: ImGuiStyleVar_WindowMinSize, val: ImVec2(0, 0)); // Lift normal size constraint
8942 bool is_open = Begin(name, NULL, flags: window_flags);
8943 PopStyleVar(count: 2);
8944
8945 return is_open;
8946}
8947
8948bool ImGui::BeginMainMenuBar()
8949{
8950 ImGuiContext& g = *GImGui;
8951 ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport();
8952
8953 // Notify of viewport change so GetFrameHeight() can be accurate in case of DPI change
8954 SetCurrentViewport(NULL, viewport);
8955
8956 // For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set.
8957 // FIXME: This could be generalized as an opt-in way to clamp window->DC.CursorStartPos to avoid SafeArea?
8958 // FIXME: Consider removing support for safe area down the line... it's messy. Nowadays consoles have support for TV calibration in OS settings.
8959 g.NextWindowData.MenuBarOffsetMinVal = ImVec2(g.Style.DisplaySafeAreaPadding.x, ImMax(lhs: g.Style.DisplaySafeAreaPadding.y - g.Style.FramePadding.y, rhs: 0.0f));
8960 ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar;
8961 float height = GetFrameHeight();
8962 bool is_open = BeginViewportSideBar(name: "##MainMenuBar", viewport_p: viewport, dir: ImGuiDir_Up, axis_size: height, window_flags);
8963 g.NextWindowData.MenuBarOffsetMinVal = ImVec2(0.0f, 0.0f);
8964 if (!is_open)
8965 {
8966 End();
8967 return false;
8968 }
8969
8970 // Temporarily disable _NoSavedSettings, in the off-chance that tables or child windows submitted within the menu-bar may want to use settings. (#8356)
8971 g.CurrentWindow->Flags &= ~ImGuiWindowFlags_NoSavedSettings;
8972 BeginMenuBar();
8973 return is_open;
8974}
8975
8976void ImGui::EndMainMenuBar()
8977{
8978 ImGuiContext& g = *GImGui;
8979 if (!g.CurrentWindow->DC.MenuBarAppending)
8980 {
8981 IM_ASSERT_USER_ERROR(0, "Calling EndMainMenuBar() not from a menu-bar!"); // Not technically testing that it is the main menu bar
8982 return;
8983 }
8984
8985 EndMenuBar();
8986 g.CurrentWindow->Flags |= ImGuiWindowFlags_NoSavedSettings; // Restore _NoSavedSettings (#8356)
8987
8988 // When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window
8989 // FIXME: With this strategy we won't be able to restore a NULL focus.
8990 if (g.CurrentWindow == g.NavWindow && g.NavLayer == ImGuiNavLayer_Main && !g.NavAnyRequest && g.ActiveId == 0)
8991 FocusTopMostWindowUnderOne(under_this_window: g.NavWindow, NULL, NULL, flags: ImGuiFocusRequestFlags_UnlessBelowModal | ImGuiFocusRequestFlags_RestoreFocusedChild);
8992
8993 End();
8994}
8995
8996static bool IsRootOfOpenMenuSet()
8997{
8998 ImGuiContext& g = *GImGui;
8999 ImGuiWindow* window = g.CurrentWindow;
9000 if ((g.OpenPopupStack.Size <= g.BeginPopupStack.Size) || (window->Flags & ImGuiWindowFlags_ChildMenu))
9001 return false;
9002
9003 // Initially we used 'upper_popup->OpenParentId == window->IDStack.back()' to differentiate multiple menu sets from each others
9004 // (e.g. inside menu bar vs loose menu items) based on parent ID.
9005 // This would however prevent the use of e.g. PushID() user code submitting menus.
9006 // Previously this worked between popup and a first child menu because the first child menu always had the _ChildWindow flag,
9007 // making hovering on parent popup possible while first child menu was focused - but this was generally a bug with other side effects.
9008 // Instead we don't treat Popup specifically (in order to consistently support menu features in them), maybe the first child menu of a Popup
9009 // doesn't have the _ChildWindow flag, and we rely on this IsRootOfOpenMenuSet() check to allow hovering between root window/popup and first child menu.
9010 // 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.
9011 // 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
9012 // 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
9013 // it likely won't be a problem anyone runs into.
9014 const ImGuiPopupData* upper_popup = &g.OpenPopupStack[g.BeginPopupStack.Size];
9015 if (window->DC.NavLayerCurrent != upper_popup->ParentNavLayer)
9016 return false;
9017 return upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu) && ImGui::IsWindowChildOf(window: upper_popup->Window, potential_parent: window, popup_hierarchy: true, dock_hierarchy: false);
9018}
9019
9020bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
9021{
9022 ImGuiWindow* window = GetCurrentWindow();
9023 if (window->SkipItems)
9024 return false;
9025
9026 ImGuiContext& g = *GImGui;
9027 const ImGuiStyle& style = g.Style;
9028 const ImGuiID id = window->GetID(str: label);
9029 bool menu_is_open = IsPopupOpen(id, popup_flags: ImGuiPopupFlags_None);
9030
9031 // 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)
9032 // 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.
9033 ImGuiWindowFlags window_flags = ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus;
9034 if (window->Flags & ImGuiWindowFlags_ChildMenu)
9035 window_flags |= ImGuiWindowFlags_ChildWindow;
9036
9037 // If a menu with same the ID was already submitted, we will append to it, matching the behavior of Begin().
9038 // 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.
9039 // If somehow this is ever becoming a problem we can switch to use e.g. ImGuiStorage mapping key to last frame used.
9040 if (g.MenusIdSubmittedThisFrame.contains(v: id))
9041 {
9042 if (menu_is_open)
9043 menu_is_open = BeginPopupMenuEx(id, label, extra_window_flags: window_flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display)
9044 else
9045 g.NextWindowData.ClearFlags(); // we behave like Begin() and need to consume those values
9046 return menu_is_open;
9047 }
9048
9049 // Tag menu as used. Next time BeginMenu() with same ID is called it will append to existing menu
9050 g.MenusIdSubmittedThisFrame.push_back(v: id);
9051
9052 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
9053
9054 // 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)
9055 // This is only done for items for the menu set and not the full parent window.
9056 const bool menuset_is_open = IsRootOfOpenMenuSet();
9057 if (menuset_is_open)
9058 PushItemFlag(option: ImGuiItemFlags_NoWindowHoverableCheck, enabled: true);
9059
9060 // The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu,
9061 // However the final position is going to be different! It is chosen by FindBestWindowPosForPopup().
9062 // e.g. Menus tend to overlap each other horizontally to amplify relative Z-ordering.
9063 ImVec2 popup_pos, pos = window->DC.CursorPos;
9064 PushID(str_id: label);
9065 if (!enabled)
9066 BeginDisabled();
9067 const ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
9068 bool pressed;
9069
9070 // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
9071 const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SelectOnClick | ImGuiSelectableFlags_NoAutoClosePopups;
9072 if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
9073 {
9074 // Menu inside a horizontal menu bar
9075 // Selectable extend their highlight by half ItemSpacing in each direction.
9076 // For ChildMenu, the popup position will be overwritten by the call to FindBestWindowPosForPopup() in Begin()
9077 popup_pos = ImVec2(pos.x - 1.0f - IM_TRUNC(style.ItemSpacing.x * 0.5f), pos.y - style.FramePadding.y + window->MenuBarHeight);
9078 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f);
9079 PushStyleVarX(idx: ImGuiStyleVar_ItemSpacing, val_x: style.ItemSpacing.x * 2.0f);
9080 float w = label_size.x;
9081 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
9082 pressed = Selectable(label: "", selected: menu_is_open, flags: selectable_flags, size_arg: ImVec2(w, label_size.y));
9083 LogSetNextTextDecoration(prefix: "[", suffix: "]");
9084 RenderText(pos: text_pos, text: label);
9085 PopStyleVar();
9086 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().
9087 }
9088 else
9089 {
9090 // Menu inside a regular/vertical menu
9091 // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
9092 // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.
9093 popup_pos = ImVec2(pos.x, pos.y - style.WindowPadding.y);
9094 float icon_w = (icon && icon[0]) ? CalcTextSize(text: icon, NULL).x : 0.0f;
9095 float checkmark_w = IM_TRUNC(g.FontSize * 1.20f);
9096 float min_w = window->DC.MenuColumns.DeclColumns(w_icon: icon_w, w_label: label_size.x, w_shortcut: 0.0f, w_mark: checkmark_w); // Feedback to next frame
9097 float extra_w = ImMax(lhs: 0.0f, rhs: GetContentRegionAvail().x - min_w);
9098 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
9099 pressed = Selectable(label: "", selected: menu_is_open, flags: selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, size_arg: ImVec2(min_w, label_size.y));
9100 LogSetNextTextDecoration(prefix: "", suffix: ">");
9101 RenderText(pos: text_pos, text: label);
9102 if (icon_w > 0.0f)
9103 RenderText(pos: pos + ImVec2(offsets->OffsetIcon, 0.0f), text: icon);
9104 RenderArrow(draw_list: window->DrawList, pos: pos + ImVec2(offsets->OffsetMark + extra_w + g.FontSize * 0.30f, 0.0f), col: GetColorU32(idx: ImGuiCol_Text), dir: ImGuiDir_Right);
9105 }
9106 if (!enabled)
9107 EndDisabled();
9108
9109 const bool hovered = (g.HoveredId == id) && enabled && !g.NavHighlightItemUnderNav;
9110 if (menuset_is_open)
9111 PopItemFlag();
9112
9113 bool want_open = false;
9114 bool want_open_nav_init = false;
9115 bool want_close = false;
9116 if (window->DC.LayoutType == ImGuiLayoutType_Vertical) // (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu))
9117 {
9118 // Close menu when not hovering it anymore unless we are moving roughly in the direction of the menu
9119 // Implement http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown to avoid using timers, so menus feels more reactive.
9120 bool moving_toward_child_menu = false;
9121 ImGuiPopupData* child_popup = (g.BeginPopupStack.Size < g.OpenPopupStack.Size) ? &g.OpenPopupStack[g.BeginPopupStack.Size] : NULL; // Popup candidate (testing below)
9122 ImGuiWindow* child_menu_window = (child_popup && child_popup->Window && child_popup->Window->ParentWindow == window) ? child_popup->Window : NULL;
9123 if (g.HoveredWindow == window && child_menu_window != NULL)
9124 {
9125 const float ref_unit = g.FontSize; // FIXME-DPI
9126 const float child_dir = (window->Pos.x < child_menu_window->Pos.x) ? 1.0f : -1.0f;
9127 const ImRect next_window_rect = child_menu_window->Rect();
9128 ImVec2 ta = (g.IO.MousePos - g.IO.MouseDelta);
9129 ImVec2 tb = (child_dir > 0.0f) ? next_window_rect.GetTL() : next_window_rect.GetTR();
9130 ImVec2 tc = (child_dir > 0.0f) ? next_window_rect.GetBL() : next_window_rect.GetBR();
9131 const float pad_farmost_h = ImClamp(ImFabs(ta.x - tb.x) * 0.30f, mn: ref_unit * 0.5f, mx: ref_unit * 2.5f); // Add a bit of extra slack.
9132 ta.x += child_dir * -0.5f;
9133 tb.x += child_dir * ref_unit;
9134 tc.x += child_dir * ref_unit;
9135 tb.y = ta.y + ImMax(lhs: (tb.y - pad_farmost_h) - ta.y, rhs: -ref_unit * 8.0f); // Triangle has maximum height to limit the slope and the bias toward large sub-menus
9136 tc.y = ta.y + ImMin(lhs: (tc.y + pad_farmost_h) - ta.y, rhs: +ref_unit * 8.0f);
9137 moving_toward_child_menu = ImTriangleContainsPoint(a: ta, b: tb, c: tc, p: g.IO.MousePos);
9138 //GetForegroundDrawList()->AddTriangleFilled(ta, tb, tc, moving_toward_child_menu ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); // [DEBUG]
9139 }
9140
9141 // 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)
9142 // 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.
9143 // (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.)
9144 if (menu_is_open && !hovered && g.HoveredWindow == window && !moving_toward_child_menu && !g.NavHighlightItemUnderNav && g.ActiveId == 0)
9145 want_close = true;
9146
9147 // Open
9148 // (note: at this point 'hovered' actually includes the NavDisableMouseHover == false test)
9149 if (!menu_is_open && pressed) // Click/activate to open
9150 want_open = true;
9151 else if (!menu_is_open && hovered && !moving_toward_child_menu) // Hover to open
9152 want_open = true;
9153 else if (!menu_is_open && hovered && g.HoveredIdTimer >= 0.30f && g.MouseStationaryTimer >= 0.30f) // Hover to open (timer fallback)
9154 want_open = true;
9155 if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open
9156 {
9157 want_open = want_open_nav_init = true;
9158 NavMoveRequestCancel();
9159 SetNavCursorVisibleAfterMove();
9160 }
9161 }
9162 else
9163 {
9164 // Menu bar
9165 if (menu_is_open && pressed && menuset_is_open) // Click an open menu again to close it
9166 {
9167 want_close = true;
9168 want_open = menu_is_open = false;
9169 }
9170 else if (pressed || (hovered && menuset_is_open && !menu_is_open)) // First click to open, then hover to open others
9171 {
9172 want_open = true;
9173 }
9174 else if (g.NavId == id && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open
9175 {
9176 want_open = true;
9177 NavMoveRequestCancel();
9178 }
9179 }
9180
9181 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.. }'
9182 want_close = true;
9183 if (want_close && IsPopupOpen(id, popup_flags: ImGuiPopupFlags_None))
9184 ClosePopupToLevel(remaining: g.BeginPopupStack.Size, restore_focus_to_window_under_popup: true);
9185
9186 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Openable | (menu_is_open ? ImGuiItemStatusFlags_Opened : 0));
9187 PopID();
9188
9189 if (want_open && !menu_is_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size)
9190 {
9191 // 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.
9192 OpenPopup(str_id: label);
9193 }
9194 else if (want_open)
9195 {
9196 menu_is_open = true;
9197 OpenPopup(str_id: label, popup_flags: ImGuiPopupFlags_NoReopen);// | (want_open_nav_init ? ImGuiPopupFlags_NoReopenAlwaysNavInit : 0));
9198 }
9199
9200 if (menu_is_open)
9201 {
9202 ImGuiLastItemData last_item_in_parent = g.LastItemData;
9203 SetNextWindowPos(pos: popup_pos, cond: ImGuiCond_Always); // Note: misleading: the value will serve as reference for FindBestWindowPosForPopup(), not actual pos.
9204 PushStyleVar(idx: ImGuiStyleVar_ChildRounding, val: style.PopupRounding); // First level will use _PopupRounding, subsequent will use _ChildRounding
9205 menu_is_open = BeginPopupMenuEx(id, label, extra_window_flags: window_flags); // menu_is_open may be 'false' when the popup is completely clipped (e.g. zero size display)
9206 PopStyleVar();
9207 if (menu_is_open)
9208 {
9209 // Implement what ImGuiPopupFlags_NoReopenAlwaysNavInit would do:
9210 // Perform an init request in the case the popup was already open (via a previous mouse hover)
9211 if (want_open && want_open_nav_init && !g.NavInitRequest)
9212 {
9213 FocusWindow(window: g.CurrentWindow, flags: ImGuiFocusRequestFlags_UnlessBelowModal);
9214 NavInitWindow(window: g.CurrentWindow, force_reinit: false);
9215 }
9216
9217 // Restore LastItemData so IsItemXXXX functions can work after BeginMenu()/EndMenu()
9218 // (This fixes using IsItemClicked() and IsItemHovered(), but IsItemHovered() also relies on its support for ImGuiItemFlags_NoWindowHoverableCheck)
9219 g.LastItemData = last_item_in_parent;
9220 if (g.HoveredWindow == window)
9221 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow;
9222 }
9223 }
9224 else
9225 {
9226 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
9227 }
9228
9229 return menu_is_open;
9230}
9231
9232bool ImGui::BeginMenu(const char* label, bool enabled)
9233{
9234 return BeginMenuEx(label, NULL, enabled);
9235}
9236
9237void ImGui::EndMenu()
9238{
9239 // Nav: When a left move request our menu failed, close ourselves.
9240 ImGuiContext& g = *GImGui;
9241 ImGuiWindow* window = g.CurrentWindow;
9242 IM_ASSERT(window->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginMenu()/EndMenu() calls
9243 ImGuiWindow* parent_window = window->ParentWindow; // Should always be != NULL is we passed assert.
9244 if (window->BeginCount == window->BeginCountPreviousFrame)
9245 if (g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet())
9246 if (g.NavWindow && (g.NavWindow->RootWindowForNav == window) && parent_window->DC.LayoutType == ImGuiLayoutType_Vertical)
9247 {
9248 ClosePopupToLevel(remaining: g.BeginPopupStack.Size - 1, restore_focus_to_window_under_popup: true);
9249 NavMoveRequestCancel();
9250 }
9251
9252 EndPopup();
9253}
9254
9255bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut, bool selected, bool enabled)
9256{
9257 ImGuiWindow* window = GetCurrentWindow();
9258 if (window->SkipItems)
9259 return false;
9260
9261 ImGuiContext& g = *GImGui;
9262 ImGuiStyle& style = g.Style;
9263 ImVec2 pos = window->DC.CursorPos;
9264 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
9265
9266 // See BeginMenuEx() for comments about this.
9267 const bool menuset_is_open = IsRootOfOpenMenuSet();
9268 if (menuset_is_open)
9269 PushItemFlag(option: ImGuiItemFlags_NoWindowHoverableCheck, enabled: true);
9270
9271 // We've been using the equivalent of ImGuiSelectableFlags_SetNavIdOnHover on all Selectable() since early Nav system days (commit 43ee5d73),
9272 // 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.
9273 bool pressed;
9274 PushID(str_id: label);
9275 if (!enabled)
9276 BeginDisabled();
9277
9278 // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
9279 const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_SelectOnRelease | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SetNavIdOnHover;
9280 const ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
9281 if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
9282 {
9283 // Mimic the exact layout spacing of BeginMenu() to allow MenuItem() inside a menu bar, which is a little misleading but may be useful
9284 // Note that in this situation: we don't render the shortcut, we render a highlight instead of the selected tick mark.
9285 float w = label_size.x;
9286 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f);
9287 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
9288 PushStyleVarX(idx: ImGuiStyleVar_ItemSpacing, val_x: style.ItemSpacing.x * 2.0f);
9289 pressed = Selectable(label: "", selected, flags: selectable_flags, size_arg: ImVec2(w, 0.0f));
9290 PopStyleVar();
9291 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
9292 RenderText(pos: text_pos, text: label);
9293 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().
9294 }
9295 else
9296 {
9297 // Menu item inside a vertical menu
9298 // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
9299 // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.
9300 float icon_w = (icon && icon[0]) ? CalcTextSize(text: icon, NULL).x : 0.0f;
9301 float shortcut_w = (shortcut && shortcut[0]) ? CalcTextSize(text: shortcut, NULL).x : 0.0f;
9302 float checkmark_w = IM_TRUNC(g.FontSize * 1.20f);
9303 float min_w = window->DC.MenuColumns.DeclColumns(w_icon: icon_w, w_label: label_size.x, w_shortcut: shortcut_w, w_mark: checkmark_w); // Feedback for next frame
9304 float stretch_w = ImMax(lhs: 0.0f, rhs: GetContentRegionAvail().x - min_w);
9305 pressed = Selectable(label: "", selected: false, flags: selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, size_arg: ImVec2(min_w, label_size.y));
9306 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
9307 {
9308 RenderText(pos: pos + ImVec2(offsets->OffsetLabel, 0.0f), text: label);
9309 if (icon_w > 0.0f)
9310 RenderText(pos: pos + ImVec2(offsets->OffsetIcon, 0.0f), text: icon);
9311 if (shortcut_w > 0.0f)
9312 {
9313 PushStyleColor(idx: ImGuiCol_Text, col: style.Colors[ImGuiCol_TextDisabled]);
9314 LogSetNextTextDecoration(prefix: "(", suffix: ")");
9315 RenderText(pos: pos + ImVec2(offsets->OffsetShortcut + stretch_w, 0.0f), text: shortcut, NULL, hide_text_after_hash: false);
9316 PopStyleColor();
9317 }
9318 if (selected)
9319 RenderCheckMark(draw_list: window->DrawList, pos: pos + ImVec2(offsets->OffsetMark + stretch_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), col: GetColorU32(idx: ImGuiCol_Text), sz: g.FontSize * 0.866f);
9320 }
9321 }
9322 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0));
9323 if (!enabled)
9324 EndDisabled();
9325 PopID();
9326 if (menuset_is_open)
9327 PopItemFlag();
9328
9329 return pressed;
9330}
9331
9332bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled)
9333{
9334 return MenuItemEx(label, NULL, shortcut, selected, enabled);
9335}
9336
9337bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled)
9338{
9339 if (MenuItemEx(label, NULL, shortcut, selected: p_selected ? *p_selected : false, enabled))
9340 {
9341 if (p_selected)
9342 *p_selected = !*p_selected;
9343 return true;
9344 }
9345 return false;
9346}
9347
9348//-------------------------------------------------------------------------
9349// [SECTION] Widgets: BeginTabBar, EndTabBar, etc.
9350//-------------------------------------------------------------------------
9351// - BeginTabBar()
9352// - BeginTabBarEx() [Internal]
9353// - EndTabBar()
9354// - TabBarLayout() [Internal]
9355// - TabBarCalcTabID() [Internal]
9356// - TabBarCalcMaxTabWidth() [Internal]
9357// - TabBarFindTabById() [Internal]
9358// - TabBarFindTabByOrder() [Internal]
9359// - TabBarFindMostRecentlySelectedTabForActiveWindow() [Internal]
9360// - TabBarGetCurrentTab() [Internal]
9361// - TabBarGetTabName() [Internal]
9362// - TabBarAddTab() [Internal]
9363// - TabBarRemoveTab() [Internal]
9364// - TabBarCloseTab() [Internal]
9365// - TabBarScrollClamp() [Internal]
9366// - TabBarScrollToTab() [Internal]
9367// - TabBarQueueFocus() [Internal]
9368// - TabBarQueueReorder() [Internal]
9369// - TabBarProcessReorderFromMousePos() [Internal]
9370// - TabBarProcessReorder() [Internal]
9371// - TabBarScrollingButtons() [Internal]
9372// - TabBarTabListPopupButton() [Internal]
9373//-------------------------------------------------------------------------
9374
9375struct ImGuiTabBarSection
9376{
9377 int TabCount; // Number of tabs in this section.
9378 float Width; // Sum of width of tabs in this section (after shrinking down)
9379 float WidthAfterShrinkMinWidth;
9380 float Spacing; // Horizontal spacing at the end of the section.
9381
9382 ImGuiTabBarSection() { memset(s: this, c: 0, n: sizeof(*this)); }
9383};
9384
9385namespace ImGui
9386{
9387 static void TabBarLayout(ImGuiTabBar* tab_bar);
9388 static ImU32 TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window);
9389 static float TabBarCalcMaxTabWidth();
9390 static float TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling);
9391 static void TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections);
9392 static ImGuiTabItem* TabBarScrollingButtons(ImGuiTabBar* tab_bar);
9393 static ImGuiTabItem* TabBarTabListPopupButton(ImGuiTabBar* tab_bar);
9394}
9395
9396ImGuiTabBar::ImGuiTabBar()
9397{
9398 memset(s: this, c: 0, n: sizeof(*this));
9399 CurrFrameVisible = PrevFrameVisible = -1;
9400 LastTabItemIdx = -1;
9401}
9402
9403static inline int TabItemGetSectionIdx(const ImGuiTabItem* tab)
9404{
9405 return (tab->Flags & ImGuiTabItemFlags_Leading) ? 0 : (tab->Flags & ImGuiTabItemFlags_Trailing) ? 2 : 1;
9406}
9407
9408static int IMGUI_CDECL TabItemComparerBySection(const void* lhs, const void* rhs)
9409{
9410 const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
9411 const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
9412 const int a_section = TabItemGetSectionIdx(tab: a);
9413 const int b_section = TabItemGetSectionIdx(tab: b);
9414 if (a_section != b_section)
9415 return a_section - b_section;
9416 return (int)(a->IndexDuringLayout - b->IndexDuringLayout);
9417}
9418
9419static int IMGUI_CDECL TabItemComparerByBeginOrder(const void* lhs, const void* rhs)
9420{
9421 const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
9422 const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
9423 return (int)(a->BeginOrder - b->BeginOrder);
9424}
9425
9426static ImGuiTabBar* GetTabBarFromTabBarRef(const ImGuiPtrOrIndex& ref)
9427{
9428 ImGuiContext& g = *GImGui;
9429 return ref.Ptr ? (ImGuiTabBar*)ref.Ptr : g.TabBars.GetByIndex(n: ref.Index);
9430}
9431
9432static ImGuiPtrOrIndex GetTabBarRefFromTabBar(ImGuiTabBar* tab_bar)
9433{
9434 ImGuiContext& g = *GImGui;
9435 if (g.TabBars.Contains(p: tab_bar))
9436 return ImGuiPtrOrIndex(g.TabBars.GetIndex(p: tab_bar));
9437 return ImGuiPtrOrIndex(tab_bar);
9438}
9439
9440bool ImGui::BeginTabBar(const char* str_id, ImGuiTabBarFlags flags)
9441{
9442 ImGuiContext& g = *GImGui;
9443 ImGuiWindow* window = g.CurrentWindow;
9444 if (window->SkipItems)
9445 return false;
9446
9447 ImGuiID id = window->GetID(str: str_id);
9448 ImGuiTabBar* tab_bar = g.TabBars.GetOrAddByKey(key: id);
9449 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);
9450 tab_bar->ID = id;
9451 tab_bar->SeparatorMinX = tab_bar_bb.Min.x - IM_TRUNC(window->WindowPadding.x * 0.5f);
9452 tab_bar->SeparatorMaxX = tab_bar_bb.Max.x + IM_TRUNC(window->WindowPadding.x * 0.5f);
9453 //if (g.NavWindow && IsWindowChildOf(g.NavWindow, window, false, false))
9454 flags |= ImGuiTabBarFlags_IsFocused;
9455 return BeginTabBarEx(tab_bar, bb: tab_bar_bb, flags);
9456}
9457
9458bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImGuiTabBarFlags flags)
9459{
9460 ImGuiContext& g = *GImGui;
9461 ImGuiWindow* window = g.CurrentWindow;
9462 if (window->SkipItems)
9463 return false;
9464
9465 IM_ASSERT(tab_bar->ID != 0);
9466 if ((flags & ImGuiTabBarFlags_DockNode) == 0)
9467 PushOverrideID(id: tab_bar->ID);
9468
9469 // Add to stack
9470 g.CurrentTabBarStack.push_back(v: GetTabBarRefFromTabBar(tab_bar));
9471 g.CurrentTabBar = tab_bar;
9472 tab_bar->Window = window;
9473
9474 // Append with multiple BeginTabBar()/EndTabBar() pairs.
9475 tab_bar->BackupCursorPos = window->DC.CursorPos;
9476 if (tab_bar->CurrFrameVisible == g.FrameCount)
9477 {
9478 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
9479 tab_bar->BeginCount++;
9480 return true;
9481 }
9482
9483 // Ensure correct ordering when toggling ImGuiTabBarFlags_Reorderable flag, or when a new tab was added while being not reorderable
9484 if ((flags & ImGuiTabBarFlags_Reorderable) != (tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (tab_bar->TabsAddedNew && !(flags & ImGuiTabBarFlags_Reorderable)))
9485 if ((flags & ImGuiTabBarFlags_DockNode) == 0) // FIXME: TabBar with DockNode can now be hybrid
9486 ImQsort(base: tab_bar->Tabs.Data, count: tab_bar->Tabs.Size, size_of_element: sizeof(ImGuiTabItem), compare_func: TabItemComparerByBeginOrder);
9487 tab_bar->TabsAddedNew = false;
9488
9489 // Flags
9490 if ((flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0)
9491 flags |= ImGuiTabBarFlags_FittingPolicyDefault_;
9492
9493 tab_bar->Flags = flags;
9494 tab_bar->BarRect = tab_bar_bb;
9495 tab_bar->WantLayout = true; // Layout will be done on the first call to ItemTab()
9496 tab_bar->PrevFrameVisible = tab_bar->CurrFrameVisible;
9497 tab_bar->CurrFrameVisible = g.FrameCount;
9498 tab_bar->PrevTabsContentsHeight = tab_bar->CurrTabsContentsHeight;
9499 tab_bar->CurrTabsContentsHeight = 0.0f;
9500 tab_bar->ItemSpacingY = g.Style.ItemSpacing.y;
9501 tab_bar->FramePadding = g.Style.FramePadding;
9502 tab_bar->TabsActiveCount = 0;
9503 tab_bar->LastTabItemIdx = -1;
9504 tab_bar->BeginCount = 1;
9505
9506 // Set cursor pos in a way which only be used in the off-chance the user erroneously submits item before BeginTabItem(): items will overlap
9507 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
9508
9509 // Draw separator
9510 // (it would be misleading to draw this in EndTabBar() suggesting that it may be drawn over tabs, as tab bar are appendable)
9511 const ImU32 col = GetColorU32(idx: (flags & ImGuiTabBarFlags_IsFocused) ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected);
9512 if (g.Style.TabBarBorderSize > 0.0f)
9513 {
9514 const float y = tab_bar->BarRect.Max.y;
9515 window->DrawList->AddRectFilled(p_min: ImVec2(tab_bar->SeparatorMinX, y - g.Style.TabBarBorderSize), p_max: ImVec2(tab_bar->SeparatorMaxX, y), col);
9516 }
9517 return true;
9518}
9519
9520void ImGui::EndTabBar()
9521{
9522 ImGuiContext& g = *GImGui;
9523 ImGuiWindow* window = g.CurrentWindow;
9524 if (window->SkipItems)
9525 return;
9526
9527 ImGuiTabBar* tab_bar = g.CurrentTabBar;
9528 if (tab_bar == NULL)
9529 {
9530 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Mismatched BeginTabBar()/EndTabBar()!");
9531 return;
9532 }
9533
9534 // Fallback in case no TabItem have been submitted
9535 if (tab_bar->WantLayout)
9536 TabBarLayout(tab_bar);
9537
9538 // Restore the last visible height if no tab is visible, this reduce vertical flicker/movement when a tabs gets removed without calling SetTabItemClosed().
9539 const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
9540 if (tab_bar->VisibleTabWasSubmitted || tab_bar->VisibleTabId == 0 || tab_bar_appearing)
9541 {
9542 tab_bar->CurrTabsContentsHeight = ImMax(lhs: window->DC.CursorPos.y - tab_bar->BarRect.Max.y, rhs: tab_bar->CurrTabsContentsHeight);
9543 window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->CurrTabsContentsHeight;
9544 }
9545 else
9546 {
9547 window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->PrevTabsContentsHeight;
9548 }
9549 if (tab_bar->BeginCount > 1)
9550 window->DC.CursorPos = tab_bar->BackupCursorPos;
9551
9552 tab_bar->LastTabItemIdx = -1;
9553 if ((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0)
9554 PopID();
9555
9556 g.CurrentTabBarStack.pop_back();
9557 g.CurrentTabBar = g.CurrentTabBarStack.empty() ? NULL : GetTabBarFromTabBarRef(ref: g.CurrentTabBarStack.back());
9558}
9559
9560// Scrolling happens only in the central section (leading/trailing sections are not scrolling)
9561static float TabBarCalcScrollableWidth(ImGuiTabBar* tab_bar, ImGuiTabBarSection* sections)
9562{
9563 return tab_bar->BarRect.GetWidth() - sections[0].Width - sections[2].Width - sections[1].Spacing;
9564}
9565
9566// This is called only once a frame before by the first call to ItemTab()
9567// The reason we're not calling it in BeginTabBar() is to leave a chance to the user to call the SetTabItemClosed() functions.
9568static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar)
9569{
9570 ImGuiContext& g = *GImGui;
9571 tab_bar->WantLayout = false;
9572
9573 // Track selected tab when resizing our parent down
9574 const bool scroll_to_selected_tab = (tab_bar->BarRectPrevWidth > tab_bar->BarRect.GetWidth());
9575 tab_bar->BarRectPrevWidth = tab_bar->BarRect.GetWidth();
9576
9577 // Garbage collect by compacting list
9578 // Detect if we need to sort out tab list (e.g. in rare case where a tab changed section)
9579 int tab_dst_n = 0;
9580 bool need_sort_by_section = false;
9581 ImGuiTabBarSection sections[3]; // Layout sections: Leading, Central, Trailing
9582 for (int tab_src_n = 0; tab_src_n < tab_bar->Tabs.Size; tab_src_n++)
9583 {
9584 ImGuiTabItem* tab = &tab_bar->Tabs[tab_src_n];
9585 if (tab->LastFrameVisible < tab_bar->PrevFrameVisible || tab->WantClose)
9586 {
9587 // Remove tab
9588 if (tab_bar->VisibleTabId == tab->ID) { tab_bar->VisibleTabId = 0; }
9589 if (tab_bar->SelectedTabId == tab->ID) { tab_bar->SelectedTabId = 0; }
9590 if (tab_bar->NextSelectedTabId == tab->ID) { tab_bar->NextSelectedTabId = 0; }
9591 continue;
9592 }
9593 if (tab_dst_n != tab_src_n)
9594 tab_bar->Tabs[tab_dst_n] = tab_bar->Tabs[tab_src_n];
9595
9596 tab = &tab_bar->Tabs[tab_dst_n];
9597 tab->IndexDuringLayout = (ImS16)tab_dst_n;
9598
9599 // We will need sorting if tabs have changed section (e.g. moved from one of Leading/Central/Trailing to another)
9600 int curr_tab_section_n = TabItemGetSectionIdx(tab);
9601 if (tab_dst_n > 0)
9602 {
9603 ImGuiTabItem* prev_tab = &tab_bar->Tabs[tab_dst_n - 1];
9604 int prev_tab_section_n = TabItemGetSectionIdx(tab: prev_tab);
9605 if (curr_tab_section_n == 0 && prev_tab_section_n != 0)
9606 need_sort_by_section = true;
9607 if (prev_tab_section_n == 2 && curr_tab_section_n != 2)
9608 need_sort_by_section = true;
9609 }
9610
9611 sections[curr_tab_section_n].TabCount++;
9612 tab_dst_n++;
9613 }
9614 if (tab_bar->Tabs.Size != tab_dst_n)
9615 tab_bar->Tabs.resize(new_size: tab_dst_n);
9616
9617 if (need_sort_by_section)
9618 ImQsort(base: tab_bar->Tabs.Data, count: tab_bar->Tabs.Size, size_of_element: sizeof(ImGuiTabItem), compare_func: TabItemComparerBySection);
9619
9620 // Calculate spacing between sections
9621 sections[0].Spacing = sections[0].TabCount > 0 && (sections[1].TabCount + sections[2].TabCount) > 0 ? g.Style.ItemInnerSpacing.x : 0.0f;
9622 sections[1].Spacing = sections[1].TabCount > 0 && sections[2].TabCount > 0 ? g.Style.ItemInnerSpacing.x : 0.0f;
9623
9624 // Setup next selected tab
9625 ImGuiID scroll_to_tab_id = 0;
9626 if (tab_bar->NextSelectedTabId)
9627 {
9628 tab_bar->SelectedTabId = tab_bar->NextSelectedTabId;
9629 tab_bar->NextSelectedTabId = 0;
9630 scroll_to_tab_id = tab_bar->SelectedTabId;
9631 }
9632
9633 // Process order change request (we could probably process it when requested but it's just saner to do it in a single spot).
9634 if (tab_bar->ReorderRequestTabId != 0)
9635 {
9636 if (TabBarProcessReorder(tab_bar))
9637 if (tab_bar->ReorderRequestTabId == tab_bar->SelectedTabId)
9638 scroll_to_tab_id = tab_bar->ReorderRequestTabId;
9639 tab_bar->ReorderRequestTabId = 0;
9640 }
9641
9642 // Tab List Popup (will alter tab_bar->BarRect and therefore the available width!)
9643 const bool tab_list_popup_button = (tab_bar->Flags & ImGuiTabBarFlags_TabListPopupButton) != 0;
9644 if (tab_list_popup_button)
9645 if (ImGuiTabItem* tab_to_select = TabBarTabListPopupButton(tab_bar)) // NB: Will alter BarRect.Min.x!
9646 scroll_to_tab_id = tab_bar->SelectedTabId = tab_to_select->ID;
9647
9648 // Leading/Trailing tabs will be shrink only if central one aren't visible anymore, so layout the shrink data as: leading, trailing, central
9649 // (whereas our tabs are stored as: leading, central, trailing)
9650 int shrink_buffer_indexes[3] = { 0, sections[0].TabCount + sections[2].TabCount, sections[0].TabCount };
9651 g.ShrinkWidthBuffer.resize(new_size: tab_bar->Tabs.Size);
9652
9653 // Minimum shrink width
9654 const float shrink_min_width = (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyMixed) ? g.Style.TabMinWidthShrink : 1.0f;
9655
9656 // Compute ideal tabs widths + store them into shrink buffer
9657 ImGuiTabItem* most_recently_selected_tab = NULL;
9658 int curr_section_n = -1;
9659 bool found_selected_tab_id = false;
9660 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
9661 {
9662 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
9663 IM_ASSERT(tab->LastFrameVisible >= tab_bar->PrevFrameVisible);
9664
9665 if ((most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) && !(tab->Flags & ImGuiTabItemFlags_Button))
9666 most_recently_selected_tab = tab;
9667 if (tab->ID == tab_bar->SelectedTabId)
9668 found_selected_tab_id = true;
9669 if (scroll_to_tab_id == 0 && g.NavJustMovedToId == tab->ID)
9670 scroll_to_tab_id = tab->ID;
9671
9672 // Refresh tab width immediately, otherwise changes of style e.g. style.FramePadding.x would noticeably lag in the tab bar.
9673 // Additionally, when using TabBarAddTab() to manipulate tab bar order we occasionally insert new tabs that don't have a width yet,
9674 // and we cannot wait for the next BeginTabItem() call. We cannot compute this width within TabBarAddTab() because font size depends on the active window.
9675 const char* tab_name = TabBarGetTabName(tab_bar, tab);
9676 const bool has_close_button_or_unsaved_marker = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) == 0 || (tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
9677 tab->ContentWidth = (tab->RequestedWidth >= 0.0f) ? tab->RequestedWidth : TabItemCalcSize(label: tab_name, has_close_button_or_unsaved_marker).x;
9678 if ((tab->Flags & ImGuiTabItemFlags_Button) == 0)
9679 tab->ContentWidth = ImMax(lhs: tab->ContentWidth, rhs: g.Style.TabMinWidthBase);
9680
9681 int section_n = TabItemGetSectionIdx(tab);
9682 ImGuiTabBarSection* section = &sections[section_n];
9683 section->Width += tab->ContentWidth + (section_n == curr_section_n ? g.Style.ItemInnerSpacing.x : 0.0f);
9684 section->WidthAfterShrinkMinWidth += ImMin(lhs: tab->ContentWidth, rhs: shrink_min_width) + (section_n == curr_section_n ? g.Style.ItemInnerSpacing.x : 0.0f);
9685 curr_section_n = section_n;
9686
9687 // Store data so we can build an array sorted by width if we need to shrink tabs down
9688 IM_MSVC_WARNING_SUPPRESS(6385);
9689 ImGuiShrinkWidthItem* shrink_width_item = &g.ShrinkWidthBuffer[shrink_buffer_indexes[section_n]++];
9690 shrink_width_item->Index = tab_n;
9691 shrink_width_item->Width = shrink_width_item->InitialWidth = tab->ContentWidth;
9692 tab->Width = ImMax(lhs: tab->ContentWidth, rhs: 1.0f);
9693 }
9694
9695 // Compute total ideal width (used for e.g. auto-resizing a window)
9696 float width_all_tabs_after_min_width_shrink = 0.0f;
9697 tab_bar->WidthAllTabsIdeal = 0.0f;
9698 for (int section_n = 0; section_n < 3; section_n++)
9699 {
9700 tab_bar->WidthAllTabsIdeal += sections[section_n].Width + sections[section_n].Spacing;
9701 width_all_tabs_after_min_width_shrink += sections[section_n].WidthAfterShrinkMinWidth + sections[section_n].Spacing;
9702 }
9703
9704 // Horizontal scrolling buttons
9705 // Important: note that TabBarScrollButtons() will alter BarRect.Max.x.
9706 const bool can_scroll = (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll) || (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyMixed);
9707 const float width_all_tabs_to_use_for_scroll = (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll) ? tab_bar->WidthAllTabs : width_all_tabs_after_min_width_shrink;
9708 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);
9709 if (tab_bar->ScrollButtonEnabled)
9710 if (ImGuiTabItem* scroll_and_select_tab = TabBarScrollingButtons(tab_bar))
9711 {
9712 scroll_to_tab_id = scroll_and_select_tab->ID;
9713 if ((scroll_and_select_tab->Flags & ImGuiTabItemFlags_Button) == 0)
9714 tab_bar->SelectedTabId = scroll_to_tab_id;
9715 }
9716 if (scroll_to_tab_id == 0 && scroll_to_selected_tab)
9717 scroll_to_tab_id = tab_bar->SelectedTabId;
9718
9719 // Shrink widths if full tabs don't fit in their allocated space
9720 float section_0_w = sections[0].Width + sections[0].Spacing;
9721 float section_1_w = sections[1].Width + sections[1].Spacing;
9722 float section_2_w = sections[2].Width + sections[2].Spacing;
9723 bool central_section_is_visible = (section_0_w + section_2_w) < tab_bar->BarRect.GetWidth();
9724 float width_excess;
9725 if (central_section_is_visible)
9726 width_excess = ImMax(lhs: section_1_w - (tab_bar->BarRect.GetWidth() - section_0_w - section_2_w), rhs: 0.0f); // Excess used to shrink central section
9727 else
9728 width_excess = (section_0_w + section_2_w) - tab_bar->BarRect.GetWidth(); // Excess used to shrink leading/trailing section
9729
9730 // With ImGuiTabBarFlags_FittingPolicyScroll policy, we will only shrink leading/trailing if the central section is not visible anymore
9731 const bool can_shrink = (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyShrink) || (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyMixed);
9732 if (width_excess >= 1.0f && (can_shrink || !central_section_is_visible))
9733 {
9734 int shrink_data_count = (central_section_is_visible ? sections[1].TabCount : sections[0].TabCount + sections[2].TabCount);
9735 int shrink_data_offset = (central_section_is_visible ? sections[0].TabCount + sections[2].TabCount : 0);
9736 ShrinkWidths(items: g.ShrinkWidthBuffer.Data + shrink_data_offset, count: shrink_data_count, width_excess, width_min: shrink_min_width);
9737
9738 // Apply shrunk values into tabs and sections
9739 for (int tab_n = shrink_data_offset; tab_n < shrink_data_offset + shrink_data_count; tab_n++)
9740 {
9741 ImGuiTabItem* tab = &tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index];
9742 float shrinked_width = IM_TRUNC(g.ShrinkWidthBuffer[tab_n].Width);
9743 if (shrinked_width < 0.0f)
9744 continue;
9745
9746 shrinked_width = ImMax(lhs: 1.0f, rhs: shrinked_width);
9747 int section_n = TabItemGetSectionIdx(tab);
9748 sections[section_n].Width -= (tab->Width - shrinked_width);
9749 tab->Width = shrinked_width;
9750 }
9751 }
9752
9753 // Layout all active tabs
9754 int section_tab_index = 0;
9755 float tab_offset = 0.0f;
9756 tab_bar->WidthAllTabs = 0.0f;
9757 for (int section_n = 0; section_n < 3; section_n++)
9758 {
9759 ImGuiTabBarSection* section = &sections[section_n];
9760 if (section_n == 2)
9761 tab_offset = ImMin(lhs: ImMax(lhs: 0.0f, rhs: tab_bar->BarRect.GetWidth() - section->Width), rhs: tab_offset);
9762
9763 for (int tab_n = 0; tab_n < section->TabCount; tab_n++)
9764 {
9765 ImGuiTabItem* tab = &tab_bar->Tabs[section_tab_index + tab_n];
9766 tab->Offset = tab_offset;
9767 tab->NameOffset = -1;
9768 tab_offset += tab->Width + (tab_n < section->TabCount - 1 ? g.Style.ItemInnerSpacing.x : 0.0f);
9769 }
9770 tab_bar->WidthAllTabs += ImMax(lhs: section->Width + section->Spacing, rhs: 0.0f);
9771 tab_offset += section->Spacing;
9772 section_tab_index += section->TabCount;
9773 }
9774
9775 // Clear name buffers
9776 tab_bar->TabsNames.Buf.resize(new_size: 0);
9777
9778 // If we have lost the selected tab, select the next most recently active one
9779 if (found_selected_tab_id == false)
9780 tab_bar->SelectedTabId = 0;
9781 if (tab_bar->SelectedTabId == 0 && tab_bar->NextSelectedTabId == 0 && most_recently_selected_tab != NULL)
9782 scroll_to_tab_id = tab_bar->SelectedTabId = most_recently_selected_tab->ID;
9783
9784 // Lock in visible tab
9785 tab_bar->VisibleTabId = tab_bar->SelectedTabId;
9786 tab_bar->VisibleTabWasSubmitted = false;
9787
9788 // CTRL+TAB can override visible tab temporarily
9789 if (g.NavWindowingTarget != NULL && g.NavWindowingTarget->DockNode && g.NavWindowingTarget->DockNode->TabBar == tab_bar)
9790 tab_bar->VisibleTabId = scroll_to_tab_id = g.NavWindowingTarget->TabId;
9791
9792 // Apply request requests
9793 if (scroll_to_tab_id != 0)
9794 TabBarScrollToTab(tab_bar, tab_id: scroll_to_tab_id, sections);
9795 else if (tab_bar->ScrollButtonEnabled && IsMouseHoveringRect(r_min: tab_bar->BarRect.Min, r_max: tab_bar->BarRect.Max, clip: true) && IsWindowContentHoverable(window: g.CurrentWindow))
9796 {
9797 const float wheel = g.IO.MouseWheelRequestAxisSwap ? g.IO.MouseWheel : g.IO.MouseWheelH;
9798 const ImGuiKey wheel_key = g.IO.MouseWheelRequestAxisSwap ? ImGuiKey_MouseWheelY : ImGuiKey_MouseWheelX;
9799 if (TestKeyOwner(key: wheel_key, owner_id: tab_bar->ID) && wheel != 0.0f)
9800 {
9801 const float scroll_step = wheel * TabBarCalcScrollableWidth(tab_bar, sections) / 3.0f;
9802 tab_bar->ScrollingTargetDistToVisibility = 0.0f;
9803 tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, scrolling: tab_bar->ScrollingTarget - scroll_step);
9804 }
9805 SetKeyOwner(key: wheel_key, owner_id: tab_bar->ID);
9806 }
9807
9808 // Update scrolling
9809 tab_bar->ScrollingAnim = TabBarScrollClamp(tab_bar, scrolling: tab_bar->ScrollingAnim);
9810 tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, scrolling: tab_bar->ScrollingTarget);
9811 if (tab_bar->ScrollingAnim != tab_bar->ScrollingTarget)
9812 {
9813 // Scrolling speed adjust itself so we can always reach our target in 1/3 seconds.
9814 // Teleport if we are aiming far off the visible line
9815 tab_bar->ScrollingSpeed = ImMax(lhs: tab_bar->ScrollingSpeed, rhs: 70.0f * g.FontSize);
9816 tab_bar->ScrollingSpeed = ImMax(lhs: tab_bar->ScrollingSpeed, ImFabs(tab_bar->ScrollingTarget - tab_bar->ScrollingAnim) / 0.3f);
9817 const bool teleport = (tab_bar->PrevFrameVisible + 1 < g.FrameCount) || (tab_bar->ScrollingTargetDistToVisibility > 10.0f * g.FontSize);
9818 tab_bar->ScrollingAnim = teleport ? tab_bar->ScrollingTarget : ImLinearSweep(current: tab_bar->ScrollingAnim, target: tab_bar->ScrollingTarget, speed: g.IO.DeltaTime * tab_bar->ScrollingSpeed);
9819 }
9820 else
9821 {
9822 tab_bar->ScrollingSpeed = 0.0f;
9823 }
9824 tab_bar->ScrollingRectMinX = tab_bar->BarRect.Min.x + sections[0].Width + sections[0].Spacing;
9825 tab_bar->ScrollingRectMaxX = tab_bar->BarRect.Max.x - sections[2].Width - sections[1].Spacing;
9826
9827 // Actual layout in host window (we don't do it in BeginTabBar() so as not to waste an extra frame)
9828 ImGuiWindow* window = g.CurrentWindow;
9829 window->DC.CursorPos = tab_bar->BarRect.Min;
9830 ItemSize(size: ImVec2(tab_bar->WidthAllTabs, tab_bar->BarRect.GetHeight()), text_baseline_y: tab_bar->FramePadding.y);
9831 window->DC.IdealMaxPos.x = ImMax(lhs: window->DC.IdealMaxPos.x, rhs: tab_bar->BarRect.Min.x + tab_bar->WidthAllTabsIdeal);
9832}
9833
9834// Dockable windows uses Name/ID in the global namespace. Non-dockable items use the ID stack.
9835static ImU32 ImGui::TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window)
9836{
9837 if (docked_window != NULL)
9838 {
9839 IM_UNUSED(tab_bar);
9840 IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_DockNode);
9841 ImGuiID id = docked_window->TabId;
9842 KeepAliveID(id);
9843 return id;
9844 }
9845 else
9846 {
9847 ImGuiWindow* window = GImGui->CurrentWindow;
9848 return window->GetID(str: label);
9849 }
9850}
9851
9852static float ImGui::TabBarCalcMaxTabWidth()
9853{
9854 ImGuiContext& g = *GImGui;
9855 return g.FontSize * 20.0f;
9856}
9857
9858ImGuiTabItem* ImGui::TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id)
9859{
9860 if (tab_id != 0)
9861 for (int n = 0; n < tab_bar->Tabs.Size; n++)
9862 if (tab_bar->Tabs[n].ID == tab_id)
9863 return &tab_bar->Tabs[n];
9864 return NULL;
9865}
9866
9867// Order = visible order, not submission order! (which is tab->BeginOrder)
9868ImGuiTabItem* ImGui::TabBarFindTabByOrder(ImGuiTabBar* tab_bar, int order)
9869{
9870 if (order < 0 || order >= tab_bar->Tabs.Size)
9871 return NULL;
9872 return &tab_bar->Tabs[order];
9873}
9874
9875// FIXME: See references to #2304 in TODO.txt
9876ImGuiTabItem* ImGui::TabBarFindMostRecentlySelectedTabForActiveWindow(ImGuiTabBar* tab_bar)
9877{
9878 ImGuiTabItem* most_recently_selected_tab = NULL;
9879 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
9880 {
9881 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
9882 if (most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected)
9883 if (tab->Window && tab->Window->WasActive)
9884 most_recently_selected_tab = tab;
9885 }
9886 return most_recently_selected_tab;
9887}
9888
9889ImGuiTabItem* ImGui::TabBarGetCurrentTab(ImGuiTabBar* tab_bar)
9890{
9891 if (tab_bar->LastTabItemIdx < 0 || tab_bar->LastTabItemIdx >= tab_bar->Tabs.Size)
9892 return NULL;
9893 return &tab_bar->Tabs[tab_bar->LastTabItemIdx];
9894}
9895
9896const char* ImGui::TabBarGetTabName(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
9897{
9898 if (tab->Window)
9899 return tab->Window->Name;
9900 if (tab->NameOffset == -1)
9901 return "N/A";
9902 IM_ASSERT(tab->NameOffset < tab_bar->TabsNames.Buf.Size);
9903 return tab_bar->TabsNames.Buf.Data + tab->NameOffset;
9904}
9905
9906// The purpose of this call is to register tab in advance so we can control their order at the time they appear.
9907// Otherwise calling this is unnecessary as tabs are appending as needed by the BeginTabItem() function.
9908void ImGui::TabBarAddTab(ImGuiTabBar* tab_bar, ImGuiTabItemFlags tab_flags, ImGuiWindow* window)
9909{
9910 ImGuiContext& g = *GImGui;
9911 IM_ASSERT(TabBarFindTabByID(tab_bar, window->TabId) == NULL);
9912 IM_ASSERT(g.CurrentTabBar != tab_bar); // Can't work while the tab bar is active as our tab doesn't have an X offset yet, in theory we could/should test something like (tab_bar->CurrFrameVisible < g.FrameCount) but we'd need to solve why triggers the commented early-out assert in BeginTabBarEx() (probably dock node going from implicit to explicit in same frame)
9913
9914 if (!window->HasCloseButton)
9915 tab_flags |= ImGuiTabItemFlags_NoCloseButton; // Set _NoCloseButton immediately because it will be used for first-frame width calculation.
9916
9917 ImGuiTabItem new_tab;
9918 new_tab.ID = window->TabId;
9919 new_tab.Flags = tab_flags;
9920 new_tab.LastFrameVisible = tab_bar->CurrFrameVisible; // Required so BeginTabBar() doesn't ditch the tab
9921 if (new_tab.LastFrameVisible == -1)
9922 new_tab.LastFrameVisible = g.FrameCount - 1;
9923 new_tab.Window = window; // Required so tab bar layout can compute the tab width before tab submission
9924 tab_bar->Tabs.push_back(v: new_tab);
9925}
9926
9927// The *TabId fields are already set by the docking system _before_ the actual TabItem was created, so we clear them regardless.
9928void ImGui::TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id)
9929{
9930 if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
9931 tab_bar->Tabs.erase(it: tab);
9932 if (tab_bar->VisibleTabId == tab_id) { tab_bar->VisibleTabId = 0; }
9933 if (tab_bar->SelectedTabId == tab_id) { tab_bar->SelectedTabId = 0; }
9934 if (tab_bar->NextSelectedTabId == tab_id) { tab_bar->NextSelectedTabId = 0; }
9935}
9936
9937// Called on manual closure attempt
9938void ImGui::TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
9939{
9940 if (tab->Flags & ImGuiTabItemFlags_Button)
9941 return; // A button appended with TabItemButton().
9942
9943 if ((tab->Flags & (ImGuiTabItemFlags_UnsavedDocument | ImGuiTabItemFlags_NoAssumedClosure)) == 0)
9944 {
9945 // This will remove a frame of lag for selecting another tab on closure.
9946 // 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
9947 tab->WantClose = true;
9948 if (tab_bar->VisibleTabId == tab->ID)
9949 {
9950 tab->LastFrameVisible = -1;
9951 tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = 0;
9952 }
9953 }
9954 else
9955 {
9956 // Actually select before expecting closure attempt (on an UnsavedDocument tab user is expect to e.g. show a popup)
9957 if (tab_bar->VisibleTabId != tab->ID)
9958 TabBarQueueFocus(tab_bar, tab);
9959 }
9960}
9961
9962static float ImGui::TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling)
9963{
9964 scrolling = ImMin(lhs: scrolling, rhs: tab_bar->WidthAllTabs - tab_bar->BarRect.GetWidth());
9965 return ImMax(lhs: scrolling, rhs: 0.0f);
9966}
9967
9968// Note: we may scroll to tab that are not selected! e.g. using keyboard arrow keys
9969static void ImGui::TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections)
9970{
9971 ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id);
9972 if (tab == NULL)
9973 return;
9974 if (tab->Flags & ImGuiTabItemFlags_SectionMask_)
9975 return;
9976
9977 ImGuiContext& g = *GImGui;
9978 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)
9979 int order = TabBarGetTabOrder(tab_bar, tab);
9980
9981 // Scrolling happens only in the central section (leading/trailing sections are not scrolling)
9982 float scrollable_width = TabBarCalcScrollableWidth(tab_bar, sections);
9983
9984 // We make all tabs positions all relative Sections[0].Width to make code simpler
9985 float tab_x1 = tab->Offset - sections[0].Width + (order > sections[0].TabCount - 1 ? -margin : 0.0f);
9986 float tab_x2 = tab->Offset - sections[0].Width + tab->Width + (order + 1 < tab_bar->Tabs.Size - sections[2].TabCount ? margin : 1.0f);
9987 tab_bar->ScrollingTargetDistToVisibility = 0.0f;
9988 if (tab_bar->ScrollingTarget > tab_x1 || (tab_x2 - tab_x1 >= scrollable_width))
9989 {
9990 // Scroll to the left
9991 tab_bar->ScrollingTargetDistToVisibility = ImMax(lhs: tab_bar->ScrollingAnim - tab_x2, rhs: 0.0f);
9992 tab_bar->ScrollingTarget = tab_x1;
9993 }
9994 else if (tab_bar->ScrollingTarget < tab_x2 - scrollable_width)
9995 {
9996 // Scroll to the right
9997 tab_bar->ScrollingTargetDistToVisibility = ImMax(lhs: (tab_x1 - scrollable_width) - tab_bar->ScrollingAnim, rhs: 0.0f);
9998 tab_bar->ScrollingTarget = tab_x2 - scrollable_width;
9999 }
10000}
10001
10002void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
10003{
10004 tab_bar->NextSelectedTabId = tab->ID;
10005}
10006
10007void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, const char* tab_name)
10008{
10009 IM_ASSERT((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0); // Only supported for manual/explicit tab bars
10010 ImGuiID tab_id = TabBarCalcTabID(tab_bar, label: tab_name, NULL);
10011 tab_bar->NextSelectedTabId = tab_id;
10012}
10013
10014void ImGui::TabBarQueueReorder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab, int offset)
10015{
10016 IM_ASSERT(offset != 0);
10017 IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
10018 tab_bar->ReorderRequestTabId = tab->ID;
10019 tab_bar->ReorderRequestOffset = (ImS16)offset;
10020}
10021
10022void ImGui::TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, ImGuiTabItem* src_tab, ImVec2 mouse_pos)
10023{
10024 ImGuiContext& g = *GImGui;
10025 IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
10026 if ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) == 0)
10027 return;
10028
10029 const bool is_central_section = (src_tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
10030 const float bar_offset = tab_bar->BarRect.Min.x - (is_central_section ? tab_bar->ScrollingTarget : 0);
10031
10032 // Count number of contiguous tabs we are crossing over
10033 const int dir = (bar_offset + src_tab->Offset) > mouse_pos.x ? -1 : +1;
10034 const int src_idx = tab_bar->Tabs.index_from_ptr(it: src_tab);
10035 int dst_idx = src_idx;
10036 for (int i = src_idx; i >= 0 && i < tab_bar->Tabs.Size; i += dir)
10037 {
10038 // Reordered tabs must share the same section
10039 const ImGuiTabItem* dst_tab = &tab_bar->Tabs[i];
10040 if (dst_tab->Flags & ImGuiTabItemFlags_NoReorder)
10041 break;
10042 if ((dst_tab->Flags & ImGuiTabItemFlags_SectionMask_) != (src_tab->Flags & ImGuiTabItemFlags_SectionMask_))
10043 break;
10044 dst_idx = i;
10045
10046 // Include spacing after tab, so when mouse cursor is between tabs we would not continue checking further tabs that are not hovered.
10047 const float x1 = bar_offset + dst_tab->Offset - g.Style.ItemInnerSpacing.x;
10048 const float x2 = bar_offset + dst_tab->Offset + dst_tab->Width + g.Style.ItemInnerSpacing.x;
10049 //GetForegroundDrawList()->AddRect(ImVec2(x1, tab_bar->BarRect.Min.y), ImVec2(x2, tab_bar->BarRect.Max.y), IM_COL32(255, 0, 0, 255));
10050 if ((dir < 0 && mouse_pos.x > x1) || (dir > 0 && mouse_pos.x < x2))
10051 break;
10052 }
10053
10054 if (dst_idx != src_idx)
10055 TabBarQueueReorder(tab_bar, tab: src_tab, offset: dst_idx - src_idx);
10056}
10057
10058bool ImGui::TabBarProcessReorder(ImGuiTabBar* tab_bar)
10059{
10060 ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_id: tab_bar->ReorderRequestTabId);
10061 if (tab1 == NULL || (tab1->Flags & ImGuiTabItemFlags_NoReorder))
10062 return false;
10063
10064 //IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_Reorderable); // <- this may happen when using debug tools
10065 int tab2_order = TabBarGetTabOrder(tab_bar, tab: tab1) + tab_bar->ReorderRequestOffset;
10066 if (tab2_order < 0 || tab2_order >= tab_bar->Tabs.Size)
10067 return false;
10068
10069 // Reordered tabs must share the same section
10070 // (Note: TabBarQueueReorderFromMousePos() also has a similar test but since we allow direct calls to TabBarQueueReorder() we do it here too)
10071 ImGuiTabItem* tab2 = &tab_bar->Tabs[tab2_order];
10072 if (tab2->Flags & ImGuiTabItemFlags_NoReorder)
10073 return false;
10074 if ((tab1->Flags & ImGuiTabItemFlags_SectionMask_) != (tab2->Flags & ImGuiTabItemFlags_SectionMask_))
10075 return false;
10076
10077 ImGuiTabItem item_tmp = *tab1;
10078 ImGuiTabItem* src_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 + 1 : tab2;
10079 ImGuiTabItem* dst_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 : tab2 + 1;
10080 const int move_count = (tab_bar->ReorderRequestOffset > 0) ? tab_bar->ReorderRequestOffset : -tab_bar->ReorderRequestOffset;
10081 memmove(dest: dst_tab, src: src_tab, n: move_count * sizeof(ImGuiTabItem));
10082 *tab2 = item_tmp;
10083
10084 if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings)
10085 MarkIniSettingsDirty();
10086 return true;
10087}
10088
10089static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar)
10090{
10091 ImGuiContext& g = *GImGui;
10092 ImGuiWindow* window = g.CurrentWindow;
10093
10094 const ImVec2 arrow_button_size(g.FontSize - 2.0f, g.FontSize + g.Style.FramePadding.y * 2.0f);
10095 const float scrolling_buttons_width = arrow_button_size.x * 2.0f;
10096
10097 const ImVec2 backup_cursor_pos = window->DC.CursorPos;
10098 //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));
10099
10100 int select_dir = 0;
10101 ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
10102 arrow_col.w *= 0.5f;
10103
10104 PushStyleColor(idx: ImGuiCol_Text, col: arrow_col);
10105 PushStyleColor(idx: ImGuiCol_Button, col: ImVec4(0, 0, 0, 0));
10106 PushItemFlag(option: ImGuiItemFlags_ButtonRepeat | ImGuiItemFlags_NoNav, enabled: true);
10107 const float backup_repeat_delay = g.IO.KeyRepeatDelay;
10108 const float backup_repeat_rate = g.IO.KeyRepeatRate;
10109 g.IO.KeyRepeatDelay = 0.250f;
10110 g.IO.KeyRepeatRate = 0.200f;
10111 float x = ImMax(lhs: tab_bar->BarRect.Min.x, rhs: tab_bar->BarRect.Max.x - scrolling_buttons_width);
10112 window->DC.CursorPos = ImVec2(x, tab_bar->BarRect.Min.y);
10113 if (ArrowButtonEx(str_id: "##<", dir: ImGuiDir_Left, size: arrow_button_size, flags: ImGuiButtonFlags_PressedOnClick))
10114 select_dir = -1;
10115 window->DC.CursorPos = ImVec2(x + arrow_button_size.x, tab_bar->BarRect.Min.y);
10116 if (ArrowButtonEx(str_id: "##>", dir: ImGuiDir_Right, size: arrow_button_size, flags: ImGuiButtonFlags_PressedOnClick))
10117 select_dir = +1;
10118 PopItemFlag();
10119 PopStyleColor(count: 2);
10120 g.IO.KeyRepeatRate = backup_repeat_rate;
10121 g.IO.KeyRepeatDelay = backup_repeat_delay;
10122
10123 ImGuiTabItem* tab_to_scroll_to = NULL;
10124 if (select_dir != 0)
10125 if (ImGuiTabItem* tab_item = TabBarFindTabByID(tab_bar, tab_id: tab_bar->SelectedTabId))
10126 {
10127 int selected_order = TabBarGetTabOrder(tab_bar, tab: tab_item);
10128 int target_order = selected_order + select_dir;
10129
10130 // Skip tab item buttons until another tab item is found or end is reached
10131 while (tab_to_scroll_to == NULL)
10132 {
10133 // If we are at the end of the list, still scroll to make our tab visible
10134 tab_to_scroll_to = &tab_bar->Tabs[(target_order >= 0 && target_order < tab_bar->Tabs.Size) ? target_order : selected_order];
10135
10136 // Cross through buttons
10137 // (even if first/last item is a button, return it so we can update the scroll)
10138 if (tab_to_scroll_to->Flags & ImGuiTabItemFlags_Button)
10139 {
10140 target_order += select_dir;
10141 selected_order += select_dir;
10142 tab_to_scroll_to = (target_order < 0 || target_order >= tab_bar->Tabs.Size) ? tab_to_scroll_to : NULL;
10143 }
10144 }
10145 }
10146 window->DC.CursorPos = backup_cursor_pos;
10147 tab_bar->BarRect.Max.x -= scrolling_buttons_width + 1.0f;
10148
10149 return tab_to_scroll_to;
10150}
10151
10152static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar)
10153{
10154 ImGuiContext& g = *GImGui;
10155 ImGuiWindow* window = g.CurrentWindow;
10156
10157 // We use g.Style.FramePadding.y to match the square ArrowButton size
10158 const float tab_list_popup_button_width = g.FontSize + g.Style.FramePadding.y;
10159 const ImVec2 backup_cursor_pos = window->DC.CursorPos;
10160 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x - g.Style.FramePadding.y, tab_bar->BarRect.Min.y);
10161 tab_bar->BarRect.Min.x += tab_list_popup_button_width;
10162
10163 ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
10164 arrow_col.w *= 0.5f;
10165 PushStyleColor(idx: ImGuiCol_Text, col: arrow_col);
10166 PushStyleColor(idx: ImGuiCol_Button, col: ImVec4(0, 0, 0, 0));
10167 bool open = BeginCombo(label: "##v", NULL, flags: ImGuiComboFlags_NoPreview | ImGuiComboFlags_HeightLargest);
10168 PopStyleColor(count: 2);
10169
10170 ImGuiTabItem* tab_to_select = NULL;
10171 if (open)
10172 {
10173 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
10174 {
10175 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
10176 if (tab->Flags & ImGuiTabItemFlags_Button)
10177 continue;
10178
10179 const char* tab_name = TabBarGetTabName(tab_bar, tab);
10180 if (Selectable(label: tab_name, selected: tab_bar->SelectedTabId == tab->ID))
10181 tab_to_select = tab;
10182 }
10183 EndCombo();
10184 }
10185
10186 window->DC.CursorPos = backup_cursor_pos;
10187 return tab_to_select;
10188}
10189
10190//-------------------------------------------------------------------------
10191// [SECTION] Widgets: BeginTabItem, EndTabItem, etc.
10192//-------------------------------------------------------------------------
10193// - BeginTabItem()
10194// - EndTabItem()
10195// - TabItemButton()
10196// - TabItemEx() [Internal]
10197// - SetTabItemClosed()
10198// - TabItemCalcSize() [Internal]
10199// - TabItemBackground() [Internal]
10200// - TabItemLabelAndCloseButton() [Internal]
10201//-------------------------------------------------------------------------
10202
10203bool ImGui::BeginTabItem(const char* label, bool* p_open, ImGuiTabItemFlags flags)
10204{
10205 ImGuiContext& g = *GImGui;
10206 ImGuiWindow* window = g.CurrentWindow;
10207 if (window->SkipItems)
10208 return false;
10209
10210 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10211 if (tab_bar == NULL)
10212 {
10213 IM_ASSERT_USER_ERROR(tab_bar, "Needs to be called between BeginTabBar() and EndTabBar()!");
10214 return false;
10215 }
10216 IM_ASSERT((flags & ImGuiTabItemFlags_Button) == 0); // BeginTabItem() Can't be used with button flags, use TabItemButton() instead!
10217
10218 bool ret = TabItemEx(tab_bar, label, p_open, flags, NULL);
10219 if (ret && !(flags & ImGuiTabItemFlags_NoPushId))
10220 {
10221 ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
10222 PushOverrideID(id: tab->ID); // We already hashed 'label' so push into the ID stack directly instead of doing another hash through PushID(label)
10223 }
10224 return ret;
10225}
10226
10227void ImGui::EndTabItem()
10228{
10229 ImGuiContext& g = *GImGui;
10230 ImGuiWindow* window = g.CurrentWindow;
10231 if (window->SkipItems)
10232 return;
10233
10234 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10235 if (tab_bar == NULL)
10236 {
10237 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
10238 return;
10239 }
10240 IM_ASSERT(tab_bar->LastTabItemIdx >= 0);
10241 ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
10242 if (!(tab->Flags & ImGuiTabItemFlags_NoPushId))
10243 PopID();
10244}
10245
10246bool ImGui::TabItemButton(const char* label, ImGuiTabItemFlags flags)
10247{
10248 ImGuiContext& g = *GImGui;
10249 ImGuiWindow* window = g.CurrentWindow;
10250 if (window->SkipItems)
10251 return false;
10252
10253 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10254 if (tab_bar == NULL)
10255 {
10256 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
10257 return false;
10258 }
10259 return TabItemEx(tab_bar, label, NULL, flags: flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder, NULL);
10260}
10261
10262void ImGui::TabItemSpacing(const char* str_id, ImGuiTabItemFlags flags, float width)
10263{
10264 ImGuiContext& g = *GImGui;
10265 ImGuiWindow* window = g.CurrentWindow;
10266 if (window->SkipItems)
10267 return;
10268
10269 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10270 if (tab_bar == NULL)
10271 {
10272 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
10273 return;
10274 }
10275 SetNextItemWidth(width);
10276 TabItemEx(tab_bar, label: str_id, NULL, flags: flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder | ImGuiTabItemFlags_Invisible, NULL);
10277}
10278
10279bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags, ImGuiWindow* docked_window)
10280{
10281 // Layout whole tab bar if not already done
10282 ImGuiContext& g = *GImGui;
10283 if (tab_bar->WantLayout)
10284 {
10285 ImGuiNextItemData backup_next_item_data = g.NextItemData;
10286 TabBarLayout(tab_bar);
10287 g.NextItemData = backup_next_item_data;
10288 }
10289 ImGuiWindow* window = g.CurrentWindow;
10290 if (window->SkipItems)
10291 return false;
10292
10293 const ImGuiStyle& style = g.Style;
10294 const ImGuiID id = TabBarCalcTabID(tab_bar, label, docked_window);
10295
10296 // If the user called us with *p_open == false, we early out and don't render.
10297 // 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.
10298 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
10299 if (p_open && !*p_open)
10300 {
10301 ItemAdd(bb: ImRect(), id, NULL, extra_flags: ImGuiItemFlags_NoNav);
10302 return false;
10303 }
10304
10305 IM_ASSERT(!p_open || !(flags & ImGuiTabItemFlags_Button));
10306 IM_ASSERT((flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)) != (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)); // Can't use both Leading and Trailing
10307
10308 // Store into ImGuiTabItemFlags_NoCloseButton, also honor ImGuiTabItemFlags_NoCloseButton passed by user (although not documented)
10309 if (flags & ImGuiTabItemFlags_NoCloseButton)
10310 p_open = NULL;
10311 else if (p_open == NULL)
10312 flags |= ImGuiTabItemFlags_NoCloseButton;
10313
10314 // Acquire tab data
10315 ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id: id);
10316 bool tab_is_new = false;
10317 if (tab == NULL)
10318 {
10319 tab_bar->Tabs.push_back(v: ImGuiTabItem());
10320 tab = &tab_bar->Tabs.back();
10321 tab->ID = id;
10322 tab_bar->TabsAddedNew = tab_is_new = true;
10323 }
10324 tab_bar->LastTabItemIdx = (ImS16)tab_bar->Tabs.index_from_ptr(it: tab);
10325
10326 // Calculate tab contents size
10327 ImVec2 size = TabItemCalcSize(label, has_close_button_or_unsaved_marker: (p_open != NULL) || (flags & ImGuiTabItemFlags_UnsavedDocument));
10328 tab->RequestedWidth = -1.0f;
10329 if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasWidth)
10330 size.x = tab->RequestedWidth = g.NextItemData.Width;
10331 if (tab_is_new)
10332 tab->Width = ImMax(lhs: 1.0f, rhs: size.x);
10333 tab->ContentWidth = size.x;
10334 tab->BeginOrder = tab_bar->TabsActiveCount++;
10335
10336 const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
10337 const bool tab_bar_focused = (tab_bar->Flags & ImGuiTabBarFlags_IsFocused) != 0;
10338 const bool tab_appearing = (tab->LastFrameVisible + 1 < g.FrameCount);
10339 const bool tab_just_unsaved = (flags & ImGuiTabItemFlags_UnsavedDocument) && !(tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
10340 const bool is_tab_button = (flags & ImGuiTabItemFlags_Button) != 0;
10341 tab->LastFrameVisible = g.FrameCount;
10342 tab->Flags = flags;
10343 tab->Window = docked_window;
10344
10345 // Append name _WITH_ the zero-terminator
10346 // (regular tabs are permitted in a DockNode tab bar, but window tabs not permitted in a non-DockNode tab bar)
10347 if (docked_window != NULL)
10348 {
10349 IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_DockNode);
10350 tab->NameOffset = -1;
10351 }
10352 else
10353 {
10354 tab->NameOffset = (ImS32)tab_bar->TabsNames.size();
10355 tab_bar->TabsNames.append(str: label, str_end: label + ImStrlen(s: label) + 1);
10356 }
10357
10358 // Update selected tab
10359 if (!is_tab_button)
10360 {
10361 if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0)
10362 if (!tab_bar_appearing || tab_bar->SelectedTabId == 0)
10363 TabBarQueueFocus(tab_bar, tab); // New tabs gets activated
10364 if ((flags & ImGuiTabItemFlags_SetSelected) && (tab_bar->SelectedTabId != id)) // _SetSelected can only be passed on explicit tab bar
10365 TabBarQueueFocus(tab_bar, tab);
10366 }
10367
10368 // Lock visibility
10369 // (Note: tab_contents_visible != tab_selected... because CTRL+TAB operations may preview some tabs without selecting them!)
10370 bool tab_contents_visible = (tab_bar->VisibleTabId == id);
10371 if (tab_contents_visible)
10372 tab_bar->VisibleTabWasSubmitted = true;
10373
10374 // On the very first frame of a tab bar we let first tab contents be visible to minimize appearing glitches
10375 if (!tab_contents_visible && tab_bar->SelectedTabId == 0 && tab_bar_appearing && docked_window == NULL)
10376 if (tab_bar->Tabs.Size == 1 && !(tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs))
10377 tab_contents_visible = true;
10378
10379 // Note that tab_is_new is not necessarily the same as tab_appearing! When a tab bar stops being submitted
10380 // and then gets submitted again, the tabs will have 'tab_appearing=true' but 'tab_is_new=false'.
10381 if (tab_appearing && (!tab_bar_appearing || tab_is_new))
10382 {
10383 ItemAdd(bb: ImRect(), id, NULL, extra_flags: ImGuiItemFlags_NoNav);
10384 if (is_tab_button)
10385 return false;
10386 return tab_contents_visible;
10387 }
10388
10389 if (tab_bar->SelectedTabId == id)
10390 tab->LastFrameSelected = g.FrameCount;
10391
10392 // Backup current layout position
10393 const ImVec2 backup_main_cursor_pos = window->DC.CursorPos;
10394
10395 // Layout
10396 const bool is_central_section = (tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
10397 size.x = tab->Width;
10398 if (is_central_section)
10399 window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_TRUNC(tab->Offset - tab_bar->ScrollingAnim), 0.0f);
10400 else
10401 window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(tab->Offset, 0.0f);
10402 ImVec2 pos = window->DC.CursorPos;
10403 ImRect bb(pos, pos + size);
10404
10405 // 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)
10406 const bool want_clip_rect = is_central_section && (bb.Min.x < tab_bar->ScrollingRectMinX || bb.Max.x > tab_bar->ScrollingRectMaxX);
10407 if (want_clip_rect)
10408 PushClipRect(clip_rect_min: ImVec2(ImMax(lhs: bb.Min.x, rhs: tab_bar->ScrollingRectMinX), bb.Min.y - 1), clip_rect_max: ImVec2(tab_bar->ScrollingRectMaxX, bb.Max.y), intersect_with_current_clip_rect: true);
10409
10410 ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos;
10411 ItemSize(size: bb.GetSize(), text_baseline_y: style.FramePadding.y);
10412 window->DC.CursorMaxPos = backup_cursor_max_pos;
10413
10414 if (!ItemAdd(bb, id))
10415 {
10416 if (want_clip_rect)
10417 PopClipRect();
10418 window->DC.CursorPos = backup_main_cursor_pos;
10419 return tab_contents_visible;
10420 }
10421
10422 // Click to Select a tab
10423 ImGuiButtonFlags button_flags = ((is_tab_button ? ImGuiButtonFlags_PressedOnClickRelease : ImGuiButtonFlags_PressedOnClick) | ImGuiButtonFlags_AllowOverlap);
10424 if (g.DragDropActive && !g.DragDropPayload.IsDataType(IMGUI_PAYLOAD_TYPE_WINDOW)) // FIXME: May be an opt-in property of the payload to disable this
10425 button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
10426 bool hovered, held, pressed;
10427 if (flags & ImGuiTabItemFlags_Invisible)
10428 hovered = held = pressed = false;
10429 else
10430 pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags: button_flags);
10431 if (pressed && !is_tab_button)
10432 TabBarQueueFocus(tab_bar, tab);
10433
10434 // Transfer active id window so the active id is not owned by the dock host (as StartMouseMovingWindow()
10435 // will only do it on the drag). This allows FocusWindow() to be more conservative in how it clears active id.
10436 if (held && docked_window && g.ActiveId == id && g.ActiveIdIsJustActivated)
10437 g.ActiveIdWindow = docked_window;
10438
10439 // Drag and drop a single floating window node moves it
10440 ImGuiDockNode* node = docked_window ? docked_window->DockNode : NULL;
10441 const bool single_floating_window_node = node && node->IsFloatingNode() && (node->Windows.Size == 1);
10442 if (held && single_floating_window_node && IsMouseDragging(button: 0, lock_threshold: 0.0f))
10443 {
10444 // Move
10445 StartMouseMovingWindow(window: docked_window);
10446 }
10447 else if (held && !tab_appearing && IsMouseDragging(button: 0))
10448 {
10449 // Drag and drop: re-order tabs
10450 int drag_dir = 0;
10451 float drag_distance_from_edge_x = 0.0f;
10452 if (!g.DragDropActive && ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (docked_window != NULL)))
10453 {
10454 // While moving a tab it will jump on the other side of the mouse, so we also test for MouseDelta.x
10455 if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < bb.Min.x)
10456 {
10457 drag_dir = -1;
10458 drag_distance_from_edge_x = bb.Min.x - g.IO.MousePos.x;
10459 TabBarQueueReorderFromMousePos(tab_bar, src_tab: tab, mouse_pos: g.IO.MousePos);
10460 }
10461 else if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > bb.Max.x)
10462 {
10463 drag_dir = +1;
10464 drag_distance_from_edge_x = g.IO.MousePos.x - bb.Max.x;
10465 TabBarQueueReorderFromMousePos(tab_bar, src_tab: tab, mouse_pos: g.IO.MousePos);
10466 }
10467 }
10468
10469 // Extract a Dockable window out of it's tab bar
10470 const bool can_undock = docked_window != NULL && !(docked_window->Flags & ImGuiWindowFlags_NoMove) && !(node->MergedFlags & ImGuiDockNodeFlags_NoUndocking);
10471 if (can_undock)
10472 {
10473 // We use a variable threshold to distinguish dragging tabs within a tab bar and extracting them out of the tab bar
10474 bool undocking_tab = (g.DragDropActive && g.DragDropPayload.SourceId == id);
10475 if (!undocking_tab) //&& (!g.IO.ConfigDockingWithShift || g.IO.KeyShift)
10476 {
10477 float threshold_base = g.FontSize;
10478 float threshold_x = (threshold_base * 2.2f);
10479 float threshold_y = (threshold_base * 1.5f) + ImClamp(v: (ImFabs(g.IO.MouseDragMaxDistanceAbs[0].x) - threshold_base * 2.0f) * 0.20f, mn: 0.0f, mx: threshold_base * 4.0f);
10480 //GetForegroundDrawList()->AddRect(ImVec2(bb.Min.x - threshold_x, bb.Min.y - threshold_y), ImVec2(bb.Max.x + threshold_x, bb.Max.y + threshold_y), IM_COL32_WHITE); // [DEBUG]
10481
10482 float distance_from_edge_y = ImMax(lhs: bb.Min.y - g.IO.MousePos.y, rhs: g.IO.MousePos.y - bb.Max.y);
10483 if (distance_from_edge_y >= threshold_y)
10484 undocking_tab = true;
10485 if (drag_distance_from_edge_x > threshold_x)
10486 if ((drag_dir < 0 && TabBarGetTabOrder(tab_bar, tab) == 0) || (drag_dir > 0 && TabBarGetTabOrder(tab_bar, tab) == tab_bar->Tabs.Size - 1))
10487 undocking_tab = true;
10488 }
10489
10490 if (undocking_tab)
10491 {
10492 // Undock
10493 // FIXME: refactor to share more code with e.g. StartMouseMovingWindow
10494 DockContextQueueUndockWindow(ctx: &g, window: docked_window);
10495 g.MovingWindow = docked_window;
10496 SetActiveID(id: g.MovingWindow->MoveId, window: g.MovingWindow);
10497 g.ActiveIdClickOffset -= g.MovingWindow->Pos - bb.Min;
10498 g.ActiveIdNoClearOnFocusLoss = true;
10499 SetActiveIdUsingAllKeyboardKeys();
10500 }
10501 }
10502 }
10503
10504#if 0
10505 if (hovered && g.HoveredIdNotActiveTimer > TOOLTIP_DELAY && bb.GetWidth() < tab->ContentWidth)
10506 {
10507 // Enlarge tab display when hovering
10508 bb.Max.x = bb.Min.x + IM_TRUNC(ImLerp(bb.GetWidth(), tab->ContentWidth, ImSaturate((g.HoveredIdNotActiveTimer - 0.40f) * 6.0f)));
10509 display_draw_list = GetForegroundDrawList(window);
10510 TabItemBackground(display_draw_list, bb, flags, GetColorU32(ImGuiCol_TitleBgActive));
10511 }
10512#endif
10513
10514 // Render tab shape
10515 const bool is_visible = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) && !(flags & ImGuiTabItemFlags_Invisible);
10516 if (is_visible)
10517 {
10518 ImDrawList* display_draw_list = window->DrawList;
10519 const ImU32 tab_col = GetColorU32(idx: (held || hovered) ? ImGuiCol_TabHovered : tab_contents_visible ? (tab_bar_focused ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected) : (tab_bar_focused ? ImGuiCol_Tab : ImGuiCol_TabDimmed));
10520 TabItemBackground(draw_list: display_draw_list, bb, flags, col: tab_col);
10521 if (tab_contents_visible && (tab_bar->Flags & ImGuiTabBarFlags_DrawSelectedOverline) && style.TabBarOverlineSize > 0.0f)
10522 {
10523 // Might be moved to TabItemBackground() ?
10524 ImVec2 tl = bb.GetTL() + ImVec2(0, 1.0f * g.CurrentDpiScale);
10525 ImVec2 tr = bb.GetTR() + ImVec2(0, 1.0f * g.CurrentDpiScale);
10526 ImU32 overline_col = GetColorU32(idx: tab_bar_focused ? ImGuiCol_TabSelectedOverline : ImGuiCol_TabDimmedSelectedOverline);
10527 if (style.TabRounding > 0.0f)
10528 {
10529 float rounding = style.TabRounding;
10530 display_draw_list->PathArcToFast(center: tl + ImVec2(+rounding, +rounding), radius: rounding, a_min_of_12: 7, a_max_of_12: 9);
10531 display_draw_list->PathArcToFast(center: tr + ImVec2(-rounding, +rounding), radius: rounding, a_min_of_12: 9, a_max_of_12: 11);
10532 display_draw_list->PathStroke(col: overline_col, flags: 0, thickness: style.TabBarOverlineSize);
10533 }
10534 else
10535 {
10536 display_draw_list->AddLine(p1: tl - ImVec2(0.5f, 0.5f), p2: tr - ImVec2(0.5f, 0.5f), col: overline_col, thickness: style.TabBarOverlineSize);
10537 }
10538 }
10539 RenderNavCursor(bb, id);
10540
10541 // Select with right mouse button. This is so the common idiom for context menu automatically highlight the current widget.
10542 const bool hovered_unblocked = IsItemHovered(flags: ImGuiHoveredFlags_AllowWhenBlockedByPopup);
10543 if (tab_bar->SelectedTabId != tab->ID && hovered_unblocked && (IsMouseClicked(button: 1) || IsMouseReleased(button: 1)) && !is_tab_button)
10544 TabBarQueueFocus(tab_bar, tab);
10545
10546 if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)
10547 flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton;
10548
10549 // Render tab label, process close button
10550 const ImGuiID close_button_id = p_open ? GetIDWithSeed(str_id_begin: "#CLOSE", NULL, seed: docked_window ? docked_window->ID : id) : 0;
10551 bool just_closed;
10552 bool text_clipped;
10553 TabItemLabelAndCloseButton(draw_list: display_draw_list, bb, flags: tab_just_unsaved ? (flags & ~ImGuiTabItemFlags_UnsavedDocument) : flags, frame_padding: tab_bar->FramePadding, label, tab_id: id, close_button_id, is_contents_visible: tab_contents_visible, out_just_closed: &just_closed, out_text_clipped: &text_clipped);
10554 if (just_closed && p_open != NULL)
10555 {
10556 *p_open = false;
10557 TabBarCloseTab(tab_bar, tab);
10558 }
10559
10560 // Forward Hovered state so IsItemHovered() after Begin() can work (even though we are technically hovering our parent)
10561 // That state is copied to window->DockTabItemStatusFlags by our caller.
10562 if (docked_window && (hovered || g.HoveredId == close_button_id))
10563 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow;
10564
10565 // Tooltip
10566 // (Won't work over the close button because ItemOverlap systems messes up with HoveredIdTimer-> seems ok)
10567 // (We test IsItemHovered() to discard e.g. when another item is active or drag and drop over the tab bar, which g.HoveredId ignores)
10568 // FIXME: This is a mess.
10569 // FIXME: We may want disabled tab to still display the tooltip?
10570 if (text_clipped && g.HoveredId == id && !held)
10571 if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip) && !(tab->Flags & ImGuiTabItemFlags_NoTooltip))
10572 SetItemTooltip("%.*s", (int)(FindRenderedTextEnd(text: label) - label), label);
10573 }
10574
10575 // Restore main window position so user can draw there
10576 if (want_clip_rect)
10577 PopClipRect();
10578 window->DC.CursorPos = backup_main_cursor_pos;
10579
10580 IM_ASSERT(!is_tab_button || !(tab_bar->SelectedTabId == tab->ID && is_tab_button)); // TabItemButton should not be selected
10581 if (is_tab_button)
10582 return pressed;
10583 return tab_contents_visible;
10584}
10585
10586// [Public] This is call is 100% optional but it allows to remove some one-frame glitches when a tab has been unexpectedly removed.
10587// To use it to need to call the function SetTabItemClosed() between BeginTabBar() and EndTabBar().
10588// Tabs closed by the close button will automatically be flagged to avoid this issue.
10589void ImGui::SetTabItemClosed(const char* label)
10590{
10591 ImGuiContext& g = *GImGui;
10592 bool is_within_manual_tab_bar = g.CurrentTabBar && !(g.CurrentTabBar->Flags & ImGuiTabBarFlags_DockNode);
10593 if (is_within_manual_tab_bar)
10594 {
10595 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10596 ImGuiID tab_id = TabBarCalcTabID(tab_bar, label, NULL);
10597 if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
10598 tab->WantClose = true; // Will be processed by next call to TabBarLayout()
10599 }
10600 else if (ImGuiWindow* window = FindWindowByName(name: label))
10601 {
10602 if (window->DockIsActive)
10603 if (ImGuiDockNode* node = window->DockNode)
10604 {
10605 ImGuiID tab_id = TabBarCalcTabID(tab_bar: node->TabBar, label, docked_window: window);
10606 TabBarRemoveTab(tab_bar: node->TabBar, tab_id);
10607 window->DockTabWantClose = true;
10608 }
10609 }
10610}
10611
10612ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button_or_unsaved_marker)
10613{
10614 ImGuiContext& g = *GImGui;
10615 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
10616 ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x, label_size.y + g.Style.FramePadding.y * 2.0f);
10617 if (has_close_button_or_unsaved_marker)
10618 size.x += g.Style.FramePadding.x + (g.Style.ItemInnerSpacing.x + g.FontSize); // We use Y intentionally to fit the close button circle.
10619 else
10620 size.x += g.Style.FramePadding.x + 1.0f;
10621 return ImVec2(ImMin(lhs: size.x, rhs: TabBarCalcMaxTabWidth()), size.y);
10622}
10623
10624ImVec2 ImGui::TabItemCalcSize(ImGuiWindow* window)
10625{
10626 return TabItemCalcSize(label: window->Name, has_close_button_or_unsaved_marker: window->HasCloseButton || (window->Flags & ImGuiWindowFlags_UnsavedDocument));
10627}
10628
10629void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImU32 col)
10630{
10631 // 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.
10632 ImGuiContext& g = *GImGui;
10633 const float width = bb.GetWidth();
10634 IM_UNUSED(flags);
10635 IM_ASSERT(width > 0.0f);
10636 const float rounding = ImMax(lhs: 0.0f, rhs: ImMin(lhs: (flags & ImGuiTabItemFlags_Button) ? g.Style.FrameRounding : g.Style.TabRounding, rhs: width * 0.5f - 1.0f));
10637 const float y1 = bb.Min.y + 1.0f;
10638 const float y2 = bb.Max.y - g.Style.TabBarBorderSize;
10639 draw_list->PathLineTo(pos: ImVec2(bb.Min.x, y2));
10640 draw_list->PathArcToFast(center: ImVec2(bb.Min.x + rounding, y1 + rounding), radius: rounding, a_min_of_12: 6, a_max_of_12: 9);
10641 draw_list->PathArcToFast(center: ImVec2(bb.Max.x - rounding, y1 + rounding), radius: rounding, a_min_of_12: 9, a_max_of_12: 12);
10642 draw_list->PathLineTo(pos: ImVec2(bb.Max.x, y2));
10643 draw_list->PathFillConvex(col);
10644 if (g.Style.TabBorderSize > 0.0f)
10645 {
10646 draw_list->PathLineTo(pos: ImVec2(bb.Min.x + 0.5f, y2));
10647 draw_list->PathArcToFast(center: ImVec2(bb.Min.x + rounding + 0.5f, y1 + rounding + 0.5f), radius: rounding, a_min_of_12: 6, a_max_of_12: 9);
10648 draw_list->PathArcToFast(center: ImVec2(bb.Max.x - rounding - 0.5f, y1 + rounding + 0.5f), radius: rounding, a_min_of_12: 9, a_max_of_12: 12);
10649 draw_list->PathLineTo(pos: ImVec2(bb.Max.x - 0.5f, y2));
10650 draw_list->PathStroke(col: GetColorU32(idx: ImGuiCol_Border), flags: 0, thickness: g.Style.TabBorderSize);
10651 }
10652}
10653
10654// Render text label (with custom clipping) + Unsaved Document marker + Close Button logic
10655// We tend to lock style.FramePadding for a given tab-bar, hence the 'frame_padding' parameter.
10656void 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)
10657{
10658 ImGuiContext& g = *GImGui;
10659 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
10660
10661 if (out_just_closed)
10662 *out_just_closed = false;
10663 if (out_text_clipped)
10664 *out_text_clipped = false;
10665
10666 if (bb.GetWidth() <= 1.0f)
10667 return;
10668
10669 // In Style V2 we'll have full override of all colors per state (e.g. focused, selected)
10670 // But right now if you want to alter text color of tabs this is what you need to do.
10671#if 0
10672 const float backup_alpha = g.Style.Alpha;
10673 if (!is_contents_visible)
10674 g.Style.Alpha *= 0.7f;
10675#endif
10676
10677 // Render text label (with clipping + alpha gradient) + unsaved marker
10678 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);
10679
10680 // Return clipped state ignoring the close button
10681 if (out_text_clipped)
10682 {
10683 *out_text_clipped = (text_ellipsis_clip_bb.Min.x + label_size.x) > text_ellipsis_clip_bb.Max.x;
10684 //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));
10685 }
10686
10687 const float button_sz = g.FontSize;
10688 const ImVec2 button_pos(ImMax(lhs: bb.Min.x, rhs: bb.Max.x - frame_padding.x - button_sz), bb.Min.y + frame_padding.y);
10689
10690 // Close Button & Unsaved Marker
10691 // We are relying on a subtle and confusing distinction between 'hovered' and 'g.HoveredId' which happens because we are using ImGuiButtonFlags_AllowOverlapMode + SetItemAllowOverlap()
10692 // 'hovered' will be true when hovering the Tab but NOT when hovering the close button
10693 // 'g.HoveredId==id' will be true when hovering the Tab including when hovering the close button
10694 // 'g.ActiveId==close_button_id' will be true when we are holding on the close button, in which case both hovered booleans are false
10695 bool close_button_pressed = false;
10696 bool close_button_visible = false;
10697 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.
10698
10699 if (close_button_id != 0)
10700 {
10701 if (is_contents_visible)
10702 close_button_visible = (g.Style.TabCloseButtonMinWidthSelected < 0.0f) ? true : (is_hovered && bb.GetWidth() >= ImMax(lhs: button_sz, rhs: g.Style.TabCloseButtonMinWidthSelected));
10703 else
10704 close_button_visible = (g.Style.TabCloseButtonMinWidthUnselected < 0.0f) ? true : (is_hovered && bb.GetWidth() >= ImMax(lhs: button_sz, rhs: g.Style.TabCloseButtonMinWidthUnselected));
10705 }
10706
10707 // When tabs/document is unsaved, the unsaved marker takes priority over the close button.
10708 const bool unsaved_marker_visible = (flags & ImGuiTabItemFlags_UnsavedDocument) != 0 && (button_pos.x + button_sz <= bb.Max.x) && (!close_button_visible || !is_hovered);
10709 if (unsaved_marker_visible)
10710 {
10711 const ImRect bullet_bb(button_pos, button_pos + ImVec2(button_sz, button_sz));
10712 RenderBullet(draw_list, pos: bullet_bb.GetCenter(), col: GetColorU32(idx: ImGuiCol_Text));
10713 }
10714 else if (close_button_visible)
10715 {
10716 ImGuiLastItemData last_item_backup = g.LastItemData;
10717 if (CloseButton(id: close_button_id, pos: button_pos))
10718 close_button_pressed = true;
10719 g.LastItemData = last_item_backup;
10720
10721 // Close with middle mouse button
10722 if (is_hovered && !(flags & ImGuiTabItemFlags_NoCloseWithMiddleMouseButton) && IsMouseClicked(button: 2))
10723 close_button_pressed = true;
10724 }
10725
10726 // This is all rather complicated
10727 // (the main idea is that because the close button only appears on hover, we don't want it to alter the ellipsis position)
10728 // 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..
10729 float ellipsis_max_x = text_ellipsis_clip_bb.Max.x;
10730 if (close_button_visible || unsaved_marker_visible)
10731 {
10732 const bool visible_without_hover = unsaved_marker_visible || (is_contents_visible ? g.Style.TabCloseButtonMinWidthSelected : g.Style.TabCloseButtonMinWidthUnselected) < 0.0f;
10733 if (visible_without_hover)
10734 {
10735 text_ellipsis_clip_bb.Max.x -= button_sz * 0.90f;
10736 ellipsis_max_x -= button_sz * 0.90f;
10737 }
10738 else
10739 {
10740 text_ellipsis_clip_bb.Max.x -= button_sz * 1.00f;
10741 }
10742 }
10743 LogSetNextTextDecoration(prefix: "/", suffix: "\\");
10744 RenderTextEllipsis(draw_list, pos_min: text_ellipsis_clip_bb.Min, pos_max: text_ellipsis_clip_bb.Max, ellipsis_max_x, text: label, NULL, text_size_if_known: &label_size);
10745
10746#if 0
10747 if (!is_contents_visible)
10748 g.Style.Alpha = backup_alpha;
10749#endif
10750
10751 if (out_just_closed)
10752 *out_just_closed = close_button_pressed;
10753}
10754
10755
10756#endif // #ifndef IMGUI_DISABLE
10757

source code of imgui/imgui_widgets.cpp