1// dear imgui, v1.91b
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::LabelText(const char* label, const char* fmt, ...)
343{
344 va_list args;
345 va_start(args, fmt);
346 LabelTextV(label, fmt, args);
347 va_end(args);
348}
349
350// Add a label+text combo aligned to other label+value widgets
351void ImGui::LabelTextV(const char* label, const char* fmt, va_list args)
352{
353 ImGuiWindow* window = GetCurrentWindow();
354 if (window->SkipItems)
355 return;
356
357 ImGuiContext& g = *GImGui;
358 const ImGuiStyle& style = g.Style;
359 const float w = CalcItemWidth();
360
361 const char* value_text_begin, *value_text_end;
362 ImFormatStringToTempBufferV(out_buf: &value_text_begin, out_buf_end: &value_text_end, fmt, args);
363 const ImVec2 value_size = CalcTextSize(text: value_text_begin, text_end: value_text_end, hide_text_after_double_hash: false);
364 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
365
366 const ImVec2 pos = window->DC.CursorPos;
367 const ImRect value_bb(pos, pos + ImVec2(w, value_size.y + style.FramePadding.y * 2));
368 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));
369 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
370 if (!ItemAdd(bb: total_bb, id: 0))
371 return;
372
373 // Render
374 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));
375 if (label_size.x > 0.0f)
376 RenderText(pos: ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), text: label);
377}
378
379void ImGui::BulletText(const char* fmt, ...)
380{
381 va_list args;
382 va_start(args, fmt);
383 BulletTextV(fmt, args);
384 va_end(args);
385}
386
387// Text with a little bullet aligned to the typical tree node.
388void ImGui::BulletTextV(const char* fmt, va_list args)
389{
390 ImGuiWindow* window = GetCurrentWindow();
391 if (window->SkipItems)
392 return;
393
394 ImGuiContext& g = *GImGui;
395 const ImGuiStyle& style = g.Style;
396
397 const char* text_begin, *text_end;
398 ImFormatStringToTempBufferV(out_buf: &text_begin, out_buf_end: &text_end, fmt, args);
399 const ImVec2 label_size = CalcTextSize(text: text_begin, text_end, hide_text_after_double_hash: false);
400 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
401 ImVec2 pos = window->DC.CursorPos;
402 pos.y += window->DC.CurrLineTextBaseOffset;
403 ItemSize(size: total_size, text_baseline_y: 0.0f);
404 const ImRect bb(pos, pos + total_size);
405 if (!ItemAdd(bb, id: 0))
406 return;
407
408 // Render
409 ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
410 RenderBullet(draw_list: window->DrawList, pos: bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, g.FontSize * 0.5f), col: text_col);
411 RenderText(pos: bb.Min + ImVec2(g.FontSize + style.FramePadding.x * 2, 0.0f), text: text_begin, text_end, hide_text_after_hash: false);
412}
413
414//-------------------------------------------------------------------------
415// [SECTION] Widgets: Main
416//-------------------------------------------------------------------------
417// - ButtonBehavior() [Internal]
418// - Button()
419// - SmallButton()
420// - InvisibleButton()
421// - ArrowButton()
422// - CloseButton() [Internal]
423// - CollapseButton() [Internal]
424// - GetWindowScrollbarID() [Internal]
425// - GetWindowScrollbarRect() [Internal]
426// - Scrollbar() [Internal]
427// - ScrollbarEx() [Internal]
428// - Image()
429// - ImageButton()
430// - Checkbox()
431// - CheckboxFlagsT() [Internal]
432// - CheckboxFlags()
433// - RadioButton()
434// - ProgressBar()
435// - Bullet()
436// - Hyperlink()
437//-------------------------------------------------------------------------
438
439// The ButtonBehavior() function is key to many interactions and used by many/most widgets.
440// Because we handle so many cases (keyboard/gamepad navigation, drag and drop) and many specific behavior (via ImGuiButtonFlags_),
441// this code is a little complex.
442// By far the most common path is interacting with the Mouse using the default ImGuiButtonFlags_PressedOnClickRelease button behavior.
443// See the series of events below and the corresponding state reported by dear imgui:
444//------------------------------------------------------------------------------------------------------------------------------------------------
445// with PressedOnClickRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
446// Frame N+0 (mouse is outside bb) - - - - - -
447// Frame N+1 (mouse moves inside bb) - true - - - -
448// Frame N+2 (mouse button is down) - true true true - true
449// Frame N+3 (mouse button is down) - true true - - -
450// Frame N+4 (mouse moves outside bb) - - true - - -
451// Frame N+5 (mouse moves inside bb) - true true - - -
452// Frame N+6 (mouse button is released) true true - - true -
453// Frame N+7 (mouse button is released) - true - - - -
454// Frame N+8 (mouse moves outside bb) - - - - - -
455//------------------------------------------------------------------------------------------------------------------------------------------------
456// with PressedOnClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
457// Frame N+2 (mouse button is down) true true true true - true
458// Frame N+3 (mouse button is down) - true true - - -
459// Frame N+6 (mouse button is released) - true - - true -
460// Frame N+7 (mouse button is released) - true - - - -
461//------------------------------------------------------------------------------------------------------------------------------------------------
462// with PressedOnRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
463// Frame N+2 (mouse button is down) - true - - - true
464// Frame N+3 (mouse button is down) - true - - - -
465// Frame N+6 (mouse button is released) true true - - - -
466// Frame N+7 (mouse button is released) - true - - - -
467//------------------------------------------------------------------------------------------------------------------------------------------------
468// with PressedOnDoubleClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
469// Frame N+0 (mouse button is down) - true - - - true
470// Frame N+1 (mouse button is down) - true - - - -
471// Frame N+2 (mouse button is released) - true - - - -
472// Frame N+3 (mouse button is released) - true - - - -
473// Frame N+4 (mouse button is down) true true true true - true
474// Frame N+5 (mouse button is down) - true true - - -
475// Frame N+6 (mouse button is released) - true - - true -
476// Frame N+7 (mouse button is released) - true - - - -
477//------------------------------------------------------------------------------------------------------------------------------------------------
478// Note that some combinations are supported,
479// - PressedOnDragDropHold can generally be associated with any flag.
480// - PressedOnDoubleClick can be associated by PressedOnClickRelease/PressedOnRelease, in which case the second release event won't be reported.
481//------------------------------------------------------------------------------------------------------------------------------------------------
482// The behavior of the return-value changes when ImGuiItemFlags_ButtonRepeat is set:
483// Repeat+ Repeat+ Repeat+ Repeat+
484// PressedOnClickRelease PressedOnClick PressedOnRelease PressedOnDoubleClick
485//-------------------------------------------------------------------------------------------------------------------------------------------------
486// Frame N+0 (mouse button is down) - true - true
487// ... - - - -
488// Frame N + RepeatDelay true true - true
489// ... - - - -
490// Frame N + RepeatDelay + RepeatRate*N true true - true
491//-------------------------------------------------------------------------------------------------------------------------------------------------
492
493// - FIXME: For refactor we could output flags, incl mouse hovered vs nav keyboard vs nav triggered etc.
494// And better standardize how widgets use 'GetColor32((held && hovered) ? ... : hovered ? ...)' vs 'GetColor32(held ? ... : hovered ? ...);'
495// For mouse feedback we typically prefer the 'held && hovered' test, but for nav feedback not always. Outputting hovered=true on Activation may be misleading.
496// - Since v1.91.2 (Sept 2024) we included io.ConfigDebugHighlightIdConflicts feature.
497// One idiom which was previously valid which will now emit a warning is when using multiple overlayed ButtonBehavior()
498// with same ID and different MouseButton (see #8030). You can fix it by:
499// (1) switching to use a single ButtonBehavior() with multiple _MouseButton flags.
500// or (2) surrounding those calls with PushItemFlag(ImGuiItemFlags_AllowDuplicateId, true); ... PopItemFlag()
501bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags)
502{
503 ImGuiContext& g = *GImGui;
504 ImGuiWindow* window = GetCurrentWindow();
505
506 // Default behavior inherited from item flags
507 // Note that _both_ ButtonFlags and ItemFlags are valid sources, so copy one into the item_flags and only check that.
508 ImGuiItemFlags item_flags = (g.LastItemData.ID == id ? g.LastItemData.ItemFlags : g.CurrentItemFlags);
509 if (flags & ImGuiButtonFlags_AllowOverlap)
510 item_flags |= ImGuiItemFlags_AllowOverlap;
511
512 // Default only reacts to left mouse button
513 if ((flags & ImGuiButtonFlags_MouseButtonMask_) == 0)
514 flags |= ImGuiButtonFlags_MouseButtonLeft;
515
516 // Default behavior requires click + release inside bounding box
517 if ((flags & ImGuiButtonFlags_PressedOnMask_) == 0)
518 flags |= (item_flags & ImGuiItemFlags_ButtonRepeat) ? ImGuiButtonFlags_PressedOnClick : ImGuiButtonFlags_PressedOnDefault_;
519
520 ImGuiWindow* backup_hovered_window = g.HoveredWindow;
521 const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredWindow && g.HoveredWindow->RootWindowDockTree == window->RootWindowDockTree;
522 if (flatten_hovered_children)
523 g.HoveredWindow = window;
524
525#ifdef IMGUI_ENABLE_TEST_ENGINE
526 // Alternate registration spot, for when caller didn't use ItemAdd()
527 if (g.LastItemData.ID != id)
528 IMGUI_TEST_ENGINE_ITEM_ADD(id, bb, NULL);
529#endif
530
531 bool pressed = false;
532 bool hovered = ItemHoverable(bb, id, item_flags);
533
534 // Special mode for Drag and Drop where holding button pressed for a long time while dragging another item triggers the button
535 if (g.DragDropActive && (flags & ImGuiButtonFlags_PressedOnDragDropHold) && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers))
536 if (IsItemHovered(flags: ImGuiHoveredFlags_AllowWhenBlockedByActiveItem))
537 {
538 hovered = true;
539 SetHoveredID(id);
540 if (g.HoveredIdTimer - g.IO.DeltaTime <= DRAGDROP_HOLD_TO_OPEN_TIMER && g.HoveredIdTimer >= DRAGDROP_HOLD_TO_OPEN_TIMER)
541 {
542 pressed = true;
543 g.DragDropHoldJustPressedId = id;
544 FocusWindow(window);
545 }
546 }
547
548 if (flatten_hovered_children)
549 g.HoveredWindow = backup_hovered_window;
550
551 // Mouse handling
552 const ImGuiID test_owner_id = (flags & ImGuiButtonFlags_NoTestKeyOwner) ? ImGuiKeyOwner_Any : id;
553 if (hovered)
554 {
555 IM_ASSERT(id != 0); // Lazily check inside rare path.
556
557 // Poll mouse buttons
558 // - 'mouse_button_clicked' is generally carried into ActiveIdMouseButton when setting ActiveId.
559 // - Technically we only need some values in one code path, but since this is gated by hovered test this is fine.
560 int mouse_button_clicked = -1;
561 int mouse_button_released = -1;
562 for (int button = 0; button < 3; button++)
563 if (flags & (ImGuiButtonFlags_MouseButtonLeft << button)) // Handle ImGuiButtonFlags_MouseButtonRight and ImGuiButtonFlags_MouseButtonMiddle here.
564 {
565 if (IsMouseClicked(button, flags: ImGuiInputFlags_None, owner_id: test_owner_id) && mouse_button_clicked == -1) { mouse_button_clicked = button; }
566 if (IsMouseReleased(button, owner_id: test_owner_id) && mouse_button_released == -1) { mouse_button_released = button; }
567 }
568
569 // Process initial action
570 const bool mods_ok = !(flags & ImGuiButtonFlags_NoKeyModsAllowed) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt);
571 if (mods_ok)
572 {
573 if (mouse_button_clicked != -1 && g.ActiveId != id)
574 {
575 if (!(flags & ImGuiButtonFlags_NoSetKeyOwner))
576 SetKeyOwner(key: MouseButtonToKey(button: mouse_button_clicked), owner_id: id);
577 if (flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClickReleaseAnywhere))
578 {
579 SetActiveID(id, window);
580 g.ActiveIdMouseButton = mouse_button_clicked;
581 if (!(flags & ImGuiButtonFlags_NoNavFocus))
582 {
583 SetFocusID(id, window);
584 FocusWindow(window);
585 }
586 else
587 {
588 FocusWindow(window, flags: ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child
589 }
590 }
591 if ((flags & ImGuiButtonFlags_PressedOnClick) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseClickedCount[mouse_button_clicked] == 2))
592 {
593 pressed = true;
594 if (flags & ImGuiButtonFlags_NoHoldingActiveId)
595 ClearActiveID();
596 else
597 SetActiveID(id, window); // Hold on ID
598 g.ActiveIdMouseButton = mouse_button_clicked;
599 if (!(flags & ImGuiButtonFlags_NoNavFocus))
600 {
601 SetFocusID(id, window);
602 FocusWindow(window);
603 }
604 else
605 {
606 FocusWindow(window, flags: ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child
607 }
608 }
609 }
610 if (flags & ImGuiButtonFlags_PressedOnRelease)
611 {
612 if (mouse_button_released != -1)
613 {
614 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
615 if (!has_repeated_at_least_once)
616 pressed = true;
617 if (!(flags & ImGuiButtonFlags_NoNavFocus))
618 SetFocusID(id, window); // FIXME: Lack of FocusWindow() call here is inconsistent with other paths. Research why.
619 ClearActiveID();
620 }
621 }
622
623 // 'Repeat' mode acts when held regardless of _PressedOn flags (see table above).
624 // Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings.
625 if (g.ActiveId == id && (item_flags & ImGuiItemFlags_ButtonRepeat))
626 if (g.IO.MouseDownDuration[g.ActiveIdMouseButton] > 0.0f && IsMouseClicked(button: g.ActiveIdMouseButton, flags: ImGuiInputFlags_Repeat, owner_id: test_owner_id))
627 pressed = true;
628 }
629
630 if (pressed && g.IO.ConfigNavCursorVisibleAuto)
631 g.NavCursorVisible = false;
632 }
633
634 // Keyboard/Gamepad navigation handling
635 // We report navigated and navigation-activated items as hovered but we don't set g.HoveredId to not interfere with mouse.
636 if (g.NavId == id && g.NavCursorVisible && g.NavHighlightItemUnderNav)
637 if (!(flags & ImGuiButtonFlags_NoHoveredOnFocus))
638 hovered = true;
639 if (g.NavActivateDownId == id)
640 {
641 bool nav_activated_by_code = (g.NavActivateId == id);
642 bool nav_activated_by_inputs = (g.NavActivatePressedId == id);
643 if (!nav_activated_by_inputs && (item_flags & ImGuiItemFlags_ButtonRepeat))
644 {
645 // Avoid pressing multiple keys from triggering excessive amount of repeat events
646 const ImGuiKeyData* key1 = GetKeyData(key: ImGuiKey_Space);
647 const ImGuiKeyData* key2 = GetKeyData(key: ImGuiKey_Enter);
648 const ImGuiKeyData* key3 = GetKeyData(ImGuiKey_NavGamepadActivate);
649 const float t1 = ImMax(lhs: ImMax(lhs: key1->DownDuration, rhs: key2->DownDuration), rhs: key3->DownDuration);
650 nav_activated_by_inputs = CalcTypematicRepeatAmount(t0: t1 - g.IO.DeltaTime, t1, repeat_delay: g.IO.KeyRepeatDelay, repeat_rate: g.IO.KeyRepeatRate) > 0;
651 }
652 if (nav_activated_by_code || nav_activated_by_inputs)
653 {
654 // Set active id so it can be queried by user via IsItemActive(), equivalent of holding the mouse button.
655 pressed = true;
656 SetActiveID(id, window);
657 g.ActiveIdSource = g.NavInputSource;
658 if (!(flags & ImGuiButtonFlags_NoNavFocus) && !(g.NavActivateFlags & ImGuiActivateFlags_FromShortcut))
659 SetFocusID(id, window);
660 if (g.NavActivateFlags & ImGuiActivateFlags_FromShortcut)
661 g.ActiveIdFromShortcut = true;
662 }
663 }
664
665 // Process while held
666 bool held = false;
667 if (g.ActiveId == id)
668 {
669 if (g.ActiveIdSource == ImGuiInputSource_Mouse)
670 {
671 if (g.ActiveIdIsJustActivated)
672 g.ActiveIdClickOffset = g.IO.MousePos - bb.Min;
673
674 const int mouse_button = g.ActiveIdMouseButton;
675 if (mouse_button == -1)
676 {
677 // Fallback for the rare situation were g.ActiveId was set programmatically or from another widget (e.g. #6304).
678 ClearActiveID();
679 }
680 else if (IsMouseDown(button: mouse_button, owner_id: test_owner_id))
681 {
682 held = true;
683 }
684 else
685 {
686 bool release_in = hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease) != 0;
687 bool release_anywhere = (flags & ImGuiButtonFlags_PressedOnClickReleaseAnywhere) != 0;
688 if ((release_in || release_anywhere) && !g.DragDropActive)
689 {
690 // Report as pressed when releasing the mouse (this is the most common path)
691 bool is_double_click_release = (flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseReleased[mouse_button] && g.IO.MouseClickedLastCount[mouse_button] == 2;
692 bool is_repeating_already = (item_flags & ImGuiItemFlags_ButtonRepeat) && g.IO.MouseDownDurationPrev[mouse_button] >= g.IO.KeyRepeatDelay; // Repeat mode trumps <on release>
693 bool is_button_avail_or_owned = TestKeyOwner(key: MouseButtonToKey(button: mouse_button), owner_id: test_owner_id);
694 if (!is_double_click_release && !is_repeating_already && is_button_avail_or_owned)
695 pressed = true;
696 }
697 ClearActiveID();
698 }
699 if (!(flags & ImGuiButtonFlags_NoNavFocus) && g.IO.ConfigNavCursorVisibleAuto)
700 g.NavCursorVisible = false;
701 }
702 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
703 {
704 // When activated using Nav, we hold on the ActiveID until activation button is released
705 if (g.NavActivateDownId == id)
706 held = true; // hovered == true not true as we are already likely hovered on direct activation.
707 else
708 ClearActiveID();
709 }
710 if (pressed)
711 g.ActiveIdHasBeenPressedBefore = true;
712 }
713
714 // Activation highlight (this may be a remote activation)
715 if (g.NavHighlightActivatedId == id)
716 hovered = true;
717
718 if (out_hovered) *out_hovered = hovered;
719 if (out_held) *out_held = held;
720
721 return pressed;
722}
723
724bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags)
725{
726 ImGuiWindow* window = GetCurrentWindow();
727 if (window->SkipItems)
728 return false;
729
730 ImGuiContext& g = *GImGui;
731 const ImGuiStyle& style = g.Style;
732 const ImGuiID id = window->GetID(str: label);
733 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
734
735 ImVec2 pos = window->DC.CursorPos;
736 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)
737 pos.y += window->DC.CurrLineTextBaseOffset - style.FramePadding.y;
738 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);
739
740 const ImRect bb(pos, pos + size);
741 ItemSize(size, text_baseline_y: style.FramePadding.y);
742 if (!ItemAdd(bb, id))
743 return false;
744
745 bool hovered, held;
746 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags);
747
748 // Render
749 const ImU32 col = GetColorU32(idx: (held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
750 RenderNavCursor(bb, id);
751 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: col, borders: true, rounding: style.FrameRounding);
752
753 if (g.LogEnabled)
754 LogSetNextTextDecoration(prefix: "[", suffix: "]");
755 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);
756
757 // Automatically close popups
758 //if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup))
759 // CloseCurrentPopup();
760
761 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
762 return pressed;
763}
764
765bool ImGui::Button(const char* label, const ImVec2& size_arg)
766{
767 return ButtonEx(label, size_arg, flags: ImGuiButtonFlags_None);
768}
769
770// Small buttons fits within text without additional vertical spacing.
771bool ImGui::SmallButton(const char* label)
772{
773 ImGuiContext& g = *GImGui;
774 float backup_padding_y = g.Style.FramePadding.y;
775 g.Style.FramePadding.y = 0.0f;
776 bool pressed = ButtonEx(label, size_arg: ImVec2(0, 0), flags: ImGuiButtonFlags_AlignTextBaseLine);
777 g.Style.FramePadding.y = backup_padding_y;
778 return pressed;
779}
780
781// Tip: use ImGui::PushID()/PopID() to push indices or pointers in the ID stack.
782// 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)
783bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg, ImGuiButtonFlags flags)
784{
785 ImGuiContext& g = *GImGui;
786 ImGuiWindow* window = GetCurrentWindow();
787 if (window->SkipItems)
788 return false;
789
790 // Cannot use zero-size for InvisibleButton(). Unlike Button() there is not way to fallback using the label size.
791 IM_ASSERT(size_arg.x != 0.0f && size_arg.y != 0.0f);
792
793 const ImGuiID id = window->GetID(str: str_id);
794 ImVec2 size = CalcItemSize(size: size_arg, default_w: 0.0f, default_h: 0.0f);
795 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
796 ItemSize(size);
797 if (!ItemAdd(bb, id, NULL, extra_flags: (flags & ImGuiButtonFlags_EnableNav) ? ImGuiItemFlags_None : ImGuiItemFlags_NoNav))
798 return false;
799
800 bool hovered, held;
801 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags);
802 RenderNavCursor(bb, id);
803
804 IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
805 return pressed;
806}
807
808bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiButtonFlags flags)
809{
810 ImGuiContext& g = *GImGui;
811 ImGuiWindow* window = GetCurrentWindow();
812 if (window->SkipItems)
813 return false;
814
815 const ImGuiID id = window->GetID(str: str_id);
816 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
817 const float default_size = GetFrameHeight();
818 ItemSize(size, text_baseline_y: (size.y >= default_size) ? g.Style.FramePadding.y : -1.0f);
819 if (!ItemAdd(bb, id))
820 return false;
821
822 bool hovered, held;
823 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags);
824
825 // Render
826 const ImU32 bg_col = GetColorU32(idx: (held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
827 const ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
828 RenderNavCursor(bb, id);
829 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: bg_col, borders: true, rounding: g.Style.FrameRounding);
830 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);
831
832 IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
833 return pressed;
834}
835
836bool ImGui::ArrowButton(const char* str_id, ImGuiDir dir)
837{
838 float sz = GetFrameHeight();
839 return ArrowButtonEx(str_id, dir, size: ImVec2(sz, sz), flags: ImGuiButtonFlags_None);
840}
841
842// Button to close a window
843bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos)
844{
845 ImGuiContext& g = *GImGui;
846 ImGuiWindow* window = g.CurrentWindow;
847
848 // 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)
849 // 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?
850 const ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize));
851 ImRect bb_interact = bb;
852 const float area_to_visible_ratio = window->OuterRectClipped.GetArea() / bb.GetArea();
853 if (area_to_visible_ratio < 1.5f)
854 bb_interact.Expand(amount: ImTrunc(v: bb_interact.GetSize() * -0.25f));
855
856 // Tweak 2: We intentionally allow interaction when clipped so that a mechanical Alt,Right,Activate sequence can always close a window.
857 // (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).
858 bool is_clipped = !ItemAdd(bb: bb_interact, id);
859
860 bool hovered, held;
861 bool pressed = ButtonBehavior(bb: bb_interact, id, out_hovered: &hovered, out_held: &held);
862 if (is_clipped)
863 return pressed;
864
865 // Render
866 ImU32 bg_col = GetColorU32(idx: held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered);
867 if (hovered)
868 window->DrawList->AddRectFilled(p_min: bb.Min, p_max: bb.Max, col: bg_col);
869 RenderNavCursor(bb, id, flags: ImGuiNavRenderCursorFlags_Compact);
870 ImU32 cross_col = GetColorU32(idx: ImGuiCol_Text);
871 ImVec2 cross_center = bb.GetCenter() - ImVec2(0.5f, 0.5f);
872 float cross_extent = g.FontSize * 0.5f * 0.7071f - 1.0f;
873 window->DrawList->AddLine(p1: cross_center + ImVec2(+cross_extent, +cross_extent), p2: cross_center + ImVec2(-cross_extent, -cross_extent), col: cross_col, thickness: 1.0f);
874 window->DrawList->AddLine(p1: cross_center + ImVec2(+cross_extent, -cross_extent), p2: cross_center + ImVec2(-cross_extent, +cross_extent), col: cross_col, thickness: 1.0f);
875
876 return pressed;
877}
878
879// The Collapse button also functions as a Dock Menu button.
880bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos, ImGuiDockNode* dock_node)
881{
882 ImGuiContext& g = *GImGui;
883 ImGuiWindow* window = g.CurrentWindow;
884
885 ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize));
886 bool is_clipped = !ItemAdd(bb, id);
887 bool hovered, held;
888 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags: ImGuiButtonFlags_None);
889 if (is_clipped)
890 return pressed;
891
892 // Render
893 //bool is_dock_menu = (window->DockNodeAsHost && !window->Collapsed);
894 ImU32 bg_col = GetColorU32(idx: (held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
895 ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
896 if (hovered || held)
897 window->DrawList->AddRectFilled(p_min: bb.Min, p_max: bb.Max, col: bg_col);
898 RenderNavCursor(bb, id, flags: ImGuiNavRenderCursorFlags_Compact);
899
900 if (dock_node)
901 RenderArrowDockMenu(draw_list: window->DrawList, p_min: bb.Min, sz: g.FontSize, col: text_col);
902 else
903 RenderArrow(draw_list: window->DrawList, pos: bb.Min, col: text_col, dir: window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, scale: 1.0f);
904
905 // Switch to moving the window after mouse is moved beyond the initial drag threshold
906 if (IsItemActive() && IsMouseDragging(button: 0))
907 StartMouseMovingWindowOrNode(window, node: dock_node, undock: true); // Undock from window/collapse menu button
908
909 return pressed;
910}
911
912ImGuiID ImGui::GetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis)
913{
914 return window->GetID(str: axis == ImGuiAxis_X ? "#SCROLLX" : "#SCROLLY");
915}
916
917// Return scrollbar rectangle, must only be called for corresponding axis if window->ScrollbarX/Y is set.
918ImRect ImGui::GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis)
919{
920 ImGuiContext& g = *GImGui;
921 const ImRect outer_rect = window->Rect();
922 const ImRect inner_rect = window->InnerRect;
923 const float scrollbar_size = window->ScrollbarSizes[axis ^ 1]; // (ScrollbarSizes.x = width of Y scrollbar; ScrollbarSizes.y = height of X scrollbar)
924 IM_ASSERT(scrollbar_size >= 0.0f);
925 const float border_size = IM_ROUND(window->WindowBorderSize * 0.5f);
926 const float border_top = (window->Flags & ImGuiWindowFlags_MenuBar) ? IM_ROUND(g.Style.FrameBorderSize * 0.5f) : 0.0f;
927 if (axis == ImGuiAxis_X)
928 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);
929 else
930 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);
931}
932
933void ImGui::Scrollbar(ImGuiAxis axis)
934{
935 ImGuiContext& g = *GImGui;
936 ImGuiWindow* window = g.CurrentWindow;
937 const ImGuiID id = GetWindowScrollbarID(window, axis);
938
939 // Calculate scrollbar bounding box
940 ImRect bb = GetWindowScrollbarRect(window, axis);
941 ImDrawFlags rounding_corners = ImDrawFlags_RoundCornersNone;
942 if (axis == ImGuiAxis_X)
943 {
944 rounding_corners |= ImDrawFlags_RoundCornersBottomLeft;
945 if (!window->ScrollbarY)
946 rounding_corners |= ImDrawFlags_RoundCornersBottomRight;
947 }
948 else
949 {
950 if ((window->Flags & ImGuiWindowFlags_NoTitleBar) && !(window->Flags & ImGuiWindowFlags_MenuBar))
951 rounding_corners |= ImDrawFlags_RoundCornersTopRight;
952 if (!window->ScrollbarX)
953 rounding_corners |= ImDrawFlags_RoundCornersBottomRight;
954 }
955 float size_visible = window->InnerRect.Max[axis] - window->InnerRect.Min[axis];
956 float size_contents = window->ContentSize[axis] + window->WindowPadding[axis] * 2.0f;
957 ImS64 scroll = (ImS64)window->Scroll[axis];
958 ScrollbarEx(bb, id, axis, p_scroll_v: &scroll, avail_v: (ImS64)size_visible, contents_v: (ImS64)size_contents, draw_rounding_flags: rounding_corners);
959 window->Scroll[axis] = (float)scroll;
960}
961
962// Vertical/Horizontal scrollbar
963// The entire piece of code below is rather confusing because:
964// - We handle absolute seeking (when first clicking outside the grab) and relative manipulation (afterward or when clicking inside the grab)
965// - We store values as normalized ratio and in a form that allows the window content to change while we are holding on a scrollbar
966// - We handle both horizontal and vertical scrollbars, which makes the terminology not ideal.
967// Still, the code should probably be made simpler..
968bool 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)
969{
970 ImGuiContext& g = *GImGui;
971 ImGuiWindow* window = g.CurrentWindow;
972 if (window->SkipItems)
973 return false;
974
975 const float bb_frame_width = bb_frame.GetWidth();
976 const float bb_frame_height = bb_frame.GetHeight();
977 if (bb_frame_width <= 0.0f || bb_frame_height <= 0.0f)
978 return false;
979
980 // 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)
981 float alpha = 1.0f;
982 if ((axis == ImGuiAxis_Y) && bb_frame_height < bb_frame_width)
983 alpha = ImSaturate(f: bb_frame_height / ImMax(lhs: bb_frame_width * 2.0f, rhs: 1.0f));
984 if (alpha <= 0.0f)
985 return false;
986
987 const ImGuiStyle& style = g.Style;
988 const bool allow_interaction = (alpha >= 1.0f);
989
990 ImRect bb = bb_frame;
991 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)));
992
993 // V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar)
994 const float scrollbar_size_v = (axis == ImGuiAxis_X) ? bb.GetWidth() : bb.GetHeight();
995
996 // Calculate the height of our grabbable box. It generally represent the amount visible (vs the total scrollable amount)
997 // But we maintain a minimum size in pixel to allow for the user to still aim inside.
998 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.
999 const ImS64 win_size_v = ImMax(lhs: ImMax(lhs: size_contents_v, rhs: size_visible_v), rhs: (ImS64)1);
1000 const float grab_h_minsize = ImMin(lhs: bb.GetSize()[axis], rhs: style.GrabMinSize);
1001 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);
1002 const float grab_h_norm = grab_h_pixels / scrollbar_size_v;
1003
1004 // Handle input right away. None of the code of Begin() is relying on scrolling position before calling Scrollbar().
1005 bool held = false;
1006 bool hovered = false;
1007 ItemAdd(bb: bb_frame, id, NULL, extra_flags: ImGuiItemFlags_NoNav);
1008 ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags: ImGuiButtonFlags_NoNavFocus);
1009
1010 const ImS64 scroll_max = ImMax(lhs: (ImS64)1, rhs: size_contents_v - size_visible_v);
1011 float scroll_ratio = ImSaturate(f: (float)*p_scroll_v / (float)scroll_max);
1012 float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; // Grab position in normalized space
1013 if (held && allow_interaction && grab_h_norm < 1.0f)
1014 {
1015 const float scrollbar_pos_v = bb.Min[axis];
1016 const float mouse_pos_v = g.IO.MousePos[axis];
1017
1018 // Click position in scrollbar normalized space (0.0f->1.0f)
1019 const float clicked_v_norm = ImSaturate(f: (mouse_pos_v - scrollbar_pos_v) / scrollbar_size_v);
1020
1021 const int held_dir = (clicked_v_norm < grab_v_norm) ? -1 : (clicked_v_norm > grab_v_norm + grab_h_norm) ? +1 : 0;
1022 if (g.ActiveIdIsJustActivated)
1023 {
1024 // On initial click when held_dir == 0 (clicked over grab): calculate the distance between mouse and the center of the grab
1025 const bool scroll_to_clicked_location = (g.IO.ConfigScrollbarScrollByPage == false || g.IO.KeyShift || held_dir == 0);
1026 g.ScrollbarSeekMode = scroll_to_clicked_location ? 0 : (short)held_dir;
1027 g.ScrollbarClickDeltaToGrabCenter = (held_dir == 0 && !g.IO.KeyShift) ? clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f : 0.0f;
1028 }
1029
1030 // Apply scroll (p_scroll_v will generally point on one member of window->Scroll)
1031 // 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
1032 if (g.ScrollbarSeekMode == 0)
1033 {
1034 // Absolute seeking
1035 const float scroll_v_norm = ImSaturate(f: (clicked_v_norm - g.ScrollbarClickDeltaToGrabCenter - grab_h_norm * 0.5f) / (1.0f - grab_h_norm));
1036 *p_scroll_v = (ImS64)(scroll_v_norm * scroll_max);
1037 }
1038 else
1039 {
1040 // Page by page
1041 if (IsMouseClicked(button: ImGuiMouseButton_Left, flags: ImGuiInputFlags_Repeat) && held_dir == g.ScrollbarSeekMode)
1042 {
1043 float page_dir = (g.ScrollbarSeekMode > 0.0f) ? +1.0f : -1.0f;
1044 *p_scroll_v = ImClamp(v: *p_scroll_v + (ImS64)(page_dir * size_visible_v), mn: (ImS64)0, mx: scroll_max);
1045 }
1046 }
1047
1048 // Update values for rendering
1049 scroll_ratio = ImSaturate(f: (float)*p_scroll_v / (float)scroll_max);
1050 grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v;
1051
1052 // Update distance to grab now that we have seek'ed and saturated
1053 //if (seek_absolute)
1054 // g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f;
1055 }
1056
1057 // Render
1058 const ImU32 bg_col = GetColorU32(idx: ImGuiCol_ScrollbarBg);
1059 const ImU32 grab_col = GetColorU32(idx: held ? ImGuiCol_ScrollbarGrabActive : hovered ? ImGuiCol_ScrollbarGrabHovered : ImGuiCol_ScrollbarGrab, alpha_mul: alpha);
1060 window->DrawList->AddRectFilled(p_min: bb_frame.Min, p_max: bb_frame.Max, col: bg_col, rounding: window->WindowRounding, flags: draw_rounding_flags);
1061 ImRect grab_rect;
1062 if (axis == ImGuiAxis_X)
1063 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);
1064 else
1065 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);
1066 window->DrawList->AddRectFilled(p_min: grab_rect.Min, p_max: grab_rect.Max, col: grab_col, rounding: style.ScrollbarRounding);
1067
1068 return held;
1069}
1070
1071// - Read about ImTextureID here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples
1072// - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above.
1073void ImGui::ImageWithBg(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col)
1074{
1075 ImGuiContext& g = *GImGui;
1076 ImGuiWindow* window = GetCurrentWindow();
1077 if (window->SkipItems)
1078 return;
1079
1080 const ImVec2 padding(g.Style.ImageBorderSize, g.Style.ImageBorderSize);
1081 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f);
1082 ItemSize(bb);
1083 if (!ItemAdd(bb, id: 0))
1084 return;
1085
1086 // Render
1087 if (g.Style.ImageBorderSize > 0.0f)
1088 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);
1089 if (bg_col.w > 0.0f)
1090 window->DrawList->AddRectFilled(p_min: bb.Min + padding, p_max: bb.Max - padding, col: GetColorU32(col: bg_col));
1091 window->DrawList->AddImage(user_texture_id, p_min: bb.Min + padding, p_max: bb.Max - padding, uv_min: uv0, uv_max: uv1, col: GetColorU32(col: tint_col));
1092}
1093
1094void ImGui::Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1)
1095{
1096 ImageWithBg(user_texture_id, image_size, uv0, uv1);
1097}
1098
1099// 1.91.9 (February 2025) removed 'tint_col' and 'border_col' parameters, made border size not depend on color value. (#8131, #8238)
1100#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1101void ImGui::Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col)
1102{
1103 ImGuiContext& g = *GImGui;
1104 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
1105 PushStyleColor(idx: ImGuiCol_Border, col: border_col);
1106 ImageWithBg(user_texture_id, image_size, uv0, uv1, bg_col: ImVec4(0, 0, 0, 0), tint_col);
1107 PopStyleColor();
1108 PopStyleVar();
1109}
1110#endif
1111
1112bool ImGui::ImageButtonEx(ImGuiID id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col, ImGuiButtonFlags flags)
1113{
1114 ImGuiContext& g = *GImGui;
1115 ImGuiWindow* window = GetCurrentWindow();
1116 if (window->SkipItems)
1117 return false;
1118
1119 const ImVec2 padding = g.Style.FramePadding;
1120 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f);
1121 ItemSize(bb);
1122 if (!ItemAdd(bb, id))
1123 return false;
1124
1125 bool hovered, held;
1126 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags);
1127
1128 // Render
1129 const ImU32 col = GetColorU32(idx: (held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1130 RenderNavCursor(bb, id);
1131 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));
1132 if (bg_col.w > 0.0f)
1133 window->DrawList->AddRectFilled(p_min: bb.Min + padding, p_max: bb.Max - padding, col: GetColorU32(col: bg_col));
1134 window->DrawList->AddImage(user_texture_id, p_min: bb.Min + padding, p_max: bb.Max - padding, uv_min: uv0, uv_max: uv1, col: GetColorU32(col: tint_col));
1135
1136 return pressed;
1137}
1138
1139// - ImageButton() adds style.FramePadding*2.0f to provided size. This is in order to facilitate fitting an image in a button.
1140// - ImageButton() draws a background based on regular Button() color + optionally an inner background if specified. (#8165) // FIXME: Maybe that's not the best design?
1141bool ImGui::ImageButton(const char* str_id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col)
1142{
1143 ImGuiContext& g = *GImGui;
1144 ImGuiWindow* window = g.CurrentWindow;
1145 if (window->SkipItems)
1146 return false;
1147
1148 return ImageButtonEx(id: window->GetID(str: str_id), user_texture_id, image_size, uv0, uv1, bg_col, tint_col);
1149}
1150
1151#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1152// Legacy API obsoleted in 1.89. Two differences with new ImageButton()
1153// - old ImageButton() used ImTextureID as item id (created issue with multiple buttons with same image, transient texture id values, opaque computation of ID)
1154// - new ImageButton() requires an explicit 'const char* str_id'
1155// - old ImageButton() had frame_padding' override argument.
1156// - new ImageButton() always use style.FramePadding.
1157/*
1158bool 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)
1159{
1160 // Default to using texture ID as ID. User can still push string/integer prefixes.
1161 PushID((ImTextureID)(intptr_t)user_texture_id);
1162 if (frame_padding >= 0)
1163 PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2((float)frame_padding, (float)frame_padding));
1164 bool ret = ImageButton("", user_texture_id, size, uv0, uv1, bg_col, tint_col);
1165 if (frame_padding >= 0)
1166 PopStyleVar();
1167 PopID();
1168 return ret;
1169}
1170*/
1171#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1172
1173bool ImGui::Checkbox(const char* label, bool* v)
1174{
1175 ImGuiWindow* window = GetCurrentWindow();
1176 if (window->SkipItems)
1177 return false;
1178
1179 ImGuiContext& g = *GImGui;
1180 const ImGuiStyle& style = g.Style;
1181 const ImGuiID id = window->GetID(str: label);
1182 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
1183
1184 const float square_sz = GetFrameHeight();
1185 const ImVec2 pos = window->DC.CursorPos;
1186 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));
1187 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
1188 const bool is_visible = ItemAdd(bb: total_bb, id);
1189 const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
1190 if (!is_visible)
1191 if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(r: total_bb)) // Extra layer of "no logic clip" for box-select support
1192 {
1193 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1194 return false;
1195 }
1196
1197 // Range-Selection/Multi-selection support (header)
1198 bool checked = *v;
1199 if (is_multi_select)
1200 MultiSelectItemHeader(id, p_selected: &checked, NULL);
1201
1202 bool hovered, held;
1203 bool pressed = ButtonBehavior(bb: total_bb, id, out_hovered: &hovered, out_held: &held);
1204
1205 // Range-Selection/Multi-selection support (footer)
1206 if (is_multi_select)
1207 MultiSelectItemFooter(id, p_selected: &checked, p_pressed: &pressed);
1208 else if (pressed)
1209 checked = !checked;
1210
1211 if (*v != checked)
1212 {
1213 *v = checked;
1214 pressed = true; // return value
1215 MarkItemEdited(id);
1216 }
1217
1218 const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1219 const bool mixed_value = (g.LastItemData.ItemFlags & ImGuiItemFlags_MixedValue) != 0;
1220 if (is_visible)
1221 {
1222 RenderNavCursor(bb: total_bb, id);
1223 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);
1224 ImU32 check_col = GetColorU32(idx: ImGuiCol_CheckMark);
1225 if (mixed_value)
1226 {
1227 // Undocumented tristate/mixed/indeterminate checkbox (#2644)
1228 // This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all widgets (not just checkbox)
1229 ImVec2 pad(ImMax(lhs: 1.0f, IM_TRUNC(square_sz / 3.6f)), ImMax(lhs: 1.0f, IM_TRUNC(square_sz / 3.6f)));
1230 window->DrawList->AddRectFilled(p_min: check_bb.Min + pad, p_max: check_bb.Max - pad, col: check_col, rounding: style.FrameRounding);
1231 }
1232 else if (*v)
1233 {
1234 const float pad = ImMax(lhs: 1.0f, IM_TRUNC(square_sz / 6.0f));
1235 RenderCheckMark(draw_list: window->DrawList, pos: check_bb.Min + ImVec2(pad, pad), col: check_col, sz: square_sz - pad * 2.0f);
1236 }
1237 }
1238 const ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1239 if (g.LogEnabled)
1240 LogRenderedText(ref_pos: &label_pos, text: mixed_value ? "[~]" : *v ? "[x]" : "[ ]");
1241 if (is_visible && label_size.x > 0.0f)
1242 RenderText(pos: label_pos, text: label);
1243
1244 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1245 return pressed;
1246}
1247
1248template<typename T>
1249bool ImGui::CheckboxFlagsT(const char* label, T* flags, T flags_value)
1250{
1251 bool all_on = (*flags & flags_value) == flags_value;
1252 bool any_on = (*flags & flags_value) != 0;
1253 bool pressed;
1254 if (!all_on && any_on)
1255 {
1256 ImGuiContext& g = *GImGui;
1257 g.NextItemData.ItemFlags |= ImGuiItemFlags_MixedValue;
1258 pressed = Checkbox(label, v: &all_on);
1259 }
1260 else
1261 {
1262 pressed = Checkbox(label, v: &all_on);
1263
1264 }
1265 if (pressed)
1266 {
1267 if (all_on)
1268 *flags |= flags_value;
1269 else
1270 *flags &= ~flags_value;
1271 }
1272 return pressed;
1273}
1274
1275bool ImGui::CheckboxFlags(const char* label, int* flags, int flags_value)
1276{
1277 return CheckboxFlagsT(label, flags, flags_value);
1278}
1279
1280bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value)
1281{
1282 return CheckboxFlagsT(label, flags, flags_value);
1283}
1284
1285bool ImGui::CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value)
1286{
1287 return CheckboxFlagsT(label, flags, flags_value);
1288}
1289
1290bool ImGui::CheckboxFlags(const char* label, ImU64* flags, ImU64 flags_value)
1291{
1292 return CheckboxFlagsT(label, flags, flags_value);
1293}
1294
1295bool ImGui::RadioButton(const char* label, bool active)
1296{
1297 ImGuiWindow* window = GetCurrentWindow();
1298 if (window->SkipItems)
1299 return false;
1300
1301 ImGuiContext& g = *GImGui;
1302 const ImGuiStyle& style = g.Style;
1303 const ImGuiID id = window->GetID(str: label);
1304 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
1305
1306 const float square_sz = GetFrameHeight();
1307 const ImVec2 pos = window->DC.CursorPos;
1308 const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1309 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));
1310 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
1311 if (!ItemAdd(bb: total_bb, id))
1312 return false;
1313
1314 ImVec2 center = check_bb.GetCenter();
1315 center.x = IM_ROUND(center.x);
1316 center.y = IM_ROUND(center.y);
1317 const float radius = (square_sz - 1.0f) * 0.5f;
1318
1319 bool hovered, held;
1320 bool pressed = ButtonBehavior(bb: total_bb, id, out_hovered: &hovered, out_held: &held);
1321 if (pressed)
1322 MarkItemEdited(id);
1323
1324 RenderNavCursor(bb: total_bb, id);
1325 const int num_segment = window->DrawList->_CalcCircleAutoSegmentCount(radius);
1326 window->DrawList->AddCircleFilled(center, radius, col: GetColorU32(idx: (held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), num_segments: num_segment);
1327 if (active)
1328 {
1329 const float pad = ImMax(lhs: 1.0f, IM_TRUNC(square_sz / 6.0f));
1330 window->DrawList->AddCircleFilled(center, radius: radius - pad, col: GetColorU32(idx: ImGuiCol_CheckMark));
1331 }
1332
1333 if (style.FrameBorderSize > 0.0f)
1334 {
1335 window->DrawList->AddCircle(center: center + ImVec2(1, 1), radius, col: GetColorU32(idx: ImGuiCol_BorderShadow), num_segments: num_segment, thickness: style.FrameBorderSize);
1336 window->DrawList->AddCircle(center, radius, col: GetColorU32(idx: ImGuiCol_Border), num_segments: num_segment, thickness: style.FrameBorderSize);
1337 }
1338
1339 ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1340 if (g.LogEnabled)
1341 LogRenderedText(ref_pos: &label_pos, text: active ? "(x)" : "( )");
1342 if (label_size.x > 0.0f)
1343 RenderText(pos: label_pos, text: label);
1344
1345 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
1346 return pressed;
1347}
1348
1349// 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..
1350bool ImGui::RadioButton(const char* label, int* v, int v_button)
1351{
1352 const bool pressed = RadioButton(label, active: *v == v_button);
1353 if (pressed)
1354 *v = v_button;
1355 return pressed;
1356}
1357
1358// size_arg (for each axis) < 0.0f: align to end, 0.0f: auto, > 0.0f: specified size
1359void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* overlay)
1360{
1361 ImGuiWindow* window = GetCurrentWindow();
1362 if (window->SkipItems)
1363 return;
1364
1365 ImGuiContext& g = *GImGui;
1366 const ImGuiStyle& style = g.Style;
1367
1368 ImVec2 pos = window->DC.CursorPos;
1369 ImVec2 size = CalcItemSize(size: size_arg, default_w: CalcItemWidth(), default_h: g.FontSize + style.FramePadding.y * 2.0f);
1370 ImRect bb(pos, pos + size);
1371 ItemSize(size, text_baseline_y: style.FramePadding.y);
1372 if (!ItemAdd(bb, id: 0))
1373 return;
1374
1375 // Fraction < 0.0f will display an indeterminate progress bar animation
1376 // The value must be animated along with time, so e.g. passing '-1.0f * ImGui::GetTime()' as fraction works.
1377 const bool is_indeterminate = (fraction < 0.0f);
1378 if (!is_indeterminate)
1379 fraction = ImSaturate(f: fraction);
1380
1381 // Out of courtesy we accept a NaN fraction without crashing
1382 float fill_n0 = 0.0f;
1383 float fill_n1 = (fraction == fraction) ? fraction : 0.0f;
1384
1385 if (is_indeterminate)
1386 {
1387 const float fill_width_n = 0.2f;
1388 fill_n0 = ImFmod(-fraction, 1.0f) * (1.0f + fill_width_n) - fill_width_n;
1389 fill_n1 = ImSaturate(f: fill_n0 + fill_width_n);
1390 fill_n0 = ImSaturate(f: fill_n0);
1391 }
1392
1393 // Render
1394 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: GetColorU32(idx: ImGuiCol_FrameBg), borders: true, rounding: style.FrameRounding);
1395 bb.Expand(amount: ImVec2(-style.FrameBorderSize, -style.FrameBorderSize));
1396 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);
1397
1398 // Default displaying the fraction as percentage string, but user can override it
1399 // Don't display text for indeterminate bars by default
1400 char overlay_buf[32];
1401 if (!is_indeterminate || overlay != NULL)
1402 {
1403 if (!overlay)
1404 {
1405 ImFormatString(buf: overlay_buf, IM_ARRAYSIZE(overlay_buf), fmt: "%.0f%%", fraction * 100 + 0.01f);
1406 overlay = overlay_buf;
1407 }
1408
1409 ImVec2 overlay_size = CalcTextSize(text: overlay, NULL);
1410 if (overlay_size.x > 0.0f)
1411 {
1412 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;
1413 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);
1414 }
1415 }
1416}
1417
1418void ImGui::Bullet()
1419{
1420 ImGuiWindow* window = GetCurrentWindow();
1421 if (window->SkipItems)
1422 return;
1423
1424 ImGuiContext& g = *GImGui;
1425 const ImGuiStyle& style = g.Style;
1426 const float line_height = ImMax(lhs: ImMin(lhs: window->DC.CurrLineSize.y, rhs: g.FontSize + style.FramePadding.y * 2), rhs: g.FontSize);
1427 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize, line_height));
1428 ItemSize(bb);
1429 if (!ItemAdd(bb, id: 0))
1430 {
1431 SameLine(offset_from_start_x: 0, spacing: style.FramePadding.x * 2);
1432 return;
1433 }
1434
1435 // Render and stay on same line
1436 ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
1437 RenderBullet(draw_list: window->DrawList, pos: bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, line_height * 0.5f), col: text_col);
1438 SameLine(offset_from_start_x: 0, spacing: style.FramePadding.x * 2.0f);
1439}
1440
1441// This is provided as a convenience for being an often requested feature.
1442// FIXME-STYLE: we delayed adding as there is a larger plan to revamp the styling system.
1443// Because of this we currently don't provide many styling options for this widget
1444// (e.g. hovered/active colors are automatically inferred from a single color).
1445bool ImGui::TextLink(const char* label)
1446{
1447 ImGuiWindow* window = GetCurrentWindow();
1448 if (window->SkipItems)
1449 return false;
1450
1451 ImGuiContext& g = *GImGui;
1452 const ImGuiID id = window->GetID(str: label);
1453 const char* label_end = FindRenderedTextEnd(text: label);
1454
1455 ImVec2 pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
1456 ImVec2 size = CalcTextSize(text: label, text_end: label_end, hide_text_after_double_hash: true);
1457 ImRect bb(pos, pos + size);
1458 ItemSize(size, text_baseline_y: 0.0f);
1459 if (!ItemAdd(bb, id))
1460 return false;
1461
1462 bool hovered, held;
1463 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held);
1464 RenderNavCursor(bb, id);
1465
1466 if (hovered)
1467 SetMouseCursor(ImGuiMouseCursor_Hand);
1468
1469 ImVec4 text_colf = g.Style.Colors[ImGuiCol_TextLink];
1470 ImVec4 line_colf = text_colf;
1471 {
1472 // FIXME-STYLE: Read comments above. This widget is NOT written in the same style as some earlier widgets,
1473 // as we are currently experimenting/planning a different styling system.
1474 float h, s, v;
1475 ColorConvertRGBtoHSV(r: text_colf.x, g: text_colf.y, b: text_colf.z, out_h&: h, out_s&: s, out_v&: v);
1476 if (held || hovered)
1477 {
1478 v = ImSaturate(f: v + (held ? 0.4f : 0.3f));
1479 h = ImFmod(h + 0.02f, 1.0f);
1480 }
1481 ColorConvertHSVtoRGB(h, s, v, out_r&: text_colf.x, out_g&: text_colf.y, out_b&: text_colf.z);
1482 v = ImSaturate(f: v - 0.20f);
1483 ColorConvertHSVtoRGB(h, s, v, out_r&: line_colf.x, out_g&: line_colf.y, out_b&: line_colf.z);
1484 }
1485
1486 float line_y = bb.Max.y + ImFloor(f: g.Font->Descent * g.FontScale * 0.20f);
1487 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.
1488
1489 PushStyleColor(idx: ImGuiCol_Text, col: GetColorU32(col: text_colf));
1490 RenderText(pos: bb.Min, text: label, text_end: label_end);
1491 PopStyleColor();
1492
1493 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
1494 return pressed;
1495}
1496
1497void ImGui::TextLinkOpenURL(const char* label, const char* url)
1498{
1499 ImGuiContext& g = *GImGui;
1500 if (url == NULL)
1501 url = label;
1502 if (TextLink(label))
1503 if (g.PlatformIO.Platform_OpenInShellFn != NULL)
1504 g.PlatformIO.Platform_OpenInShellFn(&g, url);
1505 SetItemTooltip(LocalizeGetMsg(key: ImGuiLocKey_OpenLink_s), url); // It is more reassuring for user to _always_ display URL when we same as label
1506 if (BeginPopupContextItem())
1507 {
1508 if (MenuItem(label: LocalizeGetMsg(key: ImGuiLocKey_CopyLink)))
1509 SetClipboardText(url);
1510 EndPopup();
1511 }
1512}
1513
1514//-------------------------------------------------------------------------
1515// [SECTION] Widgets: Low-level Layout helpers
1516//-------------------------------------------------------------------------
1517// - Spacing()
1518// - Dummy()
1519// - NewLine()
1520// - AlignTextToFramePadding()
1521// - SeparatorEx() [Internal]
1522// - Separator()
1523// - SplitterBehavior() [Internal]
1524// - ShrinkWidths() [Internal]
1525//-------------------------------------------------------------------------
1526
1527void ImGui::Spacing()
1528{
1529 ImGuiWindow* window = GetCurrentWindow();
1530 if (window->SkipItems)
1531 return;
1532 ItemSize(size: ImVec2(0, 0));
1533}
1534
1535void ImGui::Dummy(const ImVec2& size)
1536{
1537 ImGuiWindow* window = GetCurrentWindow();
1538 if (window->SkipItems)
1539 return;
1540
1541 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
1542 ItemSize(size);
1543 ItemAdd(bb, id: 0);
1544}
1545
1546void ImGui::NewLine()
1547{
1548 ImGuiWindow* window = GetCurrentWindow();
1549 if (window->SkipItems)
1550 return;
1551
1552 ImGuiContext& g = *GImGui;
1553 const ImGuiLayoutType backup_layout_type = window->DC.LayoutType;
1554 window->DC.LayoutType = ImGuiLayoutType_Vertical;
1555 window->DC.IsSameLine = false;
1556 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.
1557 ItemSize(size: ImVec2(0, 0));
1558 else
1559 ItemSize(size: ImVec2(0.0f, g.FontSize));
1560 window->DC.LayoutType = backup_layout_type;
1561}
1562
1563void ImGui::AlignTextToFramePadding()
1564{
1565 ImGuiWindow* window = GetCurrentWindow();
1566 if (window->SkipItems)
1567 return;
1568
1569 ImGuiContext& g = *GImGui;
1570 window->DC.CurrLineSize.y = ImMax(lhs: window->DC.CurrLineSize.y, rhs: g.FontSize + g.Style.FramePadding.y * 2);
1571 window->DC.CurrLineTextBaseOffset = ImMax(lhs: window->DC.CurrLineTextBaseOffset, rhs: g.Style.FramePadding.y);
1572}
1573
1574// Horizontal/vertical separating line
1575// FIXME: Surprisingly, this seemingly trivial widget is a victim of many different legacy/tricky layout issues.
1576// Note how thickness == 1.0f is handled specifically as not moving CursorPos by 'thickness', but other values are.
1577void ImGui::SeparatorEx(ImGuiSeparatorFlags flags, float thickness)
1578{
1579 ImGuiWindow* window = GetCurrentWindow();
1580 if (window->SkipItems)
1581 return;
1582
1583 ImGuiContext& g = *GImGui;
1584 IM_ASSERT(ImIsPowerOfTwo(flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical))); // Check that only 1 option is selected
1585 IM_ASSERT(thickness > 0.0f);
1586
1587 if (flags & ImGuiSeparatorFlags_Vertical)
1588 {
1589 // Vertical separator, for menu bars (use current line height).
1590 float y1 = window->DC.CursorPos.y;
1591 float y2 = window->DC.CursorPos.y + window->DC.CurrLineSize.y;
1592 const ImRect bb(ImVec2(window->DC.CursorPos.x, y1), ImVec2(window->DC.CursorPos.x + thickness, y2));
1593 ItemSize(size: ImVec2(thickness, 0.0f));
1594 if (!ItemAdd(bb, id: 0))
1595 return;
1596
1597 // Draw
1598 window->DrawList->AddRectFilled(p_min: bb.Min, p_max: bb.Max, col: GetColorU32(idx: ImGuiCol_Separator));
1599 if (g.LogEnabled)
1600 LogText(fmt: " |");
1601 }
1602 else if (flags & ImGuiSeparatorFlags_Horizontal)
1603 {
1604 // Horizontal Separator
1605 float x1 = window->DC.CursorPos.x;
1606 float x2 = window->WorkRect.Max.x;
1607
1608 // Preserve legacy behavior inside Columns()
1609 // Before Tables API happened, we relied on Separator() to span all columns of a Columns() set.
1610 // We currently don't need to provide the same feature for tables because tables naturally have border features.
1611 ImGuiOldColumns* columns = (flags & ImGuiSeparatorFlags_SpanAllColumns) ? window->DC.CurrentColumns : NULL;
1612 if (columns)
1613 {
1614 x1 = window->Pos.x + window->DC.Indent.x; // Used to be Pos.x before 2023/10/03
1615 x2 = window->Pos.x + window->Size.x;
1616 PushColumnsBackground();
1617 }
1618
1619 // We don't provide our width to the layout so that it doesn't get feed back into AutoFit
1620 // FIXME: This prevents ->CursorMaxPos based bounding box evaluation from working (e.g. TableEndCell)
1621 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.
1622 const ImRect bb(ImVec2(x1, window->DC.CursorPos.y), ImVec2(x2, window->DC.CursorPos.y + thickness));
1623 ItemSize(size: ImVec2(0.0f, thickness_for_layout));
1624
1625 if (ItemAdd(bb, id: 0))
1626 {
1627 // Draw
1628 window->DrawList->AddRectFilled(p_min: bb.Min, p_max: bb.Max, col: GetColorU32(idx: ImGuiCol_Separator));
1629 if (g.LogEnabled)
1630 LogRenderedText(ref_pos: &bb.Min, text: "--------------------------------\n");
1631
1632 }
1633 if (columns)
1634 {
1635 PopColumnsBackground();
1636 columns->LineMinY = window->DC.CursorPos.y;
1637 }
1638 }
1639}
1640
1641void ImGui::Separator()
1642{
1643 ImGuiContext& g = *GImGui;
1644 ImGuiWindow* window = g.CurrentWindow;
1645 if (window->SkipItems)
1646 return;
1647
1648 // Those flags should eventually be configurable by the user
1649 // FIXME: We cannot g.Style.SeparatorTextBorderSize for thickness as it relates to SeparatorText() which is a decorated separator, not defaulting to 1.0f.
1650 ImGuiSeparatorFlags flags = (window->DC.LayoutType == ImGuiLayoutType_Horizontal) ? ImGuiSeparatorFlags_Vertical : ImGuiSeparatorFlags_Horizontal;
1651
1652 // Only applies to legacy Columns() api as they relied on Separator() a lot.
1653 if (window->DC.CurrentColumns)
1654 flags |= ImGuiSeparatorFlags_SpanAllColumns;
1655
1656 SeparatorEx(flags, thickness: 1.0f);
1657}
1658
1659void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end, float extra_w)
1660{
1661 ImGuiContext& g = *GImGui;
1662 ImGuiWindow* window = g.CurrentWindow;
1663 ImGuiStyle& style = g.Style;
1664
1665 const ImVec2 label_size = CalcTextSize(text: label, text_end: label_end, hide_text_after_double_hash: false);
1666 const ImVec2 pos = window->DC.CursorPos;
1667 const ImVec2 padding = style.SeparatorTextPadding;
1668
1669 const float separator_thickness = style.SeparatorTextBorderSize;
1670 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));
1671 const ImRect bb(pos, ImVec2(window->WorkRect.Max.x, pos.y + min_size.y));
1672 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));
1673 ItemSize(size: min_size, text_baseline_y);
1674 if (!ItemAdd(bb, id))
1675 return;
1676
1677 const float sep1_x1 = pos.x;
1678 const float sep2_x2 = bb.Max.x;
1679 const float seps_y = ImTrunc(f: (bb.Min.y + bb.Max.y) * 0.5f + 0.99999f);
1680
1681 const float label_avail_w = ImMax(lhs: 0.0f, rhs: sep2_x2 - sep1_x1 - padding.x * 2.0f);
1682 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
1683
1684 // This allows using SameLine() to position something in the 'extra_w'
1685 window->DC.CursorPosPrevLine.x = label_pos.x + label_size.x;
1686
1687 const ImU32 separator_col = GetColorU32(idx: ImGuiCol_Separator);
1688 if (label_size.x > 0.0f)
1689 {
1690 const float sep1_x2 = label_pos.x - style.ItemSpacing.x;
1691 const float sep2_x1 = label_pos.x + label_size.x + extra_w + style.ItemSpacing.x;
1692 if (sep1_x2 > sep1_x1 && separator_thickness > 0.0f)
1693 window->DrawList->AddLine(p1: ImVec2(sep1_x1, seps_y), p2: ImVec2(sep1_x2, seps_y), col: separator_col, thickness: separator_thickness);
1694 if (sep2_x2 > sep2_x1 && separator_thickness > 0.0f)
1695 window->DrawList->AddLine(p1: ImVec2(sep2_x1, seps_y), p2: ImVec2(sep2_x2, seps_y), col: separator_col, thickness: separator_thickness);
1696 if (g.LogEnabled)
1697 LogSetNextTextDecoration(prefix: "---", NULL);
1698 RenderTextEllipsis(draw_list: window->DrawList, pos_min: label_pos, pos_max: ImVec2(bb.Max.x, bb.Max.y + style.ItemSpacing.y), clip_max_x: bb.Max.x, ellipsis_max_x: bb.Max.x, text: label, text_end: label_end, text_size_if_known: &label_size);
1699 }
1700 else
1701 {
1702 if (g.LogEnabled)
1703 LogText(fmt: "---");
1704 if (separator_thickness > 0.0f)
1705 window->DrawList->AddLine(p1: ImVec2(sep1_x1, seps_y), p2: ImVec2(sep2_x2, seps_y), col: separator_col, thickness: separator_thickness);
1706 }
1707}
1708
1709void ImGui::SeparatorText(const char* label)
1710{
1711 ImGuiWindow* window = GetCurrentWindow();
1712 if (window->SkipItems)
1713 return;
1714
1715 // The SeparatorText() vs SeparatorTextEx() distinction is designed to be considerate that we may want:
1716 // - allow separator-text to be draggable items (would require a stable ID + a noticeable highlight)
1717 // - this high-level entry point to allow formatting? (which in turns may require ID separate from formatted string)
1718 // - because of this we probably can't turn 'const char* label' into 'const char* fmt, ...'
1719 // Otherwise, we can decide that users wanting to drag this would layout a dedicated drag-item,
1720 // and then we can turn this into a format function.
1721 SeparatorTextEx(id: 0, label, label_end: FindRenderedTextEnd(text: label), extra_w: 0.0f);
1722}
1723
1724// 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.
1725bool 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)
1726{
1727 ImGuiContext& g = *GImGui;
1728 ImGuiWindow* window = g.CurrentWindow;
1729
1730 if (!ItemAdd(bb, id, NULL, extra_flags: ImGuiItemFlags_NoNav))
1731 return false;
1732
1733 // FIXME: AFAIK the only leftover reason for passing ImGuiButtonFlags_AllowOverlap here is
1734 // to allow caller of SplitterBehavior() to call SetItemAllowOverlap() after the item.
1735 // Nowadays we would instead want to use SetNextItemAllowOverlap() before the item.
1736 ImGuiButtonFlags button_flags = ImGuiButtonFlags_FlattenChildren;
1737#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1738 button_flags |= ImGuiButtonFlags_AllowOverlap;
1739#endif
1740
1741 bool hovered, held;
1742 ImRect bb_interact = bb;
1743 bb_interact.Expand(amount: axis == ImGuiAxis_Y ? ImVec2(0.0f, hover_extend) : ImVec2(hover_extend, 0.0f));
1744 ButtonBehavior(bb: bb_interact, id, out_hovered: &hovered, out_held: &held, flags: button_flags);
1745 if (hovered)
1746 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; // for IsItemHovered(), because bb_interact is larger than bb
1747
1748 if (held || (hovered && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= hover_visibility_delay))
1749 SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW);
1750
1751 ImRect bb_render = bb;
1752 if (held)
1753 {
1754 float mouse_delta = (g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min)[axis];
1755
1756 // Minimum pane size
1757 float size_1_maximum_delta = ImMax(lhs: 0.0f, rhs: *size1 - min_size1);
1758 float size_2_maximum_delta = ImMax(lhs: 0.0f, rhs: *size2 - min_size2);
1759 if (mouse_delta < -size_1_maximum_delta)
1760 mouse_delta = -size_1_maximum_delta;
1761 if (mouse_delta > size_2_maximum_delta)
1762 mouse_delta = size_2_maximum_delta;
1763
1764 // Apply resize
1765 if (mouse_delta != 0.0f)
1766 {
1767 *size1 = ImMax(lhs: *size1 + mouse_delta, rhs: min_size1);
1768 *size2 = ImMax(lhs: *size2 - mouse_delta, rhs: min_size2);
1769 bb_render.Translate(d: (axis == ImGuiAxis_X) ? ImVec2(mouse_delta, 0.0f) : ImVec2(0.0f, mouse_delta));
1770 MarkItemEdited(id);
1771 }
1772 }
1773
1774 // Render at new position
1775 if (bg_col & IM_COL32_A_MASK)
1776 window->DrawList->AddRectFilled(p_min: bb_render.Min, p_max: bb_render.Max, col: bg_col, rounding: 0.0f);
1777 const ImU32 col = GetColorU32(idx: held ? ImGuiCol_SeparatorActive : (hovered && g.HoveredIdTimer >= hover_visibility_delay) ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator);
1778 window->DrawList->AddRectFilled(p_min: bb_render.Min, p_max: bb_render.Max, col, rounding: 0.0f);
1779
1780 return held;
1781}
1782
1783static int IMGUI_CDECL ShrinkWidthItemComparer(const void* lhs, const void* rhs)
1784{
1785 const ImGuiShrinkWidthItem* a = (const ImGuiShrinkWidthItem*)lhs;
1786 const ImGuiShrinkWidthItem* b = (const ImGuiShrinkWidthItem*)rhs;
1787 if (int d = (int)(b->Width - a->Width))
1788 return d;
1789 return (b->Index - a->Index);
1790}
1791
1792// Shrink excess width from a set of item, by removing width from the larger items first.
1793// Set items Width to -1.0f to disable shrinking this item.
1794void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess)
1795{
1796 if (count == 1)
1797 {
1798 if (items[0].Width >= 0.0f)
1799 items[0].Width = ImMax(lhs: items[0].Width - width_excess, rhs: 1.0f);
1800 return;
1801 }
1802 ImQsort(base: items, count: (size_t)count, size_of_element: sizeof(ImGuiShrinkWidthItem), compare_func: ShrinkWidthItemComparer);
1803 int count_same_width = 1;
1804 while (width_excess > 0.0f && count_same_width < count)
1805 {
1806 while (count_same_width < count && items[0].Width <= items[count_same_width].Width)
1807 count_same_width++;
1808 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);
1809 if (max_width_to_remove_per_item <= 0.0f)
1810 break;
1811 float width_to_remove_per_item = ImMin(lhs: width_excess / count_same_width, rhs: max_width_to_remove_per_item);
1812 for (int item_n = 0; item_n < count_same_width; item_n++)
1813 items[item_n].Width -= width_to_remove_per_item;
1814 width_excess -= width_to_remove_per_item * count_same_width;
1815 }
1816
1817 // Round width and redistribute remainder
1818 // 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.
1819 width_excess = 0.0f;
1820 for (int n = 0; n < count; n++)
1821 {
1822 float width_rounded = ImTrunc(f: items[n].Width);
1823 width_excess += items[n].Width - width_rounded;
1824 items[n].Width = width_rounded;
1825 }
1826 while (width_excess > 0.0f)
1827 for (int n = 0; n < count && width_excess > 0.0f; n++)
1828 {
1829 float width_to_add = ImMin(lhs: items[n].InitialWidth - items[n].Width, rhs: 1.0f);
1830 items[n].Width += width_to_add;
1831 width_excess -= width_to_add;
1832 }
1833}
1834
1835//-------------------------------------------------------------------------
1836// [SECTION] Widgets: ComboBox
1837//-------------------------------------------------------------------------
1838// - CalcMaxPopupHeightFromItemCount() [Internal]
1839// - BeginCombo()
1840// - BeginComboPopup() [Internal]
1841// - EndCombo()
1842// - BeginComboPreview() [Internal]
1843// - EndComboPreview() [Internal]
1844// - Combo()
1845//-------------------------------------------------------------------------
1846
1847static float CalcMaxPopupHeightFromItemCount(int items_count)
1848{
1849 ImGuiContext& g = *GImGui;
1850 if (items_count <= 0)
1851 return FLT_MAX;
1852 return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2);
1853}
1854
1855bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags)
1856{
1857 ImGuiContext& g = *GImGui;
1858 ImGuiWindow* window = GetCurrentWindow();
1859
1860 ImGuiNextWindowDataFlags backup_next_window_data_flags = g.NextWindowData.HasFlags;
1861 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
1862 if (window->SkipItems)
1863 return false;
1864
1865 const ImGuiStyle& style = g.Style;
1866 const ImGuiID id = window->GetID(str: label);
1867 IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together
1868 if (flags & ImGuiComboFlags_WidthFitPreview)
1869 IM_ASSERT((flags & (ImGuiComboFlags_NoPreview | (ImGuiComboFlags)ImGuiComboFlags_CustomPreview)) == 0);
1870
1871 const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight();
1872 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
1873 const float preview_width = ((flags & ImGuiComboFlags_WidthFitPreview) && (preview_value != NULL)) ? CalcTextSize(text: preview_value, NULL, hide_text_after_double_hash: true).x : 0.0f;
1874 const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : ((flags & ImGuiComboFlags_WidthFitPreview) ? (arrow_size + preview_width + style.FramePadding.x * 2.0f) : CalcItemWidth());
1875 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
1876 const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
1877 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
1878 if (!ItemAdd(bb: total_bb, id, nav_bb: &bb))
1879 return false;
1880
1881 // Open on click
1882 bool hovered, held;
1883 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held);
1884 const ImGuiID popup_id = ImHashStr(data: "##ComboPopup", data_size: 0, seed: id);
1885 bool popup_open = IsPopupOpen(id: popup_id, popup_flags: ImGuiPopupFlags_None);
1886 if (pressed && !popup_open)
1887 {
1888 OpenPopupEx(id: popup_id, popup_flags: ImGuiPopupFlags_None);
1889 popup_open = true;
1890 }
1891
1892 // Render shape
1893 const ImU32 frame_col = GetColorU32(idx: hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
1894 const float value_x2 = ImMax(lhs: bb.Min.x, rhs: bb.Max.x - arrow_size);
1895 RenderNavCursor(bb, id);
1896 if (!(flags & ImGuiComboFlags_NoPreview))
1897 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);
1898 if (!(flags & ImGuiComboFlags_NoArrowButton))
1899 {
1900 ImU32 bg_col = GetColorU32(idx: (popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1901 ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
1902 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);
1903 if (value_x2 + arrow_size - style.FramePadding.x <= bb.Max.x)
1904 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);
1905 }
1906 RenderFrameBorder(p_min: bb.Min, p_max: bb.Max, rounding: style.FrameRounding);
1907
1908 // Custom preview
1909 if (flags & ImGuiComboFlags_CustomPreview)
1910 {
1911 g.ComboPreviewData.PreviewRect = ImRect(bb.Min.x, bb.Min.y, value_x2, bb.Max.y);
1912 IM_ASSERT(preview_value == NULL || preview_value[0] == 0);
1913 preview_value = NULL;
1914 }
1915
1916 // Render preview and label
1917 if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview))
1918 {
1919 if (g.LogEnabled)
1920 LogSetNextTextDecoration(prefix: "{", suffix: "}");
1921 RenderTextClipped(pos_min: bb.Min + style.FramePadding, pos_max: ImVec2(value_x2, bb.Max.y), text: preview_value, NULL, NULL);
1922 }
1923 if (label_size.x > 0)
1924 RenderText(pos: ImVec2(bb.Max.x + style.ItemInnerSpacing.x, bb.Min.y + style.FramePadding.y), text: label);
1925
1926 if (!popup_open)
1927 return false;
1928
1929 g.NextWindowData.HasFlags = backup_next_window_data_flags;
1930 return BeginComboPopup(popup_id, bb, flags);
1931}
1932
1933bool ImGui::BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags flags)
1934{
1935 ImGuiContext& g = *GImGui;
1936 if (!IsPopupOpen(id: popup_id, popup_flags: ImGuiPopupFlags_None))
1937 {
1938 g.NextWindowData.ClearFlags();
1939 return false;
1940 }
1941
1942 // Set popup size
1943 float w = bb.GetWidth();
1944 if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSizeConstraint)
1945 {
1946 g.NextWindowData.SizeConstraintRect.Min.x = ImMax(lhs: g.NextWindowData.SizeConstraintRect.Min.x, rhs: w);
1947 }
1948 else
1949 {
1950 if ((flags & ImGuiComboFlags_HeightMask_) == 0)
1951 flags |= ImGuiComboFlags_HeightRegular;
1952 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one
1953 int popup_max_height_in_items = -1;
1954 if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8;
1955 else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4;
1956 else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20;
1957 ImVec2 constraint_min(0.0f, 0.0f), constraint_max(FLT_MAX, FLT_MAX);
1958 if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.x <= 0.0f) // Don't apply constraints if user specified a size
1959 constraint_min.x = w;
1960 if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.y <= 0.0f)
1961 constraint_max.y = CalcMaxPopupHeightFromItemCount(items_count: popup_max_height_in_items);
1962 SetNextWindowSizeConstraints(size_min: constraint_min, size_max: constraint_max);
1963 }
1964
1965 // This is essentially a specialized version of BeginPopupEx()
1966 char name[16];
1967 ImFormatString(buf: name, IM_ARRAYSIZE(name), fmt: "##Combo_%02d", g.BeginComboDepth); // Recycle windows based on depth
1968
1969 // Set position given a custom constraint (peak into expected window size so we can position it)
1970 // FIXME: This might be easier to express with an hypothetical SetNextWindowPosConstraints() function?
1971 // FIXME: This might be moved to Begin() or at least around the same spot where Tooltips and other Popups are calling FindBestWindowPosForPopupEx()?
1972 if (ImGuiWindow* popup_window = FindWindowByName(name))
1973 if (popup_window->WasActive)
1974 {
1975 // Always override 'AutoPosLastDirection' to not leave a chance for a past value to affect us.
1976 ImVec2 size_expected = CalcWindowNextAutoFitSize(window: popup_window);
1977 popup_window->AutoPosLastDirection = (flags & ImGuiComboFlags_PopupAlignLeft) ? ImGuiDir_Left : ImGuiDir_Down; // Left = "Below, Toward Left", Down = "Below, Toward Right (default)"
1978 ImRect r_outer = GetPopupAllowedExtentRect(window: popup_window);
1979 ImVec2 pos = FindBestWindowPosForPopupEx(ref_pos: bb.GetBL(), size: size_expected, last_dir: &popup_window->AutoPosLastDirection, r_outer, r_avoid: bb, policy: ImGuiPopupPositionPolicy_ComboBox);
1980 SetNextWindowPos(pos);
1981 }
1982
1983 // We don't use BeginPopupEx() solely because we have a custom name string, which we could make an argument to BeginPopupEx()
1984 ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove;
1985 PushStyleVarX(idx: ImGuiStyleVar_WindowPadding, val_x: g.Style.FramePadding.x); // Horizontally align ourselves with the framed text
1986 bool ret = Begin(name, NULL, flags: window_flags);
1987 PopStyleVar();
1988 if (!ret)
1989 {
1990 EndPopup();
1991 IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above
1992 return false;
1993 }
1994 g.BeginComboDepth++;
1995 return true;
1996}
1997
1998void ImGui::EndCombo()
1999{
2000 ImGuiContext& g = *GImGui;
2001 EndPopup();
2002 g.BeginComboDepth--;
2003}
2004
2005// Call directly after the BeginCombo/EndCombo block. The preview is designed to only host non-interactive elements
2006// (Experimental, see GitHub issues: #1658, #4168)
2007bool ImGui::BeginComboPreview()
2008{
2009 ImGuiContext& g = *GImGui;
2010 ImGuiWindow* window = g.CurrentWindow;
2011 ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
2012
2013 if (window->SkipItems || !(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible))
2014 return false;
2015 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?
2016 if (!window->ClipRect.Overlaps(r: preview_data->PreviewRect)) // Narrower test (optional)
2017 return false;
2018
2019 // FIXME: This could be contained in a PushWorkRect() api
2020 preview_data->BackupCursorPos = window->DC.CursorPos;
2021 preview_data->BackupCursorMaxPos = window->DC.CursorMaxPos;
2022 preview_data->BackupCursorPosPrevLine = window->DC.CursorPosPrevLine;
2023 preview_data->BackupPrevLineTextBaseOffset = window->DC.PrevLineTextBaseOffset;
2024 preview_data->BackupLayout = window->DC.LayoutType;
2025 window->DC.CursorPos = preview_data->PreviewRect.Min + g.Style.FramePadding;
2026 window->DC.CursorMaxPos = window->DC.CursorPos;
2027 window->DC.LayoutType = ImGuiLayoutType_Horizontal;
2028 window->DC.IsSameLine = false;
2029 PushClipRect(clip_rect_min: preview_data->PreviewRect.Min, clip_rect_max: preview_data->PreviewRect.Max, intersect_with_current_clip_rect: true);
2030
2031 return true;
2032}
2033
2034void ImGui::EndComboPreview()
2035{
2036 ImGuiContext& g = *GImGui;
2037 ImGuiWindow* window = g.CurrentWindow;
2038 ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
2039
2040 // FIXME: Using CursorMaxPos approximation instead of correct AABB which we will store in ImDrawCmd in the future
2041 ImDrawList* draw_list = window->DrawList;
2042 if (window->DC.CursorMaxPos.x < preview_data->PreviewRect.Max.x && window->DC.CursorMaxPos.y < preview_data->PreviewRect.Max.y)
2043 if (draw_list->CmdBuffer.Size > 1) // Unlikely case that the PushClipRect() didn't create a command
2044 {
2045 draw_list->_CmdHeader.ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 2].ClipRect;
2046 draw_list->_TryMergeDrawCmds();
2047 }
2048 PopClipRect();
2049 window->DC.CursorPos = preview_data->BackupCursorPos;
2050 window->DC.CursorMaxPos = ImMax(lhs: window->DC.CursorMaxPos, rhs: preview_data->BackupCursorMaxPos);
2051 window->DC.CursorPosPrevLine = preview_data->BackupCursorPosPrevLine;
2052 window->DC.PrevLineTextBaseOffset = preview_data->BackupPrevLineTextBaseOffset;
2053 window->DC.LayoutType = preview_data->BackupLayout;
2054 window->DC.IsSameLine = false;
2055 preview_data->PreviewRect = ImRect();
2056}
2057
2058// Getter for the old Combo() API: const char*[]
2059static const char* Items_ArrayGetter(void* data, int idx)
2060{
2061 const char* const* items = (const char* const*)data;
2062 return items[idx];
2063}
2064
2065// Getter for the old Combo() API: "item1\0item2\0item3\0"
2066static const char* Items_SingleStringGetter(void* data, int idx)
2067{
2068 const char* items_separated_by_zeros = (const char*)data;
2069 int items_count = 0;
2070 const char* p = items_separated_by_zeros;
2071 while (*p)
2072 {
2073 if (idx == items_count)
2074 break;
2075 p += ImStrlen(s: p) + 1;
2076 items_count++;
2077 }
2078 return *p ? p : NULL;
2079}
2080
2081// Old API, prefer using BeginCombo() nowadays if you can.
2082bool 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)
2083{
2084 ImGuiContext& g = *GImGui;
2085
2086 // Call the getter to obtain the preview string which is a parameter to BeginCombo()
2087 const char* preview_value = NULL;
2088 if (*current_item >= 0 && *current_item < items_count)
2089 preview_value = getter(user_data, *current_item);
2090
2091 // 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.
2092 if (popup_max_height_in_items != -1 && !(g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSizeConstraint))
2093 SetNextWindowSizeConstraints(size_min: ImVec2(0, 0), size_max: ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(items_count: popup_max_height_in_items)));
2094
2095 if (!BeginCombo(label, preview_value, flags: ImGuiComboFlags_None))
2096 return false;
2097
2098 // Display items
2099 bool value_changed = false;
2100 ImGuiListClipper clipper;
2101 clipper.Begin(items_count);
2102 clipper.IncludeItemByIndex(item_index: *current_item);
2103 while (clipper.Step())
2104 for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
2105 {
2106 const char* item_text = getter(user_data, i);
2107 if (item_text == NULL)
2108 item_text = "*Unknown item*";
2109
2110 PushID(int_id: i);
2111 const bool item_selected = (i == *current_item);
2112 if (Selectable(label: item_text, selected: item_selected) && *current_item != i)
2113 {
2114 value_changed = true;
2115 *current_item = i;
2116 }
2117 if (item_selected)
2118 SetItemDefaultFocus();
2119 PopID();
2120 }
2121
2122 EndCombo();
2123 if (value_changed)
2124 MarkItemEdited(id: g.LastItemData.ID);
2125
2126 return value_changed;
2127}
2128
2129// Combo box helper allowing to pass an array of strings.
2130bool ImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items)
2131{
2132 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);
2133 return value_changed;
2134}
2135
2136// Combo box helper allowing to pass all items in a single string literal holding multiple zero-terminated items "item1\0item2\0"
2137bool ImGui::Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int height_in_items)
2138{
2139 int items_count = 0;
2140 const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open
2141 while (*p)
2142 {
2143 p += ImStrlen(s: p) + 1;
2144 items_count++;
2145 }
2146 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);
2147 return value_changed;
2148}
2149
2150#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
2151
2152struct ImGuiGetNameFromIndexOldToNewCallbackData { void* UserData; bool (*OldCallback)(void*, int, const char**); };
2153static const char* ImGuiGetNameFromIndexOldToNewCallback(void* user_data, int idx)
2154{
2155 ImGuiGetNameFromIndexOldToNewCallbackData* data = (ImGuiGetNameFromIndexOldToNewCallbackData*)user_data;
2156 const char* s = NULL;
2157 data->OldCallback(data->UserData, idx, &s);
2158 return s;
2159}
2160
2161bool 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)
2162{
2163 ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { .UserData: user_data, .OldCallback: old_getter };
2164 return ListBox(label, current_item, getter: ImGuiGetNameFromIndexOldToNewCallback, user_data: &old_to_new_data, items_count, height_in_items);
2165}
2166bool 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)
2167{
2168 ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { .UserData: user_data, .OldCallback: old_getter };
2169 return Combo(label, current_item, getter: ImGuiGetNameFromIndexOldToNewCallback, user_data: &old_to_new_data, items_count, popup_max_height_in_items);
2170}
2171
2172#endif
2173
2174//-------------------------------------------------------------------------
2175// [SECTION] Data Type and Data Formatting Helpers [Internal]
2176//-------------------------------------------------------------------------
2177// - DataTypeGetInfo()
2178// - DataTypeFormatString()
2179// - DataTypeApplyOp()
2180// - DataTypeApplyFromText()
2181// - DataTypeCompare()
2182// - DataTypeClamp()
2183// - GetMinimumStepAtDecimalPrecision
2184// - RoundScalarWithFormat<>()
2185//-------------------------------------------------------------------------
2186
2187static const ImGuiDataTypeInfo GDataTypeInfo[] =
2188{
2189 { .Size: sizeof(char), .Name: "S8", .PrintFmt: "%d", .ScanFmt: "%d" }, // ImGuiDataType_S8
2190 { .Size: sizeof(unsigned char), .Name: "U8", .PrintFmt: "%u", .ScanFmt: "%u" },
2191 { .Size: sizeof(short), .Name: "S16", .PrintFmt: "%d", .ScanFmt: "%d" }, // ImGuiDataType_S16
2192 { .Size: sizeof(unsigned short), .Name: "U16", .PrintFmt: "%u", .ScanFmt: "%u" },
2193 { .Size: sizeof(int), .Name: "S32", .PrintFmt: "%d", .ScanFmt: "%d" }, // ImGuiDataType_S32
2194 { .Size: sizeof(unsigned int), .Name: "U32", .PrintFmt: "%u", .ScanFmt: "%u" },
2195#ifdef _MSC_VER
2196 { sizeof(ImS64), "S64", "%I64d","%I64d" }, // ImGuiDataType_S64
2197 { sizeof(ImU64), "U64", "%I64u","%I64u" },
2198#else
2199 { .Size: sizeof(ImS64), .Name: "S64", .PrintFmt: "%lld", .ScanFmt: "%lld" }, // ImGuiDataType_S64
2200 { .Size: sizeof(ImU64), .Name: "U64", .PrintFmt: "%llu", .ScanFmt: "%llu" },
2201#endif
2202 { .Size: sizeof(float), .Name: "float", .PrintFmt: "%.3f",.ScanFmt: "%f" }, // ImGuiDataType_Float (float are promoted to double in va_arg)
2203 { .Size: sizeof(double), .Name: "double",.PrintFmt: "%f", .ScanFmt: "%lf" }, // ImGuiDataType_Double
2204 { .Size: sizeof(bool), .Name: "bool", .PrintFmt: "%d", .ScanFmt: "%d" }, // ImGuiDataType_Bool
2205 { .Size: 0, .Name: "char*",.PrintFmt: "%s", .ScanFmt: "%s" }, // ImGuiDataType_String
2206};
2207IM_STATIC_ASSERT(IM_ARRAYSIZE(GDataTypeInfo) == ImGuiDataType_COUNT);
2208
2209const ImGuiDataTypeInfo* ImGui::DataTypeGetInfo(ImGuiDataType data_type)
2210{
2211 IM_ASSERT(data_type >= 0 && data_type < ImGuiDataType_COUNT);
2212 return &GDataTypeInfo[data_type];
2213}
2214
2215int ImGui::DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* p_data, const char* format)
2216{
2217 // Signedness doesn't matter when pushing integer arguments
2218 if (data_type == ImGuiDataType_S32 || data_type == ImGuiDataType_U32)
2219 return ImFormatString(buf, buf_size, fmt: format, *(const ImU32*)p_data);
2220 if (data_type == ImGuiDataType_S64 || data_type == ImGuiDataType_U64)
2221 return ImFormatString(buf, buf_size, fmt: format, *(const ImU64*)p_data);
2222 if (data_type == ImGuiDataType_Float)
2223 return ImFormatString(buf, buf_size, fmt: format, *(const float*)p_data);
2224 if (data_type == ImGuiDataType_Double)
2225 return ImFormatString(buf, buf_size, fmt: format, *(const double*)p_data);
2226 if (data_type == ImGuiDataType_S8)
2227 return ImFormatString(buf, buf_size, fmt: format, *(const ImS8*)p_data);
2228 if (data_type == ImGuiDataType_U8)
2229 return ImFormatString(buf, buf_size, fmt: format, *(const ImU8*)p_data);
2230 if (data_type == ImGuiDataType_S16)
2231 return ImFormatString(buf, buf_size, fmt: format, *(const ImS16*)p_data);
2232 if (data_type == ImGuiDataType_U16)
2233 return ImFormatString(buf, buf_size, fmt: format, *(const ImU16*)p_data);
2234 IM_ASSERT(0);
2235 return 0;
2236}
2237
2238void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg1, const void* arg2)
2239{
2240 IM_ASSERT(op == '+' || op == '-');
2241 switch (data_type)
2242 {
2243 case ImGuiDataType_S8:
2244 if (op == '+') { *(ImS8*)output = ImAddClampOverflow(a: *(const ImS8*)arg1, b: *(const ImS8*)arg2, mn: IM_S8_MIN, mx: IM_S8_MAX); }
2245 if (op == '-') { *(ImS8*)output = ImSubClampOverflow(a: *(const ImS8*)arg1, b: *(const ImS8*)arg2, mn: IM_S8_MIN, mx: IM_S8_MAX); }
2246 return;
2247 case ImGuiDataType_U8:
2248 if (op == '+') { *(ImU8*)output = ImAddClampOverflow(a: *(const ImU8*)arg1, b: *(const ImU8*)arg2, mn: IM_U8_MIN, mx: IM_U8_MAX); }
2249 if (op == '-') { *(ImU8*)output = ImSubClampOverflow(a: *(const ImU8*)arg1, b: *(const ImU8*)arg2, mn: IM_U8_MIN, mx: IM_U8_MAX); }
2250 return;
2251 case ImGuiDataType_S16:
2252 if (op == '+') { *(ImS16*)output = ImAddClampOverflow(a: *(const ImS16*)arg1, b: *(const ImS16*)arg2, mn: IM_S16_MIN, mx: IM_S16_MAX); }
2253 if (op == '-') { *(ImS16*)output = ImSubClampOverflow(a: *(const ImS16*)arg1, b: *(const ImS16*)arg2, mn: IM_S16_MIN, mx: IM_S16_MAX); }
2254 return;
2255 case ImGuiDataType_U16:
2256 if (op == '+') { *(ImU16*)output = ImAddClampOverflow(a: *(const ImU16*)arg1, b: *(const ImU16*)arg2, mn: IM_U16_MIN, mx: IM_U16_MAX); }
2257 if (op == '-') { *(ImU16*)output = ImSubClampOverflow(a: *(const ImU16*)arg1, b: *(const ImU16*)arg2, mn: IM_U16_MIN, mx: IM_U16_MAX); }
2258 return;
2259 case ImGuiDataType_S32:
2260 if (op == '+') { *(ImS32*)output = ImAddClampOverflow(a: *(const ImS32*)arg1, b: *(const ImS32*)arg2, mn: IM_S32_MIN, mx: IM_S32_MAX); }
2261 if (op == '-') { *(ImS32*)output = ImSubClampOverflow(a: *(const ImS32*)arg1, b: *(const ImS32*)arg2, mn: IM_S32_MIN, mx: IM_S32_MAX); }
2262 return;
2263 case ImGuiDataType_U32:
2264 if (op == '+') { *(ImU32*)output = ImAddClampOverflow(a: *(const ImU32*)arg1, b: *(const ImU32*)arg2, mn: IM_U32_MIN, mx: IM_U32_MAX); }
2265 if (op == '-') { *(ImU32*)output = ImSubClampOverflow(a: *(const ImU32*)arg1, b: *(const ImU32*)arg2, mn: IM_U32_MIN, mx: IM_U32_MAX); }
2266 return;
2267 case ImGuiDataType_S64:
2268 if (op == '+') { *(ImS64*)output = ImAddClampOverflow(a: *(const ImS64*)arg1, b: *(const ImS64*)arg2, mn: IM_S64_MIN, mx: IM_S64_MAX); }
2269 if (op == '-') { *(ImS64*)output = ImSubClampOverflow(a: *(const ImS64*)arg1, b: *(const ImS64*)arg2, mn: IM_S64_MIN, mx: IM_S64_MAX); }
2270 return;
2271 case ImGuiDataType_U64:
2272 if (op == '+') { *(ImU64*)output = ImAddClampOverflow(a: *(const ImU64*)arg1, b: *(const ImU64*)arg2, mn: IM_U64_MIN, mx: IM_U64_MAX); }
2273 if (op == '-') { *(ImU64*)output = ImSubClampOverflow(a: *(const ImU64*)arg1, b: *(const ImU64*)arg2, mn: IM_U64_MIN, mx: IM_U64_MAX); }
2274 return;
2275 case ImGuiDataType_Float:
2276 if (op == '+') { *(float*)output = *(const float*)arg1 + *(const float*)arg2; }
2277 if (op == '-') { *(float*)output = *(const float*)arg1 - *(const float*)arg2; }
2278 return;
2279 case ImGuiDataType_Double:
2280 if (op == '+') { *(double*)output = *(const double*)arg1 + *(const double*)arg2; }
2281 if (op == '-') { *(double*)output = *(const double*)arg1 - *(const double*)arg2; }
2282 return;
2283 case ImGuiDataType_COUNT: break;
2284 }
2285 IM_ASSERT(0);
2286}
2287
2288// User can input math operators (e.g. +100) to edit a numerical values.
2289// NB: This is _not_ a full expression evaluator. We should probably add one and replace this dumb mess..
2290bool ImGui::DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format, void* p_data_when_empty)
2291{
2292 // Copy the value in an opaque buffer so we can compare at the end of the function if it changed at all.
2293 const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
2294 ImGuiDataTypeStorage data_backup;
2295 memcpy(dest: &data_backup, src: p_data, n: type_info->Size);
2296
2297 while (ImCharIsBlankA(c: *buf))
2298 buf++;
2299 if (!buf[0])
2300 {
2301 if (p_data_when_empty != NULL)
2302 {
2303 memcpy(dest: p_data, src: p_data_when_empty, n: type_info->Size);
2304 return memcmp(s1: &data_backup, s2: p_data, n: type_info->Size) != 0;
2305 }
2306 return false;
2307 }
2308
2309 // Sanitize format
2310 // - 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
2311 // - In theory could treat empty format as using default, but this would only cover rare/bizarre case of using InputScalar() + integer + format string without %.
2312 char format_sanitized[32];
2313 if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double)
2314 format = type_info->ScanFmt;
2315 else
2316 format = ImParseFormatSanitizeForScanning(fmt_in: format, fmt_out: format_sanitized, IM_ARRAYSIZE(format_sanitized));
2317
2318 // Small types need a 32-bit buffer to receive the result from scanf()
2319 int v32 = 0;
2320 if (sscanf(s: buf, format: format, type_info->Size >= 4 ? p_data : &v32) < 1)
2321 return false;
2322 if (type_info->Size < 4)
2323 {
2324 if (data_type == ImGuiDataType_S8)
2325 *(ImS8*)p_data = (ImS8)ImClamp(v: v32, mn: (int)IM_S8_MIN, mx: (int)IM_S8_MAX);
2326 else if (data_type == ImGuiDataType_U8)
2327 *(ImU8*)p_data = (ImU8)ImClamp(v: v32, mn: (int)IM_U8_MIN, mx: (int)IM_U8_MAX);
2328 else if (data_type == ImGuiDataType_S16)
2329 *(ImS16*)p_data = (ImS16)ImClamp(v: v32, mn: (int)IM_S16_MIN, mx: (int)IM_S16_MAX);
2330 else if (data_type == ImGuiDataType_U16)
2331 *(ImU16*)p_data = (ImU16)ImClamp(v: v32, mn: (int)IM_U16_MIN, mx: (int)IM_U16_MAX);
2332 else
2333 IM_ASSERT(0);
2334 }
2335
2336 return memcmp(s1: &data_backup, s2: p_data, n: type_info->Size) != 0;
2337}
2338
2339template<typename T>
2340static int DataTypeCompareT(const T* lhs, const T* rhs)
2341{
2342 if (*lhs < *rhs) return -1;
2343 if (*lhs > *rhs) return +1;
2344 return 0;
2345}
2346
2347int ImGui::DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2)
2348{
2349 switch (data_type)
2350 {
2351 case ImGuiDataType_S8: return DataTypeCompareT<ImS8 >(lhs: (const ImS8* )arg_1, rhs: (const ImS8* )arg_2);
2352 case ImGuiDataType_U8: return DataTypeCompareT<ImU8 >(lhs: (const ImU8* )arg_1, rhs: (const ImU8* )arg_2);
2353 case ImGuiDataType_S16: return DataTypeCompareT<ImS16 >(lhs: (const ImS16* )arg_1, rhs: (const ImS16* )arg_2);
2354 case ImGuiDataType_U16: return DataTypeCompareT<ImU16 >(lhs: (const ImU16* )arg_1, rhs: (const ImU16* )arg_2);
2355 case ImGuiDataType_S32: return DataTypeCompareT<ImS32 >(lhs: (const ImS32* )arg_1, rhs: (const ImS32* )arg_2);
2356 case ImGuiDataType_U32: return DataTypeCompareT<ImU32 >(lhs: (const ImU32* )arg_1, rhs: (const ImU32* )arg_2);
2357 case ImGuiDataType_S64: return DataTypeCompareT<ImS64 >(lhs: (const ImS64* )arg_1, rhs: (const ImS64* )arg_2);
2358 case ImGuiDataType_U64: return DataTypeCompareT<ImU64 >(lhs: (const ImU64* )arg_1, rhs: (const ImU64* )arg_2);
2359 case ImGuiDataType_Float: return DataTypeCompareT<float >(lhs: (const float* )arg_1, rhs: (const float* )arg_2);
2360 case ImGuiDataType_Double: return DataTypeCompareT<double>(lhs: (const double*)arg_1, rhs: (const double*)arg_2);
2361 case ImGuiDataType_COUNT: break;
2362 }
2363 IM_ASSERT(0);
2364 return 0;
2365}
2366
2367template<typename T>
2368static bool DataTypeClampT(T* v, const T* v_min, const T* v_max)
2369{
2370 // Clamp, both sides are optional, return true if modified
2371 if (v_min && *v < *v_min) { *v = *v_min; return true; }
2372 if (v_max && *v > *v_max) { *v = *v_max; return true; }
2373 return false;
2374}
2375
2376bool ImGui::DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max)
2377{
2378 switch (data_type)
2379 {
2380 case ImGuiDataType_S8: return DataTypeClampT<ImS8 >(v: (ImS8* )p_data, v_min: (const ImS8* )p_min, v_max: (const ImS8* )p_max);
2381 case ImGuiDataType_U8: return DataTypeClampT<ImU8 >(v: (ImU8* )p_data, v_min: (const ImU8* )p_min, v_max: (const ImU8* )p_max);
2382 case ImGuiDataType_S16: return DataTypeClampT<ImS16 >(v: (ImS16* )p_data, v_min: (const ImS16* )p_min, v_max: (const ImS16* )p_max);
2383 case ImGuiDataType_U16: return DataTypeClampT<ImU16 >(v: (ImU16* )p_data, v_min: (const ImU16* )p_min, v_max: (const ImU16* )p_max);
2384 case ImGuiDataType_S32: return DataTypeClampT<ImS32 >(v: (ImS32* )p_data, v_min: (const ImS32* )p_min, v_max: (const ImS32* )p_max);
2385 case ImGuiDataType_U32: return DataTypeClampT<ImU32 >(v: (ImU32* )p_data, v_min: (const ImU32* )p_min, v_max: (const ImU32* )p_max);
2386 case ImGuiDataType_S64: return DataTypeClampT<ImS64 >(v: (ImS64* )p_data, v_min: (const ImS64* )p_min, v_max: (const ImS64* )p_max);
2387 case ImGuiDataType_U64: return DataTypeClampT<ImU64 >(v: (ImU64* )p_data, v_min: (const ImU64* )p_min, v_max: (const ImU64* )p_max);
2388 case ImGuiDataType_Float: return DataTypeClampT<float >(v: (float* )p_data, v_min: (const float* )p_min, v_max: (const float* )p_max);
2389 case ImGuiDataType_Double: return DataTypeClampT<double>(v: (double*)p_data, v_min: (const double*)p_min, v_max: (const double*)p_max);
2390 case ImGuiDataType_COUNT: break;
2391 }
2392 IM_ASSERT(0);
2393 return false;
2394}
2395
2396bool ImGui::DataTypeIsZero(ImGuiDataType data_type, const void* p_data)
2397{
2398 ImGuiContext& g = *GImGui;
2399 return DataTypeCompare(data_type, arg_1: p_data, arg_2: &g.DataTypeZeroValue) == 0;
2400}
2401
2402static float GetMinimumStepAtDecimalPrecision(int decimal_precision)
2403{
2404 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 };
2405 if (decimal_precision < 0)
2406 return FLT_MIN;
2407 return (decimal_precision < IM_ARRAYSIZE(min_steps)) ? min_steps[decimal_precision] : ImPow(x: 10.0f, y: (float)-decimal_precision);
2408}
2409
2410template<typename TYPE>
2411TYPE ImGui::RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, TYPE v)
2412{
2413 IM_UNUSED(data_type);
2414 IM_ASSERT(data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double);
2415 const char* fmt_start = ImParseFormatFindStart(format);
2416 if (fmt_start[0] != '%' || fmt_start[1] == '%') // Don't apply if the value is not visible in the format string
2417 return v;
2418
2419 // Sanitize format
2420 char fmt_sanitized[32];
2421 ImParseFormatSanitizeForPrinting(fmt_in: fmt_start, fmt_out: fmt_sanitized, IM_ARRAYSIZE(fmt_sanitized));
2422 fmt_start = fmt_sanitized;
2423
2424 // Format value with our rounding, and read back
2425 char v_str[64];
2426 ImFormatString(v_str, IM_ARRAYSIZE(v_str), fmt_start, v);
2427 const char* p = v_str;
2428 while (*p == ' ')
2429 p++;
2430 v = (TYPE)ImAtof(p);
2431
2432 return v;
2433}
2434
2435//-------------------------------------------------------------------------
2436// [SECTION] Widgets: DragScalar, DragFloat, DragInt, etc.
2437//-------------------------------------------------------------------------
2438// - DragBehaviorT<>() [Internal]
2439// - DragBehavior() [Internal]
2440// - DragScalar()
2441// - DragScalarN()
2442// - DragFloat()
2443// - DragFloat2()
2444// - DragFloat3()
2445// - DragFloat4()
2446// - DragFloatRange2()
2447// - DragInt()
2448// - DragInt2()
2449// - DragInt3()
2450// - DragInt4()
2451// - DragIntRange2()
2452//-------------------------------------------------------------------------
2453
2454// This is called by DragBehavior() when the widget is active (held by mouse or being manipulated with Nav controls)
2455template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2456bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags)
2457{
2458 ImGuiContext& g = *GImGui;
2459 const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
2460 const bool is_bounded = (v_min < v_max) || ((v_min == v_max) && (v_min != 0.0f || (flags & ImGuiSliderFlags_ClampZeroRange)));
2461 const bool is_wrapped = is_bounded && (flags & ImGuiSliderFlags_WrapAround);
2462 const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
2463 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2464
2465 // Default tweak speed
2466 if (v_speed == 0.0f && is_bounded && (v_max - v_min < FLT_MAX))
2467 v_speed = (float)((v_max - v_min) * g.DragSpeedDefaultRatio);
2468
2469 // Inputs accumulates into g.DragCurrentAccum, which is flushed into the current value as soon as it makes a difference with our precision settings
2470 float adjust_delta = 0.0f;
2471 if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid() && IsMouseDragPastThreshold(button: 0, lock_threshold: g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2472 {
2473 adjust_delta = g.IO.MouseDelta[axis];
2474 if (g.IO.KeyAlt && !(flags & ImGuiSliderFlags_NoSpeedTweaks))
2475 adjust_delta *= 1.0f / 100.0f;
2476 if (g.IO.KeyShift && !(flags & ImGuiSliderFlags_NoSpeedTweaks))
2477 adjust_delta *= 10.0f;
2478 }
2479 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
2480 {
2481 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, default_value: 3) : 0;
2482 const bool tweak_slow = IsKeyDown(key: (g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
2483 const bool tweak_fast = IsKeyDown(key: (g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
2484 const float tweak_factor = (flags & ImGuiSliderFlags_NoSpeedTweaks) ? 1.0f : tweak_slow ? 1.0f / 10.0f : tweak_fast ? 10.0f : 1.0f;
2485 adjust_delta = GetNavTweakPressedAmount(axis) * tweak_factor;
2486 v_speed = ImMax(lhs: v_speed, rhs: GetMinimumStepAtDecimalPrecision(decimal_precision));
2487 }
2488 adjust_delta *= v_speed;
2489
2490 // For vertical drag we currently assume that Up=higher value (like we do with vertical sliders). This may become a parameter.
2491 if (axis == ImGuiAxis_Y)
2492 adjust_delta = -adjust_delta;
2493
2494 // For logarithmic use our range is effectively 0..1 so scale the delta into that range
2495 if (is_logarithmic && (v_max - v_min < FLT_MAX) && ((v_max - v_min) > 0.000001f)) // Epsilon to avoid /0
2496 adjust_delta /= (float)(v_max - v_min);
2497
2498 // Clear current value on activation
2499 // 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.
2500 const bool is_just_activated = g.ActiveIdIsJustActivated;
2501 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));
2502 if (is_just_activated || is_already_past_limits_and_pushing_outward)
2503 {
2504 g.DragCurrentAccum = 0.0f;
2505 g.DragCurrentAccumDirty = false;
2506 }
2507 else if (adjust_delta != 0.0f)
2508 {
2509 g.DragCurrentAccum += adjust_delta;
2510 g.DragCurrentAccumDirty = true;
2511 }
2512
2513 if (!g.DragCurrentAccumDirty)
2514 return false;
2515
2516 TYPE v_cur = *v;
2517 FLOATTYPE v_old_ref_for_accum_remainder = (FLOATTYPE)0.0f;
2518
2519 float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
2520 const float zero_deadzone_halfsize = 0.0f; // Drag widgets have no deadzone (as it doesn't make sense)
2521 if (is_logarithmic)
2522 {
2523 // 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.
2524 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, default_value: 3) : 1;
2525 logarithmic_zero_epsilon = ImPow(x: 0.1f, y: (float)decimal_precision);
2526
2527 // Convert to parametric space, apply delta, convert back
2528 float v_old_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2529 float v_new_parametric = v_old_parametric + g.DragCurrentAccum;
2530 v_cur = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new_parametric, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2531 v_old_ref_for_accum_remainder = v_old_parametric;
2532 }
2533 else
2534 {
2535 v_cur += (SIGNEDTYPE)g.DragCurrentAccum;
2536 }
2537
2538 // Round to user desired precision based on format string
2539 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
2540 v_cur = RoundScalarWithFormatT<TYPE>(format, data_type, v_cur);
2541
2542 // Preserve remainder after rounding has been applied. This also allow slow tweaking of values.
2543 g.DragCurrentAccumDirty = false;
2544 if (is_logarithmic)
2545 {
2546 // Convert to parametric space, apply delta, convert back
2547 float v_new_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2548 g.DragCurrentAccum -= (float)(v_new_parametric - v_old_ref_for_accum_remainder);
2549 }
2550 else
2551 {
2552 g.DragCurrentAccum -= (float)((SIGNEDTYPE)v_cur - (SIGNEDTYPE)*v);
2553 }
2554
2555 // Lose zero sign for float/double
2556 if (v_cur == (TYPE)-0)
2557 v_cur = (TYPE)0;
2558
2559 if (*v != v_cur && is_bounded)
2560 {
2561 if (is_wrapped)
2562 {
2563 // Wrap values
2564 if (v_cur < v_min)
2565 v_cur += v_max - v_min + (is_floating_point ? 0 : 1);
2566 if (v_cur > v_max)
2567 v_cur -= v_max - v_min + (is_floating_point ? 0 : 1);
2568 }
2569 else
2570 {
2571 // Clamp values + handle overflow/wrap-around for integer types.
2572 if (v_cur < v_min || (v_cur > *v && adjust_delta < 0.0f && !is_floating_point))
2573 v_cur = v_min;
2574 if (v_cur > v_max || (v_cur < *v && adjust_delta > 0.0f && !is_floating_point))
2575 v_cur = v_max;
2576 }
2577 }
2578
2579 // Apply result
2580 if (*v == v_cur)
2581 return false;
2582 *v = v_cur;
2583 return true;
2584}
2585
2586bool 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)
2587{
2588 // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
2589 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.");
2590
2591 ImGuiContext& g = *GImGui;
2592 if (g.ActiveId == id)
2593 {
2594 // Those are the things we can do easily outside the DragBehaviorT<> template, saves code generation.
2595 if (g.ActiveIdSource == ImGuiInputSource_Mouse && !g.IO.MouseDown[0])
2596 ClearActiveID();
2597 else if ((g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) && g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
2598 ClearActiveID();
2599 }
2600 if (g.ActiveId != id)
2601 return false;
2602 if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
2603 return false;
2604
2605 switch (data_type)
2606 {
2607 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; }
2608 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; }
2609 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; }
2610 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; }
2611 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);
2612 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);
2613 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);
2614 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);
2615 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);
2616 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);
2617 case ImGuiDataType_COUNT: break;
2618 }
2619 IM_ASSERT(0);
2620 return false;
2621}
2622
2623// 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.
2624// Read code of e.g. DragFloat(), DragInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
2625bool 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)
2626{
2627 ImGuiWindow* window = GetCurrentWindow();
2628 if (window->SkipItems)
2629 return false;
2630
2631 ImGuiContext& g = *GImGui;
2632 const ImGuiStyle& style = g.Style;
2633 const ImGuiID id = window->GetID(str: label);
2634 const float w = CalcItemWidth();
2635
2636 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
2637 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
2638 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));
2639
2640 const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
2641 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
2642 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
2643 return false;
2644
2645 // Default format string when passing NULL
2646 if (format == NULL)
2647 format = DataTypeGetInfo(data_type)->PrintFmt;
2648
2649 const bool hovered = ItemHoverable(bb: frame_bb, id, item_flags: g.LastItemData.ItemFlags);
2650 bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
2651 if (!temp_input_is_active)
2652 {
2653 // Tabbing or CTRL+click on Drag turns it into an InputText
2654 const bool clicked = hovered && IsMouseClicked(button: 0, flags: ImGuiInputFlags_None, owner_id: id);
2655 const bool double_clicked = (hovered && g.IO.MouseClickedCount[0] == 2 && TestKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id));
2656 const bool make_active = (clicked || double_clicked || g.NavActivateId == id);
2657 if (make_active && (clicked || double_clicked))
2658 SetKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id);
2659 if (make_active && temp_input_allowed)
2660 if ((clicked && g.IO.KeyCtrl) || double_clicked || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
2661 temp_input_is_active = true;
2662
2663 // (Optional) simple click (without moving) turns Drag into an InputText
2664 if (g.IO.ConfigDragClickToInputText && temp_input_allowed && !temp_input_is_active)
2665 if (g.ActiveId == id && hovered && g.IO.MouseReleased[0] && !IsMouseDragPastThreshold(button: 0, lock_threshold: g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2666 {
2667 g.NavActivateId = id;
2668 g.NavActivateFlags = ImGuiActivateFlags_PreferInput;
2669 temp_input_is_active = true;
2670 }
2671
2672 // Store initial value (not used by main lib but available as a convenience but some mods e.g. to revert)
2673 if (make_active)
2674 memcpy(dest: &g.ActiveIdValueOnActivation, src: p_data, n: DataTypeGetInfo(data_type)->Size);
2675
2676 if (make_active && !temp_input_is_active)
2677 {
2678 SetActiveID(id, window);
2679 SetFocusID(id, window);
2680 FocusWindow(window);
2681 g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
2682 }
2683 }
2684
2685 if (temp_input_is_active)
2686 {
2687 // Only clamp CTRL+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp)
2688 bool clamp_enabled = false;
2689 if ((flags & ImGuiSliderFlags_ClampOnInput) && (p_min != NULL || p_max != NULL))
2690 {
2691 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
2692 if (p_min == NULL || p_max == NULL || clamp_range_dir < 0)
2693 clamp_enabled = true;
2694 else if (clamp_range_dir == 0)
2695 clamp_enabled = DataTypeIsZero(data_type, p_data: p_min) ? ((flags & ImGuiSliderFlags_ClampZeroRange) != 0) : true;
2696 }
2697 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);
2698 }
2699
2700 // Draw frame
2701 const ImU32 frame_col = GetColorU32(idx: g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
2702 RenderNavCursor(bb: frame_bb, id);
2703 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: frame_col, borders: true, rounding: style.FrameRounding);
2704
2705 // Drag behavior
2706 const bool value_changed = DragBehavior(id, data_type, p_v: p_data, v_speed, p_min, p_max, format, flags);
2707 if (value_changed)
2708 MarkItemEdited(id);
2709
2710 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
2711 char value_buf[64];
2712 const char* value_buf_end = value_buf + DataTypeFormatString(buf: value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
2713 if (g.LogEnabled)
2714 LogSetNextTextDecoration(prefix: "{", suffix: "}");
2715 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));
2716
2717 if (label_size.x > 0.0f)
2718 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), text: label);
2719
2720 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
2721 return value_changed;
2722}
2723
2724bool 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)
2725{
2726 ImGuiWindow* window = GetCurrentWindow();
2727 if (window->SkipItems)
2728 return false;
2729
2730 ImGuiContext& g = *GImGui;
2731 bool value_changed = false;
2732 BeginGroup();
2733 PushID(str_id: label);
2734 PushMultiItemsWidths(components, width_full: CalcItemWidth());
2735 size_t type_size = GDataTypeInfo[data_type].Size;
2736 for (int i = 0; i < components; i++)
2737 {
2738 PushID(int_id: i);
2739 if (i > 0)
2740 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2741 value_changed |= DragScalar(label: "", data_type, p_data, v_speed, p_min, p_max, format, flags);
2742 PopID();
2743 PopItemWidth();
2744 p_data = (void*)((char*)p_data + type_size);
2745 }
2746 PopID();
2747
2748 const char* label_end = FindRenderedTextEnd(text: label);
2749 if (label != label_end)
2750 {
2751 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2752 TextEx(text: label, text_end: label_end);
2753 }
2754
2755 EndGroup();
2756 return value_changed;
2757}
2758
2759bool ImGui::DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2760{
2761 return DragScalar(label, data_type: ImGuiDataType_Float, p_data: v, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2762}
2763
2764bool ImGui::DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2765{
2766 return DragScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 2, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2767}
2768
2769bool ImGui::DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2770{
2771 return DragScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 3, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2772}
2773
2774bool ImGui::DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2775{
2776 return DragScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 4, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2777}
2778
2779// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2780bool 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)
2781{
2782 ImGuiWindow* window = GetCurrentWindow();
2783 if (window->SkipItems)
2784 return false;
2785
2786 ImGuiContext& g = *GImGui;
2787 PushID(str_id: label);
2788 BeginGroup();
2789 PushMultiItemsWidths(components: 2, width_full: CalcItemWidth());
2790
2791 float min_min = (v_min >= v_max) ? -FLT_MAX : v_min;
2792 float min_max = (v_min >= v_max) ? *v_current_max : ImMin(lhs: v_max, rhs: *v_current_max);
2793 ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2794 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);
2795 PopItemWidth();
2796 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2797
2798 float max_min = (v_min >= v_max) ? *v_current_min : ImMax(lhs: v_min, rhs: *v_current_min);
2799 float max_max = (v_min >= v_max) ? FLT_MAX : v_max;
2800 ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2801 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);
2802 PopItemWidth();
2803 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2804
2805 TextEx(text: label, text_end: FindRenderedTextEnd(text: label));
2806 EndGroup();
2807 PopID();
2808
2809 return value_changed;
2810}
2811
2812// NB: v_speed is float to allow adjusting the drag speed with more precision
2813bool ImGui::DragInt(const char* label, int* v, float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2814{
2815 return DragScalar(label, data_type: ImGuiDataType_S32, p_data: v, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2816}
2817
2818bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2819{
2820 return DragScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 2, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2821}
2822
2823bool ImGui::DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2824{
2825 return DragScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 3, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2826}
2827
2828bool ImGui::DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2829{
2830 return DragScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 4, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2831}
2832
2833// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2834bool 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)
2835{
2836 ImGuiWindow* window = GetCurrentWindow();
2837 if (window->SkipItems)
2838 return false;
2839
2840 ImGuiContext& g = *GImGui;
2841 PushID(str_id: label);
2842 BeginGroup();
2843 PushMultiItemsWidths(components: 2, width_full: CalcItemWidth());
2844
2845 int min_min = (v_min >= v_max) ? INT_MIN : v_min;
2846 int min_max = (v_min >= v_max) ? *v_current_max : ImMin(lhs: v_max, rhs: *v_current_max);
2847 ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2848 bool value_changed = DragInt(label: "##min", v: v_current_min, v_speed, v_min: min_min, v_max: min_max, format, flags: min_flags);
2849 PopItemWidth();
2850 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2851
2852 int max_min = (v_min >= v_max) ? *v_current_min : ImMax(lhs: v_min, rhs: *v_current_min);
2853 int max_max = (v_min >= v_max) ? INT_MAX : v_max;
2854 ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2855 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);
2856 PopItemWidth();
2857 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2858
2859 TextEx(text: label, text_end: FindRenderedTextEnd(text: label));
2860 EndGroup();
2861 PopID();
2862
2863 return value_changed;
2864}
2865
2866//-------------------------------------------------------------------------
2867// [SECTION] Widgets: SliderScalar, SliderFloat, SliderInt, etc.
2868//-------------------------------------------------------------------------
2869// - ScaleRatioFromValueT<> [Internal]
2870// - ScaleValueFromRatioT<> [Internal]
2871// - SliderBehaviorT<>() [Internal]
2872// - SliderBehavior() [Internal]
2873// - SliderScalar()
2874// - SliderScalarN()
2875// - SliderFloat()
2876// - SliderFloat2()
2877// - SliderFloat3()
2878// - SliderFloat4()
2879// - SliderAngle()
2880// - SliderInt()
2881// - SliderInt2()
2882// - SliderInt3()
2883// - SliderInt4()
2884// - VSliderScalar()
2885// - VSliderFloat()
2886// - VSliderInt()
2887//-------------------------------------------------------------------------
2888
2889// Convert a value v in the output space of a slider into a parametric position on the slider itself (the logical opposite of ScaleValueFromRatioT)
2890template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2891float ImGui::ScaleRatioFromValueT(ImGuiDataType data_type, TYPE v, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize)
2892{
2893 if (v_min == v_max)
2894 return 0.0f;
2895 IM_UNUSED(data_type);
2896
2897 const TYPE v_clamped = (v_min < v_max) ? ImClamp(v, v_min, v_max) : ImClamp(v, v_max, v_min);
2898 if (is_logarithmic)
2899 {
2900 bool flipped = v_max < v_min;
2901
2902 if (flipped) // Handle the case where the range is backwards
2903 ImSwap(v_min, v_max);
2904
2905 // Fudge min/max to avoid getting close to log(0)
2906 FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
2907 FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
2908
2909 // Awkward special cases - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
2910 if ((v_min == 0.0f) && (v_max < 0.0f))
2911 v_min_fudged = -logarithmic_zero_epsilon;
2912 else if ((v_max == 0.0f) && (v_min < 0.0f))
2913 v_max_fudged = -logarithmic_zero_epsilon;
2914
2915 float result;
2916 if (v_clamped <= v_min_fudged)
2917 result = 0.0f; // Workaround for values that are in-range but below our fudge
2918 else if (v_clamped >= v_max_fudged)
2919 result = 1.0f; // Workaround for values that are in-range but above our fudge
2920 else if ((v_min * v_max) < 0.0f) // Range crosses zero, so split into two portions
2921 {
2922 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)
2923 float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
2924 float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
2925 if (v == 0.0f)
2926 result = zero_point_center; // Special case for exactly zero
2927 else if (v < 0.0f)
2928 result = (1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(-v_min_fudged / logarithmic_zero_epsilon))) * zero_point_snap_L;
2929 else
2930 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));
2931 }
2932 else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
2933 result = 1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / -v_max_fudged) / ImLog(-v_min_fudged / -v_max_fudged));
2934 else
2935 result = (float)(ImLog((FLOATTYPE)v_clamped / v_min_fudged) / ImLog(v_max_fudged / v_min_fudged));
2936
2937 return flipped ? (1.0f - result) : result;
2938 }
2939 else
2940 {
2941 // Linear slider
2942 return (float)((FLOATTYPE)(SIGNEDTYPE)(v_clamped - v_min) / (FLOATTYPE)(SIGNEDTYPE)(v_max - v_min));
2943 }
2944}
2945
2946// Convert a parametric position on a slider into a value v in the output space (the logical opposite of ScaleRatioFromValueT)
2947template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2948TYPE ImGui::ScaleValueFromRatioT(ImGuiDataType data_type, float t, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize)
2949{
2950 // We special-case the extents because otherwise our logarithmic fudging can lead to "mathematically correct"
2951 // but non-intuitive behaviors like a fully-left slider not actually reaching the minimum value. Also generally simpler.
2952 if (t <= 0.0f || v_min == v_max)
2953 return v_min;
2954 if (t >= 1.0f)
2955 return v_max;
2956
2957 TYPE result = (TYPE)0;
2958 if (is_logarithmic)
2959 {
2960 // Fudge min/max to avoid getting silly results close to zero
2961 FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
2962 FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
2963
2964 const bool flipped = v_max < v_min; // Check if range is "backwards"
2965 if (flipped)
2966 ImSwap(v_min_fudged, v_max_fudged);
2967
2968 // Awkward special case - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
2969 if ((v_max == 0.0f) && (v_min < 0.0f))
2970 v_max_fudged = -logarithmic_zero_epsilon;
2971
2972 float t_with_flip = flipped ? (1.0f - t) : t; // t, but flipped if necessary to account for us flipping the range
2973
2974 if ((v_min * v_max) < 0.0f) // Range crosses zero, so we have to do this in two parts
2975 {
2976 float zero_point_center = (-(float)ImMin(v_min, v_max)) / ImAbs(x: (float)v_max - (float)v_min); // The zero point in parametric space
2977 float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
2978 float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
2979 if (t_with_flip >= zero_point_snap_L && t_with_flip <= zero_point_snap_R)
2980 result = (TYPE)0.0f; // Special case to make getting exactly zero possible (the epsilon prevents it otherwise)
2981 else if (t_with_flip < zero_point_center)
2982 result = (TYPE)-(logarithmic_zero_epsilon * ImPow(-v_min_fudged / logarithmic_zero_epsilon, (FLOATTYPE)(1.0f - (t_with_flip / zero_point_snap_L))));
2983 else
2984 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))));
2985 }
2986 else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
2987 result = (TYPE)-(-v_max_fudged * ImPow(-v_min_fudged / -v_max_fudged, (FLOATTYPE)(1.0f - t_with_flip)));
2988 else
2989 result = (TYPE)(v_min_fudged * ImPow(v_max_fudged / v_min_fudged, (FLOATTYPE)t_with_flip));
2990 }
2991 else
2992 {
2993 // Linear slider
2994 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2995 if (is_floating_point)
2996 {
2997 result = ImLerp(v_min, v_max, t);
2998 }
2999 else if (t < 1.0)
3000 {
3001 // - For integer values we want the clicking position to match the grab box so we round above
3002 // This code is carefully tuned to work with large values (e.g. high ranges of U64) while preserving this property..
3003 // - 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
3004 // range is going to be imprecise anyway, with this check we at least make the edge values matches expected limits.
3005 FLOATTYPE v_new_off_f = (SIGNEDTYPE)(v_max - v_min) * t;
3006 result = (TYPE)((SIGNEDTYPE)v_min + (SIGNEDTYPE)(v_new_off_f + (FLOATTYPE)(v_min > v_max ? -0.5 : 0.5)));
3007 }
3008 }
3009
3010 return result;
3011}
3012
3013// FIXME: Try to move more of the code into shared SliderBehavior()
3014template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
3015bool 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)
3016{
3017 ImGuiContext& g = *GImGui;
3018 const ImGuiStyle& style = g.Style;
3019
3020 const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
3021 const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
3022 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
3023 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.
3024
3025 // Calculate bounds
3026 const float grab_padding = 2.0f; // FIXME: Should be part of style.
3027 const float slider_sz = (bb.Max[axis] - bb.Min[axis]) - grab_padding * 2.0f;
3028 float grab_sz = style.GrabMinSize;
3029 if (!is_floating_point && v_range_f >= 0.0f) // v_range_f < 0 may happen on integer overflows
3030 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
3031 grab_sz = ImMin(lhs: grab_sz, rhs: slider_sz);
3032 const float slider_usable_sz = slider_sz - grab_sz;
3033 const float slider_usable_pos_min = bb.Min[axis] + grab_padding + grab_sz * 0.5f;
3034 const float slider_usable_pos_max = bb.Max[axis] - grab_padding - grab_sz * 0.5f;
3035
3036 float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
3037 float zero_deadzone_halfsize = 0.0f; // Only valid when is_logarithmic is true
3038 if (is_logarithmic)
3039 {
3040 // 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.
3041 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, default_value: 3) : 1;
3042 logarithmic_zero_epsilon = ImPow(x: 0.1f, y: (float)decimal_precision);
3043 zero_deadzone_halfsize = (style.LogSliderDeadzone * 0.5f) / ImMax(lhs: slider_usable_sz, rhs: 1.0f);
3044 }
3045
3046 // Process interacting with the slider
3047 bool value_changed = false;
3048 if (g.ActiveId == id)
3049 {
3050 bool set_new_value = false;
3051 float clicked_t = 0.0f;
3052 if (g.ActiveIdSource == ImGuiInputSource_Mouse)
3053 {
3054 if (!g.IO.MouseDown[0])
3055 {
3056 ClearActiveID();
3057 }
3058 else
3059 {
3060 const float mouse_abs_pos = g.IO.MousePos[axis];
3061 if (g.ActiveIdIsJustActivated)
3062 {
3063 float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3064 if (axis == ImGuiAxis_Y)
3065 grab_t = 1.0f - grab_t;
3066 const float grab_pos = ImLerp(a: slider_usable_pos_min, b: slider_usable_pos_max, t: grab_t);
3067 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.
3068 g.SliderGrabClickOffset = (clicked_around_grab && is_floating_point) ? mouse_abs_pos - grab_pos : 0.0f;
3069 }
3070 if (slider_usable_sz > 0.0f)
3071 clicked_t = ImSaturate(f: (mouse_abs_pos - g.SliderGrabClickOffset - slider_usable_pos_min) / slider_usable_sz);
3072 if (axis == ImGuiAxis_Y)
3073 clicked_t = 1.0f - clicked_t;
3074 set_new_value = true;
3075 }
3076 }
3077 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
3078 {
3079 if (g.ActiveIdIsJustActivated)
3080 {
3081 g.SliderCurrentAccum = 0.0f; // Reset any stored nav delta upon activation
3082 g.SliderCurrentAccumDirty = false;
3083 }
3084
3085 float input_delta = (axis == ImGuiAxis_X) ? GetNavTweakPressedAmount(axis) : -GetNavTweakPressedAmount(axis);
3086 if (input_delta != 0.0f)
3087 {
3088 const bool tweak_slow = IsKeyDown(key: (g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
3089 const bool tweak_fast = IsKeyDown(key: (g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
3090 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, default_value: 3) : 0;
3091 if (decimal_precision > 0)
3092 {
3093 input_delta /= 100.0f; // Keyboard/Gamepad tweak speeds in % of slider bounds
3094 if (tweak_slow)
3095 input_delta /= 10.0f;
3096 }
3097 else
3098 {
3099 if ((v_range_f >= -100.0f && v_range_f <= 100.0f && v_range_f != 0.0f) || tweak_slow)
3100 input_delta = ((input_delta < 0.0f) ? -1.0f : +1.0f) / v_range_f; // Keyboard/Gamepad tweak speeds in integer steps
3101 else
3102 input_delta /= 100.0f;
3103 }
3104 if (tweak_fast)
3105 input_delta *= 10.0f;
3106
3107 g.SliderCurrentAccum += input_delta;
3108 g.SliderCurrentAccumDirty = true;
3109 }
3110
3111 float delta = g.SliderCurrentAccum;
3112 if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
3113 {
3114 ClearActiveID();
3115 }
3116 else if (g.SliderCurrentAccumDirty)
3117 {
3118 clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3119
3120 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
3121 {
3122 set_new_value = false;
3123 g.SliderCurrentAccum = 0.0f; // If pushing up against the limits, don't continue to accumulate
3124 }
3125 else
3126 {
3127 set_new_value = true;
3128 float old_clicked_t = clicked_t;
3129 clicked_t = ImSaturate(f: clicked_t + delta);
3130
3131 // Calculate what our "new" clicked_t will be, and thus how far we actually moved the slider, and subtract this from the accumulator
3132 TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3133 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
3134 v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
3135 float new_clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3136
3137 if (delta > 0)
3138 g.SliderCurrentAccum -= ImMin(lhs: new_clicked_t - old_clicked_t, rhs: delta);
3139 else
3140 g.SliderCurrentAccum -= ImMax(lhs: new_clicked_t - old_clicked_t, rhs: delta);
3141 }
3142
3143 g.SliderCurrentAccumDirty = false;
3144 }
3145 }
3146
3147 if (set_new_value)
3148 if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
3149 set_new_value = false;
3150
3151 if (set_new_value)
3152 {
3153 TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3154
3155 // Round to user desired precision based on format string
3156 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
3157 v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
3158
3159 // Apply result
3160 if (*v != v_new)
3161 {
3162 *v = v_new;
3163 value_changed = true;
3164 }
3165 }
3166 }
3167
3168 if (slider_sz < 1.0f)
3169 {
3170 *out_grab_bb = ImRect(bb.Min, bb.Min);
3171 }
3172 else
3173 {
3174 // Output grab position so it can be displayed by the caller
3175 float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3176 if (axis == ImGuiAxis_Y)
3177 grab_t = 1.0f - grab_t;
3178 const float grab_pos = ImLerp(a: slider_usable_pos_min, b: slider_usable_pos_max, t: grab_t);
3179 if (axis == ImGuiAxis_X)
3180 *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);
3181 else
3182 *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);
3183 }
3184
3185 return value_changed;
3186}
3187
3188// For 32-bit and larger types, slider bounds are limited to half the natural type range.
3189// 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.
3190// It would be possible to lift that limitation with some work but it doesn't seem to be worth it for sliders.
3191bool 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)
3192{
3193 // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
3194 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.");
3195 IM_ASSERT((flags & ImGuiSliderFlags_WrapAround) == 0); // Not supported by SliderXXX(), only by DragXXX()
3196
3197 switch (data_type)
3198 {
3199 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; }
3200 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; }
3201 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; }
3202 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; }
3203 case ImGuiDataType_S32:
3204 IM_ASSERT(*(const ImS32*)p_min >= IM_S32_MIN / 2 && *(const ImS32*)p_max <= IM_S32_MAX / 2);
3205 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);
3206 case ImGuiDataType_U32:
3207 IM_ASSERT(*(const ImU32*)p_max <= IM_U32_MAX / 2);
3208 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);
3209 case ImGuiDataType_S64:
3210 IM_ASSERT(*(const ImS64*)p_min >= IM_S64_MIN / 2 && *(const ImS64*)p_max <= IM_S64_MAX / 2);
3211 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);
3212 case ImGuiDataType_U64:
3213 IM_ASSERT(*(const ImU64*)p_max <= IM_U64_MAX / 2);
3214 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);
3215 case ImGuiDataType_Float:
3216 IM_ASSERT(*(const float*)p_min >= -FLT_MAX / 2.0f && *(const float*)p_max <= FLT_MAX / 2.0f);
3217 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);
3218 case ImGuiDataType_Double:
3219 IM_ASSERT(*(const double*)p_min >= -DBL_MAX / 2.0f && *(const double*)p_max <= DBL_MAX / 2.0f);
3220 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);
3221 case ImGuiDataType_COUNT: break;
3222 }
3223 IM_ASSERT(0);
3224 return false;
3225}
3226
3227// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a slider, they are all required.
3228// Read code of e.g. SliderFloat(), SliderInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3229bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags)
3230{
3231 ImGuiWindow* window = GetCurrentWindow();
3232 if (window->SkipItems)
3233 return false;
3234
3235 ImGuiContext& g = *GImGui;
3236 const ImGuiStyle& style = g.Style;
3237 const ImGuiID id = window->GetID(str: label);
3238 const float w = CalcItemWidth();
3239
3240 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
3241 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
3242 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));
3243
3244 const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
3245 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
3246 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
3247 return false;
3248
3249 // Default format string when passing NULL
3250 if (format == NULL)
3251 format = DataTypeGetInfo(data_type)->PrintFmt;
3252
3253 const bool hovered = ItemHoverable(bb: frame_bb, id, item_flags: g.LastItemData.ItemFlags);
3254 bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
3255 if (!temp_input_is_active)
3256 {
3257 // Tabbing or CTRL+click on Slider turns it into an input box
3258 const bool clicked = hovered && IsMouseClicked(button: 0, flags: ImGuiInputFlags_None, owner_id: id);
3259 const bool make_active = (clicked || g.NavActivateId == id);
3260 if (make_active && clicked)
3261 SetKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id);
3262 if (make_active && temp_input_allowed)
3263 if ((clicked && g.IO.KeyCtrl) || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
3264 temp_input_is_active = true;
3265
3266 // Store initial value (not used by main lib but available as a convenience but some mods e.g. to revert)
3267 if (make_active)
3268 memcpy(dest: &g.ActiveIdValueOnActivation, src: p_data, n: DataTypeGetInfo(data_type)->Size);
3269
3270 if (make_active && !temp_input_is_active)
3271 {
3272 SetActiveID(id, window);
3273 SetFocusID(id, window);
3274 FocusWindow(window);
3275 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
3276 }
3277 }
3278
3279 if (temp_input_is_active)
3280 {
3281 // Only clamp CTRL+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp)
3282 const bool clamp_enabled = (flags & ImGuiSliderFlags_ClampOnInput) != 0;
3283 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);
3284 }
3285
3286 // Draw frame
3287 const ImU32 frame_col = GetColorU32(idx: g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3288 RenderNavCursor(bb: frame_bb, id);
3289 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: frame_col, borders: true, rounding: g.Style.FrameRounding);
3290
3291 // Slider behavior
3292 ImRect grab_bb;
3293 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);
3294 if (value_changed)
3295 MarkItemEdited(id);
3296
3297 // Render grab
3298 if (grab_bb.Max.x > grab_bb.Min.x)
3299 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);
3300
3301 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3302 char value_buf[64];
3303 const char* value_buf_end = value_buf + DataTypeFormatString(buf: value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
3304 if (g.LogEnabled)
3305 LogSetNextTextDecoration(prefix: "{", suffix: "}");
3306 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));
3307
3308 if (label_size.x > 0.0f)
3309 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), text: label);
3310
3311 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
3312 return value_changed;
3313}
3314
3315// Add multiple sliders on 1 line for compact edition of multiple components
3316bool 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)
3317{
3318 ImGuiWindow* window = GetCurrentWindow();
3319 if (window->SkipItems)
3320 return false;
3321
3322 ImGuiContext& g = *GImGui;
3323 bool value_changed = false;
3324 BeginGroup();
3325 PushID(str_id: label);
3326 PushMultiItemsWidths(components, width_full: CalcItemWidth());
3327 size_t type_size = GDataTypeInfo[data_type].Size;
3328 for (int i = 0; i < components; i++)
3329 {
3330 PushID(int_id: i);
3331 if (i > 0)
3332 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
3333 value_changed |= SliderScalar(label: "", data_type, p_data: v, p_min: v_min, p_max: v_max, format, flags);
3334 PopID();
3335 PopItemWidth();
3336 v = (void*)((char*)v + type_size);
3337 }
3338 PopID();
3339
3340 const char* label_end = FindRenderedTextEnd(text: label);
3341 if (label != label_end)
3342 {
3343 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
3344 TextEx(text: label, text_end: label_end);
3345 }
3346
3347 EndGroup();
3348 return value_changed;
3349}
3350
3351bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3352{
3353 return SliderScalar(label, data_type: ImGuiDataType_Float, p_data: v, p_min: &v_min, p_max: &v_max, format, flags);
3354}
3355
3356bool ImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3357{
3358 return SliderScalarN(label, data_type: ImGuiDataType_Float, v, components: 2, v_min: &v_min, v_max: &v_max, format, flags);
3359}
3360
3361bool ImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3362{
3363 return SliderScalarN(label, data_type: ImGuiDataType_Float, v, components: 3, v_min: &v_min, v_max: &v_max, format, flags);
3364}
3365
3366bool ImGui::SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3367{
3368 return SliderScalarN(label, data_type: ImGuiDataType_Float, v, components: 4, v_min: &v_min, v_max: &v_max, format, flags);
3369}
3370
3371bool ImGui::SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max, const char* format, ImGuiSliderFlags flags)
3372{
3373 if (format == NULL)
3374 format = "%.0f deg";
3375 float v_deg = (*v_rad) * 360.0f / (2 * IM_PI);
3376 bool value_changed = SliderFloat(label, v: &v_deg, v_min: v_degrees_min, v_max: v_degrees_max, format, flags);
3377 if (value_changed)
3378 *v_rad = v_deg * (2 * IM_PI) / 360.0f;
3379 return value_changed;
3380}
3381
3382bool ImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3383{
3384 return SliderScalar(label, data_type: ImGuiDataType_S32, p_data: v, p_min: &v_min, p_max: &v_max, format, flags);
3385}
3386
3387bool ImGui::SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3388{
3389 return SliderScalarN(label, data_type: ImGuiDataType_S32, v, components: 2, v_min: &v_min, v_max: &v_max, format, flags);
3390}
3391
3392bool ImGui::SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3393{
3394 return SliderScalarN(label, data_type: ImGuiDataType_S32, v, components: 3, v_min: &v_min, v_max: &v_max, format, flags);
3395}
3396
3397bool ImGui::SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3398{
3399 return SliderScalarN(label, data_type: ImGuiDataType_S32, v, components: 4, v_min: &v_min, v_max: &v_max, format, flags);
3400}
3401
3402bool 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)
3403{
3404 ImGuiWindow* window = GetCurrentWindow();
3405 if (window->SkipItems)
3406 return false;
3407
3408 ImGuiContext& g = *GImGui;
3409 const ImGuiStyle& style = g.Style;
3410 const ImGuiID id = window->GetID(str: label);
3411
3412 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
3413 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size);
3414 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));
3415
3416 ItemSize(bb, text_baseline_y: style.FramePadding.y);
3417 if (!ItemAdd(bb: frame_bb, id))
3418 return false;
3419
3420 // Default format string when passing NULL
3421 if (format == NULL)
3422 format = DataTypeGetInfo(data_type)->PrintFmt;
3423
3424 const bool hovered = ItemHoverable(bb: frame_bb, id, item_flags: g.LastItemData.ItemFlags);
3425 const bool clicked = hovered && IsMouseClicked(button: 0, flags: ImGuiInputFlags_None, owner_id: id);
3426 if (clicked || g.NavActivateId == id)
3427 {
3428 if (clicked)
3429 SetKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id);
3430 SetActiveID(id, window);
3431 SetFocusID(id, window);
3432 FocusWindow(window);
3433 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
3434 }
3435
3436 // Draw frame
3437 const ImU32 frame_col = GetColorU32(idx: g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3438 RenderNavCursor(bb: frame_bb, id);
3439 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: frame_col, borders: true, rounding: g.Style.FrameRounding);
3440
3441 // Slider behavior
3442 ImRect grab_bb;
3443 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);
3444 if (value_changed)
3445 MarkItemEdited(id);
3446
3447 // Render grab
3448 if (grab_bb.Max.y > grab_bb.Min.y)
3449 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);
3450
3451 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3452 // For the vertical slider we allow centered text to overlap the frame padding
3453 char value_buf[64];
3454 const char* value_buf_end = value_buf + DataTypeFormatString(buf: value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
3455 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));
3456 if (label_size.x > 0.0f)
3457 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), text: label);
3458
3459 return value_changed;
3460}
3461
3462bool ImGui::VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3463{
3464 return VSliderScalar(label, size, data_type: ImGuiDataType_Float, p_data: v, p_min: &v_min, p_max: &v_max, format, flags);
3465}
3466
3467bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3468{
3469 return VSliderScalar(label, size, data_type: ImGuiDataType_S32, p_data: v, p_min: &v_min, p_max: &v_max, format, flags);
3470}
3471
3472//-------------------------------------------------------------------------
3473// [SECTION] Widgets: InputScalar, InputFloat, InputInt, etc.
3474//-------------------------------------------------------------------------
3475// - ImParseFormatFindStart() [Internal]
3476// - ImParseFormatFindEnd() [Internal]
3477// - ImParseFormatTrimDecorations() [Internal]
3478// - ImParseFormatSanitizeForPrinting() [Internal]
3479// - ImParseFormatSanitizeForScanning() [Internal]
3480// - ImParseFormatPrecision() [Internal]
3481// - TempInputTextScalar() [Internal]
3482// - InputScalar()
3483// - InputScalarN()
3484// - InputFloat()
3485// - InputFloat2()
3486// - InputFloat3()
3487// - InputFloat4()
3488// - InputInt()
3489// - InputInt2()
3490// - InputInt3()
3491// - InputInt4()
3492// - InputDouble()
3493//-------------------------------------------------------------------------
3494
3495// We don't use strchr() because our strings are usually very short and often start with '%'
3496const char* ImParseFormatFindStart(const char* fmt)
3497{
3498 while (char c = fmt[0])
3499 {
3500 if (c == '%' && fmt[1] != '%')
3501 return fmt;
3502 else if (c == '%')
3503 fmt++;
3504 fmt++;
3505 }
3506 return fmt;
3507}
3508
3509const char* ImParseFormatFindEnd(const char* fmt)
3510{
3511 // Printf/scanf types modifiers: I/L/h/j/l/t/w/z. Other uppercase letters qualify as types aka end of the format.
3512 if (fmt[0] != '%')
3513 return fmt;
3514 const unsigned int ignored_uppercase_mask = (1 << ('I'-'A')) | (1 << ('L'-'A'));
3515 const unsigned int ignored_lowercase_mask = (1 << ('h'-'a')) | (1 << ('j'-'a')) | (1 << ('l'-'a')) | (1 << ('t'-'a')) | (1 << ('w'-'a')) | (1 << ('z'-'a'));
3516 for (char c; (c = *fmt) != 0; fmt++)
3517 {
3518 if (c >= 'A' && c <= 'Z' && ((1 << (c - 'A')) & ignored_uppercase_mask) == 0)
3519 return fmt + 1;
3520 if (c >= 'a' && c <= 'z' && ((1 << (c - 'a')) & ignored_lowercase_mask) == 0)
3521 return fmt + 1;
3522 }
3523 return fmt;
3524}
3525
3526// Extract the format out of a format string with leading or trailing decorations
3527// fmt = "blah blah" -> return ""
3528// fmt = "%.3f" -> return fmt
3529// fmt = "hello %.3f" -> return fmt + 6
3530// fmt = "%.3f hello" -> return buf written with "%.3f"
3531const char* ImParseFormatTrimDecorations(const char* fmt, char* buf, size_t buf_size)
3532{
3533 const char* fmt_start = ImParseFormatFindStart(fmt);
3534 if (fmt_start[0] != '%')
3535 return "";
3536 const char* fmt_end = ImParseFormatFindEnd(fmt: fmt_start);
3537 if (fmt_end[0] == 0) // If we only have leading decoration, we don't need to copy the data.
3538 return fmt_start;
3539 ImStrncpy(dst: buf, src: fmt_start, count: ImMin(lhs: (size_t)(fmt_end - fmt_start) + 1, rhs: buf_size));
3540 return buf;
3541}
3542
3543// Sanitize format
3544// - Zero terminate so extra characters after format (e.g. "%f123") don't confuse atof/atoi
3545// - stb_sprintf.h supports several new modifiers which format numbers in a way that also makes them incompatible atof/atoi.
3546void ImParseFormatSanitizeForPrinting(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3547{
3548 const char* fmt_end = ImParseFormatFindEnd(fmt: fmt_in);
3549 IM_UNUSED(fmt_out_size);
3550 IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3551 while (fmt_in < fmt_end)
3552 {
3553 char c = *fmt_in++;
3554 if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3555 *(fmt_out++) = c;
3556 }
3557 *fmt_out = 0; // Zero-terminate
3558}
3559
3560// - 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"
3561const char* ImParseFormatSanitizeForScanning(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3562{
3563 const char* fmt_end = ImParseFormatFindEnd(fmt: fmt_in);
3564 const char* fmt_out_begin = fmt_out;
3565 IM_UNUSED(fmt_out_size);
3566 IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3567 bool has_type = false;
3568 while (fmt_in < fmt_end)
3569 {
3570 char c = *fmt_in++;
3571 if (!has_type && ((c >= '0' && c <= '9') || c == '.' || c == '+' || c == '#'))
3572 continue;
3573 has_type |= ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')); // Stop skipping digits
3574 if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3575 *(fmt_out++) = c;
3576 }
3577 *fmt_out = 0; // Zero-terminate
3578 return fmt_out_begin;
3579}
3580
3581template<typename TYPE>
3582static const char* ImAtoi(const char* src, TYPE* output)
3583{
3584 int negative = 0;
3585 if (*src == '-') { negative = 1; src++; }
3586 if (*src == '+') { src++; }
3587 TYPE v = 0;
3588 while (*src >= '0' && *src <= '9')
3589 v = (v * 10) + (*src++ - '0');
3590 *output = negative ? -v : v;
3591 return src;
3592}
3593
3594// Parse display precision back from the display format string
3595// 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.
3596int ImParseFormatPrecision(const char* fmt, int default_precision)
3597{
3598 fmt = ImParseFormatFindStart(fmt);
3599 if (fmt[0] != '%')
3600 return default_precision;
3601 fmt++;
3602 while (*fmt >= '0' && *fmt <= '9')
3603 fmt++;
3604 int precision = INT_MAX;
3605 if (*fmt == '.')
3606 {
3607 fmt = ImAtoi<int>(src: fmt + 1, output: &precision);
3608 if (precision < 0 || precision > 99)
3609 precision = default_precision;
3610 }
3611 if (*fmt == 'e' || *fmt == 'E') // Maximum precision with scientific notation
3612 precision = -1;
3613 if ((*fmt == 'g' || *fmt == 'G') && precision == INT_MAX)
3614 precision = -1;
3615 return (precision == INT_MAX) ? default_precision : precision;
3616}
3617
3618// Create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets)
3619// FIXME: Facilitate using this in variety of other situations.
3620// FIXME: Among other things, setting ImGuiItemFlags_AllowDuplicateId in LastItemData is currently correct but
3621// the expected relationship between TempInputXXX functions and LastItemData is a little fishy.
3622bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags)
3623{
3624 // On the first frame, g.TempInputTextId == 0, then on subsequent frames it becomes == id.
3625 // We clear ActiveID on the first frame to allow the InputText() taking it back.
3626 ImGuiContext& g = *GImGui;
3627 const bool init = (g.TempInputId != id);
3628 if (init)
3629 ClearActiveID();
3630
3631 g.CurrentWindow->DC.CursorPos = bb.Min;
3632 g.LastItemData.ItemFlags |= ImGuiItemFlags_AllowDuplicateId;
3633 bool value_changed = InputTextEx(label, NULL, buf, buf_size, size_arg: bb.GetSize(), flags: flags | ImGuiInputTextFlags_MergedItem);
3634 if (init)
3635 {
3636 // First frame we started displaying the InputText widget, we expect it to take the active id.
3637 IM_ASSERT(g.ActiveId == id);
3638 g.TempInputId = g.ActiveId;
3639 }
3640 return value_changed;
3641}
3642
3643// Note that Drag/Slider functions are only forwarding the min/max values clamping values if the ImGuiSliderFlags_AlwaysClamp flag is set!
3644// This is intended: this way we allow CTRL+Click manual input to set a value out of bounds, for maximum flexibility.
3645// However this may not be ideal for all uses, as some user code may break on out of bound values.
3646bool 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)
3647{
3648 // FIXME: May need to clarify display behavior if format doesn't contain %.
3649 // "%d" -> "%d" / "There are %d items" -> "%d" / "items" -> "%d" (fallback). Also see #6405
3650 ImGuiContext& g = *GImGui;
3651 const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
3652 char fmt_buf[32];
3653 char data_buf[32];
3654 format = ImParseFormatTrimDecorations(fmt: format, buf: fmt_buf, IM_ARRAYSIZE(fmt_buf));
3655 if (format[0] == 0)
3656 format = type_info->PrintFmt;
3657 DataTypeFormatString(buf: data_buf, IM_ARRAYSIZE(data_buf), data_type, p_data, format);
3658 ImStrTrimBlanks(str: data_buf);
3659
3660 ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
3661 g.LastItemData.ItemFlags |= ImGuiItemFlags_NoMarkEdited; // Because TempInputText() uses ImGuiInputTextFlags_MergedItem it doesn't submit a new item, so we poke LastItemData.
3662 bool value_changed = false;
3663 if (TempInputText(bb, id, label, buf: data_buf, IM_ARRAYSIZE(data_buf), flags))
3664 {
3665 // Backup old value
3666 size_t data_type_size = type_info->Size;
3667 ImGuiDataTypeStorage data_backup;
3668 memcpy(dest: &data_backup, src: p_data, n: data_type_size);
3669
3670 // Apply new value (or operations) then clamp
3671 DataTypeApplyFromText(buf: data_buf, data_type, p_data, format, NULL);
3672 if (p_clamp_min || p_clamp_max)
3673 {
3674 if (p_clamp_min && p_clamp_max && DataTypeCompare(data_type, arg_1: p_clamp_min, arg_2: p_clamp_max) > 0)
3675 ImSwap(a&: p_clamp_min, b&: p_clamp_max);
3676 DataTypeClamp(data_type, p_data, p_min: p_clamp_min, p_max: p_clamp_max);
3677 }
3678
3679 // Only mark as edited if new value is different
3680 g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited;
3681 value_changed = memcmp(s1: &data_backup, s2: p_data, n: data_type_size) != 0;
3682 if (value_changed)
3683 MarkItemEdited(id);
3684 }
3685 return value_changed;
3686}
3687
3688void ImGui::SetNextItemRefVal(ImGuiDataType data_type, void* p_data)
3689{
3690 ImGuiContext& g = *GImGui;
3691 g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasRefVal;
3692 memcpy(dest: &g.NextItemData.RefVal, src: p_data, n: DataTypeGetInfo(data_type)->Size);
3693}
3694
3695// 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.
3696// Read code of e.g. InputFloat(), InputInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3697bool 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)
3698{
3699 ImGuiWindow* window = GetCurrentWindow();
3700 if (window->SkipItems)
3701 return false;
3702
3703 ImGuiContext& g = *GImGui;
3704 ImGuiStyle& style = g.Style;
3705 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()!
3706
3707 if (format == NULL)
3708 format = DataTypeGetInfo(data_type)->PrintFmt;
3709
3710 void* p_data_default = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasRefVal) ? &g.NextItemData.RefVal : &g.DataTypeZeroValue;
3711
3712 char buf[64];
3713 if ((flags & ImGuiInputTextFlags_DisplayEmptyRefVal) && DataTypeCompare(data_type, arg_1: p_data, arg_2: p_data_default) == 0)
3714 buf[0] = 0;
3715 else
3716 DataTypeFormatString(buf, IM_ARRAYSIZE(buf), data_type, p_data, format);
3717
3718 // Disable the MarkItemEdited() call in InputText but keep ImGuiItemStatusFlags_Edited.
3719 // We call MarkItemEdited() ourselves by comparing the actual data rather than the string.
3720 g.NextItemData.ItemFlags |= ImGuiItemFlags_NoMarkEdited;
3721 flags |= ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
3722
3723 bool value_changed = false;
3724 if (p_step == NULL)
3725 {
3726 if (InputText(label, buf, IM_ARRAYSIZE(buf), flags))
3727 value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, p_data_when_empty: (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL);
3728 }
3729 else
3730 {
3731 const float button_size = GetFrameHeight();
3732
3733 BeginGroup(); // The only purpose of the group here is to allow the caller to query item data e.g. IsItemActive()
3734 PushID(str_id: label);
3735 SetNextItemWidth(ImMax(lhs: 1.0f, rhs: CalcItemWidth() - (button_size + style.ItemInnerSpacing.x) * 2));
3736 if (InputText(label: "", buf, IM_ARRAYSIZE(buf), flags)) // PushId(label) + "" gives us the expected ID from outside point of view
3737 value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, p_data_when_empty: (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL);
3738 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
3739
3740 // Step buttons
3741 const ImVec2 backup_frame_padding = style.FramePadding;
3742 style.FramePadding.x = style.FramePadding.y;
3743 if (flags & ImGuiInputTextFlags_ReadOnly)
3744 BeginDisabled();
3745 PushItemFlag(option: ImGuiItemFlags_ButtonRepeat, enabled: true);
3746 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
3747 if (ButtonEx(label: "-", size_arg: ImVec2(button_size, button_size)))
3748 {
3749 DataTypeApplyOp(data_type, op: '-', output: p_data, arg1: p_data, arg2: g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3750 value_changed = true;
3751 }
3752 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
3753 if (ButtonEx(label: "+", size_arg: ImVec2(button_size, button_size)))
3754 {
3755 DataTypeApplyOp(data_type, op: '+', output: p_data, arg1: p_data, arg2: g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3756 value_changed = true;
3757 }
3758 PopItemFlag();
3759 if (flags & ImGuiInputTextFlags_ReadOnly)
3760 EndDisabled();
3761
3762 const char* label_end = FindRenderedTextEnd(text: label);
3763 if (label != label_end)
3764 {
3765 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
3766 TextEx(text: label, text_end: label_end);
3767 }
3768 style.FramePadding = backup_frame_padding;
3769
3770 PopID();
3771 EndGroup();
3772 }
3773
3774 g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited;
3775 if (value_changed)
3776 MarkItemEdited(id: g.LastItemData.ID);
3777
3778 return value_changed;
3779}
3780
3781bool 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)
3782{
3783 ImGuiWindow* window = GetCurrentWindow();
3784 if (window->SkipItems)
3785 return false;
3786
3787 ImGuiContext& g = *GImGui;
3788 bool value_changed = false;
3789 BeginGroup();
3790 PushID(str_id: label);
3791 PushMultiItemsWidths(components, width_full: CalcItemWidth());
3792 size_t type_size = GDataTypeInfo[data_type].Size;
3793 for (int i = 0; i < components; i++)
3794 {
3795 PushID(int_id: i);
3796 if (i > 0)
3797 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
3798 value_changed |= InputScalar(label: "", data_type, p_data, p_step, p_step_fast, format, flags);
3799 PopID();
3800 PopItemWidth();
3801 p_data = (void*)((char*)p_data + type_size);
3802 }
3803 PopID();
3804
3805 const char* label_end = FindRenderedTextEnd(text: label);
3806 if (label != label_end)
3807 {
3808 SameLine(offset_from_start_x: 0.0f, spacing: g.Style.ItemInnerSpacing.x);
3809 TextEx(text: label, text_end: label_end);
3810 }
3811
3812 EndGroup();
3813 return value_changed;
3814}
3815
3816bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, const char* format, ImGuiInputTextFlags flags)
3817{
3818 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);
3819}
3820
3821bool ImGui::InputFloat2(const char* label, float v[2], const char* format, ImGuiInputTextFlags flags)
3822{
3823 return InputScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 2, NULL, NULL, format, flags);
3824}
3825
3826bool ImGui::InputFloat3(const char* label, float v[3], const char* format, ImGuiInputTextFlags flags)
3827{
3828 return InputScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 3, NULL, NULL, format, flags);
3829}
3830
3831bool ImGui::InputFloat4(const char* label, float v[4], const char* format, ImGuiInputTextFlags flags)
3832{
3833 return InputScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 4, NULL, NULL, format, flags);
3834}
3835
3836bool ImGui::InputInt(const char* label, int* v, int step, int step_fast, ImGuiInputTextFlags flags)
3837{
3838 // 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.
3839 const char* format = (flags & ImGuiInputTextFlags_CharsHexadecimal) ? "%08X" : "%d";
3840 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);
3841}
3842
3843bool ImGui::InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags)
3844{
3845 return InputScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 2, NULL, NULL, format: "%d", flags);
3846}
3847
3848bool ImGui::InputInt3(const char* label, int v[3], ImGuiInputTextFlags flags)
3849{
3850 return InputScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 3, NULL, NULL, format: "%d", flags);
3851}
3852
3853bool ImGui::InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags)
3854{
3855 return InputScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 4, NULL, NULL, format: "%d", flags);
3856}
3857
3858bool ImGui::InputDouble(const char* label, double* v, double step, double step_fast, const char* format, ImGuiInputTextFlags flags)
3859{
3860 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);
3861}
3862
3863//-------------------------------------------------------------------------
3864// [SECTION] Widgets: InputText, InputTextMultiline, InputTextWithHint
3865//-------------------------------------------------------------------------
3866// - imstb_textedit.h include
3867// - InputText()
3868// - InputTextWithHint()
3869// - InputTextMultiline()
3870// - InputTextGetCharInfo() [Internal]
3871// - InputTextReindexLines() [Internal]
3872// - InputTextReindexLinesRange() [Internal]
3873// - InputTextEx() [Internal]
3874// - DebugNodeInputTextState() [Internal]
3875//-------------------------------------------------------------------------
3876
3877namespace ImStb
3878{
3879#include "imstb_textedit.h"
3880}
3881
3882bool ImGui::InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3883{
3884 IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline()
3885 return InputTextEx(label, NULL, buf, buf_size: (int)buf_size, size_arg: ImVec2(0, 0), flags, callback, user_data);
3886}
3887
3888bool ImGui::InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3889{
3890 return InputTextEx(label, NULL, buf, buf_size: (int)buf_size, size_arg: size, flags: flags | ImGuiInputTextFlags_Multiline, callback, user_data);
3891}
3892
3893bool ImGui::InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3894{
3895 IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() or InputTextEx() manually if you need multi-line + hint.
3896 return InputTextEx(label, hint, buf, buf_size: (int)buf_size, size_arg: ImVec2(0, 0), flags, callback, user_data);
3897}
3898
3899// This is only used in the path where the multiline widget is inactivate.
3900static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end)
3901{
3902 int line_count = 0;
3903 const char* s = text_begin;
3904 while (true)
3905 {
3906 const char* s_eol = strchr(s: s, c: '\n');
3907 line_count++;
3908 if (s_eol == NULL)
3909 {
3910 s = s + ImStrlen(s: s);
3911 break;
3912 }
3913 s = s_eol + 1;
3914 }
3915 *out_text_end = s;
3916 return line_count;
3917}
3918
3919// FIXME: Ideally we'd share code with ImFont::CalcTextSizeA()
3920static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end, const char** remaining, ImVec2* out_offset, bool stop_on_new_line)
3921{
3922 ImGuiContext& g = *ctx;
3923 ImFont* font = g.Font;
3924 const float line_height = g.FontSize;
3925 const float scale = line_height / font->FontSize;
3926
3927 ImVec2 text_size = ImVec2(0, 0);
3928 float line_width = 0.0f;
3929
3930 const char* s = text_begin;
3931 while (s < text_end)
3932 {
3933 unsigned int c = (unsigned int)*s;
3934 if (c < 0x80)
3935 s += 1;
3936 else
3937 s += ImTextCharFromUtf8(out_char: &c, in_text: s, in_text_end: text_end);
3938
3939 if (c == '\n')
3940 {
3941 text_size.x = ImMax(lhs: text_size.x, rhs: line_width);
3942 text_size.y += line_height;
3943 line_width = 0.0f;
3944 if (stop_on_new_line)
3945 break;
3946 continue;
3947 }
3948 if (c == '\r')
3949 continue;
3950
3951 const float char_width = ((int)c < font->IndexAdvanceX.Size ? font->IndexAdvanceX.Data[c] : font->FallbackAdvanceX) * scale;
3952 line_width += char_width;
3953 }
3954
3955 if (text_size.x < line_width)
3956 text_size.x = line_width;
3957
3958 if (out_offset)
3959 *out_offset = ImVec2(line_width, text_size.y + line_height); // offset allow for the possibility of sitting after a trailing \n
3960
3961 if (line_width > 0 || text_size.y == 0.0f) // whereas size.y will ignore the trailing \n
3962 text_size.y += line_height;
3963
3964 if (remaining)
3965 *remaining = s;
3966
3967 return text_size;
3968}
3969
3970// 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)
3971// With our UTF-8 use of stb_textedit:
3972// - 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.
3973// - 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).
3974// - ...but we don't use that feature.
3975namespace ImStb
3976{
3977static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->TextLen; }
3978static char STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { IM_ASSERT(idx <= obj->TextLen); return obj->TextSrc[idx]; }
3979static 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.Font->GetCharAdvance(c: (ImWchar)c) * g.FontScale; }
3980static char STB_TEXTEDIT_NEWLINE = '\n';
3981static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx)
3982{
3983 const char* text = obj->TextSrc;
3984 const char* text_remaining = NULL;
3985 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);
3986 r->x0 = 0.0f;
3987 r->x1 = size.x;
3988 r->baseline_y_delta = size.y;
3989 r->ymin = 0.0f;
3990 r->ymax = size.y;
3991 r->num_chars = (int)(text_remaining - (text + line_start_idx));
3992}
3993
3994#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL
3995#define IMSTB_TEXTEDIT_GETPREVCHARINDEX IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL
3996
3997static int IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx)
3998{
3999 if (idx >= obj->TextLen)
4000 return obj->TextLen + 1;
4001 unsigned int c;
4002 return idx + ImTextCharFromUtf8(out_char: &c, in_text: obj->TextSrc + idx, in_text_end: obj->TextSrc + obj->TextLen);
4003}
4004
4005static int IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx)
4006{
4007 if (idx <= 0)
4008 return -1;
4009 const char* p = ImTextFindPreviousUtf8Codepoint(in_text_start: obj->TextSrc, in_text_curr: obj->TextSrc + idx);
4010 return (int)(p - obj->TextSrc);
4011}
4012
4013static bool ImCharIsSeparatorW(unsigned int c)
4014{
4015 static const unsigned int separator_list[] =
4016 {
4017 ',', 0x3001, '.', 0x3002, ';', 0xFF1B, '(', 0xFF08, ')', 0xFF09, '{', 0xFF5B, '}', 0xFF5D,
4018 '[', 0x300C, ']', 0x300D, '|', 0xFF5C, '!', 0xFF01, '\\', 0xFFE5, '/', 0x30FB, 0xFF0F,
4019 '\n', '\r',
4020 };
4021 for (unsigned int separator : separator_list)
4022 if (c == separator)
4023 return true;
4024 return false;
4025}
4026
4027static int is_word_boundary_from_right(ImGuiInputTextState* obj, int idx)
4028{
4029 // 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.
4030 if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
4031 return 0;
4032
4033 const char* curr_p = obj->TextSrc + idx;
4034 const char* prev_p = ImTextFindPreviousUtf8Codepoint(in_text_start: obj->TextSrc, in_text_curr: curr_p);
4035 unsigned int curr_c; ImTextCharFromUtf8(out_char: &curr_c, in_text: curr_p, in_text_end: obj->TextSrc + obj->TextLen);
4036 unsigned int prev_c; ImTextCharFromUtf8(out_char: &prev_c, in_text: prev_p, in_text_end: obj->TextSrc + obj->TextLen);
4037
4038 bool prev_white = ImCharIsBlankW(c: prev_c);
4039 bool prev_separ = ImCharIsSeparatorW(c: prev_c);
4040 bool curr_white = ImCharIsBlankW(c: curr_c);
4041 bool curr_separ = ImCharIsSeparatorW(c: curr_c);
4042 return ((prev_white || prev_separ) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ);
4043}
4044static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx)
4045{
4046 if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
4047 return 0;
4048
4049 const char* curr_p = obj->TextSrc + idx;
4050 const char* prev_p = ImTextFindPreviousUtf8Codepoint(in_text_start: obj->TextSrc, in_text_curr: curr_p);
4051 unsigned int prev_c; ImTextCharFromUtf8(out_char: &prev_c, in_text: curr_p, in_text_end: obj->TextSrc + obj->TextLen);
4052 unsigned int curr_c; ImTextCharFromUtf8(out_char: &curr_c, in_text: prev_p, in_text_end: obj->TextSrc + obj->TextLen);
4053
4054 bool prev_white = ImCharIsBlankW(c: prev_c);
4055 bool prev_separ = ImCharIsSeparatorW(c: prev_c);
4056 bool curr_white = ImCharIsBlankW(c: curr_c);
4057 bool curr_separ = ImCharIsSeparatorW(c: curr_c);
4058 return ((prev_white) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ);
4059}
4060static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(ImGuiInputTextState* obj, int idx)
4061{
4062 idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx);
4063 while (idx >= 0 && !is_word_boundary_from_right(obj, idx))
4064 idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx);
4065 return idx < 0 ? 0 : idx;
4066}
4067static int STB_TEXTEDIT_MOVEWORDRIGHT_MAC(ImGuiInputTextState* obj, int idx)
4068{
4069 int len = obj->TextLen;
4070 idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4071 while (idx < len && !is_word_boundary_from_left(obj, idx))
4072 idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4073 return idx > len ? len : idx;
4074}
4075static int STB_TEXTEDIT_MOVEWORDRIGHT_WIN(ImGuiInputTextState* obj, int idx)
4076{
4077 idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4078 int len = obj->TextLen;
4079 while (idx < len && !is_word_boundary_from_right(obj, idx))
4080 idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4081 return idx > len ? len : idx;
4082}
4083static 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); }
4084#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h
4085#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL
4086
4087static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n)
4088{
4089 // Offset remaining text (+ copy zero terminator)
4090 IM_ASSERT(obj->TextSrc == obj->TextA.Data);
4091 char* dst = obj->TextA.Data + pos;
4092 char* src = obj->TextA.Data + pos + n;
4093 memmove(dest: dst, src: src, n: obj->TextLen - n - pos + 1);
4094 obj->Edited = true;
4095 obj->TextLen -= n;
4096}
4097
4098static bool STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const char* new_text, int new_text_len)
4099{
4100 const bool is_resizable = (obj->Flags & ImGuiInputTextFlags_CallbackResize) != 0;
4101 const int text_len = obj->TextLen;
4102 IM_ASSERT(pos <= text_len);
4103
4104 if (!is_resizable && (new_text_len + obj->TextLen + 1 > obj->BufCapacity))
4105 return false;
4106
4107 // Grow internal buffer if needed
4108 IM_ASSERT(obj->TextSrc == obj->TextA.Data);
4109 if (new_text_len + text_len + 1 > obj->TextA.Size)
4110 {
4111 if (!is_resizable)
4112 return false;
4113 obj->TextA.resize(new_size: text_len + ImClamp(v: new_text_len, mn: 32, mx: ImMax(lhs: 256, rhs: new_text_len)) + 1);
4114 obj->TextSrc = obj->TextA.Data;
4115 }
4116
4117 char* text = obj->TextA.Data;
4118 if (pos != text_len)
4119 memmove(dest: text + pos + new_text_len, src: text + pos, n: (size_t)(text_len - pos));
4120 memcpy(dest: text + pos, src: new_text, n: (size_t)new_text_len);
4121
4122 obj->Edited = true;
4123 obj->TextLen += new_text_len;
4124 obj->TextA[obj->TextLen] = '\0';
4125
4126 return true;
4127}
4128
4129// 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)
4130#define STB_TEXTEDIT_K_LEFT 0x200000 // keyboard input to move cursor left
4131#define STB_TEXTEDIT_K_RIGHT 0x200001 // keyboard input to move cursor right
4132#define STB_TEXTEDIT_K_UP 0x200002 // keyboard input to move cursor up
4133#define STB_TEXTEDIT_K_DOWN 0x200003 // keyboard input to move cursor down
4134#define STB_TEXTEDIT_K_LINESTART 0x200004 // keyboard input to move cursor to start of line
4135#define STB_TEXTEDIT_K_LINEEND 0x200005 // keyboard input to move cursor to end of line
4136#define STB_TEXTEDIT_K_TEXTSTART 0x200006 // keyboard input to move cursor to start of text
4137#define STB_TEXTEDIT_K_TEXTEND 0x200007 // keyboard input to move cursor to end of text
4138#define STB_TEXTEDIT_K_DELETE 0x200008 // keyboard input to delete selection or character under cursor
4139#define STB_TEXTEDIT_K_BACKSPACE 0x200009 // keyboard input to delete selection or character left of cursor
4140#define STB_TEXTEDIT_K_UNDO 0x20000A // keyboard input to perform undo
4141#define STB_TEXTEDIT_K_REDO 0x20000B // keyboard input to perform redo
4142#define STB_TEXTEDIT_K_WORDLEFT 0x20000C // keyboard input to move cursor left one word
4143#define STB_TEXTEDIT_K_WORDRIGHT 0x20000D // keyboard input to move cursor right one word
4144#define STB_TEXTEDIT_K_PGUP 0x20000E // keyboard input to move cursor up a page
4145#define STB_TEXTEDIT_K_PGDOWN 0x20000F // keyboard input to move cursor down a page
4146#define STB_TEXTEDIT_K_SHIFT 0x400000
4147
4148#define IMSTB_TEXTEDIT_IMPLEMENTATION
4149#define IMSTB_TEXTEDIT_memmove memmove
4150#include "imstb_textedit.h"
4151
4152// stb_textedit internally allows for a single undo record to do addition and deletion, but somehow, calling
4153// the stb_textedit_paste() function creates two separate records, so we perform it manually. (FIXME: Report to nothings/stb?)
4154static void stb_textedit_replace(ImGuiInputTextState* str, STB_TexteditState* state, const IMSTB_TEXTEDIT_CHARTYPE* text, int text_len)
4155{
4156 stb_text_makeundo_replace(str, state, where: 0, old_length: str->TextLen, new_length: text_len);
4157 ImStb::STB_TEXTEDIT_DELETECHARS(obj: str, pos: 0, n: str->TextLen);
4158 state->cursor = state->select_start = state->select_end = 0;
4159 if (text_len <= 0)
4160 return;
4161 if (ImStb::STB_TEXTEDIT_INSERTCHARS(obj: str, pos: 0, new_text: text, new_text_len: text_len))
4162 {
4163 state->cursor = state->select_start = state->select_end = text_len;
4164 state->has_preferred_x = 0;
4165 return;
4166 }
4167 IM_ASSERT(0); // Failed to insert character, normally shouldn't happen because of how we currently use stb_textedit_replace()
4168}
4169
4170} // namespace ImStb
4171
4172// We added an extra indirection where 'Stb' is heap-allocated, in order facilitate the work of bindings generators.
4173ImGuiInputTextState::ImGuiInputTextState()
4174{
4175 memset(s: this, c: 0, n: sizeof(*this));
4176 Stb = IM_NEW(ImStbTexteditState);
4177 memset(s: Stb, c: 0, n: sizeof(*Stb));
4178}
4179
4180ImGuiInputTextState::~ImGuiInputTextState()
4181{
4182 IM_DELETE(p: Stb);
4183}
4184
4185void ImGuiInputTextState::OnKeyPressed(int key)
4186{
4187 stb_textedit_key(str: this, state: Stb, key);
4188 CursorFollow = true;
4189 CursorAnimReset();
4190}
4191
4192void ImGuiInputTextState::OnCharPressed(unsigned int c)
4193{
4194 // Convert the key to a UTF8 byte sequence.
4195 // The changes we had to make to stb_textedit_key made it very much UTF-8 specific which is not too great.
4196 char utf8[5];
4197 ImTextCharToUtf8(out_buf: utf8, c);
4198 stb_textedit_text(str: this, state: Stb, text: utf8, text_len: (int)ImStrlen(s: utf8));
4199 CursorFollow = true;
4200 CursorAnimReset();
4201}
4202
4203// Those functions are not inlined in imgui_internal.h, allowing us to hide ImStbTexteditState from that header.
4204void ImGuiInputTextState::CursorAnimReset() { CursorAnim = -0.30f; } // After a user-input the cursor stays on for a while without blinking
4205void 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); }
4206bool ImGuiInputTextState::HasSelection() const { return Stb->select_start != Stb->select_end; }
4207void ImGuiInputTextState::ClearSelection() { Stb->select_start = Stb->select_end = Stb->cursor; }
4208int ImGuiInputTextState::GetCursorPos() const { return Stb->cursor; }
4209int ImGuiInputTextState::GetSelectionStart() const { return Stb->select_start; }
4210int ImGuiInputTextState::GetSelectionEnd() const { return Stb->select_end; }
4211void ImGuiInputTextState::SelectAll() { Stb->select_start = 0; Stb->cursor = Stb->select_end = TextLen; Stb->has_preferred_x = 0; }
4212void ImGuiInputTextState::ReloadUserBufAndSelectAll() { WantReloadUserBuf = true; ReloadSelectionStart = 0; ReloadSelectionEnd = INT_MAX; }
4213void ImGuiInputTextState::ReloadUserBufAndKeepSelection() { WantReloadUserBuf = true; ReloadSelectionStart = Stb->select_start; ReloadSelectionEnd = Stb->select_end; }
4214void ImGuiInputTextState::ReloadUserBufAndMoveToEnd() { WantReloadUserBuf = true; ReloadSelectionStart = ReloadSelectionEnd = INT_MAX; }
4215
4216ImGuiInputTextCallbackData::ImGuiInputTextCallbackData()
4217{
4218 memset(s: this, c: 0, n: sizeof(*this));
4219}
4220
4221// Public API to manipulate UTF-8 text from within a callback.
4222// FIXME: The existence of this rarely exercised code path is a bit of a nuisance.
4223// Historically they existed because STB_TEXTEDIT_INSERTCHARS() etc. worked on our ImWchar
4224// buffer, but nowadays they both work on UTF-8 data. Should aim to merge both.
4225void ImGuiInputTextCallbackData::DeleteChars(int pos, int bytes_count)
4226{
4227 IM_ASSERT(pos + bytes_count <= BufTextLen);
4228 char* dst = Buf + pos;
4229 const char* src = Buf + pos + bytes_count;
4230 memmove(dest: dst, src: src, n: BufTextLen - bytes_count - pos + 1);
4231
4232 if (CursorPos >= pos + bytes_count)
4233 CursorPos -= bytes_count;
4234 else if (CursorPos >= pos)
4235 CursorPos = pos;
4236 SelectionStart = SelectionEnd = CursorPos;
4237 BufDirty = true;
4238 BufTextLen -= bytes_count;
4239}
4240
4241void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, const char* new_text_end)
4242{
4243 // Accept null ranges
4244 if (new_text == new_text_end)
4245 return;
4246
4247 // Grow internal buffer if needed
4248 const bool is_resizable = (Flags & ImGuiInputTextFlags_CallbackResize) != 0;
4249 const int new_text_len = new_text_end ? (int)(new_text_end - new_text) : (int)ImStrlen(s: new_text);
4250 if (new_text_len + BufTextLen >= BufSize)
4251 {
4252 if (!is_resizable)
4253 return;
4254
4255 ImGuiContext& g = *Ctx;
4256 ImGuiInputTextState* edit_state = &g.InputTextState;
4257 IM_ASSERT(edit_state->ID != 0 && g.ActiveId == edit_state->ID);
4258 IM_ASSERT(Buf == edit_state->TextA.Data);
4259 int new_buf_size = BufTextLen + ImClamp(v: new_text_len * 4, mn: 32, mx: ImMax(lhs: 256, rhs: new_text_len)) + 1;
4260 edit_state->TextA.resize(new_size: new_buf_size + 1);
4261 edit_state->TextSrc = edit_state->TextA.Data;
4262 Buf = edit_state->TextA.Data;
4263 BufSize = edit_state->BufCapacity = new_buf_size;
4264 }
4265
4266 if (BufTextLen != pos)
4267 memmove(dest: Buf + pos + new_text_len, src: Buf + pos, n: (size_t)(BufTextLen - pos));
4268 memcpy(dest: Buf + pos, src: new_text, n: (size_t)new_text_len * sizeof(char));
4269 Buf[BufTextLen + new_text_len] = '\0';
4270
4271 if (CursorPos >= pos)
4272 CursorPos += new_text_len;
4273 SelectionStart = SelectionEnd = CursorPos;
4274 BufDirty = true;
4275 BufTextLen += new_text_len;
4276}
4277
4278void ImGui::PushPasswordFont()
4279{
4280 ImGuiContext& g = *GImGui;
4281 ImFont* in_font = g.Font;
4282 ImFont* out_font = &g.InputTextPasswordFont;
4283 ImFontGlyph* glyph = in_font->FindGlyph(c: '*');
4284 out_font->FontSize = in_font->FontSize;
4285 out_font->Scale = in_font->Scale;
4286 out_font->Ascent = in_font->Ascent;
4287 out_font->Descent = in_font->Descent;
4288 out_font->ContainerAtlas = in_font->ContainerAtlas;
4289 out_font->FallbackGlyph = glyph;
4290 out_font->FallbackAdvanceX = glyph->AdvanceX;
4291 IM_ASSERT(out_font->Glyphs.Size == 0 && out_font->IndexAdvanceX.Size == 0 && out_font->IndexLookup.Size == 0);
4292 PushFont(font: out_font);
4293}
4294
4295// Return false to discard a character.
4296static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard)
4297{
4298 unsigned int c = *p_char;
4299
4300 // Filter non-printable (NB: isprint is unreliable! see #2467)
4301 bool apply_named_filters = true;
4302 if (c < 0x20)
4303 {
4304 bool pass = false;
4305 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)
4306 if (c == '\n' && input_source_is_clipboard && (flags & ImGuiInputTextFlags_Multiline) == 0) // In single line mode, replace \n with a space
4307 {
4308 c = *p_char = ' ';
4309 pass = true;
4310 }
4311 pass |= (c == '\n') && (flags & ImGuiInputTextFlags_Multiline) != 0;
4312 pass |= (c == '\t') && (flags & ImGuiInputTextFlags_AllowTabInput) != 0;
4313 if (!pass)
4314 return false;
4315 apply_named_filters = false; // Override named filters below so newline and tabs can still be inserted.
4316 }
4317
4318 if (input_source_is_clipboard == false)
4319 {
4320 // We ignore Ascii representation of delete (emitted from Backspace on OSX, see #2578, #2817)
4321 if (c == 127)
4322 return false;
4323
4324 // Filter private Unicode range. GLFW on OSX seems to send private characters for special keys like arrow keys (FIXME)
4325 if (c >= 0xE000 && c <= 0xF8FF)
4326 return false;
4327 }
4328
4329 // Filter Unicode ranges we are not handling in this build
4330 if (c > IM_UNICODE_CODEPOINT_MAX)
4331 return false;
4332
4333 // Generic named filters
4334 if (apply_named_filters && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint)))
4335 {
4336 // 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 '.'.
4337 // The standard mandate that programs starts in the "C" locale where the decimal point is '.'.
4338 // 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.
4339 // Change the default decimal_point with:
4340 // ImGui::GetPlatformIO()->Platform_LocaleDecimalPoint = *localeconv()->decimal_point;
4341 // 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.
4342 ImGuiContext& g = *ctx;
4343 const unsigned c_decimal_point = (unsigned int)g.PlatformIO.Platform_LocaleDecimalPoint;
4344 if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint))
4345 if (c == '.' || c == ',')
4346 c = c_decimal_point;
4347
4348 // Full-width -> half-width conversion for numeric fields (https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
4349 // While this is mostly convenient, this has the side-effect for uninformed users accidentally inputting full-width characters that they may
4350 // scratch their head as to why it works in numerical fields vs in generic text fields it would require support in the font.
4351 if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | ImGuiInputTextFlags_CharsHexadecimal))
4352 if (c >= 0xFF01 && c <= 0xFF5E)
4353 c = c - 0xFF01 + 0x21;
4354
4355 // Allow 0-9 . - + * /
4356 if (flags & ImGuiInputTextFlags_CharsDecimal)
4357 if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/'))
4358 return false;
4359
4360 // Allow 0-9 . - + * / e E
4361 if (flags & ImGuiInputTextFlags_CharsScientific)
4362 if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/') && (c != 'e') && (c != 'E'))
4363 return false;
4364
4365 // Allow 0-9 a-F A-F
4366 if (flags & ImGuiInputTextFlags_CharsHexadecimal)
4367 if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
4368 return false;
4369
4370 // Turn a-z into A-Z
4371 if (flags & ImGuiInputTextFlags_CharsUppercase)
4372 if (c >= 'a' && c <= 'z')
4373 c += (unsigned int)('A' - 'a');
4374
4375 if (flags & ImGuiInputTextFlags_CharsNoBlank)
4376 if (ImCharIsBlankW(c))
4377 return false;
4378
4379 *p_char = c;
4380 }
4381
4382 // Custom callback filter
4383 if (flags & ImGuiInputTextFlags_CallbackCharFilter)
4384 {
4385 ImGuiContext& g = *GImGui;
4386 ImGuiInputTextCallbackData callback_data;
4387 callback_data.Ctx = &g;
4388 callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter;
4389 callback_data.EventChar = (ImWchar)c;
4390 callback_data.Flags = flags;
4391 callback_data.UserData = user_data;
4392 if (callback(&callback_data) != 0)
4393 return false;
4394 *p_char = callback_data.EventChar;
4395 if (!callback_data.EventChar)
4396 return false;
4397 }
4398
4399 return true;
4400}
4401
4402// Find the shortest single replacement we can make to get from old_buf to new_buf
4403// Note that this doesn't directly alter state->TextA, state->TextLen. They are expected to be made valid separately.
4404// 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.
4405static void InputTextReconcileUndoState(ImGuiInputTextState* state, const char* old_buf, int old_length, const char* new_buf, int new_length)
4406{
4407 const int shorter_length = ImMin(lhs: old_length, rhs: new_length);
4408 int first_diff;
4409 for (first_diff = 0; first_diff < shorter_length; first_diff++)
4410 if (old_buf[first_diff] != new_buf[first_diff])
4411 break;
4412 if (first_diff == old_length && first_diff == new_length)
4413 return;
4414
4415 int old_last_diff = old_length - 1;
4416 int new_last_diff = new_length - 1;
4417 for (; old_last_diff >= first_diff && new_last_diff >= first_diff; old_last_diff--, new_last_diff--)
4418 if (old_buf[old_last_diff] != new_buf[new_last_diff])
4419 break;
4420
4421 const int insert_len = new_last_diff - first_diff + 1;
4422 const int delete_len = old_last_diff - first_diff + 1;
4423 if (insert_len > 0 || delete_len > 0)
4424 if (IMSTB_TEXTEDIT_CHARTYPE* p = stb_text_createundo(state: &state->Stb->undostate, pos: first_diff, insert_len: delete_len, delete_len: insert_len))
4425 for (int i = 0; i < delete_len; i++)
4426 p[i] = old_buf[first_diff + i];
4427}
4428
4429// As InputText() retain textual data and we currently provide a path for user to not retain it (via local variables)
4430// we need some form of hook to reapply data back to user buffer on deactivation frame. (#4714)
4431// It would be more desirable that we discourage users from taking advantage of the "user not retaining data" trick,
4432// but that more likely be attractive when we do have _NoLiveEdit flag available.
4433void ImGui::InputTextDeactivateHook(ImGuiID id)
4434{
4435 ImGuiContext& g = *GImGui;
4436 ImGuiInputTextState* state = &g.InputTextState;
4437 if (id == 0 || state->ID != id)
4438 return;
4439 g.InputTextDeactivatedState.ID = state->ID;
4440 if (state->Flags & ImGuiInputTextFlags_ReadOnly)
4441 {
4442 g.InputTextDeactivatedState.TextA.resize(new_size: 0); // In theory this data won't be used, but clear to be neat.
4443 }
4444 else
4445 {
4446 IM_ASSERT(state->TextA.Data != 0);
4447 IM_ASSERT(state->TextA[state->TextLen] == 0);
4448 g.InputTextDeactivatedState.TextA.resize(new_size: state->TextLen + 1);
4449 memcpy(dest: g.InputTextDeactivatedState.TextA.Data, src: state->TextA.Data, n: state->TextLen + 1);
4450 }
4451}
4452
4453// Edit a string of text
4454// - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!".
4455// This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match
4456// Note that in std::string world, capacity() would omit 1 byte used by the zero-terminator.
4457// - 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.
4458// - If you want to use ImGui::InputText() with std::string, see misc/cpp/imgui_stdlib.h
4459// (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
4460// 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)
4461bool 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)
4462{
4463 ImGuiWindow* window = GetCurrentWindow();
4464 if (window->SkipItems)
4465 return false;
4466
4467 IM_ASSERT(buf != NULL && buf_size >= 0);
4468 IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys)
4469 IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key)
4470 IM_ASSERT(!((flags & ImGuiInputTextFlags_ElideLeft) && (flags & ImGuiInputTextFlags_Multiline))); // Multiline will not work with left-trimming
4471
4472 ImGuiContext& g = *GImGui;
4473 ImGuiIO& io = g.IO;
4474 const ImGuiStyle& style = g.Style;
4475
4476 const bool RENDER_SELECTION_WHEN_INACTIVE = false;
4477 const bool is_multiline = (flags & ImGuiInputTextFlags_Multiline) != 0;
4478
4479 if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope (including the scrollbar)
4480 BeginGroup();
4481 const ImGuiID id = window->GetID(str: label);
4482 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
4483 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
4484 const ImVec2 total_size = ImVec2(frame_size.x + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), frame_size.y);
4485
4486 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
4487 const ImRect total_bb(frame_bb.Min, frame_bb.Min + total_size);
4488
4489 ImGuiWindow* draw_window = window;
4490 ImVec2 inner_size = frame_size;
4491 ImGuiLastItemData item_data_backup;
4492 if (is_multiline)
4493 {
4494 ImVec2 backup_pos = window->DC.CursorPos;
4495 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
4496 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: ImGuiItemFlags_Inputable))
4497 {
4498 EndGroup();
4499 return false;
4500 }
4501 item_data_backup = g.LastItemData;
4502 window->DC.CursorPos = backup_pos;
4503
4504 // Prevent NavActivation from Tabbing when our widget accepts Tab inputs: this allows cycling through widgets without stopping.
4505 if (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_FromTabbing) && (flags & ImGuiInputTextFlags_AllowTabInput))
4506 g.NavActivateId = 0;
4507
4508 // Prevent NavActivate reactivating in BeginChild() when we are already active.
4509 const ImGuiID backup_activate_id = g.NavActivateId;
4510 if (g.ActiveId == id) // Prevent reactivation
4511 g.NavActivateId = 0;
4512
4513 // We reproduce the contents of BeginChildFrame() in order to provide 'label' so our window internal data are easier to read/debug.
4514 PushStyleColor(idx: ImGuiCol_ChildBg, col: style.Colors[ImGuiCol_FrameBg]);
4515 PushStyleVar(idx: ImGuiStyleVar_ChildRounding, val: style.FrameRounding);
4516 PushStyleVar(idx: ImGuiStyleVar_ChildBorderSize, val: style.FrameBorderSize);
4517 PushStyleVar(idx: ImGuiStyleVar_WindowPadding, val: ImVec2(0, 0)); // Ensure no clip rect so mouse hover can reach FramePadding edges
4518 bool child_visible = BeginChildEx(name: label, id, size_arg: frame_bb.GetSize(), child_flags: ImGuiChildFlags_Borders, window_flags: ImGuiWindowFlags_NoMove);
4519 g.NavActivateId = backup_activate_id;
4520 PopStyleVar(count: 3);
4521 PopStyleColor();
4522 if (!child_visible)
4523 {
4524 EndChild();
4525 EndGroup();
4526 return false;
4527 }
4528 draw_window = g.CurrentWindow; // Child window
4529 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.
4530 draw_window->DC.CursorPos += style.FramePadding;
4531 inner_size.x -= draw_window->ScrollbarSizes.x;
4532 }
4533 else
4534 {
4535 // Support for internal ImGuiInputTextFlags_MergedItem flag, which could be redesigned as an ItemFlags if needed (with test performed in ItemAdd)
4536 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
4537 if (!(flags & ImGuiInputTextFlags_MergedItem))
4538 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: ImGuiItemFlags_Inputable))
4539 return false;
4540 }
4541
4542 // Ensure mouse cursor is set even after switching to keyboard/gamepad mode. May generalize further? (#6417)
4543 bool hovered = ItemHoverable(bb: frame_bb, id, item_flags: g.LastItemData.ItemFlags | ImGuiItemFlags_NoNavDisableMouseHover);
4544 if (hovered)
4545 SetMouseCursor(ImGuiMouseCursor_TextInput);
4546 if (hovered && g.NavHighlightItemUnderNav)
4547 hovered = false;
4548
4549 // We are only allowed to access the state if we are already the active widget.
4550 ImGuiInputTextState* state = GetInputTextState(id);
4551
4552 if (g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly)
4553 flags |= ImGuiInputTextFlags_ReadOnly;
4554 const bool is_readonly = (flags & ImGuiInputTextFlags_ReadOnly) != 0;
4555 const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0;
4556 const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0;
4557 const bool is_resizable = (flags & ImGuiInputTextFlags_CallbackResize) != 0;
4558 if (is_resizable)
4559 IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag!
4560
4561 const bool input_requested_by_nav = (g.ActiveId != id) && ((g.NavActivateId == id) && ((g.NavActivateFlags & ImGuiActivateFlags_PreferInput) || (g.NavInputSource == ImGuiInputSource_Keyboard)));
4562
4563 const bool user_clicked = hovered && io.MouseClicked[0];
4564 const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == GetWindowScrollbarID(window: draw_window, axis: ImGuiAxis_Y);
4565 const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == GetWindowScrollbarID(window: draw_window, axis: ImGuiAxis_Y);
4566 bool clear_active_id = false;
4567 bool select_all = false;
4568
4569 float scroll_y = is_multiline ? draw_window->Scroll.y : FLT_MAX;
4570
4571 const bool init_reload_from_user_buf = (state != NULL && state->WantReloadUserBuf);
4572 const bool init_changed_specs = (state != NULL && state->Stb->single_line != !is_multiline); // state != NULL means its our state.
4573 const bool init_make_active = (user_clicked || user_scroll_finish || input_requested_by_nav);
4574 const bool init_state = (init_make_active || user_scroll_active);
4575 if (init_reload_from_user_buf)
4576 {
4577 int new_len = (int)ImStrlen(s: buf);
4578 IM_ASSERT(new_len + 1 <= buf_size && "Is your input buffer properly zero-terminated?");
4579 state->WantReloadUserBuf = false;
4580 InputTextReconcileUndoState(state, old_buf: state->TextA.Data, old_length: state->TextLen, new_buf: buf, new_length: new_len);
4581 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.
4582 state->TextLen = new_len;
4583 memcpy(dest: state->TextA.Data, src: buf, n: state->TextLen + 1);
4584 state->Stb->select_start = state->ReloadSelectionStart;
4585 state->Stb->cursor = state->Stb->select_end = state->ReloadSelectionEnd;
4586 state->CursorClamp();
4587 }
4588 else if ((init_state && g.ActiveId != id) || init_changed_specs)
4589 {
4590 // Access state even if we don't own it yet.
4591 state = &g.InputTextState;
4592 state->CursorAnimReset();
4593
4594 // 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)
4595 InputTextDeactivateHook(id: state->ID);
4596
4597 // Take a copy of the initial buffer value.
4598 // From the moment we focused we are normally ignoring the content of 'buf' (unless we are in read-only mode)
4599 const int buf_len = (int)ImStrlen(s: buf);
4600 IM_ASSERT(buf_len + 1 <= buf_size && "Is your input buffer properly zero-terminated?");
4601 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.
4602 memcpy(dest: state->TextToRevertTo.Data, src: buf, n: buf_len + 1);
4603
4604 // Preserve cursor position and undo/redo stack if we come back to same widget
4605 // FIXME: Since we reworked this on 2022/06, may want to differentiate recycle_cursor vs recycle_undostate?
4606 bool recycle_state = (state->ID == id && !init_changed_specs);
4607 if (recycle_state && (state->TextLen != buf_len || (state->TextA.Data == NULL || strncmp(s1: state->TextA.Data, s2: buf, n: buf_len) != 0)))
4608 recycle_state = false;
4609
4610 // Start edition
4611 state->ID = id;
4612 state->TextLen = buf_len;
4613 if (!is_readonly)
4614 {
4615 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.
4616 memcpy(dest: state->TextA.Data, src: buf, n: state->TextLen + 1);
4617 }
4618
4619 // Find initial scroll position for right alignment
4620 state->Scroll = ImVec2(0.0f, 0.0f);
4621 if (flags & ImGuiInputTextFlags_ElideLeft)
4622 state->Scroll.x += ImMax(lhs: 0.0f, rhs: CalcTextSize(text: buf).x - frame_size.x + style.FramePadding.x * 2.0f);
4623
4624 // Recycle existing cursor/selection/undo stack but clamp position
4625 // Note a single mouse click will override the cursor/position immediately by calling stb_textedit_click handler.
4626 if (recycle_state)
4627 state->CursorClamp();
4628 else
4629 stb_textedit_initialize_state(state: state->Stb, is_single_line: !is_multiline);
4630
4631 if (!is_multiline)
4632 {
4633 if (flags & ImGuiInputTextFlags_AutoSelectAll)
4634 select_all = true;
4635 if (input_requested_by_nav && (!recycle_state || !(g.NavActivateFlags & ImGuiActivateFlags_TryToPreserveState)))
4636 select_all = true;
4637 if (user_clicked && io.KeyCtrl)
4638 select_all = true;
4639 }
4640
4641 if (flags & ImGuiInputTextFlags_AlwaysOverwrite)
4642 state->Stb->insert_mode = 1; // stb field name is indeed incorrect (see #2863)
4643 }
4644
4645 const bool is_osx = io.ConfigMacOSXBehaviors;
4646 if (g.ActiveId != id && init_make_active)
4647 {
4648 IM_ASSERT(state && state->ID == id);
4649 SetActiveID(id, window);
4650 SetFocusID(id, window);
4651 FocusWindow(window);
4652 }
4653 if (g.ActiveId == id)
4654 {
4655 // Declare some inputs, the other are registered and polled via Shortcut() routing system.
4656 // FIXME: The reason we don't use Shortcut() is we would need a routing flag to specify multiple mods, or to all mods combinaison into individual shortcuts.
4657 const ImGuiKey always_owned_keys[] = { ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_Enter, ImGuiKey_KeypadEnter, ImGuiKey_Delete, ImGuiKey_Backspace, ImGuiKey_Home, ImGuiKey_End };
4658 for (ImGuiKey key : always_owned_keys)
4659 SetKeyOwner(key, owner_id: id);
4660 if (user_clicked)
4661 SetKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id);
4662 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
4663 if (is_multiline || (flags & ImGuiInputTextFlags_CallbackHistory))
4664 {
4665 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
4666 SetKeyOwner(key: ImGuiKey_UpArrow, owner_id: id);
4667 SetKeyOwner(key: ImGuiKey_DownArrow, owner_id: id);
4668 }
4669 if (is_multiline)
4670 {
4671 SetKeyOwner(key: ImGuiKey_PageUp, owner_id: id);
4672 SetKeyOwner(key: ImGuiKey_PageDown, owner_id: id);
4673 }
4674 // FIXME: May be a problem to always steal Alt on OSX, would ideally still allow an uninterrupted Alt down-up to toggle menu
4675 if (is_osx)
4676 SetKeyOwner(key: ImGuiMod_Alt, owner_id: id);
4677
4678 // Expose scroll in a manner that is agnostic to us using a child window
4679 if (is_multiline && state != NULL)
4680 state->Scroll.y = draw_window->Scroll.y;
4681
4682 // Read-only mode always ever read from source buffer. Refresh TextLen when active.
4683 if (is_readonly && state != NULL)
4684 state->TextLen = (int)ImStrlen(s: buf);
4685 //if (is_readonly && state != NULL)
4686 // state->TextA.clear(); // Uncomment to facilitate debugging, but we otherwise prefer to keep/amortize th allocation.
4687 }
4688 if (state != NULL)
4689 state->TextSrc = is_readonly ? buf : state->TextA.Data;
4690
4691 // 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)
4692 if (g.ActiveId == id && state == NULL)
4693 ClearActiveID();
4694
4695 // Release focus when we click outside
4696 if (g.ActiveId == id && io.MouseClicked[0] && !init_state && !init_make_active) //-V560
4697 clear_active_id = true;
4698
4699 // Lock the decision of whether we are going to take the path displaying the cursor or selection
4700 bool render_cursor = (g.ActiveId == id) || (state && user_scroll_active);
4701 bool render_selection = state && (state->HasSelection() || select_all) && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
4702 bool value_changed = false;
4703 bool validated = false;
4704
4705 // Select the buffer to render.
4706 const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state;
4707 bool is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0);
4708
4709 // Password pushes a temporary font with only a fallback glyph
4710 if (is_password && !is_displaying_hint)
4711 PushPasswordFont();
4712
4713 // Process mouse inputs and character inputs
4714 if (g.ActiveId == id)
4715 {
4716 IM_ASSERT(state != NULL);
4717 state->Edited = false;
4718 state->BufCapacity = buf_size;
4719 state->Flags = flags;
4720
4721 // Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget.
4722 // Down the line we should have a cleaner library-wide concept of Selected vs Active.
4723 g.ActiveIdAllowOverlap = !io.MouseDown[0];
4724
4725 // Edit in progress
4726 const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + state->Scroll.x;
4727 const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y) : (g.FontSize * 0.5f));
4728
4729 if (select_all)
4730 {
4731 state->SelectAll();
4732 state->SelectedAllMouseLock = true;
4733 }
4734 else if (hovered && io.MouseClickedCount[0] >= 2 && !io.KeyShift)
4735 {
4736 stb_textedit_click(str: state, state: state->Stb, x: mouse_x, y: mouse_y);
4737 const int multiclick_count = (io.MouseClickedCount[0] - 2);
4738 if ((multiclick_count % 2) == 0)
4739 {
4740 // Double-click: Select word
4741 // We always use the "Mac" word advance for double-click select vs CTRL+Right which use the platform dependent variant:
4742 // FIXME: There are likely many ways to improve this behavior, but there's no "right" behavior (depends on use-case, software, OS)
4743 const bool is_bol = (state->Stb->cursor == 0) || ImStb::STB_TEXTEDIT_GETCHAR(obj: state, idx: state->Stb->cursor - 1) == '\n';
4744 if (STB_TEXT_HAS_SELECTION(state->Stb) || !is_bol)
4745 state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT);
4746 //state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
4747 if (!STB_TEXT_HAS_SELECTION(state->Stb))
4748 ImStb::stb_textedit_prep_selection_at_cursor(state: state->Stb);
4749 state->Stb->cursor = ImStb::STB_TEXTEDIT_MOVEWORDRIGHT_MAC(obj: state, idx: state->Stb->cursor);
4750 state->Stb->select_end = state->Stb->cursor;
4751 ImStb::stb_textedit_clamp(str: state, state: state->Stb);
4752 }
4753 else
4754 {
4755 // Triple-click: Select line
4756 const bool is_eol = ImStb::STB_TEXTEDIT_GETCHAR(obj: state, idx: state->Stb->cursor) == '\n';
4757 state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART);
4758 state->OnKeyPressed(STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT);
4759 state->OnKeyPressed(STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT);
4760 if (!is_eol && is_multiline)
4761 {
4762 ImSwap(a&: state->Stb->select_start, b&: state->Stb->select_end);
4763 state->Stb->cursor = state->Stb->select_end;
4764 }
4765 state->CursorFollow = false;
4766 }
4767 state->CursorAnimReset();
4768 }
4769 else if (io.MouseClicked[0] && !state->SelectedAllMouseLock)
4770 {
4771 if (hovered)
4772 {
4773 if (io.KeyShift)
4774 stb_textedit_drag(str: state, state: state->Stb, x: mouse_x, y: mouse_y);
4775 else
4776 stb_textedit_click(str: state, state: state->Stb, x: mouse_x, y: mouse_y);
4777 state->CursorAnimReset();
4778 }
4779 }
4780 else if (io.MouseDown[0] && !state->SelectedAllMouseLock && (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f))
4781 {
4782 stb_textedit_drag(str: state, state: state->Stb, x: mouse_x, y: mouse_y);
4783 state->CursorAnimReset();
4784 state->CursorFollow = true;
4785 }
4786 if (state->SelectedAllMouseLock && !io.MouseDown[0])
4787 state->SelectedAllMouseLock = false;
4788
4789 // We expect backends to emit a Tab key but some also emit a Tab character which we ignore (#2467, #1336)
4790 // (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)
4791 if ((flags & ImGuiInputTextFlags_AllowTabInput) && !is_readonly)
4792 {
4793 if (Shortcut(key_chord: ImGuiKey_Tab, flags: ImGuiInputFlags_Repeat, owner_id: id))
4794 {
4795 unsigned int c = '\t'; // Insert TAB
4796 if (InputTextFilterCharacter(ctx: &g, p_char: &c, flags, callback, user_data: callback_user_data))
4797 state->OnCharPressed(c);
4798 }
4799 // FIXME: Implement Shift+Tab
4800 /*
4801 if (Shortcut(ImGuiKey_Tab | ImGuiMod_Shift, ImGuiInputFlags_Repeat, id))
4802 {
4803 }
4804 */
4805 }
4806
4807 // Process regular text input (before we check for Return because using some IME will effectively send a Return?)
4808 // 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.
4809 const bool ignore_char_inputs = (io.KeyCtrl && !io.KeyAlt) || (is_osx && io.KeyCtrl);
4810 if (io.InputQueueCharacters.Size > 0)
4811 {
4812 if (!ignore_char_inputs && !is_readonly && !input_requested_by_nav)
4813 for (int n = 0; n < io.InputQueueCharacters.Size; n++)
4814 {
4815 // Insert character if they pass filtering
4816 unsigned int c = (unsigned int)io.InputQueueCharacters[n];
4817 if (c == '\t') // Skip Tab, see above.
4818 continue;
4819 if (InputTextFilterCharacter(ctx: &g, p_char: &c, flags, callback, user_data: callback_user_data))
4820 state->OnCharPressed(c);
4821 }
4822
4823 // Consume characters
4824 io.InputQueueCharacters.resize(new_size: 0);
4825 }
4826 }
4827
4828 // Process other shortcuts/key-presses
4829 bool revert_edit = false;
4830 if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id)
4831 {
4832 IM_ASSERT(state != NULL);
4833
4834 const int row_count_per_page = ImMax(lhs: (int)((inner_size.y - style.FramePadding.y) / g.FontSize), rhs: 1);
4835 state->Stb->row_count_per_page = row_count_per_page;
4836
4837 const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0);
4838 const bool is_wordmove_key_down = is_osx ? io.KeyAlt : io.KeyCtrl; // OS X style: Text editing cursor movement using Alt instead of Ctrl
4839 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
4840
4841 // 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)
4842 // Otherwise we could simply assume that we own the keys as we are active.
4843 const ImGuiInputFlags f_repeat = ImGuiInputFlags_Repeat;
4844 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());
4845 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());
4846 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;
4847 const bool is_undo = (Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiKey_Z, flags: f_repeat, owner_id: id)) && !is_readonly && is_undoable;
4848 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;
4849 const bool is_select_all = Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiKey_A, flags: 0, owner_id: id);
4850
4851 // 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.
4852 const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0;
4853 const bool is_enter_pressed = IsKeyPressed(key: ImGuiKey_Enter, repeat: true) || IsKeyPressed(key: ImGuiKey_KeypadEnter, repeat: true);
4854 const bool is_gamepad_validate = nav_gamepad_active && (IsKeyPressed(ImGuiKey_NavGamepadActivate, repeat: false) || IsKeyPressed(ImGuiKey_NavGamepadInput, repeat: false));
4855 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));
4856
4857 // FIXME: Should use more Shortcut() and reduce IsKeyPressed()+SetKeyOwner(), but requires modifiers combination to be taken account of.
4858 // FIXME-OSX: Missing support for Alt(option)+Right/Left = go to end of line, or next line if already in end of line.
4859 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); }
4860 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); }
4861 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); }
4862 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); }
4863 else if (IsKeyPressed(key: ImGuiKey_PageUp) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGUP | k_mask); scroll_y -= row_count_per_page * g.FontSize; }
4864 else if (IsKeyPressed(key: ImGuiKey_PageDown) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGDOWN | k_mask); scroll_y += row_count_per_page * g.FontSize; }
4865 else if (IsKeyPressed(key: ImGuiKey_Home)) { state->OnKeyPressed(key: io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); }
4866 else if (IsKeyPressed(key: ImGuiKey_End)) { state->OnKeyPressed(key: io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); }
4867 else if (IsKeyPressed(key: ImGuiKey_Delete) && !is_readonly && !is_cut)
4868 {
4869 if (!state->HasSelection())
4870 {
4871 // 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)
4872 if (is_wordmove_key_down)
4873 state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
4874 }
4875 state->OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask);
4876 }
4877 else if (IsKeyPressed(key: ImGuiKey_Backspace) && !is_readonly)
4878 {
4879 if (!state->HasSelection())
4880 {
4881 if (is_wordmove_key_down)
4882 state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT);
4883 else if (is_osx && io.KeyCtrl && !io.KeyAlt && !io.KeySuper)
4884 state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT);
4885 }
4886 state->OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask);
4887 }
4888 else if (is_enter_pressed || is_gamepad_validate)
4889 {
4890 // Determine if we turn Enter into a \n character
4891 bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0;
4892 if (!is_multiline || is_gamepad_validate || (ctrl_enter_for_new_line && !io.KeyCtrl) || (!ctrl_enter_for_new_line && io.KeyCtrl))
4893 {
4894 validated = true;
4895 if (io.ConfigInputTextEnterKeepActive && !is_multiline)
4896 state->SelectAll(); // No need to scroll
4897 else
4898 clear_active_id = true;
4899 }
4900 else if (!is_readonly)
4901 {
4902 unsigned int c = '\n'; // Insert new line
4903 if (InputTextFilterCharacter(ctx: &g, p_char: &c, flags, callback, user_data: callback_user_data))
4904 state->OnCharPressed(c);
4905 }
4906 }
4907 else if (is_cancel)
4908 {
4909 if (flags & ImGuiInputTextFlags_EscapeClearsAll)
4910 {
4911 if (buf[0] != 0)
4912 {
4913 revert_edit = true;
4914 }
4915 else
4916 {
4917 render_cursor = render_selection = false;
4918 clear_active_id = true;
4919 }
4920 }
4921 else
4922 {
4923 clear_active_id = revert_edit = true;
4924 render_cursor = render_selection = false;
4925 }
4926 }
4927 else if (is_undo || is_redo)
4928 {
4929 state->OnKeyPressed(key: is_undo ? STB_TEXTEDIT_K_UNDO : STB_TEXTEDIT_K_REDO);
4930 state->ClearSelection();
4931 }
4932 else if (is_select_all)
4933 {
4934 state->SelectAll();
4935 state->CursorFollow = true;
4936 }
4937 else if (is_cut || is_copy)
4938 {
4939 // Cut, Copy
4940 if (g.PlatformIO.Platform_SetClipboardTextFn != NULL)
4941 {
4942 // SetClipboardText() only takes null terminated strings + state->TextSrc may point to read-only user buffer, so we need to make a copy.
4943 const int ib = state->HasSelection() ? ImMin(lhs: state->Stb->select_start, rhs: state->Stb->select_end) : 0;
4944 const int ie = state->HasSelection() ? ImMax(lhs: state->Stb->select_start, rhs: state->Stb->select_end) : state->TextLen;
4945 g.TempBuffer.reserve(new_capacity: ie - ib + 1);
4946 memcpy(dest: g.TempBuffer.Data, src: state->TextSrc + ib, n: ie - ib);
4947 g.TempBuffer.Data[ie - ib] = 0;
4948 SetClipboardText(g.TempBuffer.Data);
4949 }
4950 if (is_cut)
4951 {
4952 if (!state->HasSelection())
4953 state->SelectAll();
4954 state->CursorFollow = true;
4955 stb_textedit_cut(str: state, state: state->Stb);
4956 }
4957 }
4958 else if (is_paste)
4959 {
4960 if (const char* clipboard = GetClipboardText())
4961 {
4962 // Filter pasted buffer
4963 const int clipboard_len = (int)ImStrlen(s: clipboard);
4964 ImVector<char> clipboard_filtered;
4965 clipboard_filtered.reserve(new_capacity: clipboard_len + 1);
4966 for (const char* s = clipboard; *s != 0; )
4967 {
4968 unsigned int c;
4969 int in_len = ImTextCharFromUtf8(out_char: &c, in_text: s, NULL);
4970 s += in_len;
4971 if (!InputTextFilterCharacter(ctx: &g, p_char: &c, flags, callback, user_data: callback_user_data, input_source_is_clipboard: true))
4972 continue;
4973 char c_utf8[5];
4974 ImTextCharToUtf8(out_buf: c_utf8, c);
4975 int out_len = (int)ImStrlen(s: c_utf8);
4976 clipboard_filtered.resize(new_size: clipboard_filtered.Size + out_len);
4977 memcpy(dest: clipboard_filtered.Data + clipboard_filtered.Size - out_len, src: c_utf8, n: out_len);
4978 }
4979 if (clipboard_filtered.Size > 0) // If everything was filtered, ignore the pasting operation
4980 {
4981 clipboard_filtered.push_back(v: 0);
4982 stb_textedit_paste(str: state, state: state->Stb, ctext: clipboard_filtered.Data, len: clipboard_filtered.Size - 1);
4983 state->CursorFollow = true;
4984 }
4985 }
4986 }
4987
4988 // Update render selection flag after events have been handled, so selection highlight can be displayed during the same frame.
4989 render_selection |= state->HasSelection() && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
4990 }
4991
4992 // Process callbacks and apply result back to user's buffer.
4993 const char* apply_new_text = NULL;
4994 int apply_new_text_length = 0;
4995 if (g.ActiveId == id)
4996 {
4997 IM_ASSERT(state != NULL);
4998 if (revert_edit && !is_readonly)
4999 {
5000 if (flags & ImGuiInputTextFlags_EscapeClearsAll)
5001 {
5002 // Clear input
5003 IM_ASSERT(buf[0] != 0);
5004 apply_new_text = "";
5005 apply_new_text_length = 0;
5006 value_changed = true;
5007 IMSTB_TEXTEDIT_CHARTYPE empty_string;
5008 stb_textedit_replace(str: state, state: state->Stb, text: &empty_string, text_len: 0);
5009 }
5010 else if (strcmp(s1: buf, s2: state->TextToRevertTo.Data) != 0)
5011 {
5012 apply_new_text = state->TextToRevertTo.Data;
5013 apply_new_text_length = state->TextToRevertTo.Size - 1;
5014
5015 // Restore initial value. Only return true if restoring to the initial value changes the current buffer contents.
5016 // Push records into the undo stack so we can CTRL+Z the revert operation itself
5017 value_changed = true;
5018 stb_textedit_replace(str: state, state: state->Stb, text: state->TextToRevertTo.Data, text_len: state->TextToRevertTo.Size - 1);
5019 }
5020 }
5021
5022 // FIXME-OPT: We always reapply the live buffer back to the input buffer before clearing ActiveId,
5023 // even though strictly speaking it wasn't modified on this frame. Should mark dirty state from the stb_textedit callbacks.
5024 // If we do that, need to ensure that as special case, 'validated == true' also writes back.
5025 // This also allows the user to use InputText() without maintaining any user-side storage.
5026 // (please note that if you use this property along ImGuiInputTextFlags_CallbackResize you can end up with your temporary string object
5027 // unnecessarily allocating once a frame, either store your string data, either if you don't then don't use ImGuiInputTextFlags_CallbackResize).
5028 const bool apply_edit_back_to_user_buffer = true;// !revert_edit || (validated && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0);
5029 if (apply_edit_back_to_user_buffer)
5030 {
5031 // Apply current edited text immediately.
5032 // Note that as soon as the input box is active, the in-widget value gets priority over any underlying modification of the input buffer
5033
5034 // User callback
5035 if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackEdit | ImGuiInputTextFlags_CallbackAlways)) != 0)
5036 {
5037 IM_ASSERT(callback != NULL);
5038
5039 // The reason we specify the usage semantic (Completion/History) is that Completion needs to disable keyboard TABBING at the moment.
5040 ImGuiInputTextFlags event_flag = 0;
5041 ImGuiKey event_key = ImGuiKey_None;
5042 if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && Shortcut(key_chord: ImGuiKey_Tab, flags: 0, owner_id: id))
5043 {
5044 event_flag = ImGuiInputTextFlags_CallbackCompletion;
5045 event_key = ImGuiKey_Tab;
5046 }
5047 else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(key: ImGuiKey_UpArrow))
5048 {
5049 event_flag = ImGuiInputTextFlags_CallbackHistory;
5050 event_key = ImGuiKey_UpArrow;
5051 }
5052 else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(key: ImGuiKey_DownArrow))
5053 {
5054 event_flag = ImGuiInputTextFlags_CallbackHistory;
5055 event_key = ImGuiKey_DownArrow;
5056 }
5057 else if ((flags & ImGuiInputTextFlags_CallbackEdit) && state->Edited)
5058 {
5059 event_flag = ImGuiInputTextFlags_CallbackEdit;
5060 }
5061 else if (flags & ImGuiInputTextFlags_CallbackAlways)
5062 {
5063 event_flag = ImGuiInputTextFlags_CallbackAlways;
5064 }
5065
5066 if (event_flag)
5067 {
5068 ImGuiInputTextCallbackData callback_data;
5069 callback_data.Ctx = &g;
5070 callback_data.EventFlag = event_flag;
5071 callback_data.Flags = flags;
5072 callback_data.UserData = callback_user_data;
5073
5074 // FIXME-OPT: Undo stack reconcile needs a backup of the data until we rework API, see #7925
5075 char* callback_buf = is_readonly ? buf : state->TextA.Data;
5076 IM_ASSERT(callback_buf == state->TextSrc);
5077 state->CallbackTextBackup.resize(new_size: state->TextLen + 1);
5078 memcpy(dest: state->CallbackTextBackup.Data, src: callback_buf, n: state->TextLen + 1);
5079
5080 callback_data.EventKey = event_key;
5081 callback_data.Buf = callback_buf;
5082 callback_data.BufTextLen = state->TextLen;
5083 callback_data.BufSize = state->BufCapacity;
5084 callback_data.BufDirty = false;
5085
5086 const int utf8_cursor_pos = callback_data.CursorPos = state->Stb->cursor;
5087 const int utf8_selection_start = callback_data.SelectionStart = state->Stb->select_start;
5088 const int utf8_selection_end = callback_data.SelectionEnd = state->Stb->select_end;
5089
5090 // Call user code
5091 callback(&callback_data);
5092
5093 // Read back what user may have modified
5094 callback_buf = is_readonly ? buf : state->TextA.Data; // Pointer may have been invalidated by a resize callback
5095 IM_ASSERT(callback_data.Buf == callback_buf); // Invalid to modify those fields
5096 IM_ASSERT(callback_data.BufSize == state->BufCapacity);
5097 IM_ASSERT(callback_data.Flags == flags);
5098 const bool buf_dirty = callback_data.BufDirty;
5099 if (callback_data.CursorPos != utf8_cursor_pos || buf_dirty) { state->Stb->cursor = callback_data.CursorPos; state->CursorFollow = true; }
5100 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; }
5101 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; }
5102 if (buf_dirty)
5103 {
5104 // Callback may update buffer and thus set buf_dirty even in read-only mode.
5105 IM_ASSERT(callback_data.BufTextLen == (int)ImStrlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text!
5106 InputTextReconcileUndoState(state, old_buf: state->CallbackTextBackup.Data, old_length: state->CallbackTextBackup.Size - 1, new_buf: callback_data.Buf, new_length: callback_data.BufTextLen);
5107 state->TextLen = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen()
5108 state->CursorAnimReset();
5109 }
5110 }
5111 }
5112
5113 // Will copy result string if modified
5114 if (!is_readonly && strcmp(s1: state->TextSrc, s2: buf) != 0)
5115 {
5116 apply_new_text = state->TextSrc;
5117 apply_new_text_length = state->TextLen;
5118 value_changed = true;
5119 }
5120 }
5121 }
5122
5123 // Handle reapplying final data on deactivation (see InputTextDeactivateHook() for details)
5124 if (g.InputTextDeactivatedState.ID == id)
5125 {
5126 if (g.ActiveId != id && IsItemDeactivatedAfterEdit() && !is_readonly && strcmp(s1: g.InputTextDeactivatedState.TextA.Data, s2: buf) != 0)
5127 {
5128 apply_new_text = g.InputTextDeactivatedState.TextA.Data;
5129 apply_new_text_length = g.InputTextDeactivatedState.TextA.Size - 1;
5130 value_changed = true;
5131 //IMGUI_DEBUG_LOG("InputText(): apply Deactivated data for 0x%08X: \"%.*s\".\n", id, apply_new_text_length, apply_new_text);
5132 }
5133 g.InputTextDeactivatedState.ID = 0;
5134 }
5135
5136 // Copy result to user buffer. This can currently only happen when (g.ActiveId == id)
5137 if (apply_new_text != NULL)
5138 {
5139 //// We cannot test for 'backup_current_text_length != apply_new_text_length' here because we have no guarantee that the size
5140 //// of our owned buffer matches the size of the string object held by the user, and by design we allow InputText() to be used
5141 //// without any storage on user's side.
5142 IM_ASSERT(apply_new_text_length >= 0);
5143 if (is_resizable)
5144 {
5145 ImGuiInputTextCallbackData callback_data;
5146 callback_data.Ctx = &g;
5147 callback_data.EventFlag = ImGuiInputTextFlags_CallbackResize;
5148 callback_data.Flags = flags;
5149 callback_data.Buf = buf;
5150 callback_data.BufTextLen = apply_new_text_length;
5151 callback_data.BufSize = ImMax(lhs: buf_size, rhs: apply_new_text_length + 1);
5152 callback_data.UserData = callback_user_data;
5153 callback(&callback_data);
5154 buf = callback_data.Buf;
5155 buf_size = callback_data.BufSize;
5156 apply_new_text_length = ImMin(lhs: callback_data.BufTextLen, rhs: buf_size - 1);
5157 IM_ASSERT(apply_new_text_length <= buf_size);
5158 }
5159 //IMGUI_DEBUG_PRINT("InputText(\"%s\"): apply_new_text length %d\n", label, apply_new_text_length);
5160
5161 // If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size.
5162 ImStrncpy(dst: buf, src: apply_new_text, count: ImMin(lhs: apply_new_text_length + 1, rhs: buf_size));
5163 }
5164
5165 // Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value)
5166 // Otherwise request text input ahead for next frame.
5167 if (g.ActiveId == id && clear_active_id)
5168 ClearActiveID();
5169 else if (g.ActiveId == id)
5170 g.WantTextInputNextFrame = 1;
5171
5172 // Render frame
5173 if (!is_multiline)
5174 {
5175 RenderNavCursor(bb: frame_bb, id);
5176 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: GetColorU32(idx: ImGuiCol_FrameBg), borders: true, rounding: style.FrameRounding);
5177 }
5178
5179 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
5180 ImVec2 draw_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding;
5181 ImVec2 text_size(0.0f, 0.0f);
5182
5183 // Set upper limit of single-line InputTextEx() at 2 million characters strings. The current pathological worst case is a long line
5184 // without any carriage return, which would makes ImFont::RenderText() reserve too many vertices and probably crash. Avoid it altogether.
5185 // Note that we only use this limit on single-line InputText(), so a pathologically large line on a InputTextMultiline() would still crash.
5186 const int buf_display_max_length = 2 * 1024 * 1024;
5187 const char* buf_display = buf_display_from_state ? state->TextA.Data : buf; //-V595
5188 const char* buf_display_end = NULL; // We have specialized paths below for setting the length
5189
5190 // Display hint when contents is empty
5191 // At this point we need to handle the possibility that a callback could have modified the underlying buffer (#8368)
5192 const bool new_is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0);
5193 if (new_is_displaying_hint != is_displaying_hint)
5194 {
5195 if (is_password && !is_displaying_hint)
5196 PopFont();
5197 is_displaying_hint = new_is_displaying_hint;
5198 if (is_password && !is_displaying_hint)
5199 PushPasswordFont();
5200 }
5201 if (is_displaying_hint)
5202 {
5203 buf_display = hint;
5204 buf_display_end = hint + ImStrlen(s: hint);
5205 }
5206
5207 // Render text. We currently only render selection when the widget is active or while scrolling.
5208 // FIXME: We could remove the '&& render_cursor' to keep rendering selection when inactive.
5209 if (render_cursor || render_selection)
5210 {
5211 IM_ASSERT(state != NULL);
5212 if (!is_displaying_hint)
5213 buf_display_end = buf_display + state->TextLen;
5214
5215 // Render text (with cursor and selection)
5216 // This is going to be messy. We need to:
5217 // - Display the text (this alone can be more easily clipped)
5218 // - Handle scrolling, highlight selection, display cursor (those all requires some form of 1d->2d cursor position calculation)
5219 // - Measure text height (for scrollbar)
5220 // 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)
5221 // FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8.
5222 const char* text_begin = buf_display;
5223 const char* text_end = text_begin + state->TextLen;
5224 ImVec2 cursor_offset, select_start_offset;
5225
5226 {
5227 // Find lines numbers straddling cursor and selection min position
5228 int cursor_line_no = render_cursor ? -1 : -1000;
5229 int selmin_line_no = render_selection ? -1 : -1000;
5230 const char* cursor_ptr = render_cursor ? text_begin + state->Stb->cursor : NULL;
5231 const char* selmin_ptr = render_selection ? text_begin + ImMin(lhs: state->Stb->select_start, rhs: state->Stb->select_end) : NULL;
5232
5233 // Count lines and find line number for cursor and selection ends
5234 int line_count = 1;
5235 if (is_multiline)
5236 {
5237 for (const char* s = text_begin; (s = (const char*)ImMemchr(s: s, c: '\n', n: (size_t)(text_end - s))) != NULL; s++)
5238 {
5239 if (cursor_line_no == -1 && s >= cursor_ptr) { cursor_line_no = line_count; }
5240 if (selmin_line_no == -1 && s >= selmin_ptr) { selmin_line_no = line_count; }
5241 line_count++;
5242 }
5243 }
5244 if (cursor_line_no == -1)
5245 cursor_line_no = line_count;
5246 if (selmin_line_no == -1)
5247 selmin_line_no = line_count;
5248
5249 // Calculate 2d position by finding the beginning of the line and measuring distance
5250 cursor_offset.x = InputTextCalcTextSize(ctx: &g, text_begin: ImStrbol(buf_mid_line: cursor_ptr, buf_begin: text_begin), text_end: cursor_ptr).x;
5251 cursor_offset.y = cursor_line_no * g.FontSize;
5252 if (selmin_line_no >= 0)
5253 {
5254 select_start_offset.x = InputTextCalcTextSize(ctx: &g, text_begin: ImStrbol(buf_mid_line: selmin_ptr, buf_begin: text_begin), text_end: selmin_ptr).x;
5255 select_start_offset.y = selmin_line_no * g.FontSize;
5256 }
5257
5258 // Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224)
5259 if (is_multiline)
5260 text_size = ImVec2(inner_size.x, line_count * g.FontSize);
5261 }
5262
5263 // Scroll
5264 if (render_cursor && state->CursorFollow)
5265 {
5266 // Horizontal scroll in chunks of quarter width
5267 if (!(flags & ImGuiInputTextFlags_NoHorizontalScroll))
5268 {
5269 const float scroll_increment_x = inner_size.x * 0.25f;
5270 const float visible_width = inner_size.x - style.FramePadding.x;
5271 if (cursor_offset.x < state->Scroll.x)
5272 state->Scroll.x = IM_TRUNC(ImMax(0.0f, cursor_offset.x - scroll_increment_x));
5273 else if (cursor_offset.x - visible_width >= state->Scroll.x)
5274 state->Scroll.x = IM_TRUNC(cursor_offset.x - visible_width + scroll_increment_x);
5275 }
5276 else
5277 {
5278 state->Scroll.y = 0.0f;
5279 }
5280
5281 // Vertical scroll
5282 if (is_multiline)
5283 {
5284 // Test if cursor is vertically visible
5285 if (cursor_offset.y - g.FontSize < scroll_y)
5286 scroll_y = ImMax(lhs: 0.0f, rhs: cursor_offset.y - g.FontSize);
5287 else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y)
5288 scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f;
5289 const float scroll_max_y = ImMax(lhs: (text_size.y + style.FramePadding.y * 2.0f) - inner_size.y, rhs: 0.0f);
5290 scroll_y = ImClamp(v: scroll_y, mn: 0.0f, mx: scroll_max_y);
5291 draw_pos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag
5292 draw_window->Scroll.y = scroll_y;
5293 }
5294
5295 state->CursorFollow = false;
5296 }
5297
5298 // Draw selection
5299 const ImVec2 draw_scroll = ImVec2(state->Scroll.x, 0.0f);
5300 if (render_selection)
5301 {
5302 const char* text_selected_begin = text_begin + ImMin(lhs: state->Stb->select_start, rhs: state->Stb->select_end);
5303 const char* text_selected_end = text_begin + ImMax(lhs: state->Stb->select_start, rhs: state->Stb->select_end);
5304
5305 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.
5306 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.
5307 float bg_offy_dn = is_multiline ? 0.0f : 2.0f;
5308 ImVec2 rect_pos = draw_pos + select_start_offset - draw_scroll;
5309 for (const char* p = text_selected_begin; p < text_selected_end; )
5310 {
5311 if (rect_pos.y > clip_rect.w + g.FontSize)
5312 break;
5313 if (rect_pos.y < clip_rect.y)
5314 {
5315 p = (const char*)ImMemchr(s: (void*)p, c: '\n', n: text_selected_end - p);
5316 p = p ? p + 1 : text_selected_end;
5317 }
5318 else
5319 {
5320 ImVec2 rect_size = InputTextCalcTextSize(ctx: &g, text_begin: p, text_end: text_selected_end, remaining: &p, NULL, stop_on_new_line: true);
5321 if (rect_size.x <= 0.0f) rect_size.x = IM_TRUNC(g.Font->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines
5322 ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_size.x, bg_offy_dn));
5323 rect.ClipWith(r: clip_rect);
5324 if (rect.Overlaps(r: clip_rect))
5325 draw_window->DrawList->AddRectFilled(p_min: rect.Min, p_max: rect.Max, col: bg_color);
5326 rect_pos.x = draw_pos.x - draw_scroll.x;
5327 }
5328 rect_pos.y += g.FontSize;
5329 }
5330 }
5331
5332 // 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.
5333 // FIXME-OPT: Multiline could submit a smaller amount of contents to AddText() since we already iterated through it.
5334 if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
5335 {
5336 ImU32 col = GetColorU32(idx: is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
5337 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);
5338 }
5339
5340 // Draw blinking cursor
5341 if (render_cursor)
5342 {
5343 state->CursorAnim += io.DeltaTime;
5344 bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f;
5345 ImVec2 cursor_screen_pos = ImTrunc(v: draw_pos + cursor_offset - draw_scroll);
5346 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);
5347 if (cursor_is_visible && cursor_screen_rect.Overlaps(r: clip_rect))
5348 draw_window->DrawList->AddLine(p1: cursor_screen_rect.Min, p2: cursor_screen_rect.GetBL(), col: GetColorU32(idx: ImGuiCol_Text));
5349
5350 // 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.)
5351 if (!is_readonly)
5352 {
5353 g.PlatformImeData.WantVisible = true;
5354 g.PlatformImeData.InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize);
5355 g.PlatformImeData.InputLineHeight = g.FontSize;
5356 g.PlatformImeViewport = window->Viewport->ID;
5357 }
5358 }
5359 }
5360 else
5361 {
5362 // Render text only (no selection, no cursor)
5363 if (is_multiline)
5364 text_size = ImVec2(inner_size.x, InputTextCalcTextLenAndLineCount(text_begin: buf_display, out_text_end: &buf_display_end) * g.FontSize); // We don't need width
5365 else if (!is_displaying_hint && g.ActiveId == id)
5366 buf_display_end = buf_display + state->TextLen;
5367 else if (!is_displaying_hint)
5368 buf_display_end = buf_display + ImStrlen(s: buf_display);
5369
5370 if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
5371 {
5372 // Find render position for right alignment
5373 if (flags & ImGuiInputTextFlags_ElideLeft)
5374 draw_pos.x = ImMin(lhs: draw_pos.x, rhs: frame_bb.Max.x - CalcTextSize(text: buf_display, NULL).x - style.FramePadding.x);
5375
5376 const ImVec2 draw_scroll = /*state ? ImVec2(state->Scroll.x, 0.0f) :*/ ImVec2(0.0f, 0.0f); // Preserve scroll when inactive?
5377 ImU32 col = GetColorU32(idx: is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
5378 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);
5379 }
5380 }
5381
5382 if (is_password && !is_displaying_hint)
5383 PopFont();
5384
5385 if (is_multiline)
5386 {
5387 // For focus requests to work on our multiline we need to ensure our child ItemAdd() call specifies the ImGuiItemFlags_Inputable (see #4761, #7870)...
5388 Dummy(size: ImVec2(text_size.x, text_size.y + style.FramePadding.y));
5389 g.NextItemData.ItemFlags |= (ImGuiItemFlags)ImGuiItemFlags_Inputable | ImGuiItemFlags_NoTabStop;
5390 EndChild();
5391 item_data_backup.StatusFlags |= (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredWindow);
5392
5393 // ...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...
5394 // FIXME: This quite messy/tricky, should attempt to get rid of the child window.
5395 EndGroup();
5396 if (g.LastItemData.ID == 0 || g.LastItemData.ID != GetWindowScrollbarID(window: draw_window, axis: ImGuiAxis_Y))
5397 {
5398 g.LastItemData.ID = id;
5399 g.LastItemData.ItemFlags = item_data_backup.ItemFlags;
5400 g.LastItemData.StatusFlags = item_data_backup.StatusFlags;
5401 }
5402 }
5403 if (state)
5404 state->TextSrc = NULL;
5405
5406 // Log as text
5407 if (g.LogEnabled && (!is_password || is_displaying_hint))
5408 {
5409 LogSetNextTextDecoration(prefix: "{", suffix: "}");
5410 LogRenderedText(ref_pos: &draw_pos, text: buf_display, text_end: buf_display_end);
5411 }
5412
5413 if (label_size.x > 0)
5414 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), text: label);
5415
5416 if (value_changed)
5417 MarkItemEdited(id);
5418
5419 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
5420 if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0)
5421 return validated;
5422 else
5423 return value_changed;
5424}
5425
5426void ImGui::DebugNodeInputTextState(ImGuiInputTextState* state)
5427{
5428#ifndef IMGUI_DISABLE_DEBUG_TOOLS
5429 ImGuiContext& g = *GImGui;
5430 ImStb::STB_TexteditState* stb_state = state->Stb;
5431 ImStb::StbUndoState* undo_state = &stb_state->undostate;
5432 Text(fmt: "ID: 0x%08X, ActiveID: 0x%08X", state->ID, g.ActiveId);
5433 DebugLocateItemOnHover(target_id: state->ID);
5434 Text(fmt: "CurLenA: %d, Cursor: %d, Selection: %d..%d", state->TextLen, stb_state->cursor, stb_state->select_start, stb_state->select_end);
5435 Text(fmt: "BufCapacityA: %d", state->BufCapacity);
5436 Text(fmt: "(Internal Buffer: TextA Size: %d, Capacity: %d)", state->TextA.Size, state->TextA.Capacity);
5437 Text(fmt: "has_preferred_x: %d (%.2f)", stb_state->has_preferred_x, stb_state->preferred_x);
5438 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);
5439 if (BeginChild(str_id: "undopoints", size: ImVec2(0.0f, GetTextLineHeight() * 10), child_flags: ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY)) // Visualize undo state
5440 {
5441 PushStyleVar(idx: ImGuiStyleVar_ItemSpacing, val: ImVec2(0, 0));
5442 for (int n = 0; n < IMSTB_TEXTEDIT_UNDOSTATECOUNT; n++)
5443 {
5444 ImStb::StbUndoRecord* undo_rec = &undo_state->undo_rec[n];
5445 const char undo_rec_type = (n < undo_state->undo_point) ? 'u' : (n >= undo_state->redo_point) ? 'r' : ' ';
5446 if (undo_rec_type == ' ')
5447 BeginDisabled();
5448 const int buf_preview_len = (undo_rec_type != ' ' && undo_rec->char_storage != -1) ? undo_rec->insert_length : 0;
5449 const char* buf_preview_str = undo_state->undo_char + undo_rec->char_storage;
5450 Text(fmt: "%c [%02d] where %03d, insert %03d, delete %03d, char_storage %03d \"%.*s\"",
5451 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);
5452 if (undo_rec_type == ' ')
5453 EndDisabled();
5454 }
5455 PopStyleVar();
5456 }
5457 EndChild();
5458#else
5459 IM_UNUSED(state);
5460#endif
5461}
5462
5463//-------------------------------------------------------------------------
5464// [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc.
5465//-------------------------------------------------------------------------
5466// - ColorEdit3()
5467// - ColorEdit4()
5468// - ColorPicker3()
5469// - RenderColorRectWithAlphaCheckerboard() [Internal]
5470// - ColorPicker4()
5471// - ColorButton()
5472// - SetColorEditOptions()
5473// - ColorTooltip() [Internal]
5474// - ColorEditOptionsPopup() [Internal]
5475// - ColorPickerOptionsPopup() [Internal]
5476//-------------------------------------------------------------------------
5477
5478bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags)
5479{
5480 return ColorEdit4(label, col, flags: flags | ImGuiColorEditFlags_NoAlpha);
5481}
5482
5483static void ColorEditRestoreH(const float* col, float* H)
5484{
5485 ImGuiContext& g = *GImGui;
5486 IM_ASSERT(g.ColorEditCurrentID != 0);
5487 if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(in: ImVec4(col[0], col[1], col[2], 0)))
5488 return;
5489 *H = g.ColorEditSavedHue;
5490}
5491
5492// ColorEdit supports RGB and HSV inputs. In case of RGB input resulting color may have undefined hue and/or saturation.
5493// Since widget displays both RGB and HSV values we must preserve hue and saturation to prevent these values resetting.
5494static void ColorEditRestoreHS(const float* col, float* H, float* S, float* V)
5495{
5496 ImGuiContext& g = *GImGui;
5497 IM_ASSERT(g.ColorEditCurrentID != 0);
5498 if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(in: ImVec4(col[0], col[1], col[2], 0)))
5499 return;
5500
5501 // When S == 0, H is undefined.
5502 // When H == 1 it wraps around to 0.
5503 if (*S == 0.0f || (*H == 0.0f && g.ColorEditSavedHue == 1))
5504 *H = g.ColorEditSavedHue;
5505
5506 // When V == 0, S is undefined.
5507 if (*V == 0.0f)
5508 *S = g.ColorEditSavedSat;
5509}
5510
5511// Edit colors components (each component in 0.0f..1.0f range).
5512// See enum ImGuiColorEditFlags_ for available options. e.g. Only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
5513// 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.
5514bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags)
5515{
5516 ImGuiWindow* window = GetCurrentWindow();
5517 if (window->SkipItems)
5518 return false;
5519
5520 ImGuiContext& g = *GImGui;
5521 const ImGuiStyle& style = g.Style;
5522 const float square_sz = GetFrameHeight();
5523 const char* label_display_end = FindRenderedTextEnd(text: label);
5524 float w_full = CalcItemWidth();
5525 g.NextItemData.ClearFlags();
5526
5527 BeginGroup();
5528 PushID(str_id: label);
5529 const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0);
5530 if (set_current_color_edit_id)
5531 g.ColorEditCurrentID = window->IDStack.back();
5532
5533 // If we're not showing any slider there's no point in doing any HSV conversions
5534 const ImGuiColorEditFlags flags_untouched = flags;
5535 if (flags & ImGuiColorEditFlags_NoInputs)
5536 flags = (flags & (~ImGuiColorEditFlags_DisplayMask_)) | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_NoOptions;
5537
5538 // Context menu: display and modify options (before defaults are applied)
5539 if (!(flags & ImGuiColorEditFlags_NoOptions))
5540 ColorEditOptionsPopup(col, flags);
5541
5542 // Read stored options
5543 if (!(flags & ImGuiColorEditFlags_DisplayMask_))
5544 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DisplayMask_);
5545 if (!(flags & ImGuiColorEditFlags_DataTypeMask_))
5546 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DataTypeMask_);
5547 if (!(flags & ImGuiColorEditFlags_PickerMask_))
5548 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_);
5549 if (!(flags & ImGuiColorEditFlags_InputMask_))
5550 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_InputMask_);
5551 flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_));
5552 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check that only 1 is selected
5553 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
5554
5555 const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0;
5556 const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0;
5557 const int components = alpha ? 4 : 3;
5558 const float w_button = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x);
5559 const float w_inputs = ImMax(lhs: w_full - w_button, rhs: 1.0f);
5560 w_full = w_inputs + w_button;
5561
5562 // Convert to the formats we need
5563 float f[4] = { col[0], col[1], col[2], alpha ? col[3] : 1.0f };
5564 if ((flags & ImGuiColorEditFlags_InputHSV) && (flags & ImGuiColorEditFlags_DisplayRGB))
5565 ColorConvertHSVtoRGB(h: f[0], s: f[1], v: f[2], out_r&: f[0], out_g&: f[1], out_b&: f[2]);
5566 else if ((flags & ImGuiColorEditFlags_InputRGB) && (flags & ImGuiColorEditFlags_DisplayHSV))
5567 {
5568 // Hue is lost when converting from grayscale rgb (saturation=0). Restore it.
5569 ColorConvertRGBtoHSV(r: f[0], g: f[1], b: f[2], out_h&: f[0], out_s&: f[1], out_v&: f[2]);
5570 ColorEditRestoreHS(col, H: &f[0], S: &f[1], V: &f[2]);
5571 }
5572 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]) };
5573
5574 bool value_changed = false;
5575 bool value_changed_as_float = false;
5576
5577 const ImVec2 pos = window->DC.CursorPos;
5578 const float inputs_offset_x = (style.ColorButtonPosition == ImGuiDir_Left) ? w_button : 0.0f;
5579 window->DC.CursorPos.x = pos.x + inputs_offset_x;
5580
5581 if ((flags & (ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5582 {
5583 // RGB/HSV 0..255 Sliders
5584 const float w_items = w_inputs - style.ItemInnerSpacing.x * (components - 1);
5585
5586 const bool hide_prefix = (IM_TRUNC(w_items / components) <= CalcTextSize(text: (flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x);
5587 static const char* ids[4] = { "##X", "##Y", "##Z", "##W" };
5588 static const char* fmt_table_int[3][4] =
5589 {
5590 { "%3d", "%3d", "%3d", "%3d" }, // Short display
5591 { "R:%3d", "G:%3d", "B:%3d", "A:%3d" }, // Long display for RGBA
5592 { "H:%3d", "S:%3d", "V:%3d", "A:%3d" } // Long display for HSVA
5593 };
5594 static const char* fmt_table_float[3][4] =
5595 {
5596 { "%0.3f", "%0.3f", "%0.3f", "%0.3f" }, // Short display
5597 { "R:%0.3f", "G:%0.3f", "B:%0.3f", "A:%0.3f" }, // Long display for RGBA
5598 { "H:%0.3f", "S:%0.3f", "V:%0.3f", "A:%0.3f" } // Long display for HSVA
5599 };
5600 const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_DisplayHSV) ? 2 : 1;
5601
5602 float prev_split = 0.0f;
5603 for (int n = 0; n < components; n++)
5604 {
5605 if (n > 0)
5606 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
5607 float next_split = IM_TRUNC(w_items * (n + 1) / components);
5608 SetNextItemWidth(ImMax(lhs: next_split - prev_split, rhs: 1.0f));
5609 prev_split = next_split;
5610
5611 // FIXME: When ImGuiColorEditFlags_HDR flag is passed HS values snap in weird ways when SV values go below 0.
5612 if (flags & ImGuiColorEditFlags_Float)
5613 {
5614 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]);
5615 value_changed_as_float |= value_changed;
5616 }
5617 else
5618 {
5619 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]);
5620 }
5621 if (!(flags & ImGuiColorEditFlags_NoOptions))
5622 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5623 }
5624 }
5625 else if ((flags & ImGuiColorEditFlags_DisplayHex) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5626 {
5627 // RGB Hexadecimal Input
5628 char buf[64];
5629 if (alpha)
5630 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));
5631 else
5632 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));
5633 SetNextItemWidth(w_inputs);
5634 if (InputText(label: "##Text", buf, IM_ARRAYSIZE(buf), flags: ImGuiInputTextFlags_CharsUppercase))
5635 {
5636 value_changed = true;
5637 char* p = buf;
5638 while (*p == '#' || ImCharIsBlankA(c: *p))
5639 p++;
5640 i[0] = i[1] = i[2] = 0;
5641 i[3] = 0xFF; // alpha default to 255 is not parsed by scanf (e.g. inputting #FFFFFF omitting alpha)
5642 int r;
5643 if (alpha)
5644 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)
5645 else
5646 r = sscanf(s: p, format: "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]);
5647 IM_UNUSED(r); // Fixes C6031: Return value ignored: 'sscanf'.
5648 }
5649 if (!(flags & ImGuiColorEditFlags_NoOptions))
5650 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5651 }
5652
5653 ImGuiWindow* picker_active_window = NULL;
5654 if (!(flags & ImGuiColorEditFlags_NoSmallPreview))
5655 {
5656 const float button_offset_x = ((flags & ImGuiColorEditFlags_NoInputs) || (style.ColorButtonPosition == ImGuiDir_Left)) ? 0.0f : w_inputs + style.ItemInnerSpacing.x;
5657 window->DC.CursorPos = ImVec2(pos.x + button_offset_x, pos.y);
5658
5659 const ImVec4 col_v4(col[0], col[1], col[2], alpha ? col[3] : 1.0f);
5660 if (ColorButton(desc_id: "##ColorButton", col: col_v4, flags))
5661 {
5662 if (!(flags & ImGuiColorEditFlags_NoPicker))
5663 {
5664 // Store current color and open a picker
5665 g.ColorPickerRef = col_v4;
5666 OpenPopup(str_id: "picker");
5667 SetNextWindowPos(pos: g.LastItemData.Rect.GetBL() + ImVec2(0.0f, style.ItemSpacing.y));
5668 }
5669 }
5670 if (!(flags & ImGuiColorEditFlags_NoOptions))
5671 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5672
5673 if (BeginPopup(str_id: "picker"))
5674 {
5675 if (g.CurrentWindow->BeginCount == 1)
5676 {
5677 picker_active_window = g.CurrentWindow;
5678 if (label != label_display_end)
5679 {
5680 TextEx(text: label, text_end: label_display_end);
5681 Spacing();
5682 }
5683 ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar;
5684 ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf;
5685 SetNextItemWidth(square_sz * 12.0f); // Use 256 + bar sizes?
5686 value_changed |= ColorPicker4(label: "##picker", col, flags: picker_flags, ref_col: &g.ColorPickerRef.x);
5687 }
5688 EndPopup();
5689 }
5690 }
5691
5692 if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel))
5693 {
5694 // Position not necessarily next to last submitted button (e.g. if style.ColorButtonPosition == ImGuiDir_Left),
5695 // but we need to use SameLine() to setup baseline correctly. Might want to refactor SameLine() to simplify this.
5696 SameLine(offset_from_start_x: 0.0f, spacing: style.ItemInnerSpacing.x);
5697 window->DC.CursorPos.x = pos.x + ((flags & ImGuiColorEditFlags_NoInputs) ? w_button : w_full + style.ItemInnerSpacing.x);
5698 TextEx(text: label, text_end: label_display_end);
5699 }
5700
5701 // Convert back
5702 if (value_changed && picker_active_window == NULL)
5703 {
5704 if (!value_changed_as_float)
5705 for (int n = 0; n < 4; n++)
5706 f[n] = i[n] / 255.0f;
5707 if ((flags & ImGuiColorEditFlags_DisplayHSV) && (flags & ImGuiColorEditFlags_InputRGB))
5708 {
5709 g.ColorEditSavedHue = f[0];
5710 g.ColorEditSavedSat = f[1];
5711 ColorConvertHSVtoRGB(h: f[0], s: f[1], v: f[2], out_r&: f[0], out_g&: f[1], out_b&: f[2]);
5712 g.ColorEditSavedID = g.ColorEditCurrentID;
5713 g.ColorEditSavedColor = ColorConvertFloat4ToU32(in: ImVec4(f[0], f[1], f[2], 0));
5714 }
5715 if ((flags & ImGuiColorEditFlags_DisplayRGB) && (flags & ImGuiColorEditFlags_InputHSV))
5716 ColorConvertRGBtoHSV(r: f[0], g: f[1], b: f[2], out_h&: f[0], out_s&: f[1], out_v&: f[2]);
5717
5718 col[0] = f[0];
5719 col[1] = f[1];
5720 col[2] = f[2];
5721 if (alpha)
5722 col[3] = f[3];
5723 }
5724
5725 if (set_current_color_edit_id)
5726 g.ColorEditCurrentID = 0;
5727 PopID();
5728 EndGroup();
5729
5730 // Drag and Drop Target
5731 // NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test.
5732 if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget())
5733 {
5734 bool accepted_drag_drop = false;
5735 if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F))
5736 {
5737 memcpy(dest: (float*)col, src: payload->Data, n: sizeof(float) * 3); // Preserve alpha if any //-V512 //-V1086
5738 value_changed = accepted_drag_drop = true;
5739 }
5740 if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F))
5741 {
5742 memcpy(dest: (float*)col, src: payload->Data, n: sizeof(float) * components);
5743 value_changed = accepted_drag_drop = true;
5744 }
5745
5746 // Drag-drop payloads are always RGB
5747 if (accepted_drag_drop && (flags & ImGuiColorEditFlags_InputHSV))
5748 ColorConvertRGBtoHSV(r: col[0], g: col[1], b: col[2], out_h&: col[0], out_s&: col[1], out_v&: col[2]);
5749 EndDragDropTarget();
5750 }
5751
5752 // When picker is being actively used, use its active id so IsItemActive() will function on ColorEdit4().
5753 if (picker_active_window && g.ActiveId != 0 && g.ActiveIdWindow == picker_active_window)
5754 g.LastItemData.ID = g.ActiveId;
5755
5756 if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
5757 MarkItemEdited(id: g.LastItemData.ID);
5758
5759 return value_changed;
5760}
5761
5762bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags)
5763{
5764 float col4[4] = { col[0], col[1], col[2], 1.0f };
5765 if (!ColorPicker4(label, col: col4, flags: flags | ImGuiColorEditFlags_NoAlpha))
5766 return false;
5767 col[0] = col4[0]; col[1] = col4[1]; col[2] = col4[2];
5768 return true;
5769}
5770
5771// Helper for ColorPicker4()
5772static void RenderArrowsForVerticalBar(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w, float alpha)
5773{
5774 ImU32 alpha8 = IM_F32_TO_INT8_SAT(alpha);
5775 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));
5776 ImGui::RenderArrowPointingAt(draw_list, pos: ImVec2(pos.x + half_sz.x, pos.y), half_sz, direction: ImGuiDir_Right, IM_COL32(255,255,255,alpha8));
5777 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));
5778 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));
5779}
5780
5781// Note: ColorPicker4() only accesses 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
5782// (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.)
5783// 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..)
5784// 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)
5785bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags, const float* ref_col)
5786{
5787 ImGuiContext& g = *GImGui;
5788 ImGuiWindow* window = GetCurrentWindow();
5789 if (window->SkipItems)
5790 return false;
5791
5792 ImDrawList* draw_list = window->DrawList;
5793 ImGuiStyle& style = g.Style;
5794 ImGuiIO& io = g.IO;
5795
5796 const float width = CalcItemWidth();
5797 const bool is_readonly = ((g.NextItemData.ItemFlags | g.CurrentItemFlags) & ImGuiItemFlags_ReadOnly) != 0;
5798 g.NextItemData.ClearFlags();
5799
5800 PushID(str_id: label);
5801 const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0);
5802 if (set_current_color_edit_id)
5803 g.ColorEditCurrentID = window->IDStack.back();
5804 BeginGroup();
5805
5806 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5807 flags |= ImGuiColorEditFlags_NoSmallPreview;
5808
5809 // Context menu: display and store options.
5810 if (!(flags & ImGuiColorEditFlags_NoOptions))
5811 ColorPickerOptionsPopup(ref_col: col, flags);
5812
5813 // Read stored options
5814 if (!(flags & ImGuiColorEditFlags_PickerMask_))
5815 flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_PickerMask_;
5816 if (!(flags & ImGuiColorEditFlags_InputMask_))
5817 flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_InputMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_InputMask_;
5818 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check that only 1 is selected
5819 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
5820 if (!(flags & ImGuiColorEditFlags_NoOptions))
5821 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_AlphaBar);
5822
5823 // Setup
5824 int components = (flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4;
5825 bool alpha_bar = (flags & ImGuiColorEditFlags_AlphaBar) && !(flags & ImGuiColorEditFlags_NoAlpha);
5826 ImVec2 picker_pos = window->DC.CursorPos;
5827 float square_sz = GetFrameHeight();
5828 float bars_width = square_sz; // Arbitrary smallish width of Hue/Alpha picking bars
5829 float sv_picker_size = ImMax(lhs: bars_width * 1, rhs: width - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box
5830 float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x;
5831 float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x;
5832 float bars_triangles_half_sz = IM_TRUNC(bars_width * 0.20f);
5833
5834 float backup_initial_col[4];
5835 memcpy(dest: backup_initial_col, src: col, n: components * sizeof(float));
5836
5837 float wheel_thickness = sv_picker_size * 0.08f;
5838 float wheel_r_outer = sv_picker_size * 0.50f;
5839 float wheel_r_inner = wheel_r_outer - wheel_thickness;
5840 ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width)*0.5f, picker_pos.y + sv_picker_size * 0.5f);
5841
5842 // Note: the triangle is displayed rotated with triangle_pa pointing to Hue, but most coordinates stays unrotated for logic.
5843 float triangle_r = wheel_r_inner - (int)(sv_picker_size * 0.027f);
5844 ImVec2 triangle_pa = ImVec2(triangle_r, 0.0f); // Hue point.
5845 ImVec2 triangle_pb = ImVec2(triangle_r * -0.5f, triangle_r * -0.866025f); // Black point.
5846 ImVec2 triangle_pc = ImVec2(triangle_r * -0.5f, triangle_r * +0.866025f); // White point.
5847
5848 float H = col[0], S = col[1], V = col[2];
5849 float R = col[0], G = col[1], B = col[2];
5850 if (flags & ImGuiColorEditFlags_InputRGB)
5851 {
5852 // Hue is lost when converting from grayscale rgb (saturation=0). Restore it.
5853 ColorConvertRGBtoHSV(r: R, g: G, b: B, out_h&: H, out_s&: S, out_v&: V);
5854 ColorEditRestoreHS(col, H: &H, S: &S, V: &V);
5855 }
5856 else if (flags & ImGuiColorEditFlags_InputHSV)
5857 {
5858 ColorConvertHSVtoRGB(h: H, s: S, v: V, out_r&: R, out_g&: G, out_b&: B);
5859 }
5860
5861 bool value_changed = false, value_changed_h = false, value_changed_sv = false;
5862
5863 PushItemFlag(option: ImGuiItemFlags_NoNav, enabled: true);
5864 if (flags & ImGuiColorEditFlags_PickerHueWheel)
5865 {
5866 // Hue wheel + SV triangle logic
5867 InvisibleButton(str_id: "hsv", size_arg: ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size));
5868 if (IsItemActive() && !is_readonly)
5869 {
5870 ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center;
5871 ImVec2 current_off = g.IO.MousePos - wheel_center;
5872 float initial_dist2 = ImLengthSqr(lhs: initial_off);
5873 if (initial_dist2 >= (wheel_r_inner - 1) * (wheel_r_inner - 1) && initial_dist2 <= (wheel_r_outer + 1) * (wheel_r_outer + 1))
5874 {
5875 // Interactive with Hue wheel
5876 H = ImAtan2(current_off.y, current_off.x) / IM_PI * 0.5f;
5877 if (H < 0.0f)
5878 H += 1.0f;
5879 value_changed = value_changed_h = true;
5880 }
5881 float cos_hue_angle = ImCos(-H * 2.0f * IM_PI);
5882 float sin_hue_angle = ImSin(-H * 2.0f * IM_PI);
5883 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)))
5884 {
5885 // Interacting with SV triangle
5886 ImVec2 current_off_unrotated = ImRotate(v: current_off, cos_a: cos_hue_angle, sin_a: sin_hue_angle);
5887 if (!ImTriangleContainsPoint(a: triangle_pa, b: triangle_pb, c: triangle_pc, p: current_off_unrotated))
5888 current_off_unrotated = ImTriangleClosestPoint(a: triangle_pa, b: triangle_pb, c: triangle_pc, p: current_off_unrotated);
5889 float uu, vv, ww;
5890 ImTriangleBarycentricCoords(a: triangle_pa, b: triangle_pb, c: triangle_pc, p: current_off_unrotated, out_u&: uu, out_v&: vv, out_w&: ww);
5891 V = ImClamp(v: 1.0f - vv, mn: 0.0001f, mx: 1.0f);
5892 S = ImClamp(v: uu / V, mn: 0.0001f, mx: 1.0f);
5893 value_changed = value_changed_sv = true;
5894 }
5895 }
5896 if (!(flags & ImGuiColorEditFlags_NoOptions))
5897 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5898 }
5899 else if (flags & ImGuiColorEditFlags_PickerHueBar)
5900 {
5901 // SV rectangle logic
5902 InvisibleButton(str_id: "sv", size_arg: ImVec2(sv_picker_size, sv_picker_size));
5903 if (IsItemActive() && !is_readonly)
5904 {
5905 S = ImSaturate(f: (io.MousePos.x - picker_pos.x) / (sv_picker_size - 1));
5906 V = 1.0f - ImSaturate(f: (io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5907 ColorEditRestoreH(col, H: &H); // Greatly reduces hue jitter and reset to 0 when hue == 255 and color is rapidly modified using SV square.
5908 value_changed = value_changed_sv = true;
5909 }
5910 if (!(flags & ImGuiColorEditFlags_NoOptions))
5911 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5912
5913 // Hue bar logic
5914 SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y));
5915 InvisibleButton(str_id: "hue", size_arg: ImVec2(bars_width, sv_picker_size));
5916 if (IsItemActive() && !is_readonly)
5917 {
5918 H = ImSaturate(f: (io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5919 value_changed = value_changed_h = true;
5920 }
5921 }
5922
5923 // Alpha bar logic
5924 if (alpha_bar)
5925 {
5926 SetCursorScreenPos(ImVec2(bar1_pos_x, picker_pos.y));
5927 InvisibleButton(str_id: "alpha", size_arg: ImVec2(bars_width, sv_picker_size));
5928 if (IsItemActive())
5929 {
5930 col[3] = 1.0f - ImSaturate(f: (io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5931 value_changed = true;
5932 }
5933 }
5934 PopItemFlag(); // ImGuiItemFlags_NoNav
5935
5936 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5937 {
5938 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
5939 BeginGroup();
5940 }
5941
5942 if (!(flags & ImGuiColorEditFlags_NoLabel))
5943 {
5944 const char* label_display_end = FindRenderedTextEnd(text: label);
5945 if (label != label_display_end)
5946 {
5947 if ((flags & ImGuiColorEditFlags_NoSidePreview))
5948 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
5949 TextEx(text: label, text_end: label_display_end);
5950 }
5951 }
5952
5953 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5954 {
5955 PushItemFlag(option: ImGuiItemFlags_NoNavDefaultFocus, enabled: true);
5956 ImVec4 col_v4(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
5957 if ((flags & ImGuiColorEditFlags_NoLabel))
5958 Text(fmt: "Current");
5959
5960 ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaMask_ | ImGuiColorEditFlags_NoTooltip;
5961 ColorButton(desc_id: "##current", col: col_v4, flags: (flags & sub_flags_to_forward), size: ImVec2(square_sz * 3, square_sz * 2));
5962 if (ref_col != NULL)
5963 {
5964 Text(fmt: "Original");
5965 ImVec4 ref_col_v4(ref_col[0], ref_col[1], ref_col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : ref_col[3]);
5966 if (ColorButton(desc_id: "##original", col: ref_col_v4, flags: (flags & sub_flags_to_forward), size: ImVec2(square_sz * 3, square_sz * 2)))
5967 {
5968 memcpy(dest: col, src: ref_col, n: components * sizeof(float));
5969 value_changed = true;
5970 }
5971 }
5972 PopItemFlag();
5973 EndGroup();
5974 }
5975
5976 // Convert back color to RGB
5977 if (value_changed_h || value_changed_sv)
5978 {
5979 if (flags & ImGuiColorEditFlags_InputRGB)
5980 {
5981 ColorConvertHSVtoRGB(h: H, s: S, v: V, out_r&: col[0], out_g&: col[1], out_b&: col[2]);
5982 g.ColorEditSavedHue = H;
5983 g.ColorEditSavedSat = S;
5984 g.ColorEditSavedID = g.ColorEditCurrentID;
5985 g.ColorEditSavedColor = ColorConvertFloat4ToU32(in: ImVec4(col[0], col[1], col[2], 0));
5986 }
5987 else if (flags & ImGuiColorEditFlags_InputHSV)
5988 {
5989 col[0] = H;
5990 col[1] = S;
5991 col[2] = V;
5992 }
5993 }
5994
5995 // R,G,B and H,S,V slider color editor
5996 bool value_changed_fix_hue_wrap = false;
5997 if ((flags & ImGuiColorEditFlags_NoInputs) == 0)
5998 {
5999 PushItemWidth(item_width: (alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x);
6000 ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaMask_ | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoSmallPreview;
6001 ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker;
6002 if (flags & ImGuiColorEditFlags_DisplayRGB || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6003 if (ColorEdit4(label: "##rgb", col, flags: sub_flags | ImGuiColorEditFlags_DisplayRGB))
6004 {
6005 // FIXME: Hackily differentiating using the DragInt (ActiveId != 0 && !ActiveIdAllowOverlap) vs. using the InputText or DropTarget.
6006 // 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)
6007 value_changed_fix_hue_wrap = (g.ActiveId != 0 && !g.ActiveIdAllowOverlap);
6008 value_changed = true;
6009 }
6010 if (flags & ImGuiColorEditFlags_DisplayHSV || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6011 value_changed |= ColorEdit4(label: "##hsv", col, flags: sub_flags | ImGuiColorEditFlags_DisplayHSV);
6012 if (flags & ImGuiColorEditFlags_DisplayHex || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6013 value_changed |= ColorEdit4(label: "##hex", col, flags: sub_flags | ImGuiColorEditFlags_DisplayHex);
6014 PopItemWidth();
6015 }
6016
6017 // Try to cancel hue wrap (after ColorEdit4 call), if any
6018 if (value_changed_fix_hue_wrap && (flags & ImGuiColorEditFlags_InputRGB))
6019 {
6020 float new_H, new_S, new_V;
6021 ColorConvertRGBtoHSV(r: col[0], g: col[1], b: col[2], out_h&: new_H, out_s&: new_S, out_v&: new_V);
6022 if (new_H <= 0 && H > 0)
6023 {
6024 if (new_V <= 0 && V != new_V)
6025 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]);
6026 else if (new_S <= 0)
6027 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]);
6028 }
6029 }
6030
6031 if (value_changed)
6032 {
6033 if (flags & ImGuiColorEditFlags_InputRGB)
6034 {
6035 R = col[0];
6036 G = col[1];
6037 B = col[2];
6038 ColorConvertRGBtoHSV(r: R, g: G, b: B, out_h&: H, out_s&: S, out_v&: V);
6039 ColorEditRestoreHS(col, H: &H, S: &S, V: &V); // Fix local Hue as display below will use it immediately.
6040 }
6041 else if (flags & ImGuiColorEditFlags_InputHSV)
6042 {
6043 H = col[0];
6044 S = col[1];
6045 V = col[2];
6046 ColorConvertHSVtoRGB(h: H, s: S, v: V, out_r&: R, out_g&: G, out_b&: B);
6047 }
6048 }
6049
6050 const int style_alpha8 = IM_F32_TO_INT8_SAT(style.Alpha);
6051 const ImU32 col_black = IM_COL32(0,0,0,style_alpha8);
6052 const ImU32 col_white = IM_COL32(255,255,255,style_alpha8);
6053 const ImU32 col_midgrey = IM_COL32(128,128,128,style_alpha8);
6054 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) };
6055
6056 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);
6057 ImU32 hue_color32 = ColorConvertFloat4ToU32(in: hue_color_f);
6058 ImU32 user_col32_striped_of_alpha = ColorConvertFloat4ToU32(in: ImVec4(R, G, B, style.Alpha)); // Important: this is still including the main rendering/style alpha!!
6059
6060 ImVec2 sv_cursor_pos;
6061
6062 if (flags & ImGuiColorEditFlags_PickerHueWheel)
6063 {
6064 // Render Hue Wheel
6065 const float aeps = 0.5f / wheel_r_outer; // Half a pixel arc length in radians (2pi cancels out).
6066 const int segment_per_arc = ImMax(lhs: 4, rhs: (int)wheel_r_outer / 12);
6067 for (int n = 0; n < 6; n++)
6068 {
6069 const float a0 = (n) /6.0f * 2.0f * IM_PI - aeps;
6070 const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps;
6071 const int vert_start_idx = draw_list->VtxBuffer.Size;
6072 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);
6073 draw_list->PathStroke(col: col_white, flags: 0, thickness: wheel_thickness);
6074 const int vert_end_idx = draw_list->VtxBuffer.Size;
6075
6076 // Paint colors over existing vertices
6077 ImVec2 gradient_p0(wheel_center.x + ImCos(a0) * wheel_r_inner, wheel_center.y + ImSin(a0) * wheel_r_inner);
6078 ImVec2 gradient_p1(wheel_center.x + ImCos(a1) * wheel_r_inner, wheel_center.y + ImSin(a1) * wheel_r_inner);
6079 ShadeVertsLinearColorGradientKeepAlpha(draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, col0: col_hues[n], col1: col_hues[n + 1]);
6080 }
6081
6082 // Render Cursor + preview on Hue Wheel
6083 float cos_hue_angle = ImCos(H * 2.0f * IM_PI);
6084 float sin_hue_angle = ImSin(H * 2.0f * IM_PI);
6085 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);
6086 float hue_cursor_rad = value_changed_h ? wheel_thickness * 0.65f : wheel_thickness * 0.55f;
6087 int hue_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(radius: hue_cursor_rad); // Lock segment count so the +1 one matches others.
6088 draw_list->AddCircleFilled(center: hue_cursor_pos, radius: hue_cursor_rad, col: hue_color32, num_segments: hue_cursor_segments);
6089 draw_list->AddCircle(center: hue_cursor_pos, radius: hue_cursor_rad + 1, col: col_midgrey, num_segments: hue_cursor_segments);
6090 draw_list->AddCircle(center: hue_cursor_pos, radius: hue_cursor_rad, col: col_white, num_segments: hue_cursor_segments);
6091
6092 // Render SV triangle (rotated according to hue)
6093 ImVec2 tra = wheel_center + ImRotate(v: triangle_pa, cos_a: cos_hue_angle, sin_a: sin_hue_angle);
6094 ImVec2 trb = wheel_center + ImRotate(v: triangle_pb, cos_a: cos_hue_angle, sin_a: sin_hue_angle);
6095 ImVec2 trc = wheel_center + ImRotate(v: triangle_pc, cos_a: cos_hue_angle, sin_a: sin_hue_angle);
6096 ImVec2 uv_white = GetFontTexUvWhitePixel();
6097 draw_list->PrimReserve(idx_count: 3, vtx_count: 3);
6098 draw_list->PrimVtx(pos: tra, uv: uv_white, col: hue_color32);
6099 draw_list->PrimVtx(pos: trb, uv: uv_white, col: col_black);
6100 draw_list->PrimVtx(pos: trc, uv: uv_white, col: col_white);
6101 draw_list->AddTriangle(p1: tra, p2: trb, p3: trc, col: col_midgrey, thickness: 1.5f);
6102 sv_cursor_pos = ImLerp(a: ImLerp(a: trc, b: tra, t: ImSaturate(f: S)), b: trb, t: ImSaturate(f: 1 - V));
6103 }
6104 else if (flags & ImGuiColorEditFlags_PickerHueBar)
6105 {
6106 // Render SV Square
6107 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);
6108 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);
6109 RenderFrameBorder(p_min: picker_pos, p_max: picker_pos + ImVec2(sv_picker_size, sv_picker_size), rounding: 0.0f);
6110 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
6111 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);
6112
6113 // Render Hue Bar
6114 for (int i = 0; i < 6; ++i)
6115 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]);
6116 float bar0_line_y = IM_ROUND(picker_pos.y + H * sv_picker_size);
6117 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);
6118 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);
6119 }
6120
6121 // Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range)
6122 float sv_cursor_rad = value_changed_sv ? wheel_thickness * 0.55f : wheel_thickness * 0.40f;
6123 int sv_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(radius: sv_cursor_rad); // Lock segment count so the +1 one matches others.
6124 draw_list->AddCircleFilled(center: sv_cursor_pos, radius: sv_cursor_rad, col: user_col32_striped_of_alpha, num_segments: sv_cursor_segments);
6125 draw_list->AddCircle(center: sv_cursor_pos, radius: sv_cursor_rad + 1, col: col_midgrey, num_segments: sv_cursor_segments);
6126 draw_list->AddCircle(center: sv_cursor_pos, radius: sv_cursor_rad, col: col_white, num_segments: sv_cursor_segments);
6127
6128 // Render alpha bar
6129 if (alpha_bar)
6130 {
6131 float alpha = ImSaturate(f: col[3]);
6132 ImRect bar1_bb(bar1_pos_x, picker_pos.y, bar1_pos_x + bars_width, picker_pos.y + sv_picker_size);
6133 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));
6134 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);
6135 float bar1_line_y = IM_ROUND(picker_pos.y + (1.0f - alpha) * sv_picker_size);
6136 RenderFrameBorder(p_min: bar1_bb.Min, p_max: bar1_bb.Max, rounding: 0.0f);
6137 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);
6138 }
6139
6140 EndGroup();
6141
6142 if (value_changed && memcmp(s1: backup_initial_col, s2: col, n: components * sizeof(float)) == 0)
6143 value_changed = false;
6144 if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
6145 MarkItemEdited(id: g.LastItemData.ID);
6146
6147 if (set_current_color_edit_id)
6148 g.ColorEditCurrentID = 0;
6149 PopID();
6150
6151 return value_changed;
6152}
6153
6154// A little color square. Return true when clicked.
6155// FIXME: May want to display/ignore the alpha component in the color display? Yet show it in the tooltip.
6156// 'desc_id' is not called 'label' because we don't display it next to the button, but only in the tooltip.
6157// Note that 'col' may be encoded in HSV if ImGuiColorEditFlags_InputHSV is set.
6158bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, const ImVec2& size_arg)
6159{
6160 ImGuiWindow* window = GetCurrentWindow();
6161 if (window->SkipItems)
6162 return false;
6163
6164 ImGuiContext& g = *GImGui;
6165 const ImGuiID id = window->GetID(str: desc_id);
6166 const float default_size = GetFrameHeight();
6167 const ImVec2 size(size_arg.x == 0.0f ? default_size : size_arg.x, size_arg.y == 0.0f ? default_size : size_arg.y);
6168 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
6169 ItemSize(bb, text_baseline_y: (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f);
6170 if (!ItemAdd(bb, id))
6171 return false;
6172
6173 bool hovered, held;
6174 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held);
6175
6176 if (flags & (ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaOpaque))
6177 flags &= ~(ImGuiColorEditFlags_AlphaNoBg | ImGuiColorEditFlags_AlphaPreviewHalf);
6178
6179 ImVec4 col_rgb = col;
6180 if (flags & ImGuiColorEditFlags_InputHSV)
6181 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);
6182
6183 ImVec4 col_rgb_without_alpha(col_rgb.x, col_rgb.y, col_rgb.z, 1.0f);
6184 float grid_step = ImMin(lhs: size.x, rhs: size.y) / 2.99f;
6185 float rounding = ImMin(lhs: g.Style.FrameRounding, rhs: grid_step * 0.5f);
6186 ImRect bb_inner = bb;
6187 float off = 0.0f;
6188 if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
6189 {
6190 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.
6191 bb_inner.Expand(amount: off);
6192 }
6193 if ((flags & ImGuiColorEditFlags_AlphaPreviewHalf) && col_rgb.w < 1.0f)
6194 {
6195 float mid_x = IM_ROUND((bb_inner.Min.x + bb_inner.Max.x) * 0.5f);
6196 if ((flags & ImGuiColorEditFlags_AlphaNoBg) == 0)
6197 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);
6198 else
6199 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);
6200 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);
6201 }
6202 else
6203 {
6204 // Because GetColorU32() multiplies by the global style Alpha and we don't want to display a checkerboard if the source code had no alpha
6205 ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaOpaque) ? col_rgb_without_alpha : col_rgb;
6206 if (col_source.w < 1.0f && (flags & ImGuiColorEditFlags_AlphaNoBg) == 0)
6207 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);
6208 else
6209 window->DrawList->AddRectFilled(p_min: bb_inner.Min, p_max: bb_inner.Max, col: GetColorU32(col: col_source), rounding);
6210 }
6211 RenderNavCursor(bb, id);
6212 if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
6213 {
6214 if (g.Style.FrameBorderSize > 0.0f)
6215 RenderFrameBorder(p_min: bb.Min, p_max: bb.Max, rounding);
6216 else
6217 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
6218 }
6219
6220 // Drag and Drop Source
6221 // NB: The ActiveId test is merely an optional micro-optimization, BeginDragDropSource() does the same test.
6222 if (g.ActiveId == id && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropSource())
6223 {
6224 if (flags & ImGuiColorEditFlags_NoAlpha)
6225 SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F, data: &col_rgb, sz: sizeof(float) * 3, cond: ImGuiCond_Once);
6226 else
6227 SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, data: &col_rgb, sz: sizeof(float) * 4, cond: ImGuiCond_Once);
6228 ColorButton(desc_id, col, flags);
6229 SameLine();
6230 TextEx(text: "Color");
6231 EndDragDropSource();
6232 }
6233
6234 // Tooltip
6235 if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered && IsItemHovered(flags: ImGuiHoveredFlags_ForTooltip))
6236 ColorTooltip(text: desc_id, col: &col.x, flags: flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_AlphaMask_));
6237
6238 return pressed;
6239}
6240
6241// Initialize/override default color options
6242void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags)
6243{
6244 ImGuiContext& g = *GImGui;
6245 if ((flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6246 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DisplayMask_;
6247 if ((flags & ImGuiColorEditFlags_DataTypeMask_) == 0)
6248 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DataTypeMask_;
6249 if ((flags & ImGuiColorEditFlags_PickerMask_) == 0)
6250 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_PickerMask_;
6251 if ((flags & ImGuiColorEditFlags_InputMask_) == 0)
6252 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_InputMask_;
6253 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check only 1 option is selected
6254 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DataTypeMask_)); // Check only 1 option is selected
6255 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check only 1 option is selected
6256 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check only 1 option is selected
6257 g.ColorEditOptions = flags;
6258}
6259
6260// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
6261void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags)
6262{
6263 ImGuiContext& g = *GImGui;
6264
6265 if (!BeginTooltipEx(tooltip_flags: ImGuiTooltipFlags_OverridePrevious, extra_window_flags: ImGuiWindowFlags_None))
6266 return;
6267 const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text;
6268 if (text_end > text)
6269 {
6270 TextEx(text, text_end);
6271 Separator();
6272 }
6273
6274 ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2);
6275 ImVec4 cf(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6276 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]);
6277 ImGuiColorEditFlags flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_AlphaMask_;
6278 ColorButton(desc_id: "##preview", col: cf, flags: (flags & flags_to_forward) | ImGuiColorEditFlags_NoTooltip, size_arg: sz);
6279 SameLine();
6280 if ((flags & ImGuiColorEditFlags_InputRGB) || !(flags & ImGuiColorEditFlags_InputMask_))
6281 {
6282 if (flags & ImGuiColorEditFlags_NoAlpha)
6283 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]);
6284 else
6285 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]);
6286 }
6287 else if (flags & ImGuiColorEditFlags_InputHSV)
6288 {
6289 if (flags & ImGuiColorEditFlags_NoAlpha)
6290 Text(fmt: "H: %.3f, S: %.3f, V: %.3f", col[0], col[1], col[2]);
6291 else
6292 Text(fmt: "H: %.3f, S: %.3f, V: %.3f, A: %.3f", col[0], col[1], col[2], col[3]);
6293 }
6294 EndTooltip();
6295}
6296
6297void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags)
6298{
6299 bool allow_opt_inputs = !(flags & ImGuiColorEditFlags_DisplayMask_);
6300 bool allow_opt_datatype = !(flags & ImGuiColorEditFlags_DataTypeMask_);
6301 if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup(str_id: "context"))
6302 return;
6303
6304 ImGuiContext& g = *GImGui;
6305 PushItemFlag(option: ImGuiItemFlags_NoMarkEdited, enabled: true);
6306 ImGuiColorEditFlags opts = g.ColorEditOptions;
6307 if (allow_opt_inputs)
6308 {
6309 if (RadioButton(label: "RGB", active: (opts & ImGuiColorEditFlags_DisplayRGB) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayRGB;
6310 if (RadioButton(label: "HSV", active: (opts & ImGuiColorEditFlags_DisplayHSV) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHSV;
6311 if (RadioButton(label: "Hex", active: (opts & ImGuiColorEditFlags_DisplayHex) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHex;
6312 }
6313 if (allow_opt_datatype)
6314 {
6315 if (allow_opt_inputs) Separator();
6316 if (RadioButton(label: "0..255", active: (opts & ImGuiColorEditFlags_Uint8) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Uint8;
6317 if (RadioButton(label: "0.00..1.00", active: (opts & ImGuiColorEditFlags_Float) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Float;
6318 }
6319
6320 if (allow_opt_inputs || allow_opt_datatype)
6321 Separator();
6322 if (Button(label: "Copy as..", size_arg: ImVec2(-1, 0)))
6323 OpenPopup(str_id: "Copy");
6324 if (BeginPopup(str_id: "Copy"))
6325 {
6326 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]);
6327 char buf[64];
6328 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "(%.3ff, %.3ff, %.3ff, %.3ff)", col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6329 if (Selectable(label: buf))
6330 SetClipboardText(buf);
6331 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "(%d,%d,%d,%d)", cr, cg, cb, ca);
6332 if (Selectable(label: buf))
6333 SetClipboardText(buf);
6334 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "#%02X%02X%02X", cr, cg, cb);
6335 if (Selectable(label: buf))
6336 SetClipboardText(buf);
6337 if (!(flags & ImGuiColorEditFlags_NoAlpha))
6338 {
6339 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "#%02X%02X%02X%02X", cr, cg, cb, ca);
6340 if (Selectable(label: buf))
6341 SetClipboardText(buf);
6342 }
6343 EndPopup();
6344 }
6345
6346 g.ColorEditOptions = opts;
6347 PopItemFlag();
6348 EndPopup();
6349}
6350
6351void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags)
6352{
6353 bool allow_opt_picker = !(flags & ImGuiColorEditFlags_PickerMask_);
6354 bool allow_opt_alpha_bar = !(flags & ImGuiColorEditFlags_NoAlpha) && !(flags & ImGuiColorEditFlags_AlphaBar);
6355 if ((!allow_opt_picker && !allow_opt_alpha_bar) || !BeginPopup(str_id: "context"))
6356 return;
6357
6358 ImGuiContext& g = *GImGui;
6359 PushItemFlag(option: ImGuiItemFlags_NoMarkEdited, enabled: true);
6360 if (allow_opt_picker)
6361 {
6362 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
6363 PushItemWidth(item_width: picker_size.x);
6364 for (int picker_type = 0; picker_type < 2; picker_type++)
6365 {
6366 // Draw small/thumbnail version of each picker type (over an invisible button for selection)
6367 if (picker_type > 0) Separator();
6368 PushID(int_id: picker_type);
6369 ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoSidePreview | (flags & ImGuiColorEditFlags_NoAlpha);
6370 if (picker_type == 0) picker_flags |= ImGuiColorEditFlags_PickerHueBar;
6371 if (picker_type == 1) picker_flags |= ImGuiColorEditFlags_PickerHueWheel;
6372 ImVec2 backup_pos = GetCursorScreenPos();
6373 if (Selectable(label: "##selectable", selected: false, flags: 0, size: picker_size)) // By default, Selectable() is closing popup
6374 g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags_PickerMask_) | (picker_flags & ImGuiColorEditFlags_PickerMask_);
6375 SetCursorScreenPos(backup_pos);
6376 ImVec4 previewing_ref_col;
6377 memcpy(dest: &previewing_ref_col, src: ref_col, n: sizeof(float) * ((picker_flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4));
6378 ColorPicker4(label: "##previewing_picker", col: &previewing_ref_col.x, flags: picker_flags);
6379 PopID();
6380 }
6381 PopItemWidth();
6382 }
6383 if (allow_opt_alpha_bar)
6384 {
6385 if (allow_opt_picker) Separator();
6386 CheckboxFlags(label: "Alpha Bar", flags: &g.ColorEditOptions, flags_value: ImGuiColorEditFlags_AlphaBar);
6387 }
6388 PopItemFlag();
6389 EndPopup();
6390}
6391
6392//-------------------------------------------------------------------------
6393// [SECTION] Widgets: TreeNode, CollapsingHeader, etc.
6394//-------------------------------------------------------------------------
6395// - TreeNode()
6396// - TreeNodeV()
6397// - TreeNodeEx()
6398// - TreeNodeExV()
6399// - TreeNodeBehavior() [Internal]
6400// - TreePush()
6401// - TreePop()
6402// - GetTreeNodeToLabelSpacing()
6403// - SetNextItemOpen()
6404// - CollapsingHeader()
6405//-------------------------------------------------------------------------
6406
6407bool ImGui::TreeNode(const char* str_id, const char* fmt, ...)
6408{
6409 va_list args;
6410 va_start(args, fmt);
6411 bool is_open = TreeNodeExV(str_id, flags: 0, fmt, args);
6412 va_end(args);
6413 return is_open;
6414}
6415
6416bool ImGui::TreeNode(const void* ptr_id, const char* fmt, ...)
6417{
6418 va_list args;
6419 va_start(args, fmt);
6420 bool is_open = TreeNodeExV(ptr_id, flags: 0, fmt, args);
6421 va_end(args);
6422 return is_open;
6423}
6424
6425bool ImGui::TreeNode(const char* label)
6426{
6427 ImGuiWindow* window = GetCurrentWindow();
6428 if (window->SkipItems)
6429 return false;
6430 ImGuiID id = window->GetID(str: label);
6431 return TreeNodeBehavior(id, flags: ImGuiTreeNodeFlags_None, label, NULL);
6432}
6433
6434bool ImGui::TreeNodeV(const char* str_id, const char* fmt, va_list args)
6435{
6436 return TreeNodeExV(str_id, flags: 0, fmt, args);
6437}
6438
6439bool ImGui::TreeNodeV(const void* ptr_id, const char* fmt, va_list args)
6440{
6441 return TreeNodeExV(ptr_id, flags: 0, fmt, args);
6442}
6443
6444bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags)
6445{
6446 ImGuiWindow* window = GetCurrentWindow();
6447 if (window->SkipItems)
6448 return false;
6449 ImGuiID id = window->GetID(str: label);
6450 return TreeNodeBehavior(id, flags, label, NULL);
6451}
6452
6453bool ImGui::TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
6454{
6455 va_list args;
6456 va_start(args, fmt);
6457 bool is_open = TreeNodeExV(str_id, flags, fmt, args);
6458 va_end(args);
6459 return is_open;
6460}
6461
6462bool ImGui::TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
6463{
6464 va_list args;
6465 va_start(args, fmt);
6466 bool is_open = TreeNodeExV(ptr_id, flags, fmt, args);
6467 va_end(args);
6468 return is_open;
6469}
6470
6471bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
6472{
6473 ImGuiWindow* window = GetCurrentWindow();
6474 if (window->SkipItems)
6475 return false;
6476
6477 ImGuiID id = window->GetID(str: str_id);
6478 const char* label, *label_end;
6479 ImFormatStringToTempBufferV(out_buf: &label, out_buf_end: &label_end, fmt, args);
6480 return TreeNodeBehavior(id, flags, label, label_end);
6481}
6482
6483bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
6484{
6485 ImGuiWindow* window = GetCurrentWindow();
6486 if (window->SkipItems)
6487 return false;
6488
6489 ImGuiID id = window->GetID(ptr: ptr_id);
6490 const char* label, *label_end;
6491 ImFormatStringToTempBufferV(out_buf: &label, out_buf_end: &label_end, fmt, args);
6492 return TreeNodeBehavior(id, flags, label, label_end);
6493}
6494
6495bool ImGui::TreeNodeGetOpen(ImGuiID storage_id)
6496{
6497 ImGuiContext& g = *GImGui;
6498 ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
6499 return storage->GetInt(key: storage_id, default_val: 0) != 0;
6500}
6501
6502void ImGui::TreeNodeSetOpen(ImGuiID storage_id, bool open)
6503{
6504 ImGuiContext& g = *GImGui;
6505 ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
6506 storage->SetInt(key: storage_id, val: open ? 1 : 0);
6507}
6508
6509bool ImGui::TreeNodeUpdateNextOpen(ImGuiID storage_id, ImGuiTreeNodeFlags flags)
6510{
6511 if (flags & ImGuiTreeNodeFlags_Leaf)
6512 return true;
6513
6514 // We only write to the tree storage if the user clicks, or explicitly use the SetNextItemOpen function
6515 ImGuiContext& g = *GImGui;
6516 ImGuiWindow* window = g.CurrentWindow;
6517 ImGuiStorage* storage = window->DC.StateStorage;
6518
6519 bool is_open;
6520 if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasOpen)
6521 {
6522 if (g.NextItemData.OpenCond & ImGuiCond_Always)
6523 {
6524 is_open = g.NextItemData.OpenVal;
6525 TreeNodeSetOpen(storage_id, open: is_open);
6526 }
6527 else
6528 {
6529 // We treat ImGuiCond_Once and ImGuiCond_FirstUseEver the same because tree node state are not saved persistently.
6530 const int stored_value = storage->GetInt(key: storage_id, default_val: -1);
6531 if (stored_value == -1)
6532 {
6533 is_open = g.NextItemData.OpenVal;
6534 TreeNodeSetOpen(storage_id, open: is_open);
6535 }
6536 else
6537 {
6538 is_open = stored_value != 0;
6539 }
6540 }
6541 }
6542 else
6543 {
6544 is_open = storage->GetInt(key: storage_id, default_val: (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0;
6545 }
6546
6547 // When logging is enabled, we automatically expand tree nodes (but *NOT* collapsing headers.. seems like sensible behavior).
6548 // NB- If we are above max depth we still allow manually opened nodes to be logged.
6549 if (g.LogEnabled && !(flags & ImGuiTreeNodeFlags_NoAutoOpenOnLog) && (window->DC.TreeDepth - g.LogDepthRef) < g.LogDepthToExpand)
6550 is_open = true;
6551
6552 return is_open;
6553}
6554
6555// Store ImGuiTreeNodeStackData for just submitted node.
6556// Currently only supports 32 level deep and we are fine with (1 << Depth) overflowing into a zero, easy to increase.
6557static void TreeNodeStoreStackData(ImGuiTreeNodeFlags flags)
6558{
6559 ImGuiContext& g = *GImGui;
6560 ImGuiWindow* window = g.CurrentWindow;
6561
6562 g.TreeNodeStack.resize(new_size: g.TreeNodeStack.Size + 1);
6563 ImGuiTreeNodeStackData* tree_node_data = &g.TreeNodeStack.back();
6564 tree_node_data->ID = g.LastItemData.ID;
6565 tree_node_data->TreeFlags = flags;
6566 tree_node_data->ItemFlags = g.LastItemData.ItemFlags;
6567 tree_node_data->NavRect = g.LastItemData.NavRect;
6568 window->DC.TreeHasStackDataDepthMask |= (1 << window->DC.TreeDepth);
6569}
6570
6571// 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.
6572bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end)
6573{
6574 ImGuiWindow* window = GetCurrentWindow();
6575 if (window->SkipItems)
6576 return false;
6577
6578 ImGuiContext& g = *GImGui;
6579 const ImGuiStyle& style = g.Style;
6580 const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0;
6581 const ImVec2 padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, ImMin(lhs: window->DC.CurrLineTextBaseOffset, rhs: style.FramePadding.y));
6582
6583 if (!label_end)
6584 label_end = FindRenderedTextEnd(text: label);
6585 const ImVec2 label_size = CalcTextSize(text: label, text_end: label_end, hide_text_after_double_hash: false);
6586
6587 const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapsing arrow width + Spacing
6588 const float text_offset_y = ImMax(lhs: padding.y, rhs: window->DC.CurrLineTextBaseOffset); // Latch before ItemSize changes it
6589 const float text_width = g.FontSize + label_size.x + padding.x * 2; // Include collapsing arrow
6590
6591 // We vertically grow up to current line height up the typical widget height.
6592 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);
6593 const bool span_all_columns = (flags & ImGuiTreeNodeFlags_SpanAllColumns) != 0 && (g.CurrentTable != NULL);
6594 const bool span_all_columns_label = (flags & ImGuiTreeNodeFlags_LabelSpanAllColumns) != 0 && (g.CurrentTable != NULL);
6595 ImRect frame_bb;
6596 frame_bb.Min.x = span_all_columns ? window->ParentWorkRect.Min.x : (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x;
6597 frame_bb.Min.y = window->DC.CursorPos.y;
6598 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;
6599 frame_bb.Max.y = window->DC.CursorPos.y + frame_height;
6600 if (display_frame)
6601 {
6602 const float outer_extend = IM_TRUNC(window->WindowPadding.x * 0.5f); // Framed header expand a little outside of current limits
6603 frame_bb.Min.x -= outer_extend;
6604 frame_bb.Max.x += outer_extend;
6605 }
6606
6607 ImVec2 text_pos(window->DC.CursorPos.x + text_offset_x, window->DC.CursorPos.y + text_offset_y);
6608 ItemSize(size: ImVec2(text_width, frame_height), text_baseline_y: padding.y);
6609
6610 // For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing
6611 ImRect interact_bb = frame_bb;
6612 if ((flags & (ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_SpanLabelWidth | ImGuiTreeNodeFlags_SpanAllColumns)) == 0)
6613 interact_bb.Max.x = frame_bb.Min.x + text_width + (label_size.x > 0.0f ? style.ItemSpacing.x * 2.0f : 0.0f);
6614
6615 // Compute open and multi-select states before ItemAdd() as it clear NextItem data.
6616 ImGuiID storage_id = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasStorageID) ? g.NextItemData.StorageId : id;
6617 bool is_open = TreeNodeUpdateNextOpen(storage_id, flags);
6618
6619 bool is_visible;
6620 if (span_all_columns || span_all_columns_label)
6621 {
6622 // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable..
6623 const float backup_clip_rect_min_x = window->ClipRect.Min.x;
6624 const float backup_clip_rect_max_x = window->ClipRect.Max.x;
6625 window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
6626 window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
6627 is_visible = ItemAdd(bb: interact_bb, id);
6628 window->ClipRect.Min.x = backup_clip_rect_min_x;
6629 window->ClipRect.Max.x = backup_clip_rect_max_x;
6630 }
6631 else
6632 {
6633 is_visible = ItemAdd(bb: interact_bb, id);
6634 }
6635 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect;
6636 g.LastItemData.DisplayRect = frame_bb;
6637
6638 // If a NavLeft request is happening and ImGuiTreeNodeFlags_NavLeftJumpsBackHere enabled:
6639 // Store data for the current depth to allow returning to this node from any child item.
6640 // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop().
6641 // It will become tempting to enable ImGuiTreeNodeFlags_NavLeftJumpsBackHere by default or move it to ImGuiStyle.
6642 bool store_tree_node_stack_data = false;
6643 if (!(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6644 {
6645 if ((flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && is_open && !g.NavIdIsAlive)
6646 if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
6647 store_tree_node_stack_data = true;
6648 }
6649
6650 const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0;
6651 if (!is_visible)
6652 {
6653 if (store_tree_node_stack_data && is_open)
6654 TreeNodeStoreStackData(flags); // Call before TreePushOverrideID()
6655 if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6656 TreePushOverrideID(id);
6657 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
6658 return is_open;
6659 }
6660
6661 if (span_all_columns || span_all_columns_label)
6662 {
6663 TablePushBackgroundChannel();
6664 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect;
6665 g.LastItemData.ClipRect = window->ClipRect;
6666 }
6667
6668 ImGuiButtonFlags button_flags = ImGuiTreeNodeFlags_None;
6669 if ((flags & ImGuiTreeNodeFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap))
6670 button_flags |= ImGuiButtonFlags_AllowOverlap;
6671 if (!is_leaf)
6672 button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
6673
6674 // We allow clicking on the arrow section with keyboard modifiers held, in order to easily
6675 // allow browsing a tree while preserving selection with code implementing multi-selection patterns.
6676 // When clicking on the rest of the tree node we always disallow keyboard modifiers.
6677 const float arrow_hit_x1 = (text_pos.x - text_offset_x) - style.TouchExtraPadding.x;
6678 const float arrow_hit_x2 = (text_pos.x - text_offset_x) + (g.FontSize + padding.x * 2.0f) + style.TouchExtraPadding.x;
6679 const bool is_mouse_x_over_arrow = (g.IO.MousePos.x >= arrow_hit_x1 && g.IO.MousePos.x < arrow_hit_x2);
6680
6681 const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
6682 if (is_multi_select) // We absolutely need to distinguish open vs select so _OpenOnArrow comes by default
6683 flags |= (flags & ImGuiTreeNodeFlags_OpenOnMask_) == 0 ? ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick : ImGuiTreeNodeFlags_OpenOnArrow;
6684
6685 // Open behaviors can be altered with the _OpenOnArrow and _OnOnDoubleClick flags.
6686 // Some alteration have subtle effects (e.g. toggle on MouseUp vs MouseDown events) due to requirements for multi-selection and drag and drop support.
6687 // - Single-click on label = Toggle on MouseUp (default, when _OpenOnArrow=0)
6688 // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=0)
6689 // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=1)
6690 // - Double-click on label = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1)
6691 // - Double-click on arrow = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1 and _OpenOnArrow=0)
6692 // It is rather standard that arrow click react on Down rather than Up.
6693 // 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.
6694 if (is_mouse_x_over_arrow)
6695 button_flags |= ImGuiButtonFlags_PressedOnClick;
6696 else if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick)
6697 button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick;
6698 else
6699 button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
6700
6701 bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0;
6702 const bool was_selected = selected;
6703
6704 // Multi-selection support (header)
6705 if (is_multi_select)
6706 {
6707 // Handle multi-select + alter button flags for it
6708 MultiSelectItemHeader(id, p_selected: &selected, p_button_flags: &button_flags);
6709 if (is_mouse_x_over_arrow)
6710 button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease;
6711 }
6712 else
6713 {
6714 if (window != g.HoveredWindow || !is_mouse_x_over_arrow)
6715 button_flags |= ImGuiButtonFlags_NoKeyModsAllowed;
6716 }
6717
6718 bool hovered, held;
6719 bool pressed = ButtonBehavior(bb: interact_bb, id, out_hovered: &hovered, out_held: &held, flags: button_flags);
6720 bool toggled = false;
6721 if (!is_leaf)
6722 {
6723 if (pressed && g.DragDropHoldJustPressedId != id)
6724 {
6725 if ((flags & ImGuiTreeNodeFlags_OpenOnMask_) == 0 || (g.NavActivateId == id && !is_multi_select))
6726 toggled = true; // Single click
6727 if (flags & ImGuiTreeNodeFlags_OpenOnArrow)
6728 toggled |= is_mouse_x_over_arrow && !g.NavHighlightItemUnderNav; // Lightweight equivalent of IsMouseHoveringRect() since ButtonBehavior() already did the job
6729 if ((flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) && g.IO.MouseClickedCount[0] == 2)
6730 toggled = true; // Double click
6731 }
6732 else if (pressed && g.DragDropHoldJustPressedId == id)
6733 {
6734 IM_ASSERT(button_flags & ImGuiButtonFlags_PressedOnDragDropHold);
6735 if (!is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again.
6736 toggled = true;
6737 else
6738 pressed = false; // Cancel press so it doesn't trigger selection.
6739 }
6740
6741 if (g.NavId == id && g.NavMoveDir == ImGuiDir_Left && is_open)
6742 {
6743 toggled = true;
6744 NavClearPreferredPosForAxis(axis: ImGuiAxis_X);
6745 NavMoveRequestCancel();
6746 }
6747 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?
6748 {
6749 toggled = true;
6750 NavClearPreferredPosForAxis(axis: ImGuiAxis_X);
6751 NavMoveRequestCancel();
6752 }
6753
6754 if (toggled)
6755 {
6756 is_open = !is_open;
6757 window->DC.StateStorage->SetInt(key: storage_id, val: is_open);
6758 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledOpen;
6759 }
6760 }
6761
6762 // Multi-selection support (footer)
6763 if (is_multi_select)
6764 {
6765 bool pressed_copy = pressed && !toggled;
6766 MultiSelectItemFooter(id, p_selected: &selected, p_pressed: &pressed_copy);
6767 if (pressed)
6768 SetNavID(id, nav_layer: window->DC.NavLayerCurrent, focus_scope_id: g.CurrentFocusScopeId, rect_rel: interact_bb);
6769 }
6770
6771 if (selected != was_selected)
6772 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
6773
6774 // Render
6775 {
6776 const ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
6777 ImGuiNavRenderCursorFlags nav_render_cursor_flags = ImGuiNavRenderCursorFlags_Compact;
6778 if (is_multi_select)
6779 nav_render_cursor_flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle
6780 if (display_frame)
6781 {
6782 // Framed type
6783 const ImU32 bg_col = GetColorU32(idx: (held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
6784 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: bg_col, borders: true, rounding: style.FrameRounding);
6785 RenderNavCursor(bb: frame_bb, id, flags: nav_render_cursor_flags);
6786 if (flags & ImGuiTreeNodeFlags_Bullet)
6787 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);
6788 else if (!is_leaf)
6789 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);
6790 else // Leaf without bullet, left-adjusted text
6791 text_pos.x -= text_offset_x - padding.x;
6792 if (flags & ImGuiTreeNodeFlags_ClipLabelForTrailingButton)
6793 frame_bb.Max.x -= g.FontSize + style.FramePadding.x;
6794 if (g.LogEnabled)
6795 LogSetNextTextDecoration(prefix: "###", suffix: "###");
6796 }
6797 else
6798 {
6799 // Unframed typed for tree nodes
6800 if (hovered || selected)
6801 {
6802 const ImU32 bg_col = GetColorU32(idx: (held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
6803 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: bg_col, borders: false);
6804 }
6805 RenderNavCursor(bb: frame_bb, id, flags: nav_render_cursor_flags);
6806 if (flags & ImGuiTreeNodeFlags_Bullet)
6807 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);
6808 else if (!is_leaf)
6809 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);
6810 if (g.LogEnabled)
6811 LogSetNextTextDecoration(prefix: ">", NULL);
6812 }
6813
6814 if (span_all_columns && !span_all_columns_label)
6815 TablePopBackgroundChannel();
6816
6817 // Label
6818 if (display_frame)
6819 RenderTextClipped(pos_min: text_pos, pos_max: frame_bb.Max, text: label, text_end: label_end, text_size_if_known: &label_size);
6820 else
6821 RenderText(pos: text_pos, text: label, text_end: label_end, hide_text_after_hash: false);
6822
6823 if (span_all_columns_label)
6824 TablePopBackgroundChannel();
6825 }
6826
6827 if (store_tree_node_stack_data && is_open)
6828 TreeNodeStoreStackData(flags); // Call before TreePushOverrideID()
6829 if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6830 TreePushOverrideID(id); // Could use TreePush(label) but this avoid computing twice
6831
6832 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
6833 return is_open;
6834}
6835
6836void ImGui::TreePush(const char* str_id)
6837{
6838 ImGuiWindow* window = GetCurrentWindow();
6839 Indent();
6840 window->DC.TreeDepth++;
6841 PushID(str_id);
6842}
6843
6844void ImGui::TreePush(const void* ptr_id)
6845{
6846 ImGuiWindow* window = GetCurrentWindow();
6847 Indent();
6848 window->DC.TreeDepth++;
6849 PushID(ptr_id);
6850}
6851
6852void ImGui::TreePushOverrideID(ImGuiID id)
6853{
6854 ImGuiContext& g = *GImGui;
6855 ImGuiWindow* window = g.CurrentWindow;
6856 Indent();
6857 window->DC.TreeDepth++;
6858 PushOverrideID(id);
6859}
6860
6861void ImGui::TreePop()
6862{
6863 ImGuiContext& g = *GImGui;
6864 ImGuiWindow* window = g.CurrentWindow;
6865 Unindent();
6866
6867 window->DC.TreeDepth--;
6868 ImU32 tree_depth_mask = (1 << window->DC.TreeDepth);
6869
6870 if (window->DC.TreeHasStackDataDepthMask & tree_depth_mask) // Only set during request
6871 {
6872 ImGuiTreeNodeStackData* data = &g.TreeNodeStack.back();
6873 IM_ASSERT(data->ID == window->IDStack.back());
6874 if (data->TreeFlags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere)
6875 {
6876 // Handle Left arrow to move to parent tree node (when ImGuiTreeNodeFlags_NavLeftJumpsBackHere is enabled)
6877 if (g.NavIdIsAlive && g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
6878 NavMoveRequestResolveWithPastTreeNode(result: &g.NavMoveResultLocal, tree_node_data: data);
6879 }
6880 g.TreeNodeStack.pop_back();
6881 window->DC.TreeHasStackDataDepthMask &= ~tree_depth_mask;
6882 }
6883
6884 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.
6885 PopID();
6886}
6887
6888// Horizontal distance preceding label when using TreeNode() or Bullet()
6889float ImGui::GetTreeNodeToLabelSpacing()
6890{
6891 ImGuiContext& g = *GImGui;
6892 return g.FontSize + (g.Style.FramePadding.x * 2.0f);
6893}
6894
6895// Set next TreeNode/CollapsingHeader open state.
6896void ImGui::SetNextItemOpen(bool is_open, ImGuiCond cond)
6897{
6898 ImGuiContext& g = *GImGui;
6899 if (g.CurrentWindow->SkipItems)
6900 return;
6901 g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasOpen;
6902 g.NextItemData.OpenVal = is_open;
6903 g.NextItemData.OpenCond = (ImU8)(cond ? cond : ImGuiCond_Always);
6904}
6905
6906// Set next TreeNode/CollapsingHeader storage id.
6907void ImGui::SetNextItemStorageID(ImGuiID storage_id)
6908{
6909 ImGuiContext& g = *GImGui;
6910 if (g.CurrentWindow->SkipItems)
6911 return;
6912 g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasStorageID;
6913 g.NextItemData.StorageId = storage_id;
6914}
6915
6916// CollapsingHeader returns true when opened but do not indent nor push into the ID stack (because of the ImGuiTreeNodeFlags_NoTreePushOnOpen flag).
6917// 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().
6918bool ImGui::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags)
6919{
6920 ImGuiWindow* window = GetCurrentWindow();
6921 if (window->SkipItems)
6922 return false;
6923 ImGuiID id = window->GetID(str: label);
6924 return TreeNodeBehavior(id, flags: flags | ImGuiTreeNodeFlags_CollapsingHeader, label);
6925}
6926
6927// p_visible == NULL : regular collapsing header
6928// 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
6929// p_visible != NULL && *p_visible == false : do not show the header at all
6930// Do not mistake this with the Open state of the header itself, which you can adjust with SetNextItemOpen() or ImGuiTreeNodeFlags_DefaultOpen.
6931bool ImGui::CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags)
6932{
6933 ImGuiWindow* window = GetCurrentWindow();
6934 if (window->SkipItems)
6935 return false;
6936
6937 if (p_visible && !*p_visible)
6938 return false;
6939
6940 ImGuiID id = window->GetID(str: label);
6941 flags |= ImGuiTreeNodeFlags_CollapsingHeader;
6942 if (p_visible)
6943 flags |= ImGuiTreeNodeFlags_AllowOverlap | (ImGuiTreeNodeFlags)ImGuiTreeNodeFlags_ClipLabelForTrailingButton;
6944 bool is_open = TreeNodeBehavior(id, flags, label);
6945 if (p_visible != NULL)
6946 {
6947 // Create a small overlapping close button
6948 // FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc.
6949 // FIXME: CloseButton can overlap into text, need find a way to clip the text somehow.
6950 ImGuiContext& g = *GImGui;
6951 ImGuiLastItemData last_item_backup = g.LastItemData;
6952 float button_size = g.FontSize;
6953 float button_x = ImMax(lhs: g.LastItemData.Rect.Min.x, rhs: g.LastItemData.Rect.Max.x - g.Style.FramePadding.x - button_size);
6954 float button_y = g.LastItemData.Rect.Min.y + g.Style.FramePadding.y;
6955 ImGuiID close_button_id = GetIDWithSeed(str_id_begin: "#CLOSE", NULL, seed: id);
6956 if (CloseButton(id: close_button_id, pos: ImVec2(button_x, button_y)))
6957 *p_visible = false;
6958 g.LastItemData = last_item_backup;
6959 }
6960
6961 return is_open;
6962}
6963
6964//-------------------------------------------------------------------------
6965// [SECTION] Widgets: Selectable
6966//-------------------------------------------------------------------------
6967// - Selectable()
6968//-------------------------------------------------------------------------
6969
6970// Tip: pass a non-visible label (e.g. "##hello") then you can use the space to draw other text or image.
6971// But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID or use ##unique_id.
6972// With this scheme, ImGuiSelectableFlags_SpanAllColumns and ImGuiSelectableFlags_AllowOverlap are also frequently used flags.
6973// FIXME: Selectable() with (size.x == 0.0f) and (SelectableTextAlign.x > 0.0f) followed by SameLine() is currently not supported.
6974bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
6975{
6976 ImGuiWindow* window = GetCurrentWindow();
6977 if (window->SkipItems)
6978 return false;
6979
6980 ImGuiContext& g = *GImGui;
6981 const ImGuiStyle& style = g.Style;
6982
6983 // Submit label or explicit size to ItemSize(), whereas ItemAdd() will submit a larger/spanning rectangle.
6984 ImGuiID id = window->GetID(str: label);
6985 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
6986 ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y);
6987 ImVec2 pos = window->DC.CursorPos;
6988 pos.y += window->DC.CurrLineTextBaseOffset;
6989 ItemSize(size, text_baseline_y: 0.0f);
6990
6991 // Fill horizontal space
6992 // We don't support (size < 0.0f) in Selectable() because the ItemSpacing extension would make explicitly right-aligned sizes not visibly match other widgets.
6993 const bool span_all_columns = (flags & ImGuiSelectableFlags_SpanAllColumns) != 0;
6994 const float min_x = span_all_columns ? window->ParentWorkRect.Min.x : pos.x;
6995 const float max_x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x;
6996 if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_SpanAvailWidth))
6997 size.x = ImMax(lhs: label_size.x, rhs: max_x - min_x);
6998
6999 // Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable.
7000 // FIXME: Not part of layout so not included in clipper calculation, but ItemSize currently doesn't allow offsetting CursorPos.
7001 ImRect bb(min_x, pos.y, min_x + size.x, pos.y + size.y);
7002 if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0)
7003 {
7004 const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x;
7005 const float spacing_y = style.ItemSpacing.y;
7006 const float spacing_L = IM_TRUNC(spacing_x * 0.50f);
7007 const float spacing_U = IM_TRUNC(spacing_y * 0.50f);
7008 bb.Min.x -= spacing_L;
7009 bb.Min.y -= spacing_U;
7010 bb.Max.x += (spacing_x - spacing_L);
7011 bb.Max.y += (spacing_y - spacing_U);
7012 }
7013 //if (g.IO.KeyCtrl) { GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(0, 255, 0, 255)); }
7014
7015 const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0;
7016 const ImGuiItemFlags extra_item_flags = disabled_item ? (ImGuiItemFlags)ImGuiItemFlags_Disabled : ImGuiItemFlags_None;
7017 bool is_visible;
7018 if (span_all_columns)
7019 {
7020 // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable..
7021 const float backup_clip_rect_min_x = window->ClipRect.Min.x;
7022 const float backup_clip_rect_max_x = window->ClipRect.Max.x;
7023 window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
7024 window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
7025 is_visible = ItemAdd(bb, id, NULL, extra_flags: extra_item_flags);
7026 window->ClipRect.Min.x = backup_clip_rect_min_x;
7027 window->ClipRect.Max.x = backup_clip_rect_max_x;
7028 }
7029 else
7030 {
7031 is_visible = ItemAdd(bb, id, NULL, extra_flags: extra_item_flags);
7032 }
7033
7034 const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
7035 if (!is_visible)
7036 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)
7037 return false;
7038
7039 const bool disabled_global = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0;
7040 if (disabled_item && !disabled_global) // Only testing this as an optimization
7041 BeginDisabled();
7042
7043 // FIXME: We can standardize the behavior of those two, we could also keep the fast path of override ClipRect + full push on render only,
7044 // which would be advantageous since most selectable are not selected.
7045 if (span_all_columns)
7046 {
7047 if (g.CurrentTable)
7048 TablePushBackgroundChannel();
7049 else if (window->DC.CurrentColumns)
7050 PushColumnsBackground();
7051 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect;
7052 g.LastItemData.ClipRect = window->ClipRect;
7053 }
7054
7055 // We use NoHoldingActiveID on menus so user can click and _hold_ on a menu then drag to browse child entries
7056 ImGuiButtonFlags button_flags = 0;
7057 if (flags & ImGuiSelectableFlags_NoHoldingActiveID) { button_flags |= ImGuiButtonFlags_NoHoldingActiveId; }
7058 if (flags & ImGuiSelectableFlags_NoSetKeyOwner) { button_flags |= ImGuiButtonFlags_NoSetKeyOwner; }
7059 if (flags & ImGuiSelectableFlags_SelectOnClick) { button_flags |= ImGuiButtonFlags_PressedOnClick; }
7060 if (flags & ImGuiSelectableFlags_SelectOnRelease) { button_flags |= ImGuiButtonFlags_PressedOnRelease; }
7061 if (flags & ImGuiSelectableFlags_AllowDoubleClick) { button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; }
7062 if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap)) { button_flags |= ImGuiButtonFlags_AllowOverlap; }
7063
7064 // Multi-selection support (header)
7065 const bool was_selected = selected;
7066 if (is_multi_select)
7067 {
7068 // Handle multi-select + alter button flags for it
7069 MultiSelectItemHeader(id, p_selected: &selected, p_button_flags: &button_flags);
7070 }
7071
7072 bool hovered, held;
7073 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags: button_flags);
7074
7075 // Multi-selection support (footer)
7076 if (is_multi_select)
7077 {
7078 MultiSelectItemFooter(id, p_selected: &selected, p_pressed: &pressed);
7079 }
7080 else
7081 {
7082 // Auto-select when moved into
7083 // - This will be more fully fleshed in the range-select branch
7084 // - This is not exposed as it won't nicely work with some user side handling of shift/control
7085 // - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons
7086 // - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope())
7087 // - (2) usage will fail with clipped items
7088 // The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API.
7089 if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId)
7090 if (g.NavJustMovedToId == id)
7091 selected = pressed = true;
7092 }
7093
7094 // Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with keyboard/gamepad
7095 if (pressed || (hovered && (flags & ImGuiSelectableFlags_SetNavIdOnHover)))
7096 {
7097 if (!g.NavHighlightItemUnderNav && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent)
7098 {
7099 SetNavID(id, nav_layer: window->DC.NavLayerCurrent, focus_scope_id: g.CurrentFocusScopeId, rect_rel: WindowRectAbsToRel(window, r: bb)); // (bb == NavRect)
7100 if (g.IO.ConfigNavCursorVisibleAuto)
7101 g.NavCursorVisible = false;
7102 }
7103 }
7104 if (pressed)
7105 MarkItemEdited(id);
7106
7107 if (selected != was_selected)
7108 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
7109
7110 // Render
7111 if (is_visible)
7112 {
7113 const bool highlighted = hovered || (flags & ImGuiSelectableFlags_Highlight);
7114 if (highlighted || selected)
7115 {
7116 // Between 1.91.0 and 1.91.4 we made selected Selectable use an arbitrary lerp between _Header and _HeaderHovered. Removed that now. (#8106)
7117 ImU32 col = GetColorU32(idx: (held && highlighted) ? ImGuiCol_HeaderActive : highlighted ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
7118 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: col, borders: false, rounding: 0.0f);
7119 }
7120 if (g.NavId == id)
7121 {
7122 ImGuiNavRenderCursorFlags nav_render_cursor_flags = ImGuiNavRenderCursorFlags_Compact | ImGuiNavRenderCursorFlags_NoRounding;
7123 if (is_multi_select)
7124 nav_render_cursor_flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle
7125 RenderNavCursor(bb, id, flags: nav_render_cursor_flags);
7126 }
7127 }
7128
7129 if (span_all_columns)
7130 {
7131 if (g.CurrentTable)
7132 TablePopBackgroundChannel();
7133 else if (window->DC.CurrentColumns)
7134 PopColumnsBackground();
7135 }
7136
7137 // Text stays at the submission position. Alignment/clipping extents ignore SpanAllColumns.
7138 if (is_visible)
7139 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);
7140
7141 // Automatically close popups
7142 if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_NoAutoClosePopups) && (g.LastItemData.ItemFlags & ImGuiItemFlags_AutoClosePopups))
7143 CloseCurrentPopup();
7144
7145 if (disabled_item && !disabled_global)
7146 EndDisabled();
7147
7148 // Selectable() always returns a pressed state!
7149 // Users of BeginMultiSelect()/EndMultiSelect() scope: you may call ImGui::IsItemToggledSelection() to retrieve
7150 // selection toggle, only useful if you need that state updated (e.g. for rendering purpose) before reaching EndMultiSelect().
7151 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
7152 return pressed; //-V1020
7153}
7154
7155bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
7156{
7157 if (Selectable(label, selected: *p_selected, flags, size_arg))
7158 {
7159 *p_selected = !*p_selected;
7160 return true;
7161 }
7162 return false;
7163}
7164
7165
7166//-------------------------------------------------------------------------
7167// [SECTION] Widgets: Typing-Select support
7168//-------------------------------------------------------------------------
7169
7170// [Experimental] Currently not exposed in public API.
7171// Consume character inputs and return search request, if any.
7172// This would typically only be called on the focused window or location you want to grab inputs for, e.g.
7173// if (ImGui::IsWindowFocused(...))
7174// if (ImGuiTypingSelectRequest* req = ImGui::GetTypingSelectRequest())
7175// focus_idx = ImGui::TypingSelectFindMatch(req, my_items.size(), [](void*, int n) { return my_items[n]->Name; }, &my_items, -1);
7176// However the code is written in a way where calling it from multiple locations is safe (e.g. to obtain buffer).
7177ImGuiTypingSelectRequest* ImGui::GetTypingSelectRequest(ImGuiTypingSelectFlags flags)
7178{
7179 ImGuiContext& g = *GImGui;
7180 ImGuiTypingSelectState* data = &g.TypingSelectState;
7181 ImGuiTypingSelectRequest* out_request = &data->Request;
7182
7183 // Clear buffer
7184 const float TYPING_SELECT_RESET_TIMER = 1.80f; // FIXME: Potentially move to IO config.
7185 const int TYPING_SELECT_SINGLE_CHAR_COUNT_FOR_LOCK = 4; // Lock single char matching when repeating same char 4 times
7186 if (data->SearchBuffer[0] != 0)
7187 {
7188 bool clear_buffer = false;
7189 clear_buffer |= (g.NavFocusScopeId != data->FocusScope);
7190 clear_buffer |= (data->LastRequestTime + TYPING_SELECT_RESET_TIMER < g.Time);
7191 clear_buffer |= g.NavAnyRequest;
7192 clear_buffer |= g.ActiveId != 0 && g.NavActivateId == 0; // Allow temporary SPACE activation to not interfere
7193 clear_buffer |= IsKeyPressed(key: ImGuiKey_Escape) || IsKeyPressed(key: ImGuiKey_Enter);
7194 clear_buffer |= IsKeyPressed(key: ImGuiKey_Backspace) && (flags & ImGuiTypingSelectFlags_AllowBackspace) == 0;
7195 //if (clear_buffer) { IMGUI_DEBUG_LOG("GetTypingSelectRequest(): Clear SearchBuffer.\n"); }
7196 if (clear_buffer)
7197 data->Clear();
7198 }
7199
7200 // Append to buffer
7201 const int buffer_max_len = IM_ARRAYSIZE(data->SearchBuffer) - 1;
7202 int buffer_len = (int)ImStrlen(s: data->SearchBuffer);
7203 bool select_request = false;
7204 for (ImWchar w : g.IO.InputQueueCharacters)
7205 {
7206 const int w_len = ImTextCountUtf8BytesFromStr(in_text: &w, in_text_end: &w + 1);
7207 if (w < 32 || (buffer_len == 0 && ImCharIsBlankW(c: w)) || (buffer_len + w_len > buffer_max_len)) // Ignore leading blanks
7208 continue;
7209 char w_buf[5];
7210 ImTextCharToUtf8(out_buf: w_buf, c: (unsigned int)w);
7211 if (data->SingleCharModeLock && w_len == out_request->SingleCharSize && memcmp(s1: w_buf, s2: data->SearchBuffer, n: w_len) == 0)
7212 {
7213 select_request = true; // Same character: don't need to append to buffer.
7214 continue;
7215 }
7216 if (data->SingleCharModeLock)
7217 {
7218 data->Clear(); // Different character: clear
7219 buffer_len = 0;
7220 }
7221 memcpy(dest: data->SearchBuffer + buffer_len, src: w_buf, n: w_len + 1); // Append
7222 buffer_len += w_len;
7223 select_request = true;
7224 }
7225 g.IO.InputQueueCharacters.resize(new_size: 0);
7226
7227 // Handle backspace
7228 if ((flags & ImGuiTypingSelectFlags_AllowBackspace) && IsKeyPressed(key: ImGuiKey_Backspace, flags: ImGuiInputFlags_Repeat))
7229 {
7230 char* p = (char*)(void*)ImTextFindPreviousUtf8Codepoint(in_text_start: data->SearchBuffer, in_text_curr: data->SearchBuffer + buffer_len);
7231 *p = 0;
7232 buffer_len = (int)(p - data->SearchBuffer);
7233 }
7234
7235 // Return request if any
7236 if (buffer_len == 0)
7237 return NULL;
7238 if (select_request)
7239 {
7240 data->FocusScope = g.NavFocusScopeId;
7241 data->LastRequestFrame = g.FrameCount;
7242 data->LastRequestTime = (float)g.Time;
7243 }
7244 out_request->Flags = flags;
7245 out_request->SearchBufferLen = buffer_len;
7246 out_request->SearchBuffer = data->SearchBuffer;
7247 out_request->SelectRequest = (data->LastRequestFrame == g.FrameCount);
7248 out_request->SingleCharMode = false;
7249 out_request->SingleCharSize = 0;
7250
7251 // Calculate if buffer contains the same character repeated.
7252 // - This can be used to implement a special search mode on first character.
7253 // - Performed on UTF-8 codepoint for correctness.
7254 // - SingleCharMode is always set for first input character, because it usually leads to a "next".
7255 if (flags & ImGuiTypingSelectFlags_AllowSingleCharMode)
7256 {
7257 const char* buf_begin = out_request->SearchBuffer;
7258 const char* buf_end = out_request->SearchBuffer + out_request->SearchBufferLen;
7259 const int c0_len = ImTextCountUtf8BytesFromChar(in_text: buf_begin, in_text_end: buf_end);
7260 const char* p = buf_begin + c0_len;
7261 for (; p < buf_end; p += c0_len)
7262 if (memcmp(s1: buf_begin, s2: p, n: (size_t)c0_len) != 0)
7263 break;
7264 const int single_char_count = (p == buf_end) ? (out_request->SearchBufferLen / c0_len) : 0;
7265 out_request->SingleCharMode = (single_char_count > 0 || data->SingleCharModeLock);
7266 out_request->SingleCharSize = (ImS8)c0_len;
7267 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.
7268 }
7269
7270 return out_request;
7271}
7272
7273static int ImStrimatchlen(const char* s1, const char* s1_end, const char* s2)
7274{
7275 int match_len = 0;
7276 while (s1 < s1_end && ImToUpper(c: *s1++) == ImToUpper(c: *s2++))
7277 match_len++;
7278 return match_len;
7279}
7280
7281// Default handler for finding a result for typing-select. You may implement your own.
7282// You might want to display a tooltip to visualize the current request SearchBuffer
7283// When SingleCharMode is set:
7284// - it is better to NOT display a tooltip of other on-screen display indicator.
7285// - the index of the currently focused item is required.
7286// if your SetNextItemSelectionUserData() values are indices, you can obtain it from ImGuiMultiSelectIO::NavIdItem, otherwise from g.NavLastValidSelectionUserData.
7287int ImGui::TypingSelectFindMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx)
7288{
7289 if (req == NULL || req->SelectRequest == false) // Support NULL parameter so both calls can be done from same spot.
7290 return -1;
7291 int idx = -1;
7292 if (req->SingleCharMode && (req->Flags & ImGuiTypingSelectFlags_AllowSingleCharMode))
7293 idx = TypingSelectFindNextSingleCharMatch(req, items_count, get_item_name_func, user_data, nav_item_idx);
7294 else
7295 idx = TypingSelectFindBestLeadingMatch(req, items_count, get_item_name_func, user_data);
7296 if (idx != -1)
7297 SetNavCursorVisibleAfterMove();
7298 return idx;
7299}
7300
7301// Special handling when a single character is repeated: perform search on a single letter and goes to next.
7302int ImGui::TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx)
7303{
7304 // FIXME: Assume selection user data is index. Would be extremely practical.
7305 //if (nav_item_idx == -1)
7306 // nav_item_idx = (int)g.NavLastValidSelectionUserData;
7307
7308 int first_match_idx = -1;
7309 bool return_next_match = false;
7310 for (int idx = 0; idx < items_count; idx++)
7311 {
7312 const char* item_name = get_item_name_func(user_data, idx);
7313 if (ImStrimatchlen(s1: req->SearchBuffer, s1_end: req->SearchBuffer + req->SingleCharSize, s2: item_name) < req->SingleCharSize)
7314 continue;
7315 if (return_next_match) // Return next matching item after current item.
7316 return idx;
7317 if (first_match_idx == -1 && nav_item_idx == -1) // Return first match immediately if we don't have a nav_item_idx value.
7318 return idx;
7319 if (first_match_idx == -1) // Record first match for wrapping.
7320 first_match_idx = idx;
7321 if (nav_item_idx == idx) // Record that we encountering nav_item so we can return next match.
7322 return_next_match = true;
7323 }
7324 return first_match_idx; // First result
7325}
7326
7327int ImGui::TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data)
7328{
7329 int longest_match_idx = -1;
7330 int longest_match_len = 0;
7331 for (int idx = 0; idx < items_count; idx++)
7332 {
7333 const char* item_name = get_item_name_func(user_data, idx);
7334 const int match_len = ImStrimatchlen(s1: req->SearchBuffer, s1_end: req->SearchBuffer + req->SearchBufferLen, s2: item_name);
7335 if (match_len <= longest_match_len)
7336 continue;
7337 longest_match_idx = idx;
7338 longest_match_len = match_len;
7339 if (match_len == req->SearchBufferLen)
7340 break;
7341 }
7342 return longest_match_idx;
7343}
7344
7345void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data)
7346{
7347#ifndef IMGUI_DISABLE_DEBUG_TOOLS
7348 Text(fmt: "SearchBuffer = \"%s\"", data->SearchBuffer);
7349 Text(fmt: "SingleCharMode = %d, Size = %d, Lock = %d", data->Request.SingleCharMode, data->Request.SingleCharSize, data->SingleCharModeLock);
7350 Text(fmt: "LastRequest = time: %.2f, frame: %d", data->LastRequestTime, data->LastRequestFrame);
7351#else
7352 IM_UNUSED(data);
7353#endif
7354}
7355
7356//-------------------------------------------------------------------------
7357// [SECTION] Widgets: Box-Select support
7358// This has been extracted away from Multi-Select logic in the hope that it could eventually be used elsewhere, but hasn't been yet.
7359//-------------------------------------------------------------------------
7360// Extra logic in MultiSelectItemFooter() and ImGuiListClipper::Step()
7361//-------------------------------------------------------------------------
7362// - BoxSelectPreStartDrag() [Internal]
7363// - BoxSelectActivateDrag() [Internal]
7364// - BoxSelectDeactivateDrag() [Internal]
7365// - BoxSelectScrollWithMouseDrag() [Internal]
7366// - BeginBoxSelect() [Internal]
7367// - EndBoxSelect() [Internal]
7368//-------------------------------------------------------------------------
7369
7370// Call on the initial click.
7371static void BoxSelectPreStartDrag(ImGuiID id, ImGuiSelectionUserData clicked_item)
7372{
7373 ImGuiContext& g = *GImGui;
7374 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7375 bs->ID = id;
7376 bs->IsStarting = true; // Consider starting box-select.
7377 bs->IsStartedFromVoid = (clicked_item == ImGuiSelectionUserData_Invalid);
7378 bs->IsStartedSetNavIdOnce = bs->IsStartedFromVoid;
7379 bs->KeyMods = g.IO.KeyMods;
7380 bs->StartPosRel = bs->EndPosRel = ImGui::WindowPosAbsToRel(window: g.CurrentWindow, p: g.IO.MousePos);
7381 bs->ScrollAccum = ImVec2(0.0f, 0.0f);
7382}
7383
7384static void BoxSelectActivateDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window)
7385{
7386 ImGuiContext& g = *GImGui;
7387 IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Activate\n", bs->ID);
7388 bs->IsActive = true;
7389 bs->Window = window;
7390 bs->IsStarting = false;
7391 ImGui::SetActiveID(id: bs->ID, window);
7392 ImGui::SetActiveIdUsingAllKeyboardKeys();
7393 if (bs->IsStartedFromVoid && (bs->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0)
7394 bs->RequestClear = true;
7395}
7396
7397static void BoxSelectDeactivateDrag(ImGuiBoxSelectState* bs)
7398{
7399 ImGuiContext& g = *GImGui;
7400 bs->IsActive = bs->IsStarting = false;
7401 if (g.ActiveId == bs->ID)
7402 {
7403 IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Deactivate\n", bs->ID);
7404 ImGui::ClearActiveID();
7405 }
7406 bs->ID = 0;
7407}
7408
7409static void BoxSelectScrollWithMouseDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window, const ImRect& inner_r)
7410{
7411 ImGuiContext& g = *GImGui;
7412 IM_ASSERT(bs->Window == window);
7413 for (int n = 0; n < 2; n++) // each axis
7414 {
7415 const float mouse_pos = g.IO.MousePos[n];
7416 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;
7417 const float scroll_curr = window->Scroll[n];
7418 if (dist == 0.0f || (dist < 0.0f && scroll_curr < 0.0f) || (dist > 0.0f && scroll_curr >= window->ScrollMax[n]))
7419 continue;
7420
7421 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
7422 const float scroll_step = g.FontSize * 35.0f * speed_multiplier * ImSign(x: dist) * g.IO.DeltaTime;
7423 bs->ScrollAccum[n] += scroll_step;
7424
7425 // Accumulate into a stored value so we can handle high-framerate
7426 const float scroll_step_i = ImFloor(f: bs->ScrollAccum[n]);
7427 if (scroll_step_i == 0.0f)
7428 continue;
7429 if (n == 0)
7430 ImGui::SetScrollX(window, scroll_x: scroll_curr + scroll_step_i);
7431 else
7432 ImGui::SetScrollY(window, scroll_y: scroll_curr + scroll_step_i);
7433 bs->ScrollAccum[n] -= scroll_step_i;
7434 }
7435}
7436
7437bool ImGui::BeginBoxSelect(const ImRect& scope_rect, ImGuiWindow* window, ImGuiID box_select_id, ImGuiMultiSelectFlags ms_flags)
7438{
7439 ImGuiContext& g = *GImGui;
7440 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7441 KeepAliveID(id: box_select_id);
7442 if (bs->ID != box_select_id)
7443 return false;
7444
7445 // IsStarting is set by MultiSelectItemFooter() when considering a possible box-select. We validate it here and lock geometry.
7446 bs->UnclipMode = false;
7447 bs->RequestClear = false;
7448 if (bs->IsStarting && IsMouseDragPastThreshold(button: 0))
7449 BoxSelectActivateDrag(bs, window);
7450 else if ((bs->IsStarting || bs->IsActive) && g.IO.MouseDown[0] == false)
7451 BoxSelectDeactivateDrag(bs);
7452 if (!bs->IsActive)
7453 return false;
7454
7455 // Current frame absolute prev/current rectangles are used to toggle selection.
7456 // They are derived from positions relative to scrolling space.
7457 ImVec2 start_pos_abs = WindowPosRelToAbs(window, p: bs->StartPosRel);
7458 ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, p: bs->EndPosRel); // Clamped already
7459 ImVec2 curr_end_pos_abs = g.IO.MousePos;
7460 if (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) // Box-select scrolling only happens with ScopeWindow
7461 curr_end_pos_abs = ImClamp(v: curr_end_pos_abs, mn: scope_rect.Min, mx: scope_rect.Max);
7462 bs->BoxSelectRectPrev.Min = ImMin(lhs: start_pos_abs, rhs: prev_end_pos_abs);
7463 bs->BoxSelectRectPrev.Max = ImMax(lhs: start_pos_abs, rhs: prev_end_pos_abs);
7464 bs->BoxSelectRectCurr.Min = ImMin(lhs: start_pos_abs, rhs: curr_end_pos_abs);
7465 bs->BoxSelectRectCurr.Max = ImMax(lhs: start_pos_abs, rhs: curr_end_pos_abs);
7466
7467 // Box-select 2D mode detects horizontal changes (vertical ones are already picked by Clipper)
7468 // Storing an extra rect used by widgets supporting box-select.
7469 if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d)
7470 if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x)
7471 {
7472 bs->UnclipMode = true;
7473 bs->UnclipRect = bs->BoxSelectRectPrev; // FIXME-OPT: UnclipRect x coordinates could be intersection of Prev and Curr rect on X axis.
7474 bs->UnclipRect.Add(r: bs->BoxSelectRectCurr);
7475 }
7476
7477 //GetForegroundDrawList()->AddRect(bs->UnclipRect.Min, bs->UnclipRect.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f);
7478 //GetForegroundDrawList()->AddRect(bs->BoxSelectRectPrev.Min, bs->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f);
7479 //GetForegroundDrawList()->AddRect(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f);
7480 return true;
7481}
7482
7483void ImGui::EndBoxSelect(const ImRect& scope_rect, ImGuiMultiSelectFlags ms_flags)
7484{
7485 ImGuiContext& g = *GImGui;
7486 ImGuiWindow* window = g.CurrentWindow;
7487 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7488 IM_ASSERT(bs->IsActive);
7489 bs->UnclipMode = false;
7490
7491 // Render selection rectangle
7492 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
7493 ImRect box_select_r = bs->BoxSelectRectCurr;
7494 box_select_r.ClipWith(r: scope_rect);
7495 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
7496 window->DrawList->AddRect(p_min: box_select_r.Min, p_max: box_select_r.Max, col: GetColorU32(idx: ImGuiCol_NavCursor)); // FIXME-MULTISELECT: Styling
7497
7498 // Scroll
7499 const bool enable_scroll = (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms_flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0;
7500 if (enable_scroll)
7501 {
7502 ImRect scroll_r = scope_rect;
7503 scroll_r.Expand(amount: -g.FontSize);
7504 //GetForegroundDrawList()->AddRect(scroll_r.Min, scroll_r.Max, IM_COL32(0, 255, 0, 255));
7505 if (!scroll_r.Contains(p: g.IO.MousePos))
7506 BoxSelectScrollWithMouseDrag(bs, window, inner_r: scroll_r);
7507 }
7508}
7509
7510//-------------------------------------------------------------------------
7511// [SECTION] Widgets: Multi-Select support
7512//-------------------------------------------------------------------------
7513// - DebugLogMultiSelectRequests() [Internal]
7514// - CalcScopeRect() [Internal]
7515// - BeginMultiSelect()
7516// - EndMultiSelect()
7517// - SetNextItemSelectionUserData()
7518// - MultiSelectItemHeader() [Internal]
7519// - MultiSelectItemFooter() [Internal]
7520// - DebugNodeMultiSelectState() [Internal]
7521//-------------------------------------------------------------------------
7522
7523static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* io)
7524{
7525 ImGuiContext& g = *GImGui;
7526 IM_UNUSED(function);
7527 for (const ImGuiSelectionRequest& req : io->Requests)
7528 {
7529 if (req.Type == ImGuiSelectionRequestType_SetAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetAll %d (= %s)\n", function, req.Selected, req.Selected ? "SelectAll" : "Clear");
7530 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);
7531 }
7532}
7533
7534static ImRect CalcScopeRect(ImGuiMultiSelectTempData* ms, ImGuiWindow* window)
7535{
7536 ImGuiContext& g = *GImGui;
7537 if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)
7538 {
7539 // Warning: this depends on CursorMaxPos so it means to be called by EndMultiSelect() only
7540 return ImRect(ms->ScopeRectMin, ImMax(lhs: window->DC.CursorMaxPos, rhs: ms->ScopeRectMin));
7541 }
7542 else
7543 {
7544 // When a table, pull HostClipRect, which allows us to predict ClipRect before first row/layout is performed. (#7970)
7545 ImRect scope_rect = window->InnerClipRect;
7546 if (g.CurrentTable != NULL)
7547 scope_rect = g.CurrentTable->HostClipRect;
7548
7549 // Add inner table decoration (#7821) // FIXME: Why not baking in InnerClipRect?
7550 scope_rect.Min = ImMin(lhs: scope_rect.Min + ImVec2(window->DecoInnerSizeX1, window->DecoInnerSizeY1), rhs: scope_rect.Max);
7551 return scope_rect;
7552 }
7553}
7554
7555// Return ImGuiMultiSelectIO structure.
7556// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect().
7557// Passing 'selection_size' and 'items_count' parameters is currently optional.
7558// - 'selection_size' is useful to disable some shortcut routing: e.g. ImGuiMultiSelectFlags_ClearOnEscape won't claim Escape key when selection_size 0,
7559// allowing a first press to clear selection THEN the second press to leave child window and return to parent.
7560// - '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).
7561// - If they are costly for you to compute (e.g. external intrusive selection without maintaining size), you may avoid them and pass -1.
7562// - 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.
7563ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int selection_size, int items_count)
7564{
7565 ImGuiContext& g = *GImGui;
7566 ImGuiWindow* window = g.CurrentWindow;
7567
7568 if (++g.MultiSelectTempDataStacked > g.MultiSelectTempData.Size)
7569 g.MultiSelectTempData.resize(new_size: g.MultiSelectTempDataStacked, v: ImGuiMultiSelectTempData());
7570 ImGuiMultiSelectTempData* ms = &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1];
7571 IM_STATIC_ASSERT(offsetof(ImGuiMultiSelectTempData, IO) == 0); // Clear() relies on that.
7572 g.CurrentMultiSelect = ms;
7573 if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0)
7574 flags |= ImGuiMultiSelectFlags_ScopeWindow;
7575 if (flags & ImGuiMultiSelectFlags_SingleSelect)
7576 flags &= ~(ImGuiMultiSelectFlags_BoxSelect2d | ImGuiMultiSelectFlags_BoxSelect1d);
7577 if (flags & ImGuiMultiSelectFlags_BoxSelect2d)
7578 flags &= ~ImGuiMultiSelectFlags_BoxSelect1d;
7579
7580 // FIXME: Workaround to the fact we override CursorMaxPos, meaning size measurement are lost. (#8250)
7581 // They should perhaps be stacked properly?
7582 if (ImGuiTable* table = g.CurrentTable)
7583 if (table->CurrentColumn != -1)
7584 TableEndCell(table); // This is currently safe to call multiple time. If that properly is lost we can extract the "save measurement" part of it.
7585
7586 // FIXME: BeginFocusScope()
7587 const ImGuiID id = window->IDStack.back();
7588 ms->Clear();
7589 ms->FocusScopeId = id;
7590 ms->Flags = flags;
7591 ms->IsFocused = (ms->FocusScopeId == g.NavFocusScopeId);
7592 ms->BackupCursorMaxPos = window->DC.CursorMaxPos;
7593 ms->ScopeRectMin = window->DC.CursorMaxPos = window->DC.CursorPos;
7594 PushFocusScope(id: ms->FocusScopeId);
7595 if (flags & ImGuiMultiSelectFlags_ScopeWindow) // Mark parent child window as navigable into, with highlight. Assume user will always submit interactive items.
7596 window->DC.NavLayersActiveMask |= 1 << ImGuiNavLayer_Main;
7597
7598 // Use copy of keyboard mods at the time of the request, otherwise we would requires mods to be held for an extra frame.
7599 ms->KeyMods = g.NavJustMovedToId ? (g.NavJustMovedToIsTabbing ? 0 : g.NavJustMovedToKeyMods) : g.IO.KeyMods;
7600 if (flags & ImGuiMultiSelectFlags_NoRangeSelect)
7601 ms->KeyMods &= ~ImGuiMod_Shift;
7602
7603 // Bind storage
7604 ImGuiMultiSelectState* storage = g.MultiSelectStorage.GetOrAddByKey(key: id);
7605 storage->ID = id;
7606 storage->LastFrameActive = g.FrameCount;
7607 storage->LastSelectionSize = selection_size;
7608 storage->Window = window;
7609 ms->Storage = storage;
7610
7611 // Output to user
7612 ms->IO.Requests.resize(new_size: 0);
7613 ms->IO.RangeSrcItem = storage->RangeSrcItem;
7614 ms->IO.NavIdItem = storage->NavIdItem;
7615 ms->IO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false;
7616 ms->IO.ItemsCount = items_count;
7617
7618 // Clear when using Navigation to move within the scope
7619 // (we compare FocusScopeId so it possible to use multiple selections inside a same window)
7620 bool request_clear = false;
7621 bool request_select_all = false;
7622 if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData)
7623 {
7624 if (ms->KeyMods & ImGuiMod_Shift)
7625 ms->IsKeyboardSetRange = true;
7626 if (ms->IsKeyboardSetRange)
7627 IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid); // Not ready -> could clear?
7628 if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0)
7629 request_clear = true;
7630 }
7631 else if (g.NavJustMovedFromFocusScopeId == ms->FocusScopeId)
7632 {
7633 // Also clear on leaving scope (may be optional?)
7634 if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0)
7635 request_clear = true;
7636 }
7637
7638 // Box-select handling: update active state.
7639 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7640 if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7641 {
7642 ms->BoxSelectId = GetID(str_id: "##BoxSelect");
7643 if (BeginBoxSelect(scope_rect: CalcScopeRect(ms, window), window, box_select_id: ms->BoxSelectId, ms_flags: flags))
7644 request_clear |= bs->RequestClear;
7645 }
7646
7647 if (ms->IsFocused)
7648 {
7649 // Shortcut: Clear selection (Escape)
7650 // - Only claim shortcut if selection is not empty, allowing further presses on Escape to e.g. leave current child window.
7651 // - Box select also handle Escape and needs to pass an id to bypass ActiveIdUsingAllKeyboardKeys lock.
7652 if (flags & ImGuiMultiSelectFlags_ClearOnEscape)
7653 {
7654 if (selection_size != 0 || bs->IsActive)
7655 if (Shortcut(key_chord: ImGuiKey_Escape, flags: ImGuiInputFlags_None, owner_id: bs->IsActive ? bs->ID : 0))
7656 {
7657 request_clear = true;
7658 if (bs->IsActive)
7659 BoxSelectDeactivateDrag(bs);
7660 }
7661 }
7662
7663 // Shortcut: Select all (CTRL+A)
7664 if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll))
7665 if (Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiKey_A))
7666 request_select_all = true;
7667 }
7668
7669 if (request_clear || request_select_all)
7670 {
7671 MultiSelectAddSetAll(ms, selected: request_select_all);
7672 if (!request_select_all)
7673 storage->LastSelectionSize = 0;
7674 }
7675 ms->LoopRequestSetAll = request_select_all ? 1 : request_clear ? 0 : -1;
7676 ms->LastSubmittedItem = ImGuiSelectionUserData_Invalid;
7677
7678 if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)
7679 DebugLogMultiSelectRequests(function: "BeginMultiSelect", io: &ms->IO);
7680
7681 return &ms->IO;
7682}
7683
7684// Return updated ImGuiMultiSelectIO structure.
7685// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect().
7686ImGuiMultiSelectIO* ImGui::EndMultiSelect()
7687{
7688 ImGuiContext& g = *GImGui;
7689 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
7690 ImGuiMultiSelectState* storage = ms->Storage;
7691 ImGuiWindow* window = g.CurrentWindow;
7692 IM_ASSERT_USER_ERROR(ms->FocusScopeId == g.CurrentFocusScopeId, "EndMultiSelect() FocusScope mismatch!");
7693 IM_ASSERT(g.CurrentMultiSelect != NULL && storage->Window == g.CurrentWindow);
7694 IM_ASSERT(g.MultiSelectTempDataStacked > 0 && &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] == g.CurrentMultiSelect);
7695
7696 ImRect scope_rect = CalcScopeRect(ms, window);
7697 if (ms->IsFocused)
7698 {
7699 // We currently don't allow user code to modify RangeSrcItem by writing to BeginIO's version, but that would be an easy change here.
7700 if (ms->IO.RangeSrcReset || (ms->RangeSrcPassedBy == false && ms->IO.RangeSrcItem != ImGuiSelectionUserData_Invalid)) // Can't read storage->RangeSrcItem here -> we want the state at begining of the scope (see tests for easy failure)
7701 {
7702 IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset RangeSrcItem.\n"); // Will set be to NavId.
7703 storage->RangeSrcItem = ImGuiSelectionUserData_Invalid;
7704 }
7705 if (ms->NavIdPassedBy == false && storage->NavIdItem != ImGuiSelectionUserData_Invalid)
7706 {
7707 IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset NavIdItem.\n");
7708 storage->NavIdItem = ImGuiSelectionUserData_Invalid;
7709 storage->NavIdSelected = -1;
7710 }
7711
7712 if ((ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) && GetBoxSelectState(id: ms->BoxSelectId))
7713 EndBoxSelect(scope_rect, ms_flags: ms->Flags);
7714 }
7715
7716 if (ms->IsEndIO == false)
7717 ms->IO.Requests.resize(new_size: 0);
7718
7719 // Clear selection when clicking void?
7720 // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection!
7721 // The InnerRect test is necessary for non-child/decorated windows.
7722 bool scope_hovered = IsWindowHovered() && window->InnerRect.Contains(p: g.IO.MousePos);
7723 if (scope_hovered && (ms->Flags & ImGuiMultiSelectFlags_ScopeRect))
7724 scope_hovered &= scope_rect.Contains(p: g.IO.MousePos);
7725 if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0)
7726 {
7727 if (ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7728 {
7729 if (!g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && g.IO.MouseClickedCount[0] == 1)
7730 {
7731 BoxSelectPreStartDrag(id: ms->BoxSelectId, ImGuiSelectionUserData_Invalid);
7732 FocusWindow(window, flags: ImGuiFocusRequestFlags_UnlessBelowModal);
7733 SetHoveredID(ms->BoxSelectId);
7734 if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)
7735 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.
7736 }
7737 }
7738
7739 if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid)
7740 if (IsMouseReleased(button: 0) && IsMouseDragPastThreshold(button: 0) == false && g.IO.KeyMods == ImGuiMod_None)
7741 MultiSelectAddSetAll(ms, selected: false);
7742 }
7743
7744 // Courtesy nav wrapping helper flag
7745 if (ms->Flags & ImGuiMultiSelectFlags_NavWrapX)
7746 {
7747 IM_ASSERT(ms->Flags & ImGuiMultiSelectFlags_ScopeWindow); // Only supported at window scope
7748 ImGui::NavMoveRequestTryWrapping(window: ImGui::GetCurrentWindow(), move_flags: ImGuiNavMoveFlags_WrapX);
7749 }
7750
7751 // Unwind
7752 window->DC.CursorMaxPos = ImMax(lhs: ms->BackupCursorMaxPos, rhs: window->DC.CursorMaxPos);
7753 PopFocusScope();
7754
7755 if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)
7756 DebugLogMultiSelectRequests(function: "EndMultiSelect", io: &ms->IO);
7757
7758 ms->FocusScopeId = 0;
7759 ms->Flags = ImGuiMultiSelectFlags_None;
7760 g.CurrentMultiSelect = (--g.MultiSelectTempDataStacked > 0) ? &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] : NULL;
7761
7762 return &ms->IO;
7763}
7764
7765void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data)
7766{
7767 // Note that flags will be cleared by ItemAdd(), so it's only useful for Navigation code!
7768 // This designed so widgets can also cheaply set this before calling ItemAdd(), so we are not tied to MultiSelect api.
7769 ImGuiContext& g = *GImGui;
7770 g.NextItemData.SelectionUserData = selection_user_data;
7771 g.NextItemData.FocusScopeId = g.CurrentFocusScopeId;
7772
7773 if (ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect)
7774 {
7775 // Auto updating RangeSrcPassedBy for cases were clipper is not used (done before ItemAdd() clipping)
7776 g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData | ImGuiItemFlags_IsMultiSelect;
7777 if (ms->IO.RangeSrcItem == selection_user_data)
7778 ms->RangeSrcPassedBy = true;
7779 }
7780 else
7781 {
7782 g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData;
7783 }
7784}
7785
7786// In charge of:
7787// - Applying SetAll for submitted items.
7788// - Applying SetRange for submitted items and record end points.
7789// - Altering button behavior flags to facilitate use with drag and drop.
7790void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags)
7791{
7792 ImGuiContext& g = *GImGui;
7793 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
7794
7795 bool selected = *p_selected;
7796 if (ms->IsFocused)
7797 {
7798 ImGuiMultiSelectState* storage = ms->Storage;
7799 ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData;
7800 IM_ASSERT(g.NextItemData.FocusScopeId == g.CurrentFocusScopeId && "Forgot to call SetNextItemSelectionUserData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope");
7801
7802 // Apply SetAll (Clear/SelectAll) requests requested by BeginMultiSelect().
7803 // This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper.
7804 // If you are using a clipper you need to process the SetAll request after calling BeginMultiSelect()
7805 if (ms->LoopRequestSetAll != -1)
7806 selected = (ms->LoopRequestSetAll == 1);
7807
7808 // 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)
7809 // For this to work, we need someone to set 'RangeSrcPassedBy = true' at some point (either clipper either SetNextItemSelectionUserData() function)
7810 if (ms->IsKeyboardSetRange)
7811 {
7812 IM_ASSERT(id != 0 && (ms->KeyMods & ImGuiMod_Shift) != 0);
7813 const bool is_range_dst = (ms->RangeDstPassedBy == false) && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped.
7814 if (is_range_dst)
7815 ms->RangeDstPassedBy = true;
7816 if (is_range_dst && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) // If we don't have RangeSrc, assign RangeSrc = RangeDst
7817 {
7818 storage->RangeSrcItem = item_data;
7819 storage->RangeSelected = selected ? 1 : 0;
7820 }
7821 const bool is_range_src = storage->RangeSrcItem == item_data;
7822 if (is_range_src || is_range_dst || ms->RangeSrcPassedBy != ms->RangeDstPassedBy)
7823 {
7824 // Apply range-select value to visible items
7825 IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid && storage->RangeSelected != -1);
7826 selected = (storage->RangeSelected != 0);
7827 }
7828 else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0 && (ms->Flags & ImGuiMultiSelectFlags_NoAutoClear) == 0)
7829 {
7830 // Clear other items
7831 selected = false;
7832 }
7833 }
7834 *p_selected = selected;
7835 }
7836
7837 // Alter button behavior flags
7838 // To handle drag and drop of multiple items we need to avoid clearing selection on click.
7839 // 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.
7840 if (p_button_flags != NULL)
7841 {
7842 ImGuiButtonFlags button_flags = *p_button_flags;
7843 button_flags |= ImGuiButtonFlags_NoHoveredOnFocus;
7844 if ((!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) && !(ms->Flags & ImGuiMultiSelectFlags_SelectOnClickRelease))
7845 button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease;
7846 else
7847 button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
7848 *p_button_flags = button_flags;
7849 }
7850}
7851
7852// In charge of:
7853// - Auto-select on navigation.
7854// - Box-select toggle handling.
7855// - Right-click handling.
7856// - Altering selection based on Ctrl/Shift modifiers, both for keyboard and mouse.
7857// - Record current selection state for RangeSrc
7858// This is all rather complex, best to run and refer to "widgets_multiselect_xxx" tests in imgui_test_suite.
7859void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed)
7860{
7861 ImGuiContext& g = *GImGui;
7862 ImGuiWindow* window = g.CurrentWindow;
7863
7864 bool selected = *p_selected;
7865 bool pressed = *p_pressed;
7866 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
7867 ImGuiMultiSelectState* storage = ms->Storage;
7868 if (pressed)
7869 ms->IsFocused = true;
7870
7871 bool hovered = false;
7872 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect)
7873 hovered = IsItemHovered(flags: ImGuiHoveredFlags_AllowWhenBlockedByPopup);
7874 if (!ms->IsFocused && !hovered)
7875 return;
7876
7877 ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData;
7878
7879 ImGuiMultiSelectFlags flags = ms->Flags;
7880 const bool is_singleselect = (flags & ImGuiMultiSelectFlags_SingleSelect) != 0;
7881 bool is_ctrl = (ms->KeyMods & ImGuiMod_Ctrl) != 0;
7882 bool is_shift = (ms->KeyMods & ImGuiMod_Shift) != 0;
7883
7884 bool apply_to_range_src = false;
7885
7886 if (g.NavId == id && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid)
7887 apply_to_range_src = true;
7888 if (ms->IsEndIO == false)
7889 {
7890 ms->IO.Requests.resize(new_size: 0);
7891 ms->IsEndIO = true;
7892 }
7893
7894 // Auto-select as you navigate a list
7895 if (g.NavJustMovedToId == id)
7896 {
7897 if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
7898 {
7899 if (is_ctrl && is_shift)
7900 pressed = true;
7901 else if (!is_ctrl)
7902 selected = pressed = true;
7903 }
7904 else
7905 {
7906 // With NoAutoSelect, using Shift+keyboard performs a write/copy
7907 if (is_shift)
7908 pressed = true;
7909 else if (!is_ctrl)
7910 apply_to_range_src = true; // Since if (pressed) {} main block is not running we update this
7911 }
7912 }
7913
7914 if (apply_to_range_src)
7915 {
7916 storage->RangeSrcItem = item_data;
7917 storage->RangeSelected = selected; // Will be updated at the end of this function anyway.
7918 }
7919
7920 // Box-select toggle handling
7921 if (ms->BoxSelectId != 0)
7922 if (ImGuiBoxSelectState* bs = GetBoxSelectState(id: ms->BoxSelectId))
7923 {
7924 const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(r: g.LastItemData.Rect);
7925 const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(r: g.LastItemData.Rect);
7926 if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr))
7927 {
7928 if (storage->LastSelectionSize <= 0 && bs->IsStartedSetNavIdOnce)
7929 {
7930 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)
7931 bs->IsStartedSetNavIdOnce = false;
7932 }
7933 else
7934 {
7935 selected = !selected;
7936 MultiSelectAddSetRange(ms, selected, range_dir: +1, first_item: item_data, last_item: item_data);
7937 }
7938 storage->LastSelectionSize = ImMax(lhs: storage->LastSelectionSize + 1, rhs: 1);
7939 }
7940 }
7941
7942 // Right-click handling.
7943 // FIXME-MULTISELECT: Currently filtered out by ImGuiMultiSelectFlags_NoAutoSelect but maybe should be moved to Selectable(). See https://github.com/ocornut/imgui/pull/5816
7944 if (hovered && IsMouseClicked(button: 1) && (flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
7945 {
7946 if (g.ActiveId != 0 && g.ActiveId != id)
7947 ClearActiveID();
7948 SetFocusID(id, window);
7949 if (!pressed && !selected)
7950 {
7951 pressed = true;
7952 is_ctrl = is_shift = false;
7953 }
7954 }
7955
7956 // Unlike Space, Enter doesn't alter selection (but can still return a press) unless current item is not selected.
7957 // The later, "unless current item is not select", may become optional? It seems like a better default if Enter doesn't necessarily open something
7958 // (unlike e.g. Windows explorer). For use case where Enter always open something, we might decide to make this optional?
7959 const bool enter_pressed = pressed && (g.NavActivateId == id) && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput);
7960
7961 // Alter selection
7962 if (pressed && (!enter_pressed || !selected))
7963 {
7964 // Box-select
7965 ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse;
7966 if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7967 if (selected == false && !g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1)
7968 BoxSelectPreStartDrag(id: ms->BoxSelectId, clicked_item: item_data);
7969
7970 //----------------------------------------------------------------------------------------
7971 // ACTION | Begin | Pressed/Activated | End
7972 //----------------------------------------------------------------------------------------
7973 // Keys Navigated: | Clear | Src=item, Sel=1 SetRange 1
7974 // Keys Navigated: Ctrl | n/a | n/a
7975 // Keys Navigated: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1
7976 // Keys Navigated: Ctrl+Shift | n/a | Dst=item, Sel=Src => Clear + SetRange Src-Dst
7977 // Keys Activated: | n/a | Src=item, Sel=1 => Clear + SetRange 1
7978 // Keys Activated: Ctrl | n/a | Src=item, Sel=!Sel => SetSange 1
7979 // Keys Activated: Shift | n/a | Dst=item, Sel=1 => Clear + SetSange 1
7980 //----------------------------------------------------------------------------------------
7981 // Mouse Pressed: | n/a | Src=item, Sel=1, => Clear + SetRange 1
7982 // Mouse Pressed: Ctrl | n/a | Src=item, Sel=!Sel => SetRange 1
7983 // Mouse Pressed: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1
7984 // Mouse Pressed: Ctrl+Shift | n/a | Dst=item, Sel=!Sel => SetRange Src-Dst
7985 //----------------------------------------------------------------------------------------
7986
7987 if ((flags & ImGuiMultiSelectFlags_NoAutoClear) == 0)
7988 {
7989 bool request_clear = false;
7990 if (is_singleselect)
7991 request_clear = true;
7992 else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl)
7993 request_clear = (flags & ImGuiMultiSelectFlags_NoAutoClearOnReselect) ? !selected : true;
7994 else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl)
7995 request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again.
7996 if (request_clear)
7997 MultiSelectAddSetAll(ms, selected: false);
7998 }
7999
8000 int range_direction;
8001 bool range_selected;
8002 if (is_shift && !is_singleselect)
8003 {
8004 //IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue);
8005 if (storage->RangeSrcItem == ImGuiSelectionUserData_Invalid)
8006 storage->RangeSrcItem = item_data;
8007 if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
8008 {
8009 // Shift+Arrow always select
8010 // Ctrl+Shift+Arrow copy source selection state (already stored by BeginMultiSelect() in storage->RangeSelected)
8011 range_selected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true;
8012 }
8013 else
8014 {
8015 // Shift+Arrow copy source selection state
8016 // Shift+Click always copy from target selection state
8017 if (ms->IsKeyboardSetRange)
8018 range_selected = (storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true;
8019 else
8020 range_selected = !selected;
8021 }
8022 range_direction = ms->RangeSrcPassedBy ? +1 : -1;
8023 }
8024 else
8025 {
8026 // Ctrl inverts selection, otherwise always select
8027 if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
8028 selected = is_ctrl ? !selected : true;
8029 else
8030 selected = !selected;
8031 storage->RangeSrcItem = item_data;
8032 range_selected = selected;
8033 range_direction = +1;
8034 }
8035 MultiSelectAddSetRange(ms, selected: range_selected, range_dir: range_direction, first_item: storage->RangeSrcItem, last_item: item_data);
8036 }
8037
8038 // Update/store the selection state of the Source item (used by CTRL+SHIFT, when Source is unselected we perform a range unselect)
8039 if (storage->RangeSrcItem == item_data)
8040 storage->RangeSelected = selected ? 1 : 0;
8041
8042 // Update/store the selection state of focused item
8043 if (g.NavId == id)
8044 {
8045 storage->NavIdItem = item_data;
8046 storage->NavIdSelected = selected ? 1 : 0;
8047 }
8048 if (storage->NavIdItem == item_data)
8049 ms->NavIdPassedBy = true;
8050 ms->LastSubmittedItem = item_data;
8051
8052 *p_selected = selected;
8053 *p_pressed = pressed;
8054}
8055
8056void ImGui::MultiSelectAddSetAll(ImGuiMultiSelectTempData* ms, bool selected)
8057{
8058 ImGuiSelectionRequest req = { .Type: ImGuiSelectionRequestType_SetAll, .Selected: selected, .RangeDirection: 0, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid };
8059 ms->IO.Requests.resize(new_size: 0); // Can always clear previous requests
8060 ms->IO.Requests.push_back(v: req); // Add new request
8061}
8062
8063void ImGui::MultiSelectAddSetRange(ImGuiMultiSelectTempData* ms, bool selected, int range_dir, ImGuiSelectionUserData first_item, ImGuiSelectionUserData last_item)
8064{
8065 // Merge contiguous spans into same request (unless NoRangeSelect is set which guarantees single-item ranges)
8066 if (ms->IO.Requests.Size > 0 && first_item == last_item && (ms->Flags & ImGuiMultiSelectFlags_NoRangeSelect) == 0)
8067 {
8068 ImGuiSelectionRequest* prev = &ms->IO.Requests.Data[ms->IO.Requests.Size - 1];
8069 if (prev->Type == ImGuiSelectionRequestType_SetRange && prev->RangeLastItem == ms->LastSubmittedItem && prev->Selected == selected)
8070 {
8071 prev->RangeLastItem = last_item;
8072 return;
8073 }
8074 }
8075
8076 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 };
8077 ms->IO.Requests.push_back(v: req); // Add new request
8078}
8079
8080void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage)
8081{
8082#ifndef IMGUI_DISABLE_DEBUG_TOOLS
8083 const bool is_active = (storage->LastFrameActive >= GetFrameCount() - 2); // Note that fully clipped early out scrolling tables will appear as inactive here.
8084 if (!is_active) { PushStyleColor(idx: ImGuiCol_Text, col: GetStyleColorVec4(idx: ImGuiCol_TextDisabled)); }
8085 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*");
8086 if (!is_active) { PopStyleColor(); }
8087 if (!open)
8088 return;
8089 Text(fmt: "RangeSrcItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), RangeSelected = %d", storage->RangeSrcItem, storage->RangeSrcItem, storage->RangeSelected);
8090 Text(fmt: "NavIdItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), NavIdSelected = %d", storage->NavIdItem, storage->NavIdItem, storage->NavIdSelected);
8091 Text(fmt: "LastSelectionSize = %d", storage->LastSelectionSize); // Provided by user
8092 TreePop();
8093#else
8094 IM_UNUSED(storage);
8095#endif
8096}
8097
8098//-------------------------------------------------------------------------
8099// [SECTION] Widgets: Multi-Select helpers
8100//-------------------------------------------------------------------------
8101// - ImGuiSelectionBasicStorage
8102// - ImGuiSelectionExternalStorage
8103//-------------------------------------------------------------------------
8104
8105ImGuiSelectionBasicStorage::ImGuiSelectionBasicStorage()
8106{
8107 Size = 0;
8108 PreserveOrder = false;
8109 UserData = NULL;
8110 AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; };
8111 _SelectionOrder = 1; // Always >0
8112}
8113
8114void ImGuiSelectionBasicStorage::Clear()
8115{
8116 Size = 0;
8117 _SelectionOrder = 1; // Always >0
8118 _Storage.Data.resize(new_size: 0);
8119}
8120
8121void ImGuiSelectionBasicStorage::Swap(ImGuiSelectionBasicStorage& r)
8122{
8123 ImSwap(a&: Size, b&: r.Size);
8124 ImSwap(a&: _SelectionOrder, b&: r._SelectionOrder);
8125 _Storage.Data.swap(rhs&: r._Storage.Data);
8126}
8127
8128bool ImGuiSelectionBasicStorage::Contains(ImGuiID id) const
8129{
8130 return _Storage.GetInt(key: id, default_val: 0) != 0;
8131}
8132
8133static int IMGUI_CDECL PairComparerByValueInt(const void* lhs, const void* rhs)
8134{
8135 int lhs_v = ((const ImGuiStoragePair*)lhs)->val_i;
8136 int rhs_v = ((const ImGuiStoragePair*)rhs)->val_i;
8137 return (lhs_v > rhs_v ? +1 : lhs_v < rhs_v ? -1 : 0);
8138}
8139
8140// GetNextSelectedItem() is an abstraction allowing us to change our underlying actual storage system without impacting user.
8141// (e.g. store unselected vs compact down, compact down on demand, use raw ImVector<ImGuiID> instead of ImGuiStorage...)
8142bool ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it, ImGuiID* out_id)
8143{
8144 ImGuiStoragePair* it = (ImGuiStoragePair*)*opaque_it;
8145 ImGuiStoragePair* it_end = _Storage.Data.Data + _Storage.Data.Size;
8146 if (PreserveOrder && it == NULL && it_end != NULL)
8147 ImQsort(base: _Storage.Data.Data, count: (size_t)_Storage.Data.Size, size_of_element: sizeof(ImGuiStoragePair), compare_func: PairComparerByValueInt); // ~ImGuiStorage::BuildSortByValueInt()
8148 if (it == NULL)
8149 it = _Storage.Data.Data;
8150 IM_ASSERT(it >= _Storage.Data.Data && it <= it_end);
8151 if (it != it_end)
8152 while (it->val_i == 0 && it < it_end)
8153 it++;
8154 const bool has_more = (it != it_end);
8155 *opaque_it = has_more ? (void**)(it + 1) : (void**)(it);
8156 *out_id = has_more ? it->key : 0;
8157 if (PreserveOrder && !has_more)
8158 _Storage.BuildSortByKey();
8159 return has_more;
8160}
8161
8162void ImGuiSelectionBasicStorage::SetItemSelected(ImGuiID id, bool selected)
8163{
8164 int* p_int = _Storage.GetIntRef(key: id, default_val: 0);
8165 if (selected && *p_int == 0) { *p_int = _SelectionOrder++; Size++; }
8166 else if (!selected && *p_int != 0) { *p_int = 0; Size--; }
8167}
8168
8169// Optimized for batch edits (with same value of 'selected')
8170static void ImGuiSelectionBasicStorage_BatchSetItemSelected(ImGuiSelectionBasicStorage* selection, ImGuiID id, bool selected, int size_before_amends, int selection_order)
8171{
8172 ImGuiStorage* storage = &selection->_Storage;
8173 ImGuiStoragePair* it = ImLowerBound(in_begin: storage->Data.Data, in_end: storage->Data.Data + size_before_amends, key: id);
8174 const bool is_contained = (it != storage->Data.Data + size_before_amends) && (it->key == id);
8175 if (selected == (is_contained && it->val_i != 0))
8176 return;
8177 if (selected && !is_contained)
8178 storage->Data.push_back(v: ImGuiStoragePair(id, selection_order)); // Push unsorted at end of vector, will be sorted in SelectionMultiAmendsFinish()
8179 else if (is_contained)
8180 it->val_i = selected ? selection_order : 0; // Modify in-place.
8181 selection->Size += selected ? +1 : -1;
8182}
8183
8184static void ImGuiSelectionBasicStorage_BatchFinish(ImGuiSelectionBasicStorage* selection, bool selected, int size_before_amends)
8185{
8186 ImGuiStorage* storage = &selection->_Storage;
8187 if (selected && selection->Size != size_before_amends)
8188 storage->BuildSortByKey(); // When done selecting: sort everything
8189}
8190
8191// Apply requests coming from BeginMultiSelect() and EndMultiSelect().
8192// - Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen.
8193// - Honoring SetRange requests requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem.
8194// - In this demo we often submit indices to SetNextItemSelectionUserData() + store the same indices in persistent selection.
8195// - Your code may do differently. If you store pointers or objects ID in ImGuiSelectionUserData you may need to perform
8196// a lookup in order to have some way to iterate/interpolate between two items.
8197// - A full-featured application is likely to allow search/filtering which is likely to lead to using indices
8198// and constructing a view index <> object id/ptr data structure anyway.
8199// WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THIS UNNECESSARY 'ImGuiSelectionBasicStorage' INDIRECTION LOGIC.
8200// Notice that with the simplest adapter (using indices everywhere), all functions return their parameters.
8201// The most simple implementation (using indices everywhere) would look like:
8202// for (ImGuiSelectionRequest& req : ms_io->Requests)
8203// {
8204// if (req.Type == ImGuiSelectionRequestType_SetAll) { Clear(); if (req.Selected) { for (int n = 0; n < items_count; n++) { SetItemSelected(n, true); } }
8205// if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { SetItemSelected(n, ms_io->Selected); } }
8206// }
8207void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
8208{
8209 // For convenience we obtain ItemsCount as passed to BeginMultiSelect(), which is optional.
8210 // It makes sense when using ImGuiSelectionBasicStorage to simply pass your items count to BeginMultiSelect().
8211 // Other scheme may handle SetAll differently.
8212 IM_ASSERT(ms_io->ItemsCount != -1 && "Missing value for items_count in BeginMultiSelect() call!");
8213 IM_ASSERT(AdapterIndexToStorageId != NULL);
8214
8215 // This is optimized/specialized to cope with very large selections (e.g. 100k+ items)
8216 // - A simpler version could call SetItemSelected() directly instead of ImGuiSelectionBasicStorage_BatchSetItemSelected() + ImGuiSelectionBasicStorage_BatchFinish().
8217 // - Optimized select can append unsorted, then sort in a second pass. Optimized unselect can clear in-place then compact in a second pass.
8218 // - 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.
8219 // - 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
8220 // left and right: it affects coarse clipping + can emit multiple SetRange with 1 item each.)
8221 // FIXME-OPT: For each block of consecutive SetRange request:
8222 // - add all requests to a sorted list, store ID, selected, offset in ImGuiStorage.
8223 // - rewrite sorted storage a single time.
8224 for (ImGuiSelectionRequest& req : ms_io->Requests)
8225 {
8226 if (req.Type == ImGuiSelectionRequestType_SetAll)
8227 {
8228 Clear();
8229 if (req.Selected)
8230 {
8231 _Storage.Data.reserve(new_capacity: ms_io->ItemsCount);
8232 const int size_before_amends = _Storage.Data.Size;
8233 for (int idx = 0; idx < ms_io->ItemsCount; idx++, _SelectionOrder++)
8234 ImGuiSelectionBasicStorage_BatchSetItemSelected(selection: this, id: GetStorageIdFromIndex(idx), selected: req.Selected, size_before_amends, selection_order: _SelectionOrder);
8235 ImGuiSelectionBasicStorage_BatchFinish(selection: this, selected: req.Selected, size_before_amends);
8236 }
8237 }
8238 else if (req.Type == ImGuiSelectionRequestType_SetRange)
8239 {
8240 const int selection_changes = (int)req.RangeLastItem - (int)req.RangeFirstItem + 1;
8241 //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);
8242 if (selection_changes == 1 || (selection_changes < Size / 100))
8243 {
8244 // Multiple sorted insertion + copy likely to be faster.
8245 // Technically we could do a single copy with a little more work (sort sequential SetRange requests)
8246 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++)
8247 SetItemSelected(id: GetStorageIdFromIndex(idx), selected: req.Selected);
8248 }
8249 else
8250 {
8251 // Append insertion + single sort likely be faster.
8252 // Use req.RangeDirection to set order field so that shift+clicking from 1 to 5 is different than shift+clicking from 5 to 1
8253 const int size_before_amends = _Storage.Data.Size;
8254 int selection_order = _SelectionOrder + ((req.RangeDirection < 0) ? selection_changes - 1 : 0);
8255 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++, selection_order += req.RangeDirection)
8256 ImGuiSelectionBasicStorage_BatchSetItemSelected(selection: this, id: GetStorageIdFromIndex(idx), selected: req.Selected, size_before_amends, selection_order);
8257 if (req.Selected)
8258 _SelectionOrder += selection_changes;
8259 ImGuiSelectionBasicStorage_BatchFinish(selection: this, selected: req.Selected, size_before_amends);
8260 }
8261 }
8262 }
8263}
8264
8265//-------------------------------------------------------------------------
8266
8267ImGuiSelectionExternalStorage::ImGuiSelectionExternalStorage()
8268{
8269 UserData = NULL;
8270 AdapterSetItemSelected = NULL;
8271}
8272
8273// Apply requests coming from BeginMultiSelect() and EndMultiSelect().
8274// We also pull 'ms_io->ItemsCount' as passed for BeginMultiSelect() for consistency with ImGuiSelectionBasicStorage
8275// This makes no assumption about underlying storage.
8276void ImGuiSelectionExternalStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
8277{
8278 IM_ASSERT(AdapterSetItemSelected);
8279 for (ImGuiSelectionRequest& req : ms_io->Requests)
8280 {
8281 if (req.Type == ImGuiSelectionRequestType_SetAll)
8282 for (int idx = 0; idx < ms_io->ItemsCount; idx++)
8283 AdapterSetItemSelected(this, idx, req.Selected);
8284 if (req.Type == ImGuiSelectionRequestType_SetRange)
8285 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++)
8286 AdapterSetItemSelected(this, idx, req.Selected);
8287 }
8288}
8289
8290//-------------------------------------------------------------------------
8291// [SECTION] Widgets: ListBox
8292//-------------------------------------------------------------------------
8293// - BeginListBox()
8294// - EndListBox()
8295// - ListBox()
8296//-------------------------------------------------------------------------
8297
8298// This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label.
8299// This handle some subtleties with capturing info from the label.
8300// If you don't need a label you can pretty much directly use ImGui::BeginChild() with ImGuiChildFlags_FrameStyle.
8301// Tip: To have a list filling the entire window width, use size.x = -FLT_MIN and pass an non-visible label e.g. "##empty"
8302// 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).
8303bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg)
8304{
8305 ImGuiContext& g = *GImGui;
8306 ImGuiWindow* window = GetCurrentWindow();
8307 if (window->SkipItems)
8308 return false;
8309
8310 const ImGuiStyle& style = g.Style;
8311 const ImGuiID id = GetID(str_id: label);
8312 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
8313
8314 // Size default to hold ~7.25 items.
8315 // Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar.
8316 ImVec2 size = ImTrunc(v: CalcItemSize(size: size_arg, default_w: CalcItemWidth(), default_h: GetTextLineHeightWithSpacing() * 7.25f + style.FramePadding.y * 2.0f));
8317 ImVec2 frame_size = ImVec2(size.x, ImMax(lhs: size.y, rhs: label_size.y));
8318 ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
8319 ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
8320 g.NextItemData.ClearFlags();
8321
8322 if (!IsRectVisible(rect_min: bb.Min, rect_max: bb.Max))
8323 {
8324 ItemSize(size: bb.GetSize(), text_baseline_y: style.FramePadding.y);
8325 ItemAdd(bb, id: 0, nav_bb: &frame_bb);
8326 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
8327 return false;
8328 }
8329
8330 // FIXME-OPT: We could omit the BeginGroup() if label_size.x == 0.0f but would need to omit the EndGroup() as well.
8331 BeginGroup();
8332 if (label_size.x > 0.0f)
8333 {
8334 ImVec2 label_pos = ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y);
8335 RenderText(pos: label_pos, text: label);
8336 window->DC.CursorMaxPos = ImMax(lhs: window->DC.CursorMaxPos, rhs: label_pos + label_size);
8337 AlignTextToFramePadding();
8338 }
8339
8340 BeginChild(id, size: frame_bb.GetSize(), child_flags: ImGuiChildFlags_FrameStyle);
8341 return true;
8342}
8343
8344void ImGui::EndListBox()
8345{
8346 ImGuiContext& g = *GImGui;
8347 ImGuiWindow* window = g.CurrentWindow;
8348 IM_ASSERT((window->Flags & ImGuiWindowFlags_ChildWindow) && "Mismatched BeginListBox/EndListBox calls. Did you test the return value of BeginListBox?");
8349 IM_UNUSED(window);
8350
8351 EndChild();
8352 EndGroup(); // This is only required to be able to do IsItemXXX query on the whole ListBox including label
8353}
8354
8355bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items)
8356{
8357 const bool value_changed = ListBox(label, current_item, getter: Items_ArrayGetter, user_data: (void*)items, items_count, height_in_items: height_items);
8358 return value_changed;
8359}
8360
8361// This is merely a helper around BeginListBox(), EndListBox().
8362// Considering using those directly to submit custom data or store selection differently.
8363bool 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)
8364{
8365 ImGuiContext& g = *GImGui;
8366
8367 // Calculate size from "height_in_items"
8368 if (height_in_items < 0)
8369 height_in_items = ImMin(lhs: items_count, rhs: 7);
8370 float height_in_items_f = height_in_items + 0.25f;
8371 ImVec2 size(0.0f, ImTrunc(f: GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f));
8372
8373 if (!BeginListBox(label, size_arg: size))
8374 return false;
8375
8376 // Assume all items have even height (= 1 line of text). If you need items of different height,
8377 // you can create a custom version of ListBox() in your code without using the clipper.
8378 bool value_changed = false;
8379 ImGuiListClipper clipper;
8380 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.
8381 clipper.IncludeItemByIndex(item_index: *current_item);
8382 while (clipper.Step())
8383 for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
8384 {
8385 const char* item_text = getter(user_data, i);
8386 if (item_text == NULL)
8387 item_text = "*Unknown item*";
8388
8389 PushID(int_id: i);
8390 const bool item_selected = (i == *current_item);
8391 if (Selectable(label: item_text, selected: item_selected))
8392 {
8393 *current_item = i;
8394 value_changed = true;
8395 }
8396 if (item_selected)
8397 SetItemDefaultFocus();
8398 PopID();
8399 }
8400 EndListBox();
8401
8402 if (value_changed)
8403 MarkItemEdited(id: g.LastItemData.ID);
8404
8405 return value_changed;
8406}
8407
8408//-------------------------------------------------------------------------
8409// [SECTION] Widgets: PlotLines, PlotHistogram
8410//-------------------------------------------------------------------------
8411// - PlotEx() [Internal]
8412// - PlotLines()
8413// - PlotHistogram()
8414//-------------------------------------------------------------------------
8415// Plot/Graph widgets are not very good.
8416// Consider writing your own, or using a third-party one, see:
8417// - ImPlot https://github.com/epezent/implot
8418// - others https://github.com/ocornut/imgui/wiki/Useful-Extensions
8419//-------------------------------------------------------------------------
8420
8421int 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)
8422{
8423 ImGuiContext& g = *GImGui;
8424 ImGuiWindow* window = GetCurrentWindow();
8425 if (window->SkipItems)
8426 return -1;
8427
8428 const ImGuiStyle& style = g.Style;
8429 const ImGuiID id = window->GetID(str: label);
8430
8431 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
8432 const ImVec2 frame_size = CalcItemSize(size: size_arg, default_w: CalcItemWidth(), default_h: label_size.y + style.FramePadding.y * 2.0f);
8433
8434 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
8435 const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding);
8436 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));
8437 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
8438 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: ImGuiItemFlags_NoNav))
8439 return -1;
8440 bool hovered;
8441 ButtonBehavior(bb: frame_bb, id, out_hovered: &hovered, NULL);
8442
8443 // Determine scale from values if not specified
8444 if (scale_min == FLT_MAX || scale_max == FLT_MAX)
8445 {
8446 float v_min = FLT_MAX;
8447 float v_max = -FLT_MAX;
8448 for (int i = 0; i < values_count; i++)
8449 {
8450 const float v = values_getter(data, i);
8451 if (v != v) // Ignore NaN values
8452 continue;
8453 v_min = ImMin(lhs: v_min, rhs: v);
8454 v_max = ImMax(lhs: v_max, rhs: v);
8455 }
8456 if (scale_min == FLT_MAX)
8457 scale_min = v_min;
8458 if (scale_max == FLT_MAX)
8459 scale_max = v_max;
8460 }
8461
8462 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: GetColorU32(idx: ImGuiCol_FrameBg), borders: true, rounding: style.FrameRounding);
8463
8464 const int values_count_min = (plot_type == ImGuiPlotType_Lines) ? 2 : 1;
8465 int idx_hovered = -1;
8466 if (values_count >= values_count_min)
8467 {
8468 int res_w = ImMin(lhs: (int)frame_size.x, rhs: values_count) + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
8469 int item_count = values_count + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
8470
8471 // Tooltip on hover
8472 if (hovered && inner_bb.Contains(p: g.IO.MousePos))
8473 {
8474 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);
8475 const int v_idx = (int)(t * item_count);
8476 IM_ASSERT(v_idx >= 0 && v_idx < values_count);
8477
8478 const float v0 = values_getter(data, (v_idx + values_offset) % values_count);
8479 const float v1 = values_getter(data, (v_idx + 1 + values_offset) % values_count);
8480 if (plot_type == ImGuiPlotType_Lines)
8481 SetTooltip("%d: %8.4g\n%d: %8.4g", v_idx, v0, v_idx + 1, v1);
8482 else if (plot_type == ImGuiPlotType_Histogram)
8483 SetTooltip("%d: %8.4g", v_idx, v0);
8484 idx_hovered = v_idx;
8485 }
8486
8487 const float t_step = 1.0f / (float)res_w;
8488 const float inv_scale = (scale_min == scale_max) ? 0.0f : (1.0f / (scale_max - scale_min));
8489
8490 float v0 = values_getter(data, (0 + values_offset) % values_count);
8491 float t0 = 0.0f;
8492 ImVec2 tp0 = ImVec2( t0, 1.0f - ImSaturate(f: (v0 - scale_min) * inv_scale) ); // Point in the normalized space of our target rectangle
8493 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
8494
8495 const ImU32 col_base = GetColorU32(idx: (plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLines : ImGuiCol_PlotHistogram);
8496 const ImU32 col_hovered = GetColorU32(idx: (plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLinesHovered : ImGuiCol_PlotHistogramHovered);
8497
8498 for (int n = 0; n < res_w; n++)
8499 {
8500 const float t1 = t0 + t_step;
8501 const int v1_idx = (int)(t0 * item_count + 0.5f);
8502 IM_ASSERT(v1_idx >= 0 && v1_idx < values_count);
8503 const float v1 = values_getter(data, (v1_idx + values_offset + 1) % values_count);
8504 const ImVec2 tp1 = ImVec2( t1, 1.0f - ImSaturate(f: (v1 - scale_min) * inv_scale) );
8505
8506 // 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.
8507 ImVec2 pos0 = ImLerp(a: inner_bb.Min, b: inner_bb.Max, t: tp0);
8508 ImVec2 pos1 = ImLerp(a: inner_bb.Min, b: inner_bb.Max, t: (plot_type == ImGuiPlotType_Lines) ? tp1 : ImVec2(tp1.x, histogram_zero_line_t));
8509 if (plot_type == ImGuiPlotType_Lines)
8510 {
8511 window->DrawList->AddLine(p1: pos0, p2: pos1, col: idx_hovered == v1_idx ? col_hovered : col_base);
8512 }
8513 else if (plot_type == ImGuiPlotType_Histogram)
8514 {
8515 if (pos1.x >= pos0.x + 2.0f)
8516 pos1.x -= 1.0f;
8517 window->DrawList->AddRectFilled(p_min: pos0, p_max: pos1, col: idx_hovered == v1_idx ? col_hovered : col_base);
8518 }
8519
8520 t0 = t1;
8521 tp0 = tp1;
8522 }
8523 }
8524
8525 // Text overlay
8526 if (overlay_text)
8527 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));
8528
8529 if (label_size.x > 0.0f)
8530 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), text: label);
8531
8532 // Return hovered index or -1 if none are hovered.
8533 // 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().
8534 return idx_hovered;
8535}
8536
8537struct ImGuiPlotArrayGetterData
8538{
8539 const float* Values;
8540 int Stride;
8541
8542 ImGuiPlotArrayGetterData(const float* values, int stride) { Values = values; Stride = stride; }
8543};
8544
8545static float Plot_ArrayGetter(void* data, int idx)
8546{
8547 ImGuiPlotArrayGetterData* plot_data = (ImGuiPlotArrayGetterData*)data;
8548 const float v = *(const float*)(const void*)((const unsigned char*)plot_data->Values + (size_t)idx * plot_data->Stride);
8549 return v;
8550}
8551
8552void 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)
8553{
8554 ImGuiPlotArrayGetterData data(values, stride);
8555 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);
8556}
8557
8558void 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)
8559{
8560 PlotEx(plot_type: ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, size_arg: graph_size);
8561}
8562
8563void 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)
8564{
8565 ImGuiPlotArrayGetterData data(values, stride);
8566 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);
8567}
8568
8569void 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)
8570{
8571 PlotEx(plot_type: ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, size_arg: graph_size);
8572}
8573
8574//-------------------------------------------------------------------------
8575// [SECTION] Widgets: Value helpers
8576// Those is not very useful, legacy API.
8577//-------------------------------------------------------------------------
8578// - Value()
8579//-------------------------------------------------------------------------
8580
8581void ImGui::Value(const char* prefix, bool b)
8582{
8583 Text(fmt: "%s: %s", prefix, (b ? "true" : "false"));
8584}
8585
8586void ImGui::Value(const char* prefix, int v)
8587{
8588 Text(fmt: "%s: %d", prefix, v);
8589}
8590
8591void ImGui::Value(const char* prefix, unsigned int v)
8592{
8593 Text(fmt: "%s: %d", prefix, v);
8594}
8595
8596void ImGui::Value(const char* prefix, float v, const char* float_format)
8597{
8598 if (float_format)
8599 {
8600 char fmt[64];
8601 ImFormatString(buf: fmt, IM_ARRAYSIZE(fmt), fmt: "%%s: %s", float_format);
8602 Text(fmt, prefix, v);
8603 }
8604 else
8605 {
8606 Text(fmt: "%s: %.3f", prefix, v);
8607 }
8608}
8609
8610//-------------------------------------------------------------------------
8611// [SECTION] MenuItem, BeginMenu, EndMenu, etc.
8612//-------------------------------------------------------------------------
8613// - ImGuiMenuColumns [Internal]
8614// - BeginMenuBar()
8615// - EndMenuBar()
8616// - BeginMainMenuBar()
8617// - EndMainMenuBar()
8618// - BeginMenu()
8619// - EndMenu()
8620// - MenuItemEx() [Internal]
8621// - MenuItem()
8622//-------------------------------------------------------------------------
8623
8624// Helpers for internal use
8625void ImGuiMenuColumns::Update(float spacing, bool window_reappearing)
8626{
8627 if (window_reappearing)
8628 memset(s: Widths, c: 0, n: sizeof(Widths));
8629 Spacing = (ImU16)spacing;
8630 CalcNextTotalWidth(update_offsets: true);
8631 memset(s: Widths, c: 0, n: sizeof(Widths));
8632 TotalWidth = NextTotalWidth;
8633 NextTotalWidth = 0;
8634}
8635
8636void ImGuiMenuColumns::CalcNextTotalWidth(bool update_offsets)
8637{
8638 ImU16 offset = 0;
8639 bool want_spacing = false;
8640 for (int i = 0; i < IM_ARRAYSIZE(Widths); i++)
8641 {
8642 ImU16 width = Widths[i];
8643 if (want_spacing && width > 0)
8644 offset += Spacing;
8645 want_spacing |= (width > 0);
8646 if (update_offsets)
8647 {
8648 if (i == 1) { OffsetLabel = offset; }
8649 if (i == 2) { OffsetShortcut = offset; }
8650 if (i == 3) { OffsetMark = offset; }
8651 }
8652 offset += width;
8653 }
8654 NextTotalWidth = offset;
8655}
8656
8657float ImGuiMenuColumns::DeclColumns(float w_icon, float w_label, float w_shortcut, float w_mark)
8658{
8659 Widths[0] = ImMax(lhs: Widths[0], rhs: (ImU16)w_icon);
8660 Widths[1] = ImMax(lhs: Widths[1], rhs: (ImU16)w_label);
8661 Widths[2] = ImMax(lhs: Widths[2], rhs: (ImU16)w_shortcut);
8662 Widths[3] = ImMax(lhs: Widths[3], rhs: (ImU16)w_mark);
8663 CalcNextTotalWidth(update_offsets: false);
8664 return (float)ImMax(lhs: TotalWidth, rhs: NextTotalWidth);
8665}
8666
8667// FIXME: Provided a rectangle perhaps e.g. a BeginMenuBarEx() could be used anywhere..
8668// Currently the main responsibility of this function being to setup clip-rect + horizontal layout + menu navigation layer.
8669// 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.
8670// Then later the same system could be used for multiple menu-bars, scrollbars, side-bars.
8671bool ImGui::BeginMenuBar()
8672{
8673 ImGuiWindow* window = GetCurrentWindow();
8674 if (window->SkipItems)
8675 return false;
8676 if (!(window->Flags & ImGuiWindowFlags_MenuBar))
8677 return false;
8678
8679 IM_ASSERT(!window->DC.MenuBarAppending);
8680 BeginGroup(); // Backup position on layer 0 // FIXME: Misleading to use a group for that backup/restore
8681 PushID(str_id: "##MenuBar");
8682
8683 // 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.
8684 // 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.
8685 const float border_top = ImMax(IM_ROUND(window->WindowBorderSize * 0.5f - window->TitleBarHeight), rhs: 0.0f);
8686 const float border_half = IM_ROUND(window->WindowBorderSize * 0.5f);
8687 ImRect bar_rect = window->MenuBarRect();
8688 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));
8689 clip_rect.ClipWith(r: window->OuterRectClipped);
8690 PushClipRect(clip_rect_min: clip_rect.Min, clip_rect_max: clip_rect.Max, intersect_with_current_clip_rect: false);
8691
8692 // 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).
8693 window->DC.CursorPos = window->DC.CursorMaxPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffset.x, bar_rect.Min.y + window->DC.MenuBarOffset.y);
8694 window->DC.LayoutType = ImGuiLayoutType_Horizontal;
8695 window->DC.IsSameLine = false;
8696 window->DC.NavLayerCurrent = ImGuiNavLayer_Menu;
8697 window->DC.MenuBarAppending = true;
8698 AlignTextToFramePadding();
8699 return true;
8700}
8701
8702void ImGui::EndMenuBar()
8703{
8704 ImGuiWindow* window = GetCurrentWindow();
8705 if (window->SkipItems)
8706 return;
8707 ImGuiContext& g = *GImGui;
8708
8709 IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'"
8710 IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar);
8711 IM_ASSERT(window->DC.MenuBarAppending);
8712
8713 // Nav: When a move request within one of our child menu failed, capture the request to navigate among our siblings.
8714 if (NavMoveRequestButNoResultYet() && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) && (g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu))
8715 {
8716 // Try to find out if the request is for one of our child menu
8717 ImGuiWindow* nav_earliest_child = g.NavWindow;
8718 while (nav_earliest_child->ParentWindow && (nav_earliest_child->ParentWindow->Flags & ImGuiWindowFlags_ChildMenu))
8719 nav_earliest_child = nav_earliest_child->ParentWindow;
8720 if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0)
8721 {
8722 // To do so we claim focus back, restore NavId and then process the movement request for yet another frame.
8723 // 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)
8724 const ImGuiNavLayer layer = ImGuiNavLayer_Menu;
8725 IM_ASSERT(window->DC.NavLayersActiveMaskNext & (1 << layer)); // Sanity check (FIXME: Seems unnecessary)
8726 FocusWindow(window);
8727 SetNavID(id: window->NavLastIds[layer], nav_layer: layer, focus_scope_id: 0, rect_rel: window->NavRectRel[layer]);
8728 // FIXME-NAV: How to deal with this when not using g.IO.ConfigNavCursorVisibleAuto?
8729 if (g.NavCursorVisible)
8730 {
8731 g.NavCursorVisible = false; // Hide nav cursor for the current frame so we don't see the intermediary selection. Will be set again
8732 g.NavCursorHideFrames = 2;
8733 }
8734 g.NavHighlightItemUnderNav = g.NavMousePosDirty = true;
8735 NavMoveRequestForward(move_dir: g.NavMoveDir, clip_dir: g.NavMoveClipDir, move_flags: g.NavMoveFlags, scroll_flags: g.NavMoveScrollFlags); // Repeat
8736 }
8737 }
8738
8739 PopClipRect();
8740 PopID();
8741 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.
8742
8743 // FIXME: Extremely confusing, cleanup by (a) working on WorkRect stack system (b) not using a Group confusingly here.
8744 ImGuiGroupData& group_data = g.GroupStack.back();
8745 group_data.EmitItem = false;
8746 ImVec2 restore_cursor_max_pos = group_data.BackupCursorMaxPos;
8747 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.
8748 EndGroup(); // Restore position on layer 0 // FIXME: Misleading to use a group for that backup/restore
8749 window->DC.LayoutType = ImGuiLayoutType_Vertical;
8750 window->DC.IsSameLine = false;
8751 window->DC.NavLayerCurrent = ImGuiNavLayer_Main;
8752 window->DC.MenuBarAppending = false;
8753 window->DC.CursorMaxPos = restore_cursor_max_pos;
8754}
8755
8756// Important: calling order matters!
8757// FIXME: Somehow overlapping with docking tech.
8758// FIXME: The "rect-cut" aspect of this could be formalized into a lower-level helper (rect-cut: https://halt.software/dead-simple-layouts)
8759bool ImGui::BeginViewportSideBar(const char* name, ImGuiViewport* viewport_p, ImGuiDir dir, float axis_size, ImGuiWindowFlags window_flags)
8760{
8761 IM_ASSERT(dir != ImGuiDir_None);
8762
8763 ImGuiWindow* bar_window = FindWindowByName(name);
8764 ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)(viewport_p ? viewport_p : GetMainViewport());
8765 if (bar_window == NULL || bar_window->BeginCount == 0)
8766 {
8767 // Calculate and set window size/position
8768 ImRect avail_rect = viewport->GetBuildWorkRect();
8769 ImGuiAxis axis = (dir == ImGuiDir_Up || dir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X;
8770 ImVec2 pos = avail_rect.Min;
8771 if (dir == ImGuiDir_Right || dir == ImGuiDir_Down)
8772 pos[axis] = avail_rect.Max[axis] - axis_size;
8773 ImVec2 size = avail_rect.GetSize();
8774 size[axis] = axis_size;
8775 SetNextWindowPos(pos);
8776 SetNextWindowSize(size);
8777
8778 // Report our size into work area (for next frame) using actual window size
8779 if (dir == ImGuiDir_Up || dir == ImGuiDir_Left)
8780 viewport->BuildWorkInsetMin[axis] += axis_size;
8781 else if (dir == ImGuiDir_Down || dir == ImGuiDir_Right)
8782 viewport->BuildWorkInsetMax[axis] += axis_size;
8783 }
8784
8785 window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking;
8786 SetNextWindowViewport(viewport->ID); // Enforce viewport so we don't create our own viewport when ImGuiConfigFlags_ViewportsNoMerge is set.
8787 PushStyleVar(idx: ImGuiStyleVar_WindowRounding, val: 0.0f);
8788 PushStyleVar(idx: ImGuiStyleVar_WindowMinSize, val: ImVec2(0, 0)); // Lift normal size constraint
8789 bool is_open = Begin(name, NULL, flags: window_flags);
8790 PopStyleVar(count: 2);
8791
8792 return is_open;
8793}
8794
8795bool ImGui::BeginMainMenuBar()
8796{
8797 ImGuiContext& g = *GImGui;
8798 ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport();
8799
8800 // Notify of viewport change so GetFrameHeight() can be accurate in case of DPI change
8801 SetCurrentViewport(NULL, viewport);
8802
8803 // For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set.
8804 // FIXME: This could be generalized as an opt-in way to clamp window->DC.CursorStartPos to avoid SafeArea?
8805 // FIXME: Consider removing support for safe area down the line... it's messy. Nowadays consoles have support for TV calibration in OS settings.
8806 g.NextWindowData.MenuBarOffsetMinVal = ImVec2(g.Style.DisplaySafeAreaPadding.x, ImMax(lhs: g.Style.DisplaySafeAreaPadding.y - g.Style.FramePadding.y, rhs: 0.0f));
8807 ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar;
8808 float height = GetFrameHeight();
8809 bool is_open = BeginViewportSideBar(name: "##MainMenuBar", viewport_p: viewport, dir: ImGuiDir_Up, axis_size: height, window_flags);
8810 g.NextWindowData.MenuBarOffsetMinVal = ImVec2(0.0f, 0.0f);
8811 if (!is_open)
8812 {
8813 End();
8814 return false;
8815 }
8816
8817 // Temporarily disable _NoSavedSettings, in the off-chance that tables or child windows submitted within the menu-bar may want to use settings. (#8356)
8818 g.CurrentWindow->Flags &= ~ImGuiWindowFlags_NoSavedSettings;
8819 BeginMenuBar();
8820 return is_open;
8821}
8822
8823void ImGui::EndMainMenuBar()
8824{
8825 ImGuiContext& g = *GImGui;
8826 if (!g.CurrentWindow->DC.MenuBarAppending)
8827 {
8828 IM_ASSERT_USER_ERROR(0, "Calling EndMainMenuBar() not from a menu-bar!"); // Not technically testing that it is the main menu bar
8829 return;
8830 }
8831
8832 EndMenuBar();
8833 g.CurrentWindow->Flags |= ImGuiWindowFlags_NoSavedSettings; // Restore _NoSavedSettings (#8356)
8834
8835 // When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window
8836 // FIXME: With this strategy we won't be able to restore a NULL focus.
8837 if (g.CurrentWindow == g.NavWindow && g.NavLayer == ImGuiNavLayer_Main && !g.NavAnyRequest && g.ActiveId == 0)
8838 FocusTopMostWindowUnderOne(under_this_window: g.NavWindow, NULL, NULL, flags: ImGuiFocusRequestFlags_UnlessBelowModal | ImGuiFocusRequestFlags_RestoreFocusedChild);
8839
8840 End();
8841}
8842
8843static bool IsRootOfOpenMenuSet()
8844{
8845 ImGuiContext& g = *GImGui;
8846 ImGuiWindow* window = g.CurrentWindow;
8847 if ((g.OpenPopupStack.Size <= g.BeginPopupStack.Size) || (window->Flags & ImGuiWindowFlags_ChildMenu))
8848 return false;
8849
8850 // Initially we used 'upper_popup->OpenParentId == window->IDStack.back()' to differentiate multiple menu sets from each others
8851 // (e.g. inside menu bar vs loose menu items) based on parent ID.
8852 // This would however prevent the use of e.g. PushID() user code submitting menus.
8853 // Previously this worked between popup and a first child menu because the first child menu always had the _ChildWindow flag,
8854 // making hovering on parent popup possible while first child menu was focused - but this was generally a bug with other side effects.
8855 // Instead we don't treat Popup specifically (in order to consistently support menu features in them), maybe the first child menu of a Popup
8856 // doesn't have the _ChildWindow flag, and we rely on this IsRootOfOpenMenuSet() check to allow hovering between root window/popup and first child menu.
8857 // 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.
8858 // 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
8859 // 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
8860 // it likely won't be a problem anyone runs into.
8861 const ImGuiPopupData* upper_popup = &g.OpenPopupStack[g.BeginPopupStack.Size];
8862 if (window->DC.NavLayerCurrent != upper_popup->ParentNavLayer)
8863 return false;
8864 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);
8865}
8866
8867bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
8868{
8869 ImGuiWindow* window = GetCurrentWindow();
8870 if (window->SkipItems)
8871 return false;
8872
8873 ImGuiContext& g = *GImGui;
8874 const ImGuiStyle& style = g.Style;
8875 const ImGuiID id = window->GetID(str: label);
8876 bool menu_is_open = IsPopupOpen(id, popup_flags: ImGuiPopupFlags_None);
8877
8878 // 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)
8879 // 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.
8880 ImGuiWindowFlags window_flags = ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus;
8881 if (window->Flags & ImGuiWindowFlags_ChildMenu)
8882 window_flags |= ImGuiWindowFlags_ChildWindow;
8883
8884 // If a menu with same the ID was already submitted, we will append to it, matching the behavior of Begin().
8885 // 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.
8886 // If somehow this is ever becoming a problem we can switch to use e.g. ImGuiStorage mapping key to last frame used.
8887 if (g.MenusIdSubmittedThisFrame.contains(v: id))
8888 {
8889 if (menu_is_open)
8890 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)
8891 else
8892 g.NextWindowData.ClearFlags(); // we behave like Begin() and need to consume those values
8893 return menu_is_open;
8894 }
8895
8896 // Tag menu as used. Next time BeginMenu() with same ID is called it will append to existing menu
8897 g.MenusIdSubmittedThisFrame.push_back(v: id);
8898
8899 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
8900
8901 // 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)
8902 // This is only done for items for the menu set and not the full parent window.
8903 const bool menuset_is_open = IsRootOfOpenMenuSet();
8904 if (menuset_is_open)
8905 PushItemFlag(option: ImGuiItemFlags_NoWindowHoverableCheck, enabled: true);
8906
8907 // The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu,
8908 // However the final position is going to be different! It is chosen by FindBestWindowPosForPopup().
8909 // e.g. Menus tend to overlap each other horizontally to amplify relative Z-ordering.
8910 ImVec2 popup_pos, pos = window->DC.CursorPos;
8911 PushID(str_id: label);
8912 if (!enabled)
8913 BeginDisabled();
8914 const ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
8915 bool pressed;
8916
8917 // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
8918 const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SelectOnClick | ImGuiSelectableFlags_NoAutoClosePopups;
8919 if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
8920 {
8921 // Menu inside a horizontal menu bar
8922 // Selectable extend their highlight by half ItemSpacing in each direction.
8923 // For ChildMenu, the popup position will be overwritten by the call to FindBestWindowPosForPopup() in Begin()
8924 popup_pos = ImVec2(pos.x - 1.0f - IM_TRUNC(style.ItemSpacing.x * 0.5f), pos.y - style.FramePadding.y + window->MenuBarHeight);
8925 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f);
8926 PushStyleVarX(idx: ImGuiStyleVar_ItemSpacing, val_x: style.ItemSpacing.x * 2.0f);
8927 float w = label_size.x;
8928 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
8929 pressed = Selectable(label: "", selected: menu_is_open, flags: selectable_flags, size_arg: ImVec2(w, label_size.y));
8930 LogSetNextTextDecoration(prefix: "[", suffix: "]");
8931 RenderText(pos: text_pos, text: label);
8932 PopStyleVar();
8933 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().
8934 }
8935 else
8936 {
8937 // Menu inside a regular/vertical menu
8938 // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
8939 // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.
8940 popup_pos = ImVec2(pos.x, pos.y - style.WindowPadding.y);
8941 float icon_w = (icon && icon[0]) ? CalcTextSize(text: icon, NULL).x : 0.0f;
8942 float checkmark_w = IM_TRUNC(g.FontSize * 1.20f);
8943 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
8944 float extra_w = ImMax(lhs: 0.0f, rhs: GetContentRegionAvail().x - min_w);
8945 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
8946 pressed = Selectable(label: "", selected: menu_is_open, flags: selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, size_arg: ImVec2(min_w, label_size.y));
8947 LogSetNextTextDecoration(prefix: "", suffix: ">");
8948 RenderText(pos: text_pos, text: label);
8949 if (icon_w > 0.0f)
8950 RenderText(pos: pos + ImVec2(offsets->OffsetIcon, 0.0f), text: icon);
8951 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);
8952 }
8953 if (!enabled)
8954 EndDisabled();
8955
8956 const bool hovered = (g.HoveredId == id) && enabled && !g.NavHighlightItemUnderNav;
8957 if (menuset_is_open)
8958 PopItemFlag();
8959
8960 bool want_open = false;
8961 bool want_open_nav_init = false;
8962 bool want_close = false;
8963 if (window->DC.LayoutType == ImGuiLayoutType_Vertical) // (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu))
8964 {
8965 // Close menu when not hovering it anymore unless we are moving roughly in the direction of the menu
8966 // Implement http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown to avoid using timers, so menus feels more reactive.
8967 bool moving_toward_child_menu = false;
8968 ImGuiPopupData* child_popup = (g.BeginPopupStack.Size < g.OpenPopupStack.Size) ? &g.OpenPopupStack[g.BeginPopupStack.Size] : NULL; // Popup candidate (testing below)
8969 ImGuiWindow* child_menu_window = (child_popup && child_popup->Window && child_popup->Window->ParentWindow == window) ? child_popup->Window : NULL;
8970 if (g.HoveredWindow == window && child_menu_window != NULL)
8971 {
8972 const float ref_unit = g.FontSize; // FIXME-DPI
8973 const float child_dir = (window->Pos.x < child_menu_window->Pos.x) ? 1.0f : -1.0f;
8974 const ImRect next_window_rect = child_menu_window->Rect();
8975 ImVec2 ta = (g.IO.MousePos - g.IO.MouseDelta);
8976 ImVec2 tb = (child_dir > 0.0f) ? next_window_rect.GetTL() : next_window_rect.GetTR();
8977 ImVec2 tc = (child_dir > 0.0f) ? next_window_rect.GetBL() : next_window_rect.GetBR();
8978 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.
8979 ta.x += child_dir * -0.5f;
8980 tb.x += child_dir * ref_unit;
8981 tc.x += child_dir * ref_unit;
8982 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
8983 tc.y = ta.y + ImMin(lhs: (tc.y + pad_farmost_h) - ta.y, rhs: +ref_unit * 8.0f);
8984 moving_toward_child_menu = ImTriangleContainsPoint(a: ta, b: tb, c: tc, p: g.IO.MousePos);
8985 //GetForegroundDrawList()->AddTriangleFilled(ta, tb, tc, moving_toward_child_menu ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); // [DEBUG]
8986 }
8987
8988 // 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)
8989 // 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.
8990 // (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.)
8991 if (menu_is_open && !hovered && g.HoveredWindow == window && !moving_toward_child_menu && !g.NavHighlightItemUnderNav && g.ActiveId == 0)
8992 want_close = true;
8993
8994 // Open
8995 // (note: at this point 'hovered' actually includes the NavDisableMouseHover == false test)
8996 if (!menu_is_open && pressed) // Click/activate to open
8997 want_open = true;
8998 else if (!menu_is_open && hovered && !moving_toward_child_menu) // Hover to open
8999 want_open = true;
9000 else if (!menu_is_open && hovered && g.HoveredIdTimer >= 0.30f && g.MouseStationaryTimer >= 0.30f) // Hover to open (timer fallback)
9001 want_open = true;
9002 if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open
9003 {
9004 want_open = want_open_nav_init = true;
9005 NavMoveRequestCancel();
9006 SetNavCursorVisibleAfterMove();
9007 }
9008 }
9009 else
9010 {
9011 // Menu bar
9012 if (menu_is_open && pressed && menuset_is_open) // Click an open menu again to close it
9013 {
9014 want_close = true;
9015 want_open = menu_is_open = false;
9016 }
9017 else if (pressed || (hovered && menuset_is_open && !menu_is_open)) // First click to open, then hover to open others
9018 {
9019 want_open = true;
9020 }
9021 else if (g.NavId == id && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open
9022 {
9023 want_open = true;
9024 NavMoveRequestCancel();
9025 }
9026 }
9027
9028 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.. }'
9029 want_close = true;
9030 if (want_close && IsPopupOpen(id, popup_flags: ImGuiPopupFlags_None))
9031 ClosePopupToLevel(remaining: g.BeginPopupStack.Size, restore_focus_to_window_under_popup: true);
9032
9033 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Openable | (menu_is_open ? ImGuiItemStatusFlags_Opened : 0));
9034 PopID();
9035
9036 if (want_open && !menu_is_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size)
9037 {
9038 // 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.
9039 OpenPopup(str_id: label);
9040 }
9041 else if (want_open)
9042 {
9043 menu_is_open = true;
9044 OpenPopup(str_id: label, popup_flags: ImGuiPopupFlags_NoReopen);// | (want_open_nav_init ? ImGuiPopupFlags_NoReopenAlwaysNavInit : 0));
9045 }
9046
9047 if (menu_is_open)
9048 {
9049 ImGuiLastItemData last_item_in_parent = g.LastItemData;
9050 SetNextWindowPos(pos: popup_pos, cond: ImGuiCond_Always); // Note: misleading: the value will serve as reference for FindBestWindowPosForPopup(), not actual pos.
9051 PushStyleVar(idx: ImGuiStyleVar_ChildRounding, val: style.PopupRounding); // First level will use _PopupRounding, subsequent will use _ChildRounding
9052 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)
9053 PopStyleVar();
9054 if (menu_is_open)
9055 {
9056 // Implement what ImGuiPopupFlags_NoReopenAlwaysNavInit would do:
9057 // Perform an init request in the case the popup was already open (via a previous mouse hover)
9058 if (want_open && want_open_nav_init && !g.NavInitRequest)
9059 {
9060 FocusWindow(window: g.CurrentWindow, flags: ImGuiFocusRequestFlags_UnlessBelowModal);
9061 NavInitWindow(window: g.CurrentWindow, force_reinit: false);
9062 }
9063
9064 // Restore LastItemData so IsItemXXXX functions can work after BeginMenu()/EndMenu()
9065 // (This fixes using IsItemClicked() and IsItemHovered(), but IsItemHovered() also relies on its support for ImGuiItemFlags_NoWindowHoverableCheck)
9066 g.LastItemData = last_item_in_parent;
9067 if (g.HoveredWindow == window)
9068 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow;
9069 }
9070 }
9071 else
9072 {
9073 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
9074 }
9075
9076 return menu_is_open;
9077}
9078
9079bool ImGui::BeginMenu(const char* label, bool enabled)
9080{
9081 return BeginMenuEx(label, NULL, enabled);
9082}
9083
9084void ImGui::EndMenu()
9085{
9086 // Nav: When a left move request our menu failed, close ourselves.
9087 ImGuiContext& g = *GImGui;
9088 ImGuiWindow* window = g.CurrentWindow;
9089 IM_ASSERT(window->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginMenu()/EndMenu() calls
9090 ImGuiWindow* parent_window = window->ParentWindow; // Should always be != NULL is we passed assert.
9091 if (window->BeginCount == window->BeginCountPreviousFrame)
9092 if (g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet())
9093 if (g.NavWindow && (g.NavWindow->RootWindowForNav == window) && parent_window->DC.LayoutType == ImGuiLayoutType_Vertical)
9094 {
9095 ClosePopupToLevel(remaining: g.BeginPopupStack.Size - 1, restore_focus_to_window_under_popup: true);
9096 NavMoveRequestCancel();
9097 }
9098
9099 EndPopup();
9100}
9101
9102bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut, bool selected, bool enabled)
9103{
9104 ImGuiWindow* window = GetCurrentWindow();
9105 if (window->SkipItems)
9106 return false;
9107
9108 ImGuiContext& g = *GImGui;
9109 ImGuiStyle& style = g.Style;
9110 ImVec2 pos = window->DC.CursorPos;
9111 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
9112
9113 // See BeginMenuEx() for comments about this.
9114 const bool menuset_is_open = IsRootOfOpenMenuSet();
9115 if (menuset_is_open)
9116 PushItemFlag(option: ImGuiItemFlags_NoWindowHoverableCheck, enabled: true);
9117
9118 // We've been using the equivalent of ImGuiSelectableFlags_SetNavIdOnHover on all Selectable() since early Nav system days (commit 43ee5d73),
9119 // 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.
9120 bool pressed;
9121 PushID(str_id: label);
9122 if (!enabled)
9123 BeginDisabled();
9124
9125 // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
9126 const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_SelectOnRelease | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SetNavIdOnHover;
9127 const ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
9128 if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
9129 {
9130 // Mimic the exact layout spacing of BeginMenu() to allow MenuItem() inside a menu bar, which is a little misleading but may be useful
9131 // Note that in this situation: we don't render the shortcut, we render a highlight instead of the selected tick mark.
9132 float w = label_size.x;
9133 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f);
9134 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
9135 PushStyleVarX(idx: ImGuiStyleVar_ItemSpacing, val_x: style.ItemSpacing.x * 2.0f);
9136 pressed = Selectable(label: "", selected, flags: selectable_flags, size_arg: ImVec2(w, 0.0f));
9137 PopStyleVar();
9138 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
9139 RenderText(pos: text_pos, text: label);
9140 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().
9141 }
9142 else
9143 {
9144 // Menu item inside a vertical menu
9145 // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
9146 // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.
9147 float icon_w = (icon && icon[0]) ? CalcTextSize(text: icon, NULL).x : 0.0f;
9148 float shortcut_w = (shortcut && shortcut[0]) ? CalcTextSize(text: shortcut, NULL).x : 0.0f;
9149 float checkmark_w = IM_TRUNC(g.FontSize * 1.20f);
9150 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
9151 float stretch_w = ImMax(lhs: 0.0f, rhs: GetContentRegionAvail().x - min_w);
9152 pressed = Selectable(label: "", selected: false, flags: selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, size_arg: ImVec2(min_w, label_size.y));
9153 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
9154 {
9155 RenderText(pos: pos + ImVec2(offsets->OffsetLabel, 0.0f), text: label);
9156 if (icon_w > 0.0f)
9157 RenderText(pos: pos + ImVec2(offsets->OffsetIcon, 0.0f), text: icon);
9158 if (shortcut_w > 0.0f)
9159 {
9160 PushStyleColor(idx: ImGuiCol_Text, col: style.Colors[ImGuiCol_TextDisabled]);
9161 LogSetNextTextDecoration(prefix: "(", suffix: ")");
9162 RenderText(pos: pos + ImVec2(offsets->OffsetShortcut + stretch_w, 0.0f), text: shortcut, NULL, hide_text_after_hash: false);
9163 PopStyleColor();
9164 }
9165 if (selected)
9166 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);
9167 }
9168 }
9169 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0));
9170 if (!enabled)
9171 EndDisabled();
9172 PopID();
9173 if (menuset_is_open)
9174 PopItemFlag();
9175
9176 return pressed;
9177}
9178
9179bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled)
9180{
9181 return MenuItemEx(label, NULL, shortcut, selected, enabled);
9182}
9183
9184bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled)
9185{
9186 if (MenuItemEx(label, NULL, shortcut, selected: p_selected ? *p_selected : false, enabled))
9187 {
9188 if (p_selected)
9189 *p_selected = !*p_selected;
9190 return true;
9191 }
9192 return false;
9193}
9194
9195//-------------------------------------------------------------------------
9196// [SECTION] Widgets: BeginTabBar, EndTabBar, etc.
9197//-------------------------------------------------------------------------
9198// - BeginTabBar()
9199// - BeginTabBarEx() [Internal]
9200// - EndTabBar()
9201// - TabBarLayout() [Internal]
9202// - TabBarCalcTabID() [Internal]
9203// - TabBarCalcMaxTabWidth() [Internal]
9204// - TabBarFindTabById() [Internal]
9205// - TabBarFindTabByOrder() [Internal]
9206// - TabBarFindMostRecentlySelectedTabForActiveWindow() [Internal]
9207// - TabBarGetCurrentTab() [Internal]
9208// - TabBarGetTabName() [Internal]
9209// - TabBarAddTab() [Internal]
9210// - TabBarRemoveTab() [Internal]
9211// - TabBarCloseTab() [Internal]
9212// - TabBarScrollClamp() [Internal]
9213// - TabBarScrollToTab() [Internal]
9214// - TabBarQueueFocus() [Internal]
9215// - TabBarQueueReorder() [Internal]
9216// - TabBarProcessReorderFromMousePos() [Internal]
9217// - TabBarProcessReorder() [Internal]
9218// - TabBarScrollingButtons() [Internal]
9219// - TabBarTabListPopupButton() [Internal]
9220//-------------------------------------------------------------------------
9221
9222struct ImGuiTabBarSection
9223{
9224 int TabCount; // Number of tabs in this section.
9225 float Width; // Sum of width of tabs in this section (after shrinking down)
9226 float Spacing; // Horizontal spacing at the end of the section.
9227
9228 ImGuiTabBarSection() { memset(s: this, c: 0, n: sizeof(*this)); }
9229};
9230
9231namespace ImGui
9232{
9233 static void TabBarLayout(ImGuiTabBar* tab_bar);
9234 static ImU32 TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window);
9235 static float TabBarCalcMaxTabWidth();
9236 static float TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling);
9237 static void TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections);
9238 static ImGuiTabItem* TabBarScrollingButtons(ImGuiTabBar* tab_bar);
9239 static ImGuiTabItem* TabBarTabListPopupButton(ImGuiTabBar* tab_bar);
9240}
9241
9242ImGuiTabBar::ImGuiTabBar()
9243{
9244 memset(s: this, c: 0, n: sizeof(*this));
9245 CurrFrameVisible = PrevFrameVisible = -1;
9246 LastTabItemIdx = -1;
9247}
9248
9249static inline int TabItemGetSectionIdx(const ImGuiTabItem* tab)
9250{
9251 return (tab->Flags & ImGuiTabItemFlags_Leading) ? 0 : (tab->Flags & ImGuiTabItemFlags_Trailing) ? 2 : 1;
9252}
9253
9254static int IMGUI_CDECL TabItemComparerBySection(const void* lhs, const void* rhs)
9255{
9256 const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
9257 const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
9258 const int a_section = TabItemGetSectionIdx(tab: a);
9259 const int b_section = TabItemGetSectionIdx(tab: b);
9260 if (a_section != b_section)
9261 return a_section - b_section;
9262 return (int)(a->IndexDuringLayout - b->IndexDuringLayout);
9263}
9264
9265static int IMGUI_CDECL TabItemComparerByBeginOrder(const void* lhs, const void* rhs)
9266{
9267 const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
9268 const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
9269 return (int)(a->BeginOrder - b->BeginOrder);
9270}
9271
9272static ImGuiTabBar* GetTabBarFromTabBarRef(const ImGuiPtrOrIndex& ref)
9273{
9274 ImGuiContext& g = *GImGui;
9275 return ref.Ptr ? (ImGuiTabBar*)ref.Ptr : g.TabBars.GetByIndex(n: ref.Index);
9276}
9277
9278static ImGuiPtrOrIndex GetTabBarRefFromTabBar(ImGuiTabBar* tab_bar)
9279{
9280 ImGuiContext& g = *GImGui;
9281 if (g.TabBars.Contains(p: tab_bar))
9282 return ImGuiPtrOrIndex(g.TabBars.GetIndex(p: tab_bar));
9283 return ImGuiPtrOrIndex(tab_bar);
9284}
9285
9286bool ImGui::BeginTabBar(const char* str_id, ImGuiTabBarFlags flags)
9287{
9288 ImGuiContext& g = *GImGui;
9289 ImGuiWindow* window = g.CurrentWindow;
9290 if (window->SkipItems)
9291 return false;
9292
9293 ImGuiID id = window->GetID(str: str_id);
9294 ImGuiTabBar* tab_bar = g.TabBars.GetOrAddByKey(key: id);
9295 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);
9296 tab_bar->ID = id;
9297 tab_bar->SeparatorMinX = tab_bar->BarRect.Min.x - IM_TRUNC(window->WindowPadding.x * 0.5f);
9298 tab_bar->SeparatorMaxX = tab_bar->BarRect.Max.x + IM_TRUNC(window->WindowPadding.x * 0.5f);
9299 //if (g.NavWindow && IsWindowChildOf(g.NavWindow, window, false, false))
9300 flags |= ImGuiTabBarFlags_IsFocused;
9301 return BeginTabBarEx(tab_bar, bb: tab_bar_bb, flags);
9302}
9303
9304bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImGuiTabBarFlags flags)
9305{
9306 ImGuiContext& g = *GImGui;
9307 ImGuiWindow* window = g.CurrentWindow;
9308 if (window->SkipItems)
9309 return false;
9310
9311 IM_ASSERT(tab_bar->ID != 0);
9312 if ((flags & ImGuiTabBarFlags_DockNode) == 0)
9313 PushOverrideID(id: tab_bar->ID);
9314
9315 // Add to stack
9316 g.CurrentTabBarStack.push_back(v: GetTabBarRefFromTabBar(tab_bar));
9317 g.CurrentTabBar = tab_bar;
9318 tab_bar->Window = window;
9319
9320 // Append with multiple BeginTabBar()/EndTabBar() pairs.
9321 tab_bar->BackupCursorPos = window->DC.CursorPos;
9322 if (tab_bar->CurrFrameVisible == g.FrameCount)
9323 {
9324 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
9325 tab_bar->BeginCount++;
9326 return true;
9327 }
9328
9329 // Ensure correct ordering when toggling ImGuiTabBarFlags_Reorderable flag, or when a new tab was added while being not reorderable
9330 if ((flags & ImGuiTabBarFlags_Reorderable) != (tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (tab_bar->TabsAddedNew && !(flags & ImGuiTabBarFlags_Reorderable)))
9331 if ((flags & ImGuiTabBarFlags_DockNode) == 0) // FIXME: TabBar with DockNode can now be hybrid
9332 ImQsort(base: tab_bar->Tabs.Data, count: tab_bar->Tabs.Size, size_of_element: sizeof(ImGuiTabItem), compare_func: TabItemComparerByBeginOrder);
9333 tab_bar->TabsAddedNew = false;
9334
9335 // Flags
9336 if ((flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0)
9337 flags |= ImGuiTabBarFlags_FittingPolicyDefault_;
9338
9339 tab_bar->Flags = flags;
9340 tab_bar->BarRect = tab_bar_bb;
9341 tab_bar->WantLayout = true; // Layout will be done on the first call to ItemTab()
9342 tab_bar->PrevFrameVisible = tab_bar->CurrFrameVisible;
9343 tab_bar->CurrFrameVisible = g.FrameCount;
9344 tab_bar->PrevTabsContentsHeight = tab_bar->CurrTabsContentsHeight;
9345 tab_bar->CurrTabsContentsHeight = 0.0f;
9346 tab_bar->ItemSpacingY = g.Style.ItemSpacing.y;
9347 tab_bar->FramePadding = g.Style.FramePadding;
9348 tab_bar->TabsActiveCount = 0;
9349 tab_bar->LastTabItemIdx = -1;
9350 tab_bar->BeginCount = 1;
9351
9352 // Set cursor pos in a way which only be used in the off-chance the user erroneously submits item before BeginTabItem(): items will overlap
9353 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
9354
9355 // Draw separator
9356 // (it would be misleading to draw this in EndTabBar() suggesting that it may be drawn over tabs, as tab bar are appendable)
9357 const ImU32 col = GetColorU32(idx: (flags & ImGuiTabBarFlags_IsFocused) ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected);
9358 if (g.Style.TabBarBorderSize > 0.0f)
9359 {
9360 const float y = tab_bar->BarRect.Max.y;
9361 window->DrawList->AddRectFilled(p_min: ImVec2(tab_bar->SeparatorMinX, y - g.Style.TabBarBorderSize), p_max: ImVec2(tab_bar->SeparatorMaxX, y), col);
9362 }
9363 return true;
9364}
9365
9366void ImGui::EndTabBar()
9367{
9368 ImGuiContext& g = *GImGui;
9369 ImGuiWindow* window = g.CurrentWindow;
9370 if (window->SkipItems)
9371 return;
9372
9373 ImGuiTabBar* tab_bar = g.CurrentTabBar;
9374 if (tab_bar == NULL)
9375 {
9376 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Mismatched BeginTabBar()/EndTabBar()!");
9377 return;
9378 }
9379
9380 // Fallback in case no TabItem have been submitted
9381 if (tab_bar->WantLayout)
9382 TabBarLayout(tab_bar);
9383
9384 // Restore the last visible height if no tab is visible, this reduce vertical flicker/movement when a tabs gets removed without calling SetTabItemClosed().
9385 const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
9386 if (tab_bar->VisibleTabWasSubmitted || tab_bar->VisibleTabId == 0 || tab_bar_appearing)
9387 {
9388 tab_bar->CurrTabsContentsHeight = ImMax(lhs: window->DC.CursorPos.y - tab_bar->BarRect.Max.y, rhs: tab_bar->CurrTabsContentsHeight);
9389 window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->CurrTabsContentsHeight;
9390 }
9391 else
9392 {
9393 window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->PrevTabsContentsHeight;
9394 }
9395 if (tab_bar->BeginCount > 1)
9396 window->DC.CursorPos = tab_bar->BackupCursorPos;
9397
9398 tab_bar->LastTabItemIdx = -1;
9399 if ((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0)
9400 PopID();
9401
9402 g.CurrentTabBarStack.pop_back();
9403 g.CurrentTabBar = g.CurrentTabBarStack.empty() ? NULL : GetTabBarFromTabBarRef(ref: g.CurrentTabBarStack.back());
9404}
9405
9406// Scrolling happens only in the central section (leading/trailing sections are not scrolling)
9407static float TabBarCalcScrollableWidth(ImGuiTabBar* tab_bar, ImGuiTabBarSection* sections)
9408{
9409 return tab_bar->BarRect.GetWidth() - sections[0].Width - sections[2].Width - sections[1].Spacing;
9410}
9411
9412// This is called only once a frame before by the first call to ItemTab()
9413// The reason we're not calling it in BeginTabBar() is to leave a chance to the user to call the SetTabItemClosed() functions.
9414static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar)
9415{
9416 ImGuiContext& g = *GImGui;
9417 tab_bar->WantLayout = false;
9418
9419 // Garbage collect by compacting list
9420 // Detect if we need to sort out tab list (e.g. in rare case where a tab changed section)
9421 int tab_dst_n = 0;
9422 bool need_sort_by_section = false;
9423 ImGuiTabBarSection sections[3]; // Layout sections: Leading, Central, Trailing
9424 for (int tab_src_n = 0; tab_src_n < tab_bar->Tabs.Size; tab_src_n++)
9425 {
9426 ImGuiTabItem* tab = &tab_bar->Tabs[tab_src_n];
9427 if (tab->LastFrameVisible < tab_bar->PrevFrameVisible || tab->WantClose)
9428 {
9429 // Remove tab
9430 if (tab_bar->VisibleTabId == tab->ID) { tab_bar->VisibleTabId = 0; }
9431 if (tab_bar->SelectedTabId == tab->ID) { tab_bar->SelectedTabId = 0; }
9432 if (tab_bar->NextSelectedTabId == tab->ID) { tab_bar->NextSelectedTabId = 0; }
9433 continue;
9434 }
9435 if (tab_dst_n != tab_src_n)
9436 tab_bar->Tabs[tab_dst_n] = tab_bar->Tabs[tab_src_n];
9437
9438 tab = &tab_bar->Tabs[tab_dst_n];
9439 tab->IndexDuringLayout = (ImS16)tab_dst_n;
9440
9441 // We will need sorting if tabs have changed section (e.g. moved from one of Leading/Central/Trailing to another)
9442 int curr_tab_section_n = TabItemGetSectionIdx(tab);
9443 if (tab_dst_n > 0)
9444 {
9445 ImGuiTabItem* prev_tab = &tab_bar->Tabs[tab_dst_n - 1];
9446 int prev_tab_section_n = TabItemGetSectionIdx(tab: prev_tab);
9447 if (curr_tab_section_n == 0 && prev_tab_section_n != 0)
9448 need_sort_by_section = true;
9449 if (prev_tab_section_n == 2 && curr_tab_section_n != 2)
9450 need_sort_by_section = true;
9451 }
9452
9453 sections[curr_tab_section_n].TabCount++;
9454 tab_dst_n++;
9455 }
9456 if (tab_bar->Tabs.Size != tab_dst_n)
9457 tab_bar->Tabs.resize(new_size: tab_dst_n);
9458
9459 if (need_sort_by_section)
9460 ImQsort(base: tab_bar->Tabs.Data, count: tab_bar->Tabs.Size, size_of_element: sizeof(ImGuiTabItem), compare_func: TabItemComparerBySection);
9461
9462 // Calculate spacing between sections
9463 sections[0].Spacing = sections[0].TabCount > 0 && (sections[1].TabCount + sections[2].TabCount) > 0 ? g.Style.ItemInnerSpacing.x : 0.0f;
9464 sections[1].Spacing = sections[1].TabCount > 0 && sections[2].TabCount > 0 ? g.Style.ItemInnerSpacing.x : 0.0f;
9465
9466 // Setup next selected tab
9467 ImGuiID scroll_to_tab_id = 0;
9468 if (tab_bar->NextSelectedTabId)
9469 {
9470 tab_bar->SelectedTabId = tab_bar->NextSelectedTabId;
9471 tab_bar->NextSelectedTabId = 0;
9472 scroll_to_tab_id = tab_bar->SelectedTabId;
9473 }
9474
9475 // Process order change request (we could probably process it when requested but it's just saner to do it in a single spot).
9476 if (tab_bar->ReorderRequestTabId != 0)
9477 {
9478 if (TabBarProcessReorder(tab_bar))
9479 if (tab_bar->ReorderRequestTabId == tab_bar->SelectedTabId)
9480 scroll_to_tab_id = tab_bar->ReorderRequestTabId;
9481 tab_bar->ReorderRequestTabId = 0;
9482 }
9483
9484 // Tab List Popup (will alter tab_bar->BarRect and therefore the available width!)
9485 const bool tab_list_popup_button = (tab_bar->Flags & ImGuiTabBarFlags_TabListPopupButton) != 0;
9486 if (tab_list_popup_button)
9487 if (ImGuiTabItem* tab_to_select = TabBarTabListPopupButton(tab_bar)) // NB: Will alter BarRect.Min.x!
9488 scroll_to_tab_id = tab_bar->SelectedTabId = tab_to_select->ID;
9489
9490 // Leading/Trailing tabs will be shrink only if central one aren't visible anymore, so layout the shrink data as: leading, trailing, central
9491 // (whereas our tabs are stored as: leading, central, trailing)
9492 int shrink_buffer_indexes[3] = { 0, sections[0].TabCount + sections[2].TabCount, sections[0].TabCount };
9493 g.ShrinkWidthBuffer.resize(new_size: tab_bar->Tabs.Size);
9494
9495 // Compute ideal tabs widths + store them into shrink buffer
9496 ImGuiTabItem* most_recently_selected_tab = NULL;
9497 int curr_section_n = -1;
9498 bool found_selected_tab_id = false;
9499 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
9500 {
9501 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
9502 IM_ASSERT(tab->LastFrameVisible >= tab_bar->PrevFrameVisible);
9503
9504 if ((most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) && !(tab->Flags & ImGuiTabItemFlags_Button))
9505 most_recently_selected_tab = tab;
9506 if (tab->ID == tab_bar->SelectedTabId)
9507 found_selected_tab_id = true;
9508 if (scroll_to_tab_id == 0 && g.NavJustMovedToId == tab->ID)
9509 scroll_to_tab_id = tab->ID;
9510
9511 // Refresh tab width immediately, otherwise changes of style e.g. style.FramePadding.x would noticeably lag in the tab bar.
9512 // Additionally, when using TabBarAddTab() to manipulate tab bar order we occasionally insert new tabs that don't have a width yet,
9513 // and we cannot wait for the next BeginTabItem() call. We cannot compute this width within TabBarAddTab() because font size depends on the active window.
9514 const char* tab_name = TabBarGetTabName(tab_bar, tab);
9515 const bool has_close_button_or_unsaved_marker = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) == 0 || (tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
9516 tab->ContentWidth = (tab->RequestedWidth >= 0.0f) ? tab->RequestedWidth : TabItemCalcSize(label: tab_name, has_close_button_or_unsaved_marker).x;
9517
9518 int section_n = TabItemGetSectionIdx(tab);
9519 ImGuiTabBarSection* section = &sections[section_n];
9520 section->Width += tab->ContentWidth + (section_n == curr_section_n ? g.Style.ItemInnerSpacing.x : 0.0f);
9521 curr_section_n = section_n;
9522
9523 // Store data so we can build an array sorted by width if we need to shrink tabs down
9524 IM_MSVC_WARNING_SUPPRESS(6385);
9525 ImGuiShrinkWidthItem* shrink_width_item = &g.ShrinkWidthBuffer[shrink_buffer_indexes[section_n]++];
9526 shrink_width_item->Index = tab_n;
9527 shrink_width_item->Width = shrink_width_item->InitialWidth = tab->ContentWidth;
9528 tab->Width = ImMax(lhs: tab->ContentWidth, rhs: 1.0f);
9529 }
9530
9531 // Compute total ideal width (used for e.g. auto-resizing a window)
9532 tab_bar->WidthAllTabsIdeal = 0.0f;
9533 for (int section_n = 0; section_n < 3; section_n++)
9534 tab_bar->WidthAllTabsIdeal += sections[section_n].Width + sections[section_n].Spacing;
9535
9536 // Horizontal scrolling buttons
9537 // (note that TabBarScrollButtons() will alter BarRect.Max.x)
9538 if ((tab_bar->WidthAllTabsIdeal > tab_bar->BarRect.GetWidth() && tab_bar->Tabs.Size > 1) && !(tab_bar->Flags & ImGuiTabBarFlags_NoTabListScrollingButtons) && (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll))
9539 if (ImGuiTabItem* scroll_and_select_tab = TabBarScrollingButtons(tab_bar))
9540 {
9541 scroll_to_tab_id = scroll_and_select_tab->ID;
9542 if ((scroll_and_select_tab->Flags & ImGuiTabItemFlags_Button) == 0)
9543 tab_bar->SelectedTabId = scroll_to_tab_id;
9544 }
9545
9546 // Shrink widths if full tabs don't fit in their allocated space
9547 float section_0_w = sections[0].Width + sections[0].Spacing;
9548 float section_1_w = sections[1].Width + sections[1].Spacing;
9549 float section_2_w = sections[2].Width + sections[2].Spacing;
9550 bool central_section_is_visible = (section_0_w + section_2_w) < tab_bar->BarRect.GetWidth();
9551 float width_excess;
9552 if (central_section_is_visible)
9553 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
9554 else
9555 width_excess = (section_0_w + section_2_w) - tab_bar->BarRect.GetWidth(); // Excess used to shrink leading/trailing section
9556
9557 // With ImGuiTabBarFlags_FittingPolicyScroll policy, we will only shrink leading/trailing if the central section is not visible anymore
9558 if (width_excess >= 1.0f && ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyResizeDown) || !central_section_is_visible))
9559 {
9560 int shrink_data_count = (central_section_is_visible ? sections[1].TabCount : sections[0].TabCount + sections[2].TabCount);
9561 int shrink_data_offset = (central_section_is_visible ? sections[0].TabCount + sections[2].TabCount : 0);
9562 ShrinkWidths(items: g.ShrinkWidthBuffer.Data + shrink_data_offset, count: shrink_data_count, width_excess);
9563
9564 // Apply shrunk values into tabs and sections
9565 for (int tab_n = shrink_data_offset; tab_n < shrink_data_offset + shrink_data_count; tab_n++)
9566 {
9567 ImGuiTabItem* tab = &tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index];
9568 float shrinked_width = IM_TRUNC(g.ShrinkWidthBuffer[tab_n].Width);
9569 if (shrinked_width < 0.0f)
9570 continue;
9571
9572 shrinked_width = ImMax(lhs: 1.0f, rhs: shrinked_width);
9573 int section_n = TabItemGetSectionIdx(tab);
9574 sections[section_n].Width -= (tab->Width - shrinked_width);
9575 tab->Width = shrinked_width;
9576 }
9577 }
9578
9579 // Layout all active tabs
9580 int section_tab_index = 0;
9581 float tab_offset = 0.0f;
9582 tab_bar->WidthAllTabs = 0.0f;
9583 for (int section_n = 0; section_n < 3; section_n++)
9584 {
9585 ImGuiTabBarSection* section = &sections[section_n];
9586 if (section_n == 2)
9587 tab_offset = ImMin(lhs: ImMax(lhs: 0.0f, rhs: tab_bar->BarRect.GetWidth() - section->Width), rhs: tab_offset);
9588
9589 for (int tab_n = 0; tab_n < section->TabCount; tab_n++)
9590 {
9591 ImGuiTabItem* tab = &tab_bar->Tabs[section_tab_index + tab_n];
9592 tab->Offset = tab_offset;
9593 tab->NameOffset = -1;
9594 tab_offset += tab->Width + (tab_n < section->TabCount - 1 ? g.Style.ItemInnerSpacing.x : 0.0f);
9595 }
9596 tab_bar->WidthAllTabs += ImMax(lhs: section->Width + section->Spacing, rhs: 0.0f);
9597 tab_offset += section->Spacing;
9598 section_tab_index += section->TabCount;
9599 }
9600
9601 // Clear name buffers
9602 tab_bar->TabsNames.Buf.resize(new_size: 0);
9603
9604 // If we have lost the selected tab, select the next most recently active one
9605 if (found_selected_tab_id == false)
9606 tab_bar->SelectedTabId = 0;
9607 if (tab_bar->SelectedTabId == 0 && tab_bar->NextSelectedTabId == 0 && most_recently_selected_tab != NULL)
9608 scroll_to_tab_id = tab_bar->SelectedTabId = most_recently_selected_tab->ID;
9609
9610 // Lock in visible tab
9611 tab_bar->VisibleTabId = tab_bar->SelectedTabId;
9612 tab_bar->VisibleTabWasSubmitted = false;
9613
9614 // CTRL+TAB can override visible tab temporarily
9615 if (g.NavWindowingTarget != NULL && g.NavWindowingTarget->DockNode && g.NavWindowingTarget->DockNode->TabBar == tab_bar)
9616 tab_bar->VisibleTabId = scroll_to_tab_id = g.NavWindowingTarget->TabId;
9617
9618 // Apply request requests
9619 if (scroll_to_tab_id != 0)
9620 TabBarScrollToTab(tab_bar, tab_id: scroll_to_tab_id, sections);
9621 else if ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll) && IsMouseHoveringRect(r_min: tab_bar->BarRect.Min, r_max: tab_bar->BarRect.Max, clip: true) && IsWindowContentHoverable(window: g.CurrentWindow))
9622 {
9623 const float wheel = g.IO.MouseWheelRequestAxisSwap ? g.IO.MouseWheel : g.IO.MouseWheelH;
9624 const ImGuiKey wheel_key = g.IO.MouseWheelRequestAxisSwap ? ImGuiKey_MouseWheelY : ImGuiKey_MouseWheelX;
9625 if (TestKeyOwner(key: wheel_key, owner_id: tab_bar->ID) && wheel != 0.0f)
9626 {
9627 const float scroll_step = wheel * TabBarCalcScrollableWidth(tab_bar, sections) / 3.0f;
9628 tab_bar->ScrollingTargetDistToVisibility = 0.0f;
9629 tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, scrolling: tab_bar->ScrollingTarget - scroll_step);
9630 }
9631 SetKeyOwner(key: wheel_key, owner_id: tab_bar->ID);
9632 }
9633
9634 // Update scrolling
9635 tab_bar->ScrollingAnim = TabBarScrollClamp(tab_bar, scrolling: tab_bar->ScrollingAnim);
9636 tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, scrolling: tab_bar->ScrollingTarget);
9637 if (tab_bar->ScrollingAnim != tab_bar->ScrollingTarget)
9638 {
9639 // Scrolling speed adjust itself so we can always reach our target in 1/3 seconds.
9640 // Teleport if we are aiming far off the visible line
9641 tab_bar->ScrollingSpeed = ImMax(lhs: tab_bar->ScrollingSpeed, rhs: 70.0f * g.FontSize);
9642 tab_bar->ScrollingSpeed = ImMax(lhs: tab_bar->ScrollingSpeed, ImFabs(tab_bar->ScrollingTarget - tab_bar->ScrollingAnim) / 0.3f);
9643 const bool teleport = (tab_bar->PrevFrameVisible + 1 < g.FrameCount) || (tab_bar->ScrollingTargetDistToVisibility > 10.0f * g.FontSize);
9644 tab_bar->ScrollingAnim = teleport ? tab_bar->ScrollingTarget : ImLinearSweep(current: tab_bar->ScrollingAnim, target: tab_bar->ScrollingTarget, speed: g.IO.DeltaTime * tab_bar->ScrollingSpeed);
9645 }
9646 else
9647 {
9648 tab_bar->ScrollingSpeed = 0.0f;
9649 }
9650 tab_bar->ScrollingRectMinX = tab_bar->BarRect.Min.x + sections[0].Width + sections[0].Spacing;
9651 tab_bar->ScrollingRectMaxX = tab_bar->BarRect.Max.x - sections[2].Width - sections[1].Spacing;
9652
9653 // Actual layout in host window (we don't do it in BeginTabBar() so as not to waste an extra frame)
9654 ImGuiWindow* window = g.CurrentWindow;
9655 window->DC.CursorPos = tab_bar->BarRect.Min;
9656 ItemSize(size: ImVec2(tab_bar->WidthAllTabs, tab_bar->BarRect.GetHeight()), text_baseline_y: tab_bar->FramePadding.y);
9657 window->DC.IdealMaxPos.x = ImMax(lhs: window->DC.IdealMaxPos.x, rhs: tab_bar->BarRect.Min.x + tab_bar->WidthAllTabsIdeal);
9658}
9659
9660// Dockable windows uses Name/ID in the global namespace. Non-dockable items use the ID stack.
9661static ImU32 ImGui::TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window)
9662{
9663 if (docked_window != NULL)
9664 {
9665 IM_UNUSED(tab_bar);
9666 IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_DockNode);
9667 ImGuiID id = docked_window->TabId;
9668 KeepAliveID(id);
9669 return id;
9670 }
9671 else
9672 {
9673 ImGuiWindow* window = GImGui->CurrentWindow;
9674 return window->GetID(str: label);
9675 }
9676}
9677
9678static float ImGui::TabBarCalcMaxTabWidth()
9679{
9680 ImGuiContext& g = *GImGui;
9681 return g.FontSize * 20.0f;
9682}
9683
9684ImGuiTabItem* ImGui::TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id)
9685{
9686 if (tab_id != 0)
9687 for (int n = 0; n < tab_bar->Tabs.Size; n++)
9688 if (tab_bar->Tabs[n].ID == tab_id)
9689 return &tab_bar->Tabs[n];
9690 return NULL;
9691}
9692
9693// Order = visible order, not submission order! (which is tab->BeginOrder)
9694ImGuiTabItem* ImGui::TabBarFindTabByOrder(ImGuiTabBar* tab_bar, int order)
9695{
9696 if (order < 0 || order >= tab_bar->Tabs.Size)
9697 return NULL;
9698 return &tab_bar->Tabs[order];
9699}
9700
9701// FIXME: See references to #2304 in TODO.txt
9702ImGuiTabItem* ImGui::TabBarFindMostRecentlySelectedTabForActiveWindow(ImGuiTabBar* tab_bar)
9703{
9704 ImGuiTabItem* most_recently_selected_tab = NULL;
9705 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
9706 {
9707 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
9708 if (most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected)
9709 if (tab->Window && tab->Window->WasActive)
9710 most_recently_selected_tab = tab;
9711 }
9712 return most_recently_selected_tab;
9713}
9714
9715ImGuiTabItem* ImGui::TabBarGetCurrentTab(ImGuiTabBar* tab_bar)
9716{
9717 if (tab_bar->LastTabItemIdx < 0 || tab_bar->LastTabItemIdx >= tab_bar->Tabs.Size)
9718 return NULL;
9719 return &tab_bar->Tabs[tab_bar->LastTabItemIdx];
9720}
9721
9722const char* ImGui::TabBarGetTabName(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
9723{
9724 if (tab->Window)
9725 return tab->Window->Name;
9726 if (tab->NameOffset == -1)
9727 return "N/A";
9728 IM_ASSERT(tab->NameOffset < tab_bar->TabsNames.Buf.Size);
9729 return tab_bar->TabsNames.Buf.Data + tab->NameOffset;
9730}
9731
9732// The purpose of this call is to register tab in advance so we can control their order at the time they appear.
9733// Otherwise calling this is unnecessary as tabs are appending as needed by the BeginTabItem() function.
9734void ImGui::TabBarAddTab(ImGuiTabBar* tab_bar, ImGuiTabItemFlags tab_flags, ImGuiWindow* window)
9735{
9736 ImGuiContext& g = *GImGui;
9737 IM_ASSERT(TabBarFindTabByID(tab_bar, window->TabId) == NULL);
9738 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)
9739
9740 if (!window->HasCloseButton)
9741 tab_flags |= ImGuiTabItemFlags_NoCloseButton; // Set _NoCloseButton immediately because it will be used for first-frame width calculation.
9742
9743 ImGuiTabItem new_tab;
9744 new_tab.ID = window->TabId;
9745 new_tab.Flags = tab_flags;
9746 new_tab.LastFrameVisible = tab_bar->CurrFrameVisible; // Required so BeginTabBar() doesn't ditch the tab
9747 if (new_tab.LastFrameVisible == -1)
9748 new_tab.LastFrameVisible = g.FrameCount - 1;
9749 new_tab.Window = window; // Required so tab bar layout can compute the tab width before tab submission
9750 tab_bar->Tabs.push_back(v: new_tab);
9751}
9752
9753// The *TabId fields are already set by the docking system _before_ the actual TabItem was created, so we clear them regardless.
9754void ImGui::TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id)
9755{
9756 if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
9757 tab_bar->Tabs.erase(it: tab);
9758 if (tab_bar->VisibleTabId == tab_id) { tab_bar->VisibleTabId = 0; }
9759 if (tab_bar->SelectedTabId == tab_id) { tab_bar->SelectedTabId = 0; }
9760 if (tab_bar->NextSelectedTabId == tab_id) { tab_bar->NextSelectedTabId = 0; }
9761}
9762
9763// Called on manual closure attempt
9764void ImGui::TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
9765{
9766 if (tab->Flags & ImGuiTabItemFlags_Button)
9767 return; // A button appended with TabItemButton().
9768
9769 if ((tab->Flags & (ImGuiTabItemFlags_UnsavedDocument | ImGuiTabItemFlags_NoAssumedClosure)) == 0)
9770 {
9771 // This will remove a frame of lag for selecting another tab on closure.
9772 // 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
9773 tab->WantClose = true;
9774 if (tab_bar->VisibleTabId == tab->ID)
9775 {
9776 tab->LastFrameVisible = -1;
9777 tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = 0;
9778 }
9779 }
9780 else
9781 {
9782 // Actually select before expecting closure attempt (on an UnsavedDocument tab user is expect to e.g. show a popup)
9783 if (tab_bar->VisibleTabId != tab->ID)
9784 TabBarQueueFocus(tab_bar, tab);
9785 }
9786}
9787
9788static float ImGui::TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling)
9789{
9790 scrolling = ImMin(lhs: scrolling, rhs: tab_bar->WidthAllTabs - tab_bar->BarRect.GetWidth());
9791 return ImMax(lhs: scrolling, rhs: 0.0f);
9792}
9793
9794// Note: we may scroll to tab that are not selected! e.g. using keyboard arrow keys
9795static void ImGui::TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections)
9796{
9797 ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id);
9798 if (tab == NULL)
9799 return;
9800 if (tab->Flags & ImGuiTabItemFlags_SectionMask_)
9801 return;
9802
9803 ImGuiContext& g = *GImGui;
9804 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)
9805 int order = TabBarGetTabOrder(tab_bar, tab);
9806
9807 // Scrolling happens only in the central section (leading/trailing sections are not scrolling)
9808 float scrollable_width = TabBarCalcScrollableWidth(tab_bar, sections);
9809
9810 // We make all tabs positions all relative Sections[0].Width to make code simpler
9811 float tab_x1 = tab->Offset - sections[0].Width + (order > sections[0].TabCount - 1 ? -margin : 0.0f);
9812 float tab_x2 = tab->Offset - sections[0].Width + tab->Width + (order + 1 < tab_bar->Tabs.Size - sections[2].TabCount ? margin : 1.0f);
9813 tab_bar->ScrollingTargetDistToVisibility = 0.0f;
9814 if (tab_bar->ScrollingTarget > tab_x1 || (tab_x2 - tab_x1 >= scrollable_width))
9815 {
9816 // Scroll to the left
9817 tab_bar->ScrollingTargetDistToVisibility = ImMax(lhs: tab_bar->ScrollingAnim - tab_x2, rhs: 0.0f);
9818 tab_bar->ScrollingTarget = tab_x1;
9819 }
9820 else if (tab_bar->ScrollingTarget < tab_x2 - scrollable_width)
9821 {
9822 // Scroll to the right
9823 tab_bar->ScrollingTargetDistToVisibility = ImMax(lhs: (tab_x1 - scrollable_width) - tab_bar->ScrollingAnim, rhs: 0.0f);
9824 tab_bar->ScrollingTarget = tab_x2 - scrollable_width;
9825 }
9826}
9827
9828void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
9829{
9830 tab_bar->NextSelectedTabId = tab->ID;
9831}
9832
9833void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, const char* tab_name)
9834{
9835 IM_ASSERT((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0); // Only supported for manual/explicit tab bars
9836 ImGuiID tab_id = TabBarCalcTabID(tab_bar, label: tab_name, NULL);
9837 tab_bar->NextSelectedTabId = tab_id;
9838}
9839
9840void ImGui::TabBarQueueReorder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab, int offset)
9841{
9842 IM_ASSERT(offset != 0);
9843 IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
9844 tab_bar->ReorderRequestTabId = tab->ID;
9845 tab_bar->ReorderRequestOffset = (ImS16)offset;
9846}
9847
9848void ImGui::TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, ImGuiTabItem* src_tab, ImVec2 mouse_pos)
9849{
9850 ImGuiContext& g = *GImGui;
9851 IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
9852 if ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) == 0)
9853 return;
9854
9855 const bool is_central_section = (src_tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
9856 const float bar_offset = tab_bar->BarRect.Min.x - (is_central_section ? tab_bar->ScrollingTarget : 0);
9857
9858 // Count number of contiguous tabs we are crossing over
9859 const int dir = (bar_offset + src_tab->Offset) > mouse_pos.x ? -1 : +1;
9860 const int src_idx = tab_bar->Tabs.index_from_ptr(it: src_tab);
9861 int dst_idx = src_idx;
9862 for (int i = src_idx; i >= 0 && i < tab_bar->Tabs.Size; i += dir)
9863 {
9864 // Reordered tabs must share the same section
9865 const ImGuiTabItem* dst_tab = &tab_bar->Tabs[i];
9866 if (dst_tab->Flags & ImGuiTabItemFlags_NoReorder)
9867 break;
9868 if ((dst_tab->Flags & ImGuiTabItemFlags_SectionMask_) != (src_tab->Flags & ImGuiTabItemFlags_SectionMask_))
9869 break;
9870 dst_idx = i;
9871
9872 // Include spacing after tab, so when mouse cursor is between tabs we would not continue checking further tabs that are not hovered.
9873 const float x1 = bar_offset + dst_tab->Offset - g.Style.ItemInnerSpacing.x;
9874 const float x2 = bar_offset + dst_tab->Offset + dst_tab->Width + g.Style.ItemInnerSpacing.x;
9875 //GetForegroundDrawList()->AddRect(ImVec2(x1, tab_bar->BarRect.Min.y), ImVec2(x2, tab_bar->BarRect.Max.y), IM_COL32(255, 0, 0, 255));
9876 if ((dir < 0 && mouse_pos.x > x1) || (dir > 0 && mouse_pos.x < x2))
9877 break;
9878 }
9879
9880 if (dst_idx != src_idx)
9881 TabBarQueueReorder(tab_bar, tab: src_tab, offset: dst_idx - src_idx);
9882}
9883
9884bool ImGui::TabBarProcessReorder(ImGuiTabBar* tab_bar)
9885{
9886 ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_id: tab_bar->ReorderRequestTabId);
9887 if (tab1 == NULL || (tab1->Flags & ImGuiTabItemFlags_NoReorder))
9888 return false;
9889
9890 //IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_Reorderable); // <- this may happen when using debug tools
9891 int tab2_order = TabBarGetTabOrder(tab_bar, tab: tab1) + tab_bar->ReorderRequestOffset;
9892 if (tab2_order < 0 || tab2_order >= tab_bar->Tabs.Size)
9893 return false;
9894
9895 // Reordered tabs must share the same section
9896 // (Note: TabBarQueueReorderFromMousePos() also has a similar test but since we allow direct calls to TabBarQueueReorder() we do it here too)
9897 ImGuiTabItem* tab2 = &tab_bar->Tabs[tab2_order];
9898 if (tab2->Flags & ImGuiTabItemFlags_NoReorder)
9899 return false;
9900 if ((tab1->Flags & ImGuiTabItemFlags_SectionMask_) != (tab2->Flags & ImGuiTabItemFlags_SectionMask_))
9901 return false;
9902
9903 ImGuiTabItem item_tmp = *tab1;
9904 ImGuiTabItem* src_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 + 1 : tab2;
9905 ImGuiTabItem* dst_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 : tab2 + 1;
9906 const int move_count = (tab_bar->ReorderRequestOffset > 0) ? tab_bar->ReorderRequestOffset : -tab_bar->ReorderRequestOffset;
9907 memmove(dest: dst_tab, src: src_tab, n: move_count * sizeof(ImGuiTabItem));
9908 *tab2 = item_tmp;
9909
9910 if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings)
9911 MarkIniSettingsDirty();
9912 return true;
9913}
9914
9915static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar)
9916{
9917 ImGuiContext& g = *GImGui;
9918 ImGuiWindow* window = g.CurrentWindow;
9919
9920 const ImVec2 arrow_button_size(g.FontSize - 2.0f, g.FontSize + g.Style.FramePadding.y * 2.0f);
9921 const float scrolling_buttons_width = arrow_button_size.x * 2.0f;
9922
9923 const ImVec2 backup_cursor_pos = window->DC.CursorPos;
9924 //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));
9925
9926 int select_dir = 0;
9927 ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
9928 arrow_col.w *= 0.5f;
9929
9930 PushStyleColor(idx: ImGuiCol_Text, col: arrow_col);
9931 PushStyleColor(idx: ImGuiCol_Button, col: ImVec4(0, 0, 0, 0));
9932 PushItemFlag(option: ImGuiItemFlags_ButtonRepeat, enabled: true);
9933 const float backup_repeat_delay = g.IO.KeyRepeatDelay;
9934 const float backup_repeat_rate = g.IO.KeyRepeatRate;
9935 g.IO.KeyRepeatDelay = 0.250f;
9936 g.IO.KeyRepeatRate = 0.200f;
9937 float x = ImMax(lhs: tab_bar->BarRect.Min.x, rhs: tab_bar->BarRect.Max.x - scrolling_buttons_width);
9938 window->DC.CursorPos = ImVec2(x, tab_bar->BarRect.Min.y);
9939 if (ArrowButtonEx(str_id: "##<", dir: ImGuiDir_Left, size: arrow_button_size, flags: ImGuiButtonFlags_PressedOnClick))
9940 select_dir = -1;
9941 window->DC.CursorPos = ImVec2(x + arrow_button_size.x, tab_bar->BarRect.Min.y);
9942 if (ArrowButtonEx(str_id: "##>", dir: ImGuiDir_Right, size: arrow_button_size, flags: ImGuiButtonFlags_PressedOnClick))
9943 select_dir = +1;
9944 PopItemFlag();
9945 PopStyleColor(count: 2);
9946 g.IO.KeyRepeatRate = backup_repeat_rate;
9947 g.IO.KeyRepeatDelay = backup_repeat_delay;
9948
9949 ImGuiTabItem* tab_to_scroll_to = NULL;
9950 if (select_dir != 0)
9951 if (ImGuiTabItem* tab_item = TabBarFindTabByID(tab_bar, tab_id: tab_bar->SelectedTabId))
9952 {
9953 int selected_order = TabBarGetTabOrder(tab_bar, tab: tab_item);
9954 int target_order = selected_order + select_dir;
9955
9956 // Skip tab item buttons until another tab item is found or end is reached
9957 while (tab_to_scroll_to == NULL)
9958 {
9959 // If we are at the end of the list, still scroll to make our tab visible
9960 tab_to_scroll_to = &tab_bar->Tabs[(target_order >= 0 && target_order < tab_bar->Tabs.Size) ? target_order : selected_order];
9961
9962 // Cross through buttons
9963 // (even if first/last item is a button, return it so we can update the scroll)
9964 if (tab_to_scroll_to->Flags & ImGuiTabItemFlags_Button)
9965 {
9966 target_order += select_dir;
9967 selected_order += select_dir;
9968 tab_to_scroll_to = (target_order < 0 || target_order >= tab_bar->Tabs.Size) ? tab_to_scroll_to : NULL;
9969 }
9970 }
9971 }
9972 window->DC.CursorPos = backup_cursor_pos;
9973 tab_bar->BarRect.Max.x -= scrolling_buttons_width + 1.0f;
9974
9975 return tab_to_scroll_to;
9976}
9977
9978static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar)
9979{
9980 ImGuiContext& g = *GImGui;
9981 ImGuiWindow* window = g.CurrentWindow;
9982
9983 // We use g.Style.FramePadding.y to match the square ArrowButton size
9984 const float tab_list_popup_button_width = g.FontSize + g.Style.FramePadding.y;
9985 const ImVec2 backup_cursor_pos = window->DC.CursorPos;
9986 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x - g.Style.FramePadding.y, tab_bar->BarRect.Min.y);
9987 tab_bar->BarRect.Min.x += tab_list_popup_button_width;
9988
9989 ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
9990 arrow_col.w *= 0.5f;
9991 PushStyleColor(idx: ImGuiCol_Text, col: arrow_col);
9992 PushStyleColor(idx: ImGuiCol_Button, col: ImVec4(0, 0, 0, 0));
9993 bool open = BeginCombo(label: "##v", NULL, flags: ImGuiComboFlags_NoPreview | ImGuiComboFlags_HeightLargest);
9994 PopStyleColor(count: 2);
9995
9996 ImGuiTabItem* tab_to_select = NULL;
9997 if (open)
9998 {
9999 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
10000 {
10001 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
10002 if (tab->Flags & ImGuiTabItemFlags_Button)
10003 continue;
10004
10005 const char* tab_name = TabBarGetTabName(tab_bar, tab);
10006 if (Selectable(label: tab_name, selected: tab_bar->SelectedTabId == tab->ID))
10007 tab_to_select = tab;
10008 }
10009 EndCombo();
10010 }
10011
10012 window->DC.CursorPos = backup_cursor_pos;
10013 return tab_to_select;
10014}
10015
10016//-------------------------------------------------------------------------
10017// [SECTION] Widgets: BeginTabItem, EndTabItem, etc.
10018//-------------------------------------------------------------------------
10019// - BeginTabItem()
10020// - EndTabItem()
10021// - TabItemButton()
10022// - TabItemEx() [Internal]
10023// - SetTabItemClosed()
10024// - TabItemCalcSize() [Internal]
10025// - TabItemBackground() [Internal]
10026// - TabItemLabelAndCloseButton() [Internal]
10027//-------------------------------------------------------------------------
10028
10029bool ImGui::BeginTabItem(const char* label, bool* p_open, ImGuiTabItemFlags flags)
10030{
10031 ImGuiContext& g = *GImGui;
10032 ImGuiWindow* window = g.CurrentWindow;
10033 if (window->SkipItems)
10034 return false;
10035
10036 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10037 if (tab_bar == NULL)
10038 {
10039 IM_ASSERT_USER_ERROR(tab_bar, "Needs to be called between BeginTabBar() and EndTabBar()!");
10040 return false;
10041 }
10042 IM_ASSERT((flags & ImGuiTabItemFlags_Button) == 0); // BeginTabItem() Can't be used with button flags, use TabItemButton() instead!
10043
10044 bool ret = TabItemEx(tab_bar, label, p_open, flags, NULL);
10045 if (ret && !(flags & ImGuiTabItemFlags_NoPushId))
10046 {
10047 ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
10048 PushOverrideID(id: tab->ID); // We already hashed 'label' so push into the ID stack directly instead of doing another hash through PushID(label)
10049 }
10050 return ret;
10051}
10052
10053void ImGui::EndTabItem()
10054{
10055 ImGuiContext& g = *GImGui;
10056 ImGuiWindow* window = g.CurrentWindow;
10057 if (window->SkipItems)
10058 return;
10059
10060 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10061 if (tab_bar == NULL)
10062 {
10063 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
10064 return;
10065 }
10066 IM_ASSERT(tab_bar->LastTabItemIdx >= 0);
10067 ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
10068 if (!(tab->Flags & ImGuiTabItemFlags_NoPushId))
10069 PopID();
10070}
10071
10072bool ImGui::TabItemButton(const char* label, ImGuiTabItemFlags flags)
10073{
10074 ImGuiContext& g = *GImGui;
10075 ImGuiWindow* window = g.CurrentWindow;
10076 if (window->SkipItems)
10077 return false;
10078
10079 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10080 if (tab_bar == NULL)
10081 {
10082 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
10083 return false;
10084 }
10085 return TabItemEx(tab_bar, label, NULL, flags: flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder, NULL);
10086}
10087
10088void ImGui::TabItemSpacing(const char* str_id, ImGuiTabItemFlags flags, float width)
10089{
10090 ImGuiContext& g = *GImGui;
10091 ImGuiWindow* window = g.CurrentWindow;
10092 if (window->SkipItems)
10093 return;
10094
10095 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10096 if (tab_bar == NULL)
10097 {
10098 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
10099 return;
10100 }
10101 SetNextItemWidth(width);
10102 TabItemEx(tab_bar, label: str_id, NULL, flags: flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder | ImGuiTabItemFlags_Invisible, NULL);
10103}
10104
10105bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags, ImGuiWindow* docked_window)
10106{
10107 // Layout whole tab bar if not already done
10108 ImGuiContext& g = *GImGui;
10109 if (tab_bar->WantLayout)
10110 {
10111 ImGuiNextItemData backup_next_item_data = g.NextItemData;
10112 TabBarLayout(tab_bar);
10113 g.NextItemData = backup_next_item_data;
10114 }
10115 ImGuiWindow* window = g.CurrentWindow;
10116 if (window->SkipItems)
10117 return false;
10118
10119 const ImGuiStyle& style = g.Style;
10120 const ImGuiID id = TabBarCalcTabID(tab_bar, label, docked_window);
10121
10122 // If the user called us with *p_open == false, we early out and don't render.
10123 // 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.
10124 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
10125 if (p_open && !*p_open)
10126 {
10127 ItemAdd(bb: ImRect(), id, NULL, extra_flags: ImGuiItemFlags_NoNav);
10128 return false;
10129 }
10130
10131 IM_ASSERT(!p_open || !(flags & ImGuiTabItemFlags_Button));
10132 IM_ASSERT((flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)) != (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)); // Can't use both Leading and Trailing
10133
10134 // Store into ImGuiTabItemFlags_NoCloseButton, also honor ImGuiTabItemFlags_NoCloseButton passed by user (although not documented)
10135 if (flags & ImGuiTabItemFlags_NoCloseButton)
10136 p_open = NULL;
10137 else if (p_open == NULL)
10138 flags |= ImGuiTabItemFlags_NoCloseButton;
10139
10140 // Acquire tab data
10141 ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id: id);
10142 bool tab_is_new = false;
10143 if (tab == NULL)
10144 {
10145 tab_bar->Tabs.push_back(v: ImGuiTabItem());
10146 tab = &tab_bar->Tabs.back();
10147 tab->ID = id;
10148 tab_bar->TabsAddedNew = tab_is_new = true;
10149 }
10150 tab_bar->LastTabItemIdx = (ImS16)tab_bar->Tabs.index_from_ptr(it: tab);
10151
10152 // Calculate tab contents size
10153 ImVec2 size = TabItemCalcSize(label, has_close_button_or_unsaved_marker: (p_open != NULL) || (flags & ImGuiTabItemFlags_UnsavedDocument));
10154 tab->RequestedWidth = -1.0f;
10155 if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasWidth)
10156 size.x = tab->RequestedWidth = g.NextItemData.Width;
10157 if (tab_is_new)
10158 tab->Width = ImMax(lhs: 1.0f, rhs: size.x);
10159 tab->ContentWidth = size.x;
10160 tab->BeginOrder = tab_bar->TabsActiveCount++;
10161
10162 const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
10163 const bool tab_bar_focused = (tab_bar->Flags & ImGuiTabBarFlags_IsFocused) != 0;
10164 const bool tab_appearing = (tab->LastFrameVisible + 1 < g.FrameCount);
10165 const bool tab_just_unsaved = (flags & ImGuiTabItemFlags_UnsavedDocument) && !(tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
10166 const bool is_tab_button = (flags & ImGuiTabItemFlags_Button) != 0;
10167 tab->LastFrameVisible = g.FrameCount;
10168 tab->Flags = flags;
10169 tab->Window = docked_window;
10170
10171 // Append name _WITH_ the zero-terminator
10172 // (regular tabs are permitted in a DockNode tab bar, but window tabs not permitted in a non-DockNode tab bar)
10173 if (docked_window != NULL)
10174 {
10175 IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_DockNode);
10176 tab->NameOffset = -1;
10177 }
10178 else
10179 {
10180 tab->NameOffset = (ImS32)tab_bar->TabsNames.size();
10181 tab_bar->TabsNames.append(str: label, str_end: label + ImStrlen(s: label) + 1);
10182 }
10183
10184 // Update selected tab
10185 if (!is_tab_button)
10186 {
10187 if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0)
10188 if (!tab_bar_appearing || tab_bar->SelectedTabId == 0)
10189 TabBarQueueFocus(tab_bar, tab); // New tabs gets activated
10190 if ((flags & ImGuiTabItemFlags_SetSelected) && (tab_bar->SelectedTabId != id)) // _SetSelected can only be passed on explicit tab bar
10191 TabBarQueueFocus(tab_bar, tab);
10192 }
10193
10194 // Lock visibility
10195 // (Note: tab_contents_visible != tab_selected... because CTRL+TAB operations may preview some tabs without selecting them!)
10196 bool tab_contents_visible = (tab_bar->VisibleTabId == id);
10197 if (tab_contents_visible)
10198 tab_bar->VisibleTabWasSubmitted = true;
10199
10200 // On the very first frame of a tab bar we let first tab contents be visible to minimize appearing glitches
10201 if (!tab_contents_visible && tab_bar->SelectedTabId == 0 && tab_bar_appearing && docked_window == NULL)
10202 if (tab_bar->Tabs.Size == 1 && !(tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs))
10203 tab_contents_visible = true;
10204
10205 // Note that tab_is_new is not necessarily the same as tab_appearing! When a tab bar stops being submitted
10206 // and then gets submitted again, the tabs will have 'tab_appearing=true' but 'tab_is_new=false'.
10207 if (tab_appearing && (!tab_bar_appearing || tab_is_new))
10208 {
10209 ItemAdd(bb: ImRect(), id, NULL, extra_flags: ImGuiItemFlags_NoNav);
10210 if (is_tab_button)
10211 return false;
10212 return tab_contents_visible;
10213 }
10214
10215 if (tab_bar->SelectedTabId == id)
10216 tab->LastFrameSelected = g.FrameCount;
10217
10218 // Backup current layout position
10219 const ImVec2 backup_main_cursor_pos = window->DC.CursorPos;
10220
10221 // Layout
10222 const bool is_central_section = (tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
10223 size.x = tab->Width;
10224 if (is_central_section)
10225 window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_TRUNC(tab->Offset - tab_bar->ScrollingAnim), 0.0f);
10226 else
10227 window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(tab->Offset, 0.0f);
10228 ImVec2 pos = window->DC.CursorPos;
10229 ImRect bb(pos, pos + size);
10230
10231 // 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)
10232 const bool want_clip_rect = is_central_section && (bb.Min.x < tab_bar->ScrollingRectMinX || bb.Max.x > tab_bar->ScrollingRectMaxX);
10233 if (want_clip_rect)
10234 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);
10235
10236 ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos;
10237 ItemSize(size: bb.GetSize(), text_baseline_y: style.FramePadding.y);
10238 window->DC.CursorMaxPos = backup_cursor_max_pos;
10239
10240 if (!ItemAdd(bb, id))
10241 {
10242 if (want_clip_rect)
10243 PopClipRect();
10244 window->DC.CursorPos = backup_main_cursor_pos;
10245 return tab_contents_visible;
10246 }
10247
10248 // Click to Select a tab
10249 ImGuiButtonFlags button_flags = ((is_tab_button ? ImGuiButtonFlags_PressedOnClickRelease : ImGuiButtonFlags_PressedOnClick) | ImGuiButtonFlags_AllowOverlap);
10250 if (g.DragDropActive && !g.DragDropPayload.IsDataType(IMGUI_PAYLOAD_TYPE_WINDOW)) // FIXME: May be an opt-in property of the payload to disable this
10251 button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
10252 bool hovered, held, pressed;
10253 if (flags & ImGuiTabItemFlags_Invisible)
10254 hovered = held = pressed = false;
10255 else
10256 pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags: button_flags);
10257 if (pressed && !is_tab_button)
10258 TabBarQueueFocus(tab_bar, tab);
10259
10260 // Transfer active id window so the active id is not owned by the dock host (as StartMouseMovingWindow()
10261 // will only do it on the drag). This allows FocusWindow() to be more conservative in how it clears active id.
10262 if (held && docked_window && g.ActiveId == id && g.ActiveIdIsJustActivated)
10263 g.ActiveIdWindow = docked_window;
10264
10265 // Drag and drop a single floating window node moves it
10266 ImGuiDockNode* node = docked_window ? docked_window->DockNode : NULL;
10267 const bool single_floating_window_node = node && node->IsFloatingNode() && (node->Windows.Size == 1);
10268 if (held && single_floating_window_node && IsMouseDragging(button: 0, lock_threshold: 0.0f))
10269 {
10270 // Move
10271 StartMouseMovingWindow(window: docked_window);
10272 }
10273 else if (held && !tab_appearing && IsMouseDragging(button: 0))
10274 {
10275 // Drag and drop: re-order tabs
10276 int drag_dir = 0;
10277 float drag_distance_from_edge_x = 0.0f;
10278 if (!g.DragDropActive && ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (docked_window != NULL)))
10279 {
10280 // While moving a tab it will jump on the other side of the mouse, so we also test for MouseDelta.x
10281 if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < bb.Min.x)
10282 {
10283 drag_dir = -1;
10284 drag_distance_from_edge_x = bb.Min.x - g.IO.MousePos.x;
10285 TabBarQueueReorderFromMousePos(tab_bar, src_tab: tab, mouse_pos: g.IO.MousePos);
10286 }
10287 else if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > bb.Max.x)
10288 {
10289 drag_dir = +1;
10290 drag_distance_from_edge_x = g.IO.MousePos.x - bb.Max.x;
10291 TabBarQueueReorderFromMousePos(tab_bar, src_tab: tab, mouse_pos: g.IO.MousePos);
10292 }
10293 }
10294
10295 // Extract a Dockable window out of it's tab bar
10296 const bool can_undock = docked_window != NULL && !(docked_window->Flags & ImGuiWindowFlags_NoMove) && !(node->MergedFlags & ImGuiDockNodeFlags_NoUndocking);
10297 if (can_undock)
10298 {
10299 // We use a variable threshold to distinguish dragging tabs within a tab bar and extracting them out of the tab bar
10300 bool undocking_tab = (g.DragDropActive && g.DragDropPayload.SourceId == id);
10301 if (!undocking_tab) //&& (!g.IO.ConfigDockingWithShift || g.IO.KeyShift)
10302 {
10303 float threshold_base = g.FontSize;
10304 float threshold_x = (threshold_base * 2.2f);
10305 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);
10306 //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]
10307
10308 float distance_from_edge_y = ImMax(lhs: bb.Min.y - g.IO.MousePos.y, rhs: g.IO.MousePos.y - bb.Max.y);
10309 if (distance_from_edge_y >= threshold_y)
10310 undocking_tab = true;
10311 if (drag_distance_from_edge_x > threshold_x)
10312 if ((drag_dir < 0 && TabBarGetTabOrder(tab_bar, tab) == 0) || (drag_dir > 0 && TabBarGetTabOrder(tab_bar, tab) == tab_bar->Tabs.Size - 1))
10313 undocking_tab = true;
10314 }
10315
10316 if (undocking_tab)
10317 {
10318 // Undock
10319 // FIXME: refactor to share more code with e.g. StartMouseMovingWindow
10320 DockContextQueueUndockWindow(ctx: &g, window: docked_window);
10321 g.MovingWindow = docked_window;
10322 SetActiveID(id: g.MovingWindow->MoveId, window: g.MovingWindow);
10323 g.ActiveIdClickOffset -= g.MovingWindow->Pos - bb.Min;
10324 g.ActiveIdNoClearOnFocusLoss = true;
10325 SetActiveIdUsingAllKeyboardKeys();
10326 }
10327 }
10328 }
10329
10330#if 0
10331 if (hovered && g.HoveredIdNotActiveTimer > TOOLTIP_DELAY && bb.GetWidth() < tab->ContentWidth)
10332 {
10333 // Enlarge tab display when hovering
10334 bb.Max.x = bb.Min.x + IM_TRUNC(ImLerp(bb.GetWidth(), tab->ContentWidth, ImSaturate((g.HoveredIdNotActiveTimer - 0.40f) * 6.0f)));
10335 display_draw_list = GetForegroundDrawList(window);
10336 TabItemBackground(display_draw_list, bb, flags, GetColorU32(ImGuiCol_TitleBgActive));
10337 }
10338#endif
10339
10340 // Render tab shape
10341 const bool is_visible = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) && !(flags & ImGuiTabItemFlags_Invisible);
10342 if (is_visible)
10343 {
10344 ImDrawList* display_draw_list = window->DrawList;
10345 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));
10346 TabItemBackground(draw_list: display_draw_list, bb, flags, col: tab_col);
10347 if (tab_contents_visible && (tab_bar->Flags & ImGuiTabBarFlags_DrawSelectedOverline) && style.TabBarOverlineSize > 0.0f)
10348 {
10349 // Might be moved to TabItemBackground() ?
10350 ImVec2 tl = bb.GetTL() + ImVec2(0, 1.0f * g.CurrentDpiScale);
10351 ImVec2 tr = bb.GetTR() + ImVec2(0, 1.0f * g.CurrentDpiScale);
10352 ImU32 overline_col = GetColorU32(idx: tab_bar_focused ? ImGuiCol_TabSelectedOverline : ImGuiCol_TabDimmedSelectedOverline);
10353 if (style.TabRounding > 0.0f)
10354 {
10355 float rounding = style.TabRounding;
10356 display_draw_list->PathArcToFast(center: tl + ImVec2(+rounding, +rounding), radius: rounding, a_min_of_12: 7, a_max_of_12: 9);
10357 display_draw_list->PathArcToFast(center: tr + ImVec2(-rounding, +rounding), radius: rounding, a_min_of_12: 9, a_max_of_12: 11);
10358 display_draw_list->PathStroke(col: overline_col, flags: 0, thickness: style.TabBarOverlineSize);
10359 }
10360 else
10361 {
10362 display_draw_list->AddLine(p1: tl - ImVec2(0.5f, 0.5f), p2: tr - ImVec2(0.5f, 0.5f), col: overline_col, thickness: style.TabBarOverlineSize);
10363 }
10364 }
10365 RenderNavCursor(bb, id);
10366
10367 // Select with right mouse button. This is so the common idiom for context menu automatically highlight the current widget.
10368 const bool hovered_unblocked = IsItemHovered(flags: ImGuiHoveredFlags_AllowWhenBlockedByPopup);
10369 if (tab_bar->SelectedTabId != tab->ID && hovered_unblocked && (IsMouseClicked(button: 1) || IsMouseReleased(button: 1)) && !is_tab_button)
10370 TabBarQueueFocus(tab_bar, tab);
10371
10372 if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)
10373 flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton;
10374
10375 // Render tab label, process close button
10376 const ImGuiID close_button_id = p_open ? GetIDWithSeed(str_id_begin: "#CLOSE", NULL, seed: docked_window ? docked_window->ID : id) : 0;
10377 bool just_closed;
10378 bool text_clipped;
10379 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);
10380 if (just_closed && p_open != NULL)
10381 {
10382 *p_open = false;
10383 TabBarCloseTab(tab_bar, tab);
10384 }
10385
10386 // Forward Hovered state so IsItemHovered() after Begin() can work (even though we are technically hovering our parent)
10387 // That state is copied to window->DockTabItemStatusFlags by our caller.
10388 if (docked_window && (hovered || g.HoveredId == close_button_id))
10389 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow;
10390
10391 // Tooltip
10392 // (Won't work over the close button because ItemOverlap systems messes up with HoveredIdTimer-> seems ok)
10393 // (We test IsItemHovered() to discard e.g. when another item is active or drag and drop over the tab bar, which g.HoveredId ignores)
10394 // FIXME: This is a mess.
10395 // FIXME: We may want disabled tab to still display the tooltip?
10396 if (text_clipped && g.HoveredId == id && !held)
10397 if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip) && !(tab->Flags & ImGuiTabItemFlags_NoTooltip))
10398 SetItemTooltip("%.*s", (int)(FindRenderedTextEnd(text: label) - label), label);
10399 }
10400
10401 // Restore main window position so user can draw there
10402 if (want_clip_rect)
10403 PopClipRect();
10404 window->DC.CursorPos = backup_main_cursor_pos;
10405
10406 IM_ASSERT(!is_tab_button || !(tab_bar->SelectedTabId == tab->ID && is_tab_button)); // TabItemButton should not be selected
10407 if (is_tab_button)
10408 return pressed;
10409 return tab_contents_visible;
10410}
10411
10412// [Public] This is call is 100% optional but it allows to remove some one-frame glitches when a tab has been unexpectedly removed.
10413// To use it to need to call the function SetTabItemClosed() between BeginTabBar() and EndTabBar().
10414// Tabs closed by the close button will automatically be flagged to avoid this issue.
10415void ImGui::SetTabItemClosed(const char* label)
10416{
10417 ImGuiContext& g = *GImGui;
10418 bool is_within_manual_tab_bar = g.CurrentTabBar && !(g.CurrentTabBar->Flags & ImGuiTabBarFlags_DockNode);
10419 if (is_within_manual_tab_bar)
10420 {
10421 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10422 ImGuiID tab_id = TabBarCalcTabID(tab_bar, label, NULL);
10423 if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
10424 tab->WantClose = true; // Will be processed by next call to TabBarLayout()
10425 }
10426 else if (ImGuiWindow* window = FindWindowByName(name: label))
10427 {
10428 if (window->DockIsActive)
10429 if (ImGuiDockNode* node = window->DockNode)
10430 {
10431 ImGuiID tab_id = TabBarCalcTabID(tab_bar: node->TabBar, label, docked_window: window);
10432 TabBarRemoveTab(tab_bar: node->TabBar, tab_id);
10433 window->DockTabWantClose = true;
10434 }
10435 }
10436}
10437
10438ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button_or_unsaved_marker)
10439{
10440 ImGuiContext& g = *GImGui;
10441 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
10442 ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x, label_size.y + g.Style.FramePadding.y * 2.0f);
10443 if (has_close_button_or_unsaved_marker)
10444 size.x += g.Style.FramePadding.x + (g.Style.ItemInnerSpacing.x + g.FontSize); // We use Y intentionally to fit the close button circle.
10445 else
10446 size.x += g.Style.FramePadding.x + 1.0f;
10447 return ImVec2(ImMin(lhs: size.x, rhs: TabBarCalcMaxTabWidth()), size.y);
10448}
10449
10450ImVec2 ImGui::TabItemCalcSize(ImGuiWindow* window)
10451{
10452 return TabItemCalcSize(label: window->Name, has_close_button_or_unsaved_marker: window->HasCloseButton || (window->Flags & ImGuiWindowFlags_UnsavedDocument));
10453}
10454
10455void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImU32 col)
10456{
10457 // 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.
10458 ImGuiContext& g = *GImGui;
10459 const float width = bb.GetWidth();
10460 IM_UNUSED(flags);
10461 IM_ASSERT(width > 0.0f);
10462 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));
10463 const float y1 = bb.Min.y + 1.0f;
10464 const float y2 = bb.Max.y - g.Style.TabBarBorderSize;
10465 draw_list->PathLineTo(pos: ImVec2(bb.Min.x, y2));
10466 draw_list->PathArcToFast(center: ImVec2(bb.Min.x + rounding, y1 + rounding), radius: rounding, a_min_of_12: 6, a_max_of_12: 9);
10467 draw_list->PathArcToFast(center: ImVec2(bb.Max.x - rounding, y1 + rounding), radius: rounding, a_min_of_12: 9, a_max_of_12: 12);
10468 draw_list->PathLineTo(pos: ImVec2(bb.Max.x, y2));
10469 draw_list->PathFillConvex(col);
10470 if (g.Style.TabBorderSize > 0.0f)
10471 {
10472 draw_list->PathLineTo(pos: ImVec2(bb.Min.x + 0.5f, y2));
10473 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);
10474 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);
10475 draw_list->PathLineTo(pos: ImVec2(bb.Max.x - 0.5f, y2));
10476 draw_list->PathStroke(col: GetColorU32(idx: ImGuiCol_Border), flags: 0, thickness: g.Style.TabBorderSize);
10477 }
10478}
10479
10480// Render text label (with custom clipping) + Unsaved Document marker + Close Button logic
10481// We tend to lock style.FramePadding for a given tab-bar, hence the 'frame_padding' parameter.
10482void 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)
10483{
10484 ImGuiContext& g = *GImGui;
10485 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
10486
10487 if (out_just_closed)
10488 *out_just_closed = false;
10489 if (out_text_clipped)
10490 *out_text_clipped = false;
10491
10492 if (bb.GetWidth() <= 1.0f)
10493 return;
10494
10495 // In Style V2 we'll have full override of all colors per state (e.g. focused, selected)
10496 // But right now if you want to alter text color of tabs this is what you need to do.
10497#if 0
10498 const float backup_alpha = g.Style.Alpha;
10499 if (!is_contents_visible)
10500 g.Style.Alpha *= 0.7f;
10501#endif
10502
10503 // Render text label (with clipping + alpha gradient) + unsaved marker
10504 ImRect text_pixel_clip_bb(bb.Min.x + frame_padding.x, bb.Min.y + frame_padding.y, bb.Max.x - frame_padding.x, bb.Max.y);
10505 ImRect text_ellipsis_clip_bb = text_pixel_clip_bb;
10506
10507 // Return clipped state ignoring the close button
10508 if (out_text_clipped)
10509 {
10510 *out_text_clipped = (text_ellipsis_clip_bb.Min.x + label_size.x) > text_pixel_clip_bb.Max.x;
10511 //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));
10512 }
10513
10514 const float button_sz = g.FontSize;
10515 const ImVec2 button_pos(ImMax(lhs: bb.Min.x, rhs: bb.Max.x - frame_padding.x - button_sz), bb.Min.y + frame_padding.y);
10516
10517 // Close Button & Unsaved Marker
10518 // We are relying on a subtle and confusing distinction between 'hovered' and 'g.HoveredId' which happens because we are using ImGuiButtonFlags_AllowOverlapMode + SetItemAllowOverlap()
10519 // 'hovered' will be true when hovering the Tab but NOT when hovering the close button
10520 // 'g.HoveredId==id' will be true when hovering the Tab including when hovering the close button
10521 // 'g.ActiveId==close_button_id' will be true when we are holding on the close button, in which case both hovered booleans are false
10522 bool close_button_pressed = false;
10523 bool close_button_visible = false;
10524 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.
10525
10526 if (close_button_id != 0)
10527 {
10528 if (is_contents_visible)
10529 close_button_visible = (g.Style.TabCloseButtonMinWidthSelected < 0.0f) ? true : (is_hovered && bb.GetWidth() >= ImMax(lhs: button_sz, rhs: g.Style.TabCloseButtonMinWidthSelected));
10530 else
10531 close_button_visible = (g.Style.TabCloseButtonMinWidthUnselected < 0.0f) ? true : (is_hovered && bb.GetWidth() >= ImMax(lhs: button_sz, rhs: g.Style.TabCloseButtonMinWidthUnselected));
10532 }
10533
10534 // When tabs/document is unsaved, the unsaved marker takes priority over the close button.
10535 const bool unsaved_marker_visible = (flags & ImGuiTabItemFlags_UnsavedDocument) != 0 && (button_pos.x + button_sz <= bb.Max.x) && (!close_button_visible || !is_hovered);
10536 if (unsaved_marker_visible)
10537 {
10538 const ImRect bullet_bb(button_pos, button_pos + ImVec2(button_sz, button_sz));
10539 RenderBullet(draw_list, pos: bullet_bb.GetCenter(), col: GetColorU32(idx: ImGuiCol_Text));
10540 }
10541 else if (close_button_visible)
10542 {
10543 ImGuiLastItemData last_item_backup = g.LastItemData;
10544 if (CloseButton(id: close_button_id, pos: button_pos))
10545 close_button_pressed = true;
10546 g.LastItemData = last_item_backup;
10547
10548 // Close with middle mouse button
10549 if (is_hovered && !(flags & ImGuiTabItemFlags_NoCloseWithMiddleMouseButton) && IsMouseClicked(button: 2))
10550 close_button_pressed = true;
10551 }
10552
10553 // This is all rather complicated
10554 // (the main idea is that because the close button only appears on hover, we don't want it to alter the ellipsis position)
10555 // 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..
10556 float ellipsis_max_x = close_button_visible ? text_pixel_clip_bb.Max.x : bb.Max.x - 1.0f;
10557 if (close_button_visible || unsaved_marker_visible)
10558 {
10559 text_pixel_clip_bb.Max.x -= close_button_visible ? (button_sz) : (button_sz * 0.80f);
10560 text_ellipsis_clip_bb.Max.x -= unsaved_marker_visible ? (button_sz * 0.80f) : 0.0f;
10561 ellipsis_max_x = text_pixel_clip_bb.Max.x;
10562 }
10563 LogSetNextTextDecoration(prefix: "/", suffix: "\\");
10564 RenderTextEllipsis(draw_list, pos_min: text_ellipsis_clip_bb.Min, pos_max: text_ellipsis_clip_bb.Max, clip_max_x: text_pixel_clip_bb.Max.x, ellipsis_max_x, text: label, NULL, text_size_if_known: &label_size);
10565
10566#if 0
10567 if (!is_contents_visible)
10568 g.Style.Alpha = backup_alpha;
10569#endif
10570
10571 if (out_just_closed)
10572 *out_just_closed = close_button_pressed;
10573}
10574
10575
10576#endif // #ifndef IMGUI_DISABLE
10577

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

source code of imgui/imgui_widgets.cpp