openMSX
imgui_widgets.cc
Go to the documentation of this file.
1// dear imgui, v1.91.7
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-nonliteral" // warning: format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code.
74#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness
75#pragma clang diagnostic ignored "-Wunused-macros" // warning: macro is not used // we define snprintf/vsnprintf on Windows so they are available, but not always used.
76#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0
77#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double.
78#pragma clang diagnostic ignored "-Wenum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_')
79#pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated
80#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision
81#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access
82#pragma clang diagnostic ignored "-Wnontrivial-memaccess" // warning: first argument in call to 'memset' is a pointer to non-trivially copyable type
83#elif defined(__GNUC__)
84#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind
85#pragma GCC diagnostic ignored "-Wfloat-equal" // warning: comparing floating-point with '==' or '!=' is unsafe
86#pragma GCC diagnostic ignored "-Wformat" // warning: format '%p' expects argument of type 'int'/'void*', but argument X has type 'unsigned int'/'ImGuiWindow*'
87#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked
88#pragma GCC diagnostic ignored "-Wdeprecated-enum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated
89#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function
90#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
91#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
92#pragma GCC diagnostic ignored "-Wcast-qual" // warning: cast from type 'const xxxx *' to type 'xxxx *' casts away qualifiers
93#endif
94
95//-------------------------------------------------------------------------
96// Data
97//-------------------------------------------------------------------------
98
99// Widgets
100static const float DRAGDROP_HOLD_TO_OPEN_TIMER = 0.70f; // Time for drag-hold to activate items accepting the ImGuiButtonFlags_PressedOnDragDropHold button behavior.
101static 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.
102
103// Those MIN/MAX values are not define because we need to point to them
104static const signed char IM_S8_MIN = -128;
105static const signed char IM_S8_MAX = 127;
106static const unsigned char IM_U8_MIN = 0;
107static const unsigned char IM_U8_MAX = 0xFF;
108static const signed short IM_S16_MIN = -32768;
109static const signed short IM_S16_MAX = 32767;
110static const unsigned short IM_U16_MIN = 0;
111static const unsigned short IM_U16_MAX = 0xFFFF;
112static const ImS32 IM_S32_MIN = INT_MIN; // (-2147483647 - 1), (0x80000000);
113static const ImS32 IM_S32_MAX = INT_MAX; // (2147483647), (0x7FFFFFFF)
114static const ImU32 IM_U32_MIN = 0;
115static const ImU32 IM_U32_MAX = UINT_MAX; // (0xFFFFFFFF)
116#ifdef LLONG_MIN
117static const ImS64 IM_S64_MIN = LLONG_MIN; // (-9223372036854775807ll - 1ll);
118static const ImS64 IM_S64_MAX = LLONG_MAX; // (9223372036854775807ll);
119#else
120static const ImS64 IM_S64_MIN = -9223372036854775807LL - 1;
121static const ImS64 IM_S64_MAX = 9223372036854775807LL;
122#endif
123static const ImU64 IM_U64_MIN = 0;
124#ifdef ULLONG_MAX
125static const ImU64 IM_U64_MAX = ULLONG_MAX; // (0xFFFFFFFFFFFFFFFFull);
126#else
127static const ImU64 IM_U64_MAX = (2ULL * 9223372036854775807LL + 1);
128#endif
129
130//-------------------------------------------------------------------------
131// [SECTION] Forward Declarations
132//-------------------------------------------------------------------------
133
134// For InputTextEx()
135static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard = false);
136static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end);
137static 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);
138
139//-------------------------------------------------------------------------
140// [SECTION] Widgets: Text, etc.
141//-------------------------------------------------------------------------
142// - TextEx() [Internal]
143// - TextUnformatted()
144// - Text()
145// - TextV()
146// - TextColored()
147// - TextColoredV()
148// - TextDisabled()
149// - TextDisabledV()
150// - TextWrapped()
151// - TextWrappedV()
152// - LabelText()
153// - LabelTextV()
154// - BulletText()
155// - BulletTextV()
156//-------------------------------------------------------------------------
157
158void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags)
159{
160 ImGuiWindow* window = GetCurrentWindow();
161 if (window->SkipItems)
162 return;
163 ImGuiContext& g = *GImGui;
164
165 // Accept null ranges
166 if (text == text_end)
167 text = text_end = "";
168
169 // Calculate length
170 const char* text_begin = text;
171 if (text_end == NULL)
172 text_end = text + strlen(text); // FIXME-OPT
173
174 const ImVec2 text_pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
175 const float wrap_pos_x = window->DC.TextWrapPos;
176 const bool wrap_enabled = (wrap_pos_x >= 0.0f);
177 if (text_end - text <= 2000 || wrap_enabled)
178 {
179 // Common case
180 const float wrap_width = wrap_enabled ? CalcWrapWidthForPos(window->DC.CursorPos, wrap_pos_x) : 0.0f;
181 const ImVec2 text_size = CalcTextSize(text_begin, text_end, false, wrap_width);
182
183 ImRect bb(text_pos, text_pos + text_size);
184 ItemSize(text_size, 0.0f);
185 if (!ItemAdd(bb, 0))
186 return;
187
188 // Render (we don't hide text after ## in this end-user function)
189 RenderTextWrapped(bb.Min, text_begin, text_end, wrap_width);
190 }
191 else
192 {
193 // Long text!
194 // Perform manual coarse clipping to optimize for long multi-line text
195 // - From this point we will only compute the width of lines that are visible. Optimization only available when word-wrapping is disabled.
196 // - 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.
197 // - We use memchr(), pay attention that well optimized versions of those str/mem functions are much faster than a casually written loop.
198 const char* line = text;
199 const float line_height = GetTextLineHeight();
200 ImVec2 text_size(0, 0);
201
202 // Lines to skip (can't skip when logging text)
203 ImVec2 pos = text_pos;
204 if (!g.LogEnabled)
205 {
206 int lines_skippable = (int)((window->ClipRect.Min.y - text_pos.y) / line_height);
207 if (lines_skippable > 0)
208 {
209 int lines_skipped = 0;
210 while (line < text_end && lines_skipped < lines_skippable)
211 {
212 const char* line_end = (const char*)memchr(line, '\n', text_end - line);
213 if (!line_end)
214 line_end = text_end;
215 if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0)
216 text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x);
217 line = line_end + 1;
218 lines_skipped++;
219 }
220 pos.y += lines_skipped * line_height;
221 }
222 }
223
224 // Lines to render
225 if (line < text_end)
226 {
227 ImRect line_rect(pos, pos + ImVec2(FLT_MAX, line_height));
228 while (line < text_end)
229 {
230 if (IsClippedEx(line_rect, 0))
231 break;
232
233 const char* line_end = (const char*)memchr(line, '\n', text_end - line);
234 if (!line_end)
235 line_end = text_end;
236 text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x);
237 RenderText(pos, line, line_end, false);
238 line = line_end + 1;
239 line_rect.Min.y += line_height;
240 line_rect.Max.y += line_height;
241 pos.y += line_height;
242 }
243
244 // Count remaining lines
245 int lines_skipped = 0;
246 while (line < text_end)
247 {
248 const char* line_end = (const char*)memchr(line, '\n', text_end - line);
249 if (!line_end)
250 line_end = text_end;
251 if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0)
252 text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x);
253 line = line_end + 1;
254 lines_skipped++;
255 }
256 pos.y += lines_skipped * line_height;
257 }
258 text_size.y = (pos - text_pos).y;
259
260 ImRect bb(text_pos, text_pos + text_size);
261 ItemSize(text_size, 0.0f);
262 ItemAdd(bb, 0);
263 }
264}
265
266void ImGui::TextUnformatted(const char* text, const char* text_end)
267{
268 TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText);
269}
270
271void ImGui::Text(const char* fmt, ...)
272{
273 va_list args;
274 va_start(args, fmt);
275 TextV(fmt, args);
276 va_end(args);
277}
278
279void ImGui::TextV(const char* fmt, va_list args)
280{
281 ImGuiWindow* window = GetCurrentWindow();
282 if (window->SkipItems)
283 return;
284
285 const char* text, *text_end;
286 ImFormatStringToTempBufferV(&text, &text_end, fmt, args);
287 TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText);
288}
289
290void ImGui::TextColored(const ImVec4& col, const char* fmt, ...)
291{
292 va_list args;
293 va_start(args, fmt);
294 TextColoredV(col, fmt, args);
295 va_end(args);
296}
297
298void ImGui::TextColoredV(const ImVec4& col, const char* fmt, va_list args)
299{
300 PushStyleColor(ImGuiCol_Text, col);
301 TextV(fmt, args);
302 PopStyleColor();
303}
304
305void ImGui::TextDisabled(const char* fmt, ...)
306{
307 va_list args;
308 va_start(args, fmt);
309 TextDisabledV(fmt, args);
310 va_end(args);
311}
312
313void ImGui::TextDisabledV(const char* fmt, va_list args)
314{
315 ImGuiContext& g = *GImGui;
316 PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]);
317 TextV(fmt, args);
318 PopStyleColor();
319}
320
321void ImGui::TextWrapped(const char* fmt, ...)
322{
323 va_list args;
324 va_start(args, fmt);
325 TextWrappedV(fmt, args);
326 va_end(args);
327}
328
329void ImGui::TextWrappedV(const char* fmt, va_list args)
330{
331 ImGuiContext& g = *GImGui;
332 const bool need_backup = (g.CurrentWindow->DC.TextWrapPos < 0.0f); // Keep existing wrap position if one is already set
333 if (need_backup)
334 PushTextWrapPos(0.0f);
335 TextV(fmt, args);
336 if (need_backup)
337 PopTextWrapPos();
338}
339
340void ImGui::LabelText(const char* label, const char* fmt, ...)
341{
342 va_list args;
343 va_start(args, fmt);
344 LabelTextV(label, fmt, args);
345 va_end(args);
346}
347
348// Add a label+text combo aligned to other label+value widgets
349void ImGui::LabelTextV(const char* label, const char* fmt, va_list args)
350{
351 ImGuiWindow* window = GetCurrentWindow();
352 if (window->SkipItems)
353 return;
354
355 ImGuiContext& g = *GImGui;
356 const ImGuiStyle& style = g.Style;
357 const float w = CalcItemWidth();
358
359 const char* value_text_begin, *value_text_end;
360 ImFormatStringToTempBufferV(&value_text_begin, &value_text_end, fmt, args);
361 const ImVec2 value_size = CalcTextSize(value_text_begin, value_text_end, false);
362 const ImVec2 label_size = CalcTextSize(label, NULL, true);
363
364 const ImVec2 pos = window->DC.CursorPos;
365 const ImRect value_bb(pos, pos + ImVec2(w, value_size.y + style.FramePadding.y * 2));
366 const ImRect total_bb(pos, pos + ImVec2(w + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), ImMax(value_size.y, label_size.y) + style.FramePadding.y * 2));
367 ItemSize(total_bb, style.FramePadding.y);
368 if (!ItemAdd(total_bb, 0))
369 return;
370
371 // Render
372 RenderTextClipped(value_bb.Min + style.FramePadding, value_bb.Max, value_text_begin, value_text_end, &value_size, ImVec2(0.0f, 0.0f));
373 if (label_size.x > 0.0f)
374 RenderText(ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), label);
375}
376
377void ImGui::BulletText(const char* fmt, ...)
378{
379 va_list args;
380 va_start(args, fmt);
381 BulletTextV(fmt, args);
382 va_end(args);
383}
384
385// Text with a little bullet aligned to the typical tree node.
386void ImGui::BulletTextV(const char* fmt, va_list args)
387{
388 ImGuiWindow* window = GetCurrentWindow();
389 if (window->SkipItems)
390 return;
391
392 ImGuiContext& g = *GImGui;
393 const ImGuiStyle& style = g.Style;
394
395 const char* text_begin, *text_end;
396 ImFormatStringToTempBufferV(&text_begin, &text_end, fmt, args);
397 const ImVec2 label_size = CalcTextSize(text_begin, text_end, false);
398 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
399 ImVec2 pos = window->DC.CursorPos;
400 pos.y += window->DC.CurrLineTextBaseOffset;
401 ItemSize(total_size, 0.0f);
402 const ImRect bb(pos, pos + total_size);
403 if (!ItemAdd(bb, 0))
404 return;
405
406 // Render
407 ImU32 text_col = GetColorU32(ImGuiCol_Text);
408 RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, g.FontSize * 0.5f), text_col);
409 RenderText(bb.Min + ImVec2(g.FontSize + style.FramePadding.x * 2, 0.0f), text_begin, text_end, false);
410}
411
412//-------------------------------------------------------------------------
413// [SECTION] Widgets: Main
414//-------------------------------------------------------------------------
415// - ButtonBehavior() [Internal]
416// - Button()
417// - SmallButton()
418// - InvisibleButton()
419// - ArrowButton()
420// - CloseButton() [Internal]
421// - CollapseButton() [Internal]
422// - GetWindowScrollbarID() [Internal]
423// - GetWindowScrollbarRect() [Internal]
424// - Scrollbar() [Internal]
425// - ScrollbarEx() [Internal]
426// - Image()
427// - ImageButton()
428// - Checkbox()
429// - CheckboxFlagsT() [Internal]
430// - CheckboxFlags()
431// - RadioButton()
432// - ProgressBar()
433// - Bullet()
434// - Hyperlink()
435//-------------------------------------------------------------------------
436
437// The ButtonBehavior() function is key to many interactions and used by many/most widgets.
438// Because we handle so many cases (keyboard/gamepad navigation, drag and drop) and many specific behavior (via ImGuiButtonFlags_),
439// this code is a little complex.
440// By far the most common path is interacting with the Mouse using the default ImGuiButtonFlags_PressedOnClickRelease button behavior.
441// See the series of events below and the corresponding state reported by dear imgui:
442//------------------------------------------------------------------------------------------------------------------------------------------------
443// with PressedOnClickRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
444// Frame N+0 (mouse is outside bb) - - - - - -
445// Frame N+1 (mouse moves inside bb) - true - - - -
446// Frame N+2 (mouse button is down) - true true true - true
447// Frame N+3 (mouse button is down) - true true - - -
448// Frame N+4 (mouse moves outside bb) - - true - - -
449// Frame N+5 (mouse moves inside bb) - true true - - -
450// Frame N+6 (mouse button is released) true true - - true -
451// Frame N+7 (mouse button is released) - true - - - -
452// Frame N+8 (mouse moves outside bb) - - - - - -
453//------------------------------------------------------------------------------------------------------------------------------------------------
454// with PressedOnClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
455// Frame N+2 (mouse button is down) true true true true - true
456// Frame N+3 (mouse button is down) - true true - - -
457// Frame N+6 (mouse button is released) - true - - true -
458// Frame N+7 (mouse button is released) - true - - - -
459//------------------------------------------------------------------------------------------------------------------------------------------------
460// with PressedOnRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
461// Frame N+2 (mouse button is down) - true - - - true
462// Frame N+3 (mouse button is down) - true - - - -
463// Frame N+6 (mouse button is released) true true - - - -
464// Frame N+7 (mouse button is released) - true - - - -
465//------------------------------------------------------------------------------------------------------------------------------------------------
466// with PressedOnDoubleClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
467// Frame N+0 (mouse button is down) - true - - - true
468// Frame N+1 (mouse button is down) - true - - - -
469// Frame N+2 (mouse button is released) - true - - - -
470// Frame N+3 (mouse button is released) - true - - - -
471// Frame N+4 (mouse button is down) true true true true - true
472// Frame N+5 (mouse button is down) - true true - - -
473// Frame N+6 (mouse button is released) - true - - true -
474// Frame N+7 (mouse button is released) - true - - - -
475//------------------------------------------------------------------------------------------------------------------------------------------------
476// Note that some combinations are supported,
477// - PressedOnDragDropHold can generally be associated with any flag.
478// - PressedOnDoubleClick can be associated by PressedOnClickRelease/PressedOnRelease, in which case the second release event won't be reported.
479//------------------------------------------------------------------------------------------------------------------------------------------------
480// The behavior of the return-value changes when ImGuiItemFlags_ButtonRepeat is set:
481// Repeat+ Repeat+ Repeat+ Repeat+
482// PressedOnClickRelease PressedOnClick PressedOnRelease PressedOnDoubleClick
483//-------------------------------------------------------------------------------------------------------------------------------------------------
484// Frame N+0 (mouse button is down) - true - true
485// ... - - - -
486// Frame N + RepeatDelay true true - true
487// ... - - - -
488// Frame N + RepeatDelay + RepeatRate*N true true - true
489//-------------------------------------------------------------------------------------------------------------------------------------------------
490
491// - FIXME: For refactor we could output flags, incl mouse hovered vs nav keyboard vs nav triggered etc.
492// And better standardize how widgets use 'GetColor32((held && hovered) ? ... : hovered ? ...)' vs 'GetColor32(held ? ... : hovered ? ...);'
493// For mouse feedback we typically prefer the 'held && hovered' test, but for nav feedback not always. Outputting hovered=true on Activation may be misleading.
494// - Since v1.91.2 (Sept 2024) we included io.ConfigDebugHighlightIdConflicts feature.
495// One idiom which was previously valid which will now emit a warning is when using multiple overlayed ButtonBehavior()
496// with same ID and different MouseButton (see #8030). You can fix it by:
497// (1) switching to use a single ButtonBehavior() with multiple _MouseButton flags.
498// or (2) surrounding those calls with PushItemFlag(ImGuiItemFlags_AllowDuplicateId, true); ... PopItemFlag()
499bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags)
500{
501 ImGuiContext& g = *GImGui;
502 ImGuiWindow* window = GetCurrentWindow();
503
504 // Default behavior inherited from item flags
505 // Note that _both_ ButtonFlags and ItemFlags are valid sources, so copy one into the item_flags and only check that.
506 ImGuiItemFlags item_flags = (g.LastItemData.ID == id ? g.LastItemData.ItemFlags : g.CurrentItemFlags);
507 if (flags & ImGuiButtonFlags_AllowOverlap)
508 item_flags |= ImGuiItemFlags_AllowOverlap;
509
510 // Default only reacts to left mouse button
511 if ((flags & ImGuiButtonFlags_MouseButtonMask_) == 0)
512 flags |= ImGuiButtonFlags_MouseButtonLeft;
513
514 // Default behavior requires click + release inside bounding box
515 if ((flags & ImGuiButtonFlags_PressedOnMask_) == 0)
516 flags |= (item_flags & ImGuiItemFlags_ButtonRepeat) ? ImGuiButtonFlags_PressedOnClick : ImGuiButtonFlags_PressedOnDefault_;
517
518 ImGuiWindow* backup_hovered_window = g.HoveredWindow;
519 const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredWindow && g.HoveredWindow->RootWindowDockTree == window->RootWindowDockTree;
520 if (flatten_hovered_children)
521 g.HoveredWindow = window;
522
523#ifdef IMGUI_ENABLE_TEST_ENGINE
524 // Alternate registration spot, for when caller didn't use ItemAdd()
525 if (g.LastItemData.ID != id)
526 IMGUI_TEST_ENGINE_ITEM_ADD(id, bb, NULL);
527#endif
528
529 bool pressed = false;
530 bool hovered = ItemHoverable(bb, id, item_flags);
531
532 // Special mode for Drag and Drop where holding button pressed for a long time while dragging another item triggers the button
533 if (g.DragDropActive && (flags & ImGuiButtonFlags_PressedOnDragDropHold) && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers))
534 if (IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem))
535 {
536 hovered = true;
537 SetHoveredID(id);
538 if (g.HoveredIdTimer - g.IO.DeltaTime <= DRAGDROP_HOLD_TO_OPEN_TIMER && g.HoveredIdTimer >= DRAGDROP_HOLD_TO_OPEN_TIMER)
539 {
540 pressed = true;
541 g.DragDropHoldJustPressedId = id;
542 FocusWindow(window);
543 }
544 }
545
546 if (flatten_hovered_children)
547 g.HoveredWindow = backup_hovered_window;
548
549 // Mouse handling
550 const ImGuiID test_owner_id = (flags & ImGuiButtonFlags_NoTestKeyOwner) ? ImGuiKeyOwner_Any : id;
551 if (hovered)
552 {
553 IM_ASSERT(id != 0); // Lazily check inside rare path.
554
555 // Poll mouse buttons
556 // - 'mouse_button_clicked' is generally carried into ActiveIdMouseButton when setting ActiveId.
557 // - Technically we only need some values in one code path, but since this is gated by hovered test this is fine.
558 int mouse_button_clicked = -1;
559 int mouse_button_released = -1;
560 for (int button = 0; button < 3; button++)
561 if (flags & (ImGuiButtonFlags_MouseButtonLeft << button)) // Handle ImGuiButtonFlags_MouseButtonRight and ImGuiButtonFlags_MouseButtonMiddle here.
562 {
563 if (IsMouseClicked(button, ImGuiInputFlags_None, test_owner_id) && mouse_button_clicked == -1) { mouse_button_clicked = button; }
564 if (IsMouseReleased(button, test_owner_id) && mouse_button_released == -1) { mouse_button_released = button; }
565 }
566
567 // Process initial action
568 const bool mods_ok = !(flags & ImGuiButtonFlags_NoKeyModsAllowed) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt);
569 if (mods_ok)
570 {
571 if (mouse_button_clicked != -1 && g.ActiveId != id)
572 {
573 if (!(flags & ImGuiButtonFlags_NoSetKeyOwner))
574 SetKeyOwner(MouseButtonToKey(mouse_button_clicked), id);
575 if (flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClickReleaseAnywhere))
576 {
577 SetActiveID(id, window);
578 g.ActiveIdMouseButton = mouse_button_clicked;
579 if (!(flags & ImGuiButtonFlags_NoNavFocus))
580 {
581 SetFocusID(id, window);
582 FocusWindow(window);
583 }
584 else
585 {
586 FocusWindow(window, ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child
587 }
588 }
589 if ((flags & ImGuiButtonFlags_PressedOnClick) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseClickedCount[mouse_button_clicked] == 2))
590 {
591 pressed = true;
592 if (flags & ImGuiButtonFlags_NoHoldingActiveId)
593 ClearActiveID();
594 else
595 SetActiveID(id, window); // Hold on ID
596 g.ActiveIdMouseButton = mouse_button_clicked;
597 if (!(flags & ImGuiButtonFlags_NoNavFocus))
598 {
599 SetFocusID(id, window);
600 FocusWindow(window);
601 }
602 else
603 {
604 FocusWindow(window, ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child
605 }
606 }
607 }
608 if (flags & ImGuiButtonFlags_PressedOnRelease)
609 {
610 if (mouse_button_released != -1)
611 {
612 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
613 if (!has_repeated_at_least_once)
614 pressed = true;
615 if (!(flags & ImGuiButtonFlags_NoNavFocus))
616 SetFocusID(id, window); // FIXME: Lack of FocusWindow() call here is inconsistent with other paths. Research why.
617 ClearActiveID();
618 }
619 }
620
621 // 'Repeat' mode acts when held regardless of _PressedOn flags (see table above).
622 // Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings.
623 if (g.ActiveId == id && (item_flags & ImGuiItemFlags_ButtonRepeat))
624 if (g.IO.MouseDownDuration[g.ActiveIdMouseButton] > 0.0f && IsMouseClicked(g.ActiveIdMouseButton, ImGuiInputFlags_Repeat, test_owner_id))
625 pressed = true;
626 }
627
628 if (pressed && g.IO.ConfigNavCursorVisibleAuto)
629 g.NavCursorVisible = false;
630 }
631
632 // Keyboard/Gamepad navigation handling
633 // We report navigated and navigation-activated items as hovered but we don't set g.HoveredId to not interfere with mouse.
634 if (g.NavId == id && g.NavCursorVisible && g.NavHighlightItemUnderNav)
635 if (!(flags & ImGuiButtonFlags_NoHoveredOnFocus))
636 hovered = true;
637 if (g.NavActivateDownId == id)
638 {
639 bool nav_activated_by_code = (g.NavActivateId == id);
640 bool nav_activated_by_inputs = (g.NavActivatePressedId == id);
641 if (!nav_activated_by_inputs && (item_flags & ImGuiItemFlags_ButtonRepeat))
642 {
643 // Avoid pressing multiple keys from triggering excessive amount of repeat events
644 const ImGuiKeyData* key1 = GetKeyData(ImGuiKey_Space);
645 const ImGuiKeyData* key2 = GetKeyData(ImGuiKey_Enter);
646 const ImGuiKeyData* key3 = GetKeyData(ImGuiKey_NavGamepadActivate);
647 const float t1 = ImMax(ImMax(key1->DownDuration, key2->DownDuration), key3->DownDuration);
648 nav_activated_by_inputs = CalcTypematicRepeatAmount(t1 - g.IO.DeltaTime, t1, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate) > 0;
649 }
650 if (nav_activated_by_code || nav_activated_by_inputs)
651 {
652 // Set active id so it can be queried by user via IsItemActive(), equivalent of holding the mouse button.
653 pressed = true;
654 SetActiveID(id, window);
655 g.ActiveIdSource = g.NavInputSource;
656 if (!(flags & ImGuiButtonFlags_NoNavFocus) && !(g.NavActivateFlags & ImGuiActivateFlags_FromShortcut))
657 SetFocusID(id, window);
658 if (g.NavActivateFlags & ImGuiActivateFlags_FromShortcut)
659 g.ActiveIdFromShortcut = true;
660 }
661 }
662
663 // Process while held
664 bool held = false;
665 if (g.ActiveId == id)
666 {
667 if (g.ActiveIdSource == ImGuiInputSource_Mouse)
668 {
669 if (g.ActiveIdIsJustActivated)
670 g.ActiveIdClickOffset = g.IO.MousePos - bb.Min;
671
672 const int mouse_button = g.ActiveIdMouseButton;
673 if (mouse_button == -1)
674 {
675 // Fallback for the rare situation were g.ActiveId was set programmatically or from another widget (e.g. #6304).
676 ClearActiveID();
677 }
678 else if (IsMouseDown(mouse_button, test_owner_id))
679 {
680 held = true;
681 }
682 else
683 {
684 bool release_in = hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease) != 0;
685 bool release_anywhere = (flags & ImGuiButtonFlags_PressedOnClickReleaseAnywhere) != 0;
686 if ((release_in || release_anywhere) && !g.DragDropActive)
687 {
688 // Report as pressed when releasing the mouse (this is the most common path)
689 bool is_double_click_release = (flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseReleased[mouse_button] && g.IO.MouseClickedLastCount[mouse_button] == 2;
690 bool is_repeating_already = (item_flags & ImGuiItemFlags_ButtonRepeat) && g.IO.MouseDownDurationPrev[mouse_button] >= g.IO.KeyRepeatDelay; // Repeat mode trumps <on release>
691 bool is_button_avail_or_owned = TestKeyOwner(MouseButtonToKey(mouse_button), test_owner_id);
692 if (!is_double_click_release && !is_repeating_already && is_button_avail_or_owned)
693 pressed = true;
694 }
695 ClearActiveID();
696 }
697 if (!(flags & ImGuiButtonFlags_NoNavFocus) && g.IO.ConfigNavCursorVisibleAuto)
698 g.NavCursorVisible = false;
699 }
700 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
701 {
702 // When activated using Nav, we hold on the ActiveID until activation button is released
703 if (g.NavActivateDownId == id)
704 held = true; // hovered == true not true as we are already likely hovered on direct activation.
705 else
706 ClearActiveID();
707 }
708 if (pressed)
709 g.ActiveIdHasBeenPressedBefore = true;
710 }
711
712 // Activation highlight (this may be a remote activation)
713 if (g.NavHighlightActivatedId == id)
714 hovered = true;
715
716 if (out_hovered) *out_hovered = hovered;
717 if (out_held) *out_held = held;
718
719 return pressed;
720}
721
722bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags)
723{
724 ImGuiWindow* window = GetCurrentWindow();
725 if (window->SkipItems)
726 return false;
727
728 ImGuiContext& g = *GImGui;
729 const ImGuiStyle& style = g.Style;
730 const ImGuiID id = window->GetID(label);
731 const ImVec2 label_size = CalcTextSize(label, NULL, true);
732
733 ImVec2 pos = window->DC.CursorPos;
734 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)
735 pos.y += window->DC.CurrLineTextBaseOffset - style.FramePadding.y;
736 ImVec2 size = CalcItemSize(size_arg, label_size.x + style.FramePadding.x * 2.0f, label_size.y + style.FramePadding.y * 2.0f);
737
738 const ImRect bb(pos, pos + size);
739 ItemSize(size, style.FramePadding.y);
740 if (!ItemAdd(bb, id))
741 return false;
742
743 bool hovered, held;
744 bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
745
746 // Render
747 const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
748 RenderNavCursor(bb, id);
749 RenderFrame(bb.Min, bb.Max, col, true, style.FrameRounding);
750
751 if (g.LogEnabled)
752 LogSetNextTextDecoration("[", "]");
753 RenderTextClipped(bb.Min + style.FramePadding, bb.Max - style.FramePadding, label, NULL, &label_size, style.ButtonTextAlign, &bb);
754
755 // Automatically close popups
756 //if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup))
757 // CloseCurrentPopup();
758
759 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
760 return pressed;
761}
762
763bool ImGui::Button(const char* label, const ImVec2& size_arg)
764{
765 return ButtonEx(label, size_arg, ImGuiButtonFlags_None);
766}
767
768// Small buttons fits within text without additional vertical spacing.
769bool ImGui::SmallButton(const char* label)
770{
771 ImGuiContext& g = *GImGui;
772 float backup_padding_y = g.Style.FramePadding.y;
773 g.Style.FramePadding.y = 0.0f;
774 bool pressed = ButtonEx(label, ImVec2(0, 0), ImGuiButtonFlags_AlignTextBaseLine);
775 g.Style.FramePadding.y = backup_padding_y;
776 return pressed;
777}
778
779// Tip: use ImGui::PushID()/PopID() to push indices or pointers in the ID stack.
780// 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)
781bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg, ImGuiButtonFlags flags)
782{
783 ImGuiContext& g = *GImGui;
784 ImGuiWindow* window = GetCurrentWindow();
785 if (window->SkipItems)
786 return false;
787
788 // Cannot use zero-size for InvisibleButton(). Unlike Button() there is not way to fallback using the label size.
789 IM_ASSERT(size_arg.x != 0.0f && size_arg.y != 0.0f);
790
791 const ImGuiID id = window->GetID(str_id);
792 ImVec2 size = CalcItemSize(size_arg, 0.0f, 0.0f);
793 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
794 ItemSize(size);
795 if (!ItemAdd(bb, id, NULL, (flags & ImGuiButtonFlags_EnableNav) ? ImGuiItemFlags_None : ImGuiItemFlags_NoNav))
796 return false;
797
798 bool hovered, held;
799 bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
800 RenderNavCursor(bb, id);
801
802 IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
803 return pressed;
804}
805
806bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiButtonFlags flags)
807{
808 ImGuiContext& g = *GImGui;
809 ImGuiWindow* window = GetCurrentWindow();
810 if (window->SkipItems)
811 return false;
812
813 const ImGuiID id = window->GetID(str_id);
814 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
815 const float default_size = GetFrameHeight();
816 ItemSize(size, (size.y >= default_size) ? g.Style.FramePadding.y : -1.0f);
817 if (!ItemAdd(bb, id))
818 return false;
819
820 bool hovered, held;
821 bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
822
823 // Render
824 const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
825 const ImU32 text_col = GetColorU32(ImGuiCol_Text);
826 RenderNavCursor(bb, id);
827 RenderFrame(bb.Min, bb.Max, bg_col, true, g.Style.FrameRounding);
828 RenderArrow(window->DrawList, bb.Min + ImVec2(ImMax(0.0f, (size.x - g.FontSize) * 0.5f), ImMax(0.0f, (size.y - g.FontSize) * 0.5f)), text_col, dir);
829
830 IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
831 return pressed;
832}
833
834bool ImGui::ArrowButton(const char* str_id, ImGuiDir dir)
835{
836 float sz = GetFrameHeight();
837 return ArrowButtonEx(str_id, dir, ImVec2(sz, sz), ImGuiButtonFlags_None);
838}
839
840// Button to close a window
841bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos)
842{
843 ImGuiContext& g = *GImGui;
844 ImGuiWindow* window = g.CurrentWindow;
845
846 // 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)
847 // 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?
848 const ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize));
849 ImRect bb_interact = bb;
850 const float area_to_visible_ratio = window->OuterRectClipped.GetArea() / bb.GetArea();
851 if (area_to_visible_ratio < 1.5f)
852 bb_interact.Expand(ImTrunc(bb_interact.GetSize() * -0.25f));
853
854 // Tweak 2: We intentionally allow interaction when clipped so that a mechanical Alt,Right,Activate sequence can always close a window.
855 // (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).
856 bool is_clipped = !ItemAdd(bb_interact, id);
857
858 bool hovered, held;
859 bool pressed = ButtonBehavior(bb_interact, id, &hovered, &held);
860 if (is_clipped)
861 return pressed;
862
863 // Render
864 ImU32 bg_col = GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered);
865 if (hovered)
866 window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col);
867 RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact);
868 ImU32 cross_col = GetColorU32(ImGuiCol_Text);
869 ImVec2 cross_center = bb.GetCenter() - ImVec2(0.5f, 0.5f);
870 float cross_extent = g.FontSize * 0.5f * 0.7071f - 1.0f;
871 window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, +cross_extent), cross_center + ImVec2(-cross_extent, -cross_extent), cross_col, 1.0f);
872 window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, -cross_extent), cross_center + ImVec2(-cross_extent, +cross_extent), cross_col, 1.0f);
873
874 return pressed;
875}
876
877// The Collapse button also functions as a Dock Menu button.
878bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos, ImGuiDockNode* dock_node)
879{
880 ImGuiContext& g = *GImGui;
881 ImGuiWindow* window = g.CurrentWindow;
882
883 ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize));
884 bool is_clipped = !ItemAdd(bb, id);
885 bool hovered, held;
886 bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None);
887 if (is_clipped)
888 return pressed;
889
890 // Render
891 //bool is_dock_menu = (window->DockNodeAsHost && !window->Collapsed);
892 ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
893 ImU32 text_col = GetColorU32(ImGuiCol_Text);
894 if (hovered || held)
895 window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col);
896 RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact);
897
898 if (dock_node)
899 RenderArrowDockMenu(window->DrawList, bb.Min, g.FontSize, text_col);
900 else
901 RenderArrow(window->DrawList, bb.Min, text_col, window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, 1.0f);
902
903 // Switch to moving the window after mouse is moved beyond the initial drag threshold
904 if (IsItemActive() && IsMouseDragging(0))
905 StartMouseMovingWindowOrNode(window, dock_node, true); // Undock from window/collapse menu button
906
907 return pressed;
908}
909
910ImGuiID ImGui::GetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis)
911{
912 return window->GetID(axis == ImGuiAxis_X ? "#SCROLLX" : "#SCROLLY");
913}
914
915// Return scrollbar rectangle, must only be called for corresponding axis if window->ScrollbarX/Y is set.
916ImRect ImGui::GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis)
917{
918 const ImRect outer_rect = window->Rect();
919 const ImRect inner_rect = window->InnerRect;
920 const float border_size = window->WindowBorderSize;
921 const float scrollbar_size = window->ScrollbarSizes[axis ^ 1]; // (ScrollbarSizes.x = width of Y scrollbar; ScrollbarSizes.y = height of X scrollbar)
922 IM_ASSERT(scrollbar_size > 0.0f);
923 if (axis == ImGuiAxis_X)
924 return ImRect(inner_rect.Min.x, ImMax(outer_rect.Min.y, outer_rect.Max.y - border_size - scrollbar_size), inner_rect.Max.x - border_size, outer_rect.Max.y - border_size);
925 else
926 return ImRect(ImMax(outer_rect.Min.x, outer_rect.Max.x - border_size - scrollbar_size), inner_rect.Min.y, outer_rect.Max.x - border_size, inner_rect.Max.y - border_size);
927}
928
929void ImGui::Scrollbar(ImGuiAxis axis)
930{
931 ImGuiContext& g = *GImGui;
932 ImGuiWindow* window = g.CurrentWindow;
933 const ImGuiID id = GetWindowScrollbarID(window, axis);
934
935 // Calculate scrollbar bounding box
936 ImRect bb = GetWindowScrollbarRect(window, axis);
937 ImDrawFlags rounding_corners = ImDrawFlags_RoundCornersNone;
938 if (axis == ImGuiAxis_X)
939 {
940 rounding_corners |= ImDrawFlags_RoundCornersBottomLeft;
941 if (!window->ScrollbarY)
942 rounding_corners |= ImDrawFlags_RoundCornersBottomRight;
943 }
944 else
945 {
946 if ((window->Flags & ImGuiWindowFlags_NoTitleBar) && !(window->Flags & ImGuiWindowFlags_MenuBar))
947 rounding_corners |= ImDrawFlags_RoundCornersTopRight;
948 if (!window->ScrollbarX)
949 rounding_corners |= ImDrawFlags_RoundCornersBottomRight;
950 }
951 float size_visible = window->InnerRect.Max[axis] - window->InnerRect.Min[axis];
952 float size_contents = window->ContentSize[axis] + window->WindowPadding[axis] * 2.0f;
953 ImS64 scroll = (ImS64)window->Scroll[axis];
954 ScrollbarEx(bb, id, axis, &scroll, (ImS64)size_visible, (ImS64)size_contents, rounding_corners);
955 window->Scroll[axis] = (float)scroll;
956}
957
958// Vertical/Horizontal scrollbar
959// The entire piece of code below is rather confusing because:
960// - We handle absolute seeking (when first clicking outside the grab) and relative manipulation (afterward or when clicking inside the grab)
961// - We store values as normalized ratio and in a form that allows the window content to change while we are holding on a scrollbar
962// - We handle both horizontal and vertical scrollbars, which makes the terminology not ideal.
963// Still, the code should probably be made simpler..
964bool 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)
965{
966 ImGuiContext& g = *GImGui;
967 ImGuiWindow* window = g.CurrentWindow;
968 if (window->SkipItems)
969 return false;
970
971 const float bb_frame_width = bb_frame.GetWidth();
972 const float bb_frame_height = bb_frame.GetHeight();
973 if (bb_frame_width <= 0.0f || bb_frame_height <= 0.0f)
974 return false;
975
976 // 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)
977 float alpha = 1.0f;
978 if ((axis == ImGuiAxis_Y) && bb_frame_height < g.FontSize + g.Style.FramePadding.y * 2.0f)
979 alpha = ImSaturate((bb_frame_height - g.FontSize) / (g.Style.FramePadding.y * 2.0f));
980 if (alpha <= 0.0f)
981 return false;
982
983 const ImGuiStyle& style = g.Style;
984 const bool allow_interaction = (alpha >= 1.0f);
985
986 ImRect bb = bb_frame;
987 bb.Expand(ImVec2(-ImClamp(IM_TRUNC((bb_frame_width - 2.0f) * 0.5f), 0.0f, 3.0f), -ImClamp(IM_TRUNC((bb_frame_height - 2.0f) * 0.5f), 0.0f, 3.0f)));
988
989 // V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar)
990 const float scrollbar_size_v = (axis == ImGuiAxis_X) ? bb.GetWidth() : bb.GetHeight();
991
992 // Calculate the height of our grabbable box. It generally represent the amount visible (vs the total scrollable amount)
993 // But we maintain a minimum size in pixel to allow for the user to still aim inside.
994 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.
995 const ImS64 win_size_v = ImMax(ImMax(size_contents_v, size_visible_v), (ImS64)1);
996 const float grab_h_pixels = ImClamp(scrollbar_size_v * ((float)size_visible_v / (float)win_size_v), style.GrabMinSize, scrollbar_size_v);
997 const float grab_h_norm = grab_h_pixels / scrollbar_size_v;
998
999 // Handle input right away. None of the code of Begin() is relying on scrolling position before calling Scrollbar().
1000 bool held = false;
1001 bool hovered = false;
1002 ItemAdd(bb_frame, id, NULL, ImGuiItemFlags_NoNav);
1003 ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_NoNavFocus);
1004
1005 const ImS64 scroll_max = ImMax((ImS64)1, size_contents_v - size_visible_v);
1006 float scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max);
1007 float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; // Grab position in normalized space
1008 if (held && allow_interaction && grab_h_norm < 1.0f)
1009 {
1010 const float scrollbar_pos_v = bb.Min[axis];
1011 const float mouse_pos_v = g.IO.MousePos[axis];
1012
1013 // Click position in scrollbar normalized space (0.0f->1.0f)
1014 const float clicked_v_norm = ImSaturate((mouse_pos_v - scrollbar_pos_v) / scrollbar_size_v);
1015
1016 const int held_dir = (clicked_v_norm < grab_v_norm) ? -1 : (clicked_v_norm > grab_v_norm + grab_h_norm) ? +1 : 0;
1017 if (g.ActiveIdIsJustActivated)
1018 {
1019 // On initial click when held_dir == 0 (clicked over grab): calculate the distance between mouse and the center of the grab
1020 const bool scroll_to_clicked_location = (g.IO.ConfigScrollbarScrollByPage == false || g.IO.KeyShift || held_dir == 0);
1021 g.ScrollbarSeekMode = scroll_to_clicked_location ? 0 : (short)held_dir;
1022 g.ScrollbarClickDeltaToGrabCenter = (held_dir == 0 && !g.IO.KeyShift) ? clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f : 0.0f;
1023 }
1024
1025 // Apply scroll (p_scroll_v will generally point on one member of window->Scroll)
1026 // 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
1027 if (g.ScrollbarSeekMode == 0)
1028 {
1029 // Absolute seeking
1030 const float scroll_v_norm = ImSaturate((clicked_v_norm - g.ScrollbarClickDeltaToGrabCenter - grab_h_norm * 0.5f) / (1.0f - grab_h_norm));
1031 *p_scroll_v = (ImS64)(scroll_v_norm * scroll_max);
1032 }
1033 else
1034 {
1035 // Page by page
1036 if (IsMouseClicked(ImGuiMouseButton_Left, ImGuiInputFlags_Repeat) && held_dir == g.ScrollbarSeekMode)
1037 {
1038 float page_dir = (g.ScrollbarSeekMode > 0.0f) ? +1.0f : -1.0f;
1039 *p_scroll_v = ImClamp(*p_scroll_v + (ImS64)(page_dir * size_visible_v), (ImS64)0, scroll_max);
1040 }
1041 }
1042
1043 // Update values for rendering
1044 scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max);
1045 grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v;
1046
1047 // Update distance to grab now that we have seek'ed and saturated
1048 //if (seek_absolute)
1049 // g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f;
1050 }
1051
1052 // Render
1053 const ImU32 bg_col = GetColorU32(ImGuiCol_ScrollbarBg);
1054 const ImU32 grab_col = GetColorU32(held ? ImGuiCol_ScrollbarGrabActive : hovered ? ImGuiCol_ScrollbarGrabHovered : ImGuiCol_ScrollbarGrab, alpha);
1055 window->DrawList->AddRectFilled(bb_frame.Min, bb_frame.Max, bg_col, window->WindowRounding, draw_rounding_flags);
1056 ImRect grab_rect;
1057 if (axis == ImGuiAxis_X)
1058 grab_rect = ImRect(ImLerp(bb.Min.x, bb.Max.x, grab_v_norm), bb.Min.y, ImLerp(bb.Min.x, bb.Max.x, grab_v_norm) + grab_h_pixels, bb.Max.y);
1059 else
1060 grab_rect = ImRect(bb.Min.x, ImLerp(bb.Min.y, bb.Max.y, grab_v_norm), bb.Max.x, ImLerp(bb.Min.y, bb.Max.y, grab_v_norm) + grab_h_pixels);
1061 window->DrawList->AddRectFilled(grab_rect.Min, grab_rect.Max, grab_col, style.ScrollbarRounding);
1062
1063 return held;
1064}
1065
1066// - Read about ImTextureID here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples
1067// - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above.
1068void ImGui::Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col)
1069{
1070 ImGuiWindow* window = GetCurrentWindow();
1071 if (window->SkipItems)
1072 return;
1073
1074 const float border_size = (border_col.w > 0.0f) ? 1.0f : 0.0f;
1075 const ImVec2 padding(border_size, border_size);
1076 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f);
1077 ItemSize(bb);
1078 if (!ItemAdd(bb, 0))
1079 return;
1080
1081 // Render
1082 if (border_size > 0.0f)
1083 window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(border_col), 0.0f, ImDrawFlags_None, border_size);
1084 window->DrawList->AddImage(user_texture_id, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col));
1085}
1086
1087bool 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)
1088{
1089 ImGuiContext& g = *GImGui;
1090 ImGuiWindow* window = GetCurrentWindow();
1091 if (window->SkipItems)
1092 return false;
1093
1094 const ImVec2 padding = g.Style.FramePadding;
1095 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f);
1096 ItemSize(bb);
1097 if (!ItemAdd(bb, id))
1098 return false;
1099
1100 bool hovered, held;
1101 bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
1102
1103 // Render
1104 const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1105 RenderNavCursor(bb, id);
1106 RenderFrame(bb.Min, bb.Max, col, true, ImClamp((float)ImMin(padding.x, padding.y), 0.0f, g.Style.FrameRounding));
1107 if (bg_col.w > 0.0f)
1108 window->DrawList->AddRectFilled(bb.Min + padding, bb.Max - padding, GetColorU32(bg_col));
1109 window->DrawList->AddImage(user_texture_id, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col));
1110
1111 return pressed;
1112}
1113
1114// - ImageButton() adds style.FramePadding*2.0f to provided size. This is in order to facilitate fitting an image in a button.
1115// - ImageButton() draws a background based on regular Button() color + optionally an inner background if specified. (#8165) // FIXME: Maybe that's not the best design?
1116bool 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)
1117{
1118 ImGuiContext& g = *GImGui;
1119 ImGuiWindow* window = g.CurrentWindow;
1120 if (window->SkipItems)
1121 return false;
1122
1123 return ImageButtonEx(window->GetID(str_id), user_texture_id, image_size, uv0, uv1, bg_col, tint_col);
1124}
1125
1126#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1127// Legacy API obsoleted in 1.89. Two differences with new ImageButton()
1128// - old ImageButton() used ImTextureID as item id (created issue with multiple buttons with same image, transient texture id values, opaque computation of ID)
1129// - new ImageButton() requires an explicit 'const char* str_id'
1130// - old ImageButton() had frame_padding' override argument.
1131// - new ImageButton() always use style.FramePadding.
1132/*
1133bool 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)
1134{
1135 // Default to using texture ID as ID. User can still push string/integer prefixes.
1136 PushID((ImTextureID)(intptr_t)user_texture_id);
1137 if (frame_padding >= 0)
1138 PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2((float)frame_padding, (float)frame_padding));
1139 bool ret = ImageButton("", user_texture_id, size, uv0, uv1, bg_col, tint_col);
1140 if (frame_padding >= 0)
1141 PopStyleVar();
1142 PopID();
1143 return ret;
1144}
1145*/
1146#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1147
1148bool ImGui::Checkbox(const char* label, bool* v)
1149{
1150 ImGuiWindow* window = GetCurrentWindow();
1151 if (window->SkipItems)
1152 return false;
1153
1154 ImGuiContext& g = *GImGui;
1155 const ImGuiStyle& style = g.Style;
1156 const ImGuiID id = window->GetID(label);
1157 const ImVec2 label_size = CalcTextSize(label, NULL, true);
1158
1159 const float square_sz = GetFrameHeight();
1160 const ImVec2 pos = window->DC.CursorPos;
1161 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));
1162 ItemSize(total_bb, style.FramePadding.y);
1163 const bool is_visible = ItemAdd(total_bb, id);
1164 const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
1165 if (!is_visible)
1166 if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(total_bb)) // Extra layer of "no logic clip" for box-select support
1167 {
1168 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1169 return false;
1170 }
1171
1172 // Range-Selection/Multi-selection support (header)
1173 bool checked = *v;
1174 if (is_multi_select)
1175 MultiSelectItemHeader(id, &checked, NULL);
1176
1177 bool hovered, held;
1178 bool pressed = ButtonBehavior(total_bb, id, &hovered, &held);
1179
1180 // Range-Selection/Multi-selection support (footer)
1181 if (is_multi_select)
1182 MultiSelectItemFooter(id, &checked, &pressed);
1183 else if (pressed)
1184 checked = !checked;
1185
1186 if (*v != checked)
1187 {
1188 *v = checked;
1189 pressed = true; // return value
1190 MarkItemEdited(id);
1191 }
1192
1193 const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1194 const bool mixed_value = (g.LastItemData.ItemFlags & ImGuiItemFlags_MixedValue) != 0;
1195 if (is_visible)
1196 {
1197 RenderNavCursor(total_bb, id);
1198 RenderFrame(check_bb.Min, check_bb.Max, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), true, style.FrameRounding);
1199 ImU32 check_col = GetColorU32(ImGuiCol_CheckMark);
1200 if (mixed_value)
1201 {
1202 // Undocumented tristate/mixed/indeterminate checkbox (#2644)
1203 // This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all widgets (not just checkbox)
1204 ImVec2 pad(ImMax(1.0f, IM_TRUNC(square_sz / 3.6f)), ImMax(1.0f, IM_TRUNC(square_sz / 3.6f)));
1205 window->DrawList->AddRectFilled(check_bb.Min + pad, check_bb.Max - pad, check_col, style.FrameRounding);
1206 }
1207 else if (*v)
1208 {
1209 const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f));
1210 RenderCheckMark(window->DrawList, check_bb.Min + ImVec2(pad, pad), check_col, square_sz - pad * 2.0f);
1211 }
1212 }
1213 const ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1214 if (g.LogEnabled)
1215 LogRenderedText(&label_pos, mixed_value ? "[~]" : *v ? "[x]" : "[ ]");
1216 if (is_visible && label_size.x > 0.0f)
1217 RenderText(label_pos, label);
1218
1219 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1220 return pressed;
1221}
1222
1223template<typename T>
1224bool ImGui::CheckboxFlagsT(const char* label, T* flags, T flags_value)
1225{
1226 bool all_on = (*flags & flags_value) == flags_value;
1227 bool any_on = (*flags & flags_value) != 0;
1228 bool pressed;
1229 if (!all_on && any_on)
1230 {
1231 ImGuiContext& g = *GImGui;
1232 g.NextItemData.ItemFlags |= ImGuiItemFlags_MixedValue;
1233 pressed = Checkbox(label, &all_on);
1234 }
1235 else
1236 {
1237 pressed = Checkbox(label, &all_on);
1238
1239 }
1240 if (pressed)
1241 {
1242 if (all_on)
1243 *flags |= flags_value;
1244 else
1245 *flags &= ~flags_value;
1246 }
1247 return pressed;
1248}
1249
1250bool ImGui::CheckboxFlags(const char* label, int* flags, int flags_value)
1251{
1252 return CheckboxFlagsT(label, flags, flags_value);
1253}
1254
1255bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value)
1256{
1257 return CheckboxFlagsT(label, flags, flags_value);
1258}
1259
1260bool ImGui::CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value)
1261{
1262 return CheckboxFlagsT(label, flags, flags_value);
1263}
1264
1265bool ImGui::CheckboxFlags(const char* label, ImU64* flags, ImU64 flags_value)
1266{
1267 return CheckboxFlagsT(label, flags, flags_value);
1268}
1269
1270bool ImGui::RadioButton(const char* label, bool active)
1271{
1272 ImGuiWindow* window = GetCurrentWindow();
1273 if (window->SkipItems)
1274 return false;
1275
1276 ImGuiContext& g = *GImGui;
1277 const ImGuiStyle& style = g.Style;
1278 const ImGuiID id = window->GetID(label);
1279 const ImVec2 label_size = CalcTextSize(label, NULL, true);
1280
1281 const float square_sz = GetFrameHeight();
1282 const ImVec2 pos = window->DC.CursorPos;
1283 const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1284 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));
1285 ItemSize(total_bb, style.FramePadding.y);
1286 if (!ItemAdd(total_bb, id))
1287 return false;
1288
1289 ImVec2 center = check_bb.GetCenter();
1290 center.x = IM_ROUND(center.x);
1291 center.y = IM_ROUND(center.y);
1292 const float radius = (square_sz - 1.0f) * 0.5f;
1293
1294 bool hovered, held;
1295 bool pressed = ButtonBehavior(total_bb, id, &hovered, &held);
1296 if (pressed)
1297 MarkItemEdited(id);
1298
1299 RenderNavCursor(total_bb, id);
1300 const int num_segment = window->DrawList->_CalcCircleAutoSegmentCount(radius);
1301 window->DrawList->AddCircleFilled(center, radius, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), num_segment);
1302 if (active)
1303 {
1304 const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f));
1305 window->DrawList->AddCircleFilled(center, radius - pad, GetColorU32(ImGuiCol_CheckMark));
1306 }
1307
1308 if (style.FrameBorderSize > 0.0f)
1309 {
1310 window->DrawList->AddCircle(center + ImVec2(1, 1), radius, GetColorU32(ImGuiCol_BorderShadow), num_segment, style.FrameBorderSize);
1311 window->DrawList->AddCircle(center, radius, GetColorU32(ImGuiCol_Border), num_segment, style.FrameBorderSize);
1312 }
1313
1314 ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1315 if (g.LogEnabled)
1316 LogRenderedText(&label_pos, active ? "(x)" : "( )");
1317 if (label_size.x > 0.0f)
1318 RenderText(label_pos, label);
1319
1320 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
1321 return pressed;
1322}
1323
1324// 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..
1325bool ImGui::RadioButton(const char* label, int* v, int v_button)
1326{
1327 const bool pressed = RadioButton(label, *v == v_button);
1328 if (pressed)
1329 *v = v_button;
1330 return pressed;
1331}
1332
1333// size_arg (for each axis) < 0.0f: align to end, 0.0f: auto, > 0.0f: specified size
1334void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* overlay)
1335{
1336 ImGuiWindow* window = GetCurrentWindow();
1337 if (window->SkipItems)
1338 return;
1339
1340 ImGuiContext& g = *GImGui;
1341 const ImGuiStyle& style = g.Style;
1342
1343 ImVec2 pos = window->DC.CursorPos;
1344 ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), g.FontSize + style.FramePadding.y * 2.0f);
1345 ImRect bb(pos, pos + size);
1346 ItemSize(size, style.FramePadding.y);
1347 if (!ItemAdd(bb, 0))
1348 return;
1349
1350 // Fraction < 0.0f will display an indeterminate progress bar animation
1351 // The value must be animated along with time, so e.g. passing '-1.0f * ImGui::GetTime()' as fraction works.
1352 const bool is_indeterminate = (fraction < 0.0f);
1353 if (!is_indeterminate)
1354 fraction = ImSaturate(fraction);
1355
1356 // Out of courtesy we accept a NaN fraction without crashing
1357 float fill_n0 = 0.0f;
1358 float fill_n1 = (fraction == fraction) ? fraction : 0.0f;
1359
1360 if (is_indeterminate)
1361 {
1362 const float fill_width_n = 0.2f;
1363 fill_n0 = ImFmod(-fraction, 1.0f) * (1.0f + fill_width_n) - fill_width_n;
1364 fill_n1 = ImSaturate(fill_n0 + fill_width_n);
1365 fill_n0 = ImSaturate(fill_n0);
1366 }
1367
1368 // Render
1369 RenderFrame(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
1370 bb.Expand(ImVec2(-style.FrameBorderSize, -style.FrameBorderSize));
1371 RenderRectFilledRangeH(window->DrawList, bb, GetColorU32(ImGuiCol_PlotHistogram), fill_n0, fill_n1, style.FrameRounding);
1372
1373 // Default displaying the fraction as percentage string, but user can override it
1374 // Don't display text for indeterminate bars by default
1375 char overlay_buf[32];
1376 if (!is_indeterminate || overlay != NULL)
1377 {
1378 if (!overlay)
1379 {
1380 ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%.0f%%", fraction * 100 + 0.01f);
1381 overlay = overlay_buf;
1382 }
1383
1384 ImVec2 overlay_size = CalcTextSize(overlay, NULL);
1385 if (overlay_size.x > 0.0f)
1386 {
1387 float text_x = is_indeterminate ? (bb.Min.x + bb.Max.x - overlay_size.x) * 0.5f : ImLerp(bb.Min.x, bb.Max.x, fill_n1) + style.ItemSpacing.x;
1388 RenderTextClipped(ImVec2(ImClamp(text_x, bb.Min.x, bb.Max.x - overlay_size.x - style.ItemInnerSpacing.x), bb.Min.y), bb.Max, overlay, NULL, &overlay_size, ImVec2(0.0f, 0.5f), &bb);
1389 }
1390 }
1391}
1392
1393void ImGui::Bullet()
1394{
1395 ImGuiWindow* window = GetCurrentWindow();
1396 if (window->SkipItems)
1397 return;
1398
1399 ImGuiContext& g = *GImGui;
1400 const ImGuiStyle& style = g.Style;
1401 const float line_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y * 2), g.FontSize);
1402 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize, line_height));
1403 ItemSize(bb);
1404 if (!ItemAdd(bb, 0))
1405 {
1406 SameLine(0, style.FramePadding.x * 2);
1407 return;
1408 }
1409
1410 // Render and stay on same line
1411 ImU32 text_col = GetColorU32(ImGuiCol_Text);
1412 RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, line_height * 0.5f), text_col);
1413 SameLine(0, style.FramePadding.x * 2.0f);
1414}
1415
1416// This is provided as a convenience for being an often requested feature.
1417// FIXME-STYLE: we delayed adding as there is a larger plan to revamp the styling system.
1418// Because of this we currently don't provide many styling options for this widget
1419// (e.g. hovered/active colors are automatically inferred from a single color).
1420bool ImGui::TextLink(const char* label)
1421{
1422 ImGuiWindow* window = GetCurrentWindow();
1423 if (window->SkipItems)
1424 return false;
1425
1426 ImGuiContext& g = *GImGui;
1427 const ImGuiID id = window->GetID(label);
1428 const char* label_end = FindRenderedTextEnd(label);
1429
1430 ImVec2 pos = window->DC.CursorPos;
1431 ImVec2 size = CalcTextSize(label, label_end, true);
1432 ImRect bb(pos, pos + size);
1433 ItemSize(size, 0.0f);
1434 if (!ItemAdd(bb, id))
1435 return false;
1436
1437 bool hovered, held;
1438 bool pressed = ButtonBehavior(bb, id, &hovered, &held);
1439 RenderNavCursor(bb, id);
1440
1441 if (hovered)
1442 SetMouseCursor(ImGuiMouseCursor_Hand);
1443
1444 ImVec4 text_colf = g.Style.Colors[ImGuiCol_TextLink];
1445 ImVec4 line_colf = text_colf;
1446 {
1447 // FIXME-STYLE: Read comments above. This widget is NOT written in the same style as some earlier widgets,
1448 // as we are currently experimenting/planning a different styling system.
1449 float h, s, v;
1450 ColorConvertRGBtoHSV(text_colf.x, text_colf.y, text_colf.z, h, s, v);
1451 if (held || hovered)
1452 {
1453 v = ImSaturate(v + (held ? 0.4f : 0.3f));
1454 h = ImFmod(h + 0.02f, 1.0f);
1455 }
1456 ColorConvertHSVtoRGB(h, s, v, text_colf.x, text_colf.y, text_colf.z);
1457 v = ImSaturate(v - 0.20f);
1458 ColorConvertHSVtoRGB(h, s, v, line_colf.x, line_colf.y, line_colf.z);
1459 }
1460
1461 float line_y = bb.Max.y + ImFloor(g.Font->Descent * g.FontScale * 0.20f);
1462 window->DrawList->AddLine(ImVec2(bb.Min.x, line_y), ImVec2(bb.Max.x, line_y), GetColorU32(line_colf)); // FIXME-TEXT: Underline mode.
1463
1464 PushStyleColor(ImGuiCol_Text, GetColorU32(text_colf));
1465 RenderText(bb.Min, label, label_end);
1466 PopStyleColor();
1467
1468 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
1469 return pressed;
1470}
1471
1472void ImGui::TextLinkOpenURL(const char* label, const char* url)
1473{
1474 ImGuiContext& g = *GImGui;
1475 if (url == NULL)
1476 url = label;
1477 if (TextLink(label))
1478 if (g.PlatformIO.Platform_OpenInShellFn != NULL)
1479 g.PlatformIO.Platform_OpenInShellFn(&g, url);
1480 SetItemTooltip(LocalizeGetMsg(ImGuiLocKey_OpenLink_s), url); // It is more reassuring for user to _always_ display URL when we same as label
1481 if (BeginPopupContextItem())
1482 {
1483 if (MenuItem(LocalizeGetMsg(ImGuiLocKey_CopyLink)))
1484 SetClipboardText(url);
1485 EndPopup();
1486 }
1487}
1488
1489//-------------------------------------------------------------------------
1490// [SECTION] Widgets: Low-level Layout helpers
1491//-------------------------------------------------------------------------
1492// - Spacing()
1493// - Dummy()
1494// - NewLine()
1495// - AlignTextToFramePadding()
1496// - SeparatorEx() [Internal]
1497// - Separator()
1498// - SplitterBehavior() [Internal]
1499// - ShrinkWidths() [Internal]
1500//-------------------------------------------------------------------------
1501
1502void ImGui::Spacing()
1503{
1504 ImGuiWindow* window = GetCurrentWindow();
1505 if (window->SkipItems)
1506 return;
1507 ItemSize(ImVec2(0, 0));
1508}
1509
1510void ImGui::Dummy(const ImVec2& size)
1511{
1512 ImGuiWindow* window = GetCurrentWindow();
1513 if (window->SkipItems)
1514 return;
1515
1516 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
1517 ItemSize(size);
1518 ItemAdd(bb, 0);
1519}
1520
1521void ImGui::NewLine()
1522{
1523 ImGuiWindow* window = GetCurrentWindow();
1524 if (window->SkipItems)
1525 return;
1526
1527 ImGuiContext& g = *GImGui;
1528 const ImGuiLayoutType backup_layout_type = window->DC.LayoutType;
1529 window->DC.LayoutType = ImGuiLayoutType_Vertical;
1530 window->DC.IsSameLine = false;
1531 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.
1532 ItemSize(ImVec2(0, 0));
1533 else
1534 ItemSize(ImVec2(0.0f, g.FontSize));
1535 window->DC.LayoutType = backup_layout_type;
1536}
1537
1538void ImGui::AlignTextToFramePadding()
1539{
1540 ImGuiWindow* window = GetCurrentWindow();
1541 if (window->SkipItems)
1542 return;
1543
1544 ImGuiContext& g = *GImGui;
1545 window->DC.CurrLineSize.y = ImMax(window->DC.CurrLineSize.y, g.FontSize + g.Style.FramePadding.y * 2);
1546 window->DC.CurrLineTextBaseOffset = ImMax(window->DC.CurrLineTextBaseOffset, g.Style.FramePadding.y);
1547}
1548
1549// Horizontal/vertical separating line
1550// FIXME: Surprisingly, this seemingly trivial widget is a victim of many different legacy/tricky layout issues.
1551// Note how thickness == 1.0f is handled specifically as not moving CursorPos by 'thickness', but other values are.
1552void ImGui::SeparatorEx(ImGuiSeparatorFlags flags, float thickness)
1553{
1554 ImGuiWindow* window = GetCurrentWindow();
1555 if (window->SkipItems)
1556 return;
1557
1558 ImGuiContext& g = *GImGui;
1559 IM_ASSERT(ImIsPowerOfTwo(flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical))); // Check that only 1 option is selected
1560 IM_ASSERT(thickness > 0.0f);
1561
1562 if (flags & ImGuiSeparatorFlags_Vertical)
1563 {
1564 // Vertical separator, for menu bars (use current line height).
1565 float y1 = window->DC.CursorPos.y;
1566 float y2 = window->DC.CursorPos.y + window->DC.CurrLineSize.y;
1567 const ImRect bb(ImVec2(window->DC.CursorPos.x, y1), ImVec2(window->DC.CursorPos.x + thickness, y2));
1568 ItemSize(ImVec2(thickness, 0.0f));
1569 if (!ItemAdd(bb, 0))
1570 return;
1571
1572 // Draw
1573 window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Separator));
1574 if (g.LogEnabled)
1575 LogText(" |");
1576 }
1577 else if (flags & ImGuiSeparatorFlags_Horizontal)
1578 {
1579 // Horizontal Separator
1580 float x1 = window->DC.CursorPos.x;
1581 float x2 = window->WorkRect.Max.x;
1582
1583 // Preserve legacy behavior inside Columns()
1584 // Before Tables API happened, we relied on Separator() to span all columns of a Columns() set.
1585 // We currently don't need to provide the same feature for tables because tables naturally have border features.
1586 ImGuiOldColumns* columns = (flags & ImGuiSeparatorFlags_SpanAllColumns) ? window->DC.CurrentColumns : NULL;
1587 if (columns)
1588 {
1589 x1 = window->Pos.x + window->DC.Indent.x; // Used to be Pos.x before 2023/10/03
1590 x2 = window->Pos.x + window->Size.x;
1591 PushColumnsBackground();
1592 }
1593
1594 // We don't provide our width to the layout so that it doesn't get feed back into AutoFit
1595 // FIXME: This prevents ->CursorMaxPos based bounding box evaluation from working (e.g. TableEndCell)
1596 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.
1597 const ImRect bb(ImVec2(x1, window->DC.CursorPos.y), ImVec2(x2, window->DC.CursorPos.y + thickness));
1598 ItemSize(ImVec2(0.0f, thickness_for_layout));
1599
1600 if (ItemAdd(bb, 0))
1601 {
1602 // Draw
1603 window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Separator));
1604 if (g.LogEnabled)
1605 LogRenderedText(&bb.Min, "--------------------------------\n");
1606
1607 }
1608 if (columns)
1609 {
1610 PopColumnsBackground();
1611 columns->LineMinY = window->DC.CursorPos.y;
1612 }
1613 }
1614}
1615
1616void ImGui::Separator()
1617{
1618 ImGuiContext& g = *GImGui;
1619 ImGuiWindow* window = g.CurrentWindow;
1620 if (window->SkipItems)
1621 return;
1622
1623 // Those flags should eventually be configurable by the user
1624 // FIXME: We cannot g.Style.SeparatorTextBorderSize for thickness as it relates to SeparatorText() which is a decorated separator, not defaulting to 1.0f.
1625 ImGuiSeparatorFlags flags = (window->DC.LayoutType == ImGuiLayoutType_Horizontal) ? ImGuiSeparatorFlags_Vertical : ImGuiSeparatorFlags_Horizontal;
1626
1627 // Only applies to legacy Columns() api as they relied on Separator() a lot.
1628 if (window->DC.CurrentColumns)
1629 flags |= ImGuiSeparatorFlags_SpanAllColumns;
1630
1631 SeparatorEx(flags, 1.0f);
1632}
1633
1634void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end, float extra_w)
1635{
1636 ImGuiContext& g = *GImGui;
1637 ImGuiWindow* window = g.CurrentWindow;
1638 ImGuiStyle& style = g.Style;
1639
1640 const ImVec2 label_size = CalcTextSize(label, label_end, false);
1641 const ImVec2 pos = window->DC.CursorPos;
1642 const ImVec2 padding = style.SeparatorTextPadding;
1643
1644 const float separator_thickness = style.SeparatorTextBorderSize;
1645 const ImVec2 min_size(label_size.x + extra_w + padding.x * 2.0f, ImMax(label_size.y + padding.y * 2.0f, separator_thickness));
1646 const ImRect bb(pos, ImVec2(window->WorkRect.Max.x, pos.y + min_size.y));
1647 const float text_baseline_y = ImTrunc((bb.GetHeight() - label_size.y) * style.SeparatorTextAlign.y + 0.99999f); //ImMax(padding.y, ImTrunc((style.SeparatorTextSize - label_size.y) * 0.5f));
1648 ItemSize(min_size, text_baseline_y);
1649 if (!ItemAdd(bb, id))
1650 return;
1651
1652 const float sep1_x1 = pos.x;
1653 const float sep2_x2 = bb.Max.x;
1654 const float seps_y = ImTrunc((bb.Min.y + bb.Max.y) * 0.5f + 0.99999f);
1655
1656 const float label_avail_w = ImMax(0.0f, sep2_x2 - sep1_x1 - padding.x * 2.0f);
1657 const ImVec2 label_pos(pos.x + padding.x + ImMax(0.0f, (label_avail_w - label_size.x - extra_w) * style.SeparatorTextAlign.x), pos.y + text_baseline_y); // FIXME-ALIGN
1658
1659 // This allows using SameLine() to position something in the 'extra_w'
1660 window->DC.CursorPosPrevLine.x = label_pos.x + label_size.x;
1661
1662 const ImU32 separator_col = GetColorU32(ImGuiCol_Separator);
1663 if (label_size.x > 0.0f)
1664 {
1665 const float sep1_x2 = label_pos.x - style.ItemSpacing.x;
1666 const float sep2_x1 = label_pos.x + label_size.x + extra_w + style.ItemSpacing.x;
1667 if (sep1_x2 > sep1_x1 && separator_thickness > 0.0f)
1668 window->DrawList->AddLine(ImVec2(sep1_x1, seps_y), ImVec2(sep1_x2, seps_y), separator_col, separator_thickness);
1669 if (sep2_x2 > sep2_x1 && separator_thickness > 0.0f)
1670 window->DrawList->AddLine(ImVec2(sep2_x1, seps_y), ImVec2(sep2_x2, seps_y), separator_col, separator_thickness);
1671 if (g.LogEnabled)
1672 LogSetNextTextDecoration("---", NULL);
1673 RenderTextEllipsis(window->DrawList, label_pos, ImVec2(bb.Max.x, bb.Max.y + style.ItemSpacing.y), bb.Max.x, bb.Max.x, label, label_end, &label_size);
1674 }
1675 else
1676 {
1677 if (g.LogEnabled)
1678 LogText("---");
1679 if (separator_thickness > 0.0f)
1680 window->DrawList->AddLine(ImVec2(sep1_x1, seps_y), ImVec2(sep2_x2, seps_y), separator_col, separator_thickness);
1681 }
1682}
1683
1684void ImGui::SeparatorText(const char* label)
1685{
1686 ImGuiWindow* window = GetCurrentWindow();
1687 if (window->SkipItems)
1688 return;
1689
1690 // The SeparatorText() vs SeparatorTextEx() distinction is designed to be considerate that we may want:
1691 // - allow separator-text to be draggable items (would require a stable ID + a noticeable highlight)
1692 // - this high-level entry point to allow formatting? (which in turns may require ID separate from formatted string)
1693 // - because of this we probably can't turn 'const char* label' into 'const char* fmt, ...'
1694 // Otherwise, we can decide that users wanting to drag this would layout a dedicated drag-item,
1695 // and then we can turn this into a format function.
1696 SeparatorTextEx(0, label, FindRenderedTextEnd(label), 0.0f);
1697}
1698
1699// 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.
1700bool 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)
1701{
1702 ImGuiContext& g = *GImGui;
1703 ImGuiWindow* window = g.CurrentWindow;
1704
1705 if (!ItemAdd(bb, id, NULL, ImGuiItemFlags_NoNav))
1706 return false;
1707
1708 // FIXME: AFAIK the only leftover reason for passing ImGuiButtonFlags_AllowOverlap here is
1709 // to allow caller of SplitterBehavior() to call SetItemAllowOverlap() after the item.
1710 // Nowadays we would instead want to use SetNextItemAllowOverlap() before the item.
1711 ImGuiButtonFlags button_flags = ImGuiButtonFlags_FlattenChildren;
1712#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1713 button_flags |= ImGuiButtonFlags_AllowOverlap;
1714#endif
1715
1716 bool hovered, held;
1717 ImRect bb_interact = bb;
1718 bb_interact.Expand(axis == ImGuiAxis_Y ? ImVec2(0.0f, hover_extend) : ImVec2(hover_extend, 0.0f));
1719 ButtonBehavior(bb_interact, id, &hovered, &held, button_flags);
1720 if (hovered)
1721 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; // for IsItemHovered(), because bb_interact is larger than bb
1722
1723 if (held || (hovered && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= hover_visibility_delay))
1724 SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW);
1725
1726 ImRect bb_render = bb;
1727 if (held)
1728 {
1729 float mouse_delta = (g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min)[axis];
1730
1731 // Minimum pane size
1732 float size_1_maximum_delta = ImMax(0.0f, *size1 - min_size1);
1733 float size_2_maximum_delta = ImMax(0.0f, *size2 - min_size2);
1734 if (mouse_delta < -size_1_maximum_delta)
1735 mouse_delta = -size_1_maximum_delta;
1736 if (mouse_delta > size_2_maximum_delta)
1737 mouse_delta = size_2_maximum_delta;
1738
1739 // Apply resize
1740 if (mouse_delta != 0.0f)
1741 {
1742 *size1 = ImMax(*size1 + mouse_delta, min_size1);
1743 *size2 = ImMax(*size2 - mouse_delta, min_size2);
1744 bb_render.Translate((axis == ImGuiAxis_X) ? ImVec2(mouse_delta, 0.0f) : ImVec2(0.0f, mouse_delta));
1745 MarkItemEdited(id);
1746 }
1747 }
1748
1749 // Render at new position
1750 if (bg_col & IM_COL32_A_MASK)
1751 window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, bg_col, 0.0f);
1752 const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : (hovered && g.HoveredIdTimer >= hover_visibility_delay) ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator);
1753 window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, col, 0.0f);
1754
1755 return held;
1756}
1757
1758static int IMGUI_CDECL ShrinkWidthItemComparer(const void* lhs, const void* rhs)
1759{
1760 const ImGuiShrinkWidthItem* a = (const ImGuiShrinkWidthItem*)lhs;
1761 const ImGuiShrinkWidthItem* b = (const ImGuiShrinkWidthItem*)rhs;
1762 if (int d = (int)(b->Width - a->Width))
1763 return d;
1764 return (b->Index - a->Index);
1765}
1766
1767// Shrink excess width from a set of item, by removing width from the larger items first.
1768// Set items Width to -1.0f to disable shrinking this item.
1769void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess)
1770{
1771 if (count == 1)
1772 {
1773 if (items[0].Width >= 0.0f)
1774 items[0].Width = ImMax(items[0].Width - width_excess, 1.0f);
1775 return;
1776 }
1777 ImQsort(items, (size_t)count, sizeof(ImGuiShrinkWidthItem), ShrinkWidthItemComparer);
1778 int count_same_width = 1;
1779 while (width_excess > 0.0f && count_same_width < count)
1780 {
1781 while (count_same_width < count && items[0].Width <= items[count_same_width].Width)
1782 count_same_width++;
1783 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);
1784 if (max_width_to_remove_per_item <= 0.0f)
1785 break;
1786 float width_to_remove_per_item = ImMin(width_excess / count_same_width, max_width_to_remove_per_item);
1787 for (int item_n = 0; item_n < count_same_width; item_n++)
1788 items[item_n].Width -= width_to_remove_per_item;
1789 width_excess -= width_to_remove_per_item * count_same_width;
1790 }
1791
1792 // Round width and redistribute remainder
1793 // 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.
1794 width_excess = 0.0f;
1795 for (int n = 0; n < count; n++)
1796 {
1797 float width_rounded = ImTrunc(items[n].Width);
1798 width_excess += items[n].Width - width_rounded;
1799 items[n].Width = width_rounded;
1800 }
1801 while (width_excess > 0.0f)
1802 for (int n = 0; n < count && width_excess > 0.0f; n++)
1803 {
1804 float width_to_add = ImMin(items[n].InitialWidth - items[n].Width, 1.0f);
1805 items[n].Width += width_to_add;
1806 width_excess -= width_to_add;
1807 }
1808}
1809
1810//-------------------------------------------------------------------------
1811// [SECTION] Widgets: ComboBox
1812//-------------------------------------------------------------------------
1813// - CalcMaxPopupHeightFromItemCount() [Internal]
1814// - BeginCombo()
1815// - BeginComboPopup() [Internal]
1816// - EndCombo()
1817// - BeginComboPreview() [Internal]
1818// - EndComboPreview() [Internal]
1819// - Combo()
1820//-------------------------------------------------------------------------
1821
1822static float CalcMaxPopupHeightFromItemCount(int items_count)
1823{
1824 ImGuiContext& g = *GImGui;
1825 if (items_count <= 0)
1826 return FLT_MAX;
1827 return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2);
1828}
1829
1830bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags)
1831{
1832 ImGuiContext& g = *GImGui;
1833 ImGuiWindow* window = GetCurrentWindow();
1834
1835 ImGuiNextWindowDataFlags backup_next_window_data_flags = g.NextWindowData.Flags;
1836 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
1837 if (window->SkipItems)
1838 return false;
1839
1840 const ImGuiStyle& style = g.Style;
1841 const ImGuiID id = window->GetID(label);
1842 IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together
1843 if (flags & ImGuiComboFlags_WidthFitPreview)
1844 IM_ASSERT((flags & (ImGuiComboFlags_NoPreview | (ImGuiComboFlags)ImGuiComboFlags_CustomPreview)) == 0);
1845
1846 const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight();
1847 const ImVec2 label_size = CalcTextSize(label, NULL, true);
1848 const float preview_width = ((flags & ImGuiComboFlags_WidthFitPreview) && (preview_value != NULL)) ? CalcTextSize(preview_value, NULL, true).x : 0.0f;
1849 const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : ((flags & ImGuiComboFlags_WidthFitPreview) ? (arrow_size + preview_width + style.FramePadding.x * 2.0f) : CalcItemWidth());
1850 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
1851 const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
1852 ItemSize(total_bb, style.FramePadding.y);
1853 if (!ItemAdd(total_bb, id, &bb))
1854 return false;
1855
1856 // Open on click
1857 bool hovered, held;
1858 bool pressed = ButtonBehavior(bb, id, &hovered, &held);
1859 const ImGuiID popup_id = ImHashStr("##ComboPopup", 0, id);
1860 bool popup_open = IsPopupOpen(popup_id, ImGuiPopupFlags_None);
1861 if (pressed && !popup_open)
1862 {
1863 OpenPopupEx(popup_id, ImGuiPopupFlags_None);
1864 popup_open = true;
1865 }
1866
1867 // Render shape
1868 const ImU32 frame_col = GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
1869 const float value_x2 = ImMax(bb.Min.x, bb.Max.x - arrow_size);
1870 RenderNavCursor(bb, id);
1871 if (!(flags & ImGuiComboFlags_NoPreview))
1872 window->DrawList->AddRectFilled(bb.Min, ImVec2(value_x2, bb.Max.y), frame_col, style.FrameRounding, (flags & ImGuiComboFlags_NoArrowButton) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersLeft);
1873 if (!(flags & ImGuiComboFlags_NoArrowButton))
1874 {
1875 ImU32 bg_col = GetColorU32((popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1876 ImU32 text_col = GetColorU32(ImGuiCol_Text);
1877 window->DrawList->AddRectFilled(ImVec2(value_x2, bb.Min.y), bb.Max, bg_col, style.FrameRounding, (w <= arrow_size) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersRight);
1878 if (value_x2 + arrow_size - style.FramePadding.x <= bb.Max.x)
1879 RenderArrow(window->DrawList, ImVec2(value_x2 + style.FramePadding.y, bb.Min.y + style.FramePadding.y), text_col, ImGuiDir_Down, 1.0f);
1880 }
1881 RenderFrameBorder(bb.Min, bb.Max, style.FrameRounding);
1882
1883 // Custom preview
1884 if (flags & ImGuiComboFlags_CustomPreview)
1885 {
1886 g.ComboPreviewData.PreviewRect = ImRect(bb.Min.x, bb.Min.y, value_x2, bb.Max.y);
1887 IM_ASSERT(preview_value == NULL || preview_value[0] == 0);
1888 preview_value = NULL;
1889 }
1890
1891 // Render preview and label
1892 if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview))
1893 {
1894 if (g.LogEnabled)
1895 LogSetNextTextDecoration("{", "}");
1896 RenderTextClipped(bb.Min + style.FramePadding, ImVec2(value_x2, bb.Max.y), preview_value, NULL, NULL);
1897 }
1898 if (label_size.x > 0)
1899 RenderText(ImVec2(bb.Max.x + style.ItemInnerSpacing.x, bb.Min.y + style.FramePadding.y), label);
1900
1901 if (!popup_open)
1902 return false;
1903
1904 g.NextWindowData.Flags = backup_next_window_data_flags;
1905 return BeginComboPopup(popup_id, bb, flags);
1906}
1907
1908bool ImGui::BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags flags)
1909{
1910 ImGuiContext& g = *GImGui;
1911 if (!IsPopupOpen(popup_id, ImGuiPopupFlags_None))
1912 {
1913 g.NextWindowData.ClearFlags();
1914 return false;
1915 }
1916
1917 // Set popup size
1918 float w = bb.GetWidth();
1919 if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint)
1920 {
1921 g.NextWindowData.SizeConstraintRect.Min.x = ImMax(g.NextWindowData.SizeConstraintRect.Min.x, w);
1922 }
1923 else
1924 {
1925 if ((flags & ImGuiComboFlags_HeightMask_) == 0)
1926 flags |= ImGuiComboFlags_HeightRegular;
1927 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one
1928 int popup_max_height_in_items = -1;
1929 if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8;
1930 else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4;
1931 else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20;
1932 ImVec2 constraint_min(0.0f, 0.0f), constraint_max(FLT_MAX, FLT_MAX);
1933 if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.x <= 0.0f) // Don't apply constraints if user specified a size
1934 constraint_min.x = w;
1935 if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.y <= 0.0f)
1936 constraint_max.y = CalcMaxPopupHeightFromItemCount(popup_max_height_in_items);
1937 SetNextWindowSizeConstraints(constraint_min, constraint_max);
1938 }
1939
1940 // This is essentially a specialized version of BeginPopupEx()
1941 char name[16];
1942 ImFormatString(name, IM_ARRAYSIZE(name), "##Combo_%02d", g.BeginComboDepth); // Recycle windows based on depth
1943
1944 // Set position given a custom constraint (peak into expected window size so we can position it)
1945 // FIXME: This might be easier to express with an hypothetical SetNextWindowPosConstraints() function?
1946 // FIXME: This might be moved to Begin() or at least around the same spot where Tooltips and other Popups are calling FindBestWindowPosForPopupEx()?
1947 if (ImGuiWindow* popup_window = FindWindowByName(name))
1948 if (popup_window->WasActive)
1949 {
1950 // Always override 'AutoPosLastDirection' to not leave a chance for a past value to affect us.
1951 ImVec2 size_expected = CalcWindowNextAutoFitSize(popup_window);
1952 popup_window->AutoPosLastDirection = (flags & ImGuiComboFlags_PopupAlignLeft) ? ImGuiDir_Left : ImGuiDir_Down; // Left = "Below, Toward Left", Down = "Below, Toward Right (default)"
1953 ImRect r_outer = GetPopupAllowedExtentRect(popup_window);
1954 ImVec2 pos = FindBestWindowPosForPopupEx(bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, r_outer, bb, ImGuiPopupPositionPolicy_ComboBox);
1955 SetNextWindowPos(pos);
1956 }
1957
1958 // We don't use BeginPopupEx() solely because we have a custom name string, which we could make an argument to BeginPopupEx()
1959 ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove;
1960 PushStyleVarX(ImGuiStyleVar_WindowPadding, g.Style.FramePadding.x); // Horizontally align ourselves with the framed text
1961 bool ret = Begin(name, NULL, window_flags);
1962 PopStyleVar();
1963 if (!ret)
1964 {
1965 EndPopup();
1966 IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above
1967 return false;
1968 }
1969 g.BeginComboDepth++;
1970 return true;
1971}
1972
1973void ImGui::EndCombo()
1974{
1975 ImGuiContext& g = *GImGui;
1976 EndPopup();
1977 g.BeginComboDepth--;
1978}
1979
1980// Call directly after the BeginCombo/EndCombo block. The preview is designed to only host non-interactive elements
1981// (Experimental, see GitHub issues: #1658, #4168)
1982bool ImGui::BeginComboPreview()
1983{
1984 ImGuiContext& g = *GImGui;
1985 ImGuiWindow* window = g.CurrentWindow;
1986 ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
1987
1988 if (window->SkipItems || !(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible))
1989 return false;
1990 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?
1991 if (!window->ClipRect.Overlaps(preview_data->PreviewRect)) // Narrower test (optional)
1992 return false;
1993
1994 // FIXME: This could be contained in a PushWorkRect() api
1995 preview_data->BackupCursorPos = window->DC.CursorPos;
1996 preview_data->BackupCursorMaxPos = window->DC.CursorMaxPos;
1997 preview_data->BackupCursorPosPrevLine = window->DC.CursorPosPrevLine;
1998 preview_data->BackupPrevLineTextBaseOffset = window->DC.PrevLineTextBaseOffset;
1999 preview_data->BackupLayout = window->DC.LayoutType;
2000 window->DC.CursorPos = preview_data->PreviewRect.Min + g.Style.FramePadding;
2001 window->DC.CursorMaxPos = window->DC.CursorPos;
2002 window->DC.LayoutType = ImGuiLayoutType_Horizontal;
2003 window->DC.IsSameLine = false;
2004 PushClipRect(preview_data->PreviewRect.Min, preview_data->PreviewRect.Max, true);
2005
2006 return true;
2007}
2008
2009void ImGui::EndComboPreview()
2010{
2011 ImGuiContext& g = *GImGui;
2012 ImGuiWindow* window = g.CurrentWindow;
2013 ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
2014
2015 // FIXME: Using CursorMaxPos approximation instead of correct AABB which we will store in ImDrawCmd in the future
2016 ImDrawList* draw_list = window->DrawList;
2017 if (window->DC.CursorMaxPos.x < preview_data->PreviewRect.Max.x && window->DC.CursorMaxPos.y < preview_data->PreviewRect.Max.y)
2018 if (draw_list->CmdBuffer.Size > 1) // Unlikely case that the PushClipRect() didn't create a command
2019 {
2020 draw_list->_CmdHeader.ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 2].ClipRect;
2021 draw_list->_TryMergeDrawCmds();
2022 }
2023 PopClipRect();
2024 window->DC.CursorPos = preview_data->BackupCursorPos;
2025 window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, preview_data->BackupCursorMaxPos);
2026 window->DC.CursorPosPrevLine = preview_data->BackupCursorPosPrevLine;
2027 window->DC.PrevLineTextBaseOffset = preview_data->BackupPrevLineTextBaseOffset;
2028 window->DC.LayoutType = preview_data->BackupLayout;
2029 window->DC.IsSameLine = false;
2030 preview_data->PreviewRect = ImRect();
2031}
2032
2033// Getter for the old Combo() API: const char*[]
2034static const char* Items_ArrayGetter(void* data, int idx)
2035{
2036 const char* const* items = (const char* const*)data;
2037 return items[idx];
2038}
2039
2040// Getter for the old Combo() API: "item1\0item2\0item3\0"
2041static const char* Items_SingleStringGetter(void* data, int idx)
2042{
2043 const char* items_separated_by_zeros = (const char*)data;
2044 int items_count = 0;
2045 const char* p = items_separated_by_zeros;
2046 while (*p)
2047 {
2048 if (idx == items_count)
2049 break;
2050 p += strlen(p) + 1;
2051 items_count++;
2052 }
2053 return *p ? p : NULL;
2054}
2055
2056// Old API, prefer using BeginCombo() nowadays if you can.
2057bool 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)
2058{
2059 ImGuiContext& g = *GImGui;
2060
2061 // Call the getter to obtain the preview string which is a parameter to BeginCombo()
2062 const char* preview_value = NULL;
2063 if (*current_item >= 0 && *current_item < items_count)
2064 preview_value = getter(user_data, *current_item);
2065
2066 // 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.
2067 if (popup_max_height_in_items != -1 && !(g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint))
2068 SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items)));
2069
2070 if (!BeginCombo(label, preview_value, ImGuiComboFlags_None))
2071 return false;
2072
2073 // Display items
2074 bool value_changed = false;
2075 ImGuiListClipper clipper;
2076 clipper.Begin(items_count);
2077 clipper.IncludeItemByIndex(*current_item);
2078 while (clipper.Step())
2079 for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
2080 {
2081 const char* item_text = getter(user_data, i);
2082 if (item_text == NULL)
2083 item_text = "*Unknown item*";
2084
2085 PushID(i);
2086 const bool item_selected = (i == *current_item);
2087 if (Selectable(item_text, item_selected) && *current_item != i)
2088 {
2089 value_changed = true;
2090 *current_item = i;
2091 }
2092 if (item_selected)
2093 SetItemDefaultFocus();
2094 PopID();
2095 }
2096
2097 EndCombo();
2098 if (value_changed)
2099 MarkItemEdited(g.LastItemData.ID);
2100
2101 return value_changed;
2102}
2103
2104// Combo box helper allowing to pass an array of strings.
2105bool ImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items)
2106{
2107 const bool value_changed = Combo(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_in_items);
2108 return value_changed;
2109}
2110
2111// Combo box helper allowing to pass all items in a single string literal holding multiple zero-terminated items "item1\0item2\0"
2112bool ImGui::Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int height_in_items)
2113{
2114 int items_count = 0;
2115 const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open
2116 while (*p)
2117 {
2118 p += strlen(p) + 1;
2119 items_count++;
2120 }
2121 bool value_changed = Combo(label, current_item, Items_SingleStringGetter, (void*)items_separated_by_zeros, items_count, height_in_items);
2122 return value_changed;
2123}
2124
2125#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
2126
2127struct ImGuiGetNameFromIndexOldToNewCallbackData { void* UserData; bool (*OldCallback)(void*, int, const char**); };
2128static const char* ImGuiGetNameFromIndexOldToNewCallback(void* user_data, int idx)
2129{
2130 ImGuiGetNameFromIndexOldToNewCallbackData* data = (ImGuiGetNameFromIndexOldToNewCallbackData*)user_data;
2131 const char* s = NULL;
2132 data->OldCallback(data->UserData, idx, &s);
2133 return s;
2134}
2135
2136bool 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)
2137{
2138 ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { user_data, old_getter };
2139 return ListBox(label, current_item, ImGuiGetNameFromIndexOldToNewCallback, &old_to_new_data, items_count, height_in_items);
2140}
2141bool 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)
2142{
2143 ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { user_data, old_getter };
2144 return Combo(label, current_item, ImGuiGetNameFromIndexOldToNewCallback, &old_to_new_data, items_count, popup_max_height_in_items);
2145}
2146
2147#endif
2148
2149//-------------------------------------------------------------------------
2150// [SECTION] Data Type and Data Formatting Helpers [Internal]
2151//-------------------------------------------------------------------------
2152// - DataTypeGetInfo()
2153// - DataTypeFormatString()
2154// - DataTypeApplyOp()
2155// - DataTypeApplyFromText()
2156// - DataTypeCompare()
2157// - DataTypeClamp()
2158// - GetMinimumStepAtDecimalPrecision
2159// - RoundScalarWithFormat<>()
2160//-------------------------------------------------------------------------
2161
2162static const ImGuiDataTypeInfo GDataTypeInfo[] =
2163{
2164 { sizeof(char), "S8", "%d", "%d" }, // ImGuiDataType_S8
2165 { sizeof(unsigned char), "U8", "%u", "%u" },
2166 { sizeof(short), "S16", "%d", "%d" }, // ImGuiDataType_S16
2167 { sizeof(unsigned short), "U16", "%u", "%u" },
2168 { sizeof(int), "S32", "%d", "%d" }, // ImGuiDataType_S32
2169 { sizeof(unsigned int), "U32", "%u", "%u" },
2170#ifdef _MSC_VER
2171 { sizeof(ImS64), "S64", "%I64d","%I64d" }, // ImGuiDataType_S64
2172 { sizeof(ImU64), "U64", "%I64u","%I64u" },
2173#else
2174 { sizeof(ImS64), "S64", "%lld", "%lld" }, // ImGuiDataType_S64
2175 { sizeof(ImU64), "U64", "%llu", "%llu" },
2176#endif
2177 { sizeof(float), "float", "%.3f","%f" }, // ImGuiDataType_Float (float are promoted to double in va_arg)
2178 { sizeof(double), "double","%f", "%lf" }, // ImGuiDataType_Double
2179 { sizeof(bool), "bool", "%d", "%d" }, // ImGuiDataType_Bool
2180 { 0, "char*","%s", "%s" }, // ImGuiDataType_String
2181};
2182IM_STATIC_ASSERT(IM_ARRAYSIZE(GDataTypeInfo) == ImGuiDataType_COUNT);
2183
2184const ImGuiDataTypeInfo* ImGui::DataTypeGetInfo(ImGuiDataType data_type)
2185{
2186 IM_ASSERT(data_type >= 0 && data_type < ImGuiDataType_COUNT);
2187 return &GDataTypeInfo[data_type];
2188}
2189
2190int ImGui::DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* p_data, const char* format)
2191{
2192 // Signedness doesn't matter when pushing integer arguments
2193 if (data_type == ImGuiDataType_S32 || data_type == ImGuiDataType_U32)
2194 return ImFormatString(buf, buf_size, format, *(const ImU32*)p_data);
2195 if (data_type == ImGuiDataType_S64 || data_type == ImGuiDataType_U64)
2196 return ImFormatString(buf, buf_size, format, *(const ImU64*)p_data);
2197 if (data_type == ImGuiDataType_Float)
2198 return ImFormatString(buf, buf_size, format, *(const float*)p_data);
2199 if (data_type == ImGuiDataType_Double)
2200 return ImFormatString(buf, buf_size, format, *(const double*)p_data);
2201 if (data_type == ImGuiDataType_S8)
2202 return ImFormatString(buf, buf_size, format, *(const ImS8*)p_data);
2203 if (data_type == ImGuiDataType_U8)
2204 return ImFormatString(buf, buf_size, format, *(const ImU8*)p_data);
2205 if (data_type == ImGuiDataType_S16)
2206 return ImFormatString(buf, buf_size, format, *(const ImS16*)p_data);
2207 if (data_type == ImGuiDataType_U16)
2208 return ImFormatString(buf, buf_size, format, *(const ImU16*)p_data);
2209 IM_ASSERT(0);
2210 return 0;
2211}
2212
2213void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg1, const void* arg2)
2214{
2215 IM_ASSERT(op == '+' || op == '-');
2216 switch (data_type)
2217 {
2218 case ImGuiDataType_S8:
2219 if (op == '+') { *(ImS8*)output = ImAddClampOverflow(*(const ImS8*)arg1, *(const ImS8*)arg2, IM_S8_MIN, IM_S8_MAX); }
2220 if (op == '-') { *(ImS8*)output = ImSubClampOverflow(*(const ImS8*)arg1, *(const ImS8*)arg2, IM_S8_MIN, IM_S8_MAX); }
2221 return;
2222 case ImGuiDataType_U8:
2223 if (op == '+') { *(ImU8*)output = ImAddClampOverflow(*(const ImU8*)arg1, *(const ImU8*)arg2, IM_U8_MIN, IM_U8_MAX); }
2224 if (op == '-') { *(ImU8*)output = ImSubClampOverflow(*(const ImU8*)arg1, *(const ImU8*)arg2, IM_U8_MIN, IM_U8_MAX); }
2225 return;
2226 case ImGuiDataType_S16:
2227 if (op == '+') { *(ImS16*)output = ImAddClampOverflow(*(const ImS16*)arg1, *(const ImS16*)arg2, IM_S16_MIN, IM_S16_MAX); }
2228 if (op == '-') { *(ImS16*)output = ImSubClampOverflow(*(const ImS16*)arg1, *(const ImS16*)arg2, IM_S16_MIN, IM_S16_MAX); }
2229 return;
2230 case ImGuiDataType_U16:
2231 if (op == '+') { *(ImU16*)output = ImAddClampOverflow(*(const ImU16*)arg1, *(const ImU16*)arg2, IM_U16_MIN, IM_U16_MAX); }
2232 if (op == '-') { *(ImU16*)output = ImSubClampOverflow(*(const ImU16*)arg1, *(const ImU16*)arg2, IM_U16_MIN, IM_U16_MAX); }
2233 return;
2234 case ImGuiDataType_S32:
2235 if (op == '+') { *(ImS32*)output = ImAddClampOverflow(*(const ImS32*)arg1, *(const ImS32*)arg2, IM_S32_MIN, IM_S32_MAX); }
2236 if (op == '-') { *(ImS32*)output = ImSubClampOverflow(*(const ImS32*)arg1, *(const ImS32*)arg2, IM_S32_MIN, IM_S32_MAX); }
2237 return;
2238 case ImGuiDataType_U32:
2239 if (op == '+') { *(ImU32*)output = ImAddClampOverflow(*(const ImU32*)arg1, *(const ImU32*)arg2, IM_U32_MIN, IM_U32_MAX); }
2240 if (op == '-') { *(ImU32*)output = ImSubClampOverflow(*(const ImU32*)arg1, *(const ImU32*)arg2, IM_U32_MIN, IM_U32_MAX); }
2241 return;
2242 case ImGuiDataType_S64:
2243 if (op == '+') { *(ImS64*)output = ImAddClampOverflow(*(const ImS64*)arg1, *(const ImS64*)arg2, IM_S64_MIN, IM_S64_MAX); }
2244 if (op == '-') { *(ImS64*)output = ImSubClampOverflow(*(const ImS64*)arg1, *(const ImS64*)arg2, IM_S64_MIN, IM_S64_MAX); }
2245 return;
2246 case ImGuiDataType_U64:
2247 if (op == '+') { *(ImU64*)output = ImAddClampOverflow(*(const ImU64*)arg1, *(const ImU64*)arg2, IM_U64_MIN, IM_U64_MAX); }
2248 if (op == '-') { *(ImU64*)output = ImSubClampOverflow(*(const ImU64*)arg1, *(const ImU64*)arg2, IM_U64_MIN, IM_U64_MAX); }
2249 return;
2250 case ImGuiDataType_Float:
2251 if (op == '+') { *(float*)output = *(const float*)arg1 + *(const float*)arg2; }
2252 if (op == '-') { *(float*)output = *(const float*)arg1 - *(const float*)arg2; }
2253 return;
2254 case ImGuiDataType_Double:
2255 if (op == '+') { *(double*)output = *(const double*)arg1 + *(const double*)arg2; }
2256 if (op == '-') { *(double*)output = *(const double*)arg1 - *(const double*)arg2; }
2257 return;
2258 case ImGuiDataType_COUNT: break;
2259 }
2260 IM_ASSERT(0);
2261}
2262
2263// User can input math operators (e.g. +100) to edit a numerical values.
2264// NB: This is _not_ a full expression evaluator. We should probably add one and replace this dumb mess..
2265bool ImGui::DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format, void* p_data_when_empty)
2266{
2267 // Copy the value in an opaque buffer so we can compare at the end of the function if it changed at all.
2268 const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
2269 ImGuiDataTypeStorage data_backup;
2270 memcpy(&data_backup, p_data, type_info->Size);
2271
2272 while (ImCharIsBlankA(*buf))
2273 buf++;
2274 if (!buf[0])
2275 {
2276 if (p_data_when_empty != NULL)
2277 {
2278 memcpy(p_data, p_data_when_empty, type_info->Size);
2279 return memcmp(&data_backup, p_data, type_info->Size) != 0;
2280 }
2281 return false;
2282 }
2283
2284 // Sanitize format
2285 // - 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
2286 // - In theory could treat empty format as using default, but this would only cover rare/bizarre case of using InputScalar() + integer + format string without %.
2287 char format_sanitized[32];
2288 if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double)
2289 format = type_info->ScanFmt;
2290 else
2291 format = ImParseFormatSanitizeForScanning(format, format_sanitized, IM_ARRAYSIZE(format_sanitized));
2292
2293 // Small types need a 32-bit buffer to receive the result from scanf()
2294 int v32 = 0;
2295 if (sscanf(buf, format, type_info->Size >= 4 ? p_data : &v32) < 1)
2296 return false;
2297 if (type_info->Size < 4)
2298 {
2299 if (data_type == ImGuiDataType_S8)
2300 *(ImS8*)p_data = (ImS8)ImClamp(v32, (int)IM_S8_MIN, (int)IM_S8_MAX);
2301 else if (data_type == ImGuiDataType_U8)
2302 *(ImU8*)p_data = (ImU8)ImClamp(v32, (int)IM_U8_MIN, (int)IM_U8_MAX);
2303 else if (data_type == ImGuiDataType_S16)
2304 *(ImS16*)p_data = (ImS16)ImClamp(v32, (int)IM_S16_MIN, (int)IM_S16_MAX);
2305 else if (data_type == ImGuiDataType_U16)
2306 *(ImU16*)p_data = (ImU16)ImClamp(v32, (int)IM_U16_MIN, (int)IM_U16_MAX);
2307 else
2308 IM_ASSERT(0);
2309 }
2310
2311 return memcmp(&data_backup, p_data, type_info->Size) != 0;
2312}
2313
2314template<typename T>
2315static int DataTypeCompareT(const T* lhs, const T* rhs)
2316{
2317 if (*lhs < *rhs) return -1;
2318 if (*lhs > *rhs) return +1;
2319 return 0;
2320}
2321
2322int ImGui::DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2)
2323{
2324 switch (data_type)
2325 {
2326 case ImGuiDataType_S8: return DataTypeCompareT<ImS8 >((const ImS8* )arg_1, (const ImS8* )arg_2);
2327 case ImGuiDataType_U8: return DataTypeCompareT<ImU8 >((const ImU8* )arg_1, (const ImU8* )arg_2);
2328 case ImGuiDataType_S16: return DataTypeCompareT<ImS16 >((const ImS16* )arg_1, (const ImS16* )arg_2);
2329 case ImGuiDataType_U16: return DataTypeCompareT<ImU16 >((const ImU16* )arg_1, (const ImU16* )arg_2);
2330 case ImGuiDataType_S32: return DataTypeCompareT<ImS32 >((const ImS32* )arg_1, (const ImS32* )arg_2);
2331 case ImGuiDataType_U32: return DataTypeCompareT<ImU32 >((const ImU32* )arg_1, (const ImU32* )arg_2);
2332 case ImGuiDataType_S64: return DataTypeCompareT<ImS64 >((const ImS64* )arg_1, (const ImS64* )arg_2);
2333 case ImGuiDataType_U64: return DataTypeCompareT<ImU64 >((const ImU64* )arg_1, (const ImU64* )arg_2);
2334 case ImGuiDataType_Float: return DataTypeCompareT<float >((const float* )arg_1, (const float* )arg_2);
2335 case ImGuiDataType_Double: return DataTypeCompareT<double>((const double*)arg_1, (const double*)arg_2);
2336 case ImGuiDataType_COUNT: break;
2337 }
2338 IM_ASSERT(0);
2339 return 0;
2340}
2341
2342template<typename T>
2343static bool DataTypeClampT(T* v, const T* v_min, const T* v_max)
2344{
2345 // Clamp, both sides are optional, return true if modified
2346 if (v_min && *v < *v_min) { *v = *v_min; return true; }
2347 if (v_max && *v > *v_max) { *v = *v_max; return true; }
2348 return false;
2349}
2350
2351bool ImGui::DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max)
2352{
2353 switch (data_type)
2354 {
2355 case ImGuiDataType_S8: return DataTypeClampT<ImS8 >((ImS8* )p_data, (const ImS8* )p_min, (const ImS8* )p_max);
2356 case ImGuiDataType_U8: return DataTypeClampT<ImU8 >((ImU8* )p_data, (const ImU8* )p_min, (const ImU8* )p_max);
2357 case ImGuiDataType_S16: return DataTypeClampT<ImS16 >((ImS16* )p_data, (const ImS16* )p_min, (const ImS16* )p_max);
2358 case ImGuiDataType_U16: return DataTypeClampT<ImU16 >((ImU16* )p_data, (const ImU16* )p_min, (const ImU16* )p_max);
2359 case ImGuiDataType_S32: return DataTypeClampT<ImS32 >((ImS32* )p_data, (const ImS32* )p_min, (const ImS32* )p_max);
2360 case ImGuiDataType_U32: return DataTypeClampT<ImU32 >((ImU32* )p_data, (const ImU32* )p_min, (const ImU32* )p_max);
2361 case ImGuiDataType_S64: return DataTypeClampT<ImS64 >((ImS64* )p_data, (const ImS64* )p_min, (const ImS64* )p_max);
2362 case ImGuiDataType_U64: return DataTypeClampT<ImU64 >((ImU64* )p_data, (const ImU64* )p_min, (const ImU64* )p_max);
2363 case ImGuiDataType_Float: return DataTypeClampT<float >((float* )p_data, (const float* )p_min, (const float* )p_max);
2364 case ImGuiDataType_Double: return DataTypeClampT<double>((double*)p_data, (const double*)p_min, (const double*)p_max);
2365 case ImGuiDataType_COUNT: break;
2366 }
2367 IM_ASSERT(0);
2368 return false;
2369}
2370
2371bool ImGui::DataTypeIsZero(ImGuiDataType data_type, const void* p_data)
2372{
2373 ImGuiContext& g = *GImGui;
2374 return DataTypeCompare(data_type, p_data, &g.DataTypeZeroValue) == 0;
2375}
2376
2377static float GetMinimumStepAtDecimalPrecision(int decimal_precision)
2378{
2379 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 };
2380 if (decimal_precision < 0)
2381 return FLT_MIN;
2382 return (decimal_precision < IM_ARRAYSIZE(min_steps)) ? min_steps[decimal_precision] : ImPow(10.0f, (float)-decimal_precision);
2383}
2384
2385template<typename TYPE>
2386TYPE ImGui::RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, TYPE v)
2387{
2388 IM_UNUSED(data_type);
2389 IM_ASSERT(data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double);
2390 const char* fmt_start = ImParseFormatFindStart(format);
2391 if (fmt_start[0] != '%' || fmt_start[1] == '%') // Don't apply if the value is not visible in the format string
2392 return v;
2393
2394 // Sanitize format
2395 char fmt_sanitized[32];
2396 ImParseFormatSanitizeForPrinting(fmt_start, fmt_sanitized, IM_ARRAYSIZE(fmt_sanitized));
2397 fmt_start = fmt_sanitized;
2398
2399 // Format value with our rounding, and read back
2400 char v_str[64];
2401 ImFormatString(v_str, IM_ARRAYSIZE(v_str), fmt_start, v);
2402 const char* p = v_str;
2403 while (*p == ' ')
2404 p++;
2405 v = (TYPE)ImAtof(p);
2406
2407 return v;
2408}
2409
2410//-------------------------------------------------------------------------
2411// [SECTION] Widgets: DragScalar, DragFloat, DragInt, etc.
2412//-------------------------------------------------------------------------
2413// - DragBehaviorT<>() [Internal]
2414// - DragBehavior() [Internal]
2415// - DragScalar()
2416// - DragScalarN()
2417// - DragFloat()
2418// - DragFloat2()
2419// - DragFloat3()
2420// - DragFloat4()
2421// - DragFloatRange2()
2422// - DragInt()
2423// - DragInt2()
2424// - DragInt3()
2425// - DragInt4()
2426// - DragIntRange2()
2427//-------------------------------------------------------------------------
2428
2429// This is called by DragBehavior() when the widget is active (held by mouse or being manipulated with Nav controls)
2430template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2431bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags)
2432{
2433 ImGuiContext& g = *GImGui;
2434 const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
2435 const bool is_bounded = (v_min < v_max) || ((v_min == v_max) && (v_min != 0.0f || (flags & ImGuiSliderFlags_ClampZeroRange)));
2436 const bool is_wrapped = is_bounded && (flags & ImGuiSliderFlags_WrapAround);
2437 const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
2438 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2439
2440 // Default tweak speed
2441 if (v_speed == 0.0f && is_bounded && (v_max - v_min < FLT_MAX))
2442 v_speed = (float)((v_max - v_min) * g.DragSpeedDefaultRatio);
2443
2444 // Inputs accumulates into g.DragCurrentAccum, which is flushed into the current value as soon as it makes a difference with our precision settings
2445 float adjust_delta = 0.0f;
2446 if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid() && IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2447 {
2448 adjust_delta = g.IO.MouseDelta[axis];
2449 if (g.IO.KeyAlt && !(flags & ImGuiSliderFlags_NoSpeedTweaks))
2450 adjust_delta *= 1.0f / 100.0f;
2451 if (g.IO.KeyShift && !(flags & ImGuiSliderFlags_NoSpeedTweaks))
2452 adjust_delta *= 10.0f;
2453 }
2454 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
2455 {
2456 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0;
2457 const bool tweak_slow = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
2458 const bool tweak_fast = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
2459 const float tweak_factor = (flags & ImGuiSliderFlags_NoSpeedTweaks) ? 1.0f : tweak_slow ? 1.0f / 10.0f : tweak_fast ? 10.0f : 1.0f;
2460 adjust_delta = GetNavTweakPressedAmount(axis) * tweak_factor;
2461 v_speed = ImMax(v_speed, GetMinimumStepAtDecimalPrecision(decimal_precision));
2462 }
2463 adjust_delta *= v_speed;
2464
2465 // For vertical drag we currently assume that Up=higher value (like we do with vertical sliders). This may become a parameter.
2466 if (axis == ImGuiAxis_Y)
2467 adjust_delta = -adjust_delta;
2468
2469 // For logarithmic use our range is effectively 0..1 so scale the delta into that range
2470 if (is_logarithmic && (v_max - v_min < FLT_MAX) && ((v_max - v_min) > 0.000001f)) // Epsilon to avoid /0
2471 adjust_delta /= (float)(v_max - v_min);
2472
2473 // Clear current value on activation
2474 // 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.
2475 const bool is_just_activated = g.ActiveIdIsJustActivated;
2476 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));
2477 if (is_just_activated || is_already_past_limits_and_pushing_outward)
2478 {
2479 g.DragCurrentAccum = 0.0f;
2480 g.DragCurrentAccumDirty = false;
2481 }
2482 else if (adjust_delta != 0.0f)
2483 {
2484 g.DragCurrentAccum += adjust_delta;
2485 g.DragCurrentAccumDirty = true;
2486 }
2487
2488 if (!g.DragCurrentAccumDirty)
2489 return false;
2490
2491 TYPE v_cur = *v;
2492 FLOATTYPE v_old_ref_for_accum_remainder = (FLOATTYPE)0.0f;
2493
2494 float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
2495 const float zero_deadzone_halfsize = 0.0f; // Drag widgets have no deadzone (as it doesn't make sense)
2496 if (is_logarithmic)
2497 {
2498 // 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.
2499 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1;
2500 logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision);
2501
2502 // Convert to parametric space, apply delta, convert back
2503 float v_old_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2504 float v_new_parametric = v_old_parametric + g.DragCurrentAccum;
2505 v_cur = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new_parametric, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2506 v_old_ref_for_accum_remainder = v_old_parametric;
2507 }
2508 else
2509 {
2510 v_cur += (SIGNEDTYPE)g.DragCurrentAccum;
2511 }
2512
2513 // Round to user desired precision based on format string
2514 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
2515 v_cur = RoundScalarWithFormatT<TYPE>(format, data_type, v_cur);
2516
2517 // Preserve remainder after rounding has been applied. This also allow slow tweaking of values.
2518 g.DragCurrentAccumDirty = false;
2519 if (is_logarithmic)
2520 {
2521 // Convert to parametric space, apply delta, convert back
2522 float v_new_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2523 g.DragCurrentAccum -= (float)(v_new_parametric - v_old_ref_for_accum_remainder);
2524 }
2525 else
2526 {
2527 g.DragCurrentAccum -= (float)((SIGNEDTYPE)v_cur - (SIGNEDTYPE)*v);
2528 }
2529
2530 // Lose zero sign for float/double
2531 if (v_cur == (TYPE)-0)
2532 v_cur = (TYPE)0;
2533
2534 if (*v != v_cur && is_bounded)
2535 {
2536 if (is_wrapped)
2537 {
2538 // Wrap values
2539 if (v_cur < v_min)
2540 v_cur += v_max - v_min + (is_floating_point ? 0 : 1);
2541 if (v_cur > v_max)
2542 v_cur -= v_max - v_min + (is_floating_point ? 0 : 1);
2543 }
2544 else
2545 {
2546 // Clamp values + handle overflow/wrap-around for integer types.
2547 if (v_cur < v_min || (v_cur > *v && adjust_delta < 0.0f && !is_floating_point))
2548 v_cur = v_min;
2549 if (v_cur > v_max || (v_cur < *v && adjust_delta > 0.0f && !is_floating_point))
2550 v_cur = v_max;
2551 }
2552 }
2553
2554 // Apply result
2555 if (*v == v_cur)
2556 return false;
2557 *v = v_cur;
2558 return true;
2559}
2560
2561bool 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)
2562{
2563 // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
2564 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.");
2565
2566 ImGuiContext& g = *GImGui;
2567 if (g.ActiveId == id)
2568 {
2569 // Those are the things we can do easily outside the DragBehaviorT<> template, saves code generation.
2570 if (g.ActiveIdSource == ImGuiInputSource_Mouse && !g.IO.MouseDown[0])
2571 ClearActiveID();
2572 else if ((g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) && g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
2573 ClearActiveID();
2574 }
2575 if (g.ActiveId != id)
2576 return false;
2577 if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
2578 return false;
2579
2580 switch (data_type)
2581 {
2582 case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = DragBehaviorT<ImS32, ImS32, float>(ImGuiDataType_S32, &v32, v_speed, p_min ? *(const ImS8*) p_min : IM_S8_MIN, p_max ? *(const ImS8*)p_max : IM_S8_MAX, format, flags); if (r) *(ImS8*)p_v = (ImS8)v32; return r; }
2583 case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)p_v; bool r = DragBehaviorT<ImU32, ImS32, float>(ImGuiDataType_U32, &v32, v_speed, p_min ? *(const ImU8*) p_min : IM_U8_MIN, p_max ? *(const ImU8*)p_max : IM_U8_MAX, format, flags); if (r) *(ImU8*)p_v = (ImU8)v32; return r; }
2584 case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)p_v; bool r = DragBehaviorT<ImS32, ImS32, float>(ImGuiDataType_S32, &v32, v_speed, p_min ? *(const ImS16*)p_min : IM_S16_MIN, p_max ? *(const ImS16*)p_max : IM_S16_MAX, format, flags); if (r) *(ImS16*)p_v = (ImS16)v32; return r; }
2585 case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)p_v; bool r = DragBehaviorT<ImU32, ImS32, float>(ImGuiDataType_U32, &v32, v_speed, p_min ? *(const ImU16*)p_min : IM_U16_MIN, p_max ? *(const ImU16*)p_max : IM_U16_MAX, format, flags); if (r) *(ImU16*)p_v = (ImU16)v32; return r; }
2586 case ImGuiDataType_S32: return DragBehaviorT<ImS32, ImS32, float >(data_type, (ImS32*)p_v, v_speed, p_min ? *(const ImS32* )p_min : IM_S32_MIN, p_max ? *(const ImS32* )p_max : IM_S32_MAX, format, flags);
2587 case ImGuiDataType_U32: return DragBehaviorT<ImU32, ImS32, float >(data_type, (ImU32*)p_v, v_speed, p_min ? *(const ImU32* )p_min : IM_U32_MIN, p_max ? *(const ImU32* )p_max : IM_U32_MAX, format, flags);
2588 case ImGuiDataType_S64: return DragBehaviorT<ImS64, ImS64, double>(data_type, (ImS64*)p_v, v_speed, p_min ? *(const ImS64* )p_min : IM_S64_MIN, p_max ? *(const ImS64* )p_max : IM_S64_MAX, format, flags);
2589 case ImGuiDataType_U64: return DragBehaviorT<ImU64, ImS64, double>(data_type, (ImU64*)p_v, v_speed, p_min ? *(const ImU64* )p_min : IM_U64_MIN, p_max ? *(const ImU64* )p_max : IM_U64_MAX, format, flags);
2590 case ImGuiDataType_Float: return DragBehaviorT<float, float, float >(data_type, (float*)p_v, v_speed, p_min ? *(const float* )p_min : -FLT_MAX, p_max ? *(const float* )p_max : FLT_MAX, format, flags);
2591 case ImGuiDataType_Double: return DragBehaviorT<double,double,double>(data_type, (double*)p_v, v_speed, p_min ? *(const double*)p_min : -DBL_MAX, p_max ? *(const double*)p_max : DBL_MAX, format, flags);
2592 case ImGuiDataType_COUNT: break;
2593 }
2594 IM_ASSERT(0);
2595 return false;
2596}
2597
2598// 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.
2599// Read code of e.g. DragFloat(), DragInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
2600bool 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)
2601{
2602 ImGuiWindow* window = GetCurrentWindow();
2603 if (window->SkipItems)
2604 return false;
2605
2606 ImGuiContext& g = *GImGui;
2607 const ImGuiStyle& style = g.Style;
2608 const ImGuiID id = window->GetID(label);
2609 const float w = CalcItemWidth();
2610
2611 const ImVec2 label_size = CalcTextSize(label, NULL, true);
2612 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
2613 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));
2614
2615 const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
2616 ItemSize(total_bb, style.FramePadding.y);
2617 if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
2618 return false;
2619
2620 // Default format string when passing NULL
2621 if (format == NULL)
2622 format = DataTypeGetInfo(data_type)->PrintFmt;
2623
2624 const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags);
2625 bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
2626 if (!temp_input_is_active)
2627 {
2628 // Tabbing or CTRL-clicking on Drag turns it into an InputText
2629 const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id);
2630 const bool double_clicked = (hovered && g.IO.MouseClickedCount[0] == 2 && TestKeyOwner(ImGuiKey_MouseLeft, id));
2631 const bool make_active = (clicked || double_clicked || g.NavActivateId == id);
2632 if (make_active && (clicked || double_clicked))
2633 SetKeyOwner(ImGuiKey_MouseLeft, id);
2634 if (make_active && temp_input_allowed)
2635 if ((clicked && g.IO.KeyCtrl) || double_clicked || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
2636 temp_input_is_active = true;
2637
2638 // (Optional) simple click (without moving) turns Drag into an InputText
2639 if (g.IO.ConfigDragClickToInputText && temp_input_allowed && !temp_input_is_active)
2640 if (g.ActiveId == id && hovered && g.IO.MouseReleased[0] && !IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2641 {
2642 g.NavActivateId = id;
2643 g.NavActivateFlags = ImGuiActivateFlags_PreferInput;
2644 temp_input_is_active = true;
2645 }
2646
2647 // Store initial value (not used by main lib but available as a convenience but some mods e.g. to revert)
2648 if (make_active)
2649 memcpy(&g.ActiveIdValueOnActivation, p_data, DataTypeGetInfo(data_type)->Size);
2650
2651 if (make_active && !temp_input_is_active)
2652 {
2653 SetActiveID(id, window);
2654 SetFocusID(id, window);
2655 FocusWindow(window);
2656 g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
2657 }
2658 }
2659
2660 if (temp_input_is_active)
2661 {
2662 // Only clamp CTRL+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp)
2663 bool clamp_enabled = false;
2664 if ((flags & ImGuiSliderFlags_ClampOnInput) && (p_min != NULL || p_max != NULL))
2665 {
2666 const int clamp_range_dir = (p_min != NULL && p_max != NULL) ? DataTypeCompare(data_type, p_min, p_max) : 0; // -1 when *p_min < *p_max, == 0 when *p_min == *p_max
2667 if (p_min == NULL || p_max == NULL || clamp_range_dir < 0)
2668 clamp_enabled = true;
2669 else if (clamp_range_dir == 0)
2670 clamp_enabled = DataTypeIsZero(data_type, p_min) ? ((flags & ImGuiSliderFlags_ClampZeroRange) != 0) : true;
2671 }
2672 return TempInputScalar(frame_bb, id, label, data_type, p_data, format, clamp_enabled ? p_min : NULL, clamp_enabled ? p_max : NULL);
2673 }
2674
2675 // Draw frame
2676 const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
2677 RenderNavCursor(frame_bb, id);
2678 RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, style.FrameRounding);
2679
2680 // Drag behavior
2681 const bool value_changed = DragBehavior(id, data_type, p_data, v_speed, p_min, p_max, format, flags);
2682 if (value_changed)
2683 MarkItemEdited(id);
2684
2685 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
2686 char value_buf[64];
2687 const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
2688 if (g.LogEnabled)
2689 LogSetNextTextDecoration("{", "}");
2690 RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f));
2691
2692 if (label_size.x > 0.0f)
2693 RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
2694
2695 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
2696 return value_changed;
2697}
2698
2699bool 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)
2700{
2701 ImGuiWindow* window = GetCurrentWindow();
2702 if (window->SkipItems)
2703 return false;
2704
2705 ImGuiContext& g = *GImGui;
2706 bool value_changed = false;
2707 BeginGroup();
2708 PushID(label);
2709 PushMultiItemsWidths(components, CalcItemWidth());
2710 size_t type_size = GDataTypeInfo[data_type].Size;
2711 for (int i = 0; i < components; i++)
2712 {
2713 PushID(i);
2714 if (i > 0)
2715 SameLine(0, g.Style.ItemInnerSpacing.x);
2716 value_changed |= DragScalar("", data_type, p_data, v_speed, p_min, p_max, format, flags);
2717 PopID();
2718 PopItemWidth();
2719 p_data = (void*)((char*)p_data + type_size);
2720 }
2721 PopID();
2722
2723 const char* label_end = FindRenderedTextEnd(label);
2724 if (label != label_end)
2725 {
2726 SameLine(0, g.Style.ItemInnerSpacing.x);
2727 TextEx(label, label_end);
2728 }
2729
2730 EndGroup();
2731 return value_changed;
2732}
2733
2734bool ImGui::DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2735{
2736 return DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, format, flags);
2737}
2738
2739bool ImGui::DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2740{
2741 return DragScalarN(label, ImGuiDataType_Float, v, 2, v_speed, &v_min, &v_max, format, flags);
2742}
2743
2744bool ImGui::DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2745{
2746 return DragScalarN(label, ImGuiDataType_Float, v, 3, v_speed, &v_min, &v_max, format, flags);
2747}
2748
2749bool ImGui::DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2750{
2751 return DragScalarN(label, ImGuiDataType_Float, v, 4, v_speed, &v_min, &v_max, format, flags);
2752}
2753
2754// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2755bool 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)
2756{
2757 ImGuiWindow* window = GetCurrentWindow();
2758 if (window->SkipItems)
2759 return false;
2760
2761 ImGuiContext& g = *GImGui;
2762 PushID(label);
2763 BeginGroup();
2764 PushMultiItemsWidths(2, CalcItemWidth());
2765
2766 float min_min = (v_min >= v_max) ? -FLT_MAX : v_min;
2767 float min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max);
2768 ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2769 bool value_changed = DragScalar("##min", ImGuiDataType_Float, v_current_min, v_speed, &min_min, &min_max, format, min_flags);
2770 PopItemWidth();
2771 SameLine(0, g.Style.ItemInnerSpacing.x);
2772
2773 float max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min);
2774 float max_max = (v_min >= v_max) ? FLT_MAX : v_max;
2775 ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2776 value_changed |= DragScalar("##max", ImGuiDataType_Float, v_current_max, v_speed, &max_min, &max_max, format_max ? format_max : format, max_flags);
2777 PopItemWidth();
2778 SameLine(0, g.Style.ItemInnerSpacing.x);
2779
2780 TextEx(label, FindRenderedTextEnd(label));
2781 EndGroup();
2782 PopID();
2783
2784 return value_changed;
2785}
2786
2787// NB: v_speed is float to allow adjusting the drag speed with more precision
2788bool ImGui::DragInt(const char* label, int* v, float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2789{
2790 return DragScalar(label, ImGuiDataType_S32, v, v_speed, &v_min, &v_max, format, flags);
2791}
2792
2793bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2794{
2795 return DragScalarN(label, ImGuiDataType_S32, v, 2, v_speed, &v_min, &v_max, format, flags);
2796}
2797
2798bool ImGui::DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2799{
2800 return DragScalarN(label, ImGuiDataType_S32, v, 3, v_speed, &v_min, &v_max, format, flags);
2801}
2802
2803bool ImGui::DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2804{
2805 return DragScalarN(label, ImGuiDataType_S32, v, 4, v_speed, &v_min, &v_max, format, flags);
2806}
2807
2808// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2809bool 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)
2810{
2811 ImGuiWindow* window = GetCurrentWindow();
2812 if (window->SkipItems)
2813 return false;
2814
2815 ImGuiContext& g = *GImGui;
2816 PushID(label);
2817 BeginGroup();
2818 PushMultiItemsWidths(2, CalcItemWidth());
2819
2820 int min_min = (v_min >= v_max) ? INT_MIN : v_min;
2821 int min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max);
2822 ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2823 bool value_changed = DragInt("##min", v_current_min, v_speed, min_min, min_max, format, min_flags);
2824 PopItemWidth();
2825 SameLine(0, g.Style.ItemInnerSpacing.x);
2826
2827 int max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min);
2828 int max_max = (v_min >= v_max) ? INT_MAX : v_max;
2829 ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2830 value_changed |= DragInt("##max", v_current_max, v_speed, max_min, max_max, format_max ? format_max : format, max_flags);
2831 PopItemWidth();
2832 SameLine(0, g.Style.ItemInnerSpacing.x);
2833
2834 TextEx(label, FindRenderedTextEnd(label));
2835 EndGroup();
2836 PopID();
2837
2838 return value_changed;
2839}
2840
2841//-------------------------------------------------------------------------
2842// [SECTION] Widgets: SliderScalar, SliderFloat, SliderInt, etc.
2843//-------------------------------------------------------------------------
2844// - ScaleRatioFromValueT<> [Internal]
2845// - ScaleValueFromRatioT<> [Internal]
2846// - SliderBehaviorT<>() [Internal]
2847// - SliderBehavior() [Internal]
2848// - SliderScalar()
2849// - SliderScalarN()
2850// - SliderFloat()
2851// - SliderFloat2()
2852// - SliderFloat3()
2853// - SliderFloat4()
2854// - SliderAngle()
2855// - SliderInt()
2856// - SliderInt2()
2857// - SliderInt3()
2858// - SliderInt4()
2859// - VSliderScalar()
2860// - VSliderFloat()
2861// - VSliderInt()
2862//-------------------------------------------------------------------------
2863
2864// Convert a value v in the output space of a slider into a parametric position on the slider itself (the logical opposite of ScaleValueFromRatioT)
2865template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2866float ImGui::ScaleRatioFromValueT(ImGuiDataType data_type, TYPE v, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize)
2867{
2868 if (v_min == v_max)
2869 return 0.0f;
2870 IM_UNUSED(data_type);
2871
2872 const TYPE v_clamped = (v_min < v_max) ? ImClamp(v, v_min, v_max) : ImClamp(v, v_max, v_min);
2873 if (is_logarithmic)
2874 {
2875 bool flipped = v_max < v_min;
2876
2877 if (flipped) // Handle the case where the range is backwards
2878 ImSwap(v_min, v_max);
2879
2880 // Fudge min/max to avoid getting close to log(0)
2881 FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
2882 FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
2883
2884 // Awkward special cases - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
2885 if ((v_min == 0.0f) && (v_max < 0.0f))
2886 v_min_fudged = -logarithmic_zero_epsilon;
2887 else if ((v_max == 0.0f) && (v_min < 0.0f))
2888 v_max_fudged = -logarithmic_zero_epsilon;
2889
2890 float result;
2891 if (v_clamped <= v_min_fudged)
2892 result = 0.0f; // Workaround for values that are in-range but below our fudge
2893 else if (v_clamped >= v_max_fudged)
2894 result = 1.0f; // Workaround for values that are in-range but above our fudge
2895 else if ((v_min * v_max) < 0.0f) // Range crosses zero, so split into two portions
2896 {
2897 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)
2898 float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
2899 float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
2900 if (v == 0.0f)
2901 result = zero_point_center; // Special case for exactly zero
2902 else if (v < 0.0f)
2903 result = (1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(-v_min_fudged / logarithmic_zero_epsilon))) * zero_point_snap_L;
2904 else
2905 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));
2906 }
2907 else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
2908 result = 1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / -v_max_fudged) / ImLog(-v_min_fudged / -v_max_fudged));
2909 else
2910 result = (float)(ImLog((FLOATTYPE)v_clamped / v_min_fudged) / ImLog(v_max_fudged / v_min_fudged));
2911
2912 return flipped ? (1.0f - result) : result;
2913 }
2914 else
2915 {
2916 // Linear slider
2917 return (float)((FLOATTYPE)(SIGNEDTYPE)(v_clamped - v_min) / (FLOATTYPE)(SIGNEDTYPE)(v_max - v_min));
2918 }
2919}
2920
2921// Convert a parametric position on a slider into a value v in the output space (the logical opposite of ScaleRatioFromValueT)
2922template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2923TYPE ImGui::ScaleValueFromRatioT(ImGuiDataType data_type, float t, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize)
2924{
2925 // We special-case the extents because otherwise our logarithmic fudging can lead to "mathematically correct"
2926 // but non-intuitive behaviors like a fully-left slider not actually reaching the minimum value. Also generally simpler.
2927 if (t <= 0.0f || v_min == v_max)
2928 return v_min;
2929 if (t >= 1.0f)
2930 return v_max;
2931
2932 TYPE result = (TYPE)0;
2933 if (is_logarithmic)
2934 {
2935 // Fudge min/max to avoid getting silly results close to zero
2936 FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
2937 FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
2938
2939 const bool flipped = v_max < v_min; // Check if range is "backwards"
2940 if (flipped)
2941 ImSwap(v_min_fudged, v_max_fudged);
2942
2943 // Awkward special case - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
2944 if ((v_max == 0.0f) && (v_min < 0.0f))
2945 v_max_fudged = -logarithmic_zero_epsilon;
2946
2947 float t_with_flip = flipped ? (1.0f - t) : t; // t, but flipped if necessary to account for us flipping the range
2948
2949 if ((v_min * v_max) < 0.0f) // Range crosses zero, so we have to do this in two parts
2950 {
2951 float zero_point_center = (-(float)ImMin(v_min, v_max)) / ImAbs((float)v_max - (float)v_min); // The zero point in parametric space
2952 float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
2953 float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
2954 if (t_with_flip >= zero_point_snap_L && t_with_flip <= zero_point_snap_R)
2955 result = (TYPE)0.0f; // Special case to make getting exactly zero possible (the epsilon prevents it otherwise)
2956 else if (t_with_flip < zero_point_center)
2957 result = (TYPE)-(logarithmic_zero_epsilon * ImPow(-v_min_fudged / logarithmic_zero_epsilon, (FLOATTYPE)(1.0f - (t_with_flip / zero_point_snap_L))));
2958 else
2959 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))));
2960 }
2961 else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
2962 result = (TYPE)-(-v_max_fudged * ImPow(-v_min_fudged / -v_max_fudged, (FLOATTYPE)(1.0f - t_with_flip)));
2963 else
2964 result = (TYPE)(v_min_fudged * ImPow(v_max_fudged / v_min_fudged, (FLOATTYPE)t_with_flip));
2965 }
2966 else
2967 {
2968 // Linear slider
2969 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2970 if (is_floating_point)
2971 {
2972 result = ImLerp(v_min, v_max, t);
2973 }
2974 else if (t < 1.0)
2975 {
2976 // - For integer values we want the clicking position to match the grab box so we round above
2977 // This code is carefully tuned to work with large values (e.g. high ranges of U64) while preserving this property..
2978 // - 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
2979 // range is going to be imprecise anyway, with this check we at least make the edge values matches expected limits.
2980 FLOATTYPE v_new_off_f = (SIGNEDTYPE)(v_max - v_min) * t;
2981 result = (TYPE)((SIGNEDTYPE)v_min + (SIGNEDTYPE)(v_new_off_f + (FLOATTYPE)(v_min > v_max ? -0.5 : 0.5)));
2982 }
2983 }
2984
2985 return result;
2986}
2987
2988// FIXME: Try to move more of the code into shared SliderBehavior()
2989template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2990bool 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)
2991{
2992 ImGuiContext& g = *GImGui;
2993 const ImGuiStyle& style = g.Style;
2994
2995 const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
2996 const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
2997 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2998 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.
2999
3000 // Calculate bounds
3001 const float grab_padding = 2.0f; // FIXME: Should be part of style.
3002 const float slider_sz = (bb.Max[axis] - bb.Min[axis]) - grab_padding * 2.0f;
3003 float grab_sz = style.GrabMinSize;
3004 if (!is_floating_point && v_range_f >= 0.0f) // v_range_f < 0 may happen on integer overflows
3005 grab_sz = ImMax(slider_sz / (v_range_f + 1), style.GrabMinSize); // For integer sliders: if possible have the grab size represent 1 unit
3006 grab_sz = ImMin(grab_sz, slider_sz);
3007 const float slider_usable_sz = slider_sz - grab_sz;
3008 const float slider_usable_pos_min = bb.Min[axis] + grab_padding + grab_sz * 0.5f;
3009 const float slider_usable_pos_max = bb.Max[axis] - grab_padding - grab_sz * 0.5f;
3010
3011 float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
3012 float zero_deadzone_halfsize = 0.0f; // Only valid when is_logarithmic is true
3013 if (is_logarithmic)
3014 {
3015 // 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.
3016 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1;
3017 logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision);
3018 zero_deadzone_halfsize = (style.LogSliderDeadzone * 0.5f) / ImMax(slider_usable_sz, 1.0f);
3019 }
3020
3021 // Process interacting with the slider
3022 bool value_changed = false;
3023 if (g.ActiveId == id)
3024 {
3025 bool set_new_value = false;
3026 float clicked_t = 0.0f;
3027 if (g.ActiveIdSource == ImGuiInputSource_Mouse)
3028 {
3029 if (!g.IO.MouseDown[0])
3030 {
3031 ClearActiveID();
3032 }
3033 else
3034 {
3035 const float mouse_abs_pos = g.IO.MousePos[axis];
3036 if (g.ActiveIdIsJustActivated)
3037 {
3038 float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3039 if (axis == ImGuiAxis_Y)
3040 grab_t = 1.0f - grab_t;
3041 const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t);
3042 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.
3043 g.SliderGrabClickOffset = (clicked_around_grab && is_floating_point) ? mouse_abs_pos - grab_pos : 0.0f;
3044 }
3045 if (slider_usable_sz > 0.0f)
3046 clicked_t = ImSaturate((mouse_abs_pos - g.SliderGrabClickOffset - slider_usable_pos_min) / slider_usable_sz);
3047 if (axis == ImGuiAxis_Y)
3048 clicked_t = 1.0f - clicked_t;
3049 set_new_value = true;
3050 }
3051 }
3052 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
3053 {
3054 if (g.ActiveIdIsJustActivated)
3055 {
3056 g.SliderCurrentAccum = 0.0f; // Reset any stored nav delta upon activation
3057 g.SliderCurrentAccumDirty = false;
3058 }
3059
3060 float input_delta = (axis == ImGuiAxis_X) ? GetNavTweakPressedAmount(axis) : -GetNavTweakPressedAmount(axis);
3061 if (input_delta != 0.0f)
3062 {
3063 const bool tweak_slow = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
3064 const bool tweak_fast = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
3065 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0;
3066 if (decimal_precision > 0)
3067 {
3068 input_delta /= 100.0f; // Keyboard/Gamepad tweak speeds in % of slider bounds
3069 if (tweak_slow)
3070 input_delta /= 10.0f;
3071 }
3072 else
3073 {
3074 if ((v_range_f >= -100.0f && v_range_f <= 100.0f && v_range_f != 0.0f) || tweak_slow)
3075 input_delta = ((input_delta < 0.0f) ? -1.0f : +1.0f) / v_range_f; // Keyboard/Gamepad tweak speeds in integer steps
3076 else
3077 input_delta /= 100.0f;
3078 }
3079 if (tweak_fast)
3080 input_delta *= 10.0f;
3081
3082 g.SliderCurrentAccum += input_delta;
3083 g.SliderCurrentAccumDirty = true;
3084 }
3085
3086 float delta = g.SliderCurrentAccum;
3087 if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
3088 {
3089 ClearActiveID();
3090 }
3091 else if (g.SliderCurrentAccumDirty)
3092 {
3093 clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3094
3095 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
3096 {
3097 set_new_value = false;
3098 g.SliderCurrentAccum = 0.0f; // If pushing up against the limits, don't continue to accumulate
3099 }
3100 else
3101 {
3102 set_new_value = true;
3103 float old_clicked_t = clicked_t;
3104 clicked_t = ImSaturate(clicked_t + delta);
3105
3106 // Calculate what our "new" clicked_t will be, and thus how far we actually moved the slider, and subtract this from the accumulator
3107 TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3108 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
3109 v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
3110 float new_clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3111
3112 if (delta > 0)
3113 g.SliderCurrentAccum -= ImMin(new_clicked_t - old_clicked_t, delta);
3114 else
3115 g.SliderCurrentAccum -= ImMax(new_clicked_t - old_clicked_t, delta);
3116 }
3117
3118 g.SliderCurrentAccumDirty = false;
3119 }
3120 }
3121
3122 if (set_new_value)
3123 if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
3124 set_new_value = false;
3125
3126 if (set_new_value)
3127 {
3128 TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3129
3130 // Round to user desired precision based on format string
3131 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
3132 v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
3133
3134 // Apply result
3135 if (*v != v_new)
3136 {
3137 *v = v_new;
3138 value_changed = true;
3139 }
3140 }
3141 }
3142
3143 if (slider_sz < 1.0f)
3144 {
3145 *out_grab_bb = ImRect(bb.Min, bb.Min);
3146 }
3147 else
3148 {
3149 // Output grab position so it can be displayed by the caller
3150 float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3151 if (axis == ImGuiAxis_Y)
3152 grab_t = 1.0f - grab_t;
3153 const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t);
3154 if (axis == ImGuiAxis_X)
3155 *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);
3156 else
3157 *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);
3158 }
3159
3160 return value_changed;
3161}
3162
3163// For 32-bit and larger types, slider bounds are limited to half the natural type range.
3164// 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.
3165// It would be possible to lift that limitation with some work but it doesn't seem to be worth it for sliders.
3166bool 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)
3167{
3168 // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
3169 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.");
3170 IM_ASSERT((flags & ImGuiSliderFlags_WrapAround) == 0); // Not supported by SliderXXX(), only by DragXXX()
3171
3172 switch (data_type)
3173 {
3174 case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = SliderBehaviorT<ImS32, ImS32, float>(bb, id, ImGuiDataType_S32, &v32, *(const ImS8*)p_min, *(const ImS8*)p_max, format, flags, out_grab_bb); if (r) *(ImS8*)p_v = (ImS8)v32; return r; }
3175 case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)p_v; bool r = SliderBehaviorT<ImU32, ImS32, float>(bb, id, ImGuiDataType_U32, &v32, *(const ImU8*)p_min, *(const ImU8*)p_max, format, flags, out_grab_bb); if (r) *(ImU8*)p_v = (ImU8)v32; return r; }
3176 case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)p_v; bool r = SliderBehaviorT<ImS32, ImS32, float>(bb, id, ImGuiDataType_S32, &v32, *(const ImS16*)p_min, *(const ImS16*)p_max, format, flags, out_grab_bb); if (r) *(ImS16*)p_v = (ImS16)v32; return r; }
3177 case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)p_v; bool r = SliderBehaviorT<ImU32, ImS32, float>(bb, id, ImGuiDataType_U32, &v32, *(const ImU16*)p_min, *(const ImU16*)p_max, format, flags, out_grab_bb); if (r) *(ImU16*)p_v = (ImU16)v32; return r; }
3178 case ImGuiDataType_S32:
3179 IM_ASSERT(*(const ImS32*)p_min >= IM_S32_MIN / 2 && *(const ImS32*)p_max <= IM_S32_MAX / 2);
3180 return SliderBehaviorT<ImS32, ImS32, float >(bb, id, data_type, (ImS32*)p_v, *(const ImS32*)p_min, *(const ImS32*)p_max, format, flags, out_grab_bb);
3181 case ImGuiDataType_U32:
3182 IM_ASSERT(*(const ImU32*)p_max <= IM_U32_MAX / 2);
3183 return SliderBehaviorT<ImU32, ImS32, float >(bb, id, data_type, (ImU32*)p_v, *(const ImU32*)p_min, *(const ImU32*)p_max, format, flags, out_grab_bb);
3184 case ImGuiDataType_S64:
3185 IM_ASSERT(*(const ImS64*)p_min >= IM_S64_MIN / 2 && *(const ImS64*)p_max <= IM_S64_MAX / 2);
3186 return SliderBehaviorT<ImS64, ImS64, double>(bb, id, data_type, (ImS64*)p_v, *(const ImS64*)p_min, *(const ImS64*)p_max, format, flags, out_grab_bb);
3187 case ImGuiDataType_U64:
3188 IM_ASSERT(*(const ImU64*)p_max <= IM_U64_MAX / 2);
3189 return SliderBehaviorT<ImU64, ImS64, double>(bb, id, data_type, (ImU64*)p_v, *(const ImU64*)p_min, *(const ImU64*)p_max, format, flags, out_grab_bb);
3190 case ImGuiDataType_Float:
3191 IM_ASSERT(*(const float*)p_min >= -FLT_MAX / 2.0f && *(const float*)p_max <= FLT_MAX / 2.0f);
3192 return SliderBehaviorT<float, float, float >(bb, id, data_type, (float*)p_v, *(const float*)p_min, *(const float*)p_max, format, flags, out_grab_bb);
3193 case ImGuiDataType_Double:
3194 IM_ASSERT(*(const double*)p_min >= -DBL_MAX / 2.0f && *(const double*)p_max <= DBL_MAX / 2.0f);
3195 return SliderBehaviorT<double, double, double>(bb, id, data_type, (double*)p_v, *(const double*)p_min, *(const double*)p_max, format, flags, out_grab_bb);
3196 case ImGuiDataType_COUNT: break;
3197 }
3198 IM_ASSERT(0);
3199 return false;
3200}
3201
3202// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a slider, they are all required.
3203// Read code of e.g. SliderFloat(), SliderInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3204bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags)
3205{
3206 ImGuiWindow* window = GetCurrentWindow();
3207 if (window->SkipItems)
3208 return false;
3209
3210 ImGuiContext& g = *GImGui;
3211 const ImGuiStyle& style = g.Style;
3212 const ImGuiID id = window->GetID(label);
3213 const float w = CalcItemWidth();
3214
3215 const ImVec2 label_size = CalcTextSize(label, NULL, true);
3216 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
3217 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));
3218
3219 const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
3220 ItemSize(total_bb, style.FramePadding.y);
3221 if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
3222 return false;
3223
3224 // Default format string when passing NULL
3225 if (format == NULL)
3226 format = DataTypeGetInfo(data_type)->PrintFmt;
3227
3228 const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags);
3229 bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
3230 if (!temp_input_is_active)
3231 {
3232 // Tabbing or CTRL-clicking on Slider turns it into an input box
3233 const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id);
3234 const bool make_active = (clicked || g.NavActivateId == id);
3235 if (make_active && clicked)
3236 SetKeyOwner(ImGuiKey_MouseLeft, id);
3237 if (make_active && temp_input_allowed)
3238 if ((clicked && g.IO.KeyCtrl) || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
3239 temp_input_is_active = true;
3240
3241 // Store initial value (not used by main lib but available as a convenience but some mods e.g. to revert)
3242 if (make_active)
3243 memcpy(&g.ActiveIdValueOnActivation, p_data, DataTypeGetInfo(data_type)->Size);
3244
3245 if (make_active && !temp_input_is_active)
3246 {
3247 SetActiveID(id, window);
3248 SetFocusID(id, window);
3249 FocusWindow(window);
3250 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
3251 }
3252 }
3253
3254 if (temp_input_is_active)
3255 {
3256 // Only clamp CTRL+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp)
3257 const bool clamp_enabled = (flags & ImGuiSliderFlags_ClampOnInput) != 0;
3258 return TempInputScalar(frame_bb, id, label, data_type, p_data, format, clamp_enabled ? p_min : NULL, clamp_enabled ? p_max : NULL);
3259 }
3260
3261 // Draw frame
3262 const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3263 RenderNavCursor(frame_bb, id);
3264 RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, g.Style.FrameRounding);
3265
3266 // Slider behavior
3267 ImRect grab_bb;
3268 const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags, &grab_bb);
3269 if (value_changed)
3270 MarkItemEdited(id);
3271
3272 // Render grab
3273 if (grab_bb.Max.x > grab_bb.Min.x)
3274 window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding);
3275
3276 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3277 char value_buf[64];
3278 const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
3279 if (g.LogEnabled)
3280 LogSetNextTextDecoration("{", "}");
3281 RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f));
3282
3283 if (label_size.x > 0.0f)
3284 RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
3285
3286 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
3287 return value_changed;
3288}
3289
3290// Add multiple sliders on 1 line for compact edition of multiple components
3291bool 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)
3292{
3293 ImGuiWindow* window = GetCurrentWindow();
3294 if (window->SkipItems)
3295 return false;
3296
3297 ImGuiContext& g = *GImGui;
3298 bool value_changed = false;
3299 BeginGroup();
3300 PushID(label);
3301 PushMultiItemsWidths(components, CalcItemWidth());
3302 size_t type_size = GDataTypeInfo[data_type].Size;
3303 for (int i = 0; i < components; i++)
3304 {
3305 PushID(i);
3306 if (i > 0)
3307 SameLine(0, g.Style.ItemInnerSpacing.x);
3308 value_changed |= SliderScalar("", data_type, v, v_min, v_max, format, flags);
3309 PopID();
3310 PopItemWidth();
3311 v = (void*)((char*)v + type_size);
3312 }
3313 PopID();
3314
3315 const char* label_end = FindRenderedTextEnd(label);
3316 if (label != label_end)
3317 {
3318 SameLine(0, g.Style.ItemInnerSpacing.x);
3319 TextEx(label, label_end);
3320 }
3321
3322 EndGroup();
3323 return value_changed;
3324}
3325
3326bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3327{
3328 return SliderScalar(label, ImGuiDataType_Float, v, &v_min, &v_max, format, flags);
3329}
3330
3331bool ImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3332{
3333 return SliderScalarN(label, ImGuiDataType_Float, v, 2, &v_min, &v_max, format, flags);
3334}
3335
3336bool ImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3337{
3338 return SliderScalarN(label, ImGuiDataType_Float, v, 3, &v_min, &v_max, format, flags);
3339}
3340
3341bool ImGui::SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3342{
3343 return SliderScalarN(label, ImGuiDataType_Float, v, 4, &v_min, &v_max, format, flags);
3344}
3345
3346bool ImGui::SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max, const char* format, ImGuiSliderFlags flags)
3347{
3348 if (format == NULL)
3349 format = "%.0f deg";
3350 float v_deg = (*v_rad) * 360.0f / (2 * IM_PI);
3351 bool value_changed = SliderFloat(label, &v_deg, v_degrees_min, v_degrees_max, format, flags);
3352 if (value_changed)
3353 *v_rad = v_deg * (2 * IM_PI) / 360.0f;
3354 return value_changed;
3355}
3356
3357bool ImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3358{
3359 return SliderScalar(label, ImGuiDataType_S32, v, &v_min, &v_max, format, flags);
3360}
3361
3362bool ImGui::SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3363{
3364 return SliderScalarN(label, ImGuiDataType_S32, v, 2, &v_min, &v_max, format, flags);
3365}
3366
3367bool ImGui::SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3368{
3369 return SliderScalarN(label, ImGuiDataType_S32, v, 3, &v_min, &v_max, format, flags);
3370}
3371
3372bool ImGui::SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3373{
3374 return SliderScalarN(label, ImGuiDataType_S32, v, 4, &v_min, &v_max, format, flags);
3375}
3376
3377bool 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)
3378{
3379 ImGuiWindow* window = GetCurrentWindow();
3380 if (window->SkipItems)
3381 return false;
3382
3383 ImGuiContext& g = *GImGui;
3384 const ImGuiStyle& style = g.Style;
3385 const ImGuiID id = window->GetID(label);
3386
3387 const ImVec2 label_size = CalcTextSize(label, NULL, true);
3388 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size);
3389 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));
3390
3391 ItemSize(bb, style.FramePadding.y);
3392 if (!ItemAdd(frame_bb, id))
3393 return false;
3394
3395 // Default format string when passing NULL
3396 if (format == NULL)
3397 format = DataTypeGetInfo(data_type)->PrintFmt;
3398
3399 const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags);
3400 const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id);
3401 if (clicked || g.NavActivateId == id)
3402 {
3403 if (clicked)
3404 SetKeyOwner(ImGuiKey_MouseLeft, id);
3405 SetActiveID(id, window);
3406 SetFocusID(id, window);
3407 FocusWindow(window);
3408 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
3409 }
3410
3411 // Draw frame
3412 const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3413 RenderNavCursor(frame_bb, id);
3414 RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, g.Style.FrameRounding);
3415
3416 // Slider behavior
3417 ImRect grab_bb;
3418 const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags | ImGuiSliderFlags_Vertical, &grab_bb);
3419 if (value_changed)
3420 MarkItemEdited(id);
3421
3422 // Render grab
3423 if (grab_bb.Max.y > grab_bb.Min.y)
3424 window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding);
3425
3426 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3427 // For the vertical slider we allow centered text to overlap the frame padding
3428 char value_buf[64];
3429 const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
3430 RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.0f));
3431 if (label_size.x > 0.0f)
3432 RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
3433
3434 return value_changed;
3435}
3436
3437bool ImGui::VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3438{
3439 return VSliderScalar(label, size, ImGuiDataType_Float, v, &v_min, &v_max, format, flags);
3440}
3441
3442bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3443{
3444 return VSliderScalar(label, size, ImGuiDataType_S32, v, &v_min, &v_max, format, flags);
3445}
3446
3447//-------------------------------------------------------------------------
3448// [SECTION] Widgets: InputScalar, InputFloat, InputInt, etc.
3449//-------------------------------------------------------------------------
3450// - ImParseFormatFindStart() [Internal]
3451// - ImParseFormatFindEnd() [Internal]
3452// - ImParseFormatTrimDecorations() [Internal]
3453// - ImParseFormatSanitizeForPrinting() [Internal]
3454// - ImParseFormatSanitizeForScanning() [Internal]
3455// - ImParseFormatPrecision() [Internal]
3456// - TempInputTextScalar() [Internal]
3457// - InputScalar()
3458// - InputScalarN()
3459// - InputFloat()
3460// - InputFloat2()
3461// - InputFloat3()
3462// - InputFloat4()
3463// - InputInt()
3464// - InputInt2()
3465// - InputInt3()
3466// - InputInt4()
3467// - InputDouble()
3468//-------------------------------------------------------------------------
3469
3470// We don't use strchr() because our strings are usually very short and often start with '%'
3471const char* ImParseFormatFindStart(const char* fmt)
3472{
3473 while (char c = fmt[0])
3474 {
3475 if (c == '%' && fmt[1] != '%')
3476 return fmt;
3477 else if (c == '%')
3478 fmt++;
3479 fmt++;
3480 }
3481 return fmt;
3482}
3483
3484const char* ImParseFormatFindEnd(const char* fmt)
3485{
3486 // Printf/scanf types modifiers: I/L/h/j/l/t/w/z. Other uppercase letters qualify as types aka end of the format.
3487 if (fmt[0] != '%')
3488 return fmt;
3489 const unsigned int ignored_uppercase_mask = (1 << ('I'-'A')) | (1 << ('L'-'A'));
3490 const unsigned int ignored_lowercase_mask = (1 << ('h'-'a')) | (1 << ('j'-'a')) | (1 << ('l'-'a')) | (1 << ('t'-'a')) | (1 << ('w'-'a')) | (1 << ('z'-'a'));
3491 for (char c; (c = *fmt) != 0; fmt++)
3492 {
3493 if (c >= 'A' && c <= 'Z' && ((1 << (c - 'A')) & ignored_uppercase_mask) == 0)
3494 return fmt + 1;
3495 if (c >= 'a' && c <= 'z' && ((1 << (c - 'a')) & ignored_lowercase_mask) == 0)
3496 return fmt + 1;
3497 }
3498 return fmt;
3499}
3500
3501// Extract the format out of a format string with leading or trailing decorations
3502// fmt = "blah blah" -> return ""
3503// fmt = "%.3f" -> return fmt
3504// fmt = "hello %.3f" -> return fmt + 6
3505// fmt = "%.3f hello" -> return buf written with "%.3f"
3506const char* ImParseFormatTrimDecorations(const char* fmt, char* buf, size_t buf_size)
3507{
3508 const char* fmt_start = ImParseFormatFindStart(fmt);
3509 if (fmt_start[0] != '%')
3510 return "";
3511 const char* fmt_end = ImParseFormatFindEnd(fmt_start);
3512 if (fmt_end[0] == 0) // If we only have leading decoration, we don't need to copy the data.
3513 return fmt_start;
3514 ImStrncpy(buf, fmt_start, ImMin((size_t)(fmt_end - fmt_start) + 1, buf_size));
3515 return buf;
3516}
3517
3518// Sanitize format
3519// - Zero terminate so extra characters after format (e.g. "%f123") don't confuse atof/atoi
3520// - stb_sprintf.h supports several new modifiers which format numbers in a way that also makes them incompatible atof/atoi.
3521void ImParseFormatSanitizeForPrinting(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3522{
3523 const char* fmt_end = ImParseFormatFindEnd(fmt_in);
3524 IM_UNUSED(fmt_out_size);
3525 IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3526 while (fmt_in < fmt_end)
3527 {
3528 char c = *fmt_in++;
3529 if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3530 *(fmt_out++) = c;
3531 }
3532 *fmt_out = 0; // Zero-terminate
3533}
3534
3535// - 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"
3536const char* ImParseFormatSanitizeForScanning(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3537{
3538 const char* fmt_end = ImParseFormatFindEnd(fmt_in);
3539 const char* fmt_out_begin = fmt_out;
3540 IM_UNUSED(fmt_out_size);
3541 IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3542 bool has_type = false;
3543 while (fmt_in < fmt_end)
3544 {
3545 char c = *fmt_in++;
3546 if (!has_type && ((c >= '0' && c <= '9') || c == '.' || c == '+' || c == '#'))
3547 continue;
3548 has_type |= ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')); // Stop skipping digits
3549 if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3550 *(fmt_out++) = c;
3551 }
3552 *fmt_out = 0; // Zero-terminate
3553 return fmt_out_begin;
3554}
3555
3556template<typename TYPE>
3557static const char* ImAtoi(const char* src, TYPE* output)
3558{
3559 int negative = 0;
3560 if (*src == '-') { negative = 1; src++; }
3561 if (*src == '+') { src++; }
3562 TYPE v = 0;
3563 while (*src >= '0' && *src <= '9')
3564 v = (v * 10) + (*src++ - '0');
3565 *output = negative ? -v : v;
3566 return src;
3567}
3568
3569// Parse display precision back from the display format string
3570// 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.
3571int ImParseFormatPrecision(const char* fmt, int default_precision)
3572{
3573 fmt = ImParseFormatFindStart(fmt);
3574 if (fmt[0] != '%')
3575 return default_precision;
3576 fmt++;
3577 while (*fmt >= '0' && *fmt <= '9')
3578 fmt++;
3579 int precision = INT_MAX;
3580 if (*fmt == '.')
3581 {
3582 fmt = ImAtoi<int>(fmt + 1, &precision);
3583 if (precision < 0 || precision > 99)
3584 precision = default_precision;
3585 }
3586 if (*fmt == 'e' || *fmt == 'E') // Maximum precision with scientific notation
3587 precision = -1;
3588 if ((*fmt == 'g' || *fmt == 'G') && precision == INT_MAX)
3589 precision = -1;
3590 return (precision == INT_MAX) ? default_precision : precision;
3591}
3592
3593// Create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets)
3594// FIXME: Facilitate using this in variety of other situations.
3595// FIXME: Among other things, setting ImGuiItemFlags_AllowDuplicateId in LastItemData is currently correct but
3596// the expected relationship between TempInputXXX functions and LastItemData is a little fishy.
3597bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags)
3598{
3599 // On the first frame, g.TempInputTextId == 0, then on subsequent frames it becomes == id.
3600 // We clear ActiveID on the first frame to allow the InputText() taking it back.
3601 ImGuiContext& g = *GImGui;
3602 const bool init = (g.TempInputId != id);
3603 if (init)
3604 ClearActiveID();
3605
3606 g.CurrentWindow->DC.CursorPos = bb.Min;
3607 g.LastItemData.ItemFlags |= ImGuiItemFlags_AllowDuplicateId;
3608 bool value_changed = InputTextEx(label, NULL, buf, buf_size, bb.GetSize(), flags | ImGuiInputTextFlags_MergedItem);
3609 if (init)
3610 {
3611 // First frame we started displaying the InputText widget, we expect it to take the active id.
3612 IM_ASSERT(g.ActiveId == id);
3613 g.TempInputId = g.ActiveId;
3614 }
3615 return value_changed;
3616}
3617
3618// Note that Drag/Slider functions are only forwarding the min/max values clamping values if the ImGuiSliderFlags_AlwaysClamp flag is set!
3619// This is intended: this way we allow CTRL+Click manual input to set a value out of bounds, for maximum flexibility.
3620// However this may not be ideal for all uses, as some user code may break on out of bound values.
3621bool 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)
3622{
3623 // FIXME: May need to clarify display behavior if format doesn't contain %.
3624 // "%d" -> "%d" / "There are %d items" -> "%d" / "items" -> "%d" (fallback). Also see #6405
3625 ImGuiContext& g = *GImGui;
3626 const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
3627 char fmt_buf[32];
3628 char data_buf[32];
3629 format = ImParseFormatTrimDecorations(format, fmt_buf, IM_ARRAYSIZE(fmt_buf));
3630 if (format[0] == 0)
3631 format = type_info->PrintFmt;
3632 DataTypeFormatString(data_buf, IM_ARRAYSIZE(data_buf), data_type, p_data, format);
3633 ImStrTrimBlanks(data_buf);
3634
3635 ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
3636 g.LastItemData.ItemFlags |= ImGuiItemFlags_NoMarkEdited; // Because TempInputText() uses ImGuiInputTextFlags_MergedItem it doesn't submit a new item, so we poke LastItemData.
3637 bool value_changed = false;
3638 if (TempInputText(bb, id, label, data_buf, IM_ARRAYSIZE(data_buf), flags))
3639 {
3640 // Backup old value
3641 size_t data_type_size = type_info->Size;
3642 ImGuiDataTypeStorage data_backup;
3643 memcpy(&data_backup, p_data, data_type_size);
3644
3645 // Apply new value (or operations) then clamp
3646 DataTypeApplyFromText(data_buf, data_type, p_data, format, NULL);
3647 if (p_clamp_min || p_clamp_max)
3648 {
3649 if (p_clamp_min && p_clamp_max && DataTypeCompare(data_type, p_clamp_min, p_clamp_max) > 0)
3650 ImSwap(p_clamp_min, p_clamp_max);
3651 DataTypeClamp(data_type, p_data, p_clamp_min, p_clamp_max);
3652 }
3653
3654 // Only mark as edited if new value is different
3655 g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited;
3656 value_changed = memcmp(&data_backup, p_data, data_type_size) != 0;
3657 if (value_changed)
3658 MarkItemEdited(id);
3659 }
3660 return value_changed;
3661}
3662
3663void ImGui::SetNextItemRefVal(ImGuiDataType data_type, void* p_data)
3664{
3665 ImGuiContext& g = *GImGui;
3666 g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasRefVal;
3667 memcpy(&g.NextItemData.RefVal, p_data, DataTypeGetInfo(data_type)->Size);
3668}
3669
3670// 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.
3671// Read code of e.g. InputFloat(), InputInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3672bool 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)
3673{
3674 ImGuiWindow* window = GetCurrentWindow();
3675 if (window->SkipItems)
3676 return false;
3677
3678 ImGuiContext& g = *GImGui;
3679 ImGuiStyle& style = g.Style;
3680 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()!
3681
3682 if (format == NULL)
3683 format = DataTypeGetInfo(data_type)->PrintFmt;
3684
3685 void* p_data_default = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasRefVal) ? &g.NextItemData.RefVal : &g.DataTypeZeroValue;
3686
3687 char buf[64];
3688 if ((flags & ImGuiInputTextFlags_DisplayEmptyRefVal) && DataTypeCompare(data_type, p_data, p_data_default) == 0)
3689 buf[0] = 0;
3690 else
3691 DataTypeFormatString(buf, IM_ARRAYSIZE(buf), data_type, p_data, format);
3692
3693 // Disable the MarkItemEdited() call in InputText but keep ImGuiItemStatusFlags_Edited.
3694 // We call MarkItemEdited() ourselves by comparing the actual data rather than the string.
3695 g.NextItemData.ItemFlags |= ImGuiItemFlags_NoMarkEdited;
3696 flags |= ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
3697
3698 bool value_changed = false;
3699 if (p_step == NULL)
3700 {
3701 if (InputText(label, buf, IM_ARRAYSIZE(buf), flags))
3702 value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL);
3703 }
3704 else
3705 {
3706 const float button_size = GetFrameHeight();
3707
3708 BeginGroup(); // The only purpose of the group here is to allow the caller to query item data e.g. IsItemActive()
3709 PushID(label);
3710 SetNextItemWidth(ImMax(1.0f, CalcItemWidth() - (button_size + style.ItemInnerSpacing.x) * 2));
3711 if (InputText("", buf, IM_ARRAYSIZE(buf), flags)) // PushId(label) + "" gives us the expected ID from outside point of view
3712 value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL);
3713 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
3714
3715 // Step buttons
3716 const ImVec2 backup_frame_padding = style.FramePadding;
3717 style.FramePadding.x = style.FramePadding.y;
3718 if (flags & ImGuiInputTextFlags_ReadOnly)
3719 BeginDisabled();
3720 PushItemFlag(ImGuiItemFlags_ButtonRepeat, true);
3721 SameLine(0, style.ItemInnerSpacing.x);
3722 if (ButtonEx("-", ImVec2(button_size, button_size)))
3723 {
3724 DataTypeApplyOp(data_type, '-', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3725 value_changed = true;
3726 }
3727 SameLine(0, style.ItemInnerSpacing.x);
3728 if (ButtonEx("+", ImVec2(button_size, button_size)))
3729 {
3730 DataTypeApplyOp(data_type, '+', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3731 value_changed = true;
3732 }
3733 PopItemFlag();
3734 if (flags & ImGuiInputTextFlags_ReadOnly)
3735 EndDisabled();
3736
3737 const char* label_end = FindRenderedTextEnd(label);
3738 if (label != label_end)
3739 {
3740 SameLine(0, style.ItemInnerSpacing.x);
3741 TextEx(label, label_end);
3742 }
3743 style.FramePadding = backup_frame_padding;
3744
3745 PopID();
3746 EndGroup();
3747 }
3748
3749 g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited;
3750 if (value_changed)
3751 MarkItemEdited(g.LastItemData.ID);
3752
3753 return value_changed;
3754}
3755
3756bool 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)
3757{
3758 ImGuiWindow* window = GetCurrentWindow();
3759 if (window->SkipItems)
3760 return false;
3761
3762 ImGuiContext& g = *GImGui;
3763 bool value_changed = false;
3764 BeginGroup();
3765 PushID(label);
3766 PushMultiItemsWidths(components, CalcItemWidth());
3767 size_t type_size = GDataTypeInfo[data_type].Size;
3768 for (int i = 0; i < components; i++)
3769 {
3770 PushID(i);
3771 if (i > 0)
3772 SameLine(0, g.Style.ItemInnerSpacing.x);
3773 value_changed |= InputScalar("", data_type, p_data, p_step, p_step_fast, format, flags);
3774 PopID();
3775 PopItemWidth();
3776 p_data = (void*)((char*)p_data + type_size);
3777 }
3778 PopID();
3779
3780 const char* label_end = FindRenderedTextEnd(label);
3781 if (label != label_end)
3782 {
3783 SameLine(0.0f, g.Style.ItemInnerSpacing.x);
3784 TextEx(label, label_end);
3785 }
3786
3787 EndGroup();
3788 return value_changed;
3789}
3790
3791bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, const char* format, ImGuiInputTextFlags flags)
3792{
3793 return InputScalar(label, ImGuiDataType_Float, (void*)v, (void*)(step > 0.0f ? &step : NULL), (void*)(step_fast > 0.0f ? &step_fast : NULL), format, flags);
3794}
3795
3796bool ImGui::InputFloat2(const char* label, float v[2], const char* format, ImGuiInputTextFlags flags)
3797{
3798 return InputScalarN(label, ImGuiDataType_Float, v, 2, NULL, NULL, format, flags);
3799}
3800
3801bool ImGui::InputFloat3(const char* label, float v[3], const char* format, ImGuiInputTextFlags flags)
3802{
3803 return InputScalarN(label, ImGuiDataType_Float, v, 3, NULL, NULL, format, flags);
3804}
3805
3806bool ImGui::InputFloat4(const char* label, float v[4], const char* format, ImGuiInputTextFlags flags)
3807{
3808 return InputScalarN(label, ImGuiDataType_Float, v, 4, NULL, NULL, format, flags);
3809}
3810
3811bool ImGui::InputInt(const char* label, int* v, int step, int step_fast, ImGuiInputTextFlags flags)
3812{
3813 // 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.
3814 const char* format = (flags & ImGuiInputTextFlags_CharsHexadecimal) ? "%08X" : "%d";
3815 return InputScalar(label, ImGuiDataType_S32, (void*)v, (void*)(step > 0 ? &step : NULL), (void*)(step_fast > 0 ? &step_fast : NULL), format, flags);
3816}
3817
3818bool ImGui::InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags)
3819{
3820 return InputScalarN(label, ImGuiDataType_S32, v, 2, NULL, NULL, "%d", flags);
3821}
3822
3823bool ImGui::InputInt3(const char* label, int v[3], ImGuiInputTextFlags flags)
3824{
3825 return InputScalarN(label, ImGuiDataType_S32, v, 3, NULL, NULL, "%d", flags);
3826}
3827
3828bool ImGui::InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags)
3829{
3830 return InputScalarN(label, ImGuiDataType_S32, v, 4, NULL, NULL, "%d", flags);
3831}
3832
3833bool ImGui::InputDouble(const char* label, double* v, double step, double step_fast, const char* format, ImGuiInputTextFlags flags)
3834{
3835 return InputScalar(label, ImGuiDataType_Double, (void*)v, (void*)(step > 0.0 ? &step : NULL), (void*)(step_fast > 0.0 ? &step_fast : NULL), format, flags);
3836}
3837
3838//-------------------------------------------------------------------------
3839// [SECTION] Widgets: InputText, InputTextMultiline, InputTextWithHint
3840//-------------------------------------------------------------------------
3841// - imstb_textedit.h include
3842// - InputText()
3843// - InputTextWithHint()
3844// - InputTextMultiline()
3845// - InputTextGetCharInfo() [Internal]
3846// - InputTextReindexLines() [Internal]
3847// - InputTextReindexLinesRange() [Internal]
3848// - InputTextEx() [Internal]
3849// - DebugNodeInputTextState() [Internal]
3850//-------------------------------------------------------------------------
3851
3852namespace ImStb
3853{
3854#include "imstb_textedit.h"
3855}
3856
3857bool ImGui::InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3858{
3859 IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline()
3860 return InputTextEx(label, NULL, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data);
3861}
3862
3863bool ImGui::InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3864{
3865 return InputTextEx(label, NULL, buf, (int)buf_size, size, flags | ImGuiInputTextFlags_Multiline, callback, user_data);
3866}
3867
3868bool ImGui::InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3869{
3870 IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() or InputTextEx() manually if you need multi-line + hint.
3871 return InputTextEx(label, hint, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data);
3872}
3873
3874// This is only used in the path where the multiline widget is inactivate.
3875static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end)
3876{
3877 int line_count = 0;
3878 const char* s = text_begin;
3879 while (true)
3880 {
3881 const char* s_eol = strchr(s, '\n');
3882 line_count++;
3883 if (s_eol == NULL)
3884 {
3885 s = s + strlen(s);
3886 break;
3887 }
3888 s = s_eol + 1;
3889 }
3890 *out_text_end = s;
3891 return line_count;
3892}
3893
3894// FIXME: Ideally we'd share code with ImFont::CalcTextSizeA()
3895static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end, const char** remaining, ImVec2* out_offset, bool stop_on_new_line)
3896{
3897 ImGuiContext& g = *ctx;
3898 ImFont* font = g.Font;
3899 const float line_height = g.FontSize;
3900 const float scale = line_height / font->FontSize;
3901
3902 ImVec2 text_size = ImVec2(0, 0);
3903 float line_width = 0.0f;
3904
3905 const char* s = text_begin;
3906 while (s < text_end)
3907 {
3908 unsigned int c = (unsigned int)*s;
3909 if (c < 0x80)
3910 s += 1;
3911 else
3912 s += ImTextCharFromUtf8(&c, s, text_end);
3913
3914 if (c == '\n')
3915 {
3916 text_size.x = ImMax(text_size.x, line_width);
3917 text_size.y += line_height;
3918 line_width = 0.0f;
3919 if (stop_on_new_line)
3920 break;
3921 continue;
3922 }
3923 if (c == '\r')
3924 continue;
3925
3926 const float char_width = ((int)c < font->IndexAdvanceX.Size ? font->IndexAdvanceX.Data[c] : font->FallbackAdvanceX) * scale;
3927 line_width += char_width;
3928 }
3929
3930 if (text_size.x < line_width)
3931 text_size.x = line_width;
3932
3933 if (out_offset)
3934 *out_offset = ImVec2(line_width, text_size.y + line_height); // offset allow for the possibility of sitting after a trailing \n
3935
3936 if (line_width > 0 || text_size.y == 0.0f) // whereas size.y will ignore the trailing \n
3937 text_size.y += line_height;
3938
3939 if (remaining)
3940 *remaining = s;
3941
3942 return text_size;
3943}
3944
3945// 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)
3946// With our UTF-8 use of stb_textedit:
3947// - 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.
3948// - 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).
3949// - ...but we don't use that feature.
3950namespace ImStb
3951{
3952static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->TextLen; }
3953static char STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { IM_ASSERT(idx <= obj->TextLen); return obj->TextSrc[idx]; }
3954static float STB_TEXTEDIT_GETWIDTH(ImGuiInputTextState* obj, int line_start_idx, int char_idx) { unsigned int c; ImTextCharFromUtf8(&c, obj->TextSrc + line_start_idx + char_idx, obj->TextSrc + obj->TextLen); if ((ImWchar)c == '\n') return IMSTB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *obj->Ctx; return g.Font->GetCharAdvance((ImWchar)c) * g.FontScale; }
3955static char STB_TEXTEDIT_NEWLINE = '\n';
3956static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx)
3957{
3958 const char* text = obj->TextSrc;
3959 const char* text_remaining = NULL;
3960 const ImVec2 size = InputTextCalcTextSize(obj->Ctx, text + line_start_idx, text + obj->TextLen, &text_remaining, NULL, true);
3961 r->x0 = 0.0f;
3962 r->x1 = size.x;
3963 r->baseline_y_delta = size.y;
3964 r->ymin = 0.0f;
3965 r->ymax = size.y;
3966 r->num_chars = (int)(text_remaining - (text + line_start_idx));
3967}
3968
3969#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL
3970#define IMSTB_TEXTEDIT_GETPREVCHARINDEX IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL
3971
3972static int IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx)
3973{
3974 if (idx >= obj->TextLen)
3975 return obj->TextLen + 1;
3976 unsigned int c;
3977 return idx + ImTextCharFromUtf8(&c, obj->TextSrc + idx, obj->TextSrc + obj->TextLen);
3978}
3979
3980static int IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx)
3981{
3982 if (idx <= 0)
3983 return -1;
3984 const char* p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, obj->TextSrc + idx);
3985 return (int)(p - obj->TextSrc);
3986}
3987
3988static bool ImCharIsSeparatorW(unsigned int c)
3989{
3990 static const unsigned int separator_list[] =
3991 {
3992 ',', 0x3001, '.', 0x3002, ';', 0xFF1B, '(', 0xFF08, ')', 0xFF09, '{', 0xFF5B, '}', 0xFF5D,
3993 '[', 0x300C, ']', 0x300D, '|', 0xFF5C, '!', 0xFF01, '\\', 0xFFE5, '/', 0x30FB, 0xFF0F,
3994 '\n', '\r',
3995 };
3996 for (unsigned int separator : separator_list)
3997 if (c == separator)
3998 return true;
3999 return false;
4000}
4001
4002static int is_word_boundary_from_right(ImGuiInputTextState* obj, int idx)
4003{
4004 // 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.
4005 if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
4006 return 0;
4007
4008 const char* curr_p = obj->TextSrc + idx;
4009 const char* prev_p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, curr_p);
4010 unsigned int curr_c; ImTextCharFromUtf8(&curr_c, curr_p, obj->TextSrc + obj->TextLen);
4011 unsigned int prev_c; ImTextCharFromUtf8(&prev_c, prev_p, obj->TextSrc + obj->TextLen);
4012
4013 bool prev_white = ImCharIsBlankW(prev_c);
4014 bool prev_separ = ImCharIsSeparatorW(prev_c);
4015 bool curr_white = ImCharIsBlankW(curr_c);
4016 bool curr_separ = ImCharIsSeparatorW(curr_c);
4017 return ((prev_white || prev_separ) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ);
4018}
4019static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx)
4020{
4021 if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
4022 return 0;
4023
4024 const char* curr_p = obj->TextSrc + idx;
4025 const char* prev_p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, curr_p);
4026 unsigned int prev_c; ImTextCharFromUtf8(&prev_c, curr_p, obj->TextSrc + obj->TextLen);
4027 unsigned int curr_c; ImTextCharFromUtf8(&curr_c, prev_p, obj->TextSrc + obj->TextLen);
4028
4029 bool prev_white = ImCharIsBlankW(prev_c);
4030 bool prev_separ = ImCharIsSeparatorW(prev_c);
4031 bool curr_white = ImCharIsBlankW(curr_c);
4032 bool curr_separ = ImCharIsSeparatorW(curr_c);
4033 return ((prev_white) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ);
4034}
4035static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(ImGuiInputTextState* obj, int idx)
4036{
4037 idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx);
4038 while (idx >= 0 && !is_word_boundary_from_right(obj, idx))
4039 idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx);
4040 return idx < 0 ? 0 : idx;
4041}
4042static int STB_TEXTEDIT_MOVEWORDRIGHT_MAC(ImGuiInputTextState* obj, int idx)
4043{
4044 int len = obj->TextLen;
4045 idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4046 while (idx < len && !is_word_boundary_from_left(obj, idx))
4047 idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4048 return idx > len ? len : idx;
4049}
4050static int STB_TEXTEDIT_MOVEWORDRIGHT_WIN(ImGuiInputTextState* obj, int idx)
4051{
4052 idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4053 int len = obj->TextLen;
4054 while (idx < len && !is_word_boundary_from_right(obj, idx))
4055 idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4056 return idx > len ? len : idx;
4057}
4058static 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); }
4059#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h
4060#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL
4061
4062static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n)
4063{
4064 // Offset remaining text (+ copy zero terminator)
4065 IM_ASSERT(obj->TextSrc == obj->TextA.Data);
4066 char* dst = obj->TextA.Data + pos;
4067 char* src = obj->TextA.Data + pos + n;
4068 memmove(dst, src, obj->TextLen - n - pos + 1);
4069 obj->Edited = true;
4070 obj->TextLen -= n;
4071}
4072
4073static bool STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const char* new_text, int new_text_len)
4074{
4075 const bool is_resizable = (obj->Flags & ImGuiInputTextFlags_CallbackResize) != 0;
4076 const int text_len = obj->TextLen;
4077 IM_ASSERT(pos <= text_len);
4078
4079 if (!is_resizable && (new_text_len + obj->TextLen + 1 > obj->BufCapacity))
4080 return false;
4081
4082 // Grow internal buffer if needed
4083 IM_ASSERT(obj->TextSrc == obj->TextA.Data);
4084 if (new_text_len + text_len + 1 > obj->TextA.Size)
4085 {
4086 if (!is_resizable)
4087 return false;
4088 obj->TextA.resize(text_len + ImClamp(new_text_len, 32, ImMax(256, new_text_len)) + 1);
4089 obj->TextSrc = obj->TextA.Data;
4090 }
4091
4092 char* text = obj->TextA.Data;
4093 if (pos != text_len)
4094 memmove(text + pos + new_text_len, text + pos, (size_t)(text_len - pos));
4095 memcpy(text + pos, new_text, (size_t)new_text_len);
4096
4097 obj->Edited = true;
4098 obj->TextLen += new_text_len;
4099 obj->TextA[obj->TextLen] = '\0';
4100
4101 return true;
4102}
4103
4104// 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)
4105#define STB_TEXTEDIT_K_LEFT 0x200000 // keyboard input to move cursor left
4106#define STB_TEXTEDIT_K_RIGHT 0x200001 // keyboard input to move cursor right
4107#define STB_TEXTEDIT_K_UP 0x200002 // keyboard input to move cursor up
4108#define STB_TEXTEDIT_K_DOWN 0x200003 // keyboard input to move cursor down
4109#define STB_TEXTEDIT_K_LINESTART 0x200004 // keyboard input to move cursor to start of line
4110#define STB_TEXTEDIT_K_LINEEND 0x200005 // keyboard input to move cursor to end of line
4111#define STB_TEXTEDIT_K_TEXTSTART 0x200006 // keyboard input to move cursor to start of text
4112#define STB_TEXTEDIT_K_TEXTEND 0x200007 // keyboard input to move cursor to end of text
4113#define STB_TEXTEDIT_K_DELETE 0x200008 // keyboard input to delete selection or character under cursor
4114#define STB_TEXTEDIT_K_BACKSPACE 0x200009 // keyboard input to delete selection or character left of cursor
4115#define STB_TEXTEDIT_K_UNDO 0x20000A // keyboard input to perform undo
4116#define STB_TEXTEDIT_K_REDO 0x20000B // keyboard input to perform redo
4117#define STB_TEXTEDIT_K_WORDLEFT 0x20000C // keyboard input to move cursor left one word
4118#define STB_TEXTEDIT_K_WORDRIGHT 0x20000D // keyboard input to move cursor right one word
4119#define STB_TEXTEDIT_K_PGUP 0x20000E // keyboard input to move cursor up a page
4120#define STB_TEXTEDIT_K_PGDOWN 0x20000F // keyboard input to move cursor down a page
4121#define STB_TEXTEDIT_K_SHIFT 0x400000
4122
4123#define IMSTB_TEXTEDIT_IMPLEMENTATION
4124#define IMSTB_TEXTEDIT_memmove memmove
4125#include "imstb_textedit.h"
4126
4127// stb_textedit internally allows for a single undo record to do addition and deletion, but somehow, calling
4128// the stb_textedit_paste() function creates two separate records, so we perform it manually. (FIXME: Report to nothings/stb?)
4129static void stb_textedit_replace(ImGuiInputTextState* str, STB_TexteditState* state, const IMSTB_TEXTEDIT_CHARTYPE* text, int text_len)
4130{
4131 stb_text_makeundo_replace(str, state, 0, str->TextLen, text_len);
4132 ImStb::STB_TEXTEDIT_DELETECHARS(str, 0, str->TextLen);
4133 state->cursor = state->select_start = state->select_end = 0;
4134 if (text_len <= 0)
4135 return;
4136 if (ImStb::STB_TEXTEDIT_INSERTCHARS(str, 0, text, text_len))
4137 {
4138 state->cursor = state->select_start = state->select_end = text_len;
4139 state->has_preferred_x = 0;
4140 return;
4141 }
4142 IM_ASSERT(0); // Failed to insert character, normally shouldn't happen because of how we currently use stb_textedit_replace()
4143}
4144
4145} // namespace ImStb
4146
4147// We added an extra indirection where 'Stb' is heap-allocated, in order facilitate the work of bindings generators.
4148ImGuiInputTextState::ImGuiInputTextState()
4149{
4150 memset(this, 0, sizeof(*this));
4151 Stb = IM_NEW(ImStbTexteditState);
4152 memset(Stb, 0, sizeof(*Stb));
4153}
4154
4155ImGuiInputTextState::~ImGuiInputTextState()
4156{
4157 IM_DELETE(Stb);
4158}
4159
4160void ImGuiInputTextState::OnKeyPressed(int key)
4161{
4162 stb_textedit_key(this, Stb, key);
4163 CursorFollow = true;
4164 CursorAnimReset();
4165}
4166
4167void ImGuiInputTextState::OnCharPressed(unsigned int c)
4168{
4169 // Convert the key to a UTF8 byte sequence.
4170 // The changes we had to make to stb_textedit_key made it very much UTF-8 specific which is not too great.
4171 char utf8[5];
4173 stb_textedit_text(this, Stb, utf8, (int)strlen(utf8));
4174 CursorFollow = true;
4175 CursorAnimReset();
4176}
4177
4178// Those functions are not inlined in imgui_internal.h, allowing us to hide ImStbTexteditState from that header.
4179void ImGuiInputTextState::CursorAnimReset() { CursorAnim = -0.30f; } // After a user-input the cursor stays on for a while without blinking
4180void ImGuiInputTextState::CursorClamp() { Stb->cursor = ImMin(Stb->cursor, TextLen); Stb->select_start = ImMin(Stb->select_start, TextLen); Stb->select_end = ImMin(Stb->select_end, TextLen); }
4181bool ImGuiInputTextState::HasSelection() const { return Stb->select_start != Stb->select_end; }
4182void ImGuiInputTextState::ClearSelection() { Stb->select_start = Stb->select_end = Stb->cursor; }
4183int ImGuiInputTextState::GetCursorPos() const { return Stb->cursor; }
4184int ImGuiInputTextState::GetSelectionStart() const { return Stb->select_start; }
4185int ImGuiInputTextState::GetSelectionEnd() const { return Stb->select_end; }
4186void ImGuiInputTextState::SelectAll() { Stb->select_start = 0; Stb->cursor = Stb->select_end = TextLen; Stb->has_preferred_x = 0; }
4187void ImGuiInputTextState::ReloadUserBufAndSelectAll() { WantReloadUserBuf = true; ReloadSelectionStart = 0; ReloadSelectionEnd = INT_MAX; }
4188void ImGuiInputTextState::ReloadUserBufAndKeepSelection() { WantReloadUserBuf = true; ReloadSelectionStart = Stb->select_start; ReloadSelectionEnd = Stb->select_end; }
4189void ImGuiInputTextState::ReloadUserBufAndMoveToEnd() { WantReloadUserBuf = true; ReloadSelectionStart = ReloadSelectionEnd = INT_MAX; }
4190
4191ImGuiInputTextCallbackData::ImGuiInputTextCallbackData()
4192{
4193 memset(this, 0, sizeof(*this));
4194}
4195
4196// Public API to manipulate UTF-8 text from within a callback.
4197// FIXME: The existence of this rarely exercised code path is a bit of a nuisance.
4198// Historically they existed because STB_TEXTEDIT_INSERTCHARS() etc. worked on our ImWchar
4199// buffer, but nowadays they both work on UTF-8 data. Should aim to merge both.
4200void ImGuiInputTextCallbackData::DeleteChars(int pos, int bytes_count)
4201{
4202 IM_ASSERT(pos + bytes_count <= BufTextLen);
4203 char* dst = Buf + pos;
4204 const char* src = Buf + pos + bytes_count;
4205 memmove(dst, src, BufTextLen - bytes_count - pos + 1);
4206
4207 if (CursorPos >= pos + bytes_count)
4208 CursorPos -= bytes_count;
4209 else if (CursorPos >= pos)
4210 CursorPos = pos;
4211 SelectionStart = SelectionEnd = CursorPos;
4212 BufDirty = true;
4213 BufTextLen -= bytes_count;
4214}
4215
4216void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, const char* new_text_end)
4217{
4218 // Accept null ranges
4219 if (new_text == new_text_end)
4220 return;
4221
4222 // Grow internal buffer if needed
4223 const bool is_resizable = (Flags & ImGuiInputTextFlags_CallbackResize) != 0;
4224 const int new_text_len = new_text_end ? (int)(new_text_end - new_text) : (int)strlen(new_text);
4225 if (new_text_len + BufTextLen >= BufSize)
4226 {
4227 if (!is_resizable)
4228 return;
4229
4230 ImGuiContext& g = *Ctx;
4231 ImGuiInputTextState* edit_state = &g.InputTextState;
4232 IM_ASSERT(edit_state->ID != 0 && g.ActiveId == edit_state->ID);
4233 IM_ASSERT(Buf == edit_state->TextA.Data);
4234 int new_buf_size = BufTextLen + ImClamp(new_text_len * 4, 32, ImMax(256, new_text_len)) + 1;
4235 edit_state->TextA.resize(new_buf_size + 1);
4236 edit_state->TextSrc = edit_state->TextA.Data;
4237 Buf = edit_state->TextA.Data;
4238 BufSize = edit_state->BufCapacity = new_buf_size;
4239 }
4240
4241 if (BufTextLen != pos)
4242 memmove(Buf + pos + new_text_len, Buf + pos, (size_t)(BufTextLen - pos));
4243 memcpy(Buf + pos, new_text, (size_t)new_text_len * sizeof(char));
4244 Buf[BufTextLen + new_text_len] = '\0';
4245
4246 if (CursorPos >= pos)
4247 CursorPos += new_text_len;
4248 SelectionStart = SelectionEnd = CursorPos;
4249 BufDirty = true;
4250 BufTextLen += new_text_len;
4251}
4252
4253// Return false to discard a character.
4254static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard)
4255{
4256 unsigned int c = *p_char;
4257
4258 // Filter non-printable (NB: isprint is unreliable! see #2467)
4259 bool apply_named_filters = true;
4260 if (c < 0x20)
4261 {
4262 bool pass = false;
4263 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)
4264 pass |= (c == '\t') && (flags & ImGuiInputTextFlags_AllowTabInput) != 0;
4265 if (!pass)
4266 return false;
4267 apply_named_filters = false; // Override named filters below so newline and tabs can still be inserted.
4268 }
4269
4270 if (input_source_is_clipboard == false)
4271 {
4272 // We ignore Ascii representation of delete (emitted from Backspace on OSX, see #2578, #2817)
4273 if (c == 127)
4274 return false;
4275
4276 // Filter private Unicode range. GLFW on OSX seems to send private characters for special keys like arrow keys (FIXME)
4277 if (c >= 0xE000 && c <= 0xF8FF)
4278 return false;
4279 }
4280
4281 // Filter Unicode ranges we are not handling in this build
4282 if (c > IM_UNICODE_CODEPOINT_MAX)
4283 return false;
4284
4285 // Generic named filters
4286 if (apply_named_filters && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint)))
4287 {
4288 // 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 '.'.
4289 // The standard mandate that programs starts in the "C" locale where the decimal point is '.'.
4290 // 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.
4291 // Change the default decimal_point with:
4292 // ImGui::GetPlatformIO()->Platform_LocaleDecimalPoint = *localeconv()->decimal_point;
4293 // 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.
4294 ImGuiContext& g = *ctx;
4295 const unsigned c_decimal_point = (unsigned int)g.PlatformIO.Platform_LocaleDecimalPoint;
4296 if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint))
4297 if (c == '.' || c == ',')
4298 c = c_decimal_point;
4299
4300 // Full-width -> half-width conversion for numeric fields (https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
4301 // While this is mostly convenient, this has the side-effect for uninformed users accidentally inputting full-width characters that they may
4302 // scratch their head as to why it works in numerical fields vs in generic text fields it would require support in the font.
4303 if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | ImGuiInputTextFlags_CharsHexadecimal))
4304 if (c >= 0xFF01 && c <= 0xFF5E)
4305 c = c - 0xFF01 + 0x21;
4306
4307 // Allow 0-9 . - + * /
4308 if (flags & ImGuiInputTextFlags_CharsDecimal)
4309 if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/'))
4310 return false;
4311
4312 // Allow 0-9 . - + * / e E
4313 if (flags & ImGuiInputTextFlags_CharsScientific)
4314 if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/') && (c != 'e') && (c != 'E'))
4315 return false;
4316
4317 // Allow 0-9 a-F A-F
4318 if (flags & ImGuiInputTextFlags_CharsHexadecimal)
4319 if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
4320 return false;
4321
4322 // Turn a-z into A-Z
4323 if (flags & ImGuiInputTextFlags_CharsUppercase)
4324 if (c >= 'a' && c <= 'z')
4325 c += (unsigned int)('A' - 'a');
4326
4327 if (flags & ImGuiInputTextFlags_CharsNoBlank)
4328 if (ImCharIsBlankW(c))
4329 return false;
4330
4331 *p_char = c;
4332 }
4333
4334 // Custom callback filter
4335 if (flags & ImGuiInputTextFlags_CallbackCharFilter)
4336 {
4337 ImGuiContext& g = *GImGui;
4338 ImGuiInputTextCallbackData callback_data;
4339 callback_data.Ctx = &g;
4340 callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter;
4341 callback_data.EventChar = (ImWchar)c;
4342 callback_data.Flags = flags;
4343 callback_data.UserData = user_data;
4344 if (callback(&callback_data) != 0)
4345 return false;
4346 *p_char = callback_data.EventChar;
4347 if (!callback_data.EventChar)
4348 return false;
4349 }
4350
4351 return true;
4352}
4353
4354// Find the shortest single replacement we can make to get from old_buf to new_buf
4355// Note that this doesn't directly alter state->TextA, state->TextLen. They are expected to be made valid separately.
4356// 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.
4357static void InputTextReconcileUndoState(ImGuiInputTextState* state, const char* old_buf, int old_length, const char* new_buf, int new_length)
4358{
4359 const int shorter_length = ImMin(old_length, new_length);
4360 int first_diff;
4361 for (first_diff = 0; first_diff < shorter_length; first_diff++)
4362 if (old_buf[first_diff] != new_buf[first_diff])
4363 break;
4364 if (first_diff == old_length && first_diff == new_length)
4365 return;
4366
4367 int old_last_diff = old_length - 1;
4368 int new_last_diff = new_length - 1;
4369 for (; old_last_diff >= first_diff && new_last_diff >= first_diff; old_last_diff--, new_last_diff--)
4370 if (old_buf[old_last_diff] != new_buf[new_last_diff])
4371 break;
4372
4373 const int insert_len = new_last_diff - first_diff + 1;
4374 const int delete_len = old_last_diff - first_diff + 1;
4375 if (insert_len > 0 || delete_len > 0)
4376 if (IMSTB_TEXTEDIT_CHARTYPE* p = stb_text_createundo(&state->Stb->undostate, first_diff, delete_len, insert_len))
4377 for (int i = 0; i < delete_len; i++)
4378 p[i] = old_buf[first_diff + i];
4379}
4380
4381// As InputText() retain textual data and we currently provide a path for user to not retain it (via local variables)
4382// we need some form of hook to reapply data back to user buffer on deactivation frame. (#4714)
4383// It would be more desirable that we discourage users from taking advantage of the "user not retaining data" trick,
4384// but that more likely be attractive when we do have _NoLiveEdit flag available.
4385void ImGui::InputTextDeactivateHook(ImGuiID id)
4386{
4387 ImGuiContext& g = *GImGui;
4388 ImGuiInputTextState* state = &g.InputTextState;
4389 if (id == 0 || state->ID != id)
4390 return;
4391 g.InputTextDeactivatedState.ID = state->ID;
4392 if (state->Flags & ImGuiInputTextFlags_ReadOnly)
4393 {
4394 g.InputTextDeactivatedState.TextA.resize(0); // In theory this data won't be used, but clear to be neat.
4395 }
4396 else
4397 {
4398 IM_ASSERT(state->TextA.Data != 0);
4399 IM_ASSERT(state->TextA[state->TextLen] == 0);
4400 g.InputTextDeactivatedState.TextA.resize(state->TextLen + 1);
4401 memcpy(g.InputTextDeactivatedState.TextA.Data, state->TextA.Data, state->TextLen + 1);
4402 }
4403}
4404
4405// Edit a string of text
4406// - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!".
4407// This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match
4408// Note that in std::string world, capacity() would omit 1 byte used by the zero-terminator.
4409// - 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.
4410// - If you want to use ImGui::InputText() with std::string, see misc/cpp/imgui_stdlib.h
4411// (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
4412// 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)
4413bool 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)
4414{
4415 ImGuiWindow* window = GetCurrentWindow();
4416 if (window->SkipItems)
4417 return false;
4418
4419 IM_ASSERT(buf != NULL && buf_size >= 0);
4420 IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys)
4421 IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key)
4422 IM_ASSERT(!((flags & ImGuiInputTextFlags_ElideLeft) && (flags & ImGuiInputTextFlags_Multiline))); // Multiline will not work with left-trimming
4423
4424 ImGuiContext& g = *GImGui;
4425 ImGuiIO& io = g.IO;
4426 const ImGuiStyle& style = g.Style;
4427
4428 const bool RENDER_SELECTION_WHEN_INACTIVE = false;
4429 const bool is_multiline = (flags & ImGuiInputTextFlags_Multiline) != 0;
4430
4431 if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope (including the scrollbar)
4432 BeginGroup();
4433 const ImGuiID id = window->GetID(label);
4434 const ImVec2 label_size = CalcTextSize(label, NULL, true);
4435 const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), (is_multiline ? g.FontSize * 8.0f : label_size.y) + style.FramePadding.y * 2.0f); // Arbitrary default of 8 lines high for multi-line
4436 const ImVec2 total_size = ImVec2(frame_size.x + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), frame_size.y);
4437
4438 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
4439 const ImRect total_bb(frame_bb.Min, frame_bb.Min + total_size);
4440
4441 ImGuiWindow* draw_window = window;
4442 ImVec2 inner_size = frame_size;
4443 ImGuiLastItemData item_data_backup;
4444 if (is_multiline)
4445 {
4446 ImVec2 backup_pos = window->DC.CursorPos;
4447 ItemSize(total_bb, style.FramePadding.y);
4448 if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable))
4449 {
4450 EndGroup();
4451 return false;
4452 }
4453 item_data_backup = g.LastItemData;
4454 window->DC.CursorPos = backup_pos;
4455
4456 // Prevent NavActivation from Tabbing when our widget accepts Tab inputs: this allows cycling through widgets without stopping.
4457 if (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_FromTabbing) && (flags & ImGuiInputTextFlags_AllowTabInput))
4458 g.NavActivateId = 0;
4459
4460 // Prevent NavActivate reactivating in BeginChild() when we are already active.
4461 const ImGuiID backup_activate_id = g.NavActivateId;
4462 if (g.ActiveId == id) // Prevent reactivation
4463 g.NavActivateId = 0;
4464
4465 // We reproduce the contents of BeginChildFrame() in order to provide 'label' so our window internal data are easier to read/debug.
4466 PushStyleColor(ImGuiCol_ChildBg, style.Colors[ImGuiCol_FrameBg]);
4467 PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding);
4468 PushStyleVar(ImGuiStyleVar_ChildBorderSize, style.FrameBorderSize);
4469 PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); // Ensure no clip rect so mouse hover can reach FramePadding edges
4470 bool child_visible = BeginChildEx(label, id, frame_bb.GetSize(), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoMove);
4471 g.NavActivateId = backup_activate_id;
4472 PopStyleVar(3);
4473 PopStyleColor();
4474 if (!child_visible)
4475 {
4476 EndChild();
4477 EndGroup();
4478 return false;
4479 }
4480 draw_window = g.CurrentWindow; // Child window
4481 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.
4482 draw_window->DC.CursorPos += style.FramePadding;
4483 inner_size.x -= draw_window->ScrollbarSizes.x;
4484 }
4485 else
4486 {
4487 // Support for internal ImGuiInputTextFlags_MergedItem flag, which could be redesigned as an ItemFlags if needed (with test performed in ItemAdd)
4488 ItemSize(total_bb, style.FramePadding.y);
4489 if (!(flags & ImGuiInputTextFlags_MergedItem))
4490 if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable))
4491 return false;
4492 }
4493
4494 // Ensure mouse cursor is set even after switching to keyboard/gamepad mode. May generalize further? (#6417)
4495 bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags | ImGuiItemFlags_NoNavDisableMouseHover);
4496 if (hovered)
4497 SetMouseCursor(ImGuiMouseCursor_TextInput);
4498 if (hovered && g.NavHighlightItemUnderNav)
4499 hovered = false;
4500
4501 // We are only allowed to access the state if we are already the active widget.
4502 ImGuiInputTextState* state = GetInputTextState(id);
4503
4504 if (g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly)
4505 flags |= ImGuiInputTextFlags_ReadOnly;
4506 const bool is_readonly = (flags & ImGuiInputTextFlags_ReadOnly) != 0;
4507 const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0;
4508 const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0;
4509 const bool is_resizable = (flags & ImGuiInputTextFlags_CallbackResize) != 0;
4510 if (is_resizable)
4511 IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag!
4512
4513 const bool input_requested_by_nav = (g.ActiveId != id) && ((g.NavActivateId == id) && ((g.NavActivateFlags & ImGuiActivateFlags_PreferInput) || (g.NavInputSource == ImGuiInputSource_Keyboard)));
4514
4515 const bool user_clicked = hovered && io.MouseClicked[0];
4516 const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == GetWindowScrollbarID(draw_window, ImGuiAxis_Y);
4517 const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == GetWindowScrollbarID(draw_window, ImGuiAxis_Y);
4518 bool clear_active_id = false;
4519 bool select_all = false;
4520
4521 float scroll_y = is_multiline ? draw_window->Scroll.y : FLT_MAX;
4522
4523 const bool init_reload_from_user_buf = (state != NULL && state->WantReloadUserBuf);
4524 const bool init_changed_specs = (state != NULL && state->Stb->single_line != !is_multiline); // state != NULL means its our state.
4525 const bool init_make_active = (user_clicked || user_scroll_finish || input_requested_by_nav);
4526 const bool init_state = (init_make_active || user_scroll_active);
4527 if (init_reload_from_user_buf)
4528 {
4529 int new_len = (int)strlen(buf);
4530 IM_ASSERT(new_len + 1 <= buf_size && "Is your input buffer properly zero-terminated?");
4531 state->WantReloadUserBuf = false;
4532 InputTextReconcileUndoState(state, state->TextA.Data, state->TextLen, buf, new_len);
4533 state->TextA.resize(buf_size + 1); // we use +1 to make sure that .Data is always pointing to at least an empty string.
4534 state->TextLen = new_len;
4535 memcpy(state->TextA.Data, buf, state->TextLen + 1);
4536 state->Stb->select_start = state->ReloadSelectionStart;
4537 state->Stb->cursor = state->Stb->select_end = state->ReloadSelectionEnd;
4538 state->CursorClamp();
4539 }
4540 else if ((init_state && g.ActiveId != id) || init_changed_specs)
4541 {
4542 // Access state even if we don't own it yet.
4543 state = &g.InputTextState;
4544 state->CursorAnimReset();
4545
4546 // 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)
4547 InputTextDeactivateHook(state->ID);
4548
4549 // Take a copy of the initial buffer value.
4550 // From the moment we focused we are normally ignoring the content of 'buf' (unless we are in read-only mode)
4551 const int buf_len = (int)strlen(buf);
4552 IM_ASSERT(buf_len + 1 <= buf_size && "Is your input buffer properly zero-terminated?");
4553 state->TextToRevertTo.resize(buf_len + 1); // UTF-8. we use +1 to make sure that .Data is always pointing to at least an empty string.
4554 memcpy(state->TextToRevertTo.Data, buf, buf_len + 1);
4555
4556 // Preserve cursor position and undo/redo stack if we come back to same widget
4557 // FIXME: Since we reworked this on 2022/06, may want to differentiate recycle_cursor vs recycle_undostate?
4558 bool recycle_state = (state->ID == id && !init_changed_specs);
4559 if (recycle_state && (state->TextLen != buf_len || (state->TextA.Data == NULL || strncmp(state->TextA.Data, buf, buf_len) != 0)))
4560 recycle_state = false;
4561
4562 // Start edition
4563 state->ID = id;
4564 state->TextLen = buf_len;
4565 if (!is_readonly)
4566 {
4567 state->TextA.resize(buf_size + 1); // we use +1 to make sure that .Data is always pointing to at least an empty string.
4568 memcpy(state->TextA.Data, buf, state->TextLen + 1);
4569 }
4570
4571 // Find initial scroll position for right alignment
4572 state->Scroll = ImVec2(0.0f, 0.0f);
4573 if (flags & ImGuiInputTextFlags_ElideLeft)
4574 state->Scroll.x += ImMax(0.0f, CalcTextSize(buf).x - frame_size.x + style.FramePadding.x * 2.0f);
4575
4576 // Recycle existing cursor/selection/undo stack but clamp position
4577 // Note a single mouse click will override the cursor/position immediately by calling stb_textedit_click handler.
4578 if (recycle_state)
4579 state->CursorClamp();
4580 else
4581 stb_textedit_initialize_state(state->Stb, !is_multiline);
4582
4583 if (!is_multiline)
4584 {
4585 if (flags & ImGuiInputTextFlags_AutoSelectAll)
4586 select_all = true;
4587 if (input_requested_by_nav && (!recycle_state || !(g.NavActivateFlags & ImGuiActivateFlags_TryToPreserveState)))
4588 select_all = true;
4589 if (user_clicked && io.KeyCtrl)
4590 select_all = true;
4591 }
4592
4593 if (flags & ImGuiInputTextFlags_AlwaysOverwrite)
4594 state->Stb->insert_mode = 1; // stb field name is indeed incorrect (see #2863)
4595 }
4596
4597 const bool is_osx = io.ConfigMacOSXBehaviors;
4598 if (g.ActiveId != id && init_make_active)
4599 {
4600 IM_ASSERT(state && state->ID == id);
4601 SetActiveID(id, window);
4602 SetFocusID(id, window);
4603 FocusWindow(window);
4604 }
4605 if (g.ActiveId == id)
4606 {
4607 // Declare some inputs, the other are registered and polled via Shortcut() routing system.
4608 // 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.
4609 const ImGuiKey always_owned_keys[] = { ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_Enter, ImGuiKey_KeypadEnter, ImGuiKey_Delete, ImGuiKey_Backspace, ImGuiKey_Home, ImGuiKey_End };
4610 for (ImGuiKey key : always_owned_keys)
4611 SetKeyOwner(key, id);
4612 if (user_clicked)
4613 SetKeyOwner(ImGuiKey_MouseLeft, id);
4614 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
4615 if (is_multiline || (flags & ImGuiInputTextFlags_CallbackHistory))
4616 {
4617 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
4618 SetKeyOwner(ImGuiKey_UpArrow, id);
4619 SetKeyOwner(ImGuiKey_DownArrow, id);
4620 }
4621 if (is_multiline)
4622 {
4623 SetKeyOwner(ImGuiKey_PageUp, id);
4624 SetKeyOwner(ImGuiKey_PageDown, id);
4625 }
4626 // FIXME: May be a problem to always steal Alt on OSX, would ideally still allow an uninterrupted Alt down-up to toggle menu
4627 if (is_osx)
4628 SetKeyOwner(ImGuiMod_Alt, id);
4629
4630 // Expose scroll in a manner that is agnostic to us using a child window
4631 if (is_multiline && state != NULL)
4632 state->Scroll.y = draw_window->Scroll.y;
4633
4634 // Read-only mode always ever read from source buffer. Refresh TextLen when active.
4635 if (is_readonly && state != NULL)
4636 state->TextLen = (int)strlen(buf);
4637 //if (is_readonly && state != NULL)
4638 // state->TextA.clear(); // Uncomment to facilitate debugging, but we otherwise prefer to keep/amortize th allocation.
4639 }
4640 if (state != NULL)
4641 state->TextSrc = is_readonly ? buf : state->TextA.Data;
4642
4643 // 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)
4644 if (g.ActiveId == id && state == NULL)
4645 ClearActiveID();
4646
4647 // Release focus when we click outside
4648 if (g.ActiveId == id && io.MouseClicked[0] && !init_state && !init_make_active) //-V560
4649 clear_active_id = true;
4650
4651 // Lock the decision of whether we are going to take the path displaying the cursor or selection
4652 bool render_cursor = (g.ActiveId == id) || (state && user_scroll_active);
4653 bool render_selection = state && (state->HasSelection() || select_all) && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
4654 bool value_changed = false;
4655 bool validated = false;
4656
4657 // Select the buffer to render.
4658 const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state;
4659 const bool is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0);
4660
4661 // Password pushes a temporary font with only a fallback glyph
4662 if (is_password && !is_displaying_hint)
4663 {
4664 const ImFontGlyph* glyph = g.Font->FindGlyph('*');
4665 ImFont* password_font = &g.InputTextPasswordFont;
4666 password_font->FontSize = g.Font->FontSize;
4667 password_font->Scale = g.Font->Scale;
4668 password_font->Ascent = g.Font->Ascent;
4669 password_font->Descent = g.Font->Descent;
4670 password_font->ContainerAtlas = g.Font->ContainerAtlas;
4671 password_font->FallbackGlyph = glyph;
4672 password_font->FallbackAdvanceX = glyph->AdvanceX;
4673 IM_ASSERT(password_font->Glyphs.empty() && password_font->IndexAdvanceX.empty() && password_font->IndexLookup.empty());
4674 PushFont(password_font);
4675 }
4676
4677 // Process mouse inputs and character inputs
4678 if (g.ActiveId == id)
4679 {
4680 IM_ASSERT(state != NULL);
4681 state->Edited = false;
4682 state->BufCapacity = buf_size;
4683 state->Flags = flags;
4684
4685 // Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget.
4686 // Down the line we should have a cleaner library-wide concept of Selected vs Active.
4687 g.ActiveIdAllowOverlap = !io.MouseDown[0];
4688
4689 // Edit in progress
4690 const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + state->Scroll.x;
4691 const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y) : (g.FontSize * 0.5f));
4692
4693 if (select_all)
4694 {
4695 state->SelectAll();
4696 state->SelectedAllMouseLock = true;
4697 }
4698 else if (hovered && io.MouseClickedCount[0] >= 2 && !io.KeyShift)
4699 {
4700 stb_textedit_click(state, state->Stb, mouse_x, mouse_y);
4701 const int multiclick_count = (io.MouseClickedCount[0] - 2);
4702 if ((multiclick_count % 2) == 0)
4703 {
4704 // Double-click: Select word
4705 // We always use the "Mac" word advance for double-click select vs CTRL+Right which use the platform dependent variant:
4706 // FIXME: There are likely many ways to improve this behavior, but there's no "right" behavior (depends on use-case, software, OS)
4707 const bool is_bol = (state->Stb->cursor == 0) || ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb->cursor - 1) == '\n';
4708 if (STB_TEXT_HAS_SELECTION(state->Stb) || !is_bol)
4709 state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT);
4710 //state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
4711 if (!STB_TEXT_HAS_SELECTION(state->Stb))
4712 ImStb::stb_textedit_prep_selection_at_cursor(state->Stb);
4713 state->Stb->cursor = ImStb::STB_TEXTEDIT_MOVEWORDRIGHT_MAC(state, state->Stb->cursor);
4714 state->Stb->select_end = state->Stb->cursor;
4715 ImStb::stb_textedit_clamp(state, state->Stb);
4716 }
4717 else
4718 {
4719 // Triple-click: Select line
4720 const bool is_eol = ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb->cursor) == '\n';
4721 state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART);
4722 state->OnKeyPressed(STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT);
4723 state->OnKeyPressed(STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT);
4724 if (!is_eol && is_multiline)
4725 {
4726 ImSwap(state->Stb->select_start, state->Stb->select_end);
4727 state->Stb->cursor = state->Stb->select_end;
4728 }
4729 state->CursorFollow = false;
4730 }
4731 state->CursorAnimReset();
4732 }
4733 else if (io.MouseClicked[0] && !state->SelectedAllMouseLock)
4734 {
4735 if (hovered)
4736 {
4737 if (io.KeyShift)
4738 stb_textedit_drag(state, state->Stb, mouse_x, mouse_y);
4739 else
4740 stb_textedit_click(state, state->Stb, mouse_x, mouse_y);
4741 state->CursorAnimReset();
4742 }
4743 }
4744 else if (io.MouseDown[0] && !state->SelectedAllMouseLock && (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f))
4745 {
4746 stb_textedit_drag(state, state->Stb, mouse_x, mouse_y);
4747 state->CursorAnimReset();
4748 state->CursorFollow = true;
4749 }
4750 if (state->SelectedAllMouseLock && !io.MouseDown[0])
4751 state->SelectedAllMouseLock = false;
4752
4753 // We expect backends to emit a Tab key but some also emit a Tab character which we ignore (#2467, #1336)
4754 // (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)
4755 if ((flags & ImGuiInputTextFlags_AllowTabInput) && !is_readonly)
4756 {
4757 if (Shortcut(ImGuiKey_Tab, ImGuiInputFlags_Repeat, id))
4758 {
4759 unsigned int c = '\t'; // Insert TAB
4760 if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data))
4761 state->OnCharPressed(c);
4762 }
4763 // FIXME: Implement Shift+Tab
4764 /*
4765 if (Shortcut(ImGuiKey_Tab | ImGuiMod_Shift, ImGuiInputFlags_Repeat, id))
4766 {
4767 }
4768 */
4769 }
4770
4771 // Process regular text input (before we check for Return because using some IME will effectively send a Return?)
4772 // 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.
4773 const bool ignore_char_inputs = (io.KeyCtrl && !io.KeyAlt) || (is_osx && io.KeyCtrl);
4774 if (io.InputQueueCharacters.Size > 0)
4775 {
4776 if (!ignore_char_inputs && !is_readonly && !input_requested_by_nav)
4777 for (int n = 0; n < io.InputQueueCharacters.Size; n++)
4778 {
4779 // Insert character if they pass filtering
4780 unsigned int c = (unsigned int)io.InputQueueCharacters[n];
4781 if (c == '\t') // Skip Tab, see above.
4782 continue;
4783 if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data))
4784 state->OnCharPressed(c);
4785 }
4786
4787 // Consume characters
4788 io.InputQueueCharacters.resize(0);
4789 }
4790 }
4791
4792 // Process other shortcuts/key-presses
4793 bool revert_edit = false;
4794 if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id)
4795 {
4796 IM_ASSERT(state != NULL);
4797
4798 const int row_count_per_page = ImMax((int)((inner_size.y - style.FramePadding.y) / g.FontSize), 1);
4799 state->Stb->row_count_per_page = row_count_per_page;
4800
4801 const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0);
4802 const bool is_wordmove_key_down = is_osx ? io.KeyAlt : io.KeyCtrl; // OS X style: Text editing cursor movement using Alt instead of Ctrl
4803 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
4804
4805 // Using Shortcut() with ImGuiInputFlags_RouteFocused (default policy) to allow routing operations for other code (e.g. calling window trying to use CTRL+A and CTRL+B: formet would be handled by InputText)
4806 // Otherwise we could simply assume that we own the keys as we are active.
4807 const ImGuiInputFlags f_repeat = ImGuiInputFlags_Repeat;
4808 const bool is_cut = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_X, f_repeat, id) || Shortcut(ImGuiMod_Shift | ImGuiKey_Delete, f_repeat, id)) && !is_readonly && !is_password && (!is_multiline || state->HasSelection());
4809 const bool is_copy = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_C, 0, id) || Shortcut(ImGuiMod_Ctrl | ImGuiKey_Insert, 0, id)) && !is_password && (!is_multiline || state->HasSelection());
4810 const bool is_paste = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_V, f_repeat, id) || Shortcut(ImGuiMod_Shift | ImGuiKey_Insert, f_repeat, id)) && !is_readonly;
4811 const bool is_undo = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_Z, f_repeat, id)) && !is_readonly && is_undoable;
4812 const bool is_redo = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_Y, f_repeat, id) || (is_osx && Shortcut(ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Z, f_repeat, id))) && !is_readonly && is_undoable;
4813 const bool is_select_all = Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, 0, id);
4814
4815 // 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.
4816 const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0;
4817 const bool is_enter_pressed = IsKeyPressed(ImGuiKey_Enter, true) || IsKeyPressed(ImGuiKey_KeypadEnter, true);
4818 const bool is_gamepad_validate = nav_gamepad_active && (IsKeyPressed(ImGuiKey_NavGamepadActivate, false) || IsKeyPressed(ImGuiKey_NavGamepadInput, false));
4819 const bool is_cancel = Shortcut(ImGuiKey_Escape, f_repeat, id) || (nav_gamepad_active && Shortcut(ImGuiKey_NavGamepadCancel, f_repeat, id));
4820
4821 // FIXME: Should use more Shortcut() and reduce IsKeyPressed()+SetKeyOwner(), but requires modifiers combination to be taken account of.
4822 // FIXME-OSX: Missing support for Alt(option)+Right/Left = go to end of line, or next line if already in end of line.
4823 if (IsKeyPressed(ImGuiKey_LeftArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINESTART : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDLEFT : STB_TEXTEDIT_K_LEFT) | k_mask); }
4824 else if (IsKeyPressed(ImGuiKey_RightArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINEEND : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDRIGHT : STB_TEXTEDIT_K_RIGHT) | k_mask); }
4825 else if (IsKeyPressed(ImGuiKey_UpArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(draw_window, ImMax(draw_window->Scroll.y - g.FontSize, 0.0f)); else state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTSTART : STB_TEXTEDIT_K_UP) | k_mask); }
4826 else if (IsKeyPressed(ImGuiKey_DownArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(draw_window, ImMin(draw_window->Scroll.y + g.FontSize, GetScrollMaxY())); else state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTEND : STB_TEXTEDIT_K_DOWN) | k_mask); }
4827 else if (IsKeyPressed(ImGuiKey_PageUp) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGUP | k_mask); scroll_y -= row_count_per_page * g.FontSize; }
4828 else if (IsKeyPressed(ImGuiKey_PageDown) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGDOWN | k_mask); scroll_y += row_count_per_page * g.FontSize; }
4829 else if (IsKeyPressed(ImGuiKey_Home)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); }
4830 else if (IsKeyPressed(ImGuiKey_End)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); }
4831 else if (IsKeyPressed(ImGuiKey_Delete) && !is_readonly && !is_cut)
4832 {
4833 if (!state->HasSelection())
4834 {
4835 // 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)
4836 if (is_wordmove_key_down)
4837 state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
4838 }
4839 state->OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask);
4840 }
4841 else if (IsKeyPressed(ImGuiKey_Backspace) && !is_readonly)
4842 {
4843 if (!state->HasSelection())
4844 {
4845 if (is_wordmove_key_down)
4846 state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT);
4847 else if (is_osx && io.KeyCtrl && !io.KeyAlt && !io.KeySuper)
4848 state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT);
4849 }
4850 state->OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask);
4851 }
4852 else if (is_enter_pressed || is_gamepad_validate)
4853 {
4854 // Determine if we turn Enter into a \n character
4855 bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0;
4856 if (!is_multiline || is_gamepad_validate || (ctrl_enter_for_new_line && !io.KeyCtrl) || (!ctrl_enter_for_new_line && io.KeyCtrl))
4857 {
4858 validated = true;
4859 if (io.ConfigInputTextEnterKeepActive && !is_multiline)
4860 state->SelectAll(); // No need to scroll
4861 else
4862 clear_active_id = true;
4863 }
4864 else if (!is_readonly)
4865 {
4866 unsigned int c = '\n'; // Insert new line
4867 if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data))
4868 state->OnCharPressed(c);
4869 }
4870 }
4871 else if (is_cancel)
4872 {
4873 if (flags & ImGuiInputTextFlags_EscapeClearsAll)
4874 {
4875 if (buf[0] != 0)
4876 {
4877 revert_edit = true;
4878 }
4879 else
4880 {
4881 render_cursor = render_selection = false;
4882 clear_active_id = true;
4883 }
4884 }
4885 else
4886 {
4887 clear_active_id = revert_edit = true;
4888 render_cursor = render_selection = false;
4889 }
4890 }
4891 else if (is_undo || is_redo)
4892 {
4893 state->OnKeyPressed(is_undo ? STB_TEXTEDIT_K_UNDO : STB_TEXTEDIT_K_REDO);
4894 state->ClearSelection();
4895 }
4896 else if (is_select_all)
4897 {
4898 state->SelectAll();
4899 state->CursorFollow = true;
4900 }
4901 else if (is_cut || is_copy)
4902 {
4903 // Cut, Copy
4904 if (g.PlatformIO.Platform_SetClipboardTextFn != NULL)
4905 {
4906 // SetClipboardText() only takes null terminated strings + state->TextSrc may point to read-only user buffer, so we need to make a copy.
4907 const int ib = state->HasSelection() ? ImMin(state->Stb->select_start, state->Stb->select_end) : 0;
4908 const int ie = state->HasSelection() ? ImMax(state->Stb->select_start, state->Stb->select_end) : state->TextLen;
4909 g.TempBuffer.reserve(ie - ib + 1);
4910 memcpy(g.TempBuffer.Data, state->TextSrc + ib, ie - ib);
4911 g.TempBuffer.Data[ie - ib] = 0;
4912 SetClipboardText(g.TempBuffer.Data);
4913 }
4914 if (is_cut)
4915 {
4916 if (!state->HasSelection())
4917 state->SelectAll();
4918 state->CursorFollow = true;
4919 stb_textedit_cut(state, state->Stb);
4920 }
4921 }
4922 else if (is_paste)
4923 {
4924 if (const char* clipboard = GetClipboardText())
4925 {
4926 // Filter pasted buffer
4927 const int clipboard_len = (int)strlen(clipboard);
4928 ImVector<char> clipboard_filtered;
4929 clipboard_filtered.reserve(clipboard_len + 1);
4930 for (const char* s = clipboard; *s != 0; )
4931 {
4932 unsigned int c;
4933 int in_len = ImTextCharFromUtf8(&c, s, NULL);
4934 s += in_len;
4935 if (!InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data, true))
4936 continue;
4937 char c_utf8[5];
4938 ImTextCharToUtf8(c_utf8, c);
4939 int out_len = (int)strlen(c_utf8);
4940 clipboard_filtered.resize(clipboard_filtered.Size + out_len);
4941 memcpy(clipboard_filtered.Data + clipboard_filtered.Size - out_len, c_utf8, out_len);
4942 }
4943 if (clipboard_filtered.Size > 0) // If everything was filtered, ignore the pasting operation
4944 {
4945 clipboard_filtered.push_back(0);
4946 stb_textedit_paste(state, state->Stb, clipboard_filtered.Data, clipboard_filtered.Size - 1);
4947 state->CursorFollow = true;
4948 }
4949 }
4950 }
4951
4952 // Update render selection flag after events have been handled, so selection highlight can be displayed during the same frame.
4953 render_selection |= state->HasSelection() && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
4954 }
4955
4956 // Process callbacks and apply result back to user's buffer.
4957 const char* apply_new_text = NULL;
4958 int apply_new_text_length = 0;
4959 if (g.ActiveId == id)
4960 {
4961 IM_ASSERT(state != NULL);
4962 if (revert_edit && !is_readonly)
4963 {
4964 if (flags & ImGuiInputTextFlags_EscapeClearsAll)
4965 {
4966 // Clear input
4967 IM_ASSERT(buf[0] != 0);
4968 apply_new_text = "";
4969 apply_new_text_length = 0;
4970 value_changed = true;
4971 IMSTB_TEXTEDIT_CHARTYPE empty_string;
4972 stb_textedit_replace(state, state->Stb, &empty_string, 0);
4973 }
4974 else if (strcmp(buf, state->TextToRevertTo.Data) != 0)
4975 {
4976 apply_new_text = state->TextToRevertTo.Data;
4977 apply_new_text_length = state->TextToRevertTo.Size - 1;
4978
4979 // Restore initial value. Only return true if restoring to the initial value changes the current buffer contents.
4980 // Push records into the undo stack so we can CTRL+Z the revert operation itself
4981 value_changed = true;
4982 stb_textedit_replace(state, state->Stb, state->TextToRevertTo.Data, state->TextToRevertTo.Size - 1);
4983 }
4984 }
4985
4986 // FIXME-OPT: We always reapply the live buffer back to the input buffer before clearing ActiveId,
4987 // even though strictly speaking it wasn't modified on this frame. Should mark dirty state from the stb_textedit callbacks.
4988 // If we do that, need to ensure that as special case, 'validated == true' also writes back.
4989 // This also allows the user to use InputText() without maintaining any user-side storage.
4990 // (please note that if you use this property along ImGuiInputTextFlags_CallbackResize you can end up with your temporary string object
4991 // unnecessarily allocating once a frame, either store your string data, either if you don't then don't use ImGuiInputTextFlags_CallbackResize).
4992 const bool apply_edit_back_to_user_buffer = true;// !revert_edit || (validated && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0);
4993 if (apply_edit_back_to_user_buffer)
4994 {
4995 // Apply current edited text immediately.
4996 // Note that as soon as the input box is active, the in-widget value gets priority over any underlying modification of the input buffer
4997
4998 // User callback
4999 if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackEdit | ImGuiInputTextFlags_CallbackAlways)) != 0)
5000 {
5001 IM_ASSERT(callback != NULL);
5002
5003 // The reason we specify the usage semantic (Completion/History) is that Completion needs to disable keyboard TABBING at the moment.
5004 ImGuiInputTextFlags event_flag = 0;
5005 ImGuiKey event_key = ImGuiKey_None;
5006 if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && Shortcut(ImGuiKey_Tab, 0, id))
5007 {
5008 event_flag = ImGuiInputTextFlags_CallbackCompletion;
5009 event_key = ImGuiKey_Tab;
5010 }
5011 else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_UpArrow))
5012 {
5013 event_flag = ImGuiInputTextFlags_CallbackHistory;
5014 event_key = ImGuiKey_UpArrow;
5015 }
5016 else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_DownArrow))
5017 {
5018 event_flag = ImGuiInputTextFlags_CallbackHistory;
5019 event_key = ImGuiKey_DownArrow;
5020 }
5021 else if ((flags & ImGuiInputTextFlags_CallbackEdit) && state->Edited)
5022 {
5023 event_flag = ImGuiInputTextFlags_CallbackEdit;
5024 }
5025 else if (flags & ImGuiInputTextFlags_CallbackAlways)
5026 {
5027 event_flag = ImGuiInputTextFlags_CallbackAlways;
5028 }
5029
5030 if (event_flag)
5031 {
5032 ImGuiInputTextCallbackData callback_data;
5033 callback_data.Ctx = &g;
5034 callback_data.EventFlag = event_flag;
5035 callback_data.Flags = flags;
5036 callback_data.UserData = callback_user_data;
5037
5038 // FIXME-OPT: Undo stack reconcile needs a backup of the data until we rework API, see #7925
5039 char* callback_buf = is_readonly ? buf : state->TextA.Data;
5040 IM_ASSERT(callback_buf == state->TextSrc);
5041 state->CallbackTextBackup.resize(state->TextLen + 1);
5042 memcpy(state->CallbackTextBackup.Data, callback_buf, state->TextLen + 1);
5043
5044 callback_data.EventKey = event_key;
5045 callback_data.Buf = callback_buf;
5046 callback_data.BufTextLen = state->TextLen;
5047 callback_data.BufSize = state->BufCapacity;
5048 callback_data.BufDirty = false;
5049
5050 const int utf8_cursor_pos = callback_data.CursorPos = state->Stb->cursor;
5051 const int utf8_selection_start = callback_data.SelectionStart = state->Stb->select_start;
5052 const int utf8_selection_end = callback_data.SelectionEnd = state->Stb->select_end;
5053
5054 // Call user code
5055 callback(&callback_data);
5056
5057 // Read back what user may have modified
5058 callback_buf = is_readonly ? buf : state->TextA.Data; // Pointer may have been invalidated by a resize callback
5059 IM_ASSERT(callback_data.Buf == callback_buf); // Invalid to modify those fields
5060 IM_ASSERT(callback_data.BufSize == state->BufCapacity);
5061 IM_ASSERT(callback_data.Flags == flags);
5062 const bool buf_dirty = callback_data.BufDirty;
5063 if (callback_data.CursorPos != utf8_cursor_pos || buf_dirty) { state->Stb->cursor = callback_data.CursorPos; state->CursorFollow = true; }
5064 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; }
5065 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; }
5066 if (buf_dirty)
5067 {
5068 // Callback may update buffer and thus set buf_dirty even in read-only mode.
5069 IM_ASSERT(callback_data.BufTextLen == (int)strlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text!
5070 InputTextReconcileUndoState(state, state->CallbackTextBackup.Data, state->CallbackTextBackup.Size - 1, callback_data.Buf, callback_data.BufTextLen);
5071 state->TextLen = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen()
5072 state->CursorAnimReset();
5073 }
5074 }
5075 }
5076
5077 // Will copy result string if modified
5078 if (!is_readonly && strcmp(state->TextSrc, buf) != 0)
5079 {
5080 apply_new_text = state->TextSrc;
5081 apply_new_text_length = state->TextLen;
5082 value_changed = true;
5083 }
5084 }
5085 }
5086
5087 // Handle reapplying final data on deactivation (see InputTextDeactivateHook() for details)
5088 if (g.InputTextDeactivatedState.ID == id)
5089 {
5090 if (g.ActiveId != id && IsItemDeactivatedAfterEdit() && !is_readonly && strcmp(g.InputTextDeactivatedState.TextA.Data, buf) != 0)
5091 {
5092 apply_new_text = g.InputTextDeactivatedState.TextA.Data;
5093 apply_new_text_length = g.InputTextDeactivatedState.TextA.Size - 1;
5094 value_changed = true;
5095 //IMGUI_DEBUG_LOG("InputText(): apply Deactivated data for 0x%08X: \"%.*s\".\n", id, apply_new_text_length, apply_new_text);
5096 }
5097 g.InputTextDeactivatedState.ID = 0;
5098 }
5099
5100 // Copy result to user buffer. This can currently only happen when (g.ActiveId == id)
5101 if (apply_new_text != NULL)
5102 {
5106 IM_ASSERT(apply_new_text_length >= 0);
5107 if (is_resizable)
5108 {
5109 ImGuiInputTextCallbackData callback_data;
5110 callback_data.Ctx = &g;
5111 callback_data.EventFlag = ImGuiInputTextFlags_CallbackResize;
5112 callback_data.Flags = flags;
5113 callback_data.Buf = buf;
5114 callback_data.BufTextLen = apply_new_text_length;
5115 callback_data.BufSize = ImMax(buf_size, apply_new_text_length + 1);
5116 callback_data.UserData = callback_user_data;
5117 callback(&callback_data);
5118 buf = callback_data.Buf;
5119 buf_size = callback_data.BufSize;
5120 apply_new_text_length = ImMin(callback_data.BufTextLen, buf_size - 1);
5121 IM_ASSERT(apply_new_text_length <= buf_size);
5122 }
5123 //IMGUI_DEBUG_PRINT("InputText(\"%s\"): apply_new_text length %d\n", label, apply_new_text_length);
5124
5125 // If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size.
5126 ImStrncpy(buf, apply_new_text, ImMin(apply_new_text_length + 1, buf_size));
5127 }
5128
5129 // Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value)
5130 // Otherwise request text input ahead for next frame.
5131 if (g.ActiveId == id && clear_active_id)
5132 ClearActiveID();
5133 else if (g.ActiveId == id)
5134 g.WantTextInputNextFrame = 1;
5135
5136 // Render frame
5137 if (!is_multiline)
5138 {
5139 RenderNavCursor(frame_bb, id);
5140 RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
5141 }
5142
5143 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
5144 ImVec2 draw_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding;
5145 ImVec2 text_size(0.0f, 0.0f);
5146
5147 // Set upper limit of single-line InputTextEx() at 2 million characters strings. The current pathological worst case is a long line
5148 // without any carriage return, which would makes ImFont::RenderText() reserve too many vertices and probably crash. Avoid it altogether.
5149 // Note that we only use this limit on single-line InputText(), so a pathologically large line on a InputTextMultiline() would still crash.
5150 const int buf_display_max_length = 2 * 1024 * 1024;
5151 const char* buf_display = buf_display_from_state ? state->TextA.Data : buf; //-V595
5152 const char* buf_display_end = NULL; // We have specialized paths below for setting the length
5153 if (is_displaying_hint)
5154 {
5155 buf_display = hint;
5156 buf_display_end = hint + strlen(hint);
5157 }
5158
5159 // Render text. We currently only render selection when the widget is active or while scrolling.
5160 // FIXME: We could remove the '&& render_cursor' to keep rendering selection when inactive.
5161 if (render_cursor || render_selection)
5162 {
5163 IM_ASSERT(state != NULL);
5164 if (!is_displaying_hint)
5165 buf_display_end = buf_display + state->TextLen;
5166
5167 // Render text (with cursor and selection)
5168 // This is going to be messy. We need to:
5169 // - Display the text (this alone can be more easily clipped)
5170 // - Handle scrolling, highlight selection, display cursor (those all requires some form of 1d->2d cursor position calculation)
5171 // - Measure text height (for scrollbar)
5172 // 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)
5173 // FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8.
5174 const char* text_begin = buf_display;
5175 const char* text_end = text_begin + state->TextLen;
5176 ImVec2 cursor_offset, select_start_offset;
5177
5178 {
5179 // Find lines numbers straddling cursor and selection min position
5180 int cursor_line_no = render_cursor ? -1 : -1000;
5181 int selmin_line_no = render_selection ? -1 : -1000;
5182 const char* cursor_ptr = render_cursor ? text_begin + state->Stb->cursor : NULL;
5183 const char* selmin_ptr = render_selection ? text_begin + ImMin(state->Stb->select_start, state->Stb->select_end) : NULL;
5184
5185 // Count lines and find line number for cursor and selection ends
5186 int line_count = 1;
5187 if (is_multiline)
5188 {
5189 for (const char* s = text_begin; (s = (const char*)memchr(s, '\n', (size_t)(text_end - s))) != NULL; s++)
5190 {
5191 if (cursor_line_no == -1 && s >= cursor_ptr) { cursor_line_no = line_count; }
5192 if (selmin_line_no == -1 && s >= selmin_ptr) { selmin_line_no = line_count; }
5193 line_count++;
5194 }
5195 }
5196 if (cursor_line_no == -1)
5197 cursor_line_no = line_count;
5198 if (selmin_line_no == -1)
5199 selmin_line_no = line_count;
5200
5201 // Calculate 2d position by finding the beginning of the line and measuring distance
5202 cursor_offset.x = InputTextCalcTextSize(&g, ImStrbol(cursor_ptr, text_begin), cursor_ptr).x;
5203 cursor_offset.y = cursor_line_no * g.FontSize;
5204 if (selmin_line_no >= 0)
5205 {
5206 select_start_offset.x = InputTextCalcTextSize(&g, ImStrbol(selmin_ptr, text_begin), selmin_ptr).x;
5207 select_start_offset.y = selmin_line_no * g.FontSize;
5208 }
5209
5210 // Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224)
5211 if (is_multiline)
5212 text_size = ImVec2(inner_size.x, line_count * g.FontSize);
5213 }
5214
5215 // Scroll
5216 if (render_cursor && state->CursorFollow)
5217 {
5218 // Horizontal scroll in chunks of quarter width
5219 if (!(flags & ImGuiInputTextFlags_NoHorizontalScroll))
5220 {
5221 const float scroll_increment_x = inner_size.x * 0.25f;
5222 const float visible_width = inner_size.x - style.FramePadding.x;
5223 if (cursor_offset.x < state->Scroll.x)
5224 state->Scroll.x = IM_TRUNC(ImMax(0.0f, cursor_offset.x - scroll_increment_x));
5225 else if (cursor_offset.x - visible_width >= state->Scroll.x)
5226 state->Scroll.x = IM_TRUNC(cursor_offset.x - visible_width + scroll_increment_x);
5227 }
5228 else
5229 {
5230 state->Scroll.y = 0.0f;
5231 }
5232
5233 // Vertical scroll
5234 if (is_multiline)
5235 {
5236 // Test if cursor is vertically visible
5237 if (cursor_offset.y - g.FontSize < scroll_y)
5238 scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize);
5239 else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y)
5240 scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f;
5241 const float scroll_max_y = ImMax((text_size.y + style.FramePadding.y * 2.0f) - inner_size.y, 0.0f);
5242 scroll_y = ImClamp(scroll_y, 0.0f, scroll_max_y);
5243 draw_pos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag
5244 draw_window->Scroll.y = scroll_y;
5245 }
5246
5247 state->CursorFollow = false;
5248 }
5249
5250 // Draw selection
5251 const ImVec2 draw_scroll = ImVec2(state->Scroll.x, 0.0f);
5252 if (render_selection)
5253 {
5254 const char* text_selected_begin = text_begin + ImMin(state->Stb->select_start, state->Stb->select_end);
5255 const char* text_selected_end = text_begin + ImMax(state->Stb->select_start, state->Stb->select_end);
5256
5257 ImU32 bg_color = GetColorU32(ImGuiCol_TextSelectedBg, render_cursor ? 1.0f : 0.6f); // FIXME: current code flow mandate that render_cursor is always true here, we are leaving the transparent one for tests.
5258 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.
5259 float bg_offy_dn = is_multiline ? 0.0f : 2.0f;
5260 ImVec2 rect_pos = draw_pos + select_start_offset - draw_scroll;
5261 for (const char* p = text_selected_begin; p < text_selected_end; )
5262 {
5263 if (rect_pos.y > clip_rect.w + g.FontSize)
5264 break;
5265 if (rect_pos.y < clip_rect.y)
5266 {
5267 p = (const char*)memchr((void*)p, '\n', text_selected_end - p);
5268 p = p ? p + 1 : text_selected_end;
5269 }
5270 else
5271 {
5272 ImVec2 rect_size = InputTextCalcTextSize(&g, p, text_selected_end, &p, NULL, true);
5273 if (rect_size.x <= 0.0f) rect_size.x = IM_TRUNC(g.Font->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines
5274 ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_size.x, bg_offy_dn));
5275 rect.ClipWith(clip_rect);
5276 if (rect.Overlaps(clip_rect))
5277 draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color);
5278 rect_pos.x = draw_pos.x - draw_scroll.x;
5279 }
5280 rect_pos.y += g.FontSize;
5281 }
5282 }
5283
5284 // 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.
5285 // FIXME-OPT: Multiline could submit a smaller amount of contents to AddText() since we already iterated through it.
5286 if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
5287 {
5288 ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
5289 draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect);
5290 }
5291
5292 // Draw blinking cursor
5293 if (render_cursor)
5294 {
5295 state->CursorAnim += io.DeltaTime;
5296 bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f;
5297 ImVec2 cursor_screen_pos = ImTrunc(draw_pos + cursor_offset - draw_scroll);
5298 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);
5299 if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect))
5300 draw_window->DrawList->AddLine(cursor_screen_rect.Min, cursor_screen_rect.GetBL(), GetColorU32(ImGuiCol_Text));
5301
5302 // 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.)
5303 if (!is_readonly)
5304 {
5305 g.PlatformImeData.WantVisible = true;
5306 g.PlatformImeData.InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize);
5307 g.PlatformImeData.InputLineHeight = g.FontSize;
5308 g.PlatformImeViewport = window->Viewport->ID;
5309 }
5310 }
5311 }
5312 else
5313 {
5314 // Render text only (no selection, no cursor)
5315 if (is_multiline)
5316 text_size = ImVec2(inner_size.x, InputTextCalcTextLenAndLineCount(buf_display, &buf_display_end) * g.FontSize); // We don't need width
5317 else if (!is_displaying_hint && g.ActiveId == id)
5318 buf_display_end = buf_display + state->TextLen;
5319 else if (!is_displaying_hint)
5320 buf_display_end = buf_display + strlen(buf_display);
5321
5322 if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
5323 {
5324 // Find render position for right alignment
5325 if (flags & ImGuiInputTextFlags_ElideLeft)
5326 draw_pos.x = ImMin(draw_pos.x, frame_bb.Max.x - CalcTextSize(buf_display, NULL).x - style.FramePadding.x);
5327
5328 const ImVec2 draw_scroll = /*state ? ImVec2(state->Scroll.x, 0.0f) :*/ ImVec2(0.0f, 0.0f); // Preserve scroll when inactive?
5329 ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
5330 draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect);
5331 }
5332 }
5333
5334 if (is_password && !is_displaying_hint)
5335 PopFont();
5336
5337 if (is_multiline)
5338 {
5339 // For focus requests to work on our multiline we need to ensure our child ItemAdd() call specifies the ImGuiItemFlags_Inputable (see #4761, #7870)...
5340 Dummy(ImVec2(text_size.x, text_size.y + style.FramePadding.y));
5341 g.NextItemData.ItemFlags |= (ImGuiItemFlags)ImGuiItemFlags_Inputable | ImGuiItemFlags_NoTabStop;
5342 EndChild();
5343 item_data_backup.StatusFlags |= (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredWindow);
5344
5345 // ...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...
5346 // FIXME: This quite messy/tricky, should attempt to get rid of the child window.
5347 EndGroup();
5348 if (g.LastItemData.ID == 0 || g.LastItemData.ID != GetWindowScrollbarID(draw_window, ImGuiAxis_Y))
5349 {
5350 g.LastItemData.ID = id;
5351 g.LastItemData.ItemFlags = item_data_backup.ItemFlags;
5352 g.LastItemData.StatusFlags = item_data_backup.StatusFlags;
5353 }
5354 }
5355 if (state)
5356 state->TextSrc = NULL;
5357
5358 // Log as text
5359 if (g.LogEnabled && (!is_password || is_displaying_hint))
5360 {
5361 LogSetNextTextDecoration("{", "}");
5362 LogRenderedText(&draw_pos, buf_display, buf_display_end);
5363 }
5364
5365 if (label_size.x > 0)
5366 RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
5367
5368 if (value_changed)
5369 MarkItemEdited(id);
5370
5371 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
5372 if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0)
5373 return validated;
5374 else
5375 return value_changed;
5376}
5377
5378void ImGui::DebugNodeInputTextState(ImGuiInputTextState* state)
5379{
5380#ifndef IMGUI_DISABLE_DEBUG_TOOLS
5381 ImGuiContext& g = *GImGui;
5382 ImStb::STB_TexteditState* stb_state = state->Stb;
5383 ImStb::StbUndoState* undo_state = &stb_state->undostate;
5384 Text("ID: 0x%08X, ActiveID: 0x%08X", state->ID, g.ActiveId);
5385 DebugLocateItemOnHover(state->ID);
5386 Text("CurLenA: %d, Cursor: %d, Selection: %d..%d", state->TextLen, stb_state->cursor, stb_state->select_start, stb_state->select_end);
5387 Text("BufCapacityA: %d", state->BufCapacity);
5388 Text("(Internal Buffer: TextA Size: %d, Capacity: %d)", state->TextA.Size, state->TextA.Capacity);
5389 Text("has_preferred_x: %d (%.2f)", stb_state->has_preferred_x, stb_state->preferred_x);
5390 Text("undo_point: %d, redo_point: %d, undo_char_point: %d, redo_char_point: %d", undo_state->undo_point, undo_state->redo_point, undo_state->undo_char_point, undo_state->redo_char_point);
5391 if (BeginChild("undopoints", ImVec2(0.0f, GetTextLineHeight() * 10), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY)) // Visualize undo state
5392 {
5393 PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
5394 for (int n = 0; n < IMSTB_TEXTEDIT_UNDOSTATECOUNT; n++)
5395 {
5396 ImStb::StbUndoRecord* undo_rec = &undo_state->undo_rec[n];
5397 const char undo_rec_type = (n < undo_state->undo_point) ? 'u' : (n >= undo_state->redo_point) ? 'r' : ' ';
5398 if (undo_rec_type == ' ')
5399 BeginDisabled();
5400 const int buf_preview_len = (undo_rec_type != ' ' && undo_rec->char_storage != -1) ? undo_rec->insert_length : 0;
5401 const char* buf_preview_str = undo_state->undo_char + undo_rec->char_storage;
5402 Text("%c [%02d] where %03d, insert %03d, delete %03d, char_storage %03d \"%.*s\"",
5403 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);
5404 if (undo_rec_type == ' ')
5405 EndDisabled();
5406 }
5407 PopStyleVar();
5408 }
5409 EndChild();
5410#else
5411 IM_UNUSED(state);
5412#endif
5413}
5414
5415//-------------------------------------------------------------------------
5416// [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc.
5417//-------------------------------------------------------------------------
5418// - ColorEdit3()
5419// - ColorEdit4()
5420// - ColorPicker3()
5421// - RenderColorRectWithAlphaCheckerboard() [Internal]
5422// - ColorPicker4()
5423// - ColorButton()
5424// - SetColorEditOptions()
5425// - ColorTooltip() [Internal]
5426// - ColorEditOptionsPopup() [Internal]
5427// - ColorPickerOptionsPopup() [Internal]
5428//-------------------------------------------------------------------------
5429
5430bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags)
5431{
5432 return ColorEdit4(label, col, flags | ImGuiColorEditFlags_NoAlpha);
5433}
5434
5435static void ColorEditRestoreH(const float* col, float* H)
5436{
5437 ImGuiContext& g = *GImGui;
5438 IM_ASSERT(g.ColorEditCurrentID != 0);
5439 if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0)))
5440 return;
5441 *H = g.ColorEditSavedHue;
5442}
5443
5444// ColorEdit supports RGB and HSV inputs. In case of RGB input resulting color may have undefined hue and/or saturation.
5445// Since widget displays both RGB and HSV values we must preserve hue and saturation to prevent these values resetting.
5446static void ColorEditRestoreHS(const float* col, float* H, float* S, float* V)
5447{
5448 ImGuiContext& g = *GImGui;
5449 IM_ASSERT(g.ColorEditCurrentID != 0);
5450 if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0)))
5451 return;
5452
5453 // When S == 0, H is undefined.
5454 // When H == 1 it wraps around to 0.
5455 if (*S == 0.0f || (*H == 0.0f && g.ColorEditSavedHue == 1))
5456 *H = g.ColorEditSavedHue;
5457
5458 // When V == 0, S is undefined.
5459 if (*V == 0.0f)
5460 *S = g.ColorEditSavedSat;
5461}
5462
5463// Edit colors components (each component in 0.0f..1.0f range).
5464// See enum ImGuiColorEditFlags_ for available options. e.g. Only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
5465// 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.
5466bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags)
5467{
5468 ImGuiWindow* window = GetCurrentWindow();
5469 if (window->SkipItems)
5470 return false;
5471
5472 ImGuiContext& g = *GImGui;
5473 const ImGuiStyle& style = g.Style;
5474 const float square_sz = GetFrameHeight();
5475 const char* label_display_end = FindRenderedTextEnd(label);
5476 float w_full = CalcItemWidth();
5477 g.NextItemData.ClearFlags();
5478
5479 BeginGroup();
5480 PushID(label);
5481 const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0);
5482 if (set_current_color_edit_id)
5483 g.ColorEditCurrentID = window->IDStack.back();
5484
5485 // If we're not showing any slider there's no point in doing any HSV conversions
5486 const ImGuiColorEditFlags flags_untouched = flags;
5487 if (flags & ImGuiColorEditFlags_NoInputs)
5488 flags = (flags & (~ImGuiColorEditFlags_DisplayMask_)) | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_NoOptions;
5489
5490 // Context menu: display and modify options (before defaults are applied)
5491 if (!(flags & ImGuiColorEditFlags_NoOptions))
5492 ColorEditOptionsPopup(col, flags);
5493
5494 // Read stored options
5495 if (!(flags & ImGuiColorEditFlags_DisplayMask_))
5496 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DisplayMask_);
5497 if (!(flags & ImGuiColorEditFlags_DataTypeMask_))
5498 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DataTypeMask_);
5499 if (!(flags & ImGuiColorEditFlags_PickerMask_))
5500 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_);
5501 if (!(flags & ImGuiColorEditFlags_InputMask_))
5502 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_InputMask_);
5503 flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_));
5504 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check that only 1 is selected
5505 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
5506
5507 const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0;
5508 const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0;
5509 const int components = alpha ? 4 : 3;
5510 const float w_button = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x);
5511 const float w_inputs = ImMax(w_full - w_button, 1.0f);
5512 w_full = w_inputs + w_button;
5513
5514 // Convert to the formats we need
5515 float f[4] = { col[0], col[1], col[2], alpha ? col[3] : 1.0f };
5516 if ((flags & ImGuiColorEditFlags_InputHSV) && (flags & ImGuiColorEditFlags_DisplayRGB))
5517 ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]);
5518 else if ((flags & ImGuiColorEditFlags_InputRGB) && (flags & ImGuiColorEditFlags_DisplayHSV))
5519 {
5520 // Hue is lost when converting from grayscale rgb (saturation=0). Restore it.
5521 ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]);
5522 ColorEditRestoreHS(col, &f[0], &f[1], &f[2]);
5523 }
5524 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]) };
5525
5526 bool value_changed = false;
5527 bool value_changed_as_float = false;
5528
5529 const ImVec2 pos = window->DC.CursorPos;
5530 const float inputs_offset_x = (style.ColorButtonPosition == ImGuiDir_Left) ? w_button : 0.0f;
5531 window->DC.CursorPos.x = pos.x + inputs_offset_x;
5532
5533 if ((flags & (ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5534 {
5535 // RGB/HSV 0..255 Sliders
5536 const float w_items = w_inputs - style.ItemInnerSpacing.x * (components - 1);
5537
5538 const bool hide_prefix = (IM_TRUNC(w_items / components) <= CalcTextSize((flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x);
5539 static const char* ids[4] = { "##X", "##Y", "##Z", "##W" };
5540 static const char* fmt_table_int[3][4] =
5541 {
5542 { "%3d", "%3d", "%3d", "%3d" }, // Short display
5543 { "R:%3d", "G:%3d", "B:%3d", "A:%3d" }, // Long display for RGBA
5544 { "H:%3d", "S:%3d", "V:%3d", "A:%3d" } // Long display for HSVA
5545 };
5546 static const char* fmt_table_float[3][4] =
5547 {
5548 { "%0.3f", "%0.3f", "%0.3f", "%0.3f" }, // Short display
5549 { "R:%0.3f", "G:%0.3f", "B:%0.3f", "A:%0.3f" }, // Long display for RGBA
5550 { "H:%0.3f", "S:%0.3f", "V:%0.3f", "A:%0.3f" } // Long display for HSVA
5551 };
5552 const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_DisplayHSV) ? 2 : 1;
5553
5554 float prev_split = 0.0f;
5555 for (int n = 0; n < components; n++)
5556 {
5557 if (n > 0)
5558 SameLine(0, style.ItemInnerSpacing.x);
5559 float next_split = IM_TRUNC(w_items * (n + 1) / components);
5560 SetNextItemWidth(ImMax(next_split - prev_split, 1.0f));
5561 prev_split = next_split;
5562
5563 // FIXME: When ImGuiColorEditFlags_HDR flag is passed HS values snap in weird ways when SV values go below 0.
5564 if (flags & ImGuiColorEditFlags_Float)
5565 {
5566 value_changed |= DragFloat(ids[n], &f[n], 1.0f / 255.0f, 0.0f, hdr ? 0.0f : 1.0f, fmt_table_float[fmt_idx][n]);
5567 value_changed_as_float |= value_changed;
5568 }
5569 else
5570 {
5571 value_changed |= DragInt(ids[n], &i[n], 1.0f, 0, hdr ? 0 : 255, fmt_table_int[fmt_idx][n]);
5572 }
5573 if (!(flags & ImGuiColorEditFlags_NoOptions))
5574 OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5575 }
5576 }
5577 else if ((flags & ImGuiColorEditFlags_DisplayHex) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5578 {
5579 // RGB Hexadecimal Input
5580 char buf[64];
5581 if (alpha)
5582 ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255), ImClamp(i[3], 0, 255));
5583 else
5584 ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255));
5585 SetNextItemWidth(w_inputs);
5586 if (InputText("##Text", buf, IM_ARRAYSIZE(buf), ImGuiInputTextFlags_CharsUppercase))
5587 {
5588 value_changed = true;
5589 char* p = buf;
5590 while (*p == '#' || ImCharIsBlankA(*p))
5591 p++;
5592 i[0] = i[1] = i[2] = 0;
5593 i[3] = 0xFF; // alpha default to 255 is not parsed by scanf (e.g. inputting #FFFFFF omitting alpha)
5594 int r;
5595 if (alpha)
5596 r = sscanf(p, "%02X%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2], (unsigned int*)&i[3]); // Treat at unsigned (%X is unsigned)
5597 else
5598 r = sscanf(p, "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]);
5599 IM_UNUSED(r); // Fixes C6031: Return value ignored: 'sscanf'.
5600 }
5601 if (!(flags & ImGuiColorEditFlags_NoOptions))
5602 OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5603 }
5604
5605 ImGuiWindow* picker_active_window = NULL;
5606 if (!(flags & ImGuiColorEditFlags_NoSmallPreview))
5607 {
5608 const float button_offset_x = ((flags & ImGuiColorEditFlags_NoInputs) || (style.ColorButtonPosition == ImGuiDir_Left)) ? 0.0f : w_inputs + style.ItemInnerSpacing.x;
5609 window->DC.CursorPos = ImVec2(pos.x + button_offset_x, pos.y);
5610
5611 const ImVec4 col_v4(col[0], col[1], col[2], alpha ? col[3] : 1.0f);
5612 if (ColorButton("##ColorButton", col_v4, flags))
5613 {
5614 if (!(flags & ImGuiColorEditFlags_NoPicker))
5615 {
5616 // Store current color and open a picker
5617 g.ColorPickerRef = col_v4;
5618 OpenPopup("picker");
5619 SetNextWindowPos(g.LastItemData.Rect.GetBL() + ImVec2(0.0f, style.ItemSpacing.y));
5620 }
5621 }
5622 if (!(flags & ImGuiColorEditFlags_NoOptions))
5623 OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5624
5625 if (BeginPopup("picker"))
5626 {
5627 if (g.CurrentWindow->BeginCount == 1)
5628 {
5629 picker_active_window = g.CurrentWindow;
5630 if (label != label_display_end)
5631 {
5632 TextEx(label, label_display_end);
5633 Spacing();
5634 }
5635 ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar;
5636 ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf;
5637 SetNextItemWidth(square_sz * 12.0f); // Use 256 + bar sizes?
5638 value_changed |= ColorPicker4("##picker", col, picker_flags, &g.ColorPickerRef.x);
5639 }
5640 EndPopup();
5641 }
5642 }
5643
5644 if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel))
5645 {
5646 // Position not necessarily next to last submitted button (e.g. if style.ColorButtonPosition == ImGuiDir_Left),
5647 // but we need to use SameLine() to setup baseline correctly. Might want to refactor SameLine() to simplify this.
5648 SameLine(0.0f, style.ItemInnerSpacing.x);
5649 window->DC.CursorPos.x = pos.x + ((flags & ImGuiColorEditFlags_NoInputs) ? w_button : w_full + style.ItemInnerSpacing.x);
5650 TextEx(label, label_display_end);
5651 }
5652
5653 // Convert back
5654 if (value_changed && picker_active_window == NULL)
5655 {
5656 if (!value_changed_as_float)
5657 for (int n = 0; n < 4; n++)
5658 f[n] = i[n] / 255.0f;
5659 if ((flags & ImGuiColorEditFlags_DisplayHSV) && (flags & ImGuiColorEditFlags_InputRGB))
5660 {
5661 g.ColorEditSavedHue = f[0];
5662 g.ColorEditSavedSat = f[1];
5663 ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]);
5664 g.ColorEditSavedID = g.ColorEditCurrentID;
5665 g.ColorEditSavedColor = ColorConvertFloat4ToU32(ImVec4(f[0], f[1], f[2], 0));
5666 }
5667 if ((flags & ImGuiColorEditFlags_DisplayRGB) && (flags & ImGuiColorEditFlags_InputHSV))
5668 ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]);
5669
5670 col[0] = f[0];
5671 col[1] = f[1];
5672 col[2] = f[2];
5673 if (alpha)
5674 col[3] = f[3];
5675 }
5676
5677 if (set_current_color_edit_id)
5678 g.ColorEditCurrentID = 0;
5679 PopID();
5680 EndGroup();
5681
5682 // Drag and Drop Target
5683 // NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test.
5684 if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget())
5685 {
5686 bool accepted_drag_drop = false;
5687 if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F))
5688 {
5689 memcpy((float*)col, payload->Data, sizeof(float) * 3); // Preserve alpha if any //-V512 //-V1086
5690 value_changed = accepted_drag_drop = true;
5691 }
5692 if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F))
5693 {
5694 memcpy((float*)col, payload->Data, sizeof(float) * components);
5695 value_changed = accepted_drag_drop = true;
5696 }
5697
5698 // Drag-drop payloads are always RGB
5699 if (accepted_drag_drop && (flags & ImGuiColorEditFlags_InputHSV))
5700 ColorConvertRGBtoHSV(col[0], col[1], col[2], col[0], col[1], col[2]);
5701 EndDragDropTarget();
5702 }
5703
5704 // When picker is being actively used, use its active id so IsItemActive() will function on ColorEdit4().
5705 if (picker_active_window && g.ActiveId != 0 && g.ActiveIdWindow == picker_active_window)
5706 g.LastItemData.ID = g.ActiveId;
5707
5708 if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
5709 MarkItemEdited(g.LastItemData.ID);
5710
5711 return value_changed;
5712}
5713
5714bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags)
5715{
5716 float col4[4] = { col[0], col[1], col[2], 1.0f };
5717 if (!ColorPicker4(label, col4, flags | ImGuiColorEditFlags_NoAlpha))
5718 return false;
5719 col[0] = col4[0]; col[1] = col4[1]; col[2] = col4[2];
5720 return true;
5721}
5722
5723// Helper for ColorPicker4()
5724static void RenderArrowsForVerticalBar(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w, float alpha)
5725{
5726 ImU32 alpha8 = IM_F32_TO_INT8_SAT(alpha);
5727 ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + half_sz.x + 1, pos.y), ImVec2(half_sz.x + 2, half_sz.y + 1), ImGuiDir_Right, IM_COL32(0,0,0,alpha8));
5728 ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + half_sz.x, pos.y), half_sz, ImGuiDir_Right, IM_COL32(255,255,255,alpha8));
5729 ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + bar_w - half_sz.x - 1, pos.y), ImVec2(half_sz.x + 2, half_sz.y + 1), ImGuiDir_Left, IM_COL32(0,0,0,alpha8));
5730 ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + bar_w - half_sz.x, pos.y), half_sz, ImGuiDir_Left, IM_COL32(255,255,255,alpha8));
5731}
5732
5733// Note: ColorPicker4() only accesses 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
5734// (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.)
5735// 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..)
5736// 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)
5737bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags, const float* ref_col)
5738{
5739 ImGuiContext& g = *GImGui;
5740 ImGuiWindow* window = GetCurrentWindow();
5741 if (window->SkipItems)
5742 return false;
5743
5744 ImDrawList* draw_list = window->DrawList;
5745 ImGuiStyle& style = g.Style;
5746 ImGuiIO& io = g.IO;
5747
5748 const float width = CalcItemWidth();
5749 const bool is_readonly = ((g.NextItemData.ItemFlags | g.CurrentItemFlags) & ImGuiItemFlags_ReadOnly) != 0;
5750 g.NextItemData.ClearFlags();
5751
5752 PushID(label);
5753 const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0);
5754 if (set_current_color_edit_id)
5755 g.ColorEditCurrentID = window->IDStack.back();
5756 BeginGroup();
5757
5758 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5759 flags |= ImGuiColorEditFlags_NoSmallPreview;
5760
5761 // Context menu: display and store options.
5762 if (!(flags & ImGuiColorEditFlags_NoOptions))
5763 ColorPickerOptionsPopup(col, flags);
5764
5765 // Read stored options
5766 if (!(flags & ImGuiColorEditFlags_PickerMask_))
5767 flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_PickerMask_;
5768 if (!(flags & ImGuiColorEditFlags_InputMask_))
5769 flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_InputMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_InputMask_;
5770 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check that only 1 is selected
5771 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
5772 if (!(flags & ImGuiColorEditFlags_NoOptions))
5773 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_AlphaBar);
5774
5775 // Setup
5776 int components = (flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4;
5777 bool alpha_bar = (flags & ImGuiColorEditFlags_AlphaBar) && !(flags & ImGuiColorEditFlags_NoAlpha);
5778 ImVec2 picker_pos = window->DC.CursorPos;
5779 float square_sz = GetFrameHeight();
5780 float bars_width = square_sz; // Arbitrary smallish width of Hue/Alpha picking bars
5781 float sv_picker_size = ImMax(bars_width * 1, width - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box
5782 float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x;
5783 float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x;
5784 float bars_triangles_half_sz = IM_TRUNC(bars_width * 0.20f);
5785
5786 float backup_initial_col[4];
5787 memcpy(backup_initial_col, col, components * sizeof(float));
5788
5789 float wheel_thickness = sv_picker_size * 0.08f;
5790 float wheel_r_outer = sv_picker_size * 0.50f;
5791 float wheel_r_inner = wheel_r_outer - wheel_thickness;
5792 ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width)*0.5f, picker_pos.y + sv_picker_size * 0.5f);
5793
5794 // Note: the triangle is displayed rotated with triangle_pa pointing to Hue, but most coordinates stays unrotated for logic.
5795 float triangle_r = wheel_r_inner - (int)(sv_picker_size * 0.027f);
5796 ImVec2 triangle_pa = ImVec2(triangle_r, 0.0f); // Hue point.
5797 ImVec2 triangle_pb = ImVec2(triangle_r * -0.5f, triangle_r * -0.866025f); // Black point.
5798 ImVec2 triangle_pc = ImVec2(triangle_r * -0.5f, triangle_r * +0.866025f); // White point.
5799
5800 float H = col[0], S = col[1], V = col[2];
5801 float R = col[0], G = col[1], B = col[2];
5802 if (flags & ImGuiColorEditFlags_InputRGB)
5803 {
5804 // Hue is lost when converting from grayscale rgb (saturation=0). Restore it.
5805 ColorConvertRGBtoHSV(R, G, B, H, S, V);
5806 ColorEditRestoreHS(col, &H, &S, &V);
5807 }
5808 else if (flags & ImGuiColorEditFlags_InputHSV)
5809 {
5810 ColorConvertHSVtoRGB(H, S, V, R, G, B);
5811 }
5812
5813 bool value_changed = false, value_changed_h = false, value_changed_sv = false;
5814
5815 PushItemFlag(ImGuiItemFlags_NoNav, true);
5816 if (flags & ImGuiColorEditFlags_PickerHueWheel)
5817 {
5818 // Hue wheel + SV triangle logic
5819 InvisibleButton("hsv", ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size));
5820 if (IsItemActive() && !is_readonly)
5821 {
5822 ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center;
5823 ImVec2 current_off = g.IO.MousePos - wheel_center;
5824 float initial_dist2 = ImLengthSqr(initial_off);
5825 if (initial_dist2 >= (wheel_r_inner - 1) * (wheel_r_inner - 1) && initial_dist2 <= (wheel_r_outer + 1) * (wheel_r_outer + 1))
5826 {
5827 // Interactive with Hue wheel
5828 H = ImAtan2(current_off.y, current_off.x) / IM_PI * 0.5f;
5829 if (H < 0.0f)
5830 H += 1.0f;
5831 value_changed = value_changed_h = true;
5832 }
5833 float cos_hue_angle = ImCos(-H * 2.0f * IM_PI);
5834 float sin_hue_angle = ImSin(-H * 2.0f * IM_PI);
5835 if (ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, ImRotate(initial_off, cos_hue_angle, sin_hue_angle)))
5836 {
5837 // Interacting with SV triangle
5838 ImVec2 current_off_unrotated = ImRotate(current_off, cos_hue_angle, sin_hue_angle);
5839 if (!ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated))
5840 current_off_unrotated = ImTriangleClosestPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated);
5841 float uu, vv, ww;
5842 ImTriangleBarycentricCoords(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated, uu, vv, ww);
5843 V = ImClamp(1.0f - vv, 0.0001f, 1.0f);
5844 S = ImClamp(uu / V, 0.0001f, 1.0f);
5845 value_changed = value_changed_sv = true;
5846 }
5847 }
5848 if (!(flags & ImGuiColorEditFlags_NoOptions))
5849 OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5850 }
5851 else if (flags & ImGuiColorEditFlags_PickerHueBar)
5852 {
5853 // SV rectangle logic
5854 InvisibleButton("sv", ImVec2(sv_picker_size, sv_picker_size));
5855 if (IsItemActive() && !is_readonly)
5856 {
5857 S = ImSaturate((io.MousePos.x - picker_pos.x) / (sv_picker_size - 1));
5858 V = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5859 ColorEditRestoreH(col, &H); // Greatly reduces hue jitter and reset to 0 when hue == 255 and color is rapidly modified using SV square.
5860 value_changed = value_changed_sv = true;
5861 }
5862 if (!(flags & ImGuiColorEditFlags_NoOptions))
5863 OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5864
5865 // Hue bar logic
5866 SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y));
5867 InvisibleButton("hue", ImVec2(bars_width, sv_picker_size));
5868 if (IsItemActive() && !is_readonly)
5869 {
5870 H = ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5871 value_changed = value_changed_h = true;
5872 }
5873 }
5874
5875 // Alpha bar logic
5876 if (alpha_bar)
5877 {
5878 SetCursorScreenPos(ImVec2(bar1_pos_x, picker_pos.y));
5879 InvisibleButton("alpha", ImVec2(bars_width, sv_picker_size));
5880 if (IsItemActive())
5881 {
5882 col[3] = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5883 value_changed = true;
5884 }
5885 }
5886 PopItemFlag(); // ImGuiItemFlags_NoNav
5887
5888 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5889 {
5890 SameLine(0, style.ItemInnerSpacing.x);
5891 BeginGroup();
5892 }
5893
5894 if (!(flags & ImGuiColorEditFlags_NoLabel))
5895 {
5896 const char* label_display_end = FindRenderedTextEnd(label);
5897 if (label != label_display_end)
5898 {
5899 if ((flags & ImGuiColorEditFlags_NoSidePreview))
5900 SameLine(0, style.ItemInnerSpacing.x);
5901 TextEx(label, label_display_end);
5902 }
5903 }
5904
5905 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5906 {
5907 PushItemFlag(ImGuiItemFlags_NoNavDefaultFocus, true);
5908 ImVec4 col_v4(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
5909 if ((flags & ImGuiColorEditFlags_NoLabel))
5910 Text("Current");
5911
5912 ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf | ImGuiColorEditFlags_NoTooltip;
5913 ColorButton("##current", col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2));
5914 if (ref_col != NULL)
5915 {
5916 Text("Original");
5917 ImVec4 ref_col_v4(ref_col[0], ref_col[1], ref_col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : ref_col[3]);
5918 if (ColorButton("##original", ref_col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2)))
5919 {
5920 memcpy(col, ref_col, components * sizeof(float));
5921 value_changed = true;
5922 }
5923 }
5924 PopItemFlag();
5925 EndGroup();
5926 }
5927
5928 // Convert back color to RGB
5929 if (value_changed_h || value_changed_sv)
5930 {
5931 if (flags & ImGuiColorEditFlags_InputRGB)
5932 {
5933 ColorConvertHSVtoRGB(H, S, V, col[0], col[1], col[2]);
5934 g.ColorEditSavedHue = H;
5935 g.ColorEditSavedSat = S;
5936 g.ColorEditSavedID = g.ColorEditCurrentID;
5937 g.ColorEditSavedColor = ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0));
5938 }
5939 else if (flags & ImGuiColorEditFlags_InputHSV)
5940 {
5941 col[0] = H;
5942 col[1] = S;
5943 col[2] = V;
5944 }
5945 }
5946
5947 // R,G,B and H,S,V slider color editor
5948 bool value_changed_fix_hue_wrap = false;
5949 if ((flags & ImGuiColorEditFlags_NoInputs) == 0)
5950 {
5951 PushItemWidth((alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x);
5952 ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoSmallPreview | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf;
5953 ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker;
5954 if (flags & ImGuiColorEditFlags_DisplayRGB || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
5955 if (ColorEdit4("##rgb", col, sub_flags | ImGuiColorEditFlags_DisplayRGB))
5956 {
5957 // FIXME: Hackily differentiating using the DragInt (ActiveId != 0 && !ActiveIdAllowOverlap) vs. using the InputText or DropTarget.
5958 // 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)
5959 value_changed_fix_hue_wrap = (g.ActiveId != 0 && !g.ActiveIdAllowOverlap);
5960 value_changed = true;
5961 }
5962 if (flags & ImGuiColorEditFlags_DisplayHSV || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
5963 value_changed |= ColorEdit4("##hsv", col, sub_flags | ImGuiColorEditFlags_DisplayHSV);
5964 if (flags & ImGuiColorEditFlags_DisplayHex || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
5965 value_changed |= ColorEdit4("##hex", col, sub_flags | ImGuiColorEditFlags_DisplayHex);
5966 PopItemWidth();
5967 }
5968
5969 // Try to cancel hue wrap (after ColorEdit4 call), if any
5970 if (value_changed_fix_hue_wrap && (flags & ImGuiColorEditFlags_InputRGB))
5971 {
5972 float new_H, new_S, new_V;
5973 ColorConvertRGBtoHSV(col[0], col[1], col[2], new_H, new_S, new_V);
5974 if (new_H <= 0 && H > 0)
5975 {
5976 if (new_V <= 0 && V != new_V)
5977 ColorConvertHSVtoRGB(H, S, new_V <= 0 ? V * 0.5f : new_V, col[0], col[1], col[2]);
5978 else if (new_S <= 0)
5979 ColorConvertHSVtoRGB(H, new_S <= 0 ? S * 0.5f : new_S, new_V, col[0], col[1], col[2]);
5980 }
5981 }
5982
5983 if (value_changed)
5984 {
5985 if (flags & ImGuiColorEditFlags_InputRGB)
5986 {
5987 R = col[0];
5988 G = col[1];
5989 B = col[2];
5990 ColorConvertRGBtoHSV(R, G, B, H, S, V);
5991 ColorEditRestoreHS(col, &H, &S, &V); // Fix local Hue as display below will use it immediately.
5992 }
5993 else if (flags & ImGuiColorEditFlags_InputHSV)
5994 {
5995 H = col[0];
5996 S = col[1];
5997 V = col[2];
5998 ColorConvertHSVtoRGB(H, S, V, R, G, B);
5999 }
6000 }
6001
6002 const int style_alpha8 = IM_F32_TO_INT8_SAT(style.Alpha);
6003 const ImU32 col_black = IM_COL32(0,0,0,style_alpha8);
6004 const ImU32 col_white = IM_COL32(255,255,255,style_alpha8);
6005 const ImU32 col_midgrey = IM_COL32(128,128,128,style_alpha8);
6006 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) };
6007
6008 ImVec4 hue_color_f(1, 1, 1, style.Alpha); ColorConvertHSVtoRGB(H, 1, 1, hue_color_f.x, hue_color_f.y, hue_color_f.z);
6009 ImU32 hue_color32 = ColorConvertFloat4ToU32(hue_color_f);
6010 ImU32 user_col32_striped_of_alpha = ColorConvertFloat4ToU32(ImVec4(R, G, B, style.Alpha)); // Important: this is still including the main rendering/style alpha!!
6011
6012 ImVec2 sv_cursor_pos;
6013
6014 if (flags & ImGuiColorEditFlags_PickerHueWheel)
6015 {
6016 // Render Hue Wheel
6017 const float aeps = 0.5f / wheel_r_outer; // Half a pixel arc length in radians (2pi cancels out).
6018 const int segment_per_arc = ImMax(4, (int)wheel_r_outer / 12);
6019 for (int n = 0; n < 6; n++)
6020 {
6021 const float a0 = (n) /6.0f * 2.0f * IM_PI - aeps;
6022 const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps;
6023 const int vert_start_idx = draw_list->VtxBuffer.Size;
6024 draw_list->PathArcTo(wheel_center, (wheel_r_inner + wheel_r_outer)*0.5f, a0, a1, segment_per_arc);
6025 draw_list->PathStroke(col_white, 0, wheel_thickness);
6026 const int vert_end_idx = draw_list->VtxBuffer.Size;
6027
6028 // Paint colors over existing vertices
6029 ImVec2 gradient_p0(wheel_center.x + ImCos(a0) * wheel_r_inner, wheel_center.y + ImSin(a0) * wheel_r_inner);
6030 ImVec2 gradient_p1(wheel_center.x + ImCos(a1) * wheel_r_inner, wheel_center.y + ImSin(a1) * wheel_r_inner);
6031 ShadeVertsLinearColorGradientKeepAlpha(draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, col_hues[n], col_hues[n + 1]);
6032 }
6033
6034 // Render Cursor + preview on Hue Wheel
6035 float cos_hue_angle = ImCos(H * 2.0f * IM_PI);
6036 float sin_hue_angle = ImSin(H * 2.0f * IM_PI);
6037 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);
6038 float hue_cursor_rad = value_changed_h ? wheel_thickness * 0.65f : wheel_thickness * 0.55f;
6039 int hue_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(hue_cursor_rad); // Lock segment count so the +1 one matches others.
6040 draw_list->AddCircleFilled(hue_cursor_pos, hue_cursor_rad, hue_color32, hue_cursor_segments);
6041 draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad + 1, col_midgrey, hue_cursor_segments);
6042 draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad, col_white, hue_cursor_segments);
6043
6044 // Render SV triangle (rotated according to hue)
6045 ImVec2 tra = wheel_center + ImRotate(triangle_pa, cos_hue_angle, sin_hue_angle);
6046 ImVec2 trb = wheel_center + ImRotate(triangle_pb, cos_hue_angle, sin_hue_angle);
6047 ImVec2 trc = wheel_center + ImRotate(triangle_pc, cos_hue_angle, sin_hue_angle);
6048 ImVec2 uv_white = GetFontTexUvWhitePixel();
6049 draw_list->PrimReserve(3, 3);
6050 draw_list->PrimVtx(tra, uv_white, hue_color32);
6051 draw_list->PrimVtx(trb, uv_white, col_black);
6052 draw_list->PrimVtx(trc, uv_white, col_white);
6053 draw_list->AddTriangle(tra, trb, trc, col_midgrey, 1.5f);
6054 sv_cursor_pos = ImLerp(ImLerp(trc, tra, ImSaturate(S)), trb, ImSaturate(1 - V));
6055 }
6056 else if (flags & ImGuiColorEditFlags_PickerHueBar)
6057 {
6058 // Render SV Square
6059 draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), col_white, hue_color32, hue_color32, col_white);
6060 draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0, 0, col_black, col_black);
6061 RenderFrameBorder(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0.0f);
6062 sv_cursor_pos.x = ImClamp(IM_ROUND(picker_pos.x + ImSaturate(S) * sv_picker_size), picker_pos.x + 2, picker_pos.x + sv_picker_size - 2); // Sneakily prevent the circle to stick out too much
6063 sv_cursor_pos.y = ImClamp(IM_ROUND(picker_pos.y + ImSaturate(1 - V) * sv_picker_size), picker_pos.y + 2, picker_pos.y + sv_picker_size - 2);
6064
6065 // Render Hue Bar
6066 for (int i = 0; i < 6; ++i)
6067 draw_list->AddRectFilledMultiColor(ImVec2(bar0_pos_x, picker_pos.y + i * (sv_picker_size / 6)), ImVec2(bar0_pos_x + bars_width, picker_pos.y + (i + 1) * (sv_picker_size / 6)), col_hues[i], col_hues[i], col_hues[i + 1], col_hues[i + 1]);
6068 float bar0_line_y = IM_ROUND(picker_pos.y + H * sv_picker_size);
6069 RenderFrameBorder(ImVec2(bar0_pos_x, picker_pos.y), ImVec2(bar0_pos_x + bars_width, picker_pos.y + sv_picker_size), 0.0f);
6070 RenderArrowsForVerticalBar(draw_list, ImVec2(bar0_pos_x - 1, bar0_line_y), ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bars_width + 2.0f, style.Alpha);
6071 }
6072
6073 // Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range)
6074 float sv_cursor_rad = value_changed_sv ? wheel_thickness * 0.55f : wheel_thickness * 0.40f;
6075 int sv_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(sv_cursor_rad); // Lock segment count so the +1 one matches others.
6076 draw_list->AddCircleFilled(sv_cursor_pos, sv_cursor_rad, user_col32_striped_of_alpha, sv_cursor_segments);
6077 draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad + 1, col_midgrey, sv_cursor_segments);
6078 draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad, col_white, sv_cursor_segments);
6079
6080 // Render alpha bar
6081 if (alpha_bar)
6082 {
6083 float alpha = ImSaturate(col[3]);
6084 ImRect bar1_bb(bar1_pos_x, picker_pos.y, bar1_pos_x + bars_width, picker_pos.y + sv_picker_size);
6085 RenderColorRectWithAlphaCheckerboard(draw_list, bar1_bb.Min, bar1_bb.Max, 0, bar1_bb.GetWidth() / 2.0f, ImVec2(0.0f, 0.0f));
6086 draw_list->AddRectFilledMultiColor(bar1_bb.Min, bar1_bb.Max, user_col32_striped_of_alpha, user_col32_striped_of_alpha, user_col32_striped_of_alpha & ~IM_COL32_A_MASK, user_col32_striped_of_alpha & ~IM_COL32_A_MASK);
6087 float bar1_line_y = IM_ROUND(picker_pos.y + (1.0f - alpha) * sv_picker_size);
6088 RenderFrameBorder(bar1_bb.Min, bar1_bb.Max, 0.0f);
6089 RenderArrowsForVerticalBar(draw_list, ImVec2(bar1_pos_x - 1, bar1_line_y), ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bars_width + 2.0f, style.Alpha);
6090 }
6091
6092 EndGroup();
6093
6094 if (value_changed && memcmp(backup_initial_col, col, components * sizeof(float)) == 0)
6095 value_changed = false;
6096 if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
6097 MarkItemEdited(g.LastItemData.ID);
6098
6099 if (set_current_color_edit_id)
6100 g.ColorEditCurrentID = 0;
6101 PopID();
6102
6103 return value_changed;
6104}
6105
6106// A little color square. Return true when clicked.
6107// FIXME: May want to display/ignore the alpha component in the color display? Yet show it in the tooltip.
6108// 'desc_id' is not called 'label' because we don't display it next to the button, but only in the tooltip.
6109// Note that 'col' may be encoded in HSV if ImGuiColorEditFlags_InputHSV is set.
6110bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, const ImVec2& size_arg)
6111{
6112 ImGuiWindow* window = GetCurrentWindow();
6113 if (window->SkipItems)
6114 return false;
6115
6116 ImGuiContext& g = *GImGui;
6117 const ImGuiID id = window->GetID(desc_id);
6118 const float default_size = GetFrameHeight();
6119 const ImVec2 size(size_arg.x == 0.0f ? default_size : size_arg.x, size_arg.y == 0.0f ? default_size : size_arg.y);
6120 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
6121 ItemSize(bb, (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f);
6122 if (!ItemAdd(bb, id))
6123 return false;
6124
6125 bool hovered, held;
6126 bool pressed = ButtonBehavior(bb, id, &hovered, &held);
6127
6128 if (flags & ImGuiColorEditFlags_NoAlpha)
6129 flags &= ~(ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf);
6130
6131 ImVec4 col_rgb = col;
6132 if (flags & ImGuiColorEditFlags_InputHSV)
6133 ColorConvertHSVtoRGB(col_rgb.x, col_rgb.y, col_rgb.z, col_rgb.x, col_rgb.y, col_rgb.z);
6134
6135 ImVec4 col_rgb_without_alpha(col_rgb.x, col_rgb.y, col_rgb.z, 1.0f);
6136 float grid_step = ImMin(size.x, size.y) / 2.99f;
6137 float rounding = ImMin(g.Style.FrameRounding, grid_step * 0.5f);
6138 ImRect bb_inner = bb;
6139 float off = 0.0f;
6140 if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
6141 {
6142 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.
6143 bb_inner.Expand(off);
6144 }
6145 if ((flags & ImGuiColorEditFlags_AlphaPreviewHalf) && col_rgb.w < 1.0f)
6146 {
6147 float mid_x = IM_ROUND((bb_inner.Min.x + bb_inner.Max.x) * 0.5f);
6148 RenderColorRectWithAlphaCheckerboard(window->DrawList, ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), bb_inner.Max, GetColorU32(col_rgb), grid_step, ImVec2(-grid_step + off, off), rounding, ImDrawFlags_RoundCornersRight);
6149 window->DrawList->AddRectFilled(bb_inner.Min, ImVec2(mid_x, bb_inner.Max.y), GetColorU32(col_rgb_without_alpha), rounding, ImDrawFlags_RoundCornersLeft);
6150 }
6151 else
6152 {
6153 // Because GetColorU32() multiplies by the global style Alpha and we don't want to display a checkerboard if the source code had no alpha
6154 ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaPreview) ? col_rgb : col_rgb_without_alpha;
6155 if (col_source.w < 1.0f)
6156 RenderColorRectWithAlphaCheckerboard(window->DrawList, bb_inner.Min, bb_inner.Max, GetColorU32(col_source), grid_step, ImVec2(off, off), rounding);
6157 else
6158 window->DrawList->AddRectFilled(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), rounding);
6159 }
6160 RenderNavCursor(bb, id);
6161 if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
6162 {
6163 if (g.Style.FrameBorderSize > 0.0f)
6164 RenderFrameBorder(bb.Min, bb.Max, rounding);
6165 else
6166 window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), rounding); // Color button are often in need of some sort of border
6167 }
6168
6169 // Drag and Drop Source
6170 // NB: The ActiveId test is merely an optional micro-optimization, BeginDragDropSource() does the same test.
6171 if (g.ActiveId == id && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropSource())
6172 {
6173 if (flags & ImGuiColorEditFlags_NoAlpha)
6174 SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F, &col_rgb, sizeof(float) * 3, ImGuiCond_Once);
6175 else
6176 SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, &col_rgb, sizeof(float) * 4, ImGuiCond_Once);
6177 ColorButton(desc_id, col, flags);
6178 SameLine();
6179 TextEx("Color");
6180 EndDragDropSource();
6181 }
6182
6183 // Tooltip
6184 if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered && IsItemHovered(ImGuiHoveredFlags_ForTooltip))
6185 ColorTooltip(desc_id, &col.x, flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf));
6186
6187 return pressed;
6188}
6189
6190// Initialize/override default color options
6191void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags)
6192{
6193 ImGuiContext& g = *GImGui;
6194 if ((flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6195 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DisplayMask_;
6196 if ((flags & ImGuiColorEditFlags_DataTypeMask_) == 0)
6197 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DataTypeMask_;
6198 if ((flags & ImGuiColorEditFlags_PickerMask_) == 0)
6199 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_PickerMask_;
6200 if ((flags & ImGuiColorEditFlags_InputMask_) == 0)
6201 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_InputMask_;
6202 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check only 1 option is selected
6203 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DataTypeMask_)); // Check only 1 option is selected
6204 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check only 1 option is selected
6205 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check only 1 option is selected
6206 g.ColorEditOptions = flags;
6207}
6208
6209// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
6210void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags)
6211{
6212 ImGuiContext& g = *GImGui;
6213
6214 if (!BeginTooltipEx(ImGuiTooltipFlags_OverridePrevious, ImGuiWindowFlags_None))
6215 return;
6216 const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text;
6217 if (text_end > text)
6218 {
6219 TextEx(text, text_end);
6220 Separator();
6221 }
6222
6223 ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2);
6224 ImVec4 cf(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6225 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]);
6226 ColorButton("##preview", cf, (flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)) | ImGuiColorEditFlags_NoTooltip, sz);
6227 SameLine();
6228 if ((flags & ImGuiColorEditFlags_InputRGB) || !(flags & ImGuiColorEditFlags_InputMask_))
6229 {
6230 if (flags & ImGuiColorEditFlags_NoAlpha)
6231 Text("#%02X%02X%02X\nR: %d, G: %d, B: %d\n(%.3f, %.3f, %.3f)", cr, cg, cb, cr, cg, cb, col[0], col[1], col[2]);
6232 else
6233 Text("#%02X%02X%02X%02X\nR:%d, G:%d, B:%d, A:%d\n(%.3f, %.3f, %.3f, %.3f)", cr, cg, cb, ca, cr, cg, cb, ca, col[0], col[1], col[2], col[3]);
6234 }
6235 else if (flags & ImGuiColorEditFlags_InputHSV)
6236 {
6237 if (flags & ImGuiColorEditFlags_NoAlpha)
6238 Text("H: %.3f, S: %.3f, V: %.3f", col[0], col[1], col[2]);
6239 else
6240 Text("H: %.3f, S: %.3f, V: %.3f, A: %.3f", col[0], col[1], col[2], col[3]);
6241 }
6242 EndTooltip();
6243}
6244
6245void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags)
6246{
6247 bool allow_opt_inputs = !(flags & ImGuiColorEditFlags_DisplayMask_);
6248 bool allow_opt_datatype = !(flags & ImGuiColorEditFlags_DataTypeMask_);
6249 if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup("context"))
6250 return;
6251
6252 ImGuiContext& g = *GImGui;
6253 PushItemFlag(ImGuiItemFlags_NoMarkEdited, true);
6254 ImGuiColorEditFlags opts = g.ColorEditOptions;
6255 if (allow_opt_inputs)
6256 {
6257 if (RadioButton("RGB", (opts & ImGuiColorEditFlags_DisplayRGB) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayRGB;
6258 if (RadioButton("HSV", (opts & ImGuiColorEditFlags_DisplayHSV) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHSV;
6259 if (RadioButton("Hex", (opts & ImGuiColorEditFlags_DisplayHex) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHex;
6260 }
6261 if (allow_opt_datatype)
6262 {
6263 if (allow_opt_inputs) Separator();
6264 if (RadioButton("0..255", (opts & ImGuiColorEditFlags_Uint8) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Uint8;
6265 if (RadioButton("0.00..1.00", (opts & ImGuiColorEditFlags_Float) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Float;
6266 }
6267
6268 if (allow_opt_inputs || allow_opt_datatype)
6269 Separator();
6270 if (Button("Copy as..", ImVec2(-1, 0)))
6271 OpenPopup("Copy");
6272 if (BeginPopup("Copy"))
6273 {
6274 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]);
6275 char buf[64];
6276 ImFormatString(buf, IM_ARRAYSIZE(buf), "(%.3ff, %.3ff, %.3ff, %.3ff)", col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6277 if (Selectable(buf))
6278 SetClipboardText(buf);
6279 ImFormatString(buf, IM_ARRAYSIZE(buf), "(%d,%d,%d,%d)", cr, cg, cb, ca);
6280 if (Selectable(buf))
6281 SetClipboardText(buf);
6282 ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", cr, cg, cb);
6283 if (Selectable(buf))
6284 SetClipboardText(buf);
6285 if (!(flags & ImGuiColorEditFlags_NoAlpha))
6286 {
6287 ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X%02X", cr, cg, cb, ca);
6288 if (Selectable(buf))
6289 SetClipboardText(buf);
6290 }
6291 EndPopup();
6292 }
6293
6294 g.ColorEditOptions = opts;
6295 PopItemFlag();
6296 EndPopup();
6297}
6298
6299void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags)
6300{
6301 bool allow_opt_picker = !(flags & ImGuiColorEditFlags_PickerMask_);
6302 bool allow_opt_alpha_bar = !(flags & ImGuiColorEditFlags_NoAlpha) && !(flags & ImGuiColorEditFlags_AlphaBar);
6303 if ((!allow_opt_picker && !allow_opt_alpha_bar) || !BeginPopup("context"))
6304 return;
6305
6306 ImGuiContext& g = *GImGui;
6307 PushItemFlag(ImGuiItemFlags_NoMarkEdited, true);
6308 if (allow_opt_picker)
6309 {
6310 ImVec2 picker_size(g.FontSize * 8, ImMax(g.FontSize * 8 - (GetFrameHeight() + g.Style.ItemInnerSpacing.x), 1.0f)); // FIXME: Picker size copied from main picker function
6311 PushItemWidth(picker_size.x);
6312 for (int picker_type = 0; picker_type < 2; picker_type++)
6313 {
6314 // Draw small/thumbnail version of each picker type (over an invisible button for selection)
6315 if (picker_type > 0) Separator();
6316 PushID(picker_type);
6317 ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoSidePreview | (flags & ImGuiColorEditFlags_NoAlpha);
6318 if (picker_type == 0) picker_flags |= ImGuiColorEditFlags_PickerHueBar;
6319 if (picker_type == 1) picker_flags |= ImGuiColorEditFlags_PickerHueWheel;
6320 ImVec2 backup_pos = GetCursorScreenPos();
6321 if (Selectable("##selectable", false, 0, picker_size)) // By default, Selectable() is closing popup
6322 g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags_PickerMask_) | (picker_flags & ImGuiColorEditFlags_PickerMask_);
6323 SetCursorScreenPos(backup_pos);
6324 ImVec4 previewing_ref_col;
6325 memcpy(&previewing_ref_col, ref_col, sizeof(float) * ((picker_flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4));
6326 ColorPicker4("##previewing_picker", &previewing_ref_col.x, picker_flags);
6327 PopID();
6328 }
6329 PopItemWidth();
6330 }
6331 if (allow_opt_alpha_bar)
6332 {
6333 if (allow_opt_picker) Separator();
6334 CheckboxFlags("Alpha Bar", &g.ColorEditOptions, ImGuiColorEditFlags_AlphaBar);
6335 }
6336 PopItemFlag();
6337 EndPopup();
6338}
6339
6340//-------------------------------------------------------------------------
6341// [SECTION] Widgets: TreeNode, CollapsingHeader, etc.
6342//-------------------------------------------------------------------------
6343// - TreeNode()
6344// - TreeNodeV()
6345// - TreeNodeEx()
6346// - TreeNodeExV()
6347// - TreeNodeBehavior() [Internal]
6348// - TreePush()
6349// - TreePop()
6350// - GetTreeNodeToLabelSpacing()
6351// - SetNextItemOpen()
6352// - CollapsingHeader()
6353//-------------------------------------------------------------------------
6354
6355bool ImGui::TreeNode(const char* str_id, const char* fmt, ...)
6356{
6357 va_list args;
6358 va_start(args, fmt);
6359 bool is_open = TreeNodeExV(str_id, 0, fmt, args);
6360 va_end(args);
6361 return is_open;
6362}
6363
6364bool ImGui::TreeNode(const void* ptr_id, const char* fmt, ...)
6365{
6366 va_list args;
6367 va_start(args, fmt);
6368 bool is_open = TreeNodeExV(ptr_id, 0, fmt, args);
6369 va_end(args);
6370 return is_open;
6371}
6372
6373bool ImGui::TreeNode(const char* label)
6374{
6375 ImGuiWindow* window = GetCurrentWindow();
6376 if (window->SkipItems)
6377 return false;
6378 ImGuiID id = window->GetID(label);
6379 return TreeNodeBehavior(id, ImGuiTreeNodeFlags_None, label, NULL);
6380}
6381
6382bool ImGui::TreeNodeV(const char* str_id, const char* fmt, va_list args)
6383{
6384 return TreeNodeExV(str_id, 0, fmt, args);
6385}
6386
6387bool ImGui::TreeNodeV(const void* ptr_id, const char* fmt, va_list args)
6388{
6389 return TreeNodeExV(ptr_id, 0, fmt, args);
6390}
6391
6392bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags)
6393{
6394 ImGuiWindow* window = GetCurrentWindow();
6395 if (window->SkipItems)
6396 return false;
6397 ImGuiID id = window->GetID(label);
6398 return TreeNodeBehavior(id, flags, label, NULL);
6399}
6400
6401bool ImGui::TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
6402{
6403 va_list args;
6404 va_start(args, fmt);
6405 bool is_open = TreeNodeExV(str_id, flags, fmt, args);
6406 va_end(args);
6407 return is_open;
6408}
6409
6410bool ImGui::TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
6411{
6412 va_list args;
6413 va_start(args, fmt);
6414 bool is_open = TreeNodeExV(ptr_id, flags, fmt, args);
6415 va_end(args);
6416 return is_open;
6417}
6418
6419bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
6420{
6421 ImGuiWindow* window = GetCurrentWindow();
6422 if (window->SkipItems)
6423 return false;
6424
6425 ImGuiID id = window->GetID(str_id);
6426 const char* label, *label_end;
6427 ImFormatStringToTempBufferV(&label, &label_end, fmt, args);
6428 return TreeNodeBehavior(id, flags, label, label_end);
6429}
6430
6431bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
6432{
6433 ImGuiWindow* window = GetCurrentWindow();
6434 if (window->SkipItems)
6435 return false;
6436
6437 ImGuiID id = window->GetID(ptr_id);
6438 const char* label, *label_end;
6439 ImFormatStringToTempBufferV(&label, &label_end, fmt, args);
6440 return TreeNodeBehavior(id, flags, label, label_end);
6441}
6442
6443bool ImGui::TreeNodeGetOpen(ImGuiID storage_id)
6444{
6445 ImGuiContext& g = *GImGui;
6446 ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
6447 return storage->GetInt(storage_id, 0) != 0;
6448}
6449
6450void ImGui::TreeNodeSetOpen(ImGuiID storage_id, bool open)
6451{
6452 ImGuiContext& g = *GImGui;
6453 ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
6454 storage->SetInt(storage_id, open ? 1 : 0);
6455}
6456
6457bool ImGui::TreeNodeUpdateNextOpen(ImGuiID storage_id, ImGuiTreeNodeFlags flags)
6458{
6459 if (flags & ImGuiTreeNodeFlags_Leaf)
6460 return true;
6461
6462 // We only write to the tree storage if the user clicks, or explicitly use the SetNextItemOpen function
6463 ImGuiContext& g = *GImGui;
6464 ImGuiWindow* window = g.CurrentWindow;
6465 ImGuiStorage* storage = window->DC.StateStorage;
6466
6467 bool is_open;
6468 if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasOpen)
6469 {
6470 if (g.NextItemData.OpenCond & ImGuiCond_Always)
6471 {
6472 is_open = g.NextItemData.OpenVal;
6473 TreeNodeSetOpen(storage_id, is_open);
6474 }
6475 else
6476 {
6477 // We treat ImGuiCond_Once and ImGuiCond_FirstUseEver the same because tree node state are not saved persistently.
6478 const int stored_value = storage->GetInt(storage_id, -1);
6479 if (stored_value == -1)
6480 {
6481 is_open = g.NextItemData.OpenVal;
6482 TreeNodeSetOpen(storage_id, is_open);
6483 }
6484 else
6485 {
6486 is_open = stored_value != 0;
6487 }
6488 }
6489 }
6490 else
6491 {
6492 is_open = storage->GetInt(storage_id, (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0;
6493 }
6494
6495 // When logging is enabled, we automatically expand tree nodes (but *NOT* collapsing headers.. seems like sensible behavior).
6496 // NB- If we are above max depth we still allow manually opened nodes to be logged.
6497 if (g.LogEnabled && !(flags & ImGuiTreeNodeFlags_NoAutoOpenOnLog) && (window->DC.TreeDepth - g.LogDepthRef) < g.LogDepthToExpand)
6498 is_open = true;
6499
6500 return is_open;
6501}
6502
6503// Store ImGuiTreeNodeStackData for just submitted node.
6504// Currently only supports 32 level deep and we are fine with (1 << Depth) overflowing into a zero, easy to increase.
6505static void TreeNodeStoreStackData(ImGuiTreeNodeFlags flags)
6506{
6507 ImGuiContext& g = *GImGui;
6508 ImGuiWindow* window = g.CurrentWindow;
6509
6510 g.TreeNodeStack.resize(g.TreeNodeStack.Size + 1);
6511 ImGuiTreeNodeStackData* tree_node_data = &g.TreeNodeStack.back();
6512 tree_node_data->ID = g.LastItemData.ID;
6513 tree_node_data->TreeFlags = flags;
6514 tree_node_data->ItemFlags = g.LastItemData.ItemFlags;
6515 tree_node_data->NavRect = g.LastItemData.NavRect;
6516 window->DC.TreeHasStackDataDepthMask |= (1 << window->DC.TreeDepth);
6517}
6518
6519// 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.
6520bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end)
6521{
6522 ImGuiWindow* window = GetCurrentWindow();
6523 if (window->SkipItems)
6524 return false;
6525
6526 ImGuiContext& g = *GImGui;
6527 const ImGuiStyle& style = g.Style;
6528 const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0;
6529 const ImVec2 padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, ImMin(window->DC.CurrLineTextBaseOffset, style.FramePadding.y));
6530
6531 if (!label_end)
6532 label_end = FindRenderedTextEnd(label);
6533 const ImVec2 label_size = CalcTextSize(label, label_end, false);
6534
6535 const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapsing arrow width + Spacing
6536 const float text_offset_y = ImMax(padding.y, window->DC.CurrLineTextBaseOffset); // Latch before ItemSize changes it
6537 const float text_width = g.FontSize + label_size.x + padding.x * 2; // Include collapsing arrow
6538
6539 // We vertically grow up to current line height up the typical widget height.
6540 const float frame_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y * 2), label_size.y + padding.y * 2);
6541 const bool span_all_columns = (flags & ImGuiTreeNodeFlags_SpanAllColumns) != 0 && (g.CurrentTable != NULL);
6542 const bool span_all_columns_label = (flags & ImGuiTreeNodeFlags_LabelSpanAllColumns) != 0 && (g.CurrentTable != NULL);
6543 ImRect frame_bb;
6544 frame_bb.Min.x = span_all_columns ? window->ParentWorkRect.Min.x : (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x;
6545 frame_bb.Min.y = window->DC.CursorPos.y;
6546 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;
6547 frame_bb.Max.y = window->DC.CursorPos.y + frame_height;
6548 if (display_frame)
6549 {
6550 const float outer_extend = IM_TRUNC(window->WindowPadding.x * 0.5f); // Framed header expand a little outside of current limits
6551 frame_bb.Min.x -= outer_extend;
6552 frame_bb.Max.x += outer_extend;
6553 }
6554
6555 ImVec2 text_pos(window->DC.CursorPos.x + text_offset_x, window->DC.CursorPos.y + text_offset_y);
6556 ItemSize(ImVec2(text_width, frame_height), padding.y);
6557
6558 // For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing
6559 ImRect interact_bb = frame_bb;
6560 if ((flags & (ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_SpanLabelWidth | ImGuiTreeNodeFlags_SpanAllColumns)) == 0)
6561 interact_bb.Max.x = frame_bb.Min.x + text_width + (label_size.x > 0.0f ? style.ItemSpacing.x * 2.0f : 0.0f);
6562
6563 // Compute open and multi-select states before ItemAdd() as it clear NextItem data.
6564 ImGuiID storage_id = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasStorageID) ? g.NextItemData.StorageId : id;
6565 bool is_open = TreeNodeUpdateNextOpen(storage_id, flags);
6566
6567 bool is_visible;
6568 if (span_all_columns || span_all_columns_label)
6569 {
6570 // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable..
6571 const float backup_clip_rect_min_x = window->ClipRect.Min.x;
6572 const float backup_clip_rect_max_x = window->ClipRect.Max.x;
6573 window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
6574 window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
6575 is_visible = ItemAdd(interact_bb, id);
6576 window->ClipRect.Min.x = backup_clip_rect_min_x;
6577 window->ClipRect.Max.x = backup_clip_rect_max_x;
6578 }
6579 else
6580 {
6581 is_visible = ItemAdd(interact_bb, id);
6582 }
6583 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect;
6584 g.LastItemData.DisplayRect = frame_bb;
6585
6586 // If a NavLeft request is happening and ImGuiTreeNodeFlags_NavLeftJumpsBackHere enabled:
6587 // Store data for the current depth to allow returning to this node from any child item.
6588 // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop().
6589 // It will become tempting to enable ImGuiTreeNodeFlags_NavLeftJumpsBackHere by default or move it to ImGuiStyle.
6590 bool store_tree_node_stack_data = false;
6591 if (!(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6592 {
6593 if ((flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && is_open && !g.NavIdIsAlive)
6594 if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
6595 store_tree_node_stack_data = true;
6596 }
6597
6598 const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0;
6599 if (!is_visible)
6600 {
6601 if (store_tree_node_stack_data && is_open)
6602 TreeNodeStoreStackData(flags); // Call before TreePushOverrideID()
6603 if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6604 TreePushOverrideID(id);
6605 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
6606 return is_open;
6607 }
6608
6609 if (span_all_columns || span_all_columns_label)
6610 {
6611 TablePushBackgroundChannel();
6612 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect;
6613 g.LastItemData.ClipRect = window->ClipRect;
6614 }
6615
6616 ImGuiButtonFlags button_flags = ImGuiTreeNodeFlags_None;
6617 if ((flags & ImGuiTreeNodeFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap))
6618 button_flags |= ImGuiButtonFlags_AllowOverlap;
6619 if (!is_leaf)
6620 button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
6621
6622 // We allow clicking on the arrow section with keyboard modifiers held, in order to easily
6623 // allow browsing a tree while preserving selection with code implementing multi-selection patterns.
6624 // When clicking on the rest of the tree node we always disallow keyboard modifiers.
6625 const float arrow_hit_x1 = (text_pos.x - text_offset_x) - style.TouchExtraPadding.x;
6626 const float arrow_hit_x2 = (text_pos.x - text_offset_x) + (g.FontSize + padding.x * 2.0f) + style.TouchExtraPadding.x;
6627 const bool is_mouse_x_over_arrow = (g.IO.MousePos.x >= arrow_hit_x1 && g.IO.MousePos.x < arrow_hit_x2);
6628
6629 const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
6630 if (is_multi_select) // We absolutely need to distinguish open vs select so _OpenOnArrow comes by default
6631 flags |= (flags & ImGuiTreeNodeFlags_OpenOnMask_) == 0 ? ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick : ImGuiTreeNodeFlags_OpenOnArrow;
6632
6633 // Open behaviors can be altered with the _OpenOnArrow and _OnOnDoubleClick flags.
6634 // Some alteration have subtle effects (e.g. toggle on MouseUp vs MouseDown events) due to requirements for multi-selection and drag and drop support.
6635 // - Single-click on label = Toggle on MouseUp (default, when _OpenOnArrow=0)
6636 // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=0)
6637 // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=1)
6638 // - Double-click on label = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1)
6639 // - Double-click on arrow = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1 and _OpenOnArrow=0)
6640 // It is rather standard that arrow click react on Down rather than Up.
6641 // 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.
6642 if (is_mouse_x_over_arrow)
6643 button_flags |= ImGuiButtonFlags_PressedOnClick;
6644 else if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick)
6645 button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick;
6646 else
6647 button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
6648
6649 bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0;
6650 const bool was_selected = selected;
6651
6652 // Multi-selection support (header)
6653 if (is_multi_select)
6654 {
6655 // Handle multi-select + alter button flags for it
6656 MultiSelectItemHeader(id, &selected, &button_flags);
6657 if (is_mouse_x_over_arrow)
6658 button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease;
6659 }
6660 else
6661 {
6662 if (window != g.HoveredWindow || !is_mouse_x_over_arrow)
6663 button_flags |= ImGuiButtonFlags_NoKeyModsAllowed;
6664 }
6665
6666 bool hovered, held;
6667 bool pressed = ButtonBehavior(interact_bb, id, &hovered, &held, button_flags);
6668 bool toggled = false;
6669 if (!is_leaf)
6670 {
6671 if (pressed && g.DragDropHoldJustPressedId != id)
6672 {
6673 if ((flags & ImGuiTreeNodeFlags_OpenOnMask_) == 0 || (g.NavActivateId == id && !is_multi_select))
6674 toggled = true; // Single click
6675 if (flags & ImGuiTreeNodeFlags_OpenOnArrow)
6676 toggled |= is_mouse_x_over_arrow && !g.NavHighlightItemUnderNav; // Lightweight equivalent of IsMouseHoveringRect() since ButtonBehavior() already did the job
6677 if ((flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) && g.IO.MouseClickedCount[0] == 2)
6678 toggled = true; // Double click
6679 }
6680 else if (pressed && g.DragDropHoldJustPressedId == id)
6681 {
6682 IM_ASSERT(button_flags & ImGuiButtonFlags_PressedOnDragDropHold);
6683 if (!is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again.
6684 toggled = true;
6685 else
6686 pressed = false; // Cancel press so it doesn't trigger selection.
6687 }
6688
6689 if (g.NavId == id && g.NavMoveDir == ImGuiDir_Left && is_open)
6690 {
6691 toggled = true;
6692 NavClearPreferredPosForAxis(ImGuiAxis_X);
6693 NavMoveRequestCancel();
6694 }
6695 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?
6696 {
6697 toggled = true;
6698 NavClearPreferredPosForAxis(ImGuiAxis_X);
6699 NavMoveRequestCancel();
6700 }
6701
6702 if (toggled)
6703 {
6704 is_open = !is_open;
6705 window->DC.StateStorage->SetInt(storage_id, is_open);
6706 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledOpen;
6707 }
6708 }
6709
6710 // Multi-selection support (footer)
6711 if (is_multi_select)
6712 {
6713 bool pressed_copy = pressed && !toggled;
6714 MultiSelectItemFooter(id, &selected, &pressed_copy);
6715 if (pressed)
6716 SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, interact_bb);
6717 }
6718
6719 if (selected != was_selected)
6720 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
6721
6722 // Render
6723 {
6724 const ImU32 text_col = GetColorU32(ImGuiCol_Text);
6725 ImGuiNavRenderCursorFlags nav_render_cursor_flags = ImGuiNavRenderCursorFlags_Compact;
6726 if (is_multi_select)
6727 nav_render_cursor_flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle
6728 if (display_frame)
6729 {
6730 // Framed type
6731 const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
6732 RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, true, style.FrameRounding);
6733 RenderNavCursor(frame_bb, id, nav_render_cursor_flags);
6734 if (flags & ImGuiTreeNodeFlags_Bullet)
6735 RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.60f, text_pos.y + g.FontSize * 0.5f), text_col);
6736 else if (!is_leaf)
6737 RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y), text_col, is_open ? ((flags & ImGuiTreeNodeFlags_UpsideDownArrow) ? ImGuiDir_Up : ImGuiDir_Down) : ImGuiDir_Right, 1.0f);
6738 else // Leaf without bullet, left-adjusted text
6739 text_pos.x -= text_offset_x - padding.x;
6740 if (flags & ImGuiTreeNodeFlags_ClipLabelForTrailingButton)
6741 frame_bb.Max.x -= g.FontSize + style.FramePadding.x;
6742 if (g.LogEnabled)
6743 LogSetNextTextDecoration("###", "###");
6744 }
6745 else
6746 {
6747 // Unframed typed for tree nodes
6748 if (hovered || selected)
6749 {
6750 const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
6751 RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, false);
6752 }
6753 RenderNavCursor(frame_bb, id, nav_render_cursor_flags);
6754 if (flags & ImGuiTreeNodeFlags_Bullet)
6755 RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.5f, text_pos.y + g.FontSize * 0.5f), text_col);
6756 else if (!is_leaf)
6757 RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y + g.FontSize * 0.15f), text_col, is_open ? ((flags & ImGuiTreeNodeFlags_UpsideDownArrow) ? ImGuiDir_Up : ImGuiDir_Down) : ImGuiDir_Right, 0.70f);
6758 if (g.LogEnabled)
6759 LogSetNextTextDecoration(">", NULL);
6760 }
6761
6762 if (span_all_columns && !span_all_columns_label)
6763 TablePopBackgroundChannel();
6764
6765 // Label
6766 if (display_frame)
6767 RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size);
6768 else
6769 RenderText(text_pos, label, label_end, false);
6770
6771 if (span_all_columns_label)
6772 TablePopBackgroundChannel();
6773 }
6774
6775 if (store_tree_node_stack_data && is_open)
6776 TreeNodeStoreStackData(flags); // Call before TreePushOverrideID()
6777 if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6778 TreePushOverrideID(id); // Could use TreePush(label) but this avoid computing twice
6779
6780 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
6781 return is_open;
6782}
6783
6784void ImGui::TreePush(const char* str_id)
6785{
6786 ImGuiWindow* window = GetCurrentWindow();
6787 Indent();
6788 window->DC.TreeDepth++;
6789 PushID(str_id);
6790}
6791
6792void ImGui::TreePush(const void* ptr_id)
6793{
6794 ImGuiWindow* window = GetCurrentWindow();
6795 Indent();
6796 window->DC.TreeDepth++;
6797 PushID(ptr_id);
6798}
6799
6800void ImGui::TreePushOverrideID(ImGuiID id)
6801{
6802 ImGuiContext& g = *GImGui;
6803 ImGuiWindow* window = g.CurrentWindow;
6804 Indent();
6805 window->DC.TreeDepth++;
6806 PushOverrideID(id);
6807}
6808
6809void ImGui::TreePop()
6810{
6811 ImGuiContext& g = *GImGui;
6812 ImGuiWindow* window = g.CurrentWindow;
6813 Unindent();
6814
6815 window->DC.TreeDepth--;
6816 ImU32 tree_depth_mask = (1 << window->DC.TreeDepth);
6817
6818 if (window->DC.TreeHasStackDataDepthMask & tree_depth_mask) // Only set during request
6819 {
6820 ImGuiTreeNodeStackData* data = &g.TreeNodeStack.back();
6821 IM_ASSERT(data->ID == window->IDStack.back());
6822 if (data->TreeFlags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere)
6823 {
6824 // Handle Left arrow to move to parent tree node (when ImGuiTreeNodeFlags_NavLeftJumpsBackHere is enabled)
6825 if (g.NavIdIsAlive && g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
6826 NavMoveRequestResolveWithPastTreeNode(&g.NavMoveResultLocal, data);
6827 }
6828 g.TreeNodeStack.pop_back();
6829 window->DC.TreeHasStackDataDepthMask &= ~tree_depth_mask;
6830 }
6831
6832 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.
6833 PopID();
6834}
6835
6836// Horizontal distance preceding label when using TreeNode() or Bullet()
6837float ImGui::GetTreeNodeToLabelSpacing()
6838{
6839 ImGuiContext& g = *GImGui;
6840 return g.FontSize + (g.Style.FramePadding.x * 2.0f);
6841}
6842
6843// Set next TreeNode/CollapsingHeader open state.
6844void ImGui::SetNextItemOpen(bool is_open, ImGuiCond cond)
6845{
6846 ImGuiContext& g = *GImGui;
6847 if (g.CurrentWindow->SkipItems)
6848 return;
6849 g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasOpen;
6850 g.NextItemData.OpenVal = is_open;
6851 g.NextItemData.OpenCond = (ImU8)(cond ? cond : ImGuiCond_Always);
6852}
6853
6854// Set next TreeNode/CollapsingHeader storage id.
6855void ImGui::SetNextItemStorageID(ImGuiID storage_id)
6856{
6857 ImGuiContext& g = *GImGui;
6858 if (g.CurrentWindow->SkipItems)
6859 return;
6860 g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasStorageID;
6861 g.NextItemData.StorageId = storage_id;
6862}
6863
6864// CollapsingHeader returns true when opened but do not indent nor push into the ID stack (because of the ImGuiTreeNodeFlags_NoTreePushOnOpen flag).
6865// 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().
6866bool ImGui::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags)
6867{
6868 ImGuiWindow* window = GetCurrentWindow();
6869 if (window->SkipItems)
6870 return false;
6871 ImGuiID id = window->GetID(label);
6872 return TreeNodeBehavior(id, flags | ImGuiTreeNodeFlags_CollapsingHeader, label);
6873}
6874
6875// p_visible == NULL : regular collapsing header
6876// 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
6877// p_visible != NULL && *p_visible == false : do not show the header at all
6878// Do not mistake this with the Open state of the header itself, which you can adjust with SetNextItemOpen() or ImGuiTreeNodeFlags_DefaultOpen.
6879bool ImGui::CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags)
6880{
6881 ImGuiWindow* window = GetCurrentWindow();
6882 if (window->SkipItems)
6883 return false;
6884
6885 if (p_visible && !*p_visible)
6886 return false;
6887
6888 ImGuiID id = window->GetID(label);
6889 flags |= ImGuiTreeNodeFlags_CollapsingHeader;
6890 if (p_visible)
6891 flags |= ImGuiTreeNodeFlags_AllowOverlap | (ImGuiTreeNodeFlags)ImGuiTreeNodeFlags_ClipLabelForTrailingButton;
6892 bool is_open = TreeNodeBehavior(id, flags, label);
6893 if (p_visible != NULL)
6894 {
6895 // Create a small overlapping close button
6896 // FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc.
6897 // FIXME: CloseButton can overlap into text, need find a way to clip the text somehow.
6898 ImGuiContext& g = *GImGui;
6899 ImGuiLastItemData last_item_backup = g.LastItemData;
6900 float button_size = g.FontSize;
6901 float button_x = ImMax(g.LastItemData.Rect.Min.x, g.LastItemData.Rect.Max.x - g.Style.FramePadding.x - button_size);
6902 float button_y = g.LastItemData.Rect.Min.y + g.Style.FramePadding.y;
6903 ImGuiID close_button_id = GetIDWithSeed("#CLOSE", NULL, id);
6904 if (CloseButton(close_button_id, ImVec2(button_x, button_y)))
6905 *p_visible = false;
6906 g.LastItemData = last_item_backup;
6907 }
6908
6909 return is_open;
6910}
6911
6912//-------------------------------------------------------------------------
6913// [SECTION] Widgets: Selectable
6914//-------------------------------------------------------------------------
6915// - Selectable()
6916//-------------------------------------------------------------------------
6917
6918// Tip: pass a non-visible label (e.g. "##hello") then you can use the space to draw other text or image.
6919// But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID or use ##unique_id.
6920// With this scheme, ImGuiSelectableFlags_SpanAllColumns and ImGuiSelectableFlags_AllowOverlap are also frequently used flags.
6921// FIXME: Selectable() with (size.x == 0.0f) and (SelectableTextAlign.x > 0.0f) followed by SameLine() is currently not supported.
6922bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
6923{
6924 ImGuiWindow* window = GetCurrentWindow();
6925 if (window->SkipItems)
6926 return false;
6927
6928 ImGuiContext& g = *GImGui;
6929 const ImGuiStyle& style = g.Style;
6930
6931 // Submit label or explicit size to ItemSize(), whereas ItemAdd() will submit a larger/spanning rectangle.
6932 ImGuiID id = window->GetID(label);
6933 ImVec2 label_size = CalcTextSize(label, NULL, true);
6934 ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y);
6935 ImVec2 pos = window->DC.CursorPos;
6936 pos.y += window->DC.CurrLineTextBaseOffset;
6937 ItemSize(size, 0.0f);
6938
6939 // Fill horizontal space
6940 // We don't support (size < 0.0f) in Selectable() because the ItemSpacing extension would make explicitly right-aligned sizes not visibly match other widgets.
6941 const bool span_all_columns = (flags & ImGuiSelectableFlags_SpanAllColumns) != 0;
6942 const float min_x = span_all_columns ? window->ParentWorkRect.Min.x : pos.x;
6943 const float max_x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x;
6944 if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_SpanAvailWidth))
6945 size.x = ImMax(label_size.x, max_x - min_x);
6946
6947 // Text stays at the submission position, but bounding box may be extended on both sides
6948 const ImVec2 text_min = pos;
6949 const ImVec2 text_max(min_x + size.x, pos.y + size.y);
6950
6951 // Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable.
6952 // FIXME: Not part of layout so not included in clipper calculation, but ItemSize currently doesn't allow offsetting CursorPos.
6953 ImRect bb(min_x, pos.y, text_max.x, text_max.y);
6954 if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0)
6955 {
6956 const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x;
6957 const float spacing_y = style.ItemSpacing.y;
6958 const float spacing_L = IM_TRUNC(spacing_x * 0.50f);
6959 const float spacing_U = IM_TRUNC(spacing_y * 0.50f);
6960 bb.Min.x -= spacing_L;
6961 bb.Min.y -= spacing_U;
6962 bb.Max.x += (spacing_x - spacing_L);
6963 bb.Max.y += (spacing_y - spacing_U);
6964 }
6965 //if (g.IO.KeyCtrl) { GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(0, 255, 0, 255)); }
6966
6967 const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0;
6968 const ImGuiItemFlags extra_item_flags = disabled_item ? (ImGuiItemFlags)ImGuiItemFlags_Disabled : ImGuiItemFlags_None;
6969 bool is_visible;
6970 if (span_all_columns)
6971 {
6972 // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable..
6973 const float backup_clip_rect_min_x = window->ClipRect.Min.x;
6974 const float backup_clip_rect_max_x = window->ClipRect.Max.x;
6975 window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
6976 window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
6977 is_visible = ItemAdd(bb, id, NULL, extra_item_flags);
6978 window->ClipRect.Min.x = backup_clip_rect_min_x;
6979 window->ClipRect.Max.x = backup_clip_rect_max_x;
6980 }
6981 else
6982 {
6983 is_visible = ItemAdd(bb, id, NULL, extra_item_flags);
6984 }
6985
6986 const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
6987 if (!is_visible)
6988 if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(bb)) // Extra layer of "no logic clip" for box-select support (would be more overhead to add to ItemAdd)
6989 return false;
6990
6991 const bool disabled_global = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0;
6992 if (disabled_item && !disabled_global) // Only testing this as an optimization
6993 BeginDisabled();
6994
6995 // FIXME: We can standardize the behavior of those two, we could also keep the fast path of override ClipRect + full push on render only,
6996 // which would be advantageous since most selectable are not selected.
6997 if (span_all_columns)
6998 {
6999 if (g.CurrentTable)
7000 TablePushBackgroundChannel();
7001 else if (window->DC.CurrentColumns)
7002 PushColumnsBackground();
7003 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect;
7004 g.LastItemData.ClipRect = window->ClipRect;
7005 }
7006
7007 // We use NoHoldingActiveID on menus so user can click and _hold_ on a menu then drag to browse child entries
7008 ImGuiButtonFlags button_flags = 0;
7009 if (flags & ImGuiSelectableFlags_NoHoldingActiveID) { button_flags |= ImGuiButtonFlags_NoHoldingActiveId; }
7010 if (flags & ImGuiSelectableFlags_NoSetKeyOwner) { button_flags |= ImGuiButtonFlags_NoSetKeyOwner; }
7011 if (flags & ImGuiSelectableFlags_SelectOnClick) { button_flags |= ImGuiButtonFlags_PressedOnClick; }
7012 if (flags & ImGuiSelectableFlags_SelectOnRelease) { button_flags |= ImGuiButtonFlags_PressedOnRelease; }
7013 if (flags & ImGuiSelectableFlags_AllowDoubleClick) { button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; }
7014 if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap)) { button_flags |= ImGuiButtonFlags_AllowOverlap; }
7015
7016 // Multi-selection support (header)
7017 const bool was_selected = selected;
7018 if (is_multi_select)
7019 {
7020 // Handle multi-select + alter button flags for it
7021 MultiSelectItemHeader(id, &selected, &button_flags);
7022 }
7023
7024 bool hovered, held;
7025 bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags);
7026
7027 // Multi-selection support (footer)
7028 if (is_multi_select)
7029 {
7030 MultiSelectItemFooter(id, &selected, &pressed);
7031 }
7032 else
7033 {
7034 // Auto-select when moved into
7035 // - This will be more fully fleshed in the range-select branch
7036 // - This is not exposed as it won't nicely work with some user side handling of shift/control
7037 // - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons
7038 // - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope())
7039 // - (2) usage will fail with clipped items
7040 // The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API.
7041 if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId)
7042 if (g.NavJustMovedToId == id)
7043 selected = pressed = true;
7044 }
7045
7046 // Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with keyboard/gamepad
7047 if (pressed || (hovered && (flags & ImGuiSelectableFlags_SetNavIdOnHover)))
7048 {
7049 if (!g.NavHighlightItemUnderNav && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent)
7050 {
7051 SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, WindowRectAbsToRel(window, bb)); // (bb == NavRect)
7052 if (g.IO.ConfigNavCursorVisibleAuto)
7053 g.NavCursorVisible = false;
7054 }
7055 }
7056 if (pressed)
7057 MarkItemEdited(id);
7058
7059 if (selected != was_selected)
7060 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
7061
7062 // Render
7063 if (is_visible)
7064 {
7065 const bool highlighted = hovered || (flags & ImGuiSelectableFlags_Highlight);
7066 if (highlighted || selected)
7067 {
7068 // Between 1.91.0 and 1.91.4 we made selected Selectable use an arbitrary lerp between _Header and _HeaderHovered. Removed that now. (#8106)
7069 ImU32 col = GetColorU32((held && highlighted) ? ImGuiCol_HeaderActive : highlighted ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
7070 RenderFrame(bb.Min, bb.Max, col, false, 0.0f);
7071 }
7072 if (g.NavId == id)
7073 {
7074 ImGuiNavRenderCursorFlags nav_render_cursor_flags = ImGuiNavRenderCursorFlags_Compact | ImGuiNavRenderCursorFlags_NoRounding;
7075 if (is_multi_select)
7076 nav_render_cursor_flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle
7077 RenderNavCursor(bb, id, nav_render_cursor_flags);
7078 }
7079 }
7080
7081 if (span_all_columns)
7082 {
7083 if (g.CurrentTable)
7084 TablePopBackgroundChannel();
7085 else if (window->DC.CurrentColumns)
7086 PopColumnsBackground();
7087 }
7088
7089 if (is_visible)
7090 RenderTextClipped(text_min, text_max, label, NULL, &label_size, style.SelectableTextAlign, &bb);
7091
7092 // Automatically close popups
7093 if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_NoAutoClosePopups) && (g.LastItemData.ItemFlags & ImGuiItemFlags_AutoClosePopups))
7094 CloseCurrentPopup();
7095
7096 if (disabled_item && !disabled_global)
7097 EndDisabled();
7098
7099 // Selectable() always returns a pressed state!
7100 // Users of BeginMultiSelect()/EndMultiSelect() scope: you may call ImGui::IsItemToggledSelection() to retrieve
7101 // selection toggle, only useful if you need that state updated (e.g. for rendering purpose) before reaching EndMultiSelect().
7102 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
7103 return pressed; //-V1020
7104}
7105
7106bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
7107{
7108 if (Selectable(label, *p_selected, flags, size_arg))
7109 {
7110 *p_selected = !*p_selected;
7111 return true;
7112 }
7113 return false;
7114}
7115
7116
7117//-------------------------------------------------------------------------
7118// [SECTION] Widgets: Typing-Select support
7119//-------------------------------------------------------------------------
7120
7121// [Experimental] Currently not exposed in public API.
7122// Consume character inputs and return search request, if any.
7123// This would typically only be called on the focused window or location you want to grab inputs for, e.g.
7124// if (ImGui::IsWindowFocused(...))
7125// if (ImGuiTypingSelectRequest* req = ImGui::GetTypingSelectRequest())
7126// focus_idx = ImGui::TypingSelectFindMatch(req, my_items.size(), [](void*, int n) { return my_items[n]->Name; }, &my_items, -1);
7127// However the code is written in a way where calling it from multiple locations is safe (e.g. to obtain buffer).
7128ImGuiTypingSelectRequest* ImGui::GetTypingSelectRequest(ImGuiTypingSelectFlags flags)
7129{
7130 ImGuiContext& g = *GImGui;
7131 ImGuiTypingSelectState* data = &g.TypingSelectState;
7132 ImGuiTypingSelectRequest* out_request = &data->Request;
7133
7134 // Clear buffer
7135 const float TYPING_SELECT_RESET_TIMER = 1.80f; // FIXME: Potentially move to IO config.
7136 const int TYPING_SELECT_SINGLE_CHAR_COUNT_FOR_LOCK = 4; // Lock single char matching when repeating same char 4 times
7137 if (data->SearchBuffer[0] != 0)
7138 {
7139 bool clear_buffer = false;
7140 clear_buffer |= (g.NavFocusScopeId != data->FocusScope);
7141 clear_buffer |= (data->LastRequestTime + TYPING_SELECT_RESET_TIMER < g.Time);
7142 clear_buffer |= g.NavAnyRequest;
7143 clear_buffer |= g.ActiveId != 0 && g.NavActivateId == 0; // Allow temporary SPACE activation to not interfere
7144 clear_buffer |= IsKeyPressed(ImGuiKey_Escape) || IsKeyPressed(ImGuiKey_Enter);
7145 clear_buffer |= IsKeyPressed(ImGuiKey_Backspace) && (flags & ImGuiTypingSelectFlags_AllowBackspace) == 0;
7146 //if (clear_buffer) { IMGUI_DEBUG_LOG("GetTypingSelectRequest(): Clear SearchBuffer.\n"); }
7147 if (clear_buffer)
7148 data->Clear();
7149 }
7150
7151 // Append to buffer
7152 const int buffer_max_len = IM_ARRAYSIZE(data->SearchBuffer) - 1;
7153 int buffer_len = (int)strlen(data->SearchBuffer);
7154 bool select_request = false;
7155 for (ImWchar w : g.IO.InputQueueCharacters)
7156 {
7157 const int w_len = ImTextCountUtf8BytesFromStr(&w, &w + 1);
7158 if (w < 32 || (buffer_len == 0 && ImCharIsBlankW(w)) || (buffer_len + w_len > buffer_max_len)) // Ignore leading blanks
7159 continue;
7160 char w_buf[5];
7161 ImTextCharToUtf8(w_buf, (unsigned int)w);
7162 if (data->SingleCharModeLock && w_len == out_request->SingleCharSize && memcmp(w_buf, data->SearchBuffer, w_len) == 0)
7163 {
7164 select_request = true; // Same character: don't need to append to buffer.
7165 continue;
7166 }
7167 if (data->SingleCharModeLock)
7168 {
7169 data->Clear(); // Different character: clear
7170 buffer_len = 0;
7171 }
7172 memcpy(data->SearchBuffer + buffer_len, w_buf, w_len + 1); // Append
7173 buffer_len += w_len;
7174 select_request = true;
7175 }
7176 g.IO.InputQueueCharacters.resize(0);
7177
7178 // Handle backspace
7179 if ((flags & ImGuiTypingSelectFlags_AllowBackspace) && IsKeyPressed(ImGuiKey_Backspace, ImGuiInputFlags_Repeat))
7180 {
7181 char* p = (char*)(void*)ImTextFindPreviousUtf8Codepoint(data->SearchBuffer, data->SearchBuffer + buffer_len);
7182 *p = 0;
7183 buffer_len = (int)(p - data->SearchBuffer);
7184 }
7185
7186 // Return request if any
7187 if (buffer_len == 0)
7188 return NULL;
7189 if (select_request)
7190 {
7191 data->FocusScope = g.NavFocusScopeId;
7192 data->LastRequestFrame = g.FrameCount;
7193 data->LastRequestTime = (float)g.Time;
7194 }
7195 out_request->Flags = flags;
7196 out_request->SearchBufferLen = buffer_len;
7197 out_request->SearchBuffer = data->SearchBuffer;
7198 out_request->SelectRequest = (data->LastRequestFrame == g.FrameCount);
7199 out_request->SingleCharMode = false;
7200 out_request->SingleCharSize = 0;
7201
7202 // Calculate if buffer contains the same character repeated.
7203 // - This can be used to implement a special search mode on first character.
7204 // - Performed on UTF-8 codepoint for correctness.
7205 // - SingleCharMode is always set for first input character, because it usually leads to a "next".
7206 if (flags & ImGuiTypingSelectFlags_AllowSingleCharMode)
7207 {
7208 const char* buf_begin = out_request->SearchBuffer;
7209 const char* buf_end = out_request->SearchBuffer + out_request->SearchBufferLen;
7210 const int c0_len = ImTextCountUtf8BytesFromChar(buf_begin, buf_end);
7211 const char* p = buf_begin + c0_len;
7212 for (; p < buf_end; p += c0_len)
7213 if (memcmp(buf_begin, p, (size_t)c0_len) != 0)
7214 break;
7215 const int single_char_count = (p == buf_end) ? (out_request->SearchBufferLen / c0_len) : 0;
7216 out_request->SingleCharMode = (single_char_count > 0 || data->SingleCharModeLock);
7217 out_request->SingleCharSize = (ImS8)c0_len;
7218 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.
7219 }
7220
7221 return out_request;
7222}
7223
7224static int ImStrimatchlen(const char* s1, const char* s1_end, const char* s2)
7225{
7226 int match_len = 0;
7227 while (s1 < s1_end && ImToUpper(*s1++) == ImToUpper(*s2++))
7228 match_len++;
7229 return match_len;
7230}
7231
7232// Default handler for finding a result for typing-select. You may implement your own.
7233// You might want to display a tooltip to visualize the current request SearchBuffer
7234// When SingleCharMode is set:
7235// - it is better to NOT display a tooltip of other on-screen display indicator.
7236// - the index of the currently focused item is required.
7237// if your SetNextItemSelectionUserData() values are indices, you can obtain it from ImGuiMultiSelectIO::NavIdItem, otherwise from g.NavLastValidSelectionUserData.
7238int ImGui::TypingSelectFindMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx)
7239{
7240 if (req == NULL || req->SelectRequest == false) // Support NULL parameter so both calls can be done from same spot.
7241 return -1;
7242 int idx = -1;
7243 if (req->SingleCharMode && (req->Flags & ImGuiTypingSelectFlags_AllowSingleCharMode))
7244 idx = TypingSelectFindNextSingleCharMatch(req, items_count, get_item_name_func, user_data, nav_item_idx);
7245 else
7246 idx = TypingSelectFindBestLeadingMatch(req, items_count, get_item_name_func, user_data);
7247 if (idx != -1)
7248 SetNavCursorVisibleAfterMove();
7249 return idx;
7250}
7251
7252// Special handling when a single character is repeated: perform search on a single letter and goes to next.
7253int ImGui::TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx)
7254{
7255 // FIXME: Assume selection user data is index. Would be extremely practical.
7256 //if (nav_item_idx == -1)
7257 // nav_item_idx = (int)g.NavLastValidSelectionUserData;
7258
7259 int first_match_idx = -1;
7260 bool return_next_match = false;
7261 for (int idx = 0; idx < items_count; idx++)
7262 {
7263 const char* item_name = get_item_name_func(user_data, idx);
7264 if (ImStrimatchlen(req->SearchBuffer, req->SearchBuffer + req->SingleCharSize, item_name) < req->SingleCharSize)
7265 continue;
7266 if (return_next_match) // Return next matching item after current item.
7267 return idx;
7268 if (first_match_idx == -1 && nav_item_idx == -1) // Return first match immediately if we don't have a nav_item_idx value.
7269 return idx;
7270 if (first_match_idx == -1) // Record first match for wrapping.
7271 first_match_idx = idx;
7272 if (nav_item_idx == idx) // Record that we encountering nav_item so we can return next match.
7273 return_next_match = true;
7274 }
7275 return first_match_idx; // First result
7276}
7277
7278int ImGui::TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data)
7279{
7280 int longest_match_idx = -1;
7281 int longest_match_len = 0;
7282 for (int idx = 0; idx < items_count; idx++)
7283 {
7284 const char* item_name = get_item_name_func(user_data, idx);
7285 const int match_len = ImStrimatchlen(req->SearchBuffer, req->SearchBuffer + req->SearchBufferLen, item_name);
7286 if (match_len <= longest_match_len)
7287 continue;
7288 longest_match_idx = idx;
7289 longest_match_len = match_len;
7290 if (match_len == req->SearchBufferLen)
7291 break;
7292 }
7293 return longest_match_idx;
7294}
7295
7296void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data)
7297{
7298#ifndef IMGUI_DISABLE_DEBUG_TOOLS
7299 Text("SearchBuffer = \"%s\"", data->SearchBuffer);
7300 Text("SingleCharMode = %d, Size = %d, Lock = %d", data->Request.SingleCharMode, data->Request.SingleCharSize, data->SingleCharModeLock);
7301 Text("LastRequest = time: %.2f, frame: %d", data->LastRequestTime, data->LastRequestFrame);
7302#else
7303 IM_UNUSED(data);
7304#endif
7305}
7306
7307//-------------------------------------------------------------------------
7308// [SECTION] Widgets: Box-Select support
7309// This has been extracted away from Multi-Select logic in the hope that it could eventually be used elsewhere, but hasn't been yet.
7310//-------------------------------------------------------------------------
7311// Extra logic in MultiSelectItemFooter() and ImGuiListClipper::Step()
7312//-------------------------------------------------------------------------
7313// - BoxSelectPreStartDrag() [Internal]
7314// - BoxSelectActivateDrag() [Internal]
7315// - BoxSelectDeactivateDrag() [Internal]
7316// - BoxSelectScrollWithMouseDrag() [Internal]
7317// - BeginBoxSelect() [Internal]
7318// - EndBoxSelect() [Internal]
7319//-------------------------------------------------------------------------
7320
7321// Call on the initial click.
7322static void BoxSelectPreStartDrag(ImGuiID id, ImGuiSelectionUserData clicked_item)
7323{
7324 ImGuiContext& g = *GImGui;
7325 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7326 bs->ID = id;
7327 bs->IsStarting = true; // Consider starting box-select.
7328 bs->IsStartedFromVoid = (clicked_item == ImGuiSelectionUserData_Invalid);
7329 bs->IsStartedSetNavIdOnce = bs->IsStartedFromVoid;
7330 bs->KeyMods = g.IO.KeyMods;
7331 bs->StartPosRel = bs->EndPosRel = ImGui::WindowPosAbsToRel(g.CurrentWindow, g.IO.MousePos);
7332 bs->ScrollAccum = ImVec2(0.0f, 0.0f);
7333}
7334
7335static void BoxSelectActivateDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window)
7336{
7337 ImGuiContext& g = *GImGui;
7338 IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Activate\n", bs->ID);
7339 bs->IsActive = true;
7340 bs->Window = window;
7341 bs->IsStarting = false;
7342 ImGui::SetActiveID(bs->ID, window);
7343 ImGui::SetActiveIdUsingAllKeyboardKeys();
7344 if (bs->IsStartedFromVoid && (bs->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0)
7345 bs->RequestClear = true;
7346}
7347
7348static void BoxSelectDeactivateDrag(ImGuiBoxSelectState* bs)
7349{
7350 ImGuiContext& g = *GImGui;
7351 bs->IsActive = bs->IsStarting = false;
7352 if (g.ActiveId == bs->ID)
7353 {
7354 IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Deactivate\n", bs->ID);
7355 ImGui::ClearActiveID();
7356 }
7357 bs->ID = 0;
7358}
7359
7360static void BoxSelectScrollWithMouseDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window, const ImRect& inner_r)
7361{
7362 ImGuiContext& g = *GImGui;
7363 IM_ASSERT(bs->Window == window);
7364 for (int n = 0; n < 2; n++) // each axis
7365 {
7366 const float mouse_pos = g.IO.MousePos[n];
7367 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;
7368 const float scroll_curr = window->Scroll[n];
7369 if (dist == 0.0f || (dist < 0.0f && scroll_curr < 0.0f) || (dist > 0.0f && scroll_curr >= window->ScrollMax[n]))
7370 continue;
7371
7372 const float speed_multiplier = ImLinearRemapClamp(g.FontSize, g.FontSize * 5.0f, 1.0f, 4.0f, ImAbs(dist)); // x1 to x4 depending on distance
7373 const float scroll_step = g.FontSize * 35.0f * speed_multiplier * ImSign(dist) * g.IO.DeltaTime;
7374 bs->ScrollAccum[n] += scroll_step;
7375
7376 // Accumulate into a stored value so we can handle high-framerate
7377 const float scroll_step_i = ImFloor(bs->ScrollAccum[n]);
7378 if (scroll_step_i == 0.0f)
7379 continue;
7380 if (n == 0)
7381 ImGui::SetScrollX(window, scroll_curr + scroll_step_i);
7382 else
7383 ImGui::SetScrollY(window, scroll_curr + scroll_step_i);
7384 bs->ScrollAccum[n] -= scroll_step_i;
7385 }
7386}
7387
7388bool ImGui::BeginBoxSelect(const ImRect& scope_rect, ImGuiWindow* window, ImGuiID box_select_id, ImGuiMultiSelectFlags ms_flags)
7389{
7390 ImGuiContext& g = *GImGui;
7391 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7392 KeepAliveID(box_select_id);
7393 if (bs->ID != box_select_id)
7394 return false;
7395
7396 // IsStarting is set by MultiSelectItemFooter() when considering a possible box-select. We validate it here and lock geometry.
7397 bs->UnclipMode = false;
7398 bs->RequestClear = false;
7399 if (bs->IsStarting && IsMouseDragPastThreshold(0))
7400 BoxSelectActivateDrag(bs, window);
7401 else if ((bs->IsStarting || bs->IsActive) && g.IO.MouseDown[0] == false)
7402 BoxSelectDeactivateDrag(bs);
7403 if (!bs->IsActive)
7404 return false;
7405
7406 // Current frame absolute prev/current rectangles are used to toggle selection.
7407 // They are derived from positions relative to scrolling space.
7408 ImVec2 start_pos_abs = WindowPosRelToAbs(window, bs->StartPosRel);
7409 ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, bs->EndPosRel); // Clamped already
7410 ImVec2 curr_end_pos_abs = g.IO.MousePos;
7411 if (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) // Box-select scrolling only happens with ScopeWindow
7412 curr_end_pos_abs = ImClamp(curr_end_pos_abs, scope_rect.Min, scope_rect.Max);
7413 bs->BoxSelectRectPrev.Min = ImMin(start_pos_abs, prev_end_pos_abs);
7414 bs->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs);
7415 bs->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs);
7416 bs->BoxSelectRectCurr.Max = ImMax(start_pos_abs, curr_end_pos_abs);
7417
7418 // Box-select 2D mode detects horizontal changes (vertical ones are already picked by Clipper)
7419 // Storing an extra rect used by widgets supporting box-select.
7420 if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d)
7421 if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x)
7422 {
7423 bs->UnclipMode = true;
7424 bs->UnclipRect = bs->BoxSelectRectPrev; // FIXME-OPT: UnclipRect x coordinates could be intersection of Prev and Curr rect on X axis.
7425 bs->UnclipRect.Add(bs->BoxSelectRectCurr);
7426 }
7427
7428 //GetForegroundDrawList()->AddRect(bs->UnclipRect.Min, bs->UnclipRect.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f);
7429 //GetForegroundDrawList()->AddRect(bs->BoxSelectRectPrev.Min, bs->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f);
7430 //GetForegroundDrawList()->AddRect(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f);
7431 return true;
7432}
7433
7434void ImGui::EndBoxSelect(const ImRect& scope_rect, ImGuiMultiSelectFlags ms_flags)
7435{
7436 ImGuiContext& g = *GImGui;
7437 ImGuiWindow* window = g.CurrentWindow;
7438 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7439 IM_ASSERT(bs->IsActive);
7440 bs->UnclipMode = false;
7441
7442 // Render selection rectangle
7443 bs->EndPosRel = WindowPosAbsToRel(window, ImClamp(g.IO.MousePos, scope_rect.Min, scope_rect.Max)); // Clamp stored position according to current scrolling view
7444 ImRect box_select_r = bs->BoxSelectRectCurr;
7445 box_select_r.ClipWith(scope_rect);
7446 window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling
7447 window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavCursor)); // FIXME-MULTISELECT: Styling
7448
7449 // Scroll
7450 const bool enable_scroll = (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms_flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0;
7451 if (enable_scroll)
7452 {
7453 ImRect scroll_r = scope_rect;
7454 scroll_r.Expand(-g.FontSize);
7455 //GetForegroundDrawList()->AddRect(scroll_r.Min, scroll_r.Max, IM_COL32(0, 255, 0, 255));
7456 if (!scroll_r.Contains(g.IO.MousePos))
7457 BoxSelectScrollWithMouseDrag(bs, window, scroll_r);
7458 }
7459}
7460
7461//-------------------------------------------------------------------------
7462// [SECTION] Widgets: Multi-Select support
7463//-------------------------------------------------------------------------
7464// - DebugLogMultiSelectRequests() [Internal]
7465// - CalcScopeRect() [Internal]
7466// - BeginMultiSelect()
7467// - EndMultiSelect()
7468// - SetNextItemSelectionUserData()
7469// - MultiSelectItemHeader() [Internal]
7470// - MultiSelectItemFooter() [Internal]
7471// - DebugNodeMultiSelectState() [Internal]
7472//-------------------------------------------------------------------------
7473
7474static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* io)
7475{
7476 ImGuiContext& g = *GImGui;
7477 IM_UNUSED(function);
7478 for (const ImGuiSelectionRequest& req : io->Requests)
7479 {
7480 if (req.Type == ImGuiSelectionRequestType_SetAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetAll %d (= %s)\n", function, req.Selected, req.Selected ? "SelectAll" : "Clear");
7481 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);
7482 }
7483}
7484
7485static ImRect CalcScopeRect(ImGuiMultiSelectTempData* ms, ImGuiWindow* window)
7486{
7487 ImGuiContext& g = *GImGui;
7488 if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)
7489 {
7490 // Warning: this depends on CursorMaxPos so it means to be called by EndMultiSelect() only
7491 return ImRect(ms->ScopeRectMin, ImMax(window->DC.CursorMaxPos, ms->ScopeRectMin));
7492 }
7493 else
7494 {
7495 // When a table, pull HostClipRect, which allows us to predict ClipRect before first row/layout is performed. (#7970)
7496 ImRect scope_rect = window->InnerClipRect;
7497 if (g.CurrentTable != NULL)
7498 scope_rect = g.CurrentTable->HostClipRect;
7499
7500 // Add inner table decoration (#7821) // FIXME: Why not baking in InnerClipRect?
7501 scope_rect.Min = ImMin(scope_rect.Min + ImVec2(window->DecoInnerSizeX1, window->DecoInnerSizeY1), scope_rect.Max);
7502 return scope_rect;
7503 }
7504}
7505
7506// Return ImGuiMultiSelectIO structure.
7507// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect().
7508// Passing 'selection_size' and 'items_count' parameters is currently optional.
7509// - 'selection_size' is useful to disable some shortcut routing: e.g. ImGuiMultiSelectFlags_ClearOnEscape won't claim Escape key when selection_size 0,
7510// allowing a first press to clear selection THEN the second press to leave child window and return to parent.
7511// - '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).
7512// - If they are costly for you to compute (e.g. external intrusive selection without maintaining size), you may avoid them and pass -1.
7513// - 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.
7514ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int selection_size, int items_count)
7515{
7516 ImGuiContext& g = *GImGui;
7517 ImGuiWindow* window = g.CurrentWindow;
7518
7519 if (++g.MultiSelectTempDataStacked > g.MultiSelectTempData.Size)
7520 g.MultiSelectTempData.resize(g.MultiSelectTempDataStacked, ImGuiMultiSelectTempData());
7521 ImGuiMultiSelectTempData* ms = &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1];
7522 IM_STATIC_ASSERT(offsetof(ImGuiMultiSelectTempData, IO) == 0); // Clear() relies on that.
7523 g.CurrentMultiSelect = ms;
7524 if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0)
7525 flags |= ImGuiMultiSelectFlags_ScopeWindow;
7526 if (flags & ImGuiMultiSelectFlags_SingleSelect)
7527 flags &= ~(ImGuiMultiSelectFlags_BoxSelect2d | ImGuiMultiSelectFlags_BoxSelect1d);
7528 if (flags & ImGuiMultiSelectFlags_BoxSelect2d)
7529 flags &= ~ImGuiMultiSelectFlags_BoxSelect1d;
7530
7531 // FIXME: Workaround to the fact we override CursorMaxPos, meaning size measurement are lost. (#8250)
7532 // They should perhaps be stacked properly?
7533 if (ImGuiTable* table = g.CurrentTable)
7534 if (table->CurrentColumn != -1)
7535 TableEndCell(table); // This is currently safe to call multiple time. If that properly is lost we can extract the "save measurement" part of it.
7536
7537 // FIXME: BeginFocusScope()
7538 const ImGuiID id = window->IDStack.back();
7539 ms->Clear();
7540 ms->FocusScopeId = id;
7541 ms->Flags = flags;
7542 ms->IsFocused = (ms->FocusScopeId == g.NavFocusScopeId);
7543 ms->BackupCursorMaxPos = window->DC.CursorMaxPos;
7544 ms->ScopeRectMin = window->DC.CursorMaxPos = window->DC.CursorPos;
7545 PushFocusScope(ms->FocusScopeId);
7546 if (flags & ImGuiMultiSelectFlags_ScopeWindow) // Mark parent child window as navigable into, with highlight. Assume user will always submit interactive items.
7547 window->DC.NavLayersActiveMask |= 1 << ImGuiNavLayer_Main;
7548
7549 // Use copy of keyboard mods at the time of the request, otherwise we would requires mods to be held for an extra frame.
7550 ms->KeyMods = g.NavJustMovedToId ? (g.NavJustMovedToIsTabbing ? 0 : g.NavJustMovedToKeyMods) : g.IO.KeyMods;
7551 if (flags & ImGuiMultiSelectFlags_NoRangeSelect)
7552 ms->KeyMods &= ~ImGuiMod_Shift;
7553
7554 // Bind storage
7555 ImGuiMultiSelectState* storage = g.MultiSelectStorage.GetOrAddByKey(id);
7556 storage->ID = id;
7557 storage->LastFrameActive = g.FrameCount;
7558 storage->LastSelectionSize = selection_size;
7559 storage->Window = window;
7560 ms->Storage = storage;
7561
7562 // Output to user
7563 ms->IO.Requests.resize(0);
7564 ms->IO.RangeSrcItem = storage->RangeSrcItem;
7565 ms->IO.NavIdItem = storage->NavIdItem;
7566 ms->IO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false;
7567 ms->IO.ItemsCount = items_count;
7568
7569 // Clear when using Navigation to move within the scope
7570 // (we compare FocusScopeId so it possible to use multiple selections inside a same window)
7571 bool request_clear = false;
7572 bool request_select_all = false;
7573 if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData)
7574 {
7575 if (ms->KeyMods & ImGuiMod_Shift)
7576 ms->IsKeyboardSetRange = true;
7577 if (ms->IsKeyboardSetRange)
7578 IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid); // Not ready -> could clear?
7579 if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0)
7580 request_clear = true;
7581 }
7582 else if (g.NavJustMovedFromFocusScopeId == ms->FocusScopeId)
7583 {
7584 // Also clear on leaving scope (may be optional?)
7585 if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0)
7586 request_clear = true;
7587 }
7588
7589 // Box-select handling: update active state.
7590 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7591 if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7592 {
7593 ms->BoxSelectId = GetID("##BoxSelect");
7594 if (BeginBoxSelect(CalcScopeRect(ms, window), window, ms->BoxSelectId, flags))
7595 request_clear |= bs->RequestClear;
7596 }
7597
7598 if (ms->IsFocused)
7599 {
7600 // Shortcut: Clear selection (Escape)
7601 // - Only claim shortcut if selection is not empty, allowing further presses on Escape to e.g. leave current child window.
7602 // - Box select also handle Escape and needs to pass an id to bypass ActiveIdUsingAllKeyboardKeys lock.
7603 if (flags & ImGuiMultiSelectFlags_ClearOnEscape)
7604 {
7605 if (selection_size != 0 || bs->IsActive)
7606 if (Shortcut(ImGuiKey_Escape, ImGuiInputFlags_None, bs->IsActive ? bs->ID : 0))
7607 {
7608 request_clear = true;
7609 if (bs->IsActive)
7610 BoxSelectDeactivateDrag(bs);
7611 }
7612 }
7613
7614 // Shortcut: Select all (CTRL+A)
7615 if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll))
7616 if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A))
7617 request_select_all = true;
7618 }
7619
7620 if (request_clear || request_select_all)
7621 {
7622 MultiSelectAddSetAll(ms, request_select_all);
7623 if (!request_select_all)
7624 storage->LastSelectionSize = 0;
7625 }
7626 ms->LoopRequestSetAll = request_select_all ? 1 : request_clear ? 0 : -1;
7627 ms->LastSubmittedItem = ImGuiSelectionUserData_Invalid;
7628
7629 if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)
7630 DebugLogMultiSelectRequests("BeginMultiSelect", &ms->IO);
7631
7632 return &ms->IO;
7633}
7634
7635// Return updated ImGuiMultiSelectIO structure.
7636// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect().
7637ImGuiMultiSelectIO* ImGui::EndMultiSelect()
7638{
7639 ImGuiContext& g = *GImGui;
7640 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
7641 ImGuiMultiSelectState* storage = ms->Storage;
7642 ImGuiWindow* window = g.CurrentWindow;
7643 IM_ASSERT_USER_ERROR(ms->FocusScopeId == g.CurrentFocusScopeId, "EndMultiSelect() FocusScope mismatch!");
7644 IM_ASSERT(g.CurrentMultiSelect != NULL && storage->Window == g.CurrentWindow);
7645 IM_ASSERT(g.MultiSelectTempDataStacked > 0 && &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] == g.CurrentMultiSelect);
7646
7647 ImRect scope_rect = CalcScopeRect(ms, window);
7648 if (ms->IsFocused)
7649 {
7650 // We currently don't allow user code to modify RangeSrcItem by writing to BeginIO's version, but that would be an easy change here.
7651 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)
7652 {
7653 IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset RangeSrcItem.\n"); // Will set be to NavId.
7654 storage->RangeSrcItem = ImGuiSelectionUserData_Invalid;
7655 }
7656 if (ms->NavIdPassedBy == false && storage->NavIdItem != ImGuiSelectionUserData_Invalid)
7657 {
7658 IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset NavIdItem.\n");
7659 storage->NavIdItem = ImGuiSelectionUserData_Invalid;
7660 storage->NavIdSelected = -1;
7661 }
7662
7663 if ((ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) && GetBoxSelectState(ms->BoxSelectId))
7664 EndBoxSelect(scope_rect, ms->Flags);
7665 }
7666
7667 if (ms->IsEndIO == false)
7668 ms->IO.Requests.resize(0);
7669
7670 // Clear selection when clicking void?
7671 // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection!
7672 // The InnerRect test is necessary for non-child/decorated windows.
7673 bool scope_hovered = IsWindowHovered() && window->InnerRect.Contains(g.IO.MousePos);
7674 if (scope_hovered && (ms->Flags & ImGuiMultiSelectFlags_ScopeRect))
7675 scope_hovered &= scope_rect.Contains(g.IO.MousePos);
7676 if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0)
7677 {
7678 if (ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7679 {
7680 if (!g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && g.IO.MouseClickedCount[0] == 1)
7681 {
7682 BoxSelectPreStartDrag(ms->BoxSelectId, ImGuiSelectionUserData_Invalid);
7683 FocusWindow(window, ImGuiFocusRequestFlags_UnlessBelowModal);
7684 SetHoveredID(ms->BoxSelectId);
7685 if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)
7686 SetNavID(0, ImGuiNavLayer_Main, ms->FocusScopeId, ImRect(g.IO.MousePos, g.IO.MousePos)); // Automatically switch FocusScope for initial click from void to box-select.
7687 }
7688 }
7689
7690 if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid)
7691 if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None)
7692 MultiSelectAddSetAll(ms, false);
7693 }
7694
7695 // Courtesy nav wrapping helper flag
7696 if (ms->Flags & ImGuiMultiSelectFlags_NavWrapX)
7697 {
7698 IM_ASSERT(ms->Flags & ImGuiMultiSelectFlags_ScopeWindow); // Only supported at window scope
7699 ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX);
7700 }
7701
7702 // Unwind
7703 window->DC.CursorMaxPos = ImMax(ms->BackupCursorMaxPos, window->DC.CursorMaxPos);
7704 PopFocusScope();
7705
7706 if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)
7707 DebugLogMultiSelectRequests("EndMultiSelect", &ms->IO);
7708
7709 ms->FocusScopeId = 0;
7710 ms->Flags = ImGuiMultiSelectFlags_None;
7711 g.CurrentMultiSelect = (--g.MultiSelectTempDataStacked > 0) ? &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] : NULL;
7712
7713 return &ms->IO;
7714}
7715
7716void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data)
7717{
7718 // Note that flags will be cleared by ItemAdd(), so it's only useful for Navigation code!
7719 // This designed so widgets can also cheaply set this before calling ItemAdd(), so we are not tied to MultiSelect api.
7720 ImGuiContext& g = *GImGui;
7721 g.NextItemData.SelectionUserData = selection_user_data;
7722 g.NextItemData.FocusScopeId = g.CurrentFocusScopeId;
7723
7724 if (ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect)
7725 {
7726 // Auto updating RangeSrcPassedBy for cases were clipper is not used (done before ItemAdd() clipping)
7727 g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData | ImGuiItemFlags_IsMultiSelect;
7728 if (ms->IO.RangeSrcItem == selection_user_data)
7729 ms->RangeSrcPassedBy = true;
7730 }
7731 else
7732 {
7733 g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData;
7734 }
7735}
7736
7737// In charge of:
7738// - Applying SetAll for submitted items.
7739// - Applying SetRange for submitted items and record end points.
7740// - Altering button behavior flags to facilitate use with drag and drop.
7741void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags)
7742{
7743 ImGuiContext& g = *GImGui;
7744 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
7745
7746 bool selected = *p_selected;
7747 if (ms->IsFocused)
7748 {
7749 ImGuiMultiSelectState* storage = ms->Storage;
7750 ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData;
7751 IM_ASSERT(g.NextItemData.FocusScopeId == g.CurrentFocusScopeId && "Forgot to call SetNextItemSelectionUserData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope");
7752
7753 // Apply SetAll (Clear/SelectAll) requests requested by BeginMultiSelect().
7754 // This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper.
7755 // If you are using a clipper you need to process the SetAll request after calling BeginMultiSelect()
7756 if (ms->LoopRequestSetAll != -1)
7757 selected = (ms->LoopRequestSetAll == 1);
7758
7759 // 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)
7760 // For this to work, we need someone to set 'RangeSrcPassedBy = true' at some point (either clipper either SetNextItemSelectionUserData() function)
7761 if (ms->IsKeyboardSetRange)
7762 {
7763 IM_ASSERT(id != 0 && (ms->KeyMods & ImGuiMod_Shift) != 0);
7764 const bool is_range_dst = (ms->RangeDstPassedBy == false) && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped.
7765 if (is_range_dst)
7766 ms->RangeDstPassedBy = true;
7767 if (is_range_dst && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) // If we don't have RangeSrc, assign RangeSrc = RangeDst
7768 {
7769 storage->RangeSrcItem = item_data;
7770 storage->RangeSelected = selected ? 1 : 0;
7771 }
7772 const bool is_range_src = storage->RangeSrcItem == item_data;
7773 if (is_range_src || is_range_dst || ms->RangeSrcPassedBy != ms->RangeDstPassedBy)
7774 {
7775 // Apply range-select value to visible items
7776 IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid && storage->RangeSelected != -1);
7777 selected = (storage->RangeSelected != 0);
7778 }
7779 else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0 && (ms->Flags & ImGuiMultiSelectFlags_NoAutoClear) == 0)
7780 {
7781 // Clear other items
7782 selected = false;
7783 }
7784 }
7785 *p_selected = selected;
7786 }
7787
7788 // Alter button behavior flags
7789 // To handle drag and drop of multiple items we need to avoid clearing selection on click.
7790 // 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.
7791 if (p_button_flags != NULL)
7792 {
7793 ImGuiButtonFlags button_flags = *p_button_flags;
7794 button_flags |= ImGuiButtonFlags_NoHoveredOnFocus;
7795 if ((!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) && !(ms->Flags & ImGuiMultiSelectFlags_SelectOnClickRelease))
7796 button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease;
7797 else
7798 button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
7799 *p_button_flags = button_flags;
7800 }
7801}
7802
7803// In charge of:
7804// - Auto-select on navigation.
7805// - Box-select toggle handling.
7806// - Right-click handling.
7807// - Altering selection based on Ctrl/Shift modifiers, both for keyboard and mouse.
7808// - Record current selection state for RangeSrc
7809// This is all rather complex, best to run and refer to "widgets_multiselect_xxx" tests in imgui_test_suite.
7810void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed)
7811{
7812 ImGuiContext& g = *GImGui;
7813 ImGuiWindow* window = g.CurrentWindow;
7814
7815 bool selected = *p_selected;
7816 bool pressed = *p_pressed;
7817 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
7818 ImGuiMultiSelectState* storage = ms->Storage;
7819 if (pressed)
7820 ms->IsFocused = true;
7821
7822 bool hovered = false;
7823 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect)
7824 hovered = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
7825 if (!ms->IsFocused && !hovered)
7826 return;
7827
7828 ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData;
7829
7830 ImGuiMultiSelectFlags flags = ms->Flags;
7831 const bool is_singleselect = (flags & ImGuiMultiSelectFlags_SingleSelect) != 0;
7832 bool is_ctrl = (ms->KeyMods & ImGuiMod_Ctrl) != 0;
7833 bool is_shift = (ms->KeyMods & ImGuiMod_Shift) != 0;
7834
7835 bool apply_to_range_src = false;
7836
7837 if (g.NavId == id && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid)
7838 apply_to_range_src = true;
7839 if (ms->IsEndIO == false)
7840 {
7841 ms->IO.Requests.resize(0);
7842 ms->IsEndIO = true;
7843 }
7844
7845 // Auto-select as you navigate a list
7846 if (g.NavJustMovedToId == id)
7847 {
7848 if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
7849 {
7850 if (is_ctrl && is_shift)
7851 pressed = true;
7852 else if (!is_ctrl)
7853 selected = pressed = true;
7854 }
7855 else
7856 {
7857 // With NoAutoSelect, using Shift+keyboard performs a write/copy
7858 if (is_shift)
7859 pressed = true;
7860 else if (!is_ctrl)
7861 apply_to_range_src = true; // Since if (pressed) {} main block is not running we update this
7862 }
7863 }
7864
7865 if (apply_to_range_src)
7866 {
7867 storage->RangeSrcItem = item_data;
7868 storage->RangeSelected = selected; // Will be updated at the end of this function anyway.
7869 }
7870
7871 // Box-select toggle handling
7872 if (ms->BoxSelectId != 0)
7873 if (ImGuiBoxSelectState* bs = GetBoxSelectState(ms->BoxSelectId))
7874 {
7875 const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(g.LastItemData.Rect);
7876 const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(g.LastItemData.Rect);
7877 if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr))
7878 {
7879 if (storage->LastSelectionSize <= 0 && bs->IsStartedSetNavIdOnce)
7880 {
7881 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)
7882 bs->IsStartedSetNavIdOnce = false;
7883 }
7884 else
7885 {
7886 selected = !selected;
7887 MultiSelectAddSetRange(ms, selected, +1, item_data, item_data);
7888 }
7889 storage->LastSelectionSize = ImMax(storage->LastSelectionSize + 1, 1);
7890 }
7891 }
7892
7893 // Right-click handling.
7894 // FIXME-MULTISELECT: Currently filtered out by ImGuiMultiSelectFlags_NoAutoSelect but maybe should be moved to Selectable(). See https://github.com/ocornut/imgui/pull/5816
7895 if (hovered && IsMouseClicked(1) && (flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
7896 {
7897 if (g.ActiveId != 0 && g.ActiveId != id)
7898 ClearActiveID();
7899 SetFocusID(id, window);
7900 if (!pressed && !selected)
7901 {
7902 pressed = true;
7903 is_ctrl = is_shift = false;
7904 }
7905 }
7906
7907 // Unlike Space, Enter doesn't alter selection (but can still return a press) unless current item is not selected.
7908 // The later, "unless current item is not select", may become optional? It seems like a better default if Enter doesn't necessarily open something
7909 // (unlike e.g. Windows explorer). For use case where Enter always open something, we might decide to make this optional?
7910 const bool enter_pressed = pressed && (g.NavActivateId == id) && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput);
7911
7912 // Alter selection
7913 if (pressed && (!enter_pressed || !selected))
7914 {
7915 // Box-select
7916 ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse;
7917 if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7918 if (selected == false && !g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1)
7919 BoxSelectPreStartDrag(ms->BoxSelectId, item_data);
7920
7921 //----------------------------------------------------------------------------------------
7922 // ACTION | Begin | Pressed/Activated | End
7923 //----------------------------------------------------------------------------------------
7924 // Keys Navigated: | Clear | Src=item, Sel=1 SetRange 1
7925 // Keys Navigated: Ctrl | n/a | n/a
7926 // Keys Navigated: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1
7927 // Keys Navigated: Ctrl+Shift | n/a | Dst=item, Sel=Src => Clear + SetRange Src-Dst
7928 // Keys Activated: | n/a | Src=item, Sel=1 => Clear + SetRange 1
7929 // Keys Activated: Ctrl | n/a | Src=item, Sel=!Sel => SetSange 1
7930 // Keys Activated: Shift | n/a | Dst=item, Sel=1 => Clear + SetSange 1
7931 //----------------------------------------------------------------------------------------
7932 // Mouse Pressed: | n/a | Src=item, Sel=1, => Clear + SetRange 1
7933 // Mouse Pressed: Ctrl | n/a | Src=item, Sel=!Sel => SetRange 1
7934 // Mouse Pressed: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1
7935 // Mouse Pressed: Ctrl+Shift | n/a | Dst=item, Sel=!Sel => SetRange Src-Dst
7936 //----------------------------------------------------------------------------------------
7937
7938 if ((flags & ImGuiMultiSelectFlags_NoAutoClear) == 0)
7939 {
7940 bool request_clear = false;
7941 if (is_singleselect)
7942 request_clear = true;
7943 else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl)
7944 request_clear = (flags & ImGuiMultiSelectFlags_NoAutoClearOnReselect) ? !selected : true;
7945 else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl)
7946 request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again.
7947 if (request_clear)
7948 MultiSelectAddSetAll(ms, false);
7949 }
7950
7951 int range_direction;
7952 bool range_selected;
7953 if (is_shift && !is_singleselect)
7954 {
7955 //IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue);
7956 if (storage->RangeSrcItem == ImGuiSelectionUserData_Invalid)
7957 storage->RangeSrcItem = item_data;
7958 if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
7959 {
7960 // Shift+Arrow always select
7961 // Ctrl+Shift+Arrow copy source selection state (already stored by BeginMultiSelect() in storage->RangeSelected)
7962 range_selected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true;
7963 }
7964 else
7965 {
7966 // Shift+Arrow copy source selection state
7967 // Shift+Click always copy from target selection state
7968 if (ms->IsKeyboardSetRange)
7969 range_selected = (storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true;
7970 else
7971 range_selected = !selected;
7972 }
7973 range_direction = ms->RangeSrcPassedBy ? +1 : -1;
7974 }
7975 else
7976 {
7977 // Ctrl inverts selection, otherwise always select
7978 if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
7979 selected = is_ctrl ? !selected : true;
7980 else
7981 selected = !selected;
7982 storage->RangeSrcItem = item_data;
7983 range_selected = selected;
7984 range_direction = +1;
7985 }
7986 MultiSelectAddSetRange(ms, range_selected, range_direction, storage->RangeSrcItem, item_data);
7987 }
7988
7989 // Update/store the selection state of the Source item (used by CTRL+SHIFT, when Source is unselected we perform a range unselect)
7990 if (storage->RangeSrcItem == item_data)
7991 storage->RangeSelected = selected ? 1 : 0;
7992
7993 // Update/store the selection state of focused item
7994 if (g.NavId == id)
7995 {
7996 storage->NavIdItem = item_data;
7997 storage->NavIdSelected = selected ? 1 : 0;
7998 }
7999 if (storage->NavIdItem == item_data)
8000 ms->NavIdPassedBy = true;
8001 ms->LastSubmittedItem = item_data;
8002
8003 *p_selected = selected;
8004 *p_pressed = pressed;
8005}
8006
8007void ImGui::MultiSelectAddSetAll(ImGuiMultiSelectTempData* ms, bool selected)
8008{
8009 ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, selected, 0, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid };
8010 ms->IO.Requests.resize(0); // Can always clear previous requests
8011 ms->IO.Requests.push_back(req); // Add new request
8012}
8013
8014void ImGui::MultiSelectAddSetRange(ImGuiMultiSelectTempData* ms, bool selected, int range_dir, ImGuiSelectionUserData first_item, ImGuiSelectionUserData last_item)
8015{
8016 // Merge contiguous spans into same request (unless NoRangeSelect is set which guarantees single-item ranges)
8017 if (ms->IO.Requests.Size > 0 && first_item == last_item && (ms->Flags & ImGuiMultiSelectFlags_NoRangeSelect) == 0)
8018 {
8019 ImGuiSelectionRequest* prev = &ms->IO.Requests.Data[ms->IO.Requests.Size - 1];
8020 if (prev->Type == ImGuiSelectionRequestType_SetRange && prev->RangeLastItem == ms->LastSubmittedItem && prev->Selected == selected)
8021 {
8022 prev->RangeLastItem = last_item;
8023 return;
8024 }
8025 }
8026
8027 ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, (ImS8)range_dir, (range_dir > 0) ? first_item : last_item, (range_dir > 0) ? last_item : first_item };
8028 ms->IO.Requests.push_back(req); // Add new request
8029}
8030
8031void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage)
8032{
8033#ifndef IMGUI_DISABLE_DEBUG_TOOLS
8034 const bool is_active = (storage->LastFrameActive >= GetFrameCount() - 2); // Note that fully clipped early out scrolling tables will appear as inactive here.
8035 if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); }
8036 bool open = TreeNode((void*)(intptr_t)storage->ID, "MultiSelect 0x%08X in '%s'%s", storage->ID, storage->Window ? storage->Window->Name : "N/A", is_active ? "" : " *Inactive*");
8037 if (!is_active) { PopStyleColor(); }
8038 if (!open)
8039 return;
8040 Text("RangeSrcItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), RangeSelected = %d", storage->RangeSrcItem, storage->RangeSrcItem, storage->RangeSelected);
8041 Text("NavIdItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), NavIdSelected = %d", storage->NavIdItem, storage->NavIdItem, storage->NavIdSelected);
8042 Text("LastSelectionSize = %d", storage->LastSelectionSize); // Provided by user
8043 TreePop();
8044#else
8045 IM_UNUSED(storage);
8046#endif
8047}
8048
8049//-------------------------------------------------------------------------
8050// [SECTION] Widgets: Multi-Select helpers
8051//-------------------------------------------------------------------------
8052// - ImGuiSelectionBasicStorage
8053// - ImGuiSelectionExternalStorage
8054//-------------------------------------------------------------------------
8055
8056ImGuiSelectionBasicStorage::ImGuiSelectionBasicStorage()
8057{
8058 Size = 0;
8059 PreserveOrder = false;
8060 UserData = NULL;
8061 AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; };
8062 _SelectionOrder = 1; // Always >0
8063}
8064
8065void ImGuiSelectionBasicStorage::Clear()
8066{
8067 Size = 0;
8068 _SelectionOrder = 1; // Always >0
8069 _Storage.Data.resize(0);
8070}
8071
8072void ImGuiSelectionBasicStorage::Swap(ImGuiSelectionBasicStorage& r)
8073{
8074 ImSwap(Size, r.Size);
8075 ImSwap(_SelectionOrder, r._SelectionOrder);
8076 _Storage.Data.swap(r._Storage.Data);
8077}
8078
8079bool ImGuiSelectionBasicStorage::Contains(ImGuiID id) const
8080{
8081 return _Storage.GetInt(id, 0) != 0;
8082}
8083
8084static int IMGUI_CDECL PairComparerByValueInt(const void* lhs, const void* rhs)
8085{
8086 int lhs_v = ((const ImGuiStoragePair*)lhs)->val_i;
8087 int rhs_v = ((const ImGuiStoragePair*)rhs)->val_i;
8088 return (lhs_v > rhs_v ? +1 : lhs_v < rhs_v ? -1 : 0);
8089}
8090
8091// GetNextSelectedItem() is an abstraction allowing us to change our underlying actual storage system without impacting user.
8092// (e.g. store unselected vs compact down, compact down on demand, use raw ImVector<ImGuiID> instead of ImGuiStorage...)
8093bool ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it, ImGuiID* out_id)
8094{
8095 ImGuiStoragePair* it = (ImGuiStoragePair*)*opaque_it;
8096 ImGuiStoragePair* it_end = _Storage.Data.Data + _Storage.Data.Size;
8097 if (PreserveOrder && it == NULL && it_end != NULL)
8098 ImQsort(_Storage.Data.Data, (size_t)_Storage.Data.Size, sizeof(ImGuiStoragePair), PairComparerByValueInt); // ~ImGuiStorage::BuildSortByValueInt()
8099 if (it == NULL)
8100 it = _Storage.Data.Data;
8101 IM_ASSERT(it >= _Storage.Data.Data && it <= it_end);
8102 if (it != it_end)
8103 while (it->val_i == 0 && it < it_end)
8104 it++;
8105 const bool has_more = (it != it_end);
8106 *opaque_it = has_more ? (void**)(it + 1) : (void**)(it);
8107 *out_id = has_more ? it->key : 0;
8108 if (PreserveOrder && !has_more)
8109 _Storage.BuildSortByKey();
8110 return has_more;
8111}
8112
8113void ImGuiSelectionBasicStorage::SetItemSelected(ImGuiID id, bool selected)
8114{
8115 int* p_int = _Storage.GetIntRef(id, 0);
8116 if (selected && *p_int == 0) { *p_int = _SelectionOrder++; Size++; }
8117 else if (!selected && *p_int != 0) { *p_int = 0; Size--; }
8118}
8119
8120// Optimized for batch edits (with same value of 'selected')
8121static void ImGuiSelectionBasicStorage_BatchSetItemSelected(ImGuiSelectionBasicStorage* selection, ImGuiID id, bool selected, int size_before_amends, int selection_order)
8122{
8123 ImGuiStorage* storage = &selection->_Storage;
8124 ImGuiStoragePair* it = ImLowerBound(storage->Data.Data, storage->Data.Data + size_before_amends, id);
8125 const bool is_contained = (it != storage->Data.Data + size_before_amends) && (it->key == id);
8126 if (selected == (is_contained && it->val_i != 0))
8127 return;
8128 if (selected && !is_contained)
8129 storage->Data.push_back(ImGuiStoragePair(id, selection_order)); // Push unsorted at end of vector, will be sorted in SelectionMultiAmendsFinish()
8130 else if (is_contained)
8131 it->val_i = selected ? selection_order : 0; // Modify in-place.
8132 selection->Size += selected ? +1 : -1;
8133}
8134
8135static void ImGuiSelectionBasicStorage_BatchFinish(ImGuiSelectionBasicStorage* selection, bool selected, int size_before_amends)
8136{
8137 ImGuiStorage* storage = &selection->_Storage;
8138 if (selected && selection->Size != size_before_amends)
8139 storage->BuildSortByKey(); // When done selecting: sort everything
8140}
8141
8142// Apply requests coming from BeginMultiSelect() and EndMultiSelect().
8143// - Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen.
8144// - Honoring SetRange requests requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem.
8145// - In this demo we often submit indices to SetNextItemSelectionUserData() + store the same indices in persistent selection.
8146// - Your code may do differently. If you store pointers or objects ID in ImGuiSelectionUserData you may need to perform
8147// a lookup in order to have some way to iterate/interpolate between two items.
8148// - A full-featured application is likely to allow search/filtering which is likely to lead to using indices
8149// and constructing a view index <> object id/ptr data structure anyway.
8150// WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THIS UNNECESSARY 'ImGuiSelectionBasicStorage' INDIRECTION LOGIC.
8151// Notice that with the simplest adapter (using indices everywhere), all functions return their parameters.
8152// The most simple implementation (using indices everywhere) would look like:
8153// for (ImGuiSelectionRequest& req : ms_io->Requests)
8154// {
8155// if (req.Type == ImGuiSelectionRequestType_SetAll) { Clear(); if (req.Selected) { for (int n = 0; n < items_count; n++) { SetItemSelected(n, true); } }
8156// if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { SetItemSelected(n, ms_io->Selected); } }
8157// }
8158void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
8159{
8160 // For convenience we obtain ItemsCount as passed to BeginMultiSelect(), which is optional.
8161 // It makes sense when using ImGuiSelectionBasicStorage to simply pass your items count to BeginMultiSelect().
8162 // Other scheme may handle SetAll differently.
8163 IM_ASSERT(ms_io->ItemsCount != -1 && "Missing value for items_count in BeginMultiSelect() call!");
8164 IM_ASSERT(AdapterIndexToStorageId != NULL);
8165
8166 // This is optimized/specialized to cope with very large selections (e.g. 100k+ items)
8167 // - A simpler version could call SetItemSelected() directly instead of ImGuiSelectionBasicStorage_BatchSetItemSelected() + ImGuiSelectionBasicStorage_BatchFinish().
8168 // - Optimized select can append unsorted, then sort in a second pass. Optimized unselect can clear in-place then compact in a second pass.
8169 // - 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.
8170 // - 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
8171 // left and right: it affects coarse clipping + can emit multiple SetRange with 1 item each.)
8172 // FIXME-OPT: For each block of consecutive SetRange request:
8173 // - add all requests to a sorted list, store ID, selected, offset in ImGuiStorage.
8174 // - rewrite sorted storage a single time.
8175 for (ImGuiSelectionRequest& req : ms_io->Requests)
8176 {
8177 if (req.Type == ImGuiSelectionRequestType_SetAll)
8178 {
8179 Clear();
8180 if (req.Selected)
8181 {
8182 _Storage.Data.reserve(ms_io->ItemsCount);
8183 const int size_before_amends = _Storage.Data.Size;
8184 for (int idx = 0; idx < ms_io->ItemsCount; idx++, _SelectionOrder++)
8185 ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, _SelectionOrder);
8186 ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends);
8187 }
8188 }
8189 else if (req.Type == ImGuiSelectionRequestType_SetRange)
8190 {
8191 const int selection_changes = (int)req.RangeLastItem - (int)req.RangeFirstItem + 1;
8192 //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);
8193 if (selection_changes == 1 || (selection_changes < Size / 100))
8194 {
8195 // Multiple sorted insertion + copy likely to be faster.
8196 // Technically we could do a single copy with a little more work (sort sequential SetRange requests)
8197 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++)
8198 SetItemSelected(GetStorageIdFromIndex(idx), req.Selected);
8199 }
8200 else
8201 {
8202 // Append insertion + single sort likely be faster.
8203 // Use req.RangeDirection to set order field so that shift+clicking from 1 to 5 is different than shift+clicking from 5 to 1
8204 const int size_before_amends = _Storage.Data.Size;
8205 int selection_order = _SelectionOrder + ((req.RangeDirection < 0) ? selection_changes - 1 : 0);
8206 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++, selection_order += req.RangeDirection)
8207 ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, selection_order);
8208 if (req.Selected)
8209 _SelectionOrder += selection_changes;
8210 ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends);
8211 }
8212 }
8213 }
8214}
8215
8216//-------------------------------------------------------------------------
8217
8218ImGuiSelectionExternalStorage::ImGuiSelectionExternalStorage()
8219{
8220 UserData = NULL;
8221 AdapterSetItemSelected = NULL;
8222}
8223
8224// Apply requests coming from BeginMultiSelect() and EndMultiSelect().
8225// We also pull 'ms_io->ItemsCount' as passed for BeginMultiSelect() for consistency with ImGuiSelectionBasicStorage
8226// This makes no assumption about underlying storage.
8227void ImGuiSelectionExternalStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
8228{
8229 IM_ASSERT(AdapterSetItemSelected);
8230 for (ImGuiSelectionRequest& req : ms_io->Requests)
8231 {
8232 if (req.Type == ImGuiSelectionRequestType_SetAll)
8233 for (int idx = 0; idx < ms_io->ItemsCount; idx++)
8234 AdapterSetItemSelected(this, idx, req.Selected);
8235 if (req.Type == ImGuiSelectionRequestType_SetRange)
8236 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++)
8237 AdapterSetItemSelected(this, idx, req.Selected);
8238 }
8239}
8240
8241//-------------------------------------------------------------------------
8242// [SECTION] Widgets: ListBox
8243//-------------------------------------------------------------------------
8244// - BeginListBox()
8245// - EndListBox()
8246// - ListBox()
8247//-------------------------------------------------------------------------
8248
8249// This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label.
8250// This handle some subtleties with capturing info from the label.
8251// If you don't need a label you can pretty much directly use ImGui::BeginChild() with ImGuiChildFlags_FrameStyle.
8252// Tip: To have a list filling the entire window width, use size.x = -FLT_MIN and pass an non-visible label e.g. "##empty"
8253// 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).
8254bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg)
8255{
8256 ImGuiContext& g = *GImGui;
8257 ImGuiWindow* window = GetCurrentWindow();
8258 if (window->SkipItems)
8259 return false;
8260
8261 const ImGuiStyle& style = g.Style;
8262 const ImGuiID id = GetID(label);
8263 const ImVec2 label_size = CalcTextSize(label, NULL, true);
8264
8265 // Size default to hold ~7.25 items.
8266 // Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar.
8267 ImVec2 size = ImTrunc(CalcItemSize(size_arg, CalcItemWidth(), GetTextLineHeightWithSpacing() * 7.25f + style.FramePadding.y * 2.0f));
8268 ImVec2 frame_size = ImVec2(size.x, ImMax(size.y, label_size.y));
8269 ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
8270 ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
8271 g.NextItemData.ClearFlags();
8272
8273 if (!IsRectVisible(bb.Min, bb.Max))
8274 {
8275 ItemSize(bb.GetSize(), style.FramePadding.y);
8276 ItemAdd(bb, 0, &frame_bb);
8277 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
8278 return false;
8279 }
8280
8281 // FIXME-OPT: We could omit the BeginGroup() if label_size.x == 0.0f but would need to omit the EndGroup() as well.
8282 BeginGroup();
8283 if (label_size.x > 0.0f)
8284 {
8285 ImVec2 label_pos = ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y);
8286 RenderText(label_pos, label);
8287 window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, label_pos + label_size);
8288 AlignTextToFramePadding();
8289 }
8290
8291 BeginChild(id, frame_bb.GetSize(), ImGuiChildFlags_FrameStyle);
8292 return true;
8293}
8294
8295void ImGui::EndListBox()
8296{
8297 ImGuiContext& g = *GImGui;
8298 ImGuiWindow* window = g.CurrentWindow;
8299 IM_ASSERT((window->Flags & ImGuiWindowFlags_ChildWindow) && "Mismatched BeginListBox/EndListBox calls. Did you test the return value of BeginListBox?");
8300 IM_UNUSED(window);
8301
8302 EndChild();
8303 EndGroup(); // This is only required to be able to do IsItemXXX query on the whole ListBox including label
8304}
8305
8306bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items)
8307{
8308 const bool value_changed = ListBox(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_items);
8309 return value_changed;
8310}
8311
8312// This is merely a helper around BeginListBox(), EndListBox().
8313// Considering using those directly to submit custom data or store selection differently.
8314bool 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)
8315{
8316 ImGuiContext& g = *GImGui;
8317
8318 // Calculate size from "height_in_items"
8319 if (height_in_items < 0)
8320 height_in_items = ImMin(items_count, 7);
8321 float height_in_items_f = height_in_items + 0.25f;
8322 ImVec2 size(0.0f, ImTrunc(GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f));
8323
8324 if (!BeginListBox(label, size))
8325 return false;
8326
8327 // Assume all items have even height (= 1 line of text). If you need items of different height,
8328 // you can create a custom version of ListBox() in your code without using the clipper.
8329 bool value_changed = false;
8330 ImGuiListClipper clipper;
8331 clipper.Begin(items_count, GetTextLineHeightWithSpacing()); // We know exactly our line height here so we pass it as a minor optimization, but generally you don't need to.
8332 clipper.IncludeItemByIndex(*current_item);
8333 while (clipper.Step())
8334 for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
8335 {
8336 const char* item_text = getter(user_data, i);
8337 if (item_text == NULL)
8338 item_text = "*Unknown item*";
8339
8340 PushID(i);
8341 const bool item_selected = (i == *current_item);
8342 if (Selectable(item_text, item_selected))
8343 {
8344 *current_item = i;
8345 value_changed = true;
8346 }
8347 if (item_selected)
8348 SetItemDefaultFocus();
8349 PopID();
8350 }
8351 EndListBox();
8352
8353 if (value_changed)
8354 MarkItemEdited(g.LastItemData.ID);
8355
8356 return value_changed;
8357}
8358
8359//-------------------------------------------------------------------------
8360// [SECTION] Widgets: PlotLines, PlotHistogram
8361//-------------------------------------------------------------------------
8362// - PlotEx() [Internal]
8363// - PlotLines()
8364// - PlotHistogram()
8365//-------------------------------------------------------------------------
8366// Plot/Graph widgets are not very good.
8367// Consider writing your own, or using a third-party one, see:
8368// - ImPlot https://github.com/epezent/implot
8369// - others https://github.com/ocornut/imgui/wiki/Useful-Extensions
8370//-------------------------------------------------------------------------
8371
8372int 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)
8373{
8374 ImGuiContext& g = *GImGui;
8375 ImGuiWindow* window = GetCurrentWindow();
8376 if (window->SkipItems)
8377 return -1;
8378
8379 const ImGuiStyle& style = g.Style;
8380 const ImGuiID id = window->GetID(label);
8381
8382 const ImVec2 label_size = CalcTextSize(label, NULL, true);
8383 const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), label_size.y + style.FramePadding.y * 2.0f);
8384
8385 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
8386 const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding);
8387 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));
8388 ItemSize(total_bb, style.FramePadding.y);
8389 if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_NoNav))
8390 return -1;
8391 bool hovered;
8392 ButtonBehavior(frame_bb, id, &hovered, NULL);
8393
8394 // Determine scale from values if not specified
8395 if (scale_min == FLT_MAX || scale_max == FLT_MAX)
8396 {
8397 float v_min = FLT_MAX;
8398 float v_max = -FLT_MAX;
8399 for (int i = 0; i < values_count; i++)
8400 {
8401 const float v = values_getter(data, i);
8402 if (v != v) // Ignore NaN values
8403 continue;
8404 v_min = ImMin(v_min, v);
8405 v_max = ImMax(v_max, v);
8406 }
8407 if (scale_min == FLT_MAX)
8408 scale_min = v_min;
8409 if (scale_max == FLT_MAX)
8410 scale_max = v_max;
8411 }
8412
8413 RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
8414
8415 const int values_count_min = (plot_type == ImGuiPlotType_Lines) ? 2 : 1;
8416 int idx_hovered = -1;
8417 if (values_count >= values_count_min)
8418 {
8419 int res_w = ImMin((int)frame_size.x, values_count) + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
8420 int item_count = values_count + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
8421
8422 // Tooltip on hover
8423 if (hovered && inner_bb.Contains(g.IO.MousePos))
8424 {
8425 const float t = ImClamp((g.IO.MousePos.x - inner_bb.Min.x) / (inner_bb.Max.x - inner_bb.Min.x), 0.0f, 0.9999f);
8426 const int v_idx = (int)(t * item_count);
8427 IM_ASSERT(v_idx >= 0 && v_idx < values_count);
8428
8429 const float v0 = values_getter(data, (v_idx + values_offset) % values_count);
8430 const float v1 = values_getter(data, (v_idx + 1 + values_offset) % values_count);
8431 if (plot_type == ImGuiPlotType_Lines)
8432 SetTooltip("%d: %8.4g\n%d: %8.4g", v_idx, v0, v_idx + 1, v1);
8433 else if (plot_type == ImGuiPlotType_Histogram)
8434 SetTooltip("%d: %8.4g", v_idx, v0);
8435 idx_hovered = v_idx;
8436 }
8437
8438 const float t_step = 1.0f / (float)res_w;
8439 const float inv_scale = (scale_min == scale_max) ? 0.0f : (1.0f / (scale_max - scale_min));
8440
8441 float v0 = values_getter(data, (0 + values_offset) % values_count);
8442 float t0 = 0.0f;
8443 ImVec2 tp0 = ImVec2( t0, 1.0f - ImSaturate((v0 - scale_min) * inv_scale) ); // Point in the normalized space of our target rectangle
8444 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
8445
8446 const ImU32 col_base = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLines : ImGuiCol_PlotHistogram);
8447 const ImU32 col_hovered = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLinesHovered : ImGuiCol_PlotHistogramHovered);
8448
8449 for (int n = 0; n < res_w; n++)
8450 {
8451 const float t1 = t0 + t_step;
8452 const int v1_idx = (int)(t0 * item_count + 0.5f);
8453 IM_ASSERT(v1_idx >= 0 && v1_idx < values_count);
8454 const float v1 = values_getter(data, (v1_idx + values_offset + 1) % values_count);
8455 const ImVec2 tp1 = ImVec2( t1, 1.0f - ImSaturate((v1 - scale_min) * inv_scale) );
8456
8457 // 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.
8458 ImVec2 pos0 = ImLerp(inner_bb.Min, inner_bb.Max, tp0);
8459 ImVec2 pos1 = ImLerp(inner_bb.Min, inner_bb.Max, (plot_type == ImGuiPlotType_Lines) ? tp1 : ImVec2(tp1.x, histogram_zero_line_t));
8460 if (plot_type == ImGuiPlotType_Lines)
8461 {
8462 window->DrawList->AddLine(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base);
8463 }
8464 else if (plot_type == ImGuiPlotType_Histogram)
8465 {
8466 if (pos1.x >= pos0.x + 2.0f)
8467 pos1.x -= 1.0f;
8468 window->DrawList->AddRectFilled(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base);
8469 }
8470
8471 t0 = t1;
8472 tp0 = tp1;
8473 }
8474 }
8475
8476 // Text overlay
8477 if (overlay_text)
8478 RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, overlay_text, NULL, NULL, ImVec2(0.5f, 0.0f));
8479
8480 if (label_size.x > 0.0f)
8481 RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), label);
8482
8483 // Return hovered index or -1 if none are hovered.
8484 // 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().
8485 return idx_hovered;
8486}
8487
8489{
8490 const float* Values;
8492
8493 ImGuiPlotArrayGetterData(const float* values, int stride) { Values = values; Stride = stride; }
8494};
8495
8496static float Plot_ArrayGetter(void* data, int idx)
8497{
8499 const float v = *(const float*)(const void*)((const unsigned char*)plot_data->Values + (size_t)idx * plot_data->Stride);
8500 return v;
8501}
8502
8503void 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)
8504{
8505 ImGuiPlotArrayGetterData data(values, stride);
8506 PlotEx(ImGuiPlotType_Lines, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
8507}
8508
8509void 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)
8510{
8511 PlotEx(ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
8512}
8513
8514void 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)
8515{
8516 ImGuiPlotArrayGetterData data(values, stride);
8517 PlotEx(ImGuiPlotType_Histogram, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
8518}
8519
8520void 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)
8521{
8522 PlotEx(ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
8523}
8524
8525//-------------------------------------------------------------------------
8526// [SECTION] Widgets: Value helpers
8527// Those is not very useful, legacy API.
8528//-------------------------------------------------------------------------
8529// - Value()
8530//-------------------------------------------------------------------------
8531
8532void ImGui::Value(const char* prefix, bool b)
8533{
8534 Text("%s: %s", prefix, (b ? "true" : "false"));
8535}
8536
8537void ImGui::Value(const char* prefix, int v)
8538{
8539 Text("%s: %d", prefix, v);
8540}
8541
8542void ImGui::Value(const char* prefix, unsigned int v)
8543{
8544 Text("%s: %d", prefix, v);
8545}
8546
8547void ImGui::Value(const char* prefix, float v, const char* float_format)
8548{
8549 if (float_format)
8550 {
8551 char fmt[64];
8552 ImFormatString(fmt, IM_ARRAYSIZE(fmt), "%%s: %s", float_format);
8553 Text(fmt, prefix, v);
8554 }
8555 else
8556 {
8557 Text("%s: %.3f", prefix, v);
8558 }
8559}
8560
8561//-------------------------------------------------------------------------
8562// [SECTION] MenuItem, BeginMenu, EndMenu, etc.
8563//-------------------------------------------------------------------------
8564// - ImGuiMenuColumns [Internal]
8565// - BeginMenuBar()
8566// - EndMenuBar()
8567// - BeginMainMenuBar()
8568// - EndMainMenuBar()
8569// - BeginMenu()
8570// - EndMenu()
8571// - MenuItemEx() [Internal]
8572// - MenuItem()
8573//-------------------------------------------------------------------------
8574
8575// Helpers for internal use
8576void ImGuiMenuColumns::Update(float spacing, bool window_reappearing)
8577{
8578 if (window_reappearing)
8579 memset(Widths, 0, sizeof(Widths));
8580 Spacing = (ImU16)spacing;
8581 CalcNextTotalWidth(true);
8582 memset(Widths, 0, sizeof(Widths));
8583 TotalWidth = NextTotalWidth;
8584 NextTotalWidth = 0;
8585}
8586
8587void ImGuiMenuColumns::CalcNextTotalWidth(bool update_offsets)
8588{
8589 ImU16 offset = 0;
8590 bool want_spacing = false;
8591 for (int i = 0; i < IM_ARRAYSIZE(Widths); i++)
8592 {
8593 ImU16 width = Widths[i];
8594 if (want_spacing && width > 0)
8595 offset += Spacing;
8596 want_spacing |= (width > 0);
8597 if (update_offsets)
8598 {
8599 if (i == 1) { OffsetLabel = offset; }
8600 if (i == 2) { OffsetShortcut = offset; }
8601 if (i == 3) { OffsetMark = offset; }
8602 }
8603 offset += width;
8604 }
8605 NextTotalWidth = offset;
8606}
8607
8608float ImGuiMenuColumns::DeclColumns(float w_icon, float w_label, float w_shortcut, float w_mark)
8609{
8610 Widths[0] = ImMax(Widths[0], (ImU16)w_icon);
8611 Widths[1] = ImMax(Widths[1], (ImU16)w_label);
8612 Widths[2] = ImMax(Widths[2], (ImU16)w_shortcut);
8613 Widths[3] = ImMax(Widths[3], (ImU16)w_mark);
8614 CalcNextTotalWidth(false);
8615 return (float)ImMax(TotalWidth, NextTotalWidth);
8616}
8617
8618// FIXME: Provided a rectangle perhaps e.g. a BeginMenuBarEx() could be used anywhere..
8619// Currently the main responsibility of this function being to setup clip-rect + horizontal layout + menu navigation layer.
8620// 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.
8621// Then later the same system could be used for multiple menu-bars, scrollbars, side-bars.
8622bool ImGui::BeginMenuBar()
8623{
8624 ImGuiWindow* window = GetCurrentWindow();
8625 if (window->SkipItems)
8626 return false;
8627 if (!(window->Flags & ImGuiWindowFlags_MenuBar))
8628 return false;
8629
8630 IM_ASSERT(!window->DC.MenuBarAppending);
8631 BeginGroup(); // Backup position on layer 0 // FIXME: Misleading to use a group for that backup/restore
8632 PushID("##menubar");
8633
8634 // 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.
8635 // 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.
8636 ImRect bar_rect = window->MenuBarRect();
8637 ImRect clip_rect(ImFloor(bar_rect.Min.x + window->WindowBorderSize), ImFloor(bar_rect.Min.y + window->WindowBorderSize), ImFloor(ImMax(bar_rect.Min.x, bar_rect.Max.x - ImMax(window->WindowRounding, window->WindowBorderSize))), ImFloor(bar_rect.Max.y));
8638 clip_rect.ClipWith(window->OuterRectClipped);
8639 PushClipRect(clip_rect.Min, clip_rect.Max, false);
8640
8641 // 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).
8642 window->DC.CursorPos = window->DC.CursorMaxPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffset.x, bar_rect.Min.y + window->DC.MenuBarOffset.y);
8643 window->DC.LayoutType = ImGuiLayoutType_Horizontal;
8644 window->DC.IsSameLine = false;
8645 window->DC.NavLayerCurrent = ImGuiNavLayer_Menu;
8646 window->DC.MenuBarAppending = true;
8647 AlignTextToFramePadding();
8648 return true;
8649}
8650
8651void ImGui::EndMenuBar()
8652{
8653 ImGuiWindow* window = GetCurrentWindow();
8654 if (window->SkipItems)
8655 return;
8656 ImGuiContext& g = *GImGui;
8657
8658 // Nav: When a move request within one of our child menu failed, capture the request to navigate among our siblings.
8659 if (NavMoveRequestButNoResultYet() && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) && (g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu))
8660 {
8661 // Try to find out if the request is for one of our child menu
8662 ImGuiWindow* nav_earliest_child = g.NavWindow;
8663 while (nav_earliest_child->ParentWindow && (nav_earliest_child->ParentWindow->Flags & ImGuiWindowFlags_ChildMenu))
8664 nav_earliest_child = nav_earliest_child->ParentWindow;
8665 if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0)
8666 {
8667 // To do so we claim focus back, restore NavId and then process the movement request for yet another frame.
8668 // 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)
8669 const ImGuiNavLayer layer = ImGuiNavLayer_Menu;
8670 IM_ASSERT(window->DC.NavLayersActiveMaskNext & (1 << layer)); // Sanity check (FIXME: Seems unnecessary)
8671 FocusWindow(window);
8672 SetNavID(window->NavLastIds[layer], layer, 0, window->NavRectRel[layer]);
8673 // FIXME-NAV: How to deal with this when not using g.IO.ConfigNavCursorVisibleAuto?
8674 if (g.NavCursorVisible)
8675 {
8676 g.NavCursorVisible = false; // Hide nav cursor for the current frame so we don't see the intermediary selection. Will be set again
8677 g.NavCursorHideFrames = 2;
8678 }
8679 g.NavHighlightItemUnderNav = g.NavMousePosDirty = true;
8680 NavMoveRequestForward(g.NavMoveDir, g.NavMoveClipDir, g.NavMoveFlags, g.NavMoveScrollFlags); // Repeat
8681 }
8682 }
8683
8684 IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'"
8685 IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar);
8686 IM_ASSERT(window->DC.MenuBarAppending);
8687 PopClipRect();
8688 PopID();
8689 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.
8690
8691 // FIXME: Extremely confusing, cleanup by (a) working on WorkRect stack system (b) not using a Group confusingly here.
8692 ImGuiGroupData& group_data = g.GroupStack.back();
8693 group_data.EmitItem = false;
8694 ImVec2 restore_cursor_max_pos = group_data.BackupCursorMaxPos;
8695 window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, window->DC.CursorMaxPos.x - window->Scroll.x); // Convert ideal extents for scrolling layer equivalent.
8696 EndGroup(); // Restore position on layer 0 // FIXME: Misleading to use a group for that backup/restore
8697 window->DC.LayoutType = ImGuiLayoutType_Vertical;
8698 window->DC.IsSameLine = false;
8699 window->DC.NavLayerCurrent = ImGuiNavLayer_Main;
8700 window->DC.MenuBarAppending = false;
8701 window->DC.CursorMaxPos = restore_cursor_max_pos;
8702}
8703
8704// Important: calling order matters!
8705// FIXME: Somehow overlapping with docking tech.
8706// FIXME: The "rect-cut" aspect of this could be formalized into a lower-level helper (rect-cut: https://halt.software/dead-simple-layouts)
8707bool ImGui::BeginViewportSideBar(const char* name, ImGuiViewport* viewport_p, ImGuiDir dir, float axis_size, ImGuiWindowFlags window_flags)
8708{
8709 IM_ASSERT(dir != ImGuiDir_None);
8710
8711 ImGuiWindow* bar_window = FindWindowByName(name);
8712 ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)(viewport_p ? viewport_p : GetMainViewport());
8713 if (bar_window == NULL || bar_window->BeginCount == 0)
8714 {
8715 // Calculate and set window size/position
8716 ImRect avail_rect = viewport->GetBuildWorkRect();
8717 ImGuiAxis axis = (dir == ImGuiDir_Up || dir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X;
8718 ImVec2 pos = avail_rect.Min;
8719 if (dir == ImGuiDir_Right || dir == ImGuiDir_Down)
8720 pos[axis] = avail_rect.Max[axis] - axis_size;
8721 ImVec2 size = avail_rect.GetSize();
8722 size[axis] = axis_size;
8723 SetNextWindowPos(pos);
8724 SetNextWindowSize(size);
8725
8726 // Report our size into work area (for next frame) using actual window size
8727 if (dir == ImGuiDir_Up || dir == ImGuiDir_Left)
8728 viewport->BuildWorkInsetMin[axis] += axis_size;
8729 else if (dir == ImGuiDir_Down || dir == ImGuiDir_Right)
8730 viewport->BuildWorkInsetMax[axis] += axis_size;
8731 }
8732
8733 window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking;
8734 SetNextWindowViewport(viewport->ID); // Enforce viewport so we don't create our own viewport when ImGuiConfigFlags_ViewportsNoMerge is set.
8735 PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
8736 PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0)); // Lift normal size constraint
8737 bool is_open = Begin(name, NULL, window_flags);
8738 PopStyleVar(2);
8739
8740 return is_open;
8741}
8742
8743bool ImGui::BeginMainMenuBar()
8744{
8745 ImGuiContext& g = *GImGui;
8746 ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport();
8747
8748 // Notify of viewport change so GetFrameHeight() can be accurate in case of DPI change
8749 SetCurrentViewport(NULL, viewport);
8750
8751 // For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set.
8752 // FIXME: This could be generalized as an opt-in way to clamp window->DC.CursorStartPos to avoid SafeArea?
8753 // FIXME: Consider removing support for safe area down the line... it's messy. Nowadays consoles have support for TV calibration in OS settings.
8754 g.NextWindowData.MenuBarOffsetMinVal = ImVec2(g.Style.DisplaySafeAreaPadding.x, ImMax(g.Style.DisplaySafeAreaPadding.y - g.Style.FramePadding.y, 0.0f));
8755 ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar;
8756 float height = GetFrameHeight();
8757 bool is_open = BeginViewportSideBar("##MainMenuBar", viewport, ImGuiDir_Up, height, window_flags);
8758 g.NextWindowData.MenuBarOffsetMinVal = ImVec2(0.0f, 0.0f);
8759
8760 if (is_open)
8761 BeginMenuBar();
8762 else
8763 End();
8764 return is_open;
8765}
8766
8767void ImGui::EndMainMenuBar()
8768{
8769 EndMenuBar();
8770
8771 // When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window
8772 // FIXME: With this strategy we won't be able to restore a NULL focus.
8773 ImGuiContext& g = *GImGui;
8774 if (g.CurrentWindow == g.NavWindow && g.NavLayer == ImGuiNavLayer_Main && !g.NavAnyRequest)
8775 FocusTopMostWindowUnderOne(g.NavWindow, NULL, NULL, ImGuiFocusRequestFlags_UnlessBelowModal | ImGuiFocusRequestFlags_RestoreFocusedChild);
8776
8777 End();
8778}
8779
8780static bool IsRootOfOpenMenuSet()
8781{
8782 ImGuiContext& g = *GImGui;
8783 ImGuiWindow* window = g.CurrentWindow;
8784 if ((g.OpenPopupStack.Size <= g.BeginPopupStack.Size) || (window->Flags & ImGuiWindowFlags_ChildMenu))
8785 return false;
8786
8787 // Initially we used 'upper_popup->OpenParentId == window->IDStack.back()' to differentiate multiple menu sets from each others
8788 // (e.g. inside menu bar vs loose menu items) based on parent ID.
8789 // This would however prevent the use of e.g. PushID() user code submitting menus.
8790 // Previously this worked between popup and a first child menu because the first child menu always had the _ChildWindow flag,
8791 // making hovering on parent popup possible while first child menu was focused - but this was generally a bug with other side effects.
8792 // Instead we don't treat Popup specifically (in order to consistently support menu features in them), maybe the first child menu of a Popup
8793 // doesn't have the _ChildWindow flag, and we rely on this IsRootOfOpenMenuSet() check to allow hovering between root window/popup and first child menu.
8794 // 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.
8795 // 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
8796 // 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
8797 // it likely won't be a problem anyone runs into.
8798 const ImGuiPopupData* upper_popup = &g.OpenPopupStack[g.BeginPopupStack.Size];
8799 if (window->DC.NavLayerCurrent != upper_popup->ParentNavLayer)
8800 return false;
8801 return upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu) && ImGui::IsWindowChildOf(upper_popup->Window, window, true, false);
8802}
8803
8804bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
8805{
8806 ImGuiWindow* window = GetCurrentWindow();
8807 if (window->SkipItems)
8808 return false;
8809
8810 ImGuiContext& g = *GImGui;
8811 const ImGuiStyle& style = g.Style;
8812 const ImGuiID id = window->GetID(label);
8813 bool menu_is_open = IsPopupOpen(id, ImGuiPopupFlags_None);
8814
8815 // 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)
8816 // 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.
8817 ImGuiWindowFlags window_flags = ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus;
8818 if (window->Flags & ImGuiWindowFlags_ChildMenu)
8819 window_flags |= ImGuiWindowFlags_ChildWindow;
8820
8821 // If a menu with same the ID was already submitted, we will append to it, matching the behavior of Begin().
8822 // 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.
8823 // If somehow this is ever becoming a problem we can switch to use e.g. ImGuiStorage mapping key to last frame used.
8824 if (g.MenusIdSubmittedThisFrame.contains(id))
8825 {
8826 if (menu_is_open)
8827 menu_is_open = BeginPopupEx(id, window_flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display)
8828 else
8829 g.NextWindowData.ClearFlags(); // we behave like Begin() and need to consume those values
8830 return menu_is_open;
8831 }
8832
8833 // Tag menu as used. Next time BeginMenu() with same ID is called it will append to existing menu
8834 g.MenusIdSubmittedThisFrame.push_back(id);
8835
8836 ImVec2 label_size = CalcTextSize(label, NULL, true);
8837
8838 // 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)
8839 // This is only done for items for the menu set and not the full parent window.
8840 const bool menuset_is_open = IsRootOfOpenMenuSet();
8841 if (menuset_is_open)
8842 PushItemFlag(ImGuiItemFlags_NoWindowHoverableCheck, true);
8843
8844 // The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu,
8845 // However the final position is going to be different! It is chosen by FindBestWindowPosForPopup().
8846 // e.g. Menus tend to overlap each other horizontally to amplify relative Z-ordering.
8847 ImVec2 popup_pos, pos = window->DC.CursorPos;
8848 PushID(label);
8849 if (!enabled)
8850 BeginDisabled();
8851 const ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
8852 bool pressed;
8853
8854 // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
8855 const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SelectOnClick | ImGuiSelectableFlags_NoAutoClosePopups;
8856 if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
8857 {
8858 // Menu inside an horizontal menu bar
8859 // Selectable extend their highlight by half ItemSpacing in each direction.
8860 // For ChildMenu, the popup position will be overwritten by the call to FindBestWindowPosForPopup() in Begin()
8861 popup_pos = ImVec2(pos.x - 1.0f - IM_TRUNC(style.ItemSpacing.x * 0.5f), pos.y - style.FramePadding.y + window->MenuBarHeight);
8862 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f);
8863 PushStyleVarX(ImGuiStyleVar_ItemSpacing, style.ItemSpacing.x * 2.0f);
8864 float w = label_size.x;
8865 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
8866 pressed = Selectable("", menu_is_open, selectable_flags, ImVec2(w, label_size.y));
8867 LogSetNextTextDecoration("[", "]");
8868 RenderText(text_pos, label);
8869 PopStyleVar();
8870 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().
8871 }
8872 else
8873 {
8874 // Menu inside a regular/vertical menu
8875 // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
8876 // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.
8877 popup_pos = ImVec2(pos.x, pos.y - style.WindowPadding.y);
8878 float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f;
8879 float checkmark_w = IM_TRUNC(g.FontSize * 1.20f);
8880 float min_w = window->DC.MenuColumns.DeclColumns(icon_w, label_size.x, 0.0f, checkmark_w); // Feedback to next frame
8881 float extra_w = ImMax(0.0f, GetContentRegionAvail().x - min_w);
8882 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
8883 pressed = Selectable("", menu_is_open, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y));
8884 LogSetNextTextDecoration("", ">");
8885 RenderText(text_pos, label);
8886 if (icon_w > 0.0f)
8887 RenderText(pos + ImVec2(offsets->OffsetIcon, 0.0f), icon);
8888 RenderArrow(window->DrawList, pos + ImVec2(offsets->OffsetMark + extra_w + g.FontSize * 0.30f, 0.0f), GetColorU32(ImGuiCol_Text), ImGuiDir_Right);
8889 }
8890 if (!enabled)
8891 EndDisabled();
8892
8893 const bool hovered = (g.HoveredId == id) && enabled && !g.NavHighlightItemUnderNav;
8894 if (menuset_is_open)
8895 PopItemFlag();
8896
8897 bool want_open = false;
8898 bool want_open_nav_init = false;
8899 bool want_close = false;
8900 if (window->DC.LayoutType == ImGuiLayoutType_Vertical) // (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu))
8901 {
8902 // Close menu when not hovering it anymore unless we are moving roughly in the direction of the menu
8903 // Implement http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown to avoid using timers, so menus feels more reactive.
8904 bool moving_toward_child_menu = false;
8905 ImGuiPopupData* child_popup = (g.BeginPopupStack.Size < g.OpenPopupStack.Size) ? &g.OpenPopupStack[g.BeginPopupStack.Size] : NULL; // Popup candidate (testing below)
8906 ImGuiWindow* child_menu_window = (child_popup && child_popup->Window && child_popup->Window->ParentWindow == window) ? child_popup->Window : NULL;
8907 if (g.HoveredWindow == window && child_menu_window != NULL)
8908 {
8909 const float ref_unit = g.FontSize; // FIXME-DPI
8910 const float child_dir = (window->Pos.x < child_menu_window->Pos.x) ? 1.0f : -1.0f;
8911 const ImRect next_window_rect = child_menu_window->Rect();
8912 ImVec2 ta = (g.IO.MousePos - g.IO.MouseDelta);
8913 ImVec2 tb = (child_dir > 0.0f) ? next_window_rect.GetTL() : next_window_rect.GetTR();
8914 ImVec2 tc = (child_dir > 0.0f) ? next_window_rect.GetBL() : next_window_rect.GetBR();
8915 const float pad_farmost_h = ImClamp(ImFabs(ta.x - tb.x) * 0.30f, ref_unit * 0.5f, ref_unit * 2.5f); // Add a bit of extra slack.
8916 ta.x += child_dir * -0.5f;
8917 tb.x += child_dir * ref_unit;
8918 tc.x += child_dir * ref_unit;
8919 tb.y = ta.y + ImMax((tb.y - pad_farmost_h) - ta.y, -ref_unit * 8.0f); // Triangle has maximum height to limit the slope and the bias toward large sub-menus
8920 tc.y = ta.y + ImMin((tc.y + pad_farmost_h) - ta.y, +ref_unit * 8.0f);
8921 moving_toward_child_menu = ImTriangleContainsPoint(ta, tb, tc, g.IO.MousePos);
8922 //GetForegroundDrawList()->AddTriangleFilled(ta, tb, tc, moving_toward_child_menu ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); // [DEBUG]
8923 }
8924
8925 // 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)
8926 // 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.
8927 // (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.)
8928 if (menu_is_open && !hovered && g.HoveredWindow == window && !moving_toward_child_menu && !g.NavHighlightItemUnderNav && g.ActiveId == 0)
8929 want_close = true;
8930
8931 // Open
8932 // (note: at this point 'hovered' actually includes the NavDisableMouseHover == false test)
8933 if (!menu_is_open && pressed) // Click/activate to open
8934 want_open = true;
8935 else if (!menu_is_open && hovered && !moving_toward_child_menu) // Hover to open
8936 want_open = true;
8937 else if (!menu_is_open && hovered && g.HoveredIdTimer >= 0.30f && g.MouseStationaryTimer >= 0.30f) // Hover to open (timer fallback)
8938 want_open = true;
8939 if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open
8940 {
8941 want_open = want_open_nav_init = true;
8942 NavMoveRequestCancel();
8943 SetNavCursorVisibleAfterMove();
8944 }
8945 }
8946 else
8947 {
8948 // Menu bar
8949 if (menu_is_open && pressed && menuset_is_open) // Click an open menu again to close it
8950 {
8951 want_close = true;
8952 want_open = menu_is_open = false;
8953 }
8954 else if (pressed || (hovered && menuset_is_open && !menu_is_open)) // First click to open, then hover to open others
8955 {
8956 want_open = true;
8957 }
8958 else if (g.NavId == id && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open
8959 {
8960 want_open = true;
8961 NavMoveRequestCancel();
8962 }
8963 }
8964
8965 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.. }'
8966 want_close = true;
8967 if (want_close && IsPopupOpen(id, ImGuiPopupFlags_None))
8968 ClosePopupToLevel(g.BeginPopupStack.Size, true);
8969
8970 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Openable | (menu_is_open ? ImGuiItemStatusFlags_Opened : 0));
8971 PopID();
8972
8973 if (want_open && !menu_is_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size)
8974 {
8975 // 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.
8976 OpenPopup(label);
8977 }
8978 else if (want_open)
8979 {
8980 menu_is_open = true;
8981 OpenPopup(label, ImGuiPopupFlags_NoReopen);// | (want_open_nav_init ? ImGuiPopupFlags_NoReopenAlwaysNavInit : 0));
8982 }
8983
8984 if (menu_is_open)
8985 {
8986 ImGuiLastItemData last_item_in_parent = g.LastItemData;
8987 SetNextWindowPos(popup_pos, ImGuiCond_Always); // Note: misleading: the value will serve as reference for FindBestWindowPosForPopup(), not actual pos.
8988 PushStyleVar(ImGuiStyleVar_ChildRounding, style.PopupRounding); // First level will use _PopupRounding, subsequent will use _ChildRounding
8989 menu_is_open = BeginPopupEx(id, window_flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display)
8990 PopStyleVar();
8991 if (menu_is_open)
8992 {
8993 // Implement what ImGuiPopupFlags_NoReopenAlwaysNavInit would do:
8994 // Perform an init request in the case the popup was already open (via a previous mouse hover)
8995 if (want_open && want_open_nav_init && !g.NavInitRequest)
8996 {
8997 FocusWindow(g.CurrentWindow, ImGuiFocusRequestFlags_UnlessBelowModal);
8998 NavInitWindow(g.CurrentWindow, false);
8999 }
9000
9001 // Restore LastItemData so IsItemXXXX functions can work after BeginMenu()/EndMenu()
9002 // (This fixes using IsItemClicked() and IsItemHovered(), but IsItemHovered() also relies on its support for ImGuiItemFlags_NoWindowHoverableCheck)
9003 g.LastItemData = last_item_in_parent;
9004 if (g.HoveredWindow == window)
9005 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow;
9006 }
9007 }
9008 else
9009 {
9010 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
9011 }
9012
9013 return menu_is_open;
9014}
9015
9016bool ImGui::BeginMenu(const char* label, bool enabled)
9017{
9018 return BeginMenuEx(label, NULL, enabled);
9019}
9020
9021void ImGui::EndMenu()
9022{
9023 // Nav: When a left move request our menu failed, close ourselves.
9024 ImGuiContext& g = *GImGui;
9025 ImGuiWindow* window = g.CurrentWindow;
9026 IM_ASSERT(window->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginMenu()/EndMenu() calls
9027 ImGuiWindow* parent_window = window->ParentWindow; // Should always be != NULL is we passed assert.
9028 if (window->BeginCount == window->BeginCountPreviousFrame)
9029 if (g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet())
9030 if (g.NavWindow && (g.NavWindow->RootWindowForNav == window) && parent_window->DC.LayoutType == ImGuiLayoutType_Vertical)
9031 {
9032 ClosePopupToLevel(g.BeginPopupStack.Size - 1, true);
9033 NavMoveRequestCancel();
9034 }
9035
9036 EndPopup();
9037}
9038
9039bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut, bool selected, bool enabled)
9040{
9041 ImGuiWindow* window = GetCurrentWindow();
9042 if (window->SkipItems)
9043 return false;
9044
9045 ImGuiContext& g = *GImGui;
9046 ImGuiStyle& style = g.Style;
9047 ImVec2 pos = window->DC.CursorPos;
9048 ImVec2 label_size = CalcTextSize(label, NULL, true);
9049
9050 // See BeginMenuEx() for comments about this.
9051 const bool menuset_is_open = IsRootOfOpenMenuSet();
9052 if (menuset_is_open)
9053 PushItemFlag(ImGuiItemFlags_NoWindowHoverableCheck, true);
9054
9055 // We've been using the equivalent of ImGuiSelectableFlags_SetNavIdOnHover on all Selectable() since early Nav system days (commit 43ee5d73),
9056 // 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.
9057 bool pressed;
9058 PushID(label);
9059 if (!enabled)
9060 BeginDisabled();
9061
9062 // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
9063 const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_SelectOnRelease | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SetNavIdOnHover;
9064 const ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
9065 if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
9066 {
9067 // Mimic the exact layout spacing of BeginMenu() to allow MenuItem() inside a menu bar, which is a little misleading but may be useful
9068 // Note that in this situation: we don't render the shortcut, we render a highlight instead of the selected tick mark.
9069 float w = label_size.x;
9070 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f);
9071 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
9072 PushStyleVarX(ImGuiStyleVar_ItemSpacing, style.ItemSpacing.x * 2.0f);
9073 pressed = Selectable("", selected, selectable_flags, ImVec2(w, 0.0f));
9074 PopStyleVar();
9075 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
9076 RenderText(text_pos, label);
9077 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().
9078 }
9079 else
9080 {
9081 // Menu item inside a vertical menu
9082 // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
9083 // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.
9084 float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f;
9085 float shortcut_w = (shortcut && shortcut[0]) ? CalcTextSize(shortcut, NULL).x : 0.0f;
9086 float checkmark_w = IM_TRUNC(g.FontSize * 1.20f);
9087 float min_w = window->DC.MenuColumns.DeclColumns(icon_w, label_size.x, shortcut_w, checkmark_w); // Feedback for next frame
9088 float stretch_w = ImMax(0.0f, GetContentRegionAvail().x - min_w);
9089 pressed = Selectable("", false, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y));
9090 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
9091 {
9092 RenderText(pos + ImVec2(offsets->OffsetLabel, 0.0f), label);
9093 if (icon_w > 0.0f)
9094 RenderText(pos + ImVec2(offsets->OffsetIcon, 0.0f), icon);
9095 if (shortcut_w > 0.0f)
9096 {
9097 PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]);
9098 LogSetNextTextDecoration("(", ")");
9099 RenderText(pos + ImVec2(offsets->OffsetShortcut + stretch_w, 0.0f), shortcut, NULL, false);
9100 PopStyleColor();
9101 }
9102 if (selected)
9103 RenderCheckMark(window->DrawList, pos + ImVec2(offsets->OffsetMark + stretch_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), GetColorU32(ImGuiCol_Text), g.FontSize * 0.866f);
9104 }
9105 }
9106 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0));
9107 if (!enabled)
9108 EndDisabled();
9109 PopID();
9110 if (menuset_is_open)
9111 PopItemFlag();
9112
9113 return pressed;
9114}
9115
9116bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled)
9117{
9118 return MenuItemEx(label, NULL, shortcut, selected, enabled);
9119}
9120
9121bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled)
9122{
9123 if (MenuItemEx(label, NULL, shortcut, p_selected ? *p_selected : false, enabled))
9124 {
9125 if (p_selected)
9126 *p_selected = !*p_selected;
9127 return true;
9128 }
9129 return false;
9130}
9131
9132//-------------------------------------------------------------------------
9133// [SECTION] Widgets: BeginTabBar, EndTabBar, etc.
9134//-------------------------------------------------------------------------
9135// - BeginTabBar()
9136// - BeginTabBarEx() [Internal]
9137// - EndTabBar()
9138// - TabBarLayout() [Internal]
9139// - TabBarCalcTabID() [Internal]
9140// - TabBarCalcMaxTabWidth() [Internal]
9141// - TabBarFindTabById() [Internal]
9142// - TabBarFindTabByOrder() [Internal]
9143// - TabBarFindMostRecentlySelectedTabForActiveWindow() [Internal]
9144// - TabBarGetCurrentTab() [Internal]
9145// - TabBarGetTabName() [Internal]
9146// - TabBarAddTab() [Internal]
9147// - TabBarRemoveTab() [Internal]
9148// - TabBarCloseTab() [Internal]
9149// - TabBarScrollClamp() [Internal]
9150// - TabBarScrollToTab() [Internal]
9151// - TabBarQueueFocus() [Internal]
9152// - TabBarQueueReorder() [Internal]
9153// - TabBarProcessReorderFromMousePos() [Internal]
9154// - TabBarProcessReorder() [Internal]
9155// - TabBarScrollingButtons() [Internal]
9156// - TabBarTabListPopupButton() [Internal]
9157//-------------------------------------------------------------------------
9158
9160{
9161 int TabCount; // Number of tabs in this section.
9162 float Width; // Sum of width of tabs in this section (after shrinking down)
9163 float Spacing; // Horizontal spacing at the end of the section.
9164
9165 ImGuiTabBarSection() { memset(this, 0, sizeof(*this)); }
9166};
9167
9168namespace ImGui
9169{
9170 static void TabBarLayout(ImGuiTabBar* tab_bar);
9171 static ImU32 TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window);
9172 static float TabBarCalcMaxTabWidth();
9173 static float TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling);
9174 static void TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections);
9175 static ImGuiTabItem* TabBarScrollingButtons(ImGuiTabBar* tab_bar);
9176 static ImGuiTabItem* TabBarTabListPopupButton(ImGuiTabBar* tab_bar);
9177}
9178
9179ImGuiTabBar::ImGuiTabBar()
9180{
9181 memset(this, 0, sizeof(*this));
9182 CurrFrameVisible = PrevFrameVisible = -1;
9183 LastTabItemIdx = -1;
9184}
9185
9186static inline int TabItemGetSectionIdx(const ImGuiTabItem* tab)
9187{
9188 return (tab->Flags & ImGuiTabItemFlags_Leading) ? 0 : (tab->Flags & ImGuiTabItemFlags_Trailing) ? 2 : 1;
9189}
9190
9191static int IMGUI_CDECL TabItemComparerBySection(const void* lhs, const void* rhs)
9192{
9193 const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
9194 const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
9195 const int a_section = TabItemGetSectionIdx(a);
9196 const int b_section = TabItemGetSectionIdx(b);
9197 if (a_section != b_section)
9198 return a_section - b_section;
9199 return (int)(a->IndexDuringLayout - b->IndexDuringLayout);
9200}
9201
9202static int IMGUI_CDECL TabItemComparerByBeginOrder(const void* lhs, const void* rhs)
9203{
9204 const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
9205 const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
9206 return (int)(a->BeginOrder - b->BeginOrder);
9207}
9208
9209static ImGuiTabBar* GetTabBarFromTabBarRef(const ImGuiPtrOrIndex& ref)
9210{
9211 ImGuiContext& g = *GImGui;
9212 return ref.Ptr ? (ImGuiTabBar*)ref.Ptr : g.TabBars.GetByIndex(ref.Index);
9213}
9214
9215static ImGuiPtrOrIndex GetTabBarRefFromTabBar(ImGuiTabBar* tab_bar)
9216{
9217 ImGuiContext& g = *GImGui;
9218 if (g.TabBars.Contains(tab_bar))
9219 return ImGuiPtrOrIndex(g.TabBars.GetIndex(tab_bar));
9220 return ImGuiPtrOrIndex(tab_bar);
9221}
9222
9223bool ImGui::BeginTabBar(const char* str_id, ImGuiTabBarFlags flags)
9224{
9225 ImGuiContext& g = *GImGui;
9226 ImGuiWindow* window = g.CurrentWindow;
9227 if (window->SkipItems)
9228 return false;
9229
9230 ImGuiID id = window->GetID(str_id);
9231 ImGuiTabBar* tab_bar = g.TabBars.GetOrAddByKey(id);
9232 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);
9233 tab_bar->ID = id;
9234 tab_bar->SeparatorMinX = tab_bar->BarRect.Min.x - IM_TRUNC(window->WindowPadding.x * 0.5f);
9235 tab_bar->SeparatorMaxX = tab_bar->BarRect.Max.x + IM_TRUNC(window->WindowPadding.x * 0.5f);
9236 //if (g.NavWindow && IsWindowChildOf(g.NavWindow, window, false, false))
9237 flags |= ImGuiTabBarFlags_IsFocused;
9238 return BeginTabBarEx(tab_bar, tab_bar_bb, flags);
9239}
9240
9241bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImGuiTabBarFlags flags)
9242{
9243 ImGuiContext& g = *GImGui;
9244 ImGuiWindow* window = g.CurrentWindow;
9245 if (window->SkipItems)
9246 return false;
9247
9248 IM_ASSERT(tab_bar->ID != 0);
9249 if ((flags & ImGuiTabBarFlags_DockNode) == 0)
9250 PushOverrideID(tab_bar->ID);
9251
9252 // Add to stack
9253 g.CurrentTabBarStack.push_back(GetTabBarRefFromTabBar(tab_bar));
9254 g.CurrentTabBar = tab_bar;
9255 tab_bar->Window = window;
9256
9257 // Append with multiple BeginTabBar()/EndTabBar() pairs.
9258 tab_bar->BackupCursorPos = window->DC.CursorPos;
9259 if (tab_bar->CurrFrameVisible == g.FrameCount)
9260 {
9261 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
9262 tab_bar->BeginCount++;
9263 return true;
9264 }
9265
9266 // Ensure correct ordering when toggling ImGuiTabBarFlags_Reorderable flag, or when a new tab was added while being not reorderable
9267 if ((flags & ImGuiTabBarFlags_Reorderable) != (tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (tab_bar->TabsAddedNew && !(flags & ImGuiTabBarFlags_Reorderable)))
9268 if ((flags & ImGuiTabBarFlags_DockNode) == 0) // FIXME: TabBar with DockNode can now be hybrid
9269 ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerByBeginOrder);
9270 tab_bar->TabsAddedNew = false;
9271
9272 // Flags
9273 if ((flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0)
9274 flags |= ImGuiTabBarFlags_FittingPolicyDefault_;
9275
9276 tab_bar->Flags = flags;
9277 tab_bar->BarRect = tab_bar_bb;
9278 tab_bar->WantLayout = true; // Layout will be done on the first call to ItemTab()
9279 tab_bar->PrevFrameVisible = tab_bar->CurrFrameVisible;
9280 tab_bar->CurrFrameVisible = g.FrameCount;
9281 tab_bar->PrevTabsContentsHeight = tab_bar->CurrTabsContentsHeight;
9282 tab_bar->CurrTabsContentsHeight = 0.0f;
9283 tab_bar->ItemSpacingY = g.Style.ItemSpacing.y;
9284 tab_bar->FramePadding = g.Style.FramePadding;
9285 tab_bar->TabsActiveCount = 0;
9286 tab_bar->LastTabItemIdx = -1;
9287 tab_bar->BeginCount = 1;
9288
9289 // Set cursor pos in a way which only be used in the off-chance the user erroneously submits item before BeginTabItem(): items will overlap
9290 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
9291
9292 // Draw separator
9293 // (it would be misleading to draw this in EndTabBar() suggesting that it may be drawn over tabs, as tab bar are appendable)
9294 const ImU32 col = GetColorU32((flags & ImGuiTabBarFlags_IsFocused) ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected);
9295 if (g.Style.TabBarBorderSize > 0.0f)
9296 {
9297 const float y = tab_bar->BarRect.Max.y;
9298 window->DrawList->AddRectFilled(ImVec2(tab_bar->SeparatorMinX, y - g.Style.TabBarBorderSize), ImVec2(tab_bar->SeparatorMaxX, y), col);
9299 }
9300 return true;
9301}
9302
9303void ImGui::EndTabBar()
9304{
9305 ImGuiContext& g = *GImGui;
9306 ImGuiWindow* window = g.CurrentWindow;
9307 if (window->SkipItems)
9308 return;
9309
9310 ImGuiTabBar* tab_bar = g.CurrentTabBar;
9311 if (tab_bar == NULL)
9312 {
9313 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Mismatched BeginTabBar()/EndTabBar()!");
9314 return;
9315 }
9316
9317 // Fallback in case no TabItem have been submitted
9318 if (tab_bar->WantLayout)
9319 TabBarLayout(tab_bar);
9320
9321 // Restore the last visible height if no tab is visible, this reduce vertical flicker/movement when a tabs gets removed without calling SetTabItemClosed().
9322 const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
9323 if (tab_bar->VisibleTabWasSubmitted || tab_bar->VisibleTabId == 0 || tab_bar_appearing)
9324 {
9325 tab_bar->CurrTabsContentsHeight = ImMax(window->DC.CursorPos.y - tab_bar->BarRect.Max.y, tab_bar->CurrTabsContentsHeight);
9326 window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->CurrTabsContentsHeight;
9327 }
9328 else
9329 {
9330 window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->PrevTabsContentsHeight;
9331 }
9332 if (tab_bar->BeginCount > 1)
9333 window->DC.CursorPos = tab_bar->BackupCursorPos;
9334
9335 tab_bar->LastTabItemIdx = -1;
9336 if ((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0)
9337 PopID();
9338
9339 g.CurrentTabBarStack.pop_back();
9340 g.CurrentTabBar = g.CurrentTabBarStack.empty() ? NULL : GetTabBarFromTabBarRef(g.CurrentTabBarStack.back());
9341}
9342
9343// Scrolling happens only in the central section (leading/trailing sections are not scrolling)
9344static float TabBarCalcScrollableWidth(ImGuiTabBar* tab_bar, ImGuiTabBarSection* sections)
9345{
9346 return tab_bar->BarRect.GetWidth() - sections[0].Width - sections[2].Width - sections[1].Spacing;
9347}
9348
9349// This is called only once a frame before by the first call to ItemTab()
9350// The reason we're not calling it in BeginTabBar() is to leave a chance to the user to call the SetTabItemClosed() functions.
9351static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar)
9352{
9353 ImGuiContext& g = *GImGui;
9354 tab_bar->WantLayout = false;
9355
9356 // Garbage collect by compacting list
9357 // Detect if we need to sort out tab list (e.g. in rare case where a tab changed section)
9358 int tab_dst_n = 0;
9359 bool need_sort_by_section = false;
9360 ImGuiTabBarSection sections[3]; // Layout sections: Leading, Central, Trailing
9361 for (int tab_src_n = 0; tab_src_n < tab_bar->Tabs.Size; tab_src_n++)
9362 {
9363 ImGuiTabItem* tab = &tab_bar->Tabs[tab_src_n];
9364 if (tab->LastFrameVisible < tab_bar->PrevFrameVisible || tab->WantClose)
9365 {
9366 // Remove tab
9367 if (tab_bar->VisibleTabId == tab->ID) { tab_bar->VisibleTabId = 0; }
9368 if (tab_bar->SelectedTabId == tab->ID) { tab_bar->SelectedTabId = 0; }
9369 if (tab_bar->NextSelectedTabId == tab->ID) { tab_bar->NextSelectedTabId = 0; }
9370 continue;
9371 }
9372 if (tab_dst_n != tab_src_n)
9373 tab_bar->Tabs[tab_dst_n] = tab_bar->Tabs[tab_src_n];
9374
9375 tab = &tab_bar->Tabs[tab_dst_n];
9376 tab->IndexDuringLayout = (ImS16)tab_dst_n;
9377
9378 // We will need sorting if tabs have changed section (e.g. moved from one of Leading/Central/Trailing to another)
9379 int curr_tab_section_n = TabItemGetSectionIdx(tab);
9380 if (tab_dst_n > 0)
9381 {
9382 ImGuiTabItem* prev_tab = &tab_bar->Tabs[tab_dst_n - 1];
9383 int prev_tab_section_n = TabItemGetSectionIdx(prev_tab);
9384 if (curr_tab_section_n == 0 && prev_tab_section_n != 0)
9385 need_sort_by_section = true;
9386 if (prev_tab_section_n == 2 && curr_tab_section_n != 2)
9387 need_sort_by_section = true;
9388 }
9389
9390 sections[curr_tab_section_n].TabCount++;
9391 tab_dst_n++;
9392 }
9393 if (tab_bar->Tabs.Size != tab_dst_n)
9394 tab_bar->Tabs.resize(tab_dst_n);
9395
9396 if (need_sort_by_section)
9397 ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerBySection);
9398
9399 // Calculate spacing between sections
9400 sections[0].Spacing = sections[0].TabCount > 0 && (sections[1].TabCount + sections[2].TabCount) > 0 ? g.Style.ItemInnerSpacing.x : 0.0f;
9401 sections[1].Spacing = sections[1].TabCount > 0 && sections[2].TabCount > 0 ? g.Style.ItemInnerSpacing.x : 0.0f;
9402
9403 // Setup next selected tab
9404 ImGuiID scroll_to_tab_id = 0;
9405 if (tab_bar->NextSelectedTabId)
9406 {
9407 tab_bar->SelectedTabId = tab_bar->NextSelectedTabId;
9408 tab_bar->NextSelectedTabId = 0;
9409 scroll_to_tab_id = tab_bar->SelectedTabId;
9410 }
9411
9412 // Process order change request (we could probably process it when requested but it's just saner to do it in a single spot).
9413 if (tab_bar->ReorderRequestTabId != 0)
9414 {
9415 if (TabBarProcessReorder(tab_bar))
9416 if (tab_bar->ReorderRequestTabId == tab_bar->SelectedTabId)
9417 scroll_to_tab_id = tab_bar->ReorderRequestTabId;
9418 tab_bar->ReorderRequestTabId = 0;
9419 }
9420
9421 // Tab List Popup (will alter tab_bar->BarRect and therefore the available width!)
9422 const bool tab_list_popup_button = (tab_bar->Flags & ImGuiTabBarFlags_TabListPopupButton) != 0;
9423 if (tab_list_popup_button)
9424 if (ImGuiTabItem* tab_to_select = TabBarTabListPopupButton(tab_bar)) // NB: Will alter BarRect.Min.x!
9425 scroll_to_tab_id = tab_bar->SelectedTabId = tab_to_select->ID;
9426
9427 // Leading/Trailing tabs will be shrink only if central one aren't visible anymore, so layout the shrink data as: leading, trailing, central
9428 // (whereas our tabs are stored as: leading, central, trailing)
9429 int shrink_buffer_indexes[3] = { 0, sections[0].TabCount + sections[2].TabCount, sections[0].TabCount };
9430 g.ShrinkWidthBuffer.resize(tab_bar->Tabs.Size);
9431
9432 // Compute ideal tabs widths + store them into shrink buffer
9433 ImGuiTabItem* most_recently_selected_tab = NULL;
9434 int curr_section_n = -1;
9435 bool found_selected_tab_id = false;
9436 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
9437 {
9438 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
9439 IM_ASSERT(tab->LastFrameVisible >= tab_bar->PrevFrameVisible);
9440
9441 if ((most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) && !(tab->Flags & ImGuiTabItemFlags_Button))
9442 most_recently_selected_tab = tab;
9443 if (tab->ID == tab_bar->SelectedTabId)
9444 found_selected_tab_id = true;
9445 if (scroll_to_tab_id == 0 && g.NavJustMovedToId == tab->ID)
9446 scroll_to_tab_id = tab->ID;
9447
9448 // Refresh tab width immediately, otherwise changes of style e.g. style.FramePadding.x would noticeably lag in the tab bar.
9449 // Additionally, when using TabBarAddTab() to manipulate tab bar order we occasionally insert new tabs that don't have a width yet,
9450 // and we cannot wait for the next BeginTabItem() call. We cannot compute this width within TabBarAddTab() because font size depends on the active window.
9451 const char* tab_name = TabBarGetTabName(tab_bar, tab);
9452 const bool has_close_button_or_unsaved_marker = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) == 0 || (tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
9453 tab->ContentWidth = (tab->RequestedWidth >= 0.0f) ? tab->RequestedWidth : TabItemCalcSize(tab_name, has_close_button_or_unsaved_marker).x;
9454
9455 int section_n = TabItemGetSectionIdx(tab);
9456 ImGuiTabBarSection* section = &sections[section_n];
9457 section->Width += tab->ContentWidth + (section_n == curr_section_n ? g.Style.ItemInnerSpacing.x : 0.0f);
9458 curr_section_n = section_n;
9459
9460 // Store data so we can build an array sorted by width if we need to shrink tabs down
9461 IM_MSVC_WARNING_SUPPRESS(6385);
9462 ImGuiShrinkWidthItem* shrink_width_item = &g.ShrinkWidthBuffer[shrink_buffer_indexes[section_n]++];
9463 shrink_width_item->Index = tab_n;
9464 shrink_width_item->Width = shrink_width_item->InitialWidth = tab->ContentWidth;
9465 tab->Width = ImMax(tab->ContentWidth, 1.0f);
9466 }
9467
9468 // Compute total ideal width (used for e.g. auto-resizing a window)
9469 tab_bar->WidthAllTabsIdeal = 0.0f;
9470 for (int section_n = 0; section_n < 3; section_n++)
9471 tab_bar->WidthAllTabsIdeal += sections[section_n].Width + sections[section_n].Spacing;
9472
9473 // Horizontal scrolling buttons
9474 // (note that TabBarScrollButtons() will alter BarRect.Max.x)
9475 if ((tab_bar->WidthAllTabsIdeal > tab_bar->BarRect.GetWidth() && tab_bar->Tabs.Size > 1) && !(tab_bar->Flags & ImGuiTabBarFlags_NoTabListScrollingButtons) && (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll))
9476 if (ImGuiTabItem* scroll_and_select_tab = TabBarScrollingButtons(tab_bar))
9477 {
9478 scroll_to_tab_id = scroll_and_select_tab->ID;
9479 if ((scroll_and_select_tab->Flags & ImGuiTabItemFlags_Button) == 0)
9480 tab_bar->SelectedTabId = scroll_to_tab_id;
9481 }
9482
9483 // Shrink widths if full tabs don't fit in their allocated space
9484 float section_0_w = sections[0].Width + sections[0].Spacing;
9485 float section_1_w = sections[1].Width + sections[1].Spacing;
9486 float section_2_w = sections[2].Width + sections[2].Spacing;
9487 bool central_section_is_visible = (section_0_w + section_2_w) < tab_bar->BarRect.GetWidth();
9488 float width_excess;
9489 if (central_section_is_visible)
9490 width_excess = ImMax(section_1_w - (tab_bar->BarRect.GetWidth() - section_0_w - section_2_w), 0.0f); // Excess used to shrink central section
9491 else
9492 width_excess = (section_0_w + section_2_w) - tab_bar->BarRect.GetWidth(); // Excess used to shrink leading/trailing section
9493
9494 // With ImGuiTabBarFlags_FittingPolicyScroll policy, we will only shrink leading/trailing if the central section is not visible anymore
9495 if (width_excess >= 1.0f && ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyResizeDown) || !central_section_is_visible))
9496 {
9497 int shrink_data_count = (central_section_is_visible ? sections[1].TabCount : sections[0].TabCount + sections[2].TabCount);
9498 int shrink_data_offset = (central_section_is_visible ? sections[0].TabCount + sections[2].TabCount : 0);
9499 ShrinkWidths(g.ShrinkWidthBuffer.Data + shrink_data_offset, shrink_data_count, width_excess);
9500
9501 // Apply shrunk values into tabs and sections
9502 for (int tab_n = shrink_data_offset; tab_n < shrink_data_offset + shrink_data_count; tab_n++)
9503 {
9504 ImGuiTabItem* tab = &tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index];
9505 float shrinked_width = IM_TRUNC(g.ShrinkWidthBuffer[tab_n].Width);
9506 if (shrinked_width < 0.0f)
9507 continue;
9508
9509 shrinked_width = ImMax(1.0f, shrinked_width);
9510 int section_n = TabItemGetSectionIdx(tab);
9511 sections[section_n].Width -= (tab->Width - shrinked_width);
9512 tab->Width = shrinked_width;
9513 }
9514 }
9515
9516 // Layout all active tabs
9517 int section_tab_index = 0;
9518 float tab_offset = 0.0f;
9519 tab_bar->WidthAllTabs = 0.0f;
9520 for (int section_n = 0; section_n < 3; section_n++)
9521 {
9522 ImGuiTabBarSection* section = &sections[section_n];
9523 if (section_n == 2)
9524 tab_offset = ImMin(ImMax(0.0f, tab_bar->BarRect.GetWidth() - section->Width), tab_offset);
9525
9526 for (int tab_n = 0; tab_n < section->TabCount; tab_n++)
9527 {
9528 ImGuiTabItem* tab = &tab_bar->Tabs[section_tab_index + tab_n];
9529 tab->Offset = tab_offset;
9530 tab->NameOffset = -1;
9531 tab_offset += tab->Width + (tab_n < section->TabCount - 1 ? g.Style.ItemInnerSpacing.x : 0.0f);
9532 }
9533 tab_bar->WidthAllTabs += ImMax(section->Width + section->Spacing, 0.0f);
9534 tab_offset += section->Spacing;
9535 section_tab_index += section->TabCount;
9536 }
9537
9538 // Clear name buffers
9539 tab_bar->TabsNames.Buf.resize(0);
9540
9541 // If we have lost the selected tab, select the next most recently active one
9542 if (found_selected_tab_id == false)
9543 tab_bar->SelectedTabId = 0;
9544 if (tab_bar->SelectedTabId == 0 && tab_bar->NextSelectedTabId == 0 && most_recently_selected_tab != NULL)
9545 scroll_to_tab_id = tab_bar->SelectedTabId = most_recently_selected_tab->ID;
9546
9547 // Lock in visible tab
9548 tab_bar->VisibleTabId = tab_bar->SelectedTabId;
9549 tab_bar->VisibleTabWasSubmitted = false;
9550
9551 // CTRL+TAB can override visible tab temporarily
9552 if (g.NavWindowingTarget != NULL && g.NavWindowingTarget->DockNode && g.NavWindowingTarget->DockNode->TabBar == tab_bar)
9553 tab_bar->VisibleTabId = scroll_to_tab_id = g.NavWindowingTarget->TabId;
9554
9555 // Apply request requests
9556 if (scroll_to_tab_id != 0)
9557 TabBarScrollToTab(tab_bar, scroll_to_tab_id, sections);
9558 else if ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll) && IsMouseHoveringRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max, true) && IsWindowContentHoverable(g.CurrentWindow))
9559 {
9560 const float wheel = g.IO.MouseWheelRequestAxisSwap ? g.IO.MouseWheel : g.IO.MouseWheelH;
9561 const ImGuiKey wheel_key = g.IO.MouseWheelRequestAxisSwap ? ImGuiKey_MouseWheelY : ImGuiKey_MouseWheelX;
9562 if (TestKeyOwner(wheel_key, tab_bar->ID) && wheel != 0.0f)
9563 {
9564 const float scroll_step = wheel * TabBarCalcScrollableWidth(tab_bar, sections) / 3.0f;
9565 tab_bar->ScrollingTargetDistToVisibility = 0.0f;
9566 tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, tab_bar->ScrollingTarget - scroll_step);
9567 }
9568 SetKeyOwner(wheel_key, tab_bar->ID);
9569 }
9570
9571 // Update scrolling
9572 tab_bar->ScrollingAnim = TabBarScrollClamp(tab_bar, tab_bar->ScrollingAnim);
9573 tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, tab_bar->ScrollingTarget);
9574 if (tab_bar->ScrollingAnim != tab_bar->ScrollingTarget)
9575 {
9576 // Scrolling speed adjust itself so we can always reach our target in 1/3 seconds.
9577 // Teleport if we are aiming far off the visible line
9578 tab_bar->ScrollingSpeed = ImMax(tab_bar->ScrollingSpeed, 70.0f * g.FontSize);
9579 tab_bar->ScrollingSpeed = ImMax(tab_bar->ScrollingSpeed, ImFabs(tab_bar->ScrollingTarget - tab_bar->ScrollingAnim) / 0.3f);
9580 const bool teleport = (tab_bar->PrevFrameVisible + 1 < g.FrameCount) || (tab_bar->ScrollingTargetDistToVisibility > 10.0f * g.FontSize);
9581 tab_bar->ScrollingAnim = teleport ? tab_bar->ScrollingTarget : ImLinearSweep(tab_bar->ScrollingAnim, tab_bar->ScrollingTarget, g.IO.DeltaTime * tab_bar->ScrollingSpeed);
9582 }
9583 else
9584 {
9585 tab_bar->ScrollingSpeed = 0.0f;
9586 }
9587 tab_bar->ScrollingRectMinX = tab_bar->BarRect.Min.x + sections[0].Width + sections[0].Spacing;
9588 tab_bar->ScrollingRectMaxX = tab_bar->BarRect.Max.x - sections[2].Width - sections[1].Spacing;
9589
9590 // Actual layout in host window (we don't do it in BeginTabBar() so as not to waste an extra frame)
9591 ImGuiWindow* window = g.CurrentWindow;
9592 window->DC.CursorPos = tab_bar->BarRect.Min;
9593 ItemSize(ImVec2(tab_bar->WidthAllTabs, tab_bar->BarRect.GetHeight()), tab_bar->FramePadding.y);
9594 window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, tab_bar->BarRect.Min.x + tab_bar->WidthAllTabsIdeal);
9595}
9596
9597// Dockable windows uses Name/ID in the global namespace. Non-dockable items use the ID stack.
9598static ImU32 ImGui::TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window)
9599{
9600 if (docked_window != NULL)
9601 {
9602 IM_UNUSED(tab_bar);
9603 IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_DockNode);
9604 ImGuiID id = docked_window->TabId;
9605 KeepAliveID(id);
9606 return id;
9607 }
9608 else
9609 {
9610 ImGuiWindow* window = GImGui->CurrentWindow;
9611 return window->GetID(label);
9612 }
9613}
9614
9615static float ImGui::TabBarCalcMaxTabWidth()
9616{
9617 ImGuiContext& g = *GImGui;
9618 return g.FontSize * 20.0f;
9619}
9620
9621ImGuiTabItem* ImGui::TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id)
9622{
9623 if (tab_id != 0)
9624 for (int n = 0; n < tab_bar->Tabs.Size; n++)
9625 if (tab_bar->Tabs[n].ID == tab_id)
9626 return &tab_bar->Tabs[n];
9627 return NULL;
9628}
9629
9630// Order = visible order, not submission order! (which is tab->BeginOrder)
9631ImGuiTabItem* ImGui::TabBarFindTabByOrder(ImGuiTabBar* tab_bar, int order)
9632{
9633 if (order < 0 || order >= tab_bar->Tabs.Size)
9634 return NULL;
9635 return &tab_bar->Tabs[order];
9636}
9637
9638// FIXME: See references to #2304 in TODO.txt
9639ImGuiTabItem* ImGui::TabBarFindMostRecentlySelectedTabForActiveWindow(ImGuiTabBar* tab_bar)
9640{
9641 ImGuiTabItem* most_recently_selected_tab = NULL;
9642 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
9643 {
9644 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
9645 if (most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected)
9646 if (tab->Window && tab->Window->WasActive)
9647 most_recently_selected_tab = tab;
9648 }
9649 return most_recently_selected_tab;
9650}
9651
9652ImGuiTabItem* ImGui::TabBarGetCurrentTab(ImGuiTabBar* tab_bar)
9653{
9654 if (tab_bar->LastTabItemIdx < 0 || tab_bar->LastTabItemIdx >= tab_bar->Tabs.Size)
9655 return NULL;
9656 return &tab_bar->Tabs[tab_bar->LastTabItemIdx];
9657}
9658
9659const char* ImGui::TabBarGetTabName(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
9660{
9661 if (tab->Window)
9662 return tab->Window->Name;
9663 if (tab->NameOffset == -1)
9664 return "N/A";
9665 IM_ASSERT(tab->NameOffset < tab_bar->TabsNames.Buf.Size);
9666 return tab_bar->TabsNames.Buf.Data + tab->NameOffset;
9667}
9668
9669// The purpose of this call is to register tab in advance so we can control their order at the time they appear.
9670// Otherwise calling this is unnecessary as tabs are appending as needed by the BeginTabItem() function.
9671void ImGui::TabBarAddTab(ImGuiTabBar* tab_bar, ImGuiTabItemFlags tab_flags, ImGuiWindow* window)
9672{
9673 ImGuiContext& g = *GImGui;
9674 IM_ASSERT(TabBarFindTabByID(tab_bar, window->TabId) == NULL);
9675 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)
9676
9677 if (!window->HasCloseButton)
9678 tab_flags |= ImGuiTabItemFlags_NoCloseButton; // Set _NoCloseButton immediately because it will be used for first-frame width calculation.
9679
9680 ImGuiTabItem new_tab;
9681 new_tab.ID = window->TabId;
9682 new_tab.Flags = tab_flags;
9683 new_tab.LastFrameVisible = tab_bar->CurrFrameVisible; // Required so BeginTabBar() doesn't ditch the tab
9684 if (new_tab.LastFrameVisible == -1)
9685 new_tab.LastFrameVisible = g.FrameCount - 1;
9686 new_tab.Window = window; // Required so tab bar layout can compute the tab width before tab submission
9687 tab_bar->Tabs.push_back(new_tab);
9688}
9689
9690// The *TabId fields are already set by the docking system _before_ the actual TabItem was created, so we clear them regardless.
9691void ImGui::TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id)
9692{
9693 if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
9694 tab_bar->Tabs.erase(tab);
9695 if (tab_bar->VisibleTabId == tab_id) { tab_bar->VisibleTabId = 0; }
9696 if (tab_bar->SelectedTabId == tab_id) { tab_bar->SelectedTabId = 0; }
9697 if (tab_bar->NextSelectedTabId == tab_id) { tab_bar->NextSelectedTabId = 0; }
9698}
9699
9700// Called on manual closure attempt
9701void ImGui::TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
9702{
9703 if (tab->Flags & ImGuiTabItemFlags_Button)
9704 return; // A button appended with TabItemButton().
9705
9706 if ((tab->Flags & (ImGuiTabItemFlags_UnsavedDocument | ImGuiTabItemFlags_NoAssumedClosure)) == 0)
9707 {
9708 // This will remove a frame of lag for selecting another tab on closure.
9709 // 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
9710 tab->WantClose = true;
9711 if (tab_bar->VisibleTabId == tab->ID)
9712 {
9713 tab->LastFrameVisible = -1;
9714 tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = 0;
9715 }
9716 }
9717 else
9718 {
9719 // Actually select before expecting closure attempt (on an UnsavedDocument tab user is expect to e.g. show a popup)
9720 if (tab_bar->VisibleTabId != tab->ID)
9721 TabBarQueueFocus(tab_bar, tab);
9722 }
9723}
9724
9725static float ImGui::TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling)
9726{
9727 scrolling = ImMin(scrolling, tab_bar->WidthAllTabs - tab_bar->BarRect.GetWidth());
9728 return ImMax(scrolling, 0.0f);
9729}
9730
9731// Note: we may scroll to tab that are not selected! e.g. using keyboard arrow keys
9732static void ImGui::TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections)
9733{
9734 ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id);
9735 if (tab == NULL)
9736 return;
9737 if (tab->Flags & ImGuiTabItemFlags_SectionMask_)
9738 return;
9739
9740 ImGuiContext& g = *GImGui;
9741 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)
9742 int order = TabBarGetTabOrder(tab_bar, tab);
9743
9744 // Scrolling happens only in the central section (leading/trailing sections are not scrolling)
9745 float scrollable_width = TabBarCalcScrollableWidth(tab_bar, sections);
9746
9747 // We make all tabs positions all relative Sections[0].Width to make code simpler
9748 float tab_x1 = tab->Offset - sections[0].Width + (order > sections[0].TabCount - 1 ? -margin : 0.0f);
9749 float tab_x2 = tab->Offset - sections[0].Width + tab->Width + (order + 1 < tab_bar->Tabs.Size - sections[2].TabCount ? margin : 1.0f);
9750 tab_bar->ScrollingTargetDistToVisibility = 0.0f;
9751 if (tab_bar->ScrollingTarget > tab_x1 || (tab_x2 - tab_x1 >= scrollable_width))
9752 {
9753 // Scroll to the left
9754 tab_bar->ScrollingTargetDistToVisibility = ImMax(tab_bar->ScrollingAnim - tab_x2, 0.0f);
9755 tab_bar->ScrollingTarget = tab_x1;
9756 }
9757 else if (tab_bar->ScrollingTarget < tab_x2 - scrollable_width)
9758 {
9759 // Scroll to the right
9760 tab_bar->ScrollingTargetDistToVisibility = ImMax((tab_x1 - scrollable_width) - tab_bar->ScrollingAnim, 0.0f);
9761 tab_bar->ScrollingTarget = tab_x2 - scrollable_width;
9762 }
9763}
9764
9765void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
9766{
9767 tab_bar->NextSelectedTabId = tab->ID;
9768}
9769
9770void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, const char* tab_name)
9771{
9772 IM_ASSERT((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0); // Only supported for manual/explicit tab bars
9773 ImGuiID tab_id = TabBarCalcTabID(tab_bar, tab_name, NULL);
9774 tab_bar->NextSelectedTabId = tab_id;
9775}
9776
9777void ImGui::TabBarQueueReorder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab, int offset)
9778{
9779 IM_ASSERT(offset != 0);
9780 IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
9781 tab_bar->ReorderRequestTabId = tab->ID;
9782 tab_bar->ReorderRequestOffset = (ImS16)offset;
9783}
9784
9785void ImGui::TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, ImGuiTabItem* src_tab, ImVec2 mouse_pos)
9786{
9787 ImGuiContext& g = *GImGui;
9788 IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
9789 if ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) == 0)
9790 return;
9791
9792 const bool is_central_section = (src_tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
9793 const float bar_offset = tab_bar->BarRect.Min.x - (is_central_section ? tab_bar->ScrollingTarget : 0);
9794
9795 // Count number of contiguous tabs we are crossing over
9796 const int dir = (bar_offset + src_tab->Offset) > mouse_pos.x ? -1 : +1;
9797 const int src_idx = tab_bar->Tabs.index_from_ptr(src_tab);
9798 int dst_idx = src_idx;
9799 for (int i = src_idx; i >= 0 && i < tab_bar->Tabs.Size; i += dir)
9800 {
9801 // Reordered tabs must share the same section
9802 const ImGuiTabItem* dst_tab = &tab_bar->Tabs[i];
9803 if (dst_tab->Flags & ImGuiTabItemFlags_NoReorder)
9804 break;
9805 if ((dst_tab->Flags & ImGuiTabItemFlags_SectionMask_) != (src_tab->Flags & ImGuiTabItemFlags_SectionMask_))
9806 break;
9807 dst_idx = i;
9808
9809 // Include spacing after tab, so when mouse cursor is between tabs we would not continue checking further tabs that are not hovered.
9810 const float x1 = bar_offset + dst_tab->Offset - g.Style.ItemInnerSpacing.x;
9811 const float x2 = bar_offset + dst_tab->Offset + dst_tab->Width + g.Style.ItemInnerSpacing.x;
9812 //GetForegroundDrawList()->AddRect(ImVec2(x1, tab_bar->BarRect.Min.y), ImVec2(x2, tab_bar->BarRect.Max.y), IM_COL32(255, 0, 0, 255));
9813 if ((dir < 0 && mouse_pos.x > x1) || (dir > 0 && mouse_pos.x < x2))
9814 break;
9815 }
9816
9817 if (dst_idx != src_idx)
9818 TabBarQueueReorder(tab_bar, src_tab, dst_idx - src_idx);
9819}
9820
9821bool ImGui::TabBarProcessReorder(ImGuiTabBar* tab_bar)
9822{
9823 ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_bar->ReorderRequestTabId);
9824 if (tab1 == NULL || (tab1->Flags & ImGuiTabItemFlags_NoReorder))
9825 return false;
9826
9827 //IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_Reorderable); // <- this may happen when using debug tools
9828 int tab2_order = TabBarGetTabOrder(tab_bar, tab1) + tab_bar->ReorderRequestOffset;
9829 if (tab2_order < 0 || tab2_order >= tab_bar->Tabs.Size)
9830 return false;
9831
9832 // Reordered tabs must share the same section
9833 // (Note: TabBarQueueReorderFromMousePos() also has a similar test but since we allow direct calls to TabBarQueueReorder() we do it here too)
9834 ImGuiTabItem* tab2 = &tab_bar->Tabs[tab2_order];
9835 if (tab2->Flags & ImGuiTabItemFlags_NoReorder)
9836 return false;
9837 if ((tab1->Flags & ImGuiTabItemFlags_SectionMask_) != (tab2->Flags & ImGuiTabItemFlags_SectionMask_))
9838 return false;
9839
9840 ImGuiTabItem item_tmp = *tab1;
9841 ImGuiTabItem* src_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 + 1 : tab2;
9842 ImGuiTabItem* dst_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 : tab2 + 1;
9843 const int move_count = (tab_bar->ReorderRequestOffset > 0) ? tab_bar->ReorderRequestOffset : -tab_bar->ReorderRequestOffset;
9844 memmove(dst_tab, src_tab, move_count * sizeof(ImGuiTabItem));
9845 *tab2 = item_tmp;
9846
9847 if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings)
9848 MarkIniSettingsDirty();
9849 return true;
9850}
9851
9852static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar)
9853{
9854 ImGuiContext& g = *GImGui;
9855 ImGuiWindow* window = g.CurrentWindow;
9856
9857 const ImVec2 arrow_button_size(g.FontSize - 2.0f, g.FontSize + g.Style.FramePadding.y * 2.0f);
9858 const float scrolling_buttons_width = arrow_button_size.x * 2.0f;
9859
9860 const ImVec2 backup_cursor_pos = window->DC.CursorPos;
9861 //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));
9862
9863 int select_dir = 0;
9864 ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
9865 arrow_col.w *= 0.5f;
9866
9867 PushStyleColor(ImGuiCol_Text, arrow_col);
9868 PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
9869 PushItemFlag(ImGuiItemFlags_ButtonRepeat, true);
9870 const float backup_repeat_delay = g.IO.KeyRepeatDelay;
9871 const float backup_repeat_rate = g.IO.KeyRepeatRate;
9872 g.IO.KeyRepeatDelay = 0.250f;
9873 g.IO.KeyRepeatRate = 0.200f;
9874 float x = ImMax(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.x - scrolling_buttons_width);
9875 window->DC.CursorPos = ImVec2(x, tab_bar->BarRect.Min.y);
9876 if (ArrowButtonEx("##<", ImGuiDir_Left, arrow_button_size, ImGuiButtonFlags_PressedOnClick))
9877 select_dir = -1;
9878 window->DC.CursorPos = ImVec2(x + arrow_button_size.x, tab_bar->BarRect.Min.y);
9879 if (ArrowButtonEx("##>", ImGuiDir_Right, arrow_button_size, ImGuiButtonFlags_PressedOnClick))
9880 select_dir = +1;
9881 PopItemFlag();
9882 PopStyleColor(2);
9883 g.IO.KeyRepeatRate = backup_repeat_rate;
9884 g.IO.KeyRepeatDelay = backup_repeat_delay;
9885
9886 ImGuiTabItem* tab_to_scroll_to = NULL;
9887 if (select_dir != 0)
9888 if (ImGuiTabItem* tab_item = TabBarFindTabByID(tab_bar, tab_bar->SelectedTabId))
9889 {
9890 int selected_order = TabBarGetTabOrder(tab_bar, tab_item);
9891 int target_order = selected_order + select_dir;
9892
9893 // Skip tab item buttons until another tab item is found or end is reached
9894 while (tab_to_scroll_to == NULL)
9895 {
9896 // If we are at the end of the list, still scroll to make our tab visible
9897 tab_to_scroll_to = &tab_bar->Tabs[(target_order >= 0 && target_order < tab_bar->Tabs.Size) ? target_order : selected_order];
9898
9899 // Cross through buttons
9900 // (even if first/last item is a button, return it so we can update the scroll)
9901 if (tab_to_scroll_to->Flags & ImGuiTabItemFlags_Button)
9902 {
9903 target_order += select_dir;
9904 selected_order += select_dir;
9905 tab_to_scroll_to = (target_order < 0 || target_order >= tab_bar->Tabs.Size) ? tab_to_scroll_to : NULL;
9906 }
9907 }
9908 }
9909 window->DC.CursorPos = backup_cursor_pos;
9910 tab_bar->BarRect.Max.x -= scrolling_buttons_width + 1.0f;
9911
9912 return tab_to_scroll_to;
9913}
9914
9915static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar)
9916{
9917 ImGuiContext& g = *GImGui;
9918 ImGuiWindow* window = g.CurrentWindow;
9919
9920 // We use g.Style.FramePadding.y to match the square ArrowButton size
9921 const float tab_list_popup_button_width = g.FontSize + g.Style.FramePadding.y;
9922 const ImVec2 backup_cursor_pos = window->DC.CursorPos;
9923 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x - g.Style.FramePadding.y, tab_bar->BarRect.Min.y);
9924 tab_bar->BarRect.Min.x += tab_list_popup_button_width;
9925
9926 ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
9927 arrow_col.w *= 0.5f;
9928 PushStyleColor(ImGuiCol_Text, arrow_col);
9929 PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
9930 bool open = BeginCombo("##v", NULL, ImGuiComboFlags_NoPreview | ImGuiComboFlags_HeightLargest);
9931 PopStyleColor(2);
9932
9933 ImGuiTabItem* tab_to_select = NULL;
9934 if (open)
9935 {
9936 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
9937 {
9938 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
9939 if (tab->Flags & ImGuiTabItemFlags_Button)
9940 continue;
9941
9942 const char* tab_name = TabBarGetTabName(tab_bar, tab);
9943 if (Selectable(tab_name, tab_bar->SelectedTabId == tab->ID))
9944 tab_to_select = tab;
9945 }
9946 EndCombo();
9947 }
9948
9949 window->DC.CursorPos = backup_cursor_pos;
9950 return tab_to_select;
9951}
9952
9953//-------------------------------------------------------------------------
9954// [SECTION] Widgets: BeginTabItem, EndTabItem, etc.
9955//-------------------------------------------------------------------------
9956// - BeginTabItem()
9957// - EndTabItem()
9958// - TabItemButton()
9959// - TabItemEx() [Internal]
9960// - SetTabItemClosed()
9961// - TabItemCalcSize() [Internal]
9962// - TabItemBackground() [Internal]
9963// - TabItemLabelAndCloseButton() [Internal]
9964//-------------------------------------------------------------------------
9965
9966bool ImGui::BeginTabItem(const char* label, bool* p_open, ImGuiTabItemFlags flags)
9967{
9968 ImGuiContext& g = *GImGui;
9969 ImGuiWindow* window = g.CurrentWindow;
9970 if (window->SkipItems)
9971 return false;
9972
9973 ImGuiTabBar* tab_bar = g.CurrentTabBar;
9974 if (tab_bar == NULL)
9975 {
9976 IM_ASSERT_USER_ERROR(tab_bar, "Needs to be called between BeginTabBar() and EndTabBar()!");
9977 return false;
9978 }
9979 IM_ASSERT((flags & ImGuiTabItemFlags_Button) == 0); // BeginTabItem() Can't be used with button flags, use TabItemButton() instead!
9980
9981 bool ret = TabItemEx(tab_bar, label, p_open, flags, NULL);
9982 if (ret && !(flags & ImGuiTabItemFlags_NoPushId))
9983 {
9984 ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
9985 PushOverrideID(tab->ID); // We already hashed 'label' so push into the ID stack directly instead of doing another hash through PushID(label)
9986 }
9987 return ret;
9988}
9989
9990void ImGui::EndTabItem()
9991{
9992 ImGuiContext& g = *GImGui;
9993 ImGuiWindow* window = g.CurrentWindow;
9994 if (window->SkipItems)
9995 return;
9996
9997 ImGuiTabBar* tab_bar = g.CurrentTabBar;
9998 if (tab_bar == NULL)
9999 {
10000 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
10001 return;
10002 }
10003 IM_ASSERT(tab_bar->LastTabItemIdx >= 0);
10004 ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
10005 if (!(tab->Flags & ImGuiTabItemFlags_NoPushId))
10006 PopID();
10007}
10008
10009bool ImGui::TabItemButton(const char* label, ImGuiTabItemFlags flags)
10010{
10011 ImGuiContext& g = *GImGui;
10012 ImGuiWindow* window = g.CurrentWindow;
10013 if (window->SkipItems)
10014 return false;
10015
10016 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10017 if (tab_bar == NULL)
10018 {
10019 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
10020 return false;
10021 }
10022 return TabItemEx(tab_bar, label, NULL, flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder, NULL);
10023}
10024
10025bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags, ImGuiWindow* docked_window)
10026{
10027 // Layout whole tab bar if not already done
10028 ImGuiContext& g = *GImGui;
10029 if (tab_bar->WantLayout)
10030 {
10031 ImGuiNextItemData backup_next_item_data = g.NextItemData;
10032 TabBarLayout(tab_bar);
10033 g.NextItemData = backup_next_item_data;
10034 }
10035 ImGuiWindow* window = g.CurrentWindow;
10036 if (window->SkipItems)
10037 return false;
10038
10039 const ImGuiStyle& style = g.Style;
10040 const ImGuiID id = TabBarCalcTabID(tab_bar, label, docked_window);
10041
10042 // If the user called us with *p_open == false, we early out and don't render.
10043 // 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.
10044 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
10045 if (p_open && !*p_open)
10046 {
10047 ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav);
10048 return false;
10049 }
10050
10051 IM_ASSERT(!p_open || !(flags & ImGuiTabItemFlags_Button));
10052 IM_ASSERT((flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)) != (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)); // Can't use both Leading and Trailing
10053
10054 // Store into ImGuiTabItemFlags_NoCloseButton, also honor ImGuiTabItemFlags_NoCloseButton passed by user (although not documented)
10055 if (flags & ImGuiTabItemFlags_NoCloseButton)
10056 p_open = NULL;
10057 else if (p_open == NULL)
10058 flags |= ImGuiTabItemFlags_NoCloseButton;
10059
10060 // Acquire tab data
10061 ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, id);
10062 bool tab_is_new = false;
10063 if (tab == NULL)
10064 {
10065 tab_bar->Tabs.push_back(ImGuiTabItem());
10066 tab = &tab_bar->Tabs.back();
10067 tab->ID = id;
10068 tab_bar->TabsAddedNew = tab_is_new = true;
10069 }
10070 tab_bar->LastTabItemIdx = (ImS16)tab_bar->Tabs.index_from_ptr(tab);
10071
10072 // Calculate tab contents size
10073 ImVec2 size = TabItemCalcSize(label, (p_open != NULL) || (flags & ImGuiTabItemFlags_UnsavedDocument));
10074 tab->RequestedWidth = -1.0f;
10075 if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasWidth)
10076 size.x = tab->RequestedWidth = g.NextItemData.Width;
10077 if (tab_is_new)
10078 tab->Width = ImMax(1.0f, size.x);
10079 tab->ContentWidth = size.x;
10080 tab->BeginOrder = tab_bar->TabsActiveCount++;
10081
10082 const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
10083 const bool tab_bar_focused = (tab_bar->Flags & ImGuiTabBarFlags_IsFocused) != 0;
10084 const bool tab_appearing = (tab->LastFrameVisible + 1 < g.FrameCount);
10085 const bool tab_just_unsaved = (flags & ImGuiTabItemFlags_UnsavedDocument) && !(tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
10086 const bool is_tab_button = (flags & ImGuiTabItemFlags_Button) != 0;
10087 tab->LastFrameVisible = g.FrameCount;
10088 tab->Flags = flags;
10089 tab->Window = docked_window;
10090
10091 // Append name _WITH_ the zero-terminator
10092 // (regular tabs are permitted in a DockNode tab bar, but window tabs not permitted in a non-DockNode tab bar)
10093 if (docked_window != NULL)
10094 {
10095 IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_DockNode);
10096 tab->NameOffset = -1;
10097 }
10098 else
10099 {
10100 tab->NameOffset = (ImS32)tab_bar->TabsNames.size();
10101 tab_bar->TabsNames.append(label, label + strlen(label) + 1);
10102 }
10103
10104 // Update selected tab
10105 if (!is_tab_button)
10106 {
10107 if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0)
10108 if (!tab_bar_appearing || tab_bar->SelectedTabId == 0)
10109 TabBarQueueFocus(tab_bar, tab); // New tabs gets activated
10110 if ((flags & ImGuiTabItemFlags_SetSelected) && (tab_bar->SelectedTabId != id)) // _SetSelected can only be passed on explicit tab bar
10111 TabBarQueueFocus(tab_bar, tab);
10112 }
10113
10114 // Lock visibility
10115 // (Note: tab_contents_visible != tab_selected... because CTRL+TAB operations may preview some tabs without selecting them!)
10116 bool tab_contents_visible = (tab_bar->VisibleTabId == id);
10117 if (tab_contents_visible)
10118 tab_bar->VisibleTabWasSubmitted = true;
10119
10120 // On the very first frame of a tab bar we let first tab contents be visible to minimize appearing glitches
10121 if (!tab_contents_visible && tab_bar->SelectedTabId == 0 && tab_bar_appearing && docked_window == NULL)
10122 if (tab_bar->Tabs.Size == 1 && !(tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs))
10123 tab_contents_visible = true;
10124
10125 // Note that tab_is_new is not necessarily the same as tab_appearing! When a tab bar stops being submitted
10126 // and then gets submitted again, the tabs will have 'tab_appearing=true' but 'tab_is_new=false'.
10127 if (tab_appearing && (!tab_bar_appearing || tab_is_new))
10128 {
10129 ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav);
10130 if (is_tab_button)
10131 return false;
10132 return tab_contents_visible;
10133 }
10134
10135 if (tab_bar->SelectedTabId == id)
10136 tab->LastFrameSelected = g.FrameCount;
10137
10138 // Backup current layout position
10139 const ImVec2 backup_main_cursor_pos = window->DC.CursorPos;
10140
10141 // Layout
10142 const bool is_central_section = (tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
10143 size.x = tab->Width;
10144 if (is_central_section)
10145 window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_TRUNC(tab->Offset - tab_bar->ScrollingAnim), 0.0f);
10146 else
10147 window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(tab->Offset, 0.0f);
10148 ImVec2 pos = window->DC.CursorPos;
10149 ImRect bb(pos, pos + size);
10150
10151 // 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)
10152 const bool want_clip_rect = is_central_section && (bb.Min.x < tab_bar->ScrollingRectMinX || bb.Max.x > tab_bar->ScrollingRectMaxX);
10153 if (want_clip_rect)
10154 PushClipRect(ImVec2(ImMax(bb.Min.x, tab_bar->ScrollingRectMinX), bb.Min.y - 1), ImVec2(tab_bar->ScrollingRectMaxX, bb.Max.y), true);
10155
10156 ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos;
10157 ItemSize(bb.GetSize(), style.FramePadding.y);
10158 window->DC.CursorMaxPos = backup_cursor_max_pos;
10159
10160 if (!ItemAdd(bb, id))
10161 {
10162 if (want_clip_rect)
10163 PopClipRect();
10164 window->DC.CursorPos = backup_main_cursor_pos;
10165 return tab_contents_visible;
10166 }
10167
10168 // Click to Select a tab
10169 ImGuiButtonFlags button_flags = ((is_tab_button ? ImGuiButtonFlags_PressedOnClickRelease : ImGuiButtonFlags_PressedOnClick) | ImGuiButtonFlags_AllowOverlap);
10170 if (g.DragDropActive && !g.DragDropPayload.IsDataType(IMGUI_PAYLOAD_TYPE_WINDOW)) // FIXME: May be an opt-in property of the payload to disable this
10171 button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
10172 bool hovered, held;
10173 bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags);
10174 if (pressed && !is_tab_button)
10175 TabBarQueueFocus(tab_bar, tab);
10176
10177 // Transfer active id window so the active id is not owned by the dock host (as StartMouseMovingWindow()
10178 // will only do it on the drag). This allows FocusWindow() to be more conservative in how it clears active id.
10179 if (held && docked_window && g.ActiveId == id && g.ActiveIdIsJustActivated)
10180 g.ActiveIdWindow = docked_window;
10181
10182 // Drag and drop a single floating window node moves it
10183 ImGuiDockNode* node = docked_window ? docked_window->DockNode : NULL;
10184 const bool single_floating_window_node = node && node->IsFloatingNode() && (node->Windows.Size == 1);
10185 if (held && single_floating_window_node && IsMouseDragging(0, 0.0f))
10186 {
10187 // Move
10188 StartMouseMovingWindow(docked_window);
10189 }
10190 else if (held && !tab_appearing && IsMouseDragging(0))
10191 {
10192 // Drag and drop: re-order tabs
10193 int drag_dir = 0;
10194 float drag_distance_from_edge_x = 0.0f;
10195 if (!g.DragDropActive && ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (docked_window != NULL)))
10196 {
10197 // While moving a tab it will jump on the other side of the mouse, so we also test for MouseDelta.x
10198 if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < bb.Min.x)
10199 {
10200 drag_dir = -1;
10201 drag_distance_from_edge_x = bb.Min.x - g.IO.MousePos.x;
10202 TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos);
10203 }
10204 else if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > bb.Max.x)
10205 {
10206 drag_dir = +1;
10207 drag_distance_from_edge_x = g.IO.MousePos.x - bb.Max.x;
10208 TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos);
10209 }
10210 }
10211
10212 // Extract a Dockable window out of it's tab bar
10213 const bool can_undock = docked_window != NULL && !(docked_window->Flags & ImGuiWindowFlags_NoMove) && !(node->MergedFlags & ImGuiDockNodeFlags_NoUndocking);
10214 if (can_undock)
10215 {
10216 // We use a variable threshold to distinguish dragging tabs within a tab bar and extracting them out of the tab bar
10217 bool undocking_tab = (g.DragDropActive && g.DragDropPayload.SourceId == id);
10218 if (!undocking_tab) //&& (!g.IO.ConfigDockingWithShift || g.IO.KeyShift)
10219 {
10220 float threshold_base = g.FontSize;
10221 float threshold_x = (threshold_base * 2.2f);
10222 float threshold_y = (threshold_base * 1.5f) + ImClamp((ImFabs(g.IO.MouseDragMaxDistanceAbs[0].x) - threshold_base * 2.0f) * 0.20f, 0.0f, threshold_base * 4.0f);
10223 //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]
10224
10225 float distance_from_edge_y = ImMax(bb.Min.y - g.IO.MousePos.y, g.IO.MousePos.y - bb.Max.y);
10226 if (distance_from_edge_y >= threshold_y)
10227 undocking_tab = true;
10228 if (drag_distance_from_edge_x > threshold_x)
10229 if ((drag_dir < 0 && TabBarGetTabOrder(tab_bar, tab) == 0) || (drag_dir > 0 && TabBarGetTabOrder(tab_bar, tab) == tab_bar->Tabs.Size - 1))
10230 undocking_tab = true;
10231 }
10232
10233 if (undocking_tab)
10234 {
10235 // Undock
10236 // FIXME: refactor to share more code with e.g. StartMouseMovingWindow
10237 DockContextQueueUndockWindow(&g, docked_window);
10238 g.MovingWindow = docked_window;
10239 SetActiveID(g.MovingWindow->MoveId, g.MovingWindow);
10240 g.ActiveIdClickOffset -= g.MovingWindow->Pos - bb.Min;
10241 g.ActiveIdNoClearOnFocusLoss = true;
10242 SetActiveIdUsingAllKeyboardKeys();
10243 }
10244 }
10245 }
10246
10247#if 0
10248 if (hovered && g.HoveredIdNotActiveTimer > TOOLTIP_DELAY && bb.GetWidth() < tab->ContentWidth)
10249 {
10250 // Enlarge tab display when hovering
10251 bb.Max.x = bb.Min.x + IM_TRUNC(ImLerp(bb.GetWidth(), tab->ContentWidth, ImSaturate((g.HoveredIdNotActiveTimer - 0.40f) * 6.0f)));
10252 display_draw_list = GetForegroundDrawList(window);
10253 TabItemBackground(display_draw_list, bb, flags, GetColorU32(ImGuiCol_TitleBgActive));
10254 }
10255#endif
10256
10257 // Render tab shape
10258 ImDrawList* display_draw_list = window->DrawList;
10259 const ImU32 tab_col = GetColorU32((held || hovered) ? ImGuiCol_TabHovered : tab_contents_visible ? (tab_bar_focused ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected) : (tab_bar_focused ? ImGuiCol_Tab : ImGuiCol_TabDimmed));
10260 TabItemBackground(display_draw_list, bb, flags, tab_col);
10261 if (tab_contents_visible && (tab_bar->Flags & ImGuiTabBarFlags_DrawSelectedOverline) && style.TabBarOverlineSize > 0.0f)
10262 {
10263 float x_offset = IM_TRUNC(0.4f * style.TabRounding);
10264 if (x_offset < 2.0f * g.CurrentDpiScale)
10265 x_offset = 0.0f;
10266 float y_offset = 1.0f * g.CurrentDpiScale;
10267 display_draw_list->AddLine(bb.GetTL() + ImVec2(x_offset, y_offset), bb.GetTR() + ImVec2(-x_offset, y_offset), GetColorU32(tab_bar_focused ? ImGuiCol_TabSelectedOverline : ImGuiCol_TabDimmedSelectedOverline), style.TabBarOverlineSize);
10268 }
10269 RenderNavCursor(bb, id);
10270
10271 // Select with right mouse button. This is so the common idiom for context menu automatically highlight the current widget.
10272 const bool hovered_unblocked = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
10273 if (tab_bar->SelectedTabId != tab->ID && hovered_unblocked && (IsMouseClicked(1) || IsMouseReleased(1)) && !is_tab_button)
10274 TabBarQueueFocus(tab_bar, tab);
10275
10276 if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)
10277 flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton;
10278
10279 // Render tab label, process close button
10280 const ImGuiID close_button_id = p_open ? GetIDWithSeed("#CLOSE", NULL, docked_window ? docked_window->ID : id) : 0;
10281 bool just_closed;
10282 bool text_clipped;
10283 TabItemLabelAndCloseButton(display_draw_list, bb, tab_just_unsaved ? (flags & ~ImGuiTabItemFlags_UnsavedDocument) : flags, tab_bar->FramePadding, label, id, close_button_id, tab_contents_visible, &just_closed, &text_clipped);
10284 if (just_closed && p_open != NULL)
10285 {
10286 *p_open = false;
10287 TabBarCloseTab(tab_bar, tab);
10288 }
10289
10290 // Forward Hovered state so IsItemHovered() after Begin() can work (even though we are technically hovering our parent)
10291 // That state is copied to window->DockTabItemStatusFlags by our caller.
10292 if (docked_window && (hovered || g.HoveredId == close_button_id))
10293 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow;
10294
10295 // Restore main window position so user can draw there
10296 if (want_clip_rect)
10297 PopClipRect();
10298 window->DC.CursorPos = backup_main_cursor_pos;
10299
10300 // Tooltip
10301 // (Won't work over the close button because ItemOverlap systems messes up with HoveredIdTimer-> seems ok)
10302 // (We test IsItemHovered() to discard e.g. when another item is active or drag and drop over the tab bar, which g.HoveredId ignores)
10303 // FIXME: This is a mess.
10304 // FIXME: We may want disabled tab to still display the tooltip?
10305 if (text_clipped && g.HoveredId == id && !held)
10306 if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip) && !(tab->Flags & ImGuiTabItemFlags_NoTooltip))
10307 SetItemTooltip("%.*s", (int)(FindRenderedTextEnd(label) - label), label);
10308
10309 IM_ASSERT(!is_tab_button || !(tab_bar->SelectedTabId == tab->ID && is_tab_button)); // TabItemButton should not be selected
10310 if (is_tab_button)
10311 return pressed;
10312 return tab_contents_visible;
10313}
10314
10315// [Public] This is call is 100% optional but it allows to remove some one-frame glitches when a tab has been unexpectedly removed.
10316// To use it to need to call the function SetTabItemClosed() between BeginTabBar() and EndTabBar().
10317// Tabs closed by the close button will automatically be flagged to avoid this issue.
10318void ImGui::SetTabItemClosed(const char* label)
10319{
10320 ImGuiContext& g = *GImGui;
10321 bool is_within_manual_tab_bar = g.CurrentTabBar && !(g.CurrentTabBar->Flags & ImGuiTabBarFlags_DockNode);
10322 if (is_within_manual_tab_bar)
10323 {
10324 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10325 ImGuiID tab_id = TabBarCalcTabID(tab_bar, label, NULL);
10326 if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
10327 tab->WantClose = true; // Will be processed by next call to TabBarLayout()
10328 }
10329 else if (ImGuiWindow* window = FindWindowByName(label))
10330 {
10331 if (window->DockIsActive)
10332 if (ImGuiDockNode* node = window->DockNode)
10333 {
10334 ImGuiID tab_id = TabBarCalcTabID(node->TabBar, label, window);
10335 TabBarRemoveTab(node->TabBar, tab_id);
10336 window->DockTabWantClose = true;
10337 }
10338 }
10339}
10340
10341ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button_or_unsaved_marker)
10342{
10343 ImGuiContext& g = *GImGui;
10344 ImVec2 label_size = CalcTextSize(label, NULL, true);
10345 ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x, label_size.y + g.Style.FramePadding.y * 2.0f);
10346 if (has_close_button_or_unsaved_marker)
10347 size.x += g.Style.FramePadding.x + (g.Style.ItemInnerSpacing.x + g.FontSize); // We use Y intentionally to fit the close button circle.
10348 else
10349 size.x += g.Style.FramePadding.x + 1.0f;
10350 return ImVec2(ImMin(size.x, TabBarCalcMaxTabWidth()), size.y);
10351}
10352
10353ImVec2 ImGui::TabItemCalcSize(ImGuiWindow* window)
10354{
10355 return TabItemCalcSize(window->Name, window->HasCloseButton || (window->Flags & ImGuiWindowFlags_UnsavedDocument));
10356}
10357
10358void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImU32 col)
10359{
10360 // 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.
10361 ImGuiContext& g = *GImGui;
10362 const float width = bb.GetWidth();
10363 IM_UNUSED(flags);
10364 IM_ASSERT(width > 0.0f);
10365 const float rounding = ImMax(0.0f, ImMin((flags & ImGuiTabItemFlags_Button) ? g.Style.FrameRounding : g.Style.TabRounding, width * 0.5f - 1.0f));
10366 const float y1 = bb.Min.y + 1.0f;
10367 const float y2 = bb.Max.y - g.Style.TabBarBorderSize;
10368 draw_list->PathLineTo(ImVec2(bb.Min.x, y2));
10369 draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding, y1 + rounding), rounding, 6, 9);
10370 draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding, y1 + rounding), rounding, 9, 12);
10371 draw_list->PathLineTo(ImVec2(bb.Max.x, y2));
10372 draw_list->PathFillConvex(col);
10373 if (g.Style.TabBorderSize > 0.0f)
10374 {
10375 draw_list->PathLineTo(ImVec2(bb.Min.x + 0.5f, y2));
10376 draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding + 0.5f, y1 + rounding + 0.5f), rounding, 6, 9);
10377 draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding - 0.5f, y1 + rounding + 0.5f), rounding, 9, 12);
10378 draw_list->PathLineTo(ImVec2(bb.Max.x - 0.5f, y2));
10379 draw_list->PathStroke(GetColorU32(ImGuiCol_Border), 0, g.Style.TabBorderSize);
10380 }
10381}
10382
10383// Render text label (with custom clipping) + Unsaved Document marker + Close Button logic
10384// We tend to lock style.FramePadding for a given tab-bar, hence the 'frame_padding' parameter.
10385void 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)
10386{
10387 ImGuiContext& g = *GImGui;
10388 ImVec2 label_size = CalcTextSize(label, NULL, true);
10389
10390 if (out_just_closed)
10391 *out_just_closed = false;
10392 if (out_text_clipped)
10393 *out_text_clipped = false;
10394
10395 if (bb.GetWidth() <= 1.0f)
10396 return;
10397
10398 // In Style V2 we'll have full override of all colors per state (e.g. focused, selected)
10399 // But right now if you want to alter text color of tabs this is what you need to do.
10400#if 0
10401 const float backup_alpha = g.Style.Alpha;
10402 if (!is_contents_visible)
10403 g.Style.Alpha *= 0.7f;
10404#endif
10405
10406 // Render text label (with clipping + alpha gradient) + unsaved marker
10407 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);
10408 ImRect text_ellipsis_clip_bb = text_pixel_clip_bb;
10409
10410 // Return clipped state ignoring the close button
10411 if (out_text_clipped)
10412 {
10413 *out_text_clipped = (text_ellipsis_clip_bb.Min.x + label_size.x) > text_pixel_clip_bb.Max.x;
10414 //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));
10415 }
10416
10417 const float button_sz = g.FontSize;
10418 const ImVec2 button_pos(ImMax(bb.Min.x, bb.Max.x - frame_padding.x - button_sz), bb.Min.y + frame_padding.y);
10419
10420 // Close Button & Unsaved Marker
10421 // We are relying on a subtle and confusing distinction between 'hovered' and 'g.HoveredId' which happens because we are using ImGuiButtonFlags_AllowOverlapMode + SetItemAllowOverlap()
10422 // 'hovered' will be true when hovering the Tab but NOT when hovering the close button
10423 // 'g.HoveredId==id' will be true when hovering the Tab including when hovering the close button
10424 // 'g.ActiveId==close_button_id' will be true when we are holding on the close button, in which case both hovered booleans are false
10425 bool close_button_pressed = false;
10426 bool close_button_visible = false;
10427 if (close_button_id != 0)
10428 if (is_contents_visible || bb.GetWidth() >= ImMax(button_sz, g.Style.TabMinWidthForCloseButton))
10429 if (g.HoveredId == tab_id || g.HoveredId == close_button_id || g.ActiveId == tab_id || g.ActiveId == close_button_id)
10430 close_button_visible = true;
10431 bool unsaved_marker_visible = (flags & ImGuiTabItemFlags_UnsavedDocument) != 0 && (button_pos.x + button_sz <= bb.Max.x);
10432
10433 if (close_button_visible)
10434 {
10435 ImGuiLastItemData last_item_backup = g.LastItemData;
10436 if (CloseButton(close_button_id, button_pos))
10437 close_button_pressed = true;
10438 g.LastItemData = last_item_backup;
10439
10440 // Close with middle mouse button
10441 if (!(flags & ImGuiTabItemFlags_NoCloseWithMiddleMouseButton) && IsMouseClicked(2))
10442 close_button_pressed = true;
10443 }
10444 else if (unsaved_marker_visible)
10445 {
10446 const ImRect bullet_bb(button_pos, button_pos + ImVec2(button_sz, button_sz));
10447 RenderBullet(draw_list, bullet_bb.GetCenter(), GetColorU32(ImGuiCol_Text));
10448 }
10449
10450 // This is all rather complicated
10451 // (the main idea is that because the close button only appears on hover, we don't want it to alter the ellipsis position)
10452 // 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..
10453 float ellipsis_max_x = close_button_visible ? text_pixel_clip_bb.Max.x : bb.Max.x - 1.0f;
10454 if (close_button_visible || unsaved_marker_visible)
10455 {
10456 text_pixel_clip_bb.Max.x -= close_button_visible ? (button_sz) : (button_sz * 0.80f);
10457 text_ellipsis_clip_bb.Max.x -= unsaved_marker_visible ? (button_sz * 0.80f) : 0.0f;
10458 ellipsis_max_x = text_pixel_clip_bb.Max.x;
10459 }
10460 LogSetNextTextDecoration("/", "\\");
10461 RenderTextEllipsis(draw_list, text_ellipsis_clip_bb.Min, text_ellipsis_clip_bb.Max, text_pixel_clip_bb.Max.x, ellipsis_max_x, label, NULL, &label_size);
10462
10463#if 0
10464 if (!is_contents_visible)
10465 g.Style.Alpha = backup_alpha;
10466#endif
10467
10468 if (out_just_closed)
10469 *out_just_closed = close_button_pressed;
10470}
10471
10472
10473#endif // #ifndef IMGUI_DISABLE
uintptr_t id
int g
TclObject t
const char * ImStrbol(const char *buf_mid_line, const char *buf_begin)
Definition imgui.cc:2069
ImGuiContext * GImGui
Definition imgui.cc:1313
ImGuiStoragePair * ImLowerBound(ImGuiStoragePair *in_begin, ImGuiStoragePair *in_end, ImGuiID key)
Definition imgui.cc:2713
void ImTriangleBarycentricCoords(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, const ImVec2 &p, float &out_u, float &out_v, float &out_w)
Definition imgui.cc:1972
int ImFormatString(char *buf, size_t buf_size, const char *fmt,...)
Definition imgui.cc:2145
void ImStrTrimBlanks(char *buf)
Definition imgui.cc:2099
void ImFormatStringToTempBufferV(const char **out_buf, const char **out_buf_end, const char *fmt, va_list args)
Definition imgui.cc:2191
const char * ImTextCharToUtf8(char out_buf[5], unsigned int c)
Definition imgui.cc:2542
bool ImTriangleContainsPoint(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, const ImVec2 &p)
Definition imgui.cc:1964
int ImTextCountUtf8BytesFromStr(const ImWchar *in_text, const ImWchar *in_text_end)
Definition imgui.cc:2581
ImGuiID ImHashStr(const char *data_p, size_t data_size, ImGuiID seed)
Definition imgui.cc:2299
const char * ImTextFindPreviousUtf8Codepoint(const char *in_text_start, const char *in_text_curr)
Definition imgui.cc:2595
IM_MSVC_RUNTIME_CHECKS_OFF int ImTextCharFromUtf8(unsigned int *out_char, const char *in_text, const char *in_text_end)
Definition imgui.cc:2426
int ImTextCountUtf8BytesFromChar(const char *in_text, const char *in_text_end)
Definition imgui.cc:2550
void ImStrncpy(char *dst, const char *src, size_t count)
Definition imgui.cc:2018
ImVec2 ImTriangleClosestPoint(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, const ImVec2 &p)
Definition imgui.cc:1983
#define IMGUI_CDECL
#define STB_TEXTEDIT_K_DOWN
#define STB_TEXTEDIT_K_DELETE
#define STB_TEXTEDIT_K_RIGHT
#define STB_TEXTEDIT_K_SHIFT
const char * ImParseFormatSanitizeForScanning(const char *fmt_in, char *fmt_out, size_t fmt_out_size)
#define STB_TEXTEDIT_K_REDO
#define STB_TEXTEDIT_K_BACKSPACE
#define STB_TEXTEDIT_K_WORDRIGHT
IM_STATIC_ASSERT(IM_ARRAYSIZE(GDataTypeInfo)==ImGuiDataType_COUNT)
#define STB_TEXTEDIT_K_UP
const char * ImParseFormatFindStart(const char *fmt)
#define STB_TEXTEDIT_K_TEXTEND
int ImParseFormatPrecision(const char *fmt, int default_precision)
#define STB_TEXTEDIT_K_LINEEND
#define IMSTB_TEXTEDIT_GETPREVCHARINDEX
#define STB_TEXTEDIT_K_UNDO
#define STB_TEXTEDIT_K_LEFT
#define STB_TEXTEDIT_K_PGUP
#define STB_TEXTEDIT_K_WORDLEFT
#define STB_TEXTEDIT_K_TEXTSTART
const char * ImParseFormatFindEnd(const char *fmt)
const char * ImParseFormatTrimDecorations(const char *fmt, char *buf, size_t buf_size)
void ImParseFormatSanitizeForPrinting(const char *fmt_in, char *fmt_out, size_t fmt_out_size)
#define STB_TEXTEDIT_K_LINESTART
#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX
#define STB_TEXTEDIT_K_PGDOWN
auto CalcTextSize(std::string_view str)
Definition ImGuiUtils.hh:39
void TextUnformatted(const std::string &str)
Definition ImGuiUtils.hh:26
constexpr mat4 scale(const vec3 &xyz)
bool TreeNode(const char *label, ImGuiTreeNodeFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:302
void Combo(const char *label, const char *preview_value, ImGuiComboFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:289
void ListBox(const char *label, const ImVec2 &size, std::invocable<> auto next)
Definition ImGuiCpp.hh:328
void Indent(float indent_w, std::invocable<> auto next)
Definition ImGuiCpp.hh:224
void format(SectorAccessibleDisk &disk, MSXBootSectorType bootType)
Format the given disk (= a single partition).
bool Checkbox(const HotKey &hotKey, BooleanSetting &setting)
Definition ImGuiUtils.cc:58
bool SliderFloat(FloatSetting &setting, const char *format, ImGuiSliderFlags flags)
bool InputText(Setting &setting)
RegFunction R
auto count(InputRange &&range, const T &value)
Definition ranges.hh:349
size_t size(std::string_view utf8)
ImGuiPlotArrayGetterData(const float *values, int stride)
Definition stl_test.cc:7