1// dear imgui, v1.91.0
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#elif defined(__GNUC__)
83#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind
84#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked
85#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead
86#pragma GCC diagnostic ignored "-Wdeprecated-enum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated
87#endif
88
89//-------------------------------------------------------------------------
90// Data
91//-------------------------------------------------------------------------
92
93// Widgets
94static const float DRAGDROP_HOLD_TO_OPEN_TIMER = 0.70f; // Time for drag-hold to activate items accepting the ImGuiButtonFlags_PressedOnDragDropHold button behavior.
95static const float DRAG_MOUSE_THRESHOLD_FACTOR = 0.50f; // Multiplier for the default value of io.MouseDragThreshold to make DragFloat/DragInt react faster to mouse drags.
96
97// Those MIN/MAX values are not define because we need to point to them
98static const signed char IM_S8_MIN = -128;
99static const signed char IM_S8_MAX = 127;
100static const unsigned char IM_U8_MIN = 0;
101static const unsigned char IM_U8_MAX = 0xFF;
102static const signed short IM_S16_MIN = -32768;
103static const signed short IM_S16_MAX = 32767;
104static const unsigned short IM_U16_MIN = 0;
105static const unsigned short IM_U16_MAX = 0xFFFF;
106static const ImS32 IM_S32_MIN = INT_MIN; // (-2147483647 - 1), (0x80000000);
107static const ImS32 IM_S32_MAX = INT_MAX; // (2147483647), (0x7FFFFFFF)
108static const ImU32 IM_U32_MIN = 0;
109static const ImU32 IM_U32_MAX = UINT_MAX; // (0xFFFFFFFF)
110#ifdef LLONG_MIN
111static const ImS64 IM_S64_MIN = LLONG_MIN; // (-9223372036854775807ll - 1ll);
112static const ImS64 IM_S64_MAX = LLONG_MAX; // (9223372036854775807ll);
113#else
114static const ImS64 IM_S64_MIN = -9223372036854775807LL - 1;
115static const ImS64 IM_S64_MAX = 9223372036854775807LL;
116#endif
117static const ImU64 IM_U64_MIN = 0;
118#ifdef ULLONG_MAX
119static const ImU64 IM_U64_MAX = ULLONG_MAX; // (0xFFFFFFFFFFFFFFFFull);
120#else
121static const ImU64 IM_U64_MAX = (2ULL * 9223372036854775807LL + 1);
122#endif
123
124//-------------------------------------------------------------------------
125// [SECTION] Forward Declarations
126//-------------------------------------------------------------------------
127
128// For InputTextEx()
129static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard = false);
130static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end);
131static ImVec2 InputTextCalcTextSizeW(ImGuiContext* ctx, const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false);
132
133//-------------------------------------------------------------------------
134// [SECTION] Widgets: Text, etc.
135//-------------------------------------------------------------------------
136// - TextEx() [Internal]
137// - TextUnformatted()
138// - Text()
139// - TextV()
140// - TextColored()
141// - TextColoredV()
142// - TextDisabled()
143// - TextDisabledV()
144// - TextWrapped()
145// - TextWrappedV()
146// - LabelText()
147// - LabelTextV()
148// - BulletText()
149// - BulletTextV()
150//-------------------------------------------------------------------------
151
152void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags)
153{
154 ImGuiWindow* window = GetCurrentWindow();
155 if (window->SkipItems)
156 return;
157 ImGuiContext& g = *GImGui;
158
159 // Accept null ranges
160 if (text == text_end)
161 text = text_end = "";
162
163 // Calculate length
164 const char* text_begin = text;
165 if (text_end == NULL)
166 text_end = text + strlen(s: text); // FIXME-OPT
167
168 const ImVec2 text_pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
169 const float wrap_pos_x = window->DC.TextWrapPos;
170 const bool wrap_enabled = (wrap_pos_x >= 0.0f);
171 if (text_end - text <= 2000 || wrap_enabled)
172 {
173 // Common case
174 const float wrap_width = wrap_enabled ? CalcWrapWidthForPos(pos: window->DC.CursorPos, wrap_pos_x) : 0.0f;
175 const ImVec2 text_size = CalcTextSize(text: text_begin, text_end, hide_text_after_double_hash: false, wrap_width);
176
177 ImRect bb(text_pos, text_pos + text_size);
178 ItemSize(size: text_size, text_baseline_y: 0.0f);
179 if (!ItemAdd(bb, id: 0))
180 return;
181
182 // Render (we don't hide text after ## in this end-user function)
183 RenderTextWrapped(pos: bb.Min, text: text_begin, text_end, wrap_width);
184 }
185 else
186 {
187 // Long text!
188 // Perform manual coarse clipping to optimize for long multi-line text
189 // - From this point we will only compute the width of lines that are visible. Optimization only available when word-wrapping is disabled.
190 // - We also don't vertically center the text within the line full height, which is unlikely to matter because we are likely the biggest and only item on the line.
191 // - We use memchr(), pay attention that well optimized versions of those str/mem functions are much faster than a casually written loop.
192 const char* line = text;
193 const float line_height = GetTextLineHeight();
194 ImVec2 text_size(0, 0);
195
196 // Lines to skip (can't skip when logging text)
197 ImVec2 pos = text_pos;
198 if (!g.LogEnabled)
199 {
200 int lines_skippable = (int)((window->ClipRect.Min.y - text_pos.y) / line_height);
201 if (lines_skippable > 0)
202 {
203 int lines_skipped = 0;
204 while (line < text_end && lines_skipped < lines_skippable)
205 {
206 const char* line_end = (const char*)memchr(s: line, c: '\n', n: text_end - line);
207 if (!line_end)
208 line_end = text_end;
209 if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0)
210 text_size.x = ImMax(lhs: text_size.x, rhs: CalcTextSize(text: line, text_end: line_end).x);
211 line = line_end + 1;
212 lines_skipped++;
213 }
214 pos.y += lines_skipped * line_height;
215 }
216 }
217
218 // Lines to render
219 if (line < text_end)
220 {
221 ImRect line_rect(pos, pos + ImVec2(FLT_MAX, line_height));
222 while (line < text_end)
223 {
224 if (IsClippedEx(bb: line_rect, id: 0))
225 break;
226
227 const char* line_end = (const char*)memchr(s: line, c: '\n', n: text_end - line);
228 if (!line_end)
229 line_end = text_end;
230 text_size.x = ImMax(lhs: text_size.x, rhs: CalcTextSize(text: line, text_end: line_end).x);
231 RenderText(pos, text: line, text_end: line_end, hide_text_after_hash: false);
232 line = line_end + 1;
233 line_rect.Min.y += line_height;
234 line_rect.Max.y += line_height;
235 pos.y += line_height;
236 }
237
238 // Count remaining lines
239 int lines_skipped = 0;
240 while (line < text_end)
241 {
242 const char* line_end = (const char*)memchr(s: line, c: '\n', n: text_end - line);
243 if (!line_end)
244 line_end = text_end;
245 if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0)
246 text_size.x = ImMax(lhs: text_size.x, rhs: CalcTextSize(text: line, text_end: line_end).x);
247 line = line_end + 1;
248 lines_skipped++;
249 }
250 pos.y += lines_skipped * line_height;
251 }
252 text_size.y = (pos - text_pos).y;
253
254 ImRect bb(text_pos, text_pos + text_size);
255 ItemSize(size: text_size, text_baseline_y: 0.0f);
256 ItemAdd(bb, id: 0);
257 }
258}
259
260void ImGui::TextUnformatted(const char* text, const char* text_end)
261{
262 TextEx(text, text_end, flags: ImGuiTextFlags_NoWidthForLargeClippedText);
263}
264
265void ImGui::Text(const char* fmt, ...)
266{
267 va_list args;
268 va_start(args, fmt);
269 TextV(fmt, args);
270 va_end(args);
271}
272
273void ImGui::TextV(const char* fmt, va_list args)
274{
275 ImGuiWindow* window = GetCurrentWindow();
276 if (window->SkipItems)
277 return;
278
279 const char* text, *text_end;
280 ImFormatStringToTempBufferV(out_buf: &text, out_buf_end: &text_end, fmt, args);
281 TextEx(text, text_end, flags: ImGuiTextFlags_NoWidthForLargeClippedText);
282}
283
284void ImGui::TextColored(const ImVec4& col, const char* fmt, ...)
285{
286 va_list args;
287 va_start(args, fmt);
288 TextColoredV(col, fmt, args);
289 va_end(args);
290}
291
292void ImGui::TextColoredV(const ImVec4& col, const char* fmt, va_list args)
293{
294 PushStyleColor(idx: ImGuiCol_Text, col);
295 TextV(fmt, args);
296 PopStyleColor();
297}
298
299void ImGui::TextDisabled(const char* fmt, ...)
300{
301 va_list args;
302 va_start(args, fmt);
303 TextDisabledV(fmt, args);
304 va_end(args);
305}
306
307void ImGui::TextDisabledV(const char* fmt, va_list args)
308{
309 ImGuiContext& g = *GImGui;
310 PushStyleColor(idx: ImGuiCol_Text, col: g.Style.Colors[ImGuiCol_TextDisabled]);
311 TextV(fmt, args);
312 PopStyleColor();
313}
314
315void ImGui::TextWrapped(const char* fmt, ...)
316{
317 va_list args;
318 va_start(args, fmt);
319 TextWrappedV(fmt, args);
320 va_end(args);
321}
322
323void ImGui::TextWrappedV(const char* fmt, va_list args)
324{
325 ImGuiContext& g = *GImGui;
326 const bool need_backup = (g.CurrentWindow->DC.TextWrapPos < 0.0f); // Keep existing wrap position if one is already set
327 if (need_backup)
328 PushTextWrapPos(wrap_local_pos_x: 0.0f);
329 TextV(fmt, args);
330 if (need_backup)
331 PopTextWrapPos();
332}
333
334void ImGui::LabelText(const char* label, const char* fmt, ...)
335{
336 va_list args;
337 va_start(args, fmt);
338 LabelTextV(label, fmt, args);
339 va_end(args);
340}
341
342// Add a label+text combo aligned to other label+value widgets
343void ImGui::LabelTextV(const char* label, const char* fmt, va_list args)
344{
345 ImGuiWindow* window = GetCurrentWindow();
346 if (window->SkipItems)
347 return;
348
349 ImGuiContext& g = *GImGui;
350 const ImGuiStyle& style = g.Style;
351 const float w = CalcItemWidth();
352
353 const char* value_text_begin, *value_text_end;
354 ImFormatStringToTempBufferV(out_buf: &value_text_begin, out_buf_end: &value_text_end, fmt, args);
355 const ImVec2 value_size = CalcTextSize(text: value_text_begin, text_end: value_text_end, hide_text_after_double_hash: false);
356 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
357
358 const ImVec2 pos = window->DC.CursorPos;
359 const ImRect value_bb(pos, pos + ImVec2(w, value_size.y + style.FramePadding.y * 2));
360 const ImRect total_bb(pos, pos + ImVec2(w + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), ImMax(lhs: value_size.y, rhs: label_size.y) + style.FramePadding.y * 2));
361 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
362 if (!ItemAdd(bb: total_bb, id: 0))
363 return;
364
365 // Render
366 RenderTextClipped(pos_min: value_bb.Min + style.FramePadding, pos_max: value_bb.Max, text: value_text_begin, text_end: value_text_end, text_size_if_known: &value_size, align: ImVec2(0.0f, 0.0f));
367 if (label_size.x > 0.0f)
368 RenderText(pos: ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), text: label);
369}
370
371void ImGui::BulletText(const char* fmt, ...)
372{
373 va_list args;
374 va_start(args, fmt);
375 BulletTextV(fmt, args);
376 va_end(args);
377}
378
379// Text with a little bullet aligned to the typical tree node.
380void ImGui::BulletTextV(const char* fmt, va_list args)
381{
382 ImGuiWindow* window = GetCurrentWindow();
383 if (window->SkipItems)
384 return;
385
386 ImGuiContext& g = *GImGui;
387 const ImGuiStyle& style = g.Style;
388
389 const char* text_begin, *text_end;
390 ImFormatStringToTempBufferV(out_buf: &text_begin, out_buf_end: &text_end, fmt, args);
391 const ImVec2 label_size = CalcTextSize(text: text_begin, text_end, hide_text_after_double_hash: false);
392 const ImVec2 total_size = ImVec2(g.FontSize + (label_size.x > 0.0f ? (label_size.x + style.FramePadding.x * 2) : 0.0f), label_size.y); // Empty text doesn't add padding
393 ImVec2 pos = window->DC.CursorPos;
394 pos.y += window->DC.CurrLineTextBaseOffset;
395 ItemSize(size: total_size, text_baseline_y: 0.0f);
396 const ImRect bb(pos, pos + total_size);
397 if (!ItemAdd(bb, id: 0))
398 return;
399
400 // Render
401 ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
402 RenderBullet(draw_list: window->DrawList, pos: bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, g.FontSize * 0.5f), col: text_col);
403 RenderText(pos: bb.Min + ImVec2(g.FontSize + style.FramePadding.x * 2, 0.0f), text: text_begin, text_end, hide_text_after_hash: false);
404}
405
406//-------------------------------------------------------------------------
407// [SECTION] Widgets: Main
408//-------------------------------------------------------------------------
409// - ButtonBehavior() [Internal]
410// - Button()
411// - SmallButton()
412// - InvisibleButton()
413// - ArrowButton()
414// - CloseButton() [Internal]
415// - CollapseButton() [Internal]
416// - GetWindowScrollbarID() [Internal]
417// - GetWindowScrollbarRect() [Internal]
418// - Scrollbar() [Internal]
419// - ScrollbarEx() [Internal]
420// - Image()
421// - ImageButton()
422// - Checkbox()
423// - CheckboxFlagsT() [Internal]
424// - CheckboxFlags()
425// - RadioButton()
426// - ProgressBar()
427// - Bullet()
428// - Hyperlink()
429//-------------------------------------------------------------------------
430
431// The ButtonBehavior() function is key to many interactions and used by many/most widgets.
432// Because we handle so many cases (keyboard/gamepad navigation, drag and drop) and many specific behavior (via ImGuiButtonFlags_),
433// this code is a little complex.
434// By far the most common path is interacting with the Mouse using the default ImGuiButtonFlags_PressedOnClickRelease button behavior.
435// See the series of events below and the corresponding state reported by dear imgui:
436//------------------------------------------------------------------------------------------------------------------------------------------------
437// with PressedOnClickRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
438// Frame N+0 (mouse is outside bb) - - - - - -
439// Frame N+1 (mouse moves inside bb) - true - - - -
440// Frame N+2 (mouse button is down) - true true true - true
441// Frame N+3 (mouse button is down) - true true - - -
442// Frame N+4 (mouse moves outside bb) - - true - - -
443// Frame N+5 (mouse moves inside bb) - true true - - -
444// Frame N+6 (mouse button is released) true true - - true -
445// Frame N+7 (mouse button is released) - true - - - -
446// Frame N+8 (mouse moves outside bb) - - - - - -
447//------------------------------------------------------------------------------------------------------------------------------------------------
448// with PressedOnClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
449// Frame N+2 (mouse button is down) true true true true - true
450// Frame N+3 (mouse button is down) - true true - - -
451// Frame N+6 (mouse button is released) - true - - true -
452// Frame N+7 (mouse button is released) - true - - - -
453//------------------------------------------------------------------------------------------------------------------------------------------------
454// with PressedOnRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
455// Frame N+2 (mouse button is down) - true - - - true
456// Frame N+3 (mouse button is down) - true - - - -
457// Frame N+6 (mouse button is released) true true - - - -
458// Frame N+7 (mouse button is released) - true - - - -
459//------------------------------------------------------------------------------------------------------------------------------------------------
460// with PressedOnDoubleClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
461// Frame N+0 (mouse button is down) - true - - - true
462// Frame N+1 (mouse button is down) - true - - - -
463// Frame N+2 (mouse button is released) - true - - - -
464// Frame N+3 (mouse button is released) - true - - - -
465// Frame N+4 (mouse button is down) true true true true - true
466// Frame N+5 (mouse button is down) - true true - - -
467// Frame N+6 (mouse button is released) - true - - true -
468// Frame N+7 (mouse button is released) - true - - - -
469//------------------------------------------------------------------------------------------------------------------------------------------------
470// Note that some combinations are supported,
471// - PressedOnDragDropHold can generally be associated with any flag.
472// - PressedOnDoubleClick can be associated by PressedOnClickRelease/PressedOnRelease, in which case the second release event won't be reported.
473//------------------------------------------------------------------------------------------------------------------------------------------------
474// The behavior of the return-value changes when ImGuiButtonFlags_Repeat is set:
475// Repeat+ Repeat+ Repeat+ Repeat+
476// PressedOnClickRelease PressedOnClick PressedOnRelease PressedOnDoubleClick
477//-------------------------------------------------------------------------------------------------------------------------------------------------
478// Frame N+0 (mouse button is down) - true - true
479// ... - - - -
480// Frame N + RepeatDelay true true - true
481// ... - - - -
482// Frame N + RepeatDelay + RepeatRate*N true true - true
483//-------------------------------------------------------------------------------------------------------------------------------------------------
484
485// FIXME: For refactor we could output flags, incl mouse hovered vs nav keyboard vs nav triggered etc.
486// And better standardize how widgets use 'GetColor32((held && hovered) ? ... : hovered ? ...)' vs 'GetColor32(held ? ... : hovered ? ...);'
487// For mouse feedback we typically prefer the 'held && hovered' test, but for nav feedback not always. Outputting hovered=true on Activation may be misleading.
488bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags)
489{
490 ImGuiContext& g = *GImGui;
491 ImGuiWindow* window = GetCurrentWindow();
492
493 // Default only reacts to left mouse button
494 if ((flags & ImGuiButtonFlags_MouseButtonMask_) == 0)
495 flags |= ImGuiButtonFlags_MouseButtonLeft;
496
497 // Default behavior requires click + release inside bounding box
498 if ((flags & ImGuiButtonFlags_PressedOnMask_) == 0)
499 flags |= ImGuiButtonFlags_PressedOnDefault_;
500
501 // Default behavior inherited from item flags
502 // Note that _both_ ButtonFlags and ItemFlags are valid sources, so copy one into the item_flags and only check that.
503 ImGuiItemFlags item_flags = (g.LastItemData.ID == id ? g.LastItemData.InFlags : g.CurrentItemFlags);
504 if (flags & ImGuiButtonFlags_AllowOverlap)
505 item_flags |= ImGuiItemFlags_AllowOverlap;
506 if (flags & ImGuiButtonFlags_Repeat)
507 item_flags |= ImGuiItemFlags_ButtonRepeat;
508
509 ImGuiWindow* backup_hovered_window = g.HoveredWindow;
510 const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredWindow && g.HoveredWindow->RootWindow == window;
511 if (flatten_hovered_children)
512 g.HoveredWindow = window;
513
514#ifdef IMGUI_ENABLE_TEST_ENGINE
515 // Alternate registration spot, for when caller didn't use ItemAdd()
516 if (g.LastItemData.ID != id)
517 IMGUI_TEST_ENGINE_ITEM_ADD(id, bb, NULL);
518#endif
519
520 bool pressed = false;
521 bool hovered = ItemHoverable(bb, id, item_flags);
522
523 // Special mode for Drag and Drop where holding button pressed for a long time while dragging another item triggers the button
524 if (g.DragDropActive && (flags & ImGuiButtonFlags_PressedOnDragDropHold) && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers))
525 if (IsItemHovered(flags: ImGuiHoveredFlags_AllowWhenBlockedByActiveItem))
526 {
527 hovered = true;
528 SetHoveredID(id);
529 if (g.HoveredIdTimer - g.IO.DeltaTime <= DRAGDROP_HOLD_TO_OPEN_TIMER && g.HoveredIdTimer >= DRAGDROP_HOLD_TO_OPEN_TIMER)
530 {
531 pressed = true;
532 g.DragDropHoldJustPressedId = id;
533 FocusWindow(window);
534 }
535 }
536
537 if (flatten_hovered_children)
538 g.HoveredWindow = backup_hovered_window;
539
540 // Mouse handling
541 const ImGuiID test_owner_id = (flags & ImGuiButtonFlags_NoTestKeyOwner) ? ImGuiKeyOwner_Any : id;
542 if (hovered)
543 {
544 IM_ASSERT(id != 0); // Lazily check inside rare path.
545
546 // Poll mouse buttons
547 // - 'mouse_button_clicked' is generally carried into ActiveIdMouseButton when setting ActiveId.
548 // - Technically we only need some values in one code path, but since this is gated by hovered test this is fine.
549 int mouse_button_clicked = -1;
550 int mouse_button_released = -1;
551 for (int button = 0; button < 3; button++)
552 if (flags & (ImGuiButtonFlags_MouseButtonLeft << button)) // Handle ImGuiButtonFlags_MouseButtonRight and ImGuiButtonFlags_MouseButtonMiddle here.
553 {
554 if (IsMouseClicked(button, flags: ImGuiInputFlags_None, owner_id: test_owner_id) && mouse_button_clicked == -1) { mouse_button_clicked = button; }
555 if (IsMouseReleased(button, owner_id: test_owner_id) && mouse_button_released == -1) { mouse_button_released = button; }
556 }
557
558 // Process initial action
559 if (!(flags & ImGuiButtonFlags_NoKeyModifiers) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt))
560 {
561 if (mouse_button_clicked != -1 && g.ActiveId != id)
562 {
563 if (!(flags & ImGuiButtonFlags_NoSetKeyOwner))
564 SetKeyOwner(key: MouseButtonToKey(button: mouse_button_clicked), owner_id: id);
565 if (flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClickReleaseAnywhere))
566 {
567 SetActiveID(id, window);
568 g.ActiveIdMouseButton = mouse_button_clicked;
569 if (!(flags & ImGuiButtonFlags_NoNavFocus))
570 {
571 SetFocusID(id, window);
572 FocusWindow(window);
573 }
574 else
575 {
576 FocusWindow(window, flags: ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child
577 }
578 }
579 if ((flags & ImGuiButtonFlags_PressedOnClick) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseClickedCount[mouse_button_clicked] == 2))
580 {
581 pressed = true;
582 if (flags & ImGuiButtonFlags_NoHoldingActiveId)
583 ClearActiveID();
584 else
585 SetActiveID(id, window); // Hold on ID
586 g.ActiveIdMouseButton = mouse_button_clicked;
587 if (!(flags & ImGuiButtonFlags_NoNavFocus))
588 {
589 SetFocusID(id, window);
590 FocusWindow(window);
591 }
592 else
593 {
594 FocusWindow(window, flags: ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child
595 }
596 }
597 }
598 if (flags & ImGuiButtonFlags_PressedOnRelease)
599 {
600 if (mouse_button_released != -1)
601 {
602 const bool has_repeated_at_least_once = (item_flags & ImGuiItemFlags_ButtonRepeat) && g.IO.MouseDownDurationPrev[mouse_button_released] >= g.IO.KeyRepeatDelay; // Repeat mode trumps on release behavior
603 if (!has_repeated_at_least_once)
604 pressed = true;
605 if (!(flags & ImGuiButtonFlags_NoNavFocus))
606 SetFocusID(id, window);
607 ClearActiveID();
608 }
609 }
610
611 // 'Repeat' mode acts when held regardless of _PressedOn flags (see table above).
612 // Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings.
613 if (g.ActiveId == id && (item_flags & ImGuiItemFlags_ButtonRepeat))
614 if (g.IO.MouseDownDuration[g.ActiveIdMouseButton] > 0.0f && IsMouseClicked(button: g.ActiveIdMouseButton, flags: ImGuiInputFlags_Repeat, owner_id: test_owner_id))
615 pressed = true;
616 }
617
618 if (pressed)
619 g.NavDisableHighlight = true;
620 }
621
622 // Gamepad/Keyboard handling
623 // We report navigated and navigation-activated items as hovered but we don't set g.HoveredId to not interfere with mouse.
624 if (g.NavId == id && !g.NavDisableHighlight && g.NavDisableMouseHover)
625 if (!(flags & ImGuiButtonFlags_NoHoveredOnFocus))
626 hovered = true;
627 if (g.NavActivateDownId == id)
628 {
629 bool nav_activated_by_code = (g.NavActivateId == id);
630 bool nav_activated_by_inputs = (g.NavActivatePressedId == id);
631 if (!nav_activated_by_inputs && (item_flags & ImGuiItemFlags_ButtonRepeat))
632 {
633 // Avoid pressing multiple keys from triggering excessive amount of repeat events
634 const ImGuiKeyData* key1 = GetKeyData(key: ImGuiKey_Space);
635 const ImGuiKeyData* key2 = GetKeyData(key: ImGuiKey_Enter);
636 const ImGuiKeyData* key3 = GetKeyData(ImGuiKey_NavGamepadActivate);
637 const float t1 = ImMax(lhs: ImMax(lhs: key1->DownDuration, rhs: key2->DownDuration), rhs: key3->DownDuration);
638 nav_activated_by_inputs = CalcTypematicRepeatAmount(t0: t1 - g.IO.DeltaTime, t1, repeat_delay: g.IO.KeyRepeatDelay, repeat_rate: g.IO.KeyRepeatRate) > 0;
639 }
640 if (nav_activated_by_code || nav_activated_by_inputs)
641 {
642 // Set active id so it can be queried by user via IsItemActive(), equivalent of holding the mouse button.
643 pressed = true;
644 SetActiveID(id, window);
645 g.ActiveIdSource = g.NavInputSource;
646 if (!(flags & ImGuiButtonFlags_NoNavFocus) && !(g.NavActivateFlags & ImGuiActivateFlags_FromShortcut))
647 SetFocusID(id, window);
648 if (g.NavActivateFlags & ImGuiActivateFlags_FromShortcut)
649 g.ActiveIdFromShortcut = true;
650 }
651 }
652
653 // Process while held
654 bool held = false;
655 if (g.ActiveId == id)
656 {
657 if (g.ActiveIdSource == ImGuiInputSource_Mouse)
658 {
659 if (g.ActiveIdIsJustActivated)
660 g.ActiveIdClickOffset = g.IO.MousePos - bb.Min;
661
662 const int mouse_button = g.ActiveIdMouseButton;
663 if (mouse_button == -1)
664 {
665 // Fallback for the rare situation were g.ActiveId was set programmatically or from another widget (e.g. #6304).
666 ClearActiveID();
667 }
668 else if (IsMouseDown(button: mouse_button, owner_id: test_owner_id))
669 {
670 held = true;
671 }
672 else
673 {
674 bool release_in = hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease) != 0;
675 bool release_anywhere = (flags & ImGuiButtonFlags_PressedOnClickReleaseAnywhere) != 0;
676 if ((release_in || release_anywhere) && !g.DragDropActive)
677 {
678 // Report as pressed when releasing the mouse (this is the most common path)
679 bool is_double_click_release = (flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseReleased[mouse_button] && g.IO.MouseClickedLastCount[mouse_button] == 2;
680 bool is_repeating_already = (item_flags & ImGuiItemFlags_ButtonRepeat) && g.IO.MouseDownDurationPrev[mouse_button] >= g.IO.KeyRepeatDelay; // Repeat mode trumps <on release>
681 bool is_button_avail_or_owned = TestKeyOwner(key: MouseButtonToKey(button: mouse_button), owner_id: test_owner_id);
682 if (!is_double_click_release && !is_repeating_already && is_button_avail_or_owned)
683 pressed = true;
684 }
685 ClearActiveID();
686 }
687 if (!(flags & ImGuiButtonFlags_NoNavFocus))
688 g.NavDisableHighlight = true;
689 }
690 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
691 {
692 // When activated using Nav, we hold on the ActiveID until activation button is released
693 if (g.NavActivateDownId == id)
694 held = true; // hovered == true not true as we are already likely hovered on direct activation.
695 else
696 ClearActiveID();
697 }
698 if (pressed)
699 g.ActiveIdHasBeenPressedBefore = true;
700 }
701
702 // Activation highlight (this may be a remote activation)
703 if (g.NavHighlightActivatedId == id)
704 hovered = true;
705
706 if (out_hovered) *out_hovered = hovered;
707 if (out_held) *out_held = held;
708
709 return pressed;
710}
711
712bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags)
713{
714 ImGuiWindow* window = GetCurrentWindow();
715 if (window->SkipItems)
716 return false;
717
718 ImGuiContext& g = *GImGui;
719 const ImGuiStyle& style = g.Style;
720 const ImGuiID id = window->GetID(str: label);
721 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
722
723 ImVec2 pos = window->DC.CursorPos;
724 if ((flags & ImGuiButtonFlags_AlignTextBaseLine) && style.FramePadding.y < window->DC.CurrLineTextBaseOffset) // Try to vertically align buttons that are smaller/have no padding so that text baseline matches (bit hacky, since it shouldn't be a flag)
725 pos.y += window->DC.CurrLineTextBaseOffset - style.FramePadding.y;
726 ImVec2 size = CalcItemSize(size: size_arg, default_w: label_size.x + style.FramePadding.x * 2.0f, default_h: label_size.y + style.FramePadding.y * 2.0f);
727
728 const ImRect bb(pos, pos + size);
729 ItemSize(size, text_baseline_y: style.FramePadding.y);
730 if (!ItemAdd(bb, id))
731 return false;
732
733 bool hovered, held;
734 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags);
735
736 // Render
737 const ImU32 col = GetColorU32(idx: (held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
738 RenderNavHighlight(bb, id);
739 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: col, border: true, rounding: style.FrameRounding);
740
741 if (g.LogEnabled)
742 LogSetNextTextDecoration(prefix: "[", suffix: "]");
743 RenderTextClipped(pos_min: bb.Min + style.FramePadding, pos_max: bb.Max - style.FramePadding, text: label, NULL, text_size_if_known: &label_size, align: style.ButtonTextAlign, clip_rect: &bb);
744
745 // Automatically close popups
746 //if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup))
747 // CloseCurrentPopup();
748
749 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
750 return pressed;
751}
752
753bool ImGui::Button(const char* label, const ImVec2& size_arg)
754{
755 return ButtonEx(label, size_arg, flags: ImGuiButtonFlags_None);
756}
757
758// Small buttons fits within text without additional vertical spacing.
759bool ImGui::SmallButton(const char* label)
760{
761 ImGuiContext& g = *GImGui;
762 float backup_padding_y = g.Style.FramePadding.y;
763 g.Style.FramePadding.y = 0.0f;
764 bool pressed = ButtonEx(label, size_arg: ImVec2(0, 0), flags: ImGuiButtonFlags_AlignTextBaseLine);
765 g.Style.FramePadding.y = backup_padding_y;
766 return pressed;
767}
768
769// Tip: use ImGui::PushID()/PopID() to push indices or pointers in the ID stack.
770// Then you can keep 'str_id' empty or the same for all your buttons (instead of creating a string based on a non-string id)
771bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg, ImGuiButtonFlags flags)
772{
773 ImGuiContext& g = *GImGui;
774 ImGuiWindow* window = GetCurrentWindow();
775 if (window->SkipItems)
776 return false;
777
778 // Cannot use zero-size for InvisibleButton(). Unlike Button() there is not way to fallback using the label size.
779 IM_ASSERT(size_arg.x != 0.0f && size_arg.y != 0.0f);
780
781 const ImGuiID id = window->GetID(str: str_id);
782 ImVec2 size = CalcItemSize(size: size_arg, default_w: 0.0f, default_h: 0.0f);
783 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
784 ItemSize(size);
785 if (!ItemAdd(bb, id))
786 return false;
787
788 bool hovered, held;
789 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags);
790
791 IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
792 return pressed;
793}
794
795bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiButtonFlags flags)
796{
797 ImGuiContext& g = *GImGui;
798 ImGuiWindow* window = GetCurrentWindow();
799 if (window->SkipItems)
800 return false;
801
802 const ImGuiID id = window->GetID(str: str_id);
803 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
804 const float default_size = GetFrameHeight();
805 ItemSize(size, text_baseline_y: (size.y >= default_size) ? g.Style.FramePadding.y : -1.0f);
806 if (!ItemAdd(bb, id))
807 return false;
808
809 bool hovered, held;
810 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags);
811
812 // Render
813 const ImU32 bg_col = GetColorU32(idx: (held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
814 const ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
815 RenderNavHighlight(bb, id);
816 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: bg_col, border: true, rounding: g.Style.FrameRounding);
817 RenderArrow(draw_list: window->DrawList, pos: bb.Min + ImVec2(ImMax(lhs: 0.0f, rhs: (size.x - g.FontSize) * 0.5f), ImMax(lhs: 0.0f, rhs: (size.y - g.FontSize) * 0.5f)), col: text_col, dir);
818
819 IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
820 return pressed;
821}
822
823bool ImGui::ArrowButton(const char* str_id, ImGuiDir dir)
824{
825 float sz = GetFrameHeight();
826 return ArrowButtonEx(str_id, dir, size: ImVec2(sz, sz), flags: ImGuiButtonFlags_None);
827}
828
829// Button to close a window
830bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos)
831{
832 ImGuiContext& g = *GImGui;
833 ImGuiWindow* window = g.CurrentWindow;
834
835 // Tweak 1: Shrink hit-testing area if button covers an abnormally large proportion of the visible region. That's in order to facilitate moving the window away. (#3825)
836 // This may better be applied as a general hit-rect reduction mechanism for all widgets to ensure the area to move window is always accessible?
837 const ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize));
838 ImRect bb_interact = bb;
839 const float area_to_visible_ratio = window->OuterRectClipped.GetArea() / bb.GetArea();
840 if (area_to_visible_ratio < 1.5f)
841 bb_interact.Expand(amount: ImTrunc(v: bb_interact.GetSize() * -0.25f));
842
843 // Tweak 2: We intentionally allow interaction when clipped so that a mechanical Alt,Right,Activate sequence can always close a window.
844 // (this isn't the common behavior of buttons, but it doesn't affect the user because navigation tends to keep items visible in scrolling layer).
845 bool is_clipped = !ItemAdd(bb: bb_interact, id);
846
847 bool hovered, held;
848 bool pressed = ButtonBehavior(bb: bb_interact, id, out_hovered: &hovered, out_held: &held);
849 if (is_clipped)
850 return pressed;
851
852 // Render
853 ImU32 bg_col = GetColorU32(idx: held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered);
854 if (hovered)
855 window->DrawList->AddRectFilled(p_min: bb.Min, p_max: bb.Max, col: bg_col);
856 RenderNavHighlight(bb, id, flags: ImGuiNavHighlightFlags_Compact);
857 ImU32 cross_col = GetColorU32(idx: ImGuiCol_Text);
858 ImVec2 cross_center = bb.GetCenter() - ImVec2(0.5f, 0.5f);
859 float cross_extent = g.FontSize * 0.5f * 0.7071f - 1.0f;
860 window->DrawList->AddLine(p1: cross_center + ImVec2(+cross_extent, +cross_extent), p2: cross_center + ImVec2(-cross_extent, -cross_extent), col: cross_col, thickness: 1.0f);
861 window->DrawList->AddLine(p1: cross_center + ImVec2(+cross_extent, -cross_extent), p2: cross_center + ImVec2(-cross_extent, +cross_extent), col: cross_col, thickness: 1.0f);
862
863 return pressed;
864}
865
866bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos)
867{
868 ImGuiContext& g = *GImGui;
869 ImGuiWindow* window = g.CurrentWindow;
870
871 ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize));
872 bool is_clipped = !ItemAdd(bb, id);
873 bool hovered, held;
874 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags: ImGuiButtonFlags_None);
875 if (is_clipped)
876 return pressed;
877
878 // Render
879 ImU32 bg_col = GetColorU32(idx: (held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
880 ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
881 if (hovered || held)
882 window->DrawList->AddRectFilled(p_min: bb.Min, p_max: bb.Max, col: bg_col);
883 RenderNavHighlight(bb, id, flags: ImGuiNavHighlightFlags_Compact);
884 RenderArrow(draw_list: window->DrawList, pos: bb.Min, col: text_col, dir: window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, scale: 1.0f);
885
886 // Switch to moving the window after mouse is moved beyond the initial drag threshold
887 if (IsItemActive() && IsMouseDragging(button: 0))
888 StartMouseMovingWindow(window);
889
890 return pressed;
891}
892
893ImGuiID ImGui::GetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis)
894{
895 return window->GetID(str: axis == ImGuiAxis_X ? "#SCROLLX" : "#SCROLLY");
896}
897
898// Return scrollbar rectangle, must only be called for corresponding axis if window->ScrollbarX/Y is set.
899ImRect ImGui::GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis)
900{
901 const ImRect outer_rect = window->Rect();
902 const ImRect inner_rect = window->InnerRect;
903 const float border_size = window->WindowBorderSize;
904 const float scrollbar_size = window->ScrollbarSizes[axis ^ 1]; // (ScrollbarSizes.x = width of Y scrollbar; ScrollbarSizes.y = height of X scrollbar)
905 IM_ASSERT(scrollbar_size > 0.0f);
906 if (axis == ImGuiAxis_X)
907 return ImRect(inner_rect.Min.x, ImMax(lhs: outer_rect.Min.y, rhs: outer_rect.Max.y - border_size - scrollbar_size), inner_rect.Max.x - border_size, outer_rect.Max.y - border_size);
908 else
909 return ImRect(ImMax(lhs: outer_rect.Min.x, rhs: outer_rect.Max.x - border_size - scrollbar_size), inner_rect.Min.y, outer_rect.Max.x - border_size, inner_rect.Max.y - border_size);
910}
911
912void ImGui::Scrollbar(ImGuiAxis axis)
913{
914 ImGuiContext& g = *GImGui;
915 ImGuiWindow* window = g.CurrentWindow;
916 const ImGuiID id = GetWindowScrollbarID(window, axis);
917
918 // Calculate scrollbar bounding box
919 ImRect bb = GetWindowScrollbarRect(window, axis);
920 ImDrawFlags rounding_corners = ImDrawFlags_RoundCornersNone;
921 if (axis == ImGuiAxis_X)
922 {
923 rounding_corners |= ImDrawFlags_RoundCornersBottomLeft;
924 if (!window->ScrollbarY)
925 rounding_corners |= ImDrawFlags_RoundCornersBottomRight;
926 }
927 else
928 {
929 if ((window->Flags & ImGuiWindowFlags_NoTitleBar) && !(window->Flags & ImGuiWindowFlags_MenuBar))
930 rounding_corners |= ImDrawFlags_RoundCornersTopRight;
931 if (!window->ScrollbarX)
932 rounding_corners |= ImDrawFlags_RoundCornersBottomRight;
933 }
934 float size_visible = window->InnerRect.Max[axis] - window->InnerRect.Min[axis];
935 float size_contents = window->ContentSize[axis] + window->WindowPadding[axis] * 2.0f;
936 ImS64 scroll = (ImS64)window->Scroll[axis];
937 ScrollbarEx(bb, id, axis, p_scroll_v: &scroll, avail_v: (ImS64)size_visible, contents_v: (ImS64)size_contents, flags: rounding_corners);
938 window->Scroll[axis] = (float)scroll;
939}
940
941// Vertical/Horizontal scrollbar
942// The entire piece of code below is rather confusing because:
943// - We handle absolute seeking (when first clicking outside the grab) and relative manipulation (afterward or when clicking inside the grab)
944// - We store values as normalized ratio and in a form that allows the window content to change while we are holding on a scrollbar
945// - We handle both horizontal and vertical scrollbars, which makes the terminology not ideal.
946// Still, the code should probably be made simpler..
947bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, ImS64* p_scroll_v, ImS64 size_visible_v, ImS64 size_contents_v, ImDrawFlags flags)
948{
949 ImGuiContext& g = *GImGui;
950 ImGuiWindow* window = g.CurrentWindow;
951 if (window->SkipItems)
952 return false;
953
954 const float bb_frame_width = bb_frame.GetWidth();
955 const float bb_frame_height = bb_frame.GetHeight();
956 if (bb_frame_width <= 0.0f || bb_frame_height <= 0.0f)
957 return false;
958
959 // When we are too small, start hiding and disabling the grab (this reduce visual noise on very small window and facilitate using the window resize grab)
960 float alpha = 1.0f;
961 if ((axis == ImGuiAxis_Y) && bb_frame_height < g.FontSize + g.Style.FramePadding.y * 2.0f)
962 alpha = ImSaturate(f: (bb_frame_height - g.FontSize) / (g.Style.FramePadding.y * 2.0f));
963 if (alpha <= 0.0f)
964 return false;
965
966 const ImGuiStyle& style = g.Style;
967 const bool allow_interaction = (alpha >= 1.0f);
968
969 ImRect bb = bb_frame;
970 bb.Expand(amount: ImVec2(-ImClamp(IM_TRUNC((bb_frame_width - 2.0f) * 0.5f), mn: 0.0f, mx: 3.0f), -ImClamp(IM_TRUNC((bb_frame_height - 2.0f) * 0.5f), mn: 0.0f, mx: 3.0f)));
971
972 // V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar)
973 const float scrollbar_size_v = (axis == ImGuiAxis_X) ? bb.GetWidth() : bb.GetHeight();
974
975 // Calculate the height of our grabbable box. It generally represent the amount visible (vs the total scrollable amount)
976 // But we maintain a minimum size in pixel to allow for the user to still aim inside.
977 IM_ASSERT(ImMax(size_contents_v, size_visible_v) > 0.0f); // Adding this assert to check if the ImMax(XXX,1.0f) is still needed. PLEASE CONTACT ME if this triggers.
978 const ImS64 win_size_v = ImMax(lhs: ImMax(lhs: size_contents_v, rhs: size_visible_v), rhs: (ImS64)1);
979 const float grab_h_pixels = ImClamp(v: scrollbar_size_v * ((float)size_visible_v / (float)win_size_v), mn: style.GrabMinSize, mx: scrollbar_size_v);
980 const float grab_h_norm = grab_h_pixels / scrollbar_size_v;
981
982 // Handle input right away. None of the code of Begin() is relying on scrolling position before calling Scrollbar().
983 bool held = false;
984 bool hovered = false;
985 ItemAdd(bb: bb_frame, id, NULL, extra_flags: ImGuiItemFlags_NoNav);
986 ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags: ImGuiButtonFlags_NoNavFocus);
987
988 const ImS64 scroll_max = ImMax(lhs: (ImS64)1, rhs: size_contents_v - size_visible_v);
989 float scroll_ratio = ImSaturate(f: (float)*p_scroll_v / (float)scroll_max);
990 float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; // Grab position in normalized space
991 if (held && allow_interaction && grab_h_norm < 1.0f)
992 {
993 const float scrollbar_pos_v = bb.Min[axis];
994 const float mouse_pos_v = g.IO.MousePos[axis];
995
996 // Click position in scrollbar normalized space (0.0f->1.0f)
997 const float clicked_v_norm = ImSaturate(f: (mouse_pos_v - scrollbar_pos_v) / scrollbar_size_v);
998
999 const int held_dir = (clicked_v_norm < grab_v_norm) ? -1 : (clicked_v_norm > grab_v_norm + grab_h_norm) ? +1 : 0;
1000 if (g.ActiveIdIsJustActivated)
1001 {
1002 // On initial click calculate the distance between mouse and the center of the grab
1003 g.ScrollbarSeekMode = (short)held_dir;
1004 g.ScrollbarClickDeltaToGrabCenter = (g.ScrollbarSeekMode == 0.0f) ? clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f : 0.0f;
1005 }
1006
1007 // Apply scroll (p_scroll_v will generally point on one member of window->Scroll)
1008 // It is ok to modify Scroll here because we are being called in Begin() after the calculation of ContentSize and before setting up our starting position
1009 if (g.ScrollbarSeekMode == 0)
1010 {
1011 // Absolute seeking
1012 const float scroll_v_norm = ImSaturate(f: (clicked_v_norm - g.ScrollbarClickDeltaToGrabCenter - grab_h_norm * 0.5f) / (1.0f - grab_h_norm));
1013 *p_scroll_v = (ImS64)(scroll_v_norm * scroll_max);
1014 }
1015 else
1016 {
1017 // Page by page
1018 if (IsMouseClicked(button: ImGuiMouseButton_Left, flags: ImGuiInputFlags_Repeat) && held_dir == g.ScrollbarSeekMode)
1019 {
1020 float page_dir = (g.ScrollbarSeekMode > 0.0f) ? +1.0f : -1.0f;
1021 *p_scroll_v = ImClamp(v: *p_scroll_v + (ImS64)(page_dir * size_visible_v), mn: (ImS64)0, mx: scroll_max);
1022 }
1023 }
1024
1025 // Update values for rendering
1026 scroll_ratio = ImSaturate(f: (float)*p_scroll_v / (float)scroll_max);
1027 grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v;
1028
1029 // Update distance to grab now that we have seek'ed and saturated
1030 //if (seek_absolute)
1031 // g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f;
1032 }
1033
1034 // Render
1035 const ImU32 bg_col = GetColorU32(idx: ImGuiCol_ScrollbarBg);
1036 const ImU32 grab_col = GetColorU32(idx: held ? ImGuiCol_ScrollbarGrabActive : hovered ? ImGuiCol_ScrollbarGrabHovered : ImGuiCol_ScrollbarGrab, alpha_mul: alpha);
1037 window->DrawList->AddRectFilled(p_min: bb_frame.Min, p_max: bb_frame.Max, col: bg_col, rounding: window->WindowRounding, flags);
1038 ImRect grab_rect;
1039 if (axis == ImGuiAxis_X)
1040 grab_rect = ImRect(ImLerp(a: bb.Min.x, b: bb.Max.x, t: grab_v_norm), bb.Min.y, ImLerp(a: bb.Min.x, b: bb.Max.x, t: grab_v_norm) + grab_h_pixels, bb.Max.y);
1041 else
1042 grab_rect = ImRect(bb.Min.x, ImLerp(a: bb.Min.y, b: bb.Max.y, t: grab_v_norm), bb.Max.x, ImLerp(a: bb.Min.y, b: bb.Max.y, t: grab_v_norm) + grab_h_pixels);
1043 window->DrawList->AddRectFilled(p_min: grab_rect.Min, p_max: grab_rect.Max, col: grab_col, rounding: style.ScrollbarRounding);
1044
1045 return held;
1046}
1047
1048// - Read about ImTextureID here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples
1049// - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above.
1050void ImGui::Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col)
1051{
1052 ImGuiWindow* window = GetCurrentWindow();
1053 if (window->SkipItems)
1054 return;
1055
1056 const float border_size = (border_col.w > 0.0f) ? 1.0f : 0.0f;
1057 const ImVec2 padding(border_size, border_size);
1058 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f);
1059 ItemSize(bb);
1060 if (!ItemAdd(bb, id: 0))
1061 return;
1062
1063 // Render
1064 if (border_size > 0.0f)
1065 window->DrawList->AddRect(p_min: bb.Min, p_max: bb.Max, col: GetColorU32(col: border_col), rounding: 0.0f, flags: ImDrawFlags_None, thickness: border_size);
1066 window->DrawList->AddImage(user_texture_id, p_min: bb.Min + padding, p_max: bb.Max - padding, uv_min: uv0, uv_max: uv1, col: GetColorU32(col: tint_col));
1067}
1068
1069// ImageButton() is flawed as 'id' is always derived from 'texture_id' (see #2464 #1390)
1070// We provide this internal helper to write your own variant while we figure out how to redesign the public ImageButton() API.
1071bool ImGui::ImageButtonEx(ImGuiID id, ImTextureID texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col, ImGuiButtonFlags flags)
1072{
1073 ImGuiContext& g = *GImGui;
1074 ImGuiWindow* window = GetCurrentWindow();
1075 if (window->SkipItems)
1076 return false;
1077
1078 const ImVec2 padding = g.Style.FramePadding;
1079 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f);
1080 ItemSize(bb);
1081 if (!ItemAdd(bb, id))
1082 return false;
1083
1084 bool hovered, held;
1085 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags);
1086
1087 // Render
1088 const ImU32 col = GetColorU32(idx: (held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1089 RenderNavHighlight(bb, id);
1090 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: col, border: true, rounding: ImClamp(v: (float)ImMin(lhs: padding.x, rhs: padding.y), mn: 0.0f, mx: g.Style.FrameRounding));
1091 if (bg_col.w > 0.0f)
1092 window->DrawList->AddRectFilled(p_min: bb.Min + padding, p_max: bb.Max - padding, col: GetColorU32(col: bg_col));
1093 window->DrawList->AddImage(user_texture_id: texture_id, p_min: bb.Min + padding, p_max: bb.Max - padding, uv_min: uv0, uv_max: uv1, col: GetColorU32(col: tint_col));
1094
1095 return pressed;
1096}
1097
1098// Note that ImageButton() adds style.FramePadding*2.0f to provided size. This is in order to facilitate fitting an image in a button.
1099bool ImGui::ImageButton(const char* str_id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col)
1100{
1101 ImGuiContext& g = *GImGui;
1102 ImGuiWindow* window = g.CurrentWindow;
1103 if (window->SkipItems)
1104 return false;
1105
1106 return ImageButtonEx(id: window->GetID(str: str_id), texture_id: user_texture_id, image_size, uv0, uv1, bg_col, tint_col);
1107}
1108
1109#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1110// Legacy API obsoleted in 1.89. Two differences with new ImageButton()
1111// - new ImageButton() requires an explicit 'const char* str_id' Old ImageButton() used opaque imTextureId (created issue with: multiple buttons with same image, transient texture id values, opaque computation of ID)
1112// - new ImageButton() always use style.FramePadding Old ImageButton() had an override argument.
1113// If you need to change padding with new ImageButton() you can use PushStyleVar(ImGuiStyleVar_FramePadding, value), consistent with other Button functions.
1114bool ImGui::ImageButton(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, int frame_padding, const ImVec4& bg_col, const ImVec4& tint_col)
1115{
1116 ImGuiContext& g = *GImGui;
1117 ImGuiWindow* window = g.CurrentWindow;
1118 if (window->SkipItems)
1119 return false;
1120
1121 // Default to using texture ID as ID. User can still push string/integer prefixes.
1122 PushID(ptr_id: (void*)(intptr_t)user_texture_id);
1123 const ImGuiID id = window->GetID(str: "#image");
1124 PopID();
1125
1126 if (frame_padding >= 0)
1127 PushStyleVar(idx: ImGuiStyleVar_FramePadding, val: ImVec2((float)frame_padding, (float)frame_padding));
1128 bool ret = ImageButtonEx(id, texture_id: user_texture_id, image_size: size, uv0, uv1, bg_col, tint_col);
1129 if (frame_padding >= 0)
1130 PopStyleVar();
1131 return ret;
1132}
1133#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1134
1135bool ImGui::Checkbox(const char* label, bool* v)
1136{
1137 ImGuiWindow* window = GetCurrentWindow();
1138 if (window->SkipItems)
1139 return false;
1140
1141 ImGuiContext& g = *GImGui;
1142 const ImGuiStyle& style = g.Style;
1143 const ImGuiID id = window->GetID(str: label);
1144 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
1145
1146 const float square_sz = GetFrameHeight();
1147 const ImVec2 pos = window->DC.CursorPos;
1148 const ImRect total_bb(pos, pos + ImVec2(square_sz + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), label_size.y + style.FramePadding.y * 2.0f));
1149 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
1150 const bool is_visible = ItemAdd(bb: total_bb, id);
1151 const bool is_multi_select = (g.LastItemData.InFlags & ImGuiItemFlags_IsMultiSelect) != 0;
1152 if (!is_visible)
1153 if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(r: total_bb)) // Extra layer of "no logic clip" for box-select support
1154 {
1155 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1156 return false;
1157 }
1158
1159 // Range-Selection/Multi-selection support (header)
1160 bool checked = *v;
1161 if (is_multi_select)
1162 MultiSelectItemHeader(id, p_selected: &checked, NULL);
1163
1164 bool hovered, held;
1165 bool pressed = ButtonBehavior(bb: total_bb, id, out_hovered: &hovered, out_held: &held);
1166
1167 // Range-Selection/Multi-selection support (footer)
1168 if (is_multi_select)
1169 MultiSelectItemFooter(id, p_selected: &checked, p_pressed: &pressed);
1170 else if (pressed)
1171 checked = !checked;
1172
1173 if (*v != checked)
1174 {
1175 *v = checked;
1176 pressed = true; // return value
1177 MarkItemEdited(id);
1178 }
1179
1180 const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1181 const bool mixed_value = (g.LastItemData.InFlags & ImGuiItemFlags_MixedValue) != 0;
1182 if (is_visible)
1183 {
1184 RenderNavHighlight(bb: total_bb, id);
1185 RenderFrame(p_min: check_bb.Min, p_max: check_bb.Max, fill_col: GetColorU32(idx: (held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), border: true, rounding: style.FrameRounding);
1186 ImU32 check_col = GetColorU32(idx: ImGuiCol_CheckMark);
1187 if (mixed_value)
1188 {
1189 // Undocumented tristate/mixed/indeterminate checkbox (#2644)
1190 // This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all widgets (not just checkbox)
1191 ImVec2 pad(ImMax(lhs: 1.0f, IM_TRUNC(square_sz / 3.6f)), ImMax(lhs: 1.0f, IM_TRUNC(square_sz / 3.6f)));
1192 window->DrawList->AddRectFilled(p_min: check_bb.Min + pad, p_max: check_bb.Max - pad, col: check_col, rounding: style.FrameRounding);
1193 }
1194 else if (*v)
1195 {
1196 const float pad = ImMax(lhs: 1.0f, IM_TRUNC(square_sz / 6.0f));
1197 RenderCheckMark(draw_list: window->DrawList, pos: check_bb.Min + ImVec2(pad, pad), col: check_col, sz: square_sz - pad * 2.0f);
1198 }
1199 }
1200 const ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1201 if (g.LogEnabled)
1202 LogRenderedText(ref_pos: &label_pos, text: mixed_value ? "[~]" : *v ? "[x]" : "[ ]");
1203 if (is_visible && label_size.x > 0.0f)
1204 RenderText(pos: label_pos, text: label);
1205
1206 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1207 return pressed;
1208}
1209
1210template<typename T>
1211bool ImGui::CheckboxFlagsT(const char* label, T* flags, T flags_value)
1212{
1213 bool all_on = (*flags & flags_value) == flags_value;
1214 bool any_on = (*flags & flags_value) != 0;
1215 bool pressed;
1216 if (!all_on && any_on)
1217 {
1218 ImGuiContext& g = *GImGui;
1219 g.NextItemData.ItemFlags |= ImGuiItemFlags_MixedValue;
1220 pressed = Checkbox(label, v: &all_on);
1221 }
1222 else
1223 {
1224 pressed = Checkbox(label, v: &all_on);
1225
1226 }
1227 if (pressed)
1228 {
1229 if (all_on)
1230 *flags |= flags_value;
1231 else
1232 *flags &= ~flags_value;
1233 }
1234 return pressed;
1235}
1236
1237bool ImGui::CheckboxFlags(const char* label, int* flags, int flags_value)
1238{
1239 return CheckboxFlagsT(label, flags, flags_value);
1240}
1241
1242bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value)
1243{
1244 return CheckboxFlagsT(label, flags, flags_value);
1245}
1246
1247bool ImGui::CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value)
1248{
1249 return CheckboxFlagsT(label, flags, flags_value);
1250}
1251
1252bool ImGui::CheckboxFlags(const char* label, ImU64* flags, ImU64 flags_value)
1253{
1254 return CheckboxFlagsT(label, flags, flags_value);
1255}
1256
1257bool ImGui::RadioButton(const char* label, bool active)
1258{
1259 ImGuiWindow* window = GetCurrentWindow();
1260 if (window->SkipItems)
1261 return false;
1262
1263 ImGuiContext& g = *GImGui;
1264 const ImGuiStyle& style = g.Style;
1265 const ImGuiID id = window->GetID(str: label);
1266 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
1267
1268 const float square_sz = GetFrameHeight();
1269 const ImVec2 pos = window->DC.CursorPos;
1270 const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1271 const ImRect total_bb(pos, pos + ImVec2(square_sz + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), label_size.y + style.FramePadding.y * 2.0f));
1272 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
1273 if (!ItemAdd(bb: total_bb, id))
1274 return false;
1275
1276 ImVec2 center = check_bb.GetCenter();
1277 center.x = IM_ROUND(center.x);
1278 center.y = IM_ROUND(center.y);
1279 const float radius = (square_sz - 1.0f) * 0.5f;
1280
1281 bool hovered, held;
1282 bool pressed = ButtonBehavior(bb: total_bb, id, out_hovered: &hovered, out_held: &held);
1283 if (pressed)
1284 MarkItemEdited(id);
1285
1286 RenderNavHighlight(bb: total_bb, id);
1287 const int num_segment = window->DrawList->_CalcCircleAutoSegmentCount(radius);
1288 window->DrawList->AddCircleFilled(center, radius, col: GetColorU32(idx: (held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), num_segments: num_segment);
1289 if (active)
1290 {
1291 const float pad = ImMax(lhs: 1.0f, IM_TRUNC(square_sz / 6.0f));
1292 window->DrawList->AddCircleFilled(center, radius: radius - pad, col: GetColorU32(idx: ImGuiCol_CheckMark));
1293 }
1294
1295 if (style.FrameBorderSize > 0.0f)
1296 {
1297 window->DrawList->AddCircle(center: center + ImVec2(1, 1), radius, col: GetColorU32(idx: ImGuiCol_BorderShadow), num_segments: num_segment, thickness: style.FrameBorderSize);
1298 window->DrawList->AddCircle(center, radius, col: GetColorU32(idx: ImGuiCol_Border), num_segments: num_segment, thickness: style.FrameBorderSize);
1299 }
1300
1301 ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1302 if (g.LogEnabled)
1303 LogRenderedText(ref_pos: &label_pos, text: active ? "(x)" : "( )");
1304 if (label_size.x > 0.0f)
1305 RenderText(pos: label_pos, text: label);
1306
1307 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
1308 return pressed;
1309}
1310
1311// FIXME: This would work nicely if it was a public template, e.g. 'template<T> RadioButton(const char* label, T* v, T v_button)', but I'm not sure how we would expose it..
1312bool ImGui::RadioButton(const char* label, int* v, int v_button)
1313{
1314 const bool pressed = RadioButton(label, active: *v == v_button);
1315 if (pressed)
1316 *v = v_button;
1317 return pressed;
1318}
1319
1320// size_arg (for each axis) < 0.0f: align to end, 0.0f: auto, > 0.0f: specified size
1321void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* overlay)
1322{
1323 ImGuiWindow* window = GetCurrentWindow();
1324 if (window->SkipItems)
1325 return;
1326
1327 ImGuiContext& g = *GImGui;
1328 const ImGuiStyle& style = g.Style;
1329
1330 ImVec2 pos = window->DC.CursorPos;
1331 ImVec2 size = CalcItemSize(size: size_arg, default_w: CalcItemWidth(), default_h: g.FontSize + style.FramePadding.y * 2.0f);
1332 ImRect bb(pos, pos + size);
1333 ItemSize(size, text_baseline_y: style.FramePadding.y);
1334 if (!ItemAdd(bb, id: 0))
1335 return;
1336
1337 // Fraction < 0.0f will display an indeterminate progress bar animation
1338 // The value must be animated along with time, so e.g. passing '-1.0f * ImGui::GetTime()' as fraction works.
1339 const bool is_indeterminate = (fraction < 0.0f);
1340 if (!is_indeterminate)
1341 fraction = ImSaturate(f: fraction);
1342
1343 // Out of courtesy we accept a NaN fraction without crashing
1344 float fill_n0 = 0.0f;
1345 float fill_n1 = (fraction == fraction) ? fraction : 0.0f;
1346
1347 if (is_indeterminate)
1348 {
1349 const float fill_width_n = 0.2f;
1350 fill_n0 = ImFmod(-fraction, 1.0f) * (1.0f + fill_width_n) - fill_width_n;
1351 fill_n1 = ImSaturate(f: fill_n0 + fill_width_n);
1352 fill_n0 = ImSaturate(f: fill_n0);
1353 }
1354
1355 // Render
1356 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: GetColorU32(idx: ImGuiCol_FrameBg), border: true, rounding: style.FrameRounding);
1357 bb.Expand(amount: ImVec2(-style.FrameBorderSize, -style.FrameBorderSize));
1358 RenderRectFilledRangeH(draw_list: window->DrawList, rect: bb, col: GetColorU32(idx: ImGuiCol_PlotHistogram), x_start_norm: fill_n0, x_end_norm: fill_n1, rounding: style.FrameRounding);
1359
1360 // Default displaying the fraction as percentage string, but user can override it
1361 // Don't display text for indeterminate bars by default
1362 char overlay_buf[32];
1363 if (!is_indeterminate || overlay != NULL)
1364 {
1365 if (!overlay)
1366 {
1367 ImFormatString(buf: overlay_buf, IM_ARRAYSIZE(overlay_buf), fmt: "%.0f%%", fraction * 100 + 0.01f);
1368 overlay = overlay_buf;
1369 }
1370
1371 ImVec2 overlay_size = CalcTextSize(text: overlay, NULL);
1372 if (overlay_size.x > 0.0f)
1373 {
1374 float text_x = is_indeterminate ? (bb.Min.x + bb.Max.x - overlay_size.x) * 0.5f : ImLerp(a: bb.Min.x, b: bb.Max.x, t: fill_n1) + style.ItemSpacing.x;
1375 RenderTextClipped(pos_min: ImVec2(ImClamp(v: text_x, mn: bb.Min.x, mx: bb.Max.x - overlay_size.x - style.ItemInnerSpacing.x), bb.Min.y), pos_max: bb.Max, text: overlay, NULL, text_size_if_known: &overlay_size, align: ImVec2(0.0f, 0.5f), clip_rect: &bb);
1376 }
1377 }
1378}
1379
1380void ImGui::Bullet()
1381{
1382 ImGuiWindow* window = GetCurrentWindow();
1383 if (window->SkipItems)
1384 return;
1385
1386 ImGuiContext& g = *GImGui;
1387 const ImGuiStyle& style = g.Style;
1388 const float line_height = ImMax(lhs: ImMin(lhs: window->DC.CurrLineSize.y, rhs: g.FontSize + style.FramePadding.y * 2), rhs: g.FontSize);
1389 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize, line_height));
1390 ItemSize(bb);
1391 if (!ItemAdd(bb, id: 0))
1392 {
1393 SameLine(offset_from_start_x: 0, spacing: style.FramePadding.x * 2);
1394 return;
1395 }
1396
1397 // Render and stay on same line
1398 ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
1399 RenderBullet(draw_list: window->DrawList, pos: bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, line_height * 0.5f), col: text_col);
1400 SameLine(offset_from_start_x: 0, spacing: style.FramePadding.x * 2.0f);
1401}
1402
1403// This is provided as a convenience for being an often requested feature.
1404// FIXME-STYLE: we delayed adding as there is a larger plan to revamp the styling system.
1405// Because of this we currently don't provide many styling options for this widget
1406// (e.g. hovered/active colors are automatically inferred from a single color).
1407bool ImGui::TextLink(const char* label)
1408{
1409 ImGuiWindow* window = GetCurrentWindow();
1410 if (window->SkipItems)
1411 return false;
1412
1413 ImGuiContext& g = *GImGui;
1414 const ImGuiID id = window->GetID(str: label);
1415 const char* label_end = FindRenderedTextEnd(text: label);
1416
1417 ImVec2 pos = window->DC.CursorPos;
1418 ImVec2 size = CalcTextSize(text: label, text_end: label_end, hide_text_after_double_hash: true);
1419 ImRect bb(pos, pos + size);
1420 ItemSize(size, text_baseline_y: 0.0f);
1421 if (!ItemAdd(bb, id))
1422 return false;
1423
1424 bool hovered, held;
1425 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held);
1426 RenderNavHighlight(bb, id, flags: ImGuiNavHighlightFlags_None);
1427
1428 ImVec4 text_colf = g.Style.Colors[ImGuiCol_TextLink];
1429 ImVec4 line_colf = text_colf;
1430 {
1431 // FIXME-STYLE: Read comments above. This widget is NOT written in the same style as some earlier widgets,
1432 // as we are currently experimenting/planning a different styling system.
1433 float h, s, v;
1434 ColorConvertRGBtoHSV(r: text_colf.x, g: text_colf.y, b: text_colf.z, out_h&: h, out_s&: s, out_v&: v);
1435 if (held || hovered)
1436 {
1437 v = ImSaturate(f: v + (held ? 0.4f : 0.3f));
1438 h = ImFmod(h + 0.02f, 1.0f);
1439 }
1440 ColorConvertHSVtoRGB(h, s, v, out_r&: text_colf.x, out_g&: text_colf.y, out_b&: text_colf.z);
1441 v = ImSaturate(f: v - 0.20f);
1442 ColorConvertHSVtoRGB(h, s, v, out_r&: line_colf.x, out_g&: line_colf.y, out_b&: line_colf.z);
1443 }
1444
1445 float line_y = bb.Max.y + ImFloor(f: g.Font->Descent * g.FontScale * 0.20f);
1446 window->DrawList->AddLine(p1: ImVec2(bb.Min.x, line_y), p2: ImVec2(bb.Max.x, line_y), col: GetColorU32(col: line_colf)); // FIXME-TEXT: Underline mode.
1447
1448 PushStyleColor(idx: ImGuiCol_Text, col: GetColorU32(col: text_colf));
1449 RenderText(pos: bb.Min, text: label, text_end: label_end);
1450 PopStyleColor();
1451
1452 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
1453 return pressed;
1454}
1455
1456void ImGui::TextLinkOpenURL(const char* label, const char* url)
1457{
1458 ImGuiContext& g = *GImGui;
1459 if (url == NULL)
1460 url = label;
1461 if (TextLink(label))
1462 if (g.IO.PlatformOpenInShellFn != NULL)
1463 g.IO.PlatformOpenInShellFn(&g, url);
1464 SetItemTooltip("%s", url); // It is more reassuring for user to _always_ display URL when we same as label
1465 if (BeginPopupContextItem())
1466 {
1467 if (MenuItem(label: LocalizeGetMsg(key: ImGuiLocKey_CopyLink)))
1468 SetClipboardText(url);
1469 EndPopup();
1470 }
1471}
1472
1473//-------------------------------------------------------------------------
1474// [SECTION] Widgets: Low-level Layout helpers
1475//-------------------------------------------------------------------------
1476// - Spacing()
1477// - Dummy()
1478// - NewLine()
1479// - AlignTextToFramePadding()
1480// - SeparatorEx() [Internal]
1481// - Separator()
1482// - SplitterBehavior() [Internal]
1483// - ShrinkWidths() [Internal]
1484//-------------------------------------------------------------------------
1485
1486void ImGui::Spacing()
1487{
1488 ImGuiWindow* window = GetCurrentWindow();
1489 if (window->SkipItems)
1490 return;
1491 ItemSize(size: ImVec2(0, 0));
1492}
1493
1494void ImGui::Dummy(const ImVec2& size)
1495{
1496 ImGuiWindow* window = GetCurrentWindow();
1497 if (window->SkipItems)
1498 return;
1499
1500 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
1501 ItemSize(size);
1502 ItemAdd(bb, id: 0);
1503}
1504
1505void ImGui::NewLine()
1506{
1507 ImGuiWindow* window = GetCurrentWindow();
1508 if (window->SkipItems)
1509 return;
1510
1511 ImGuiContext& g = *GImGui;
1512 const ImGuiLayoutType backup_layout_type = window->DC.LayoutType;
1513 window->DC.LayoutType = ImGuiLayoutType_Vertical;
1514 window->DC.IsSameLine = false;
1515 if (window->DC.CurrLineSize.y > 0.0f) // In the event that we are on a line with items that is smaller that FontSize high, we will preserve its height.
1516 ItemSize(size: ImVec2(0, 0));
1517 else
1518 ItemSize(size: ImVec2(0.0f, g.FontSize));
1519 window->DC.LayoutType = backup_layout_type;
1520}
1521
1522void ImGui::AlignTextToFramePadding()
1523{
1524 ImGuiWindow* window = GetCurrentWindow();
1525 if (window->SkipItems)
1526 return;
1527
1528 ImGuiContext& g = *GImGui;
1529 window->DC.CurrLineSize.y = ImMax(lhs: window->DC.CurrLineSize.y, rhs: g.FontSize + g.Style.FramePadding.y * 2);
1530 window->DC.CurrLineTextBaseOffset = ImMax(lhs: window->DC.CurrLineTextBaseOffset, rhs: g.Style.FramePadding.y);
1531}
1532
1533// Horizontal/vertical separating line
1534// FIXME: Surprisingly, this seemingly trivial widget is a victim of many different legacy/tricky layout issues.
1535// Note how thickness == 1.0f is handled specifically as not moving CursorPos by 'thickness', but other values are.
1536void ImGui::SeparatorEx(ImGuiSeparatorFlags flags, float thickness)
1537{
1538 ImGuiWindow* window = GetCurrentWindow();
1539 if (window->SkipItems)
1540 return;
1541
1542 ImGuiContext& g = *GImGui;
1543 IM_ASSERT(ImIsPowerOfTwo(flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical))); // Check that only 1 option is selected
1544 IM_ASSERT(thickness > 0.0f);
1545
1546 if (flags & ImGuiSeparatorFlags_Vertical)
1547 {
1548 // Vertical separator, for menu bars (use current line height).
1549 float y1 = window->DC.CursorPos.y;
1550 float y2 = window->DC.CursorPos.y + window->DC.CurrLineSize.y;
1551 const ImRect bb(ImVec2(window->DC.CursorPos.x, y1), ImVec2(window->DC.CursorPos.x + thickness, y2));
1552 ItemSize(size: ImVec2(thickness, 0.0f));
1553 if (!ItemAdd(bb, id: 0))
1554 return;
1555
1556 // Draw
1557 window->DrawList->AddRectFilled(p_min: bb.Min, p_max: bb.Max, col: GetColorU32(idx: ImGuiCol_Separator));
1558 if (g.LogEnabled)
1559 LogText(fmt: " |");
1560 }
1561 else if (flags & ImGuiSeparatorFlags_Horizontal)
1562 {
1563 // Horizontal Separator
1564 float x1 = window->DC.CursorPos.x;
1565 float x2 = window->WorkRect.Max.x;
1566
1567 // Preserve legacy behavior inside Columns()
1568 // Before Tables API happened, we relied on Separator() to span all columns of a Columns() set.
1569 // We currently don't need to provide the same feature for tables because tables naturally have border features.
1570 ImGuiOldColumns* columns = (flags & ImGuiSeparatorFlags_SpanAllColumns) ? window->DC.CurrentColumns : NULL;
1571 if (columns)
1572 {
1573 x1 = window->Pos.x + window->DC.Indent.x; // Used to be Pos.x before 2023/10/03
1574 x2 = window->Pos.x + window->Size.x;
1575 PushColumnsBackground();
1576 }
1577
1578 // We don't provide our width to the layout so that it doesn't get feed back into AutoFit
1579 // FIXME: This prevents ->CursorMaxPos based bounding box evaluation from working (e.g. TableEndCell)
1580 const float thickness_for_layout = (thickness == 1.0f) ? 0.0f : thickness; // FIXME: See 1.70/1.71 Separator() change: makes legacy 1-px separator not affect layout yet. Should change.
1581 const ImRect bb(ImVec2(x1, window->DC.CursorPos.y), ImVec2(x2, window->DC.CursorPos.y + thickness));
1582 ItemSize(size: ImVec2(0.0f, thickness_for_layout));
1583
1584 if (ItemAdd(bb, id: 0))
1585 {
1586 // Draw
1587 window->DrawList->AddRectFilled(p_min: bb.Min, p_max: bb.Max, col: GetColorU32(idx: ImGuiCol_Separator));
1588 if (g.LogEnabled)
1589 LogRenderedText(ref_pos: &bb.Min, text: "--------------------------------\n");
1590
1591 }
1592 if (columns)
1593 {
1594 PopColumnsBackground();
1595 columns->LineMinY = window->DC.CursorPos.y;
1596 }
1597 }
1598}
1599
1600void ImGui::Separator()
1601{
1602 ImGuiContext& g = *GImGui;
1603 ImGuiWindow* window = g.CurrentWindow;
1604 if (window->SkipItems)
1605 return;
1606
1607 // Those flags should eventually be configurable by the user
1608 // FIXME: We cannot g.Style.SeparatorTextBorderSize for thickness as it relates to SeparatorText() which is a decorated separator, not defaulting to 1.0f.
1609 ImGuiSeparatorFlags flags = (window->DC.LayoutType == ImGuiLayoutType_Horizontal) ? ImGuiSeparatorFlags_Vertical : ImGuiSeparatorFlags_Horizontal;
1610
1611 // Only applies to legacy Columns() api as they relied on Separator() a lot.
1612 if (window->DC.CurrentColumns)
1613 flags |= ImGuiSeparatorFlags_SpanAllColumns;
1614
1615 SeparatorEx(flags, thickness: 1.0f);
1616}
1617
1618void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end, float extra_w)
1619{
1620 ImGuiContext& g = *GImGui;
1621 ImGuiWindow* window = g.CurrentWindow;
1622 ImGuiStyle& style = g.Style;
1623
1624 const ImVec2 label_size = CalcTextSize(text: label, text_end: label_end, hide_text_after_double_hash: false);
1625 const ImVec2 pos = window->DC.CursorPos;
1626 const ImVec2 padding = style.SeparatorTextPadding;
1627
1628 const float separator_thickness = style.SeparatorTextBorderSize;
1629 const ImVec2 min_size(label_size.x + extra_w + padding.x * 2.0f, ImMax(lhs: label_size.y + padding.y * 2.0f, rhs: separator_thickness));
1630 const ImRect bb(pos, ImVec2(window->WorkRect.Max.x, pos.y + min_size.y));
1631 const float text_baseline_y = ImTrunc(f: (bb.GetHeight() - label_size.y) * style.SeparatorTextAlign.y + 0.99999f); //ImMax(padding.y, ImFloor((style.SeparatorTextSize - label_size.y) * 0.5f));
1632 ItemSize(size: min_size, text_baseline_y);
1633 if (!ItemAdd(bb, id))
1634 return;
1635
1636 const float sep1_x1 = pos.x;
1637 const float sep2_x2 = bb.Max.x;
1638 const float seps_y = ImTrunc(f: (bb.Min.y + bb.Max.y) * 0.5f + 0.99999f);
1639
1640 const float label_avail_w = ImMax(lhs: 0.0f, rhs: sep2_x2 - sep1_x1 - padding.x * 2.0f);
1641 const ImVec2 label_pos(pos.x + padding.x + ImMax(lhs: 0.0f, rhs: (label_avail_w - label_size.x - extra_w) * style.SeparatorTextAlign.x), pos.y + text_baseline_y); // FIXME-ALIGN
1642
1643 // This allows using SameLine() to position something in the 'extra_w'
1644 window->DC.CursorPosPrevLine.x = label_pos.x + label_size.x;
1645
1646 const ImU32 separator_col = GetColorU32(idx: ImGuiCol_Separator);
1647 if (label_size.x > 0.0f)
1648 {
1649 const float sep1_x2 = label_pos.x - style.ItemSpacing.x;
1650 const float sep2_x1 = label_pos.x + label_size.x + extra_w + style.ItemSpacing.x;
1651 if (sep1_x2 > sep1_x1 && separator_thickness > 0.0f)
1652 window->DrawList->AddLine(p1: ImVec2(sep1_x1, seps_y), p2: ImVec2(sep1_x2, seps_y), col: separator_col, thickness: separator_thickness);
1653 if (sep2_x2 > sep2_x1 && separator_thickness > 0.0f)
1654 window->DrawList->AddLine(p1: ImVec2(sep2_x1, seps_y), p2: ImVec2(sep2_x2, seps_y), col: separator_col, thickness: separator_thickness);
1655 if (g.LogEnabled)
1656 LogSetNextTextDecoration(prefix: "---", NULL);
1657 RenderTextEllipsis(draw_list: window->DrawList, pos_min: label_pos, pos_max: ImVec2(bb.Max.x, bb.Max.y + style.ItemSpacing.y), clip_max_x: bb.Max.x, ellipsis_max_x: bb.Max.x, text: label, text_end: label_end, text_size_if_known: &label_size);
1658 }
1659 else
1660 {
1661 if (g.LogEnabled)
1662 LogText(fmt: "---");
1663 if (separator_thickness > 0.0f)
1664 window->DrawList->AddLine(p1: ImVec2(sep1_x1, seps_y), p2: ImVec2(sep2_x2, seps_y), col: separator_col, thickness: separator_thickness);
1665 }
1666}
1667
1668void ImGui::SeparatorText(const char* label)
1669{
1670 ImGuiWindow* window = GetCurrentWindow();
1671 if (window->SkipItems)
1672 return;
1673
1674 // The SeparatorText() vs SeparatorTextEx() distinction is designed to be considerate that we may want:
1675 // - allow separator-text to be draggable items (would require a stable ID + a noticeable highlight)
1676 // - this high-level entry point to allow formatting? (which in turns may require ID separate from formatted string)
1677 // - because of this we probably can't turn 'const char* label' into 'const char* fmt, ...'
1678 // Otherwise, we can decide that users wanting to drag this would layout a dedicated drag-item,
1679 // and then we can turn this into a format function.
1680 SeparatorTextEx(id: 0, label, label_end: FindRenderedTextEnd(text: label), extra_w: 0.0f);
1681}
1682
1683// Using 'hover_visibility_delay' allows us to hide the highlight and mouse cursor for a short time, which can be convenient to reduce visual noise.
1684bool ImGui::SplitterBehavior(const ImRect& bb, ImGuiID id, ImGuiAxis axis, float* size1, float* size2, float min_size1, float min_size2, float hover_extend, float hover_visibility_delay, ImU32 bg_col)
1685{
1686 ImGuiContext& g = *GImGui;
1687 ImGuiWindow* window = g.CurrentWindow;
1688
1689 if (!ItemAdd(bb, id, NULL, extra_flags: ImGuiItemFlags_NoNav))
1690 return false;
1691
1692 // FIXME: AFAIK the only leftover reason for passing ImGuiButtonFlags_AllowOverlap here is
1693 // to allow caller of SplitterBehavior() to call SetItemAllowOverlap() after the item.
1694 // Nowadays we would instead want to use SetNextItemAllowOverlap() before the item.
1695 ImGuiButtonFlags button_flags = ImGuiButtonFlags_FlattenChildren;
1696#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1697 button_flags |= ImGuiButtonFlags_AllowOverlap;
1698#endif
1699
1700 bool hovered, held;
1701 ImRect bb_interact = bb;
1702 bb_interact.Expand(amount: axis == ImGuiAxis_Y ? ImVec2(0.0f, hover_extend) : ImVec2(hover_extend, 0.0f));
1703 ButtonBehavior(bb: bb_interact, id, out_hovered: &hovered, out_held: &held, flags: button_flags);
1704 if (hovered)
1705 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; // for IsItemHovered(), because bb_interact is larger than bb
1706
1707 if (held || (hovered && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= hover_visibility_delay))
1708 SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW);
1709
1710 ImRect bb_render = bb;
1711 if (held)
1712 {
1713 float mouse_delta = (g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min)[axis];
1714
1715 // Minimum pane size
1716 float size_1_maximum_delta = ImMax(lhs: 0.0f, rhs: *size1 - min_size1);
1717 float size_2_maximum_delta = ImMax(lhs: 0.0f, rhs: *size2 - min_size2);
1718 if (mouse_delta < -size_1_maximum_delta)
1719 mouse_delta = -size_1_maximum_delta;
1720 if (mouse_delta > size_2_maximum_delta)
1721 mouse_delta = size_2_maximum_delta;
1722
1723 // Apply resize
1724 if (mouse_delta != 0.0f)
1725 {
1726 *size1 = ImMax(lhs: *size1 + mouse_delta, rhs: min_size1);
1727 *size2 = ImMax(lhs: *size2 - mouse_delta, rhs: min_size2);
1728 bb_render.Translate(d: (axis == ImGuiAxis_X) ? ImVec2(mouse_delta, 0.0f) : ImVec2(0.0f, mouse_delta));
1729 MarkItemEdited(id);
1730 }
1731 }
1732
1733 // Render at new position
1734 if (bg_col & IM_COL32_A_MASK)
1735 window->DrawList->AddRectFilled(p_min: bb_render.Min, p_max: bb_render.Max, col: bg_col, rounding: 0.0f);
1736 const ImU32 col = GetColorU32(idx: held ? ImGuiCol_SeparatorActive : (hovered && g.HoveredIdTimer >= hover_visibility_delay) ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator);
1737 window->DrawList->AddRectFilled(p_min: bb_render.Min, p_max: bb_render.Max, col, rounding: 0.0f);
1738
1739 return held;
1740}
1741
1742static int IMGUI_CDECL ShrinkWidthItemComparer(const void* lhs, const void* rhs)
1743{
1744 const ImGuiShrinkWidthItem* a = (const ImGuiShrinkWidthItem*)lhs;
1745 const ImGuiShrinkWidthItem* b = (const ImGuiShrinkWidthItem*)rhs;
1746 if (int d = (int)(b->Width - a->Width))
1747 return d;
1748 return (b->Index - a->Index);
1749}
1750
1751// Shrink excess width from a set of item, by removing width from the larger items first.
1752// Set items Width to -1.0f to disable shrinking this item.
1753void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess)
1754{
1755 if (count == 1)
1756 {
1757 if (items[0].Width >= 0.0f)
1758 items[0].Width = ImMax(lhs: items[0].Width - width_excess, rhs: 1.0f);
1759 return;
1760 }
1761 ImQsort(base: items, count: (size_t)count, size_of_element: sizeof(ImGuiShrinkWidthItem), compare_func: ShrinkWidthItemComparer);
1762 int count_same_width = 1;
1763 while (width_excess > 0.0f && count_same_width < count)
1764 {
1765 while (count_same_width < count && items[0].Width <= items[count_same_width].Width)
1766 count_same_width++;
1767 float max_width_to_remove_per_item = (count_same_width < count && items[count_same_width].Width >= 0.0f) ? (items[0].Width - items[count_same_width].Width) : (items[0].Width - 1.0f);
1768 if (max_width_to_remove_per_item <= 0.0f)
1769 break;
1770 float width_to_remove_per_item = ImMin(lhs: width_excess / count_same_width, rhs: max_width_to_remove_per_item);
1771 for (int item_n = 0; item_n < count_same_width; item_n++)
1772 items[item_n].Width -= width_to_remove_per_item;
1773 width_excess -= width_to_remove_per_item * count_same_width;
1774 }
1775
1776 // Round width and redistribute remainder
1777 // Ensure that e.g. the right-most tab of a shrunk tab-bar always reaches exactly at the same distance from the right-most edge of the tab bar separator.
1778 width_excess = 0.0f;
1779 for (int n = 0; n < count; n++)
1780 {
1781 float width_rounded = ImTrunc(f: items[n].Width);
1782 width_excess += items[n].Width - width_rounded;
1783 items[n].Width = width_rounded;
1784 }
1785 while (width_excess > 0.0f)
1786 for (int n = 0; n < count && width_excess > 0.0f; n++)
1787 {
1788 float width_to_add = ImMin(lhs: items[n].InitialWidth - items[n].Width, rhs: 1.0f);
1789 items[n].Width += width_to_add;
1790 width_excess -= width_to_add;
1791 }
1792}
1793
1794//-------------------------------------------------------------------------
1795// [SECTION] Widgets: ComboBox
1796//-------------------------------------------------------------------------
1797// - CalcMaxPopupHeightFromItemCount() [Internal]
1798// - BeginCombo()
1799// - BeginComboPopup() [Internal]
1800// - EndCombo()
1801// - BeginComboPreview() [Internal]
1802// - EndComboPreview() [Internal]
1803// - Combo()
1804//-------------------------------------------------------------------------
1805
1806static float CalcMaxPopupHeightFromItemCount(int items_count)
1807{
1808 ImGuiContext& g = *GImGui;
1809 if (items_count <= 0)
1810 return FLT_MAX;
1811 return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2);
1812}
1813
1814bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags)
1815{
1816 ImGuiContext& g = *GImGui;
1817 ImGuiWindow* window = GetCurrentWindow();
1818
1819 ImGuiNextWindowDataFlags backup_next_window_data_flags = g.NextWindowData.Flags;
1820 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
1821 if (window->SkipItems)
1822 return false;
1823
1824 const ImGuiStyle& style = g.Style;
1825 const ImGuiID id = window->GetID(str: label);
1826 IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together
1827 if (flags & ImGuiComboFlags_WidthFitPreview)
1828 IM_ASSERT((flags & (ImGuiComboFlags_NoPreview | (ImGuiComboFlags)ImGuiComboFlags_CustomPreview)) == 0);
1829
1830 const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight();
1831 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
1832 const float preview_width = ((flags & ImGuiComboFlags_WidthFitPreview) && (preview_value != NULL)) ? CalcTextSize(text: preview_value, NULL, hide_text_after_double_hash: true).x : 0.0f;
1833 const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : ((flags & ImGuiComboFlags_WidthFitPreview) ? (arrow_size + preview_width + style.FramePadding.x * 2.0f) : CalcItemWidth());
1834 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
1835 const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
1836 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
1837 if (!ItemAdd(bb: total_bb, id, nav_bb: &bb))
1838 return false;
1839
1840 // Open on click
1841 bool hovered, held;
1842 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held);
1843 const ImGuiID popup_id = ImHashStr(data: "##ComboPopup", data_size: 0, seed: id);
1844 bool popup_open = IsPopupOpen(id: popup_id, popup_flags: ImGuiPopupFlags_None);
1845 if (pressed && !popup_open)
1846 {
1847 OpenPopupEx(id: popup_id, popup_flags: ImGuiPopupFlags_None);
1848 popup_open = true;
1849 }
1850
1851 // Render shape
1852 const ImU32 frame_col = GetColorU32(idx: hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
1853 const float value_x2 = ImMax(lhs: bb.Min.x, rhs: bb.Max.x - arrow_size);
1854 RenderNavHighlight(bb, id);
1855 if (!(flags & ImGuiComboFlags_NoPreview))
1856 window->DrawList->AddRectFilled(p_min: bb.Min, p_max: ImVec2(value_x2, bb.Max.y), col: frame_col, rounding: style.FrameRounding, flags: (flags & ImGuiComboFlags_NoArrowButton) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersLeft);
1857 if (!(flags & ImGuiComboFlags_NoArrowButton))
1858 {
1859 ImU32 bg_col = GetColorU32(idx: (popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1860 ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
1861 window->DrawList->AddRectFilled(p_min: ImVec2(value_x2, bb.Min.y), p_max: bb.Max, col: bg_col, rounding: style.FrameRounding, flags: (w <= arrow_size) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersRight);
1862 if (value_x2 + arrow_size - style.FramePadding.x <= bb.Max.x)
1863 RenderArrow(draw_list: window->DrawList, pos: ImVec2(value_x2 + style.FramePadding.y, bb.Min.y + style.FramePadding.y), col: text_col, dir: ImGuiDir_Down, scale: 1.0f);
1864 }
1865 RenderFrameBorder(p_min: bb.Min, p_max: bb.Max, rounding: style.FrameRounding);
1866
1867 // Custom preview
1868 if (flags & ImGuiComboFlags_CustomPreview)
1869 {
1870 g.ComboPreviewData.PreviewRect = ImRect(bb.Min.x, bb.Min.y, value_x2, bb.Max.y);
1871 IM_ASSERT(preview_value == NULL || preview_value[0] == 0);
1872 preview_value = NULL;
1873 }
1874
1875 // Render preview and label
1876 if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview))
1877 {
1878 if (g.LogEnabled)
1879 LogSetNextTextDecoration(prefix: "{", suffix: "}");
1880 RenderTextClipped(pos_min: bb.Min + style.FramePadding, pos_max: ImVec2(value_x2, bb.Max.y), text: preview_value, NULL, NULL);
1881 }
1882 if (label_size.x > 0)
1883 RenderText(pos: ImVec2(bb.Max.x + style.ItemInnerSpacing.x, bb.Min.y + style.FramePadding.y), text: label);
1884
1885 if (!popup_open)
1886 return false;
1887
1888 g.NextWindowData.Flags = backup_next_window_data_flags;
1889 return BeginComboPopup(popup_id, bb, flags);
1890}
1891
1892bool ImGui::BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags flags)
1893{
1894 ImGuiContext& g = *GImGui;
1895 if (!IsPopupOpen(id: popup_id, popup_flags: ImGuiPopupFlags_None))
1896 {
1897 g.NextWindowData.ClearFlags();
1898 return false;
1899 }
1900
1901 // Set popup size
1902 float w = bb.GetWidth();
1903 if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint)
1904 {
1905 g.NextWindowData.SizeConstraintRect.Min.x = ImMax(lhs: g.NextWindowData.SizeConstraintRect.Min.x, rhs: w);
1906 }
1907 else
1908 {
1909 if ((flags & ImGuiComboFlags_HeightMask_) == 0)
1910 flags |= ImGuiComboFlags_HeightRegular;
1911 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one
1912 int popup_max_height_in_items = -1;
1913 if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8;
1914 else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4;
1915 else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20;
1916 ImVec2 constraint_min(0.0f, 0.0f), constraint_max(FLT_MAX, FLT_MAX);
1917 if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.x <= 0.0f) // Don't apply constraints if user specified a size
1918 constraint_min.x = w;
1919 if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.y <= 0.0f)
1920 constraint_max.y = CalcMaxPopupHeightFromItemCount(items_count: popup_max_height_in_items);
1921 SetNextWindowSizeConstraints(size_min: constraint_min, size_max: constraint_max);
1922 }
1923
1924 // This is essentially a specialized version of BeginPopupEx()
1925 char name[16];
1926 ImFormatString(buf: name, IM_ARRAYSIZE(name), fmt: "##Combo_%02d", g.BeginComboDepth); // Recycle windows based on depth
1927
1928 // Set position given a custom constraint (peak into expected window size so we can position it)
1929 // FIXME: This might be easier to express with an hypothetical SetNextWindowPosConstraints() function?
1930 // FIXME: This might be moved to Begin() or at least around the same spot where Tooltips and other Popups are calling FindBestWindowPosForPopupEx()?
1931 if (ImGuiWindow* popup_window = FindWindowByName(name))
1932 if (popup_window->WasActive)
1933 {
1934 // Always override 'AutoPosLastDirection' to not leave a chance for a past value to affect us.
1935 ImVec2 size_expected = CalcWindowNextAutoFitSize(window: popup_window);
1936 popup_window->AutoPosLastDirection = (flags & ImGuiComboFlags_PopupAlignLeft) ? ImGuiDir_Left : ImGuiDir_Down; // Left = "Below, Toward Left", Down = "Below, Toward Right (default)"
1937 ImRect r_outer = GetPopupAllowedExtentRect(window: popup_window);
1938 ImVec2 pos = FindBestWindowPosForPopupEx(ref_pos: bb.GetBL(), size: size_expected, last_dir: &popup_window->AutoPosLastDirection, r_outer, r_avoid: bb, policy: ImGuiPopupPositionPolicy_ComboBox);
1939 SetNextWindowPos(pos);
1940 }
1941
1942 // We don't use BeginPopupEx() solely because we have a custom name string, which we could make an argument to BeginPopupEx()
1943 ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove;
1944 PushStyleVar(idx: ImGuiStyleVar_WindowPadding, val: ImVec2(g.Style.FramePadding.x, g.Style.WindowPadding.y)); // Horizontally align ourselves with the framed text
1945 bool ret = Begin(name, NULL, flags: window_flags);
1946 PopStyleVar();
1947 if (!ret)
1948 {
1949 EndPopup();
1950 IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above
1951 return false;
1952 }
1953 g.BeginComboDepth++;
1954 return true;
1955}
1956
1957void ImGui::EndCombo()
1958{
1959 ImGuiContext& g = *GImGui;
1960 EndPopup();
1961 g.BeginComboDepth--;
1962}
1963
1964// Call directly after the BeginCombo/EndCombo block. The preview is designed to only host non-interactive elements
1965// (Experimental, see GitHub issues: #1658, #4168)
1966bool ImGui::BeginComboPreview()
1967{
1968 ImGuiContext& g = *GImGui;
1969 ImGuiWindow* window = g.CurrentWindow;
1970 ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
1971
1972 if (window->SkipItems || !(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible))
1973 return false;
1974 IM_ASSERT(g.LastItemData.Rect.Min.x == preview_data->PreviewRect.Min.x && g.LastItemData.Rect.Min.y == preview_data->PreviewRect.Min.y); // Didn't call after BeginCombo/EndCombo block or forgot to pass ImGuiComboFlags_CustomPreview flag?
1975 if (!window->ClipRect.Overlaps(r: preview_data->PreviewRect)) // Narrower test (optional)
1976 return false;
1977
1978 // FIXME: This could be contained in a PushWorkRect() api
1979 preview_data->BackupCursorPos = window->DC.CursorPos;
1980 preview_data->BackupCursorMaxPos = window->DC.CursorMaxPos;
1981 preview_data->BackupCursorPosPrevLine = window->DC.CursorPosPrevLine;
1982 preview_data->BackupPrevLineTextBaseOffset = window->DC.PrevLineTextBaseOffset;
1983 preview_data->BackupLayout = window->DC.LayoutType;
1984 window->DC.CursorPos = preview_data->PreviewRect.Min + g.Style.FramePadding;
1985 window->DC.CursorMaxPos = window->DC.CursorPos;
1986 window->DC.LayoutType = ImGuiLayoutType_Horizontal;
1987 window->DC.IsSameLine = false;
1988 PushClipRect(clip_rect_min: preview_data->PreviewRect.Min, clip_rect_max: preview_data->PreviewRect.Max, intersect_with_current_clip_rect: true);
1989
1990 return true;
1991}
1992
1993void ImGui::EndComboPreview()
1994{
1995 ImGuiContext& g = *GImGui;
1996 ImGuiWindow* window = g.CurrentWindow;
1997 ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
1998
1999 // FIXME: Using CursorMaxPos approximation instead of correct AABB which we will store in ImDrawCmd in the future
2000 ImDrawList* draw_list = window->DrawList;
2001 if (window->DC.CursorMaxPos.x < preview_data->PreviewRect.Max.x && window->DC.CursorMaxPos.y < preview_data->PreviewRect.Max.y)
2002 if (draw_list->CmdBuffer.Size > 1) // Unlikely case that the PushClipRect() didn't create a command
2003 {
2004 draw_list->_CmdHeader.ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 2].ClipRect;
2005 draw_list->_TryMergeDrawCmds();
2006 }
2007 PopClipRect();
2008 window->DC.CursorPos = preview_data->BackupCursorPos;
2009 window->DC.CursorMaxPos = ImMax(lhs: window->DC.CursorMaxPos, rhs: preview_data->BackupCursorMaxPos);
2010 window->DC.CursorPosPrevLine = preview_data->BackupCursorPosPrevLine;
2011 window->DC.PrevLineTextBaseOffset = preview_data->BackupPrevLineTextBaseOffset;
2012 window->DC.LayoutType = preview_data->BackupLayout;
2013 window->DC.IsSameLine = false;
2014 preview_data->PreviewRect = ImRect();
2015}
2016
2017// Getter for the old Combo() API: const char*[]
2018static const char* Items_ArrayGetter(void* data, int idx)
2019{
2020 const char* const* items = (const char* const*)data;
2021 return items[idx];
2022}
2023
2024// Getter for the old Combo() API: "item1\0item2\0item3\0"
2025static const char* Items_SingleStringGetter(void* data, int idx)
2026{
2027 const char* items_separated_by_zeros = (const char*)data;
2028 int items_count = 0;
2029 const char* p = items_separated_by_zeros;
2030 while (*p)
2031 {
2032 if (idx == items_count)
2033 break;
2034 p += strlen(s: p) + 1;
2035 items_count++;
2036 }
2037 return *p ? p : NULL;
2038}
2039
2040// Old API, prefer using BeginCombo() nowadays if you can.
2041bool ImGui::Combo(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), void* user_data, int items_count, int popup_max_height_in_items)
2042{
2043 ImGuiContext& g = *GImGui;
2044
2045 // Call the getter to obtain the preview string which is a parameter to BeginCombo()
2046 const char* preview_value = NULL;
2047 if (*current_item >= 0 && *current_item < items_count)
2048 preview_value = getter(user_data, *current_item);
2049
2050 // The old Combo() API exposed "popup_max_height_in_items". The new more general BeginCombo() API doesn't have/need it, but we emulate it here.
2051 if (popup_max_height_in_items != -1 && !(g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint))
2052 SetNextWindowSizeConstraints(size_min: ImVec2(0, 0), size_max: ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(items_count: popup_max_height_in_items)));
2053
2054 if (!BeginCombo(label, preview_value, flags: ImGuiComboFlags_None))
2055 return false;
2056
2057 // Display items
2058 bool value_changed = false;
2059 ImGuiListClipper clipper;
2060 clipper.Begin(items_count);
2061 clipper.IncludeItemByIndex(item_index: *current_item);
2062 while (clipper.Step())
2063 for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
2064 {
2065 const char* item_text = getter(user_data, i);
2066 if (item_text == NULL)
2067 item_text = "*Unknown item*";
2068
2069 PushID(int_id: i);
2070 const bool item_selected = (i == *current_item);
2071 if (Selectable(label: item_text, selected: item_selected) && *current_item != i)
2072 {
2073 value_changed = true;
2074 *current_item = i;
2075 }
2076 if (item_selected)
2077 SetItemDefaultFocus();
2078 PopID();
2079 }
2080
2081 EndCombo();
2082 if (value_changed)
2083 MarkItemEdited(id: g.LastItemData.ID);
2084
2085 return value_changed;
2086}
2087
2088// Combo box helper allowing to pass an array of strings.
2089bool ImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items)
2090{
2091 const bool value_changed = Combo(label, current_item, getter: Items_ArrayGetter, user_data: (void*)items, items_count, popup_max_height_in_items: height_in_items);
2092 return value_changed;
2093}
2094
2095// Combo box helper allowing to pass all items in a single string literal holding multiple zero-terminated items "item1\0item2\0"
2096bool ImGui::Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int height_in_items)
2097{
2098 int items_count = 0;
2099 const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open
2100 while (*p)
2101 {
2102 p += strlen(s: p) + 1;
2103 items_count++;
2104 }
2105 bool value_changed = Combo(label, current_item, getter: Items_SingleStringGetter, user_data: (void*)items_separated_by_zeros, items_count, popup_max_height_in_items: height_in_items);
2106 return value_changed;
2107}
2108
2109#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
2110
2111struct ImGuiGetNameFromIndexOldToNewCallbackData { void* UserData; bool (*OldCallback)(void*, int, const char**); };
2112static const char* ImGuiGetNameFromIndexOldToNewCallback(void* user_data, int idx)
2113{
2114 ImGuiGetNameFromIndexOldToNewCallbackData* data = (ImGuiGetNameFromIndexOldToNewCallbackData*)user_data;
2115 const char* s = NULL;
2116 data->OldCallback(data->UserData, idx, &s);
2117 return s;
2118}
2119
2120bool ImGui::ListBox(const char* label, int* current_item, bool (*old_getter)(void*, int, const char**), void* user_data, int items_count, int height_in_items)
2121{
2122 ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { .UserData: user_data, .OldCallback: old_getter };
2123 return ListBox(label, current_item, getter: ImGuiGetNameFromIndexOldToNewCallback, user_data: &old_to_new_data, items_count, height_in_items);
2124}
2125bool ImGui::Combo(const char* label, int* current_item, bool (*old_getter)(void*, int, const char**), void* user_data, int items_count, int popup_max_height_in_items)
2126{
2127 ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { .UserData: user_data, .OldCallback: old_getter };
2128 return Combo(label, current_item, getter: ImGuiGetNameFromIndexOldToNewCallback, user_data: &old_to_new_data, items_count, popup_max_height_in_items);
2129}
2130
2131#endif
2132
2133//-------------------------------------------------------------------------
2134// [SECTION] Data Type and Data Formatting Helpers [Internal]
2135//-------------------------------------------------------------------------
2136// - DataTypeGetInfo()
2137// - DataTypeFormatString()
2138// - DataTypeApplyOp()
2139// - DataTypeApplyFromText()
2140// - DataTypeCompare()
2141// - DataTypeClamp()
2142// - GetMinimumStepAtDecimalPrecision
2143// - RoundScalarWithFormat<>()
2144//-------------------------------------------------------------------------
2145
2146static const ImGuiDataTypeInfo GDataTypeInfo[] =
2147{
2148 { .Size: sizeof(char), .Name: "S8", .PrintFmt: "%d", .ScanFmt: "%d" }, // ImGuiDataType_S8
2149 { .Size: sizeof(unsigned char), .Name: "U8", .PrintFmt: "%u", .ScanFmt: "%u" },
2150 { .Size: sizeof(short), .Name: "S16", .PrintFmt: "%d", .ScanFmt: "%d" }, // ImGuiDataType_S16
2151 { .Size: sizeof(unsigned short), .Name: "U16", .PrintFmt: "%u", .ScanFmt: "%u" },
2152 { .Size: sizeof(int), .Name: "S32", .PrintFmt: "%d", .ScanFmt: "%d" }, // ImGuiDataType_S32
2153 { .Size: sizeof(unsigned int), .Name: "U32", .PrintFmt: "%u", .ScanFmt: "%u" },
2154#ifdef _MSC_VER
2155 { sizeof(ImS64), "S64", "%I64d","%I64d" }, // ImGuiDataType_S64
2156 { sizeof(ImU64), "U64", "%I64u","%I64u" },
2157#else
2158 { .Size: sizeof(ImS64), .Name: "S64", .PrintFmt: "%lld", .ScanFmt: "%lld" }, // ImGuiDataType_S64
2159 { .Size: sizeof(ImU64), .Name: "U64", .PrintFmt: "%llu", .ScanFmt: "%llu" },
2160#endif
2161 { .Size: sizeof(float), .Name: "float", .PrintFmt: "%.3f",.ScanFmt: "%f" }, // ImGuiDataType_Float (float are promoted to double in va_arg)
2162 { .Size: sizeof(double), .Name: "double",.PrintFmt: "%f", .ScanFmt: "%lf" }, // ImGuiDataType_Double
2163 { .Size: sizeof(bool), .Name: "bool", .PrintFmt: "%d", .ScanFmt: "%d" }, // ImGuiDataType_Bool
2164};
2165IM_STATIC_ASSERT(IM_ARRAYSIZE(GDataTypeInfo) == ImGuiDataType_COUNT);
2166
2167const ImGuiDataTypeInfo* ImGui::DataTypeGetInfo(ImGuiDataType data_type)
2168{
2169 IM_ASSERT(data_type >= 0 && data_type < ImGuiDataType_COUNT);
2170 return &GDataTypeInfo[data_type];
2171}
2172
2173int ImGui::DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* p_data, const char* format)
2174{
2175 // Signedness doesn't matter when pushing integer arguments
2176 if (data_type == ImGuiDataType_S32 || data_type == ImGuiDataType_U32)
2177 return ImFormatString(buf, buf_size, fmt: format, *(const ImU32*)p_data);
2178 if (data_type == ImGuiDataType_S64 || data_type == ImGuiDataType_U64)
2179 return ImFormatString(buf, buf_size, fmt: format, *(const ImU64*)p_data);
2180 if (data_type == ImGuiDataType_Float)
2181 return ImFormatString(buf, buf_size, fmt: format, *(const float*)p_data);
2182 if (data_type == ImGuiDataType_Double)
2183 return ImFormatString(buf, buf_size, fmt: format, *(const double*)p_data);
2184 if (data_type == ImGuiDataType_S8)
2185 return ImFormatString(buf, buf_size, fmt: format, *(const ImS8*)p_data);
2186 if (data_type == ImGuiDataType_U8)
2187 return ImFormatString(buf, buf_size, fmt: format, *(const ImU8*)p_data);
2188 if (data_type == ImGuiDataType_S16)
2189 return ImFormatString(buf, buf_size, fmt: format, *(const ImS16*)p_data);
2190 if (data_type == ImGuiDataType_U16)
2191 return ImFormatString(buf, buf_size, fmt: format, *(const ImU16*)p_data);
2192 IM_ASSERT(0);
2193 return 0;
2194}
2195
2196void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg1, const void* arg2)
2197{
2198 IM_ASSERT(op == '+' || op == '-');
2199 switch (data_type)
2200 {
2201 case ImGuiDataType_S8:
2202 if (op == '+') { *(ImS8*)output = ImAddClampOverflow(a: *(const ImS8*)arg1, b: *(const ImS8*)arg2, mn: IM_S8_MIN, mx: IM_S8_MAX); }
2203 if (op == '-') { *(ImS8*)output = ImSubClampOverflow(a: *(const ImS8*)arg1, b: *(const ImS8*)arg2, mn: IM_S8_MIN, mx: IM_S8_MAX); }
2204 return;
2205 case ImGuiDataType_U8:
2206 if (op == '+') { *(ImU8*)output = ImAddClampOverflow(a: *(const ImU8*)arg1, b: *(const ImU8*)arg2, mn: IM_U8_MIN, mx: IM_U8_MAX); }
2207 if (op == '-') { *(ImU8*)output = ImSubClampOverflow(a: *(const ImU8*)arg1, b: *(const ImU8*)arg2, mn: IM_U8_MIN, mx: IM_U8_MAX); }
2208 return;
2209 case ImGuiDataType_S16:
2210 if (op == '+') { *(ImS16*)output = ImAddClampOverflow(a: *(const ImS16*)arg1, b: *(const ImS16*)arg2, mn: IM_S16_MIN, mx: IM_S16_MAX); }
2211 if (op == '-') { *(ImS16*)output = ImSubClampOverflow(a: *(const ImS16*)arg1, b: *(const ImS16*)arg2, mn: IM_S16_MIN, mx: IM_S16_MAX); }
2212 return;
2213 case ImGuiDataType_U16:
2214 if (op == '+') { *(ImU16*)output = ImAddClampOverflow(a: *(const ImU16*)arg1, b: *(const ImU16*)arg2, mn: IM_U16_MIN, mx: IM_U16_MAX); }
2215 if (op == '-') { *(ImU16*)output = ImSubClampOverflow(a: *(const ImU16*)arg1, b: *(const ImU16*)arg2, mn: IM_U16_MIN, mx: IM_U16_MAX); }
2216 return;
2217 case ImGuiDataType_S32:
2218 if (op == '+') { *(ImS32*)output = ImAddClampOverflow(a: *(const ImS32*)arg1, b: *(const ImS32*)arg2, mn: IM_S32_MIN, mx: IM_S32_MAX); }
2219 if (op == '-') { *(ImS32*)output = ImSubClampOverflow(a: *(const ImS32*)arg1, b: *(const ImS32*)arg2, mn: IM_S32_MIN, mx: IM_S32_MAX); }
2220 return;
2221 case ImGuiDataType_U32:
2222 if (op == '+') { *(ImU32*)output = ImAddClampOverflow(a: *(const ImU32*)arg1, b: *(const ImU32*)arg2, mn: IM_U32_MIN, mx: IM_U32_MAX); }
2223 if (op == '-') { *(ImU32*)output = ImSubClampOverflow(a: *(const ImU32*)arg1, b: *(const ImU32*)arg2, mn: IM_U32_MIN, mx: IM_U32_MAX); }
2224 return;
2225 case ImGuiDataType_S64:
2226 if (op == '+') { *(ImS64*)output = ImAddClampOverflow(a: *(const ImS64*)arg1, b: *(const ImS64*)arg2, mn: IM_S64_MIN, mx: IM_S64_MAX); }
2227 if (op == '-') { *(ImS64*)output = ImSubClampOverflow(a: *(const ImS64*)arg1, b: *(const ImS64*)arg2, mn: IM_S64_MIN, mx: IM_S64_MAX); }
2228 return;
2229 case ImGuiDataType_U64:
2230 if (op == '+') { *(ImU64*)output = ImAddClampOverflow(a: *(const ImU64*)arg1, b: *(const ImU64*)arg2, mn: IM_U64_MIN, mx: IM_U64_MAX); }
2231 if (op == '-') { *(ImU64*)output = ImSubClampOverflow(a: *(const ImU64*)arg1, b: *(const ImU64*)arg2, mn: IM_U64_MIN, mx: IM_U64_MAX); }
2232 return;
2233 case ImGuiDataType_Float:
2234 if (op == '+') { *(float*)output = *(const float*)arg1 + *(const float*)arg2; }
2235 if (op == '-') { *(float*)output = *(const float*)arg1 - *(const float*)arg2; }
2236 return;
2237 case ImGuiDataType_Double:
2238 if (op == '+') { *(double*)output = *(const double*)arg1 + *(const double*)arg2; }
2239 if (op == '-') { *(double*)output = *(const double*)arg1 - *(const double*)arg2; }
2240 return;
2241 case ImGuiDataType_COUNT: break;
2242 }
2243 IM_ASSERT(0);
2244}
2245
2246// User can input math operators (e.g. +100) to edit a numerical values.
2247// NB: This is _not_ a full expression evaluator. We should probably add one and replace this dumb mess..
2248bool ImGui::DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format, void* p_data_when_empty)
2249{
2250 // Copy the value in an opaque buffer so we can compare at the end of the function if it changed at all.
2251 const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
2252 ImGuiDataTypeStorage data_backup;
2253 memcpy(dest: &data_backup, src: p_data, n: type_info->Size);
2254
2255 while (ImCharIsBlankA(c: *buf))
2256 buf++;
2257 if (!buf[0])
2258 {
2259 if (p_data_when_empty != NULL)
2260 {
2261 memcpy(dest: p_data, src: p_data_when_empty, n: type_info->Size);
2262 return memcmp(s1: &data_backup, s2: p_data, n: type_info->Size) != 0;
2263 }
2264 return false;
2265 }
2266
2267 // Sanitize format
2268 // - For float/double we have to ignore format with precision (e.g. "%.2f") because sscanf doesn't take them in, so force them into %f and %lf
2269 // - In theory could treat empty format as using default, but this would only cover rare/bizarre case of using InputScalar() + integer + format string without %.
2270 char format_sanitized[32];
2271 if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double)
2272 format = type_info->ScanFmt;
2273 else
2274 format = ImParseFormatSanitizeForScanning(fmt_in: format, fmt_out: format_sanitized, IM_ARRAYSIZE(format_sanitized));
2275
2276 // Small types need a 32-bit buffer to receive the result from scanf()
2277 int v32 = 0;
2278 if (sscanf(s: buf, format: format, type_info->Size >= 4 ? p_data : &v32) < 1)
2279 return false;
2280 if (type_info->Size < 4)
2281 {
2282 if (data_type == ImGuiDataType_S8)
2283 *(ImS8*)p_data = (ImS8)ImClamp(v: v32, mn: (int)IM_S8_MIN, mx: (int)IM_S8_MAX);
2284 else if (data_type == ImGuiDataType_U8)
2285 *(ImU8*)p_data = (ImU8)ImClamp(v: v32, mn: (int)IM_U8_MIN, mx: (int)IM_U8_MAX);
2286 else if (data_type == ImGuiDataType_S16)
2287 *(ImS16*)p_data = (ImS16)ImClamp(v: v32, mn: (int)IM_S16_MIN, mx: (int)IM_S16_MAX);
2288 else if (data_type == ImGuiDataType_U16)
2289 *(ImU16*)p_data = (ImU16)ImClamp(v: v32, mn: (int)IM_U16_MIN, mx: (int)IM_U16_MAX);
2290 else
2291 IM_ASSERT(0);
2292 }
2293
2294 return memcmp(s1: &data_backup, s2: p_data, n: type_info->Size) != 0;
2295}
2296
2297template<typename T>
2298static int DataTypeCompareT(const T* lhs, const T* rhs)
2299{
2300 if (*lhs < *rhs) return -1;
2301 if (*lhs > *rhs) return +1;
2302 return 0;
2303}
2304
2305int ImGui::DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2)
2306{
2307 switch (data_type)
2308 {
2309 case ImGuiDataType_S8: return DataTypeCompareT<ImS8 >(lhs: (const ImS8* )arg_1, rhs: (const ImS8* )arg_2);
2310 case ImGuiDataType_U8: return DataTypeCompareT<ImU8 >(lhs: (const ImU8* )arg_1, rhs: (const ImU8* )arg_2);
2311 case ImGuiDataType_S16: return DataTypeCompareT<ImS16 >(lhs: (const ImS16* )arg_1, rhs: (const ImS16* )arg_2);
2312 case ImGuiDataType_U16: return DataTypeCompareT<ImU16 >(lhs: (const ImU16* )arg_1, rhs: (const ImU16* )arg_2);
2313 case ImGuiDataType_S32: return DataTypeCompareT<ImS32 >(lhs: (const ImS32* )arg_1, rhs: (const ImS32* )arg_2);
2314 case ImGuiDataType_U32: return DataTypeCompareT<ImU32 >(lhs: (const ImU32* )arg_1, rhs: (const ImU32* )arg_2);
2315 case ImGuiDataType_S64: return DataTypeCompareT<ImS64 >(lhs: (const ImS64* )arg_1, rhs: (const ImS64* )arg_2);
2316 case ImGuiDataType_U64: return DataTypeCompareT<ImU64 >(lhs: (const ImU64* )arg_1, rhs: (const ImU64* )arg_2);
2317 case ImGuiDataType_Float: return DataTypeCompareT<float >(lhs: (const float* )arg_1, rhs: (const float* )arg_2);
2318 case ImGuiDataType_Double: return DataTypeCompareT<double>(lhs: (const double*)arg_1, rhs: (const double*)arg_2);
2319 case ImGuiDataType_COUNT: break;
2320 }
2321 IM_ASSERT(0);
2322 return 0;
2323}
2324
2325template<typename T>
2326static bool DataTypeClampT(T* v, const T* v_min, const T* v_max)
2327{
2328 // Clamp, both sides are optional, return true if modified
2329 if (v_min && *v < *v_min) { *v = *v_min; return true; }
2330 if (v_max && *v > *v_max) { *v = *v_max; return true; }
2331 return false;
2332}
2333
2334bool ImGui::DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max)
2335{
2336 switch (data_type)
2337 {
2338 case ImGuiDataType_S8: return DataTypeClampT<ImS8 >(v: (ImS8* )p_data, v_min: (const ImS8* )p_min, v_max: (const ImS8* )p_max);
2339 case ImGuiDataType_U8: return DataTypeClampT<ImU8 >(v: (ImU8* )p_data, v_min: (const ImU8* )p_min, v_max: (const ImU8* )p_max);
2340 case ImGuiDataType_S16: return DataTypeClampT<ImS16 >(v: (ImS16* )p_data, v_min: (const ImS16* )p_min, v_max: (const ImS16* )p_max);
2341 case ImGuiDataType_U16: return DataTypeClampT<ImU16 >(v: (ImU16* )p_data, v_min: (const ImU16* )p_min, v_max: (const ImU16* )p_max);
2342 case ImGuiDataType_S32: return DataTypeClampT<ImS32 >(v: (ImS32* )p_data, v_min: (const ImS32* )p_min, v_max: (const ImS32* )p_max);
2343 case ImGuiDataType_U32: return DataTypeClampT<ImU32 >(v: (ImU32* )p_data, v_min: (const ImU32* )p_min, v_max: (const ImU32* )p_max);
2344 case ImGuiDataType_S64: return DataTypeClampT<ImS64 >(v: (ImS64* )p_data, v_min: (const ImS64* )p_min, v_max: (const ImS64* )p_max);
2345 case ImGuiDataType_U64: return DataTypeClampT<ImU64 >(v: (ImU64* )p_data, v_min: (const ImU64* )p_min, v_max: (const ImU64* )p_max);
2346 case ImGuiDataType_Float: return DataTypeClampT<float >(v: (float* )p_data, v_min: (const float* )p_min, v_max: (const float* )p_max);
2347 case ImGuiDataType_Double: return DataTypeClampT<double>(v: (double*)p_data, v_min: (const double*)p_min, v_max: (const double*)p_max);
2348 case ImGuiDataType_COUNT: break;
2349 }
2350 IM_ASSERT(0);
2351 return false;
2352}
2353
2354static float GetMinimumStepAtDecimalPrecision(int decimal_precision)
2355{
2356 static const float min_steps[10] = { 1.0f, 0.1f, 0.01f, 0.001f, 0.0001f, 0.00001f, 0.000001f, 0.0000001f, 0.00000001f, 0.000000001f };
2357 if (decimal_precision < 0)
2358 return FLT_MIN;
2359 return (decimal_precision < IM_ARRAYSIZE(min_steps)) ? min_steps[decimal_precision] : ImPow(x: 10.0f, y: (float)-decimal_precision);
2360}
2361
2362template<typename TYPE>
2363TYPE ImGui::RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, TYPE v)
2364{
2365 IM_UNUSED(data_type);
2366 IM_ASSERT(data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double);
2367 const char* fmt_start = ImParseFormatFindStart(format);
2368 if (fmt_start[0] != '%' || fmt_start[1] == '%') // Don't apply if the value is not visible in the format string
2369 return v;
2370
2371 // Sanitize format
2372 char fmt_sanitized[32];
2373 ImParseFormatSanitizeForPrinting(fmt_in: fmt_start, fmt_out: fmt_sanitized, IM_ARRAYSIZE(fmt_sanitized));
2374 fmt_start = fmt_sanitized;
2375
2376 // Format value with our rounding, and read back
2377 char v_str[64];
2378 ImFormatString(v_str, IM_ARRAYSIZE(v_str), fmt_start, v);
2379 const char* p = v_str;
2380 while (*p == ' ')
2381 p++;
2382 v = (TYPE)ImAtof(p);
2383
2384 return v;
2385}
2386
2387//-------------------------------------------------------------------------
2388// [SECTION] Widgets: DragScalar, DragFloat, DragInt, etc.
2389//-------------------------------------------------------------------------
2390// - DragBehaviorT<>() [Internal]
2391// - DragBehavior() [Internal]
2392// - DragScalar()
2393// - DragScalarN()
2394// - DragFloat()
2395// - DragFloat2()
2396// - DragFloat3()
2397// - DragFloat4()
2398// - DragFloatRange2()
2399// - DragInt()
2400// - DragInt2()
2401// - DragInt3()
2402// - DragInt4()
2403// - DragIntRange2()
2404//-------------------------------------------------------------------------
2405
2406// This is called by DragBehavior() when the widget is active (held by mouse or being manipulated with Nav controls)
2407template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2408bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags)
2409{
2410 ImGuiContext& g = *GImGui;
2411 const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
2412 const bool is_bounded = (v_min < v_max);
2413 const bool is_wrapped = is_bounded && (flags & ImGuiSliderFlags_WrapAround);
2414 const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
2415 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2416
2417 // Default tweak speed
2418 if (v_speed == 0.0f && is_bounded && (v_max - v_min < FLT_MAX))
2419 v_speed = (float)((v_max - v_min) * g.DragSpeedDefaultRatio);
2420
2421 // Inputs accumulates into g.DragCurrentAccum, which is flushed into the current value as soon as it makes a difference with our precision settings
2422 float adjust_delta = 0.0f;
2423 if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid() && IsMouseDragPastThreshold(button: 0, lock_threshold: g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2424 {
2425 adjust_delta = g.IO.MouseDelta[axis];
2426 if (g.IO.KeyAlt)
2427 adjust_delta *= 1.0f / 100.0f;
2428 if (g.IO.KeyShift)
2429 adjust_delta *= 10.0f;
2430 }
2431 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
2432 {
2433 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, default_value: 3) : 0;
2434 const bool tweak_slow = IsKeyDown(key: (g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
2435 const bool tweak_fast = IsKeyDown(key: (g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
2436 const float tweak_factor = tweak_slow ? 1.0f / 10.0f : tweak_fast ? 10.0f : 1.0f;
2437 adjust_delta = GetNavTweakPressedAmount(axis) * tweak_factor;
2438 v_speed = ImMax(lhs: v_speed, rhs: GetMinimumStepAtDecimalPrecision(decimal_precision));
2439 }
2440 adjust_delta *= v_speed;
2441
2442 // For vertical drag we currently assume that Up=higher value (like we do with vertical sliders). This may become a parameter.
2443 if (axis == ImGuiAxis_Y)
2444 adjust_delta = -adjust_delta;
2445
2446 // For logarithmic use our range is effectively 0..1 so scale the delta into that range
2447 if (is_logarithmic && (v_max - v_min < FLT_MAX) && ((v_max - v_min) > 0.000001f)) // Epsilon to avoid /0
2448 adjust_delta /= (float)(v_max - v_min);
2449
2450 // Clear current value on activation
2451 // Avoid altering values and clamping when we are _already_ past the limits and heading in the same direction, so e.g. if range is 0..255, current value is 300 and we are pushing to the right side, keep the 300.
2452 const bool is_just_activated = g.ActiveIdIsJustActivated;
2453 const bool is_already_past_limits_and_pushing_outward = is_bounded && !is_wrapped && ((*v >= v_max && adjust_delta > 0.0f) || (*v <= v_min && adjust_delta < 0.0f));
2454 if (is_just_activated || is_already_past_limits_and_pushing_outward)
2455 {
2456 g.DragCurrentAccum = 0.0f;
2457 g.DragCurrentAccumDirty = false;
2458 }
2459 else if (adjust_delta != 0.0f)
2460 {
2461 g.DragCurrentAccum += adjust_delta;
2462 g.DragCurrentAccumDirty = true;
2463 }
2464
2465 if (!g.DragCurrentAccumDirty)
2466 return false;
2467
2468 TYPE v_cur = *v;
2469 FLOATTYPE v_old_ref_for_accum_remainder = (FLOATTYPE)0.0f;
2470
2471 float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
2472 const float zero_deadzone_halfsize = 0.0f; // Drag widgets have no deadzone (as it doesn't make sense)
2473 if (is_logarithmic)
2474 {
2475 // When using logarithmic sliders, we need to clamp to avoid hitting zero, but our choice of clamp value greatly affects slider precision. We attempt to use the specified precision to estimate a good lower bound.
2476 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, default_value: 3) : 1;
2477 logarithmic_zero_epsilon = ImPow(x: 0.1f, y: (float)decimal_precision);
2478
2479 // Convert to parametric space, apply delta, convert back
2480 float v_old_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2481 float v_new_parametric = v_old_parametric + g.DragCurrentAccum;
2482 v_cur = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new_parametric, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2483 v_old_ref_for_accum_remainder = v_old_parametric;
2484 }
2485 else
2486 {
2487 v_cur += (SIGNEDTYPE)g.DragCurrentAccum;
2488 }
2489
2490 // Round to user desired precision based on format string
2491 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
2492 v_cur = RoundScalarWithFormatT<TYPE>(format, data_type, v_cur);
2493
2494 // Preserve remainder after rounding has been applied. This also allow slow tweaking of values.
2495 g.DragCurrentAccumDirty = false;
2496 if (is_logarithmic)
2497 {
2498 // Convert to parametric space, apply delta, convert back
2499 float v_new_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2500 g.DragCurrentAccum -= (float)(v_new_parametric - v_old_ref_for_accum_remainder);
2501 }
2502 else
2503 {
2504 g.DragCurrentAccum -= (float)((SIGNEDTYPE)v_cur - (SIGNEDTYPE)*v);
2505 }
2506
2507 // Lose zero sign for float/double
2508 if (v_cur == (TYPE)-0)
2509 v_cur = (TYPE)0;
2510
2511 if (*v != v_cur && is_bounded)
2512 {
2513 if (is_wrapped)
2514 {
2515 // Wrap values
2516 if (v_cur < v_min)
2517 v_cur += v_max - v_min + (is_floating_point ? 0 : 1);
2518 if (v_cur > v_max)
2519 v_cur -= v_max - v_min + (is_floating_point ? 0 : 1);
2520 }
2521 else
2522 {
2523 // Clamp values + handle overflow/wrap-around for integer types.
2524 if (v_cur < v_min || (v_cur > *v && adjust_delta < 0.0f && !is_floating_point))
2525 v_cur = v_min;
2526 if (v_cur > v_max || (v_cur < *v && adjust_delta > 0.0f && !is_floating_point))
2527 v_cur = v_max;
2528 }
2529 }
2530
2531 // Apply result
2532 if (*v == v_cur)
2533 return false;
2534 *v = v_cur;
2535 return true;
2536}
2537
2538bool ImGui::DragBehavior(ImGuiID id, ImGuiDataType data_type, void* p_v, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags)
2539{
2540 // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
2541 IM_ASSERT((flags == 1 || (flags & ImGuiSliderFlags_InvalidMask_) == 0) && "Invalid ImGuiSliderFlags flags! Has the legacy 'float power' argument been mistakenly cast to flags? Call function with ImGuiSliderFlags_Logarithmic flags instead.");
2542
2543 ImGuiContext& g = *GImGui;
2544 if (g.ActiveId == id)
2545 {
2546 // Those are the things we can do easily outside the DragBehaviorT<> template, saves code generation.
2547 if (g.ActiveIdSource == ImGuiInputSource_Mouse && !g.IO.MouseDown[0])
2548 ClearActiveID();
2549 else if ((g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) && g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
2550 ClearActiveID();
2551 }
2552 if (g.ActiveId != id)
2553 return false;
2554 if ((g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
2555 return false;
2556
2557 switch (data_type)
2558 {
2559 case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = DragBehaviorT<ImS32, ImS32, float>(data_type: ImGuiDataType_S32, v: &v32, v_speed, v_min: p_min ? *(const ImS8*) p_min : IM_S8_MIN, v_max: p_max ? *(const ImS8*)p_max : IM_S8_MAX, format, flags); if (r) *(ImS8*)p_v = (ImS8)v32; return r; }
2560 case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)p_v; bool r = DragBehaviorT<ImU32, ImS32, float>(data_type: ImGuiDataType_U32, v: &v32, v_speed, v_min: p_min ? *(const ImU8*) p_min : IM_U8_MIN, v_max: p_max ? *(const ImU8*)p_max : IM_U8_MAX, format, flags); if (r) *(ImU8*)p_v = (ImU8)v32; return r; }
2561 case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)p_v; bool r = DragBehaviorT<ImS32, ImS32, float>(data_type: ImGuiDataType_S32, v: &v32, v_speed, v_min: p_min ? *(const ImS16*)p_min : IM_S16_MIN, v_max: p_max ? *(const ImS16*)p_max : IM_S16_MAX, format, flags); if (r) *(ImS16*)p_v = (ImS16)v32; return r; }
2562 case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)p_v; bool r = DragBehaviorT<ImU32, ImS32, float>(data_type: ImGuiDataType_U32, v: &v32, v_speed, v_min: p_min ? *(const ImU16*)p_min : IM_U16_MIN, v_max: p_max ? *(const ImU16*)p_max : IM_U16_MAX, format, flags); if (r) *(ImU16*)p_v = (ImU16)v32; return r; }
2563 case ImGuiDataType_S32: return DragBehaviorT<ImS32, ImS32, float >(data_type, v: (ImS32*)p_v, v_speed, v_min: p_min ? *(const ImS32* )p_min : IM_S32_MIN, v_max: p_max ? *(const ImS32* )p_max : IM_S32_MAX, format, flags);
2564 case ImGuiDataType_U32: return DragBehaviorT<ImU32, ImS32, float >(data_type, v: (ImU32*)p_v, v_speed, v_min: p_min ? *(const ImU32* )p_min : IM_U32_MIN, v_max: p_max ? *(const ImU32* )p_max : IM_U32_MAX, format, flags);
2565 case ImGuiDataType_S64: return DragBehaviorT<ImS64, ImS64, double>(data_type, v: (ImS64*)p_v, v_speed, v_min: p_min ? *(const ImS64* )p_min : IM_S64_MIN, v_max: p_max ? *(const ImS64* )p_max : IM_S64_MAX, format, flags);
2566 case ImGuiDataType_U64: return DragBehaviorT<ImU64, ImS64, double>(data_type, v: (ImU64*)p_v, v_speed, v_min: p_min ? *(const ImU64* )p_min : IM_U64_MIN, v_max: p_max ? *(const ImU64* )p_max : IM_U64_MAX, format, flags);
2567 case ImGuiDataType_Float: return DragBehaviorT<float, float, float >(data_type, v: (float*)p_v, v_speed, v_min: p_min ? *(const float* )p_min : -FLT_MAX, v_max: p_max ? *(const float* )p_max : FLT_MAX, format, flags);
2568 case ImGuiDataType_Double: return DragBehaviorT<double,double,double>(data_type, v: (double*)p_v, v_speed, v_min: p_min ? *(const double*)p_min : -DBL_MAX, v_max: p_max ? *(const double*)p_max : DBL_MAX, format, flags);
2569 case ImGuiDataType_COUNT: break;
2570 }
2571 IM_ASSERT(0);
2572 return false;
2573}
2574
2575// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a Drag widget, p_min and p_max are optional.
2576// Read code of e.g. DragFloat(), DragInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
2577bool ImGui::DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags)
2578{
2579 ImGuiWindow* window = GetCurrentWindow();
2580 if (window->SkipItems)
2581 return false;
2582
2583 ImGuiContext& g = *GImGui;
2584 const ImGuiStyle& style = g.Style;
2585 const ImGuiID id = window->GetID(str: label);
2586 const float w = CalcItemWidth();
2587
2588 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
2589 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
2590 const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
2591
2592 const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
2593 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
2594 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
2595 return false;
2596
2597 // Default format string when passing NULL
2598 if (format == NULL)
2599 format = DataTypeGetInfo(data_type)->PrintFmt;
2600
2601 const bool hovered = ItemHoverable(bb: frame_bb, id, item_flags: g.LastItemData.InFlags);
2602 bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
2603 if (!temp_input_is_active)
2604 {
2605 // Tabbing or CTRL-clicking on Drag turns it into an InputText
2606 const bool clicked = hovered && IsMouseClicked(button: 0, flags: ImGuiInputFlags_None, owner_id: id);
2607 const bool double_clicked = (hovered && g.IO.MouseClickedCount[0] == 2 && TestKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id));
2608 const bool make_active = (clicked || double_clicked || g.NavActivateId == id);
2609 if (make_active && (clicked || double_clicked))
2610 SetKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id);
2611 if (make_active && temp_input_allowed)
2612 if ((clicked && g.IO.KeyCtrl) || double_clicked || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
2613 temp_input_is_active = true;
2614
2615 // (Optional) simple click (without moving) turns Drag into an InputText
2616 if (g.IO.ConfigDragClickToInputText && temp_input_allowed && !temp_input_is_active)
2617 if (g.ActiveId == id && hovered && g.IO.MouseReleased[0] && !IsMouseDragPastThreshold(button: 0, lock_threshold: g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2618 {
2619 g.NavActivateId = id;
2620 g.NavActivateFlags = ImGuiActivateFlags_PreferInput;
2621 temp_input_is_active = true;
2622 }
2623
2624 if (make_active && !temp_input_is_active)
2625 {
2626 SetActiveID(id, window);
2627 SetFocusID(id, window);
2628 FocusWindow(window);
2629 g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
2630 }
2631 }
2632
2633 if (temp_input_is_active)
2634 {
2635 // Only clamp CTRL+Click input when ImGuiSliderFlags_AlwaysClamp is set
2636 const bool is_clamp_input = (flags & ImGuiSliderFlags_AlwaysClamp) != 0 && (p_min == NULL || p_max == NULL || DataTypeCompare(data_type, arg_1: p_min, arg_2: p_max) < 0);
2637 return TempInputScalar(bb: frame_bb, id, label, data_type, p_data, format, p_clamp_min: is_clamp_input ? p_min : NULL, p_clamp_max: is_clamp_input ? p_max : NULL);
2638 }
2639
2640 // Draw frame
2641 const ImU32 frame_col = GetColorU32(idx: g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
2642 RenderNavHighlight(bb: frame_bb, id);
2643 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: frame_col, border: true, rounding: style.FrameRounding);
2644
2645 // Drag behavior
2646 const bool value_changed = DragBehavior(id, data_type, p_v: p_data, v_speed, p_min, p_max, format, flags);
2647 if (value_changed)
2648 MarkItemEdited(id);
2649
2650 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
2651 char value_buf[64];
2652 const char* value_buf_end = value_buf + DataTypeFormatString(buf: value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
2653 if (g.LogEnabled)
2654 LogSetNextTextDecoration(prefix: "{", suffix: "}");
2655 RenderTextClipped(pos_min: frame_bb.Min, pos_max: frame_bb.Max, text: value_buf, text_end: value_buf_end, NULL, align: ImVec2(0.5f, 0.5f));
2656
2657 if (label_size.x > 0.0f)
2658 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), text: label);
2659
2660 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
2661 return value_changed;
2662}
2663
2664bool ImGui::DragScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags)
2665{
2666 ImGuiWindow* window = GetCurrentWindow();
2667 if (window->SkipItems)
2668 return false;
2669
2670 ImGuiContext& g = *GImGui;
2671 bool value_changed = false;
2672 BeginGroup();
2673 PushID(str_id: label);
2674 PushMultiItemsWidths(components, width_full: CalcItemWidth());
2675 size_t type_size = GDataTypeInfo[data_type].Size;
2676 for (int i = 0; i < components; i++)
2677 {
2678 PushID(int_id: i);
2679 if (i > 0)
2680 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2681 value_changed |= DragScalar(label: "", data_type, p_data, v_speed, p_min, p_max, format, flags);
2682 PopID();
2683 PopItemWidth();
2684 p_data = (void*)((char*)p_data + type_size);
2685 }
2686 PopID();
2687
2688 const char* label_end = FindRenderedTextEnd(text: label);
2689 if (label != label_end)
2690 {
2691 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2692 TextEx(text: label, text_end: label_end);
2693 }
2694
2695 EndGroup();
2696 return value_changed;
2697}
2698
2699bool ImGui::DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2700{
2701 return DragScalar(label, data_type: ImGuiDataType_Float, p_data: v, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2702}
2703
2704bool ImGui::DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2705{
2706 return DragScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 2, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2707}
2708
2709bool ImGui::DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2710{
2711 return DragScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 3, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2712}
2713
2714bool ImGui::DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2715{
2716 return DragScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 4, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2717}
2718
2719// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2720bool ImGui::DragFloatRange2(const char* label, float* v_current_min, float* v_current_max, float v_speed, float v_min, float v_max, const char* format, const char* format_max, ImGuiSliderFlags flags)
2721{
2722 ImGuiWindow* window = GetCurrentWindow();
2723 if (window->SkipItems)
2724 return false;
2725
2726 ImGuiContext& g = *GImGui;
2727 PushID(str_id: label);
2728 BeginGroup();
2729 PushMultiItemsWidths(components: 2, width_full: CalcItemWidth());
2730
2731 float min_min = (v_min >= v_max) ? -FLT_MAX : v_min;
2732 float min_max = (v_min >= v_max) ? *v_current_max : ImMin(lhs: v_max, rhs: *v_current_max);
2733 ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2734 bool value_changed = DragScalar(label: "##min", data_type: ImGuiDataType_Float, p_data: v_current_min, v_speed, p_min: &min_min, p_max: &min_max, format, flags: min_flags);
2735 PopItemWidth();
2736 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2737
2738 float max_min = (v_min >= v_max) ? *v_current_min : ImMax(lhs: v_min, rhs: *v_current_min);
2739 float max_max = (v_min >= v_max) ? FLT_MAX : v_max;
2740 ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2741 value_changed |= DragScalar(label: "##max", data_type: ImGuiDataType_Float, p_data: v_current_max, v_speed, p_min: &max_min, p_max: &max_max, format: format_max ? format_max : format, flags: max_flags);
2742 PopItemWidth();
2743 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2744
2745 TextEx(text: label, text_end: FindRenderedTextEnd(text: label));
2746 EndGroup();
2747 PopID();
2748
2749 return value_changed;
2750}
2751
2752// NB: v_speed is float to allow adjusting the drag speed with more precision
2753bool ImGui::DragInt(const char* label, int* v, float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2754{
2755 return DragScalar(label, data_type: ImGuiDataType_S32, p_data: v, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2756}
2757
2758bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2759{
2760 return DragScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 2, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2761}
2762
2763bool ImGui::DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2764{
2765 return DragScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 3, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2766}
2767
2768bool ImGui::DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2769{
2770 return DragScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 4, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2771}
2772
2773// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2774bool ImGui::DragIntRange2(const char* label, int* v_current_min, int* v_current_max, float v_speed, int v_min, int v_max, const char* format, const char* format_max, ImGuiSliderFlags flags)
2775{
2776 ImGuiWindow* window = GetCurrentWindow();
2777 if (window->SkipItems)
2778 return false;
2779
2780 ImGuiContext& g = *GImGui;
2781 PushID(str_id: label);
2782 BeginGroup();
2783 PushMultiItemsWidths(components: 2, width_full: CalcItemWidth());
2784
2785 int min_min = (v_min >= v_max) ? INT_MIN : v_min;
2786 int min_max = (v_min >= v_max) ? *v_current_max : ImMin(lhs: v_max, rhs: *v_current_max);
2787 ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2788 bool value_changed = DragInt(label: "##min", v: v_current_min, v_speed, v_min: min_min, v_max: min_max, format, flags: min_flags);
2789 PopItemWidth();
2790 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2791
2792 int max_min = (v_min >= v_max) ? *v_current_min : ImMax(lhs: v_min, rhs: *v_current_min);
2793 int max_max = (v_min >= v_max) ? INT_MAX : v_max;
2794 ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2795 value_changed |= DragInt(label: "##max", v: v_current_max, v_speed, v_min: max_min, v_max: max_max, format: format_max ? format_max : format, flags: max_flags);
2796 PopItemWidth();
2797 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2798
2799 TextEx(text: label, text_end: FindRenderedTextEnd(text: label));
2800 EndGroup();
2801 PopID();
2802
2803 return value_changed;
2804}
2805
2806//-------------------------------------------------------------------------
2807// [SECTION] Widgets: SliderScalar, SliderFloat, SliderInt, etc.
2808//-------------------------------------------------------------------------
2809// - ScaleRatioFromValueT<> [Internal]
2810// - ScaleValueFromRatioT<> [Internal]
2811// - SliderBehaviorT<>() [Internal]
2812// - SliderBehavior() [Internal]
2813// - SliderScalar()
2814// - SliderScalarN()
2815// - SliderFloat()
2816// - SliderFloat2()
2817// - SliderFloat3()
2818// - SliderFloat4()
2819// - SliderAngle()
2820// - SliderInt()
2821// - SliderInt2()
2822// - SliderInt3()
2823// - SliderInt4()
2824// - VSliderScalar()
2825// - VSliderFloat()
2826// - VSliderInt()
2827//-------------------------------------------------------------------------
2828
2829// Convert a value v in the output space of a slider into a parametric position on the slider itself (the logical opposite of ScaleValueFromRatioT)
2830template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2831float ImGui::ScaleRatioFromValueT(ImGuiDataType data_type, TYPE v, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize)
2832{
2833 if (v_min == v_max)
2834 return 0.0f;
2835 IM_UNUSED(data_type);
2836
2837 const TYPE v_clamped = (v_min < v_max) ? ImClamp(v, v_min, v_max) : ImClamp(v, v_max, v_min);
2838 if (is_logarithmic)
2839 {
2840 bool flipped = v_max < v_min;
2841
2842 if (flipped) // Handle the case where the range is backwards
2843 ImSwap(v_min, v_max);
2844
2845 // Fudge min/max to avoid getting close to log(0)
2846 FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
2847 FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
2848
2849 // Awkward special cases - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
2850 if ((v_min == 0.0f) && (v_max < 0.0f))
2851 v_min_fudged = -logarithmic_zero_epsilon;
2852 else if ((v_max == 0.0f) && (v_min < 0.0f))
2853 v_max_fudged = -logarithmic_zero_epsilon;
2854
2855 float result;
2856 if (v_clamped <= v_min_fudged)
2857 result = 0.0f; // Workaround for values that are in-range but below our fudge
2858 else if (v_clamped >= v_max_fudged)
2859 result = 1.0f; // Workaround for values that are in-range but above our fudge
2860 else if ((v_min * v_max) < 0.0f) // Range crosses zero, so split into two portions
2861 {
2862 float zero_point_center = (-(float)v_min) / ((float)v_max - (float)v_min); // The zero point in parametric space. There's an argument we should take the logarithmic nature into account when calculating this, but for now this should do (and the most common case of a symmetrical range works fine)
2863 float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
2864 float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
2865 if (v == 0.0f)
2866 result = zero_point_center; // Special case for exactly zero
2867 else if (v < 0.0f)
2868 result = (1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(-v_min_fudged / logarithmic_zero_epsilon))) * zero_point_snap_L;
2869 else
2870 result = zero_point_snap_R + ((float)(ImLog((FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(v_max_fudged / logarithmic_zero_epsilon)) * (1.0f - zero_point_snap_R));
2871 }
2872 else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
2873 result = 1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / -v_max_fudged) / ImLog(-v_min_fudged / -v_max_fudged));
2874 else
2875 result = (float)(ImLog((FLOATTYPE)v_clamped / v_min_fudged) / ImLog(v_max_fudged / v_min_fudged));
2876
2877 return flipped ? (1.0f - result) : result;
2878 }
2879 else
2880 {
2881 // Linear slider
2882 return (float)((FLOATTYPE)(SIGNEDTYPE)(v_clamped - v_min) / (FLOATTYPE)(SIGNEDTYPE)(v_max - v_min));
2883 }
2884}
2885
2886// Convert a parametric position on a slider into a value v in the output space (the logical opposite of ScaleRatioFromValueT)
2887template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2888TYPE ImGui::ScaleValueFromRatioT(ImGuiDataType data_type, float t, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize)
2889{
2890 // We special-case the extents because otherwise our logarithmic fudging can lead to "mathematically correct"
2891 // but non-intuitive behaviors like a fully-left slider not actually reaching the minimum value. Also generally simpler.
2892 if (t <= 0.0f || v_min == v_max)
2893 return v_min;
2894 if (t >= 1.0f)
2895 return v_max;
2896
2897 TYPE result = (TYPE)0;
2898 if (is_logarithmic)
2899 {
2900 // Fudge min/max to avoid getting silly results close to zero
2901 FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
2902 FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
2903
2904 const bool flipped = v_max < v_min; // Check if range is "backwards"
2905 if (flipped)
2906 ImSwap(v_min_fudged, v_max_fudged);
2907
2908 // Awkward special case - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
2909 if ((v_max == 0.0f) && (v_min < 0.0f))
2910 v_max_fudged = -logarithmic_zero_epsilon;
2911
2912 float t_with_flip = flipped ? (1.0f - t) : t; // t, but flipped if necessary to account for us flipping the range
2913
2914 if ((v_min * v_max) < 0.0f) // Range crosses zero, so we have to do this in two parts
2915 {
2916 float zero_point_center = (-(float)ImMin(v_min, v_max)) / ImAbs(x: (float)v_max - (float)v_min); // The zero point in parametric space
2917 float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
2918 float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
2919 if (t_with_flip >= zero_point_snap_L && t_with_flip <= zero_point_snap_R)
2920 result = (TYPE)0.0f; // Special case to make getting exactly zero possible (the epsilon prevents it otherwise)
2921 else if (t_with_flip < zero_point_center)
2922 result = (TYPE)-(logarithmic_zero_epsilon * ImPow(-v_min_fudged / logarithmic_zero_epsilon, (FLOATTYPE)(1.0f - (t_with_flip / zero_point_snap_L))));
2923 else
2924 result = (TYPE)(logarithmic_zero_epsilon * ImPow(v_max_fudged / logarithmic_zero_epsilon, (FLOATTYPE)((t_with_flip - zero_point_snap_R) / (1.0f - zero_point_snap_R))));
2925 }
2926 else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
2927 result = (TYPE)-(-v_max_fudged * ImPow(-v_min_fudged / -v_max_fudged, (FLOATTYPE)(1.0f - t_with_flip)));
2928 else
2929 result = (TYPE)(v_min_fudged * ImPow(v_max_fudged / v_min_fudged, (FLOATTYPE)t_with_flip));
2930 }
2931 else
2932 {
2933 // Linear slider
2934 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2935 if (is_floating_point)
2936 {
2937 result = ImLerp(v_min, v_max, t);
2938 }
2939 else if (t < 1.0)
2940 {
2941 // - For integer values we want the clicking position to match the grab box so we round above
2942 // This code is carefully tuned to work with large values (e.g. high ranges of U64) while preserving this property..
2943 // - Not doing a *1.0 multiply at the end of a range as it tends to be lossy. While absolute aiming at a large s64/u64
2944 // range is going to be imprecise anyway, with this check we at least make the edge values matches expected limits.
2945 FLOATTYPE v_new_off_f = (SIGNEDTYPE)(v_max - v_min) * t;
2946 result = (TYPE)((SIGNEDTYPE)v_min + (SIGNEDTYPE)(v_new_off_f + (FLOATTYPE)(v_min > v_max ? -0.5 : 0.5)));
2947 }
2948 }
2949
2950 return result;
2951}
2952
2953// FIXME: Try to move more of the code into shared SliderBehavior()
2954template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2955bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, TYPE* v, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb)
2956{
2957 ImGuiContext& g = *GImGui;
2958 const ImGuiStyle& style = g.Style;
2959
2960 const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
2961 const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
2962 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2963 const float v_range_f = (float)(v_min < v_max ? v_max - v_min : v_min - v_max); // We don't need high precision for what we do with it.
2964
2965 // Calculate bounds
2966 const float grab_padding = 2.0f; // FIXME: Should be part of style.
2967 const float slider_sz = (bb.Max[axis] - bb.Min[axis]) - grab_padding * 2.0f;
2968 float grab_sz = style.GrabMinSize;
2969 if (!is_floating_point && v_range_f >= 0.0f) // v_range_f < 0 may happen on integer overflows
2970 grab_sz = ImMax(lhs: slider_sz / (v_range_f + 1), rhs: style.GrabMinSize); // For integer sliders: if possible have the grab size represent 1 unit
2971 grab_sz = ImMin(lhs: grab_sz, rhs: slider_sz);
2972 const float slider_usable_sz = slider_sz - grab_sz;
2973 const float slider_usable_pos_min = bb.Min[axis] + grab_padding + grab_sz * 0.5f;
2974 const float slider_usable_pos_max = bb.Max[axis] - grab_padding - grab_sz * 0.5f;
2975
2976 float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
2977 float zero_deadzone_halfsize = 0.0f; // Only valid when is_logarithmic is true
2978 if (is_logarithmic)
2979 {
2980 // When using logarithmic sliders, we need to clamp to avoid hitting zero, but our choice of clamp value greatly affects slider precision. We attempt to use the specified precision to estimate a good lower bound.
2981 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, default_value: 3) : 1;
2982 logarithmic_zero_epsilon = ImPow(x: 0.1f, y: (float)decimal_precision);
2983 zero_deadzone_halfsize = (style.LogSliderDeadzone * 0.5f) / ImMax(lhs: slider_usable_sz, rhs: 1.0f);
2984 }
2985
2986 // Process interacting with the slider
2987 bool value_changed = false;
2988 if (g.ActiveId == id)
2989 {
2990 bool set_new_value = false;
2991 float clicked_t = 0.0f;
2992 if (g.ActiveIdSource == ImGuiInputSource_Mouse)
2993 {
2994 if (!g.IO.MouseDown[0])
2995 {
2996 ClearActiveID();
2997 }
2998 else
2999 {
3000 const float mouse_abs_pos = g.IO.MousePos[axis];
3001 if (g.ActiveIdIsJustActivated)
3002 {
3003 float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3004 if (axis == ImGuiAxis_Y)
3005 grab_t = 1.0f - grab_t;
3006 const float grab_pos = ImLerp(a: slider_usable_pos_min, b: slider_usable_pos_max, t: grab_t);
3007 const bool clicked_around_grab = (mouse_abs_pos >= grab_pos - grab_sz * 0.5f - 1.0f) && (mouse_abs_pos <= grab_pos + grab_sz * 0.5f + 1.0f); // No harm being extra generous here.
3008 g.SliderGrabClickOffset = (clicked_around_grab && is_floating_point) ? mouse_abs_pos - grab_pos : 0.0f;
3009 }
3010 if (slider_usable_sz > 0.0f)
3011 clicked_t = ImSaturate(f: (mouse_abs_pos - g.SliderGrabClickOffset - slider_usable_pos_min) / slider_usable_sz);
3012 if (axis == ImGuiAxis_Y)
3013 clicked_t = 1.0f - clicked_t;
3014 set_new_value = true;
3015 }
3016 }
3017 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
3018 {
3019 if (g.ActiveIdIsJustActivated)
3020 {
3021 g.SliderCurrentAccum = 0.0f; // Reset any stored nav delta upon activation
3022 g.SliderCurrentAccumDirty = false;
3023 }
3024
3025 float input_delta = (axis == ImGuiAxis_X) ? GetNavTweakPressedAmount(axis) : -GetNavTweakPressedAmount(axis);
3026 if (input_delta != 0.0f)
3027 {
3028 const bool tweak_slow = IsKeyDown(key: (g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
3029 const bool tweak_fast = IsKeyDown(key: (g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
3030 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, default_value: 3) : 0;
3031 if (decimal_precision > 0)
3032 {
3033 input_delta /= 100.0f; // Gamepad/keyboard tweak speeds in % of slider bounds
3034 if (tweak_slow)
3035 input_delta /= 10.0f;
3036 }
3037 else
3038 {
3039 if ((v_range_f >= -100.0f && v_range_f <= 100.0f && v_range_f != 0.0f) || tweak_slow)
3040 input_delta = ((input_delta < 0.0f) ? -1.0f : +1.0f) / v_range_f; // Gamepad/keyboard tweak speeds in integer steps
3041 else
3042 input_delta /= 100.0f;
3043 }
3044 if (tweak_fast)
3045 input_delta *= 10.0f;
3046
3047 g.SliderCurrentAccum += input_delta;
3048 g.SliderCurrentAccumDirty = true;
3049 }
3050
3051 float delta = g.SliderCurrentAccum;
3052 if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
3053 {
3054 ClearActiveID();
3055 }
3056 else if (g.SliderCurrentAccumDirty)
3057 {
3058 clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3059
3060 if ((clicked_t >= 1.0f && delta > 0.0f) || (clicked_t <= 0.0f && delta < 0.0f)) // This is to avoid applying the saturation when already past the limits
3061 {
3062 set_new_value = false;
3063 g.SliderCurrentAccum = 0.0f; // If pushing up against the limits, don't continue to accumulate
3064 }
3065 else
3066 {
3067 set_new_value = true;
3068 float old_clicked_t = clicked_t;
3069 clicked_t = ImSaturate(f: clicked_t + delta);
3070
3071 // Calculate what our "new" clicked_t will be, and thus how far we actually moved the slider, and subtract this from the accumulator
3072 TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3073 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
3074 v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
3075 float new_clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3076
3077 if (delta > 0)
3078 g.SliderCurrentAccum -= ImMin(lhs: new_clicked_t - old_clicked_t, rhs: delta);
3079 else
3080 g.SliderCurrentAccum -= ImMax(lhs: new_clicked_t - old_clicked_t, rhs: delta);
3081 }
3082
3083 g.SliderCurrentAccumDirty = false;
3084 }
3085 }
3086
3087 if (set_new_value)
3088 if ((g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
3089 set_new_value = false;
3090
3091 if (set_new_value)
3092 {
3093 TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3094
3095 // Round to user desired precision based on format string
3096 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
3097 v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
3098
3099 // Apply result
3100 if (*v != v_new)
3101 {
3102 *v = v_new;
3103 value_changed = true;
3104 }
3105 }
3106 }
3107
3108 if (slider_sz < 1.0f)
3109 {
3110 *out_grab_bb = ImRect(bb.Min, bb.Min);
3111 }
3112 else
3113 {
3114 // Output grab position so it can be displayed by the caller
3115 float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3116 if (axis == ImGuiAxis_Y)
3117 grab_t = 1.0f - grab_t;
3118 const float grab_pos = ImLerp(a: slider_usable_pos_min, b: slider_usable_pos_max, t: grab_t);
3119 if (axis == ImGuiAxis_X)
3120 *out_grab_bb = ImRect(grab_pos - grab_sz * 0.5f, bb.Min.y + grab_padding, grab_pos + grab_sz * 0.5f, bb.Max.y - grab_padding);
3121 else
3122 *out_grab_bb = ImRect(bb.Min.x + grab_padding, grab_pos - grab_sz * 0.5f, bb.Max.x - grab_padding, grab_pos + grab_sz * 0.5f);
3123 }
3124
3125 return value_changed;
3126}
3127
3128// For 32-bit and larger types, slider bounds are limited to half the natural type range.
3129// So e.g. an integer Slider between INT_MAX-10 and INT_MAX will fail, but an integer Slider between INT_MAX/2-10 and INT_MAX/2 will be ok.
3130// It would be possible to lift that limitation with some work but it doesn't seem to be worth it for sliders.
3131bool ImGui::SliderBehavior(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, void* p_v, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb)
3132{
3133 // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
3134 IM_ASSERT((flags == 1 || (flags & ImGuiSliderFlags_InvalidMask_) == 0) && "Invalid ImGuiSliderFlags flags! Has the legacy 'float power' argument been mistakenly cast to flags? Call function with ImGuiSliderFlags_Logarithmic flags instead.");
3135 IM_ASSERT((flags & ImGuiSliderFlags_WrapAround) == 0); // Not supported by SliderXXX(), only by DragXXX()
3136
3137 switch (data_type)
3138 {
3139 case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = SliderBehaviorT<ImS32, ImS32, float>(bb, id, data_type: ImGuiDataType_S32, v: &v32, v_min: *(const ImS8*)p_min, v_max: *(const ImS8*)p_max, format, flags, out_grab_bb); if (r) *(ImS8*)p_v = (ImS8)v32; return r; }
3140 case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)p_v; bool r = SliderBehaviorT<ImU32, ImS32, float>(bb, id, data_type: ImGuiDataType_U32, v: &v32, v_min: *(const ImU8*)p_min, v_max: *(const ImU8*)p_max, format, flags, out_grab_bb); if (r) *(ImU8*)p_v = (ImU8)v32; return r; }
3141 case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)p_v; bool r = SliderBehaviorT<ImS32, ImS32, float>(bb, id, data_type: ImGuiDataType_S32, v: &v32, v_min: *(const ImS16*)p_min, v_max: *(const ImS16*)p_max, format, flags, out_grab_bb); if (r) *(ImS16*)p_v = (ImS16)v32; return r; }
3142 case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)p_v; bool r = SliderBehaviorT<ImU32, ImS32, float>(bb, id, data_type: ImGuiDataType_U32, v: &v32, v_min: *(const ImU16*)p_min, v_max: *(const ImU16*)p_max, format, flags, out_grab_bb); if (r) *(ImU16*)p_v = (ImU16)v32; return r; }
3143 case ImGuiDataType_S32:
3144 IM_ASSERT(*(const ImS32*)p_min >= IM_S32_MIN / 2 && *(const ImS32*)p_max <= IM_S32_MAX / 2);
3145 return SliderBehaviorT<ImS32, ImS32, float >(bb, id, data_type, v: (ImS32*)p_v, v_min: *(const ImS32*)p_min, v_max: *(const ImS32*)p_max, format, flags, out_grab_bb);
3146 case ImGuiDataType_U32:
3147 IM_ASSERT(*(const ImU32*)p_max <= IM_U32_MAX / 2);
3148 return SliderBehaviorT<ImU32, ImS32, float >(bb, id, data_type, v: (ImU32*)p_v, v_min: *(const ImU32*)p_min, v_max: *(const ImU32*)p_max, format, flags, out_grab_bb);
3149 case ImGuiDataType_S64:
3150 IM_ASSERT(*(const ImS64*)p_min >= IM_S64_MIN / 2 && *(const ImS64*)p_max <= IM_S64_MAX / 2);
3151 return SliderBehaviorT<ImS64, ImS64, double>(bb, id, data_type, v: (ImS64*)p_v, v_min: *(const ImS64*)p_min, v_max: *(const ImS64*)p_max, format, flags, out_grab_bb);
3152 case ImGuiDataType_U64:
3153 IM_ASSERT(*(const ImU64*)p_max <= IM_U64_MAX / 2);
3154 return SliderBehaviorT<ImU64, ImS64, double>(bb, id, data_type, v: (ImU64*)p_v, v_min: *(const ImU64*)p_min, v_max: *(const ImU64*)p_max, format, flags, out_grab_bb);
3155 case ImGuiDataType_Float:
3156 IM_ASSERT(*(const float*)p_min >= -FLT_MAX / 2.0f && *(const float*)p_max <= FLT_MAX / 2.0f);
3157 return SliderBehaviorT<float, float, float >(bb, id, data_type, v: (float*)p_v, v_min: *(const float*)p_min, v_max: *(const float*)p_max, format, flags, out_grab_bb);
3158 case ImGuiDataType_Double:
3159 IM_ASSERT(*(const double*)p_min >= -DBL_MAX / 2.0f && *(const double*)p_max <= DBL_MAX / 2.0f);
3160 return SliderBehaviorT<double, double, double>(bb, id, data_type, v: (double*)p_v, v_min: *(const double*)p_min, v_max: *(const double*)p_max, format, flags, out_grab_bb);
3161 case ImGuiDataType_COUNT: break;
3162 }
3163 IM_ASSERT(0);
3164 return false;
3165}
3166
3167// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a slider, they are all required.
3168// Read code of e.g. SliderFloat(), SliderInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3169bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags)
3170{
3171 ImGuiWindow* window = GetCurrentWindow();
3172 if (window->SkipItems)
3173 return false;
3174
3175 ImGuiContext& g = *GImGui;
3176 const ImGuiStyle& style = g.Style;
3177 const ImGuiID id = window->GetID(str: label);
3178 const float w = CalcItemWidth();
3179
3180 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
3181 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
3182 const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
3183
3184 const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
3185 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
3186 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
3187 return false;
3188
3189 // Default format string when passing NULL
3190 if (format == NULL)
3191 format = DataTypeGetInfo(data_type)->PrintFmt;
3192
3193 const bool hovered = ItemHoverable(bb: frame_bb, id, item_flags: g.LastItemData.InFlags);
3194 bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
3195 if (!temp_input_is_active)
3196 {
3197 // Tabbing or CTRL-clicking on Slider turns it into an input box
3198 const bool clicked = hovered && IsMouseClicked(button: 0, flags: ImGuiInputFlags_None, owner_id: id);
3199 const bool make_active = (clicked || g.NavActivateId == id);
3200 if (make_active && clicked)
3201 SetKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id);
3202 if (make_active && temp_input_allowed)
3203 if ((clicked && g.IO.KeyCtrl) || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
3204 temp_input_is_active = true;
3205
3206 if (make_active && !temp_input_is_active)
3207 {
3208 SetActiveID(id, window);
3209 SetFocusID(id, window);
3210 FocusWindow(window);
3211 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
3212 }
3213 }
3214
3215 if (temp_input_is_active)
3216 {
3217 // Only clamp CTRL+Click input when ImGuiSliderFlags_AlwaysClamp is set
3218 const bool is_clamp_input = (flags & ImGuiSliderFlags_AlwaysClamp) != 0;
3219 return TempInputScalar(bb: frame_bb, id, label, data_type, p_data, format, p_clamp_min: is_clamp_input ? p_min : NULL, p_clamp_max: is_clamp_input ? p_max : NULL);
3220 }
3221
3222 // Draw frame
3223 const ImU32 frame_col = GetColorU32(idx: g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3224 RenderNavHighlight(bb: frame_bb, id);
3225 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: frame_col, border: true, rounding: g.Style.FrameRounding);
3226
3227 // Slider behavior
3228 ImRect grab_bb;
3229 const bool value_changed = SliderBehavior(bb: frame_bb, id, data_type, p_v: p_data, p_min, p_max, format, flags, out_grab_bb: &grab_bb);
3230 if (value_changed)
3231 MarkItemEdited(id);
3232
3233 // Render grab
3234 if (grab_bb.Max.x > grab_bb.Min.x)
3235 window->DrawList->AddRectFilled(p_min: grab_bb.Min, p_max: grab_bb.Max, col: GetColorU32(idx: g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), rounding: style.GrabRounding);
3236
3237 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3238 char value_buf[64];
3239 const char* value_buf_end = value_buf + DataTypeFormatString(buf: value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
3240 if (g.LogEnabled)
3241 LogSetNextTextDecoration(prefix: "{", suffix: "}");
3242 RenderTextClipped(pos_min: frame_bb.Min, pos_max: frame_bb.Max, text: value_buf, text_end: value_buf_end, NULL, align: ImVec2(0.5f, 0.5f));
3243
3244 if (label_size.x > 0.0f)
3245 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), text: label);
3246
3247 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
3248 return value_changed;
3249}
3250
3251// Add multiple sliders on 1 line for compact edition of multiple components
3252bool ImGui::SliderScalarN(const char* label, ImGuiDataType data_type, void* v, int components, const void* v_min, const void* v_max, const char* format, ImGuiSliderFlags flags)
3253{
3254 ImGuiWindow* window = GetCurrentWindow();
3255 if (window->SkipItems)
3256 return false;
3257
3258 ImGuiContext& g = *GImGui;
3259 bool value_changed = false;
3260 BeginGroup();
3261 PushID(str_id: label);
3262 PushMultiItemsWidths(components, width_full: CalcItemWidth());
3263 size_t type_size = GDataTypeInfo[data_type].Size;
3264 for (int i = 0; i < components; i++)
3265 {
3266 PushID(int_id: i);
3267 if (i > 0)
3268 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
3269 value_changed |= SliderScalar(label: "", data_type, p_data: v, p_min: v_min, p_max: v_max, format, flags);
3270 PopID();
3271 PopItemWidth();
3272 v = (void*)((char*)v + type_size);
3273 }
3274 PopID();
3275
3276 const char* label_end = FindRenderedTextEnd(text: label);
3277 if (label != label_end)
3278 {
3279 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
3280 TextEx(text: label, text_end: label_end);
3281 }
3282
3283 EndGroup();
3284 return value_changed;
3285}
3286
3287bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3288{
3289 return SliderScalar(label, data_type: ImGuiDataType_Float, p_data: v, p_min: &v_min, p_max: &v_max, format, flags);
3290}
3291
3292bool ImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3293{
3294 return SliderScalarN(label, data_type: ImGuiDataType_Float, v, components: 2, v_min: &v_min, v_max: &v_max, format, flags);
3295}
3296
3297bool ImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3298{
3299 return SliderScalarN(label, data_type: ImGuiDataType_Float, v, components: 3, v_min: &v_min, v_max: &v_max, format, flags);
3300}
3301
3302bool ImGui::SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3303{
3304 return SliderScalarN(label, data_type: ImGuiDataType_Float, v, components: 4, v_min: &v_min, v_max: &v_max, format, flags);
3305}
3306
3307bool ImGui::SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max, const char* format, ImGuiSliderFlags flags)
3308{
3309 if (format == NULL)
3310 format = "%.0f deg";
3311 float v_deg = (*v_rad) * 360.0f / (2 * IM_PI);
3312 bool value_changed = SliderFloat(label, v: &v_deg, v_min: v_degrees_min, v_max: v_degrees_max, format, flags);
3313 *v_rad = v_deg * (2 * IM_PI) / 360.0f;
3314 return value_changed;
3315}
3316
3317bool ImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3318{
3319 return SliderScalar(label, data_type: ImGuiDataType_S32, p_data: v, p_min: &v_min, p_max: &v_max, format, flags);
3320}
3321
3322bool ImGui::SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3323{
3324 return SliderScalarN(label, data_type: ImGuiDataType_S32, v, components: 2, v_min: &v_min, v_max: &v_max, format, flags);
3325}
3326
3327bool ImGui::SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3328{
3329 return SliderScalarN(label, data_type: ImGuiDataType_S32, v, components: 3, v_min: &v_min, v_max: &v_max, format, flags);
3330}
3331
3332bool ImGui::SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3333{
3334 return SliderScalarN(label, data_type: ImGuiDataType_S32, v, components: 4, v_min: &v_min, v_max: &v_max, format, flags);
3335}
3336
3337bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags)
3338{
3339 ImGuiWindow* window = GetCurrentWindow();
3340 if (window->SkipItems)
3341 return false;
3342
3343 ImGuiContext& g = *GImGui;
3344 const ImGuiStyle& style = g.Style;
3345 const ImGuiID id = window->GetID(str: label);
3346
3347 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
3348 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size);
3349 const ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
3350
3351 ItemSize(bb, text_baseline_y: style.FramePadding.y);
3352 if (!ItemAdd(bb: frame_bb, id))
3353 return false;
3354
3355 // Default format string when passing NULL
3356 if (format == NULL)
3357 format = DataTypeGetInfo(data_type)->PrintFmt;
3358
3359 const bool hovered = ItemHoverable(bb: frame_bb, id, item_flags: g.LastItemData.InFlags);
3360 const bool clicked = hovered && IsMouseClicked(button: 0, flags: ImGuiInputFlags_None, owner_id: id);
3361 if (clicked || g.NavActivateId == id)
3362 {
3363 if (clicked)
3364 SetKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id);
3365 SetActiveID(id, window);
3366 SetFocusID(id, window);
3367 FocusWindow(window);
3368 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
3369 }
3370
3371 // Draw frame
3372 const ImU32 frame_col = GetColorU32(idx: g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3373 RenderNavHighlight(bb: frame_bb, id);
3374 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: frame_col, border: true, rounding: g.Style.FrameRounding);
3375
3376 // Slider behavior
3377 ImRect grab_bb;
3378 const bool value_changed = SliderBehavior(bb: frame_bb, id, data_type, p_v: p_data, p_min, p_max, format, flags: flags | ImGuiSliderFlags_Vertical, out_grab_bb: &grab_bb);
3379 if (value_changed)
3380 MarkItemEdited(id);
3381
3382 // Render grab
3383 if (grab_bb.Max.y > grab_bb.Min.y)
3384 window->DrawList->AddRectFilled(p_min: grab_bb.Min, p_max: grab_bb.Max, col: GetColorU32(idx: g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), rounding: style.GrabRounding);
3385
3386 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3387 // For the vertical slider we allow centered text to overlap the frame padding
3388 char value_buf[64];
3389 const char* value_buf_end = value_buf + DataTypeFormatString(buf: value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
3390 RenderTextClipped(pos_min: ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), pos_max: frame_bb.Max, text: value_buf, text_end: value_buf_end, NULL, align: ImVec2(0.5f, 0.0f));
3391 if (label_size.x > 0.0f)
3392 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), text: label);
3393
3394 return value_changed;
3395}
3396
3397bool ImGui::VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3398{
3399 return VSliderScalar(label, size, data_type: ImGuiDataType_Float, p_data: v, p_min: &v_min, p_max: &v_max, format, flags);
3400}
3401
3402bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3403{
3404 return VSliderScalar(label, size, data_type: ImGuiDataType_S32, p_data: v, p_min: &v_min, p_max: &v_max, format, flags);
3405}
3406
3407//-------------------------------------------------------------------------
3408// [SECTION] Widgets: InputScalar, InputFloat, InputInt, etc.
3409//-------------------------------------------------------------------------
3410// - ImParseFormatFindStart() [Internal]
3411// - ImParseFormatFindEnd() [Internal]
3412// - ImParseFormatTrimDecorations() [Internal]
3413// - ImParseFormatSanitizeForPrinting() [Internal]
3414// - ImParseFormatSanitizeForScanning() [Internal]
3415// - ImParseFormatPrecision() [Internal]
3416// - TempInputTextScalar() [Internal]
3417// - InputScalar()
3418// - InputScalarN()
3419// - InputFloat()
3420// - InputFloat2()
3421// - InputFloat3()
3422// - InputFloat4()
3423// - InputInt()
3424// - InputInt2()
3425// - InputInt3()
3426// - InputInt4()
3427// - InputDouble()
3428//-------------------------------------------------------------------------
3429
3430// We don't use strchr() because our strings are usually very short and often start with '%'
3431const char* ImParseFormatFindStart(const char* fmt)
3432{
3433 while (char c = fmt[0])
3434 {
3435 if (c == '%' && fmt[1] != '%')
3436 return fmt;
3437 else if (c == '%')
3438 fmt++;
3439 fmt++;
3440 }
3441 return fmt;
3442}
3443
3444const char* ImParseFormatFindEnd(const char* fmt)
3445{
3446 // Printf/scanf types modifiers: I/L/h/j/l/t/w/z. Other uppercase letters qualify as types aka end of the format.
3447 if (fmt[0] != '%')
3448 return fmt;
3449 const unsigned int ignored_uppercase_mask = (1 << ('I'-'A')) | (1 << ('L'-'A'));
3450 const unsigned int ignored_lowercase_mask = (1 << ('h'-'a')) | (1 << ('j'-'a')) | (1 << ('l'-'a')) | (1 << ('t'-'a')) | (1 << ('w'-'a')) | (1 << ('z'-'a'));
3451 for (char c; (c = *fmt) != 0; fmt++)
3452 {
3453 if (c >= 'A' && c <= 'Z' && ((1 << (c - 'A')) & ignored_uppercase_mask) == 0)
3454 return fmt + 1;
3455 if (c >= 'a' && c <= 'z' && ((1 << (c - 'a')) & ignored_lowercase_mask) == 0)
3456 return fmt + 1;
3457 }
3458 return fmt;
3459}
3460
3461// Extract the format out of a format string with leading or trailing decorations
3462// fmt = "blah blah" -> return ""
3463// fmt = "%.3f" -> return fmt
3464// fmt = "hello %.3f" -> return fmt + 6
3465// fmt = "%.3f hello" -> return buf written with "%.3f"
3466const char* ImParseFormatTrimDecorations(const char* fmt, char* buf, size_t buf_size)
3467{
3468 const char* fmt_start = ImParseFormatFindStart(fmt);
3469 if (fmt_start[0] != '%')
3470 return "";
3471 const char* fmt_end = ImParseFormatFindEnd(fmt: fmt_start);
3472 if (fmt_end[0] == 0) // If we only have leading decoration, we don't need to copy the data.
3473 return fmt_start;
3474 ImStrncpy(dst: buf, src: fmt_start, count: ImMin(lhs: (size_t)(fmt_end - fmt_start) + 1, rhs: buf_size));
3475 return buf;
3476}
3477
3478// Sanitize format
3479// - Zero terminate so extra characters after format (e.g. "%f123") don't confuse atof/atoi
3480// - stb_sprintf.h supports several new modifiers which format numbers in a way that also makes them incompatible atof/atoi.
3481void ImParseFormatSanitizeForPrinting(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3482{
3483 const char* fmt_end = ImParseFormatFindEnd(fmt: fmt_in);
3484 IM_UNUSED(fmt_out_size);
3485 IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3486 while (fmt_in < fmt_end)
3487 {
3488 char c = *fmt_in++;
3489 if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3490 *(fmt_out++) = c;
3491 }
3492 *fmt_out = 0; // Zero-terminate
3493}
3494
3495// - For scanning we need to remove all width and precision fields and flags "%+3.7f" -> "%f". BUT don't strip types like "%I64d" which includes digits. ! "%07I64d" -> "%I64d"
3496const char* ImParseFormatSanitizeForScanning(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3497{
3498 const char* fmt_end = ImParseFormatFindEnd(fmt: fmt_in);
3499 const char* fmt_out_begin = fmt_out;
3500 IM_UNUSED(fmt_out_size);
3501 IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3502 bool has_type = false;
3503 while (fmt_in < fmt_end)
3504 {
3505 char c = *fmt_in++;
3506 if (!has_type && ((c >= '0' && c <= '9') || c == '.' || c == '+' || c == '#'))
3507 continue;
3508 has_type |= ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')); // Stop skipping digits
3509 if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3510 *(fmt_out++) = c;
3511 }
3512 *fmt_out = 0; // Zero-terminate
3513 return fmt_out_begin;
3514}
3515
3516template<typename TYPE>
3517static const char* ImAtoi(const char* src, TYPE* output)
3518{
3519 int negative = 0;
3520 if (*src == '-') { negative = 1; src++; }
3521 if (*src == '+') { src++; }
3522 TYPE v = 0;
3523 while (*src >= '0' && *src <= '9')
3524 v = (v * 10) + (*src++ - '0');
3525 *output = negative ? -v : v;
3526 return src;
3527}
3528
3529// Parse display precision back from the display format string
3530// FIXME: This is still used by some navigation code path to infer a minimum tweak step, but we should aim to rework widgets so it isn't needed.
3531int ImParseFormatPrecision(const char* fmt, int default_precision)
3532{
3533 fmt = ImParseFormatFindStart(fmt);
3534 if (fmt[0] != '%')
3535 return default_precision;
3536 fmt++;
3537 while (*fmt >= '0' && *fmt <= '9')
3538 fmt++;
3539 int precision = INT_MAX;
3540 if (*fmt == '.')
3541 {
3542 fmt = ImAtoi<int>(src: fmt + 1, output: &precision);
3543 if (precision < 0 || precision > 99)
3544 precision = default_precision;
3545 }
3546 if (*fmt == 'e' || *fmt == 'E') // Maximum precision with scientific notation
3547 precision = -1;
3548 if ((*fmt == 'g' || *fmt == 'G') && precision == INT_MAX)
3549 precision = -1;
3550 return (precision == INT_MAX) ? default_precision : precision;
3551}
3552
3553// Create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets)
3554// FIXME: Facilitate using this in variety of other situations.
3555bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags)
3556{
3557 // On the first frame, g.TempInputTextId == 0, then on subsequent frames it becomes == id.
3558 // We clear ActiveID on the first frame to allow the InputText() taking it back.
3559 ImGuiContext& g = *GImGui;
3560 const bool init = (g.TempInputId != id);
3561 if (init)
3562 ClearActiveID();
3563
3564 g.CurrentWindow->DC.CursorPos = bb.Min;
3565 bool value_changed = InputTextEx(label, NULL, buf, buf_size, size_arg: bb.GetSize(), flags: flags | ImGuiInputTextFlags_MergedItem);
3566 if (init)
3567 {
3568 // First frame we started displaying the InputText widget, we expect it to take the active id.
3569 IM_ASSERT(g.ActiveId == id);
3570 g.TempInputId = g.ActiveId;
3571 }
3572 return value_changed;
3573}
3574
3575// Note that Drag/Slider functions are only forwarding the min/max values clamping values if the ImGuiSliderFlags_AlwaysClamp flag is set!
3576// This is intended: this way we allow CTRL+Click manual input to set a value out of bounds, for maximum flexibility.
3577// However this may not be ideal for all uses, as some user code may break on out of bound values.
3578bool ImGui::TempInputScalar(const ImRect& bb, ImGuiID id, const char* label, ImGuiDataType data_type, void* p_data, const char* format, const void* p_clamp_min, const void* p_clamp_max)
3579{
3580 // FIXME: May need to clarify display behavior if format doesn't contain %.
3581 // "%d" -> "%d" / "There are %d items" -> "%d" / "items" -> "%d" (fallback). Also see #6405
3582 const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
3583 char fmt_buf[32];
3584 char data_buf[32];
3585 format = ImParseFormatTrimDecorations(fmt: format, buf: fmt_buf, IM_ARRAYSIZE(fmt_buf));
3586 if (format[0] == 0)
3587 format = type_info->PrintFmt;
3588 DataTypeFormatString(buf: data_buf, IM_ARRAYSIZE(data_buf), data_type, p_data, format);
3589 ImStrTrimBlanks(str: data_buf);
3590
3591 ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_NoMarkEdited | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
3592
3593 bool value_changed = false;
3594 if (TempInputText(bb, id, label, buf: data_buf, IM_ARRAYSIZE(data_buf), flags))
3595 {
3596 // Backup old value
3597 size_t data_type_size = type_info->Size;
3598 ImGuiDataTypeStorage data_backup;
3599 memcpy(dest: &data_backup, src: p_data, n: data_type_size);
3600
3601 // Apply new value (or operations) then clamp
3602 DataTypeApplyFromText(buf: data_buf, data_type, p_data, format, NULL);
3603 if (p_clamp_min || p_clamp_max)
3604 {
3605 if (p_clamp_min && p_clamp_max && DataTypeCompare(data_type, arg_1: p_clamp_min, arg_2: p_clamp_max) > 0)
3606 ImSwap(a&: p_clamp_min, b&: p_clamp_max);
3607 DataTypeClamp(data_type, p_data, p_min: p_clamp_min, p_max: p_clamp_max);
3608 }
3609
3610 // Only mark as edited if new value is different
3611 value_changed = memcmp(s1: &data_backup, s2: p_data, n: data_type_size) != 0;
3612 if (value_changed)
3613 MarkItemEdited(id);
3614 }
3615 return value_changed;
3616}
3617
3618void ImGui::SetNextItemRefVal(ImGuiDataType data_type, void* p_data)
3619{
3620 ImGuiContext& g = *GImGui;
3621 g.NextItemData.Flags |= ImGuiNextItemDataFlags_HasRefVal;
3622 memcpy(dest: &g.NextItemData.RefVal, src: p_data, n: DataTypeGetInfo(data_type)->Size);
3623}
3624
3625// Note: p_data, p_step, p_step_fast are _pointers_ to a memory address holding the data. For an Input widget, p_step and p_step_fast are optional.
3626// Read code of e.g. InputFloat(), InputInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3627bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_step, const void* p_step_fast, const char* format, ImGuiInputTextFlags flags)
3628{
3629 ImGuiWindow* window = GetCurrentWindow();
3630 if (window->SkipItems)
3631 return false;
3632
3633 ImGuiContext& g = *GImGui;
3634 ImGuiStyle& style = g.Style;
3635
3636 if (format == NULL)
3637 format = DataTypeGetInfo(data_type)->PrintFmt;
3638
3639 void* p_data_default = (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasRefVal) ? &g.NextItemData.RefVal : &g.DataTypeZeroValue;
3640
3641 char buf[64];
3642 if ((flags & ImGuiInputTextFlags_DisplayEmptyRefVal) && DataTypeCompare(data_type, arg_1: p_data, arg_2: p_data_default) == 0)
3643 buf[0] = 0;
3644 else
3645 DataTypeFormatString(buf, IM_ARRAYSIZE(buf), data_type, p_data, format);
3646
3647 flags |= ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_NoMarkEdited; // We call MarkItemEdited() ourselves by comparing the actual data rather than the string.
3648 flags |= (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
3649
3650 bool value_changed = false;
3651 if (p_step == NULL)
3652 {
3653 if (InputText(label, buf, IM_ARRAYSIZE(buf), flags))
3654 value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, p_data_when_empty: (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL);
3655 }
3656 else
3657 {
3658 const float button_size = GetFrameHeight();
3659
3660 BeginGroup(); // The only purpose of the group here is to allow the caller to query item data e.g. IsItemActive()
3661 PushID(str_id: label);
3662 SetNextItemWidth(ImMax(lhs: 1.0f, rhs: CalcItemWidth() - (button_size + style.ItemInnerSpacing.x) * 2));
3663 if (InputText(label: "", buf, IM_ARRAYSIZE(buf), flags)) // PushId(label) + "" gives us the expected ID from outside point of view
3664 value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, p_data_when_empty: (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL);
3665 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
3666
3667 // Step buttons
3668 const ImVec2 backup_frame_padding = style.FramePadding;
3669 style.FramePadding.x = style.FramePadding.y;
3670 ImGuiButtonFlags button_flags = ImGuiButtonFlags_Repeat | ImGuiButtonFlags_DontClosePopups;
3671 if (flags & ImGuiInputTextFlags_ReadOnly)
3672 BeginDisabled();
3673 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
3674 if (ButtonEx(label: "-", size_arg: ImVec2(button_size, button_size), flags: button_flags))
3675 {
3676 DataTypeApplyOp(data_type, op: '-', output: p_data, arg1: p_data, arg2: g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3677 value_changed = true;
3678 }
3679 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
3680 if (ButtonEx(label: "+", size_arg: ImVec2(button_size, button_size), flags: button_flags))
3681 {
3682 DataTypeApplyOp(data_type, op: '+', output: p_data, arg1: p_data, arg2: g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3683 value_changed = true;
3684 }
3685 if (flags & ImGuiInputTextFlags_ReadOnly)
3686 EndDisabled();
3687
3688 const char* label_end = FindRenderedTextEnd(text: label);
3689 if (label != label_end)
3690 {
3691 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
3692 TextEx(text: label, text_end: label_end);
3693 }
3694 style.FramePadding = backup_frame_padding;
3695
3696 PopID();
3697 EndGroup();
3698 }
3699 if (value_changed)
3700 MarkItemEdited(id: g.LastItemData.ID);
3701
3702 return value_changed;
3703}
3704
3705bool ImGui::InputScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_step, const void* p_step_fast, const char* format, ImGuiInputTextFlags flags)
3706{
3707 ImGuiWindow* window = GetCurrentWindow();
3708 if (window->SkipItems)
3709 return false;
3710
3711 ImGuiContext& g = *GImGui;
3712 bool value_changed = false;
3713 BeginGroup();
3714 PushID(str_id: label);
3715 PushMultiItemsWidths(components, width_full: CalcItemWidth());
3716 size_t type_size = GDataTypeInfo[data_type].Size;
3717 for (int i = 0; i < components; i++)
3718 {
3719 PushID(int_id: i);
3720 if (i > 0)
3721 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
3722 value_changed |= InputScalar(label: "", data_type, p_data, p_step, p_step_fast, format, flags);
3723 PopID();
3724 PopItemWidth();
3725 p_data = (void*)((char*)p_data + type_size);
3726 }
3727 PopID();
3728
3729 const char* label_end = FindRenderedTextEnd(text: label);
3730 if (label != label_end)
3731 {
3732 SameLine(offset_from_start_x: 0.0f, spacing: g.Style.ItemInnerSpacing.x);
3733 TextEx(text: label, text_end: label_end);
3734 }
3735
3736 EndGroup();
3737 return value_changed;
3738}
3739
3740bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, const char* format, ImGuiInputTextFlags flags)
3741{
3742 return InputScalar(label, data_type: ImGuiDataType_Float, p_data: (void*)v, p_step: (void*)(step > 0.0f ? &step : NULL), p_step_fast: (void*)(step_fast > 0.0f ? &step_fast : NULL), format, flags);
3743}
3744
3745bool ImGui::InputFloat2(const char* label, float v[2], const char* format, ImGuiInputTextFlags flags)
3746{
3747 return InputScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 2, NULL, NULL, format, flags);
3748}
3749
3750bool ImGui::InputFloat3(const char* label, float v[3], const char* format, ImGuiInputTextFlags flags)
3751{
3752 return InputScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 3, NULL, NULL, format, flags);
3753}
3754
3755bool ImGui::InputFloat4(const char* label, float v[4], const char* format, ImGuiInputTextFlags flags)
3756{
3757 return InputScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 4, NULL, NULL, format, flags);
3758}
3759
3760bool ImGui::InputInt(const char* label, int* v, int step, int step_fast, ImGuiInputTextFlags flags)
3761{
3762 // Hexadecimal input provided as a convenience but the flag name is awkward. Typically you'd use InputText() to parse your own data, if you want to handle prefixes.
3763 const char* format = (flags & ImGuiInputTextFlags_CharsHexadecimal) ? "%08X" : "%d";
3764 return InputScalar(label, data_type: ImGuiDataType_S32, p_data: (void*)v, p_step: (void*)(step > 0 ? &step : NULL), p_step_fast: (void*)(step_fast > 0 ? &step_fast : NULL), format, flags);
3765}
3766
3767bool ImGui::InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags)
3768{
3769 return InputScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 2, NULL, NULL, format: "%d", flags);
3770}
3771
3772bool ImGui::InputInt3(const char* label, int v[3], ImGuiInputTextFlags flags)
3773{
3774 return InputScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 3, NULL, NULL, format: "%d", flags);
3775}
3776
3777bool ImGui::InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags)
3778{
3779 return InputScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 4, NULL, NULL, format: "%d", flags);
3780}
3781
3782bool ImGui::InputDouble(const char* label, double* v, double step, double step_fast, const char* format, ImGuiInputTextFlags flags)
3783{
3784 return InputScalar(label, data_type: ImGuiDataType_Double, p_data: (void*)v, p_step: (void*)(step > 0.0 ? &step : NULL), p_step_fast: (void*)(step_fast > 0.0 ? &step_fast : NULL), format, flags);
3785}
3786
3787//-------------------------------------------------------------------------
3788// [SECTION] Widgets: InputText, InputTextMultiline, InputTextWithHint
3789//-------------------------------------------------------------------------
3790// - InputText()
3791// - InputTextWithHint()
3792// - InputTextMultiline()
3793// - InputTextGetCharInfo() [Internal]
3794// - InputTextReindexLines() [Internal]
3795// - InputTextReindexLinesRange() [Internal]
3796// - InputTextEx() [Internal]
3797// - DebugNodeInputTextState() [Internal]
3798//-------------------------------------------------------------------------
3799
3800bool ImGui::InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3801{
3802 IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline()
3803 return InputTextEx(label, NULL, buf, buf_size: (int)buf_size, size_arg: ImVec2(0, 0), flags, callback, user_data);
3804}
3805
3806bool ImGui::InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3807{
3808 return InputTextEx(label, NULL, buf, buf_size: (int)buf_size, size_arg: size, flags: flags | ImGuiInputTextFlags_Multiline, callback, user_data);
3809}
3810
3811bool ImGui::InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3812{
3813 IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() or InputTextEx() manually if you need multi-line + hint.
3814 return InputTextEx(label, hint, buf, buf_size: (int)buf_size, size_arg: ImVec2(0, 0), flags, callback, user_data);
3815}
3816
3817static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end)
3818{
3819 int line_count = 0;
3820 const char* s = text_begin;
3821 while (char c = *s++) // We are only matching for \n so we can ignore UTF-8 decoding
3822 if (c == '\n')
3823 line_count++;
3824 s--;
3825 if (s[0] != '\n' && s[0] != '\r')
3826 line_count++;
3827 *out_text_end = s;
3828 return line_count;
3829}
3830
3831static ImVec2 InputTextCalcTextSizeW(ImGuiContext* ctx, const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining, ImVec2* out_offset, bool stop_on_new_line)
3832{
3833 ImGuiContext& g = *ctx;
3834 ImFont* font = g.Font;
3835 const float line_height = g.FontSize;
3836 const float scale = line_height / font->FontSize;
3837
3838 ImVec2 text_size = ImVec2(0, 0);
3839 float line_width = 0.0f;
3840
3841 const ImWchar* s = text_begin;
3842 while (s < text_end)
3843 {
3844 unsigned int c = (unsigned int)(*s++);
3845 if (c == '\n')
3846 {
3847 text_size.x = ImMax(lhs: text_size.x, rhs: line_width);
3848 text_size.y += line_height;
3849 line_width = 0.0f;
3850 if (stop_on_new_line)
3851 break;
3852 continue;
3853 }
3854 if (c == '\r')
3855 continue;
3856
3857 const float char_width = font->GetCharAdvance(c: (ImWchar)c) * scale;
3858 line_width += char_width;
3859 }
3860
3861 if (text_size.x < line_width)
3862 text_size.x = line_width;
3863
3864 if (out_offset)
3865 *out_offset = ImVec2(line_width, text_size.y + line_height); // offset allow for the possibility of sitting after a trailing \n
3866
3867 if (line_width > 0 || text_size.y == 0.0f) // whereas size.y will ignore the trailing \n
3868 text_size.y += line_height;
3869
3870 if (remaining)
3871 *remaining = s;
3872
3873 return text_size;
3874}
3875
3876// Wrapper for stb_textedit.h to edit text (our wrapper is for: statically sized buffer, single-line, wchar characters. InputText converts between UTF-8 and wchar)
3877namespace ImStb
3878{
3879
3880static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->CurLenW; }
3881static ImWchar STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { IM_ASSERT(idx <= obj->CurLenW); return obj->TextW[idx]; }
3882static float STB_TEXTEDIT_GETWIDTH(ImGuiInputTextState* obj, int line_start_idx, int char_idx) { ImWchar c = obj->TextW[line_start_idx + char_idx]; if (c == '\n') return IMSTB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *obj->Ctx; return g.Font->GetCharAdvance(c) * g.FontScale; }
3883static int STB_TEXTEDIT_KEYTOTEXT(int key) { return key >= 0x200000 ? 0 : key; }
3884static ImWchar STB_TEXTEDIT_NEWLINE = '\n';
3885static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx)
3886{
3887 const ImWchar* text = obj->TextW.Data;
3888 const ImWchar* text_remaining = NULL;
3889 const ImVec2 size = InputTextCalcTextSizeW(ctx: obj->Ctx, text_begin: text + line_start_idx, text_end: text + obj->CurLenW, remaining: &text_remaining, NULL, stop_on_new_line: true);
3890 r->x0 = 0.0f;
3891 r->x1 = size.x;
3892 r->baseline_y_delta = size.y;
3893 r->ymin = 0.0f;
3894 r->ymax = size.y;
3895 r->num_chars = (int)(text_remaining - (text + line_start_idx));
3896}
3897
3898static bool is_separator(unsigned int c)
3899{
3900 return c==',' || c==';' || c=='(' || c==')' || c=='{' || c=='}' || c=='[' || c==']' || c=='|' || c=='\n' || c=='\r' || c=='.' || c=='!' || c=='\\' || c=='/';
3901}
3902
3903static int is_word_boundary_from_right(ImGuiInputTextState* obj, int idx)
3904{
3905 // When ImGuiInputTextFlags_Password is set, we don't want actions such as CTRL+Arrow to leak the fact that underlying data are blanks or separators.
3906 if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
3907 return 0;
3908
3909 bool prev_white = ImCharIsBlankW(c: obj->TextW[idx - 1]);
3910 bool prev_separ = is_separator(c: obj->TextW[idx - 1]);
3911 bool curr_white = ImCharIsBlankW(c: obj->TextW[idx]);
3912 bool curr_separ = is_separator(c: obj->TextW[idx]);
3913 return ((prev_white || prev_separ) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ);
3914}
3915static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx)
3916{
3917 if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
3918 return 0;
3919
3920 bool prev_white = ImCharIsBlankW(c: obj->TextW[idx]);
3921 bool prev_separ = is_separator(c: obj->TextW[idx]);
3922 bool curr_white = ImCharIsBlankW(c: obj->TextW[idx - 1]);
3923 bool curr_separ = is_separator(c: obj->TextW[idx - 1]);
3924 return ((prev_white) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ);
3925}
3926static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(ImGuiInputTextState* obj, int idx) { idx--; while (idx >= 0 && !is_word_boundary_from_right(obj, idx)) idx--; return idx < 0 ? 0 : idx; }
3927static int STB_TEXTEDIT_MOVEWORDRIGHT_MAC(ImGuiInputTextState* obj, int idx) { idx++; int len = obj->CurLenW; while (idx < len && !is_word_boundary_from_left(obj, idx)) idx++; return idx > len ? len : idx; }
3928static int STB_TEXTEDIT_MOVEWORDRIGHT_WIN(ImGuiInputTextState* obj, int idx) { idx++; int len = obj->CurLenW; while (idx < len && !is_word_boundary_from_right(obj, idx)) idx++; return idx > len ? len : idx; }
3929static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(ImGuiInputTextState* obj, int idx) { ImGuiContext& g = *obj->Ctx; if (g.IO.ConfigMacOSXBehaviors) return STB_TEXTEDIT_MOVEWORDRIGHT_MAC(obj, idx); else return STB_TEXTEDIT_MOVEWORDRIGHT_WIN(obj, idx); }
3930#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h
3931#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL
3932
3933static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n)
3934{
3935 ImWchar* dst = obj->TextW.Data + pos;
3936
3937 // We maintain our buffer length in both UTF-8 and wchar formats
3938 obj->Edited = true;
3939 obj->CurLenA -= ImTextCountUtf8BytesFromStr(in_text: dst, in_text_end: dst + n);
3940 obj->CurLenW -= n;
3941
3942 // Offset remaining text (FIXME-OPT: Use memmove)
3943 const ImWchar* src = obj->TextW.Data + pos + n;
3944 while (ImWchar c = *src++)
3945 *dst++ = c;
3946 *dst = '\0';
3947}
3948
3949static bool STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const ImWchar* new_text, int new_text_len)
3950{
3951 const bool is_resizable = (obj->Flags & ImGuiInputTextFlags_CallbackResize) != 0;
3952 const int text_len = obj->CurLenW;
3953 IM_ASSERT(pos <= text_len);
3954
3955 const int new_text_len_utf8 = ImTextCountUtf8BytesFromStr(in_text: new_text, in_text_end: new_text + new_text_len);
3956 if (!is_resizable && (new_text_len_utf8 + obj->CurLenA + 1 > obj->BufCapacityA))
3957 return false;
3958
3959 // Grow internal buffer if needed
3960 if (new_text_len + text_len + 1 > obj->TextW.Size)
3961 {
3962 if (!is_resizable)
3963 return false;
3964 IM_ASSERT(text_len < obj->TextW.Size);
3965 obj->TextW.resize(new_size: text_len + ImClamp(v: new_text_len * 4, mn: 32, mx: ImMax(lhs: 256, rhs: new_text_len)) + 1);
3966 }
3967
3968 ImWchar* text = obj->TextW.Data;
3969 if (pos != text_len)
3970 memmove(dest: text + pos + new_text_len, src: text + pos, n: (size_t)(text_len - pos) * sizeof(ImWchar));
3971 memcpy(dest: text + pos, src: new_text, n: (size_t)new_text_len * sizeof(ImWchar));
3972
3973 obj->Edited = true;
3974 obj->CurLenW += new_text_len;
3975 obj->CurLenA += new_text_len_utf8;
3976 obj->TextW[obj->CurLenW] = '\0';
3977
3978 return true;
3979}
3980
3981// We don't use an enum so we can build even with conflicting symbols (if another user of stb_textedit.h leak their STB_TEXTEDIT_K_* symbols)
3982#define STB_TEXTEDIT_K_LEFT 0x200000 // keyboard input to move cursor left
3983#define STB_TEXTEDIT_K_RIGHT 0x200001 // keyboard input to move cursor right
3984#define STB_TEXTEDIT_K_UP 0x200002 // keyboard input to move cursor up
3985#define STB_TEXTEDIT_K_DOWN 0x200003 // keyboard input to move cursor down
3986#define STB_TEXTEDIT_K_LINESTART 0x200004 // keyboard input to move cursor to start of line
3987#define STB_TEXTEDIT_K_LINEEND 0x200005 // keyboard input to move cursor to end of line
3988#define STB_TEXTEDIT_K_TEXTSTART 0x200006 // keyboard input to move cursor to start of text
3989#define STB_TEXTEDIT_K_TEXTEND 0x200007 // keyboard input to move cursor to end of text
3990#define STB_TEXTEDIT_K_DELETE 0x200008 // keyboard input to delete selection or character under cursor
3991#define STB_TEXTEDIT_K_BACKSPACE 0x200009 // keyboard input to delete selection or character left of cursor
3992#define STB_TEXTEDIT_K_UNDO 0x20000A // keyboard input to perform undo
3993#define STB_TEXTEDIT_K_REDO 0x20000B // keyboard input to perform redo
3994#define STB_TEXTEDIT_K_WORDLEFT 0x20000C // keyboard input to move cursor left one word
3995#define STB_TEXTEDIT_K_WORDRIGHT 0x20000D // keyboard input to move cursor right one word
3996#define STB_TEXTEDIT_K_PGUP 0x20000E // keyboard input to move cursor up a page
3997#define STB_TEXTEDIT_K_PGDOWN 0x20000F // keyboard input to move cursor down a page
3998#define STB_TEXTEDIT_K_SHIFT 0x400000
3999
4000#define IMSTB_TEXTEDIT_IMPLEMENTATION
4001#define IMSTB_TEXTEDIT_memmove memmove
4002#include "imstb_textedit.h"
4003
4004// stb_textedit internally allows for a single undo record to do addition and deletion, but somehow, calling
4005// the stb_textedit_paste() function creates two separate records, so we perform it manually. (FIXME: Report to nothings/stb?)
4006static void stb_textedit_replace(ImGuiInputTextState* str, STB_TexteditState* state, const IMSTB_TEXTEDIT_CHARTYPE* text, int text_len)
4007{
4008 stb_text_makeundo_replace(str, state, where: 0, old_length: str->CurLenW, new_length: text_len);
4009 ImStb::STB_TEXTEDIT_DELETECHARS(obj: str, pos: 0, n: str->CurLenW);
4010 state->cursor = state->select_start = state->select_end = 0;
4011 if (text_len <= 0)
4012 return;
4013 if (ImStb::STB_TEXTEDIT_INSERTCHARS(obj: str, pos: 0, new_text: text, new_text_len: text_len))
4014 {
4015 state->cursor = state->select_start = state->select_end = text_len;
4016 state->has_preferred_x = 0;
4017 return;
4018 }
4019 IM_ASSERT(0); // Failed to insert character, normally shouldn't happen because of how we currently use stb_textedit_replace()
4020}
4021
4022} // namespace ImStb
4023
4024void ImGuiInputTextState::OnKeyPressed(int key)
4025{
4026 stb_textedit_key(str: this, state: &Stb, key);
4027 CursorFollow = true;
4028 CursorAnimReset();
4029}
4030
4031ImGuiInputTextCallbackData::ImGuiInputTextCallbackData()
4032{
4033 memset(s: this, c: 0, n: sizeof(*this));
4034}
4035
4036// Public API to manipulate UTF-8 text
4037// We expose UTF-8 to the user (unlike the STB_TEXTEDIT_* functions which are manipulating wchar)
4038// FIXME: The existence of this rarely exercised code path is a bit of a nuisance.
4039void ImGuiInputTextCallbackData::DeleteChars(int pos, int bytes_count)
4040{
4041 IM_ASSERT(pos + bytes_count <= BufTextLen);
4042 char* dst = Buf + pos;
4043 const char* src = Buf + pos + bytes_count;
4044 while (char c = *src++)
4045 *dst++ = c;
4046 *dst = '\0';
4047
4048 if (CursorPos >= pos + bytes_count)
4049 CursorPos -= bytes_count;
4050 else if (CursorPos >= pos)
4051 CursorPos = pos;
4052 SelectionStart = SelectionEnd = CursorPos;
4053 BufDirty = true;
4054 BufTextLen -= bytes_count;
4055}
4056
4057void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, const char* new_text_end)
4058{
4059 // Accept null ranges
4060 if (new_text == new_text_end)
4061 return;
4062
4063 const bool is_resizable = (Flags & ImGuiInputTextFlags_CallbackResize) != 0;
4064 const int new_text_len = new_text_end ? (int)(new_text_end - new_text) : (int)strlen(s: new_text);
4065 if (new_text_len + BufTextLen >= BufSize)
4066 {
4067 if (!is_resizable)
4068 return;
4069
4070 // Contrary to STB_TEXTEDIT_INSERTCHARS() this is working in the UTF8 buffer, hence the mildly similar code (until we remove the U16 buffer altogether!)
4071 ImGuiContext& g = *Ctx;
4072 ImGuiInputTextState* edit_state = &g.InputTextState;
4073 IM_ASSERT(edit_state->ID != 0 && g.ActiveId == edit_state->ID);
4074 IM_ASSERT(Buf == edit_state->TextA.Data);
4075 int new_buf_size = BufTextLen + ImClamp(v: new_text_len * 4, mn: 32, mx: ImMax(lhs: 256, rhs: new_text_len)) + 1;
4076 edit_state->TextA.reserve(new_capacity: new_buf_size + 1);
4077 Buf = edit_state->TextA.Data;
4078 BufSize = edit_state->BufCapacityA = new_buf_size;
4079 }
4080
4081 if (BufTextLen != pos)
4082 memmove(dest: Buf + pos + new_text_len, src: Buf + pos, n: (size_t)(BufTextLen - pos));
4083 memcpy(dest: Buf + pos, src: new_text, n: (size_t)new_text_len * sizeof(char));
4084 Buf[BufTextLen + new_text_len] = '\0';
4085
4086 if (CursorPos >= pos)
4087 CursorPos += new_text_len;
4088 SelectionStart = SelectionEnd = CursorPos;
4089 BufDirty = true;
4090 BufTextLen += new_text_len;
4091}
4092
4093// Return false to discard a character.
4094static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard)
4095{
4096 unsigned int c = *p_char;
4097
4098 // Filter non-printable (NB: isprint is unreliable! see #2467)
4099 bool apply_named_filters = true;
4100 if (c < 0x20)
4101 {
4102 bool pass = false;
4103 pass |= (c == '\n') && (flags & ImGuiInputTextFlags_Multiline) != 0; // Note that an Enter KEY will emit \r and be ignored (we poll for KEY in InputText() code)
4104 pass |= (c == '\t') && (flags & ImGuiInputTextFlags_AllowTabInput) != 0;
4105 if (!pass)
4106 return false;
4107 apply_named_filters = false; // Override named filters below so newline and tabs can still be inserted.
4108 }
4109
4110 if (input_source_is_clipboard == false)
4111 {
4112 // We ignore Ascii representation of delete (emitted from Backspace on OSX, see #2578, #2817)
4113 if (c == 127)
4114 return false;
4115
4116 // Filter private Unicode range. GLFW on OSX seems to send private characters for special keys like arrow keys (FIXME)
4117 if (c >= 0xE000 && c <= 0xF8FF)
4118 return false;
4119 }
4120
4121 // Filter Unicode ranges we are not handling in this build
4122 if (c > IM_UNICODE_CODEPOINT_MAX)
4123 return false;
4124
4125 // Generic named filters
4126 if (apply_named_filters && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint)))
4127 {
4128 // The libc allows overriding locale, with e.g. 'setlocale(LC_NUMERIC, "de_DE.UTF-8");' which affect the output/input of printf/scanf to use e.g. ',' instead of '.'.
4129 // The standard mandate that programs starts in the "C" locale where the decimal point is '.'.
4130 // We don't really intend to provide widespread support for it, but out of empathy for people stuck with using odd API, we support the bare minimum aka overriding the decimal point.
4131 // Change the default decimal_point with:
4132 // ImGui::GetIO()->PlatformLocaleDecimalPoint = *localeconv()->decimal_point;
4133 // Users of non-default decimal point (in particular ',') may be affected by word-selection logic (is_word_boundary_from_right/is_word_boundary_from_left) functions.
4134 ImGuiContext& g = *ctx;
4135 const unsigned c_decimal_point = (unsigned int)g.IO.PlatformLocaleDecimalPoint;
4136 if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint))
4137 if (c == '.' || c == ',')
4138 c = c_decimal_point;
4139
4140 // Full-width -> half-width conversion for numeric fields (https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
4141 // While this is mostly convenient, this has the side-effect for uninformed users accidentally inputting full-width characters that they may
4142 // scratch their head as to why it works in numerical fields vs in generic text fields it would require support in the font.
4143 if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | ImGuiInputTextFlags_CharsHexadecimal))
4144 if (c >= 0xFF01 && c <= 0xFF5E)
4145 c = c - 0xFF01 + 0x21;
4146
4147 // Allow 0-9 . - + * /
4148 if (flags & ImGuiInputTextFlags_CharsDecimal)
4149 if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/'))
4150 return false;
4151
4152 // Allow 0-9 . - + * / e E
4153 if (flags & ImGuiInputTextFlags_CharsScientific)
4154 if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/') && (c != 'e') && (c != 'E'))
4155 return false;
4156
4157 // Allow 0-9 a-F A-F
4158 if (flags & ImGuiInputTextFlags_CharsHexadecimal)
4159 if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
4160 return false;
4161
4162 // Turn a-z into A-Z
4163 if (flags & ImGuiInputTextFlags_CharsUppercase)
4164 if (c >= 'a' && c <= 'z')
4165 c += (unsigned int)('A' - 'a');
4166
4167 if (flags & ImGuiInputTextFlags_CharsNoBlank)
4168 if (ImCharIsBlankW(c))
4169 return false;
4170
4171 *p_char = c;
4172 }
4173
4174 // Custom callback filter
4175 if (flags & ImGuiInputTextFlags_CallbackCharFilter)
4176 {
4177 ImGuiContext& g = *GImGui;
4178 ImGuiInputTextCallbackData callback_data;
4179 callback_data.Ctx = &g;
4180 callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter;
4181 callback_data.EventChar = (ImWchar)c;
4182 callback_data.Flags = flags;
4183 callback_data.UserData = user_data;
4184 if (callback(&callback_data) != 0)
4185 return false;
4186 *p_char = callback_data.EventChar;
4187 if (!callback_data.EventChar)
4188 return false;
4189 }
4190
4191 return true;
4192}
4193
4194// Find the shortest single replacement we can make to get the new text from the old text.
4195// Important: needs to be run before TextW is rewritten with the new characters because calling STB_TEXTEDIT_GETCHAR() at the end.
4196// FIXME: Ideally we should transition toward (1) making InsertChars()/DeleteChars() update undo-stack (2) discourage (and keep reconcile) or obsolete (and remove reconcile) accessing buffer directly.
4197static void InputTextReconcileUndoStateAfterUserCallback(ImGuiInputTextState* state, const char* new_buf_a, int new_length_a)
4198{
4199 ImGuiContext& g = *GImGui;
4200 const ImWchar* old_buf = state->TextW.Data;
4201 const int old_length = state->CurLenW;
4202 const int new_length = ImTextCountCharsFromUtf8(in_text: new_buf_a, in_text_end: new_buf_a + new_length_a);
4203 g.TempBuffer.reserve_discard(new_capacity: (new_length + 1) * sizeof(ImWchar));
4204 ImWchar* new_buf = (ImWchar*)(void*)g.TempBuffer.Data;
4205 ImTextStrFromUtf8(out_buf: new_buf, out_buf_size: new_length + 1, in_text: new_buf_a, in_text_end: new_buf_a + new_length_a);
4206
4207 const int shorter_length = ImMin(lhs: old_length, rhs: new_length);
4208 int first_diff;
4209 for (first_diff = 0; first_diff < shorter_length; first_diff++)
4210 if (old_buf[first_diff] != new_buf[first_diff])
4211 break;
4212 if (first_diff == old_length && first_diff == new_length)
4213 return;
4214
4215 int old_last_diff = old_length - 1;
4216 int new_last_diff = new_length - 1;
4217 for (; old_last_diff >= first_diff && new_last_diff >= first_diff; old_last_diff--, new_last_diff--)
4218 if (old_buf[old_last_diff] != new_buf[new_last_diff])
4219 break;
4220
4221 const int insert_len = new_last_diff - first_diff + 1;
4222 const int delete_len = old_last_diff - first_diff + 1;
4223 if (insert_len > 0 || delete_len > 0)
4224 if (IMSTB_TEXTEDIT_CHARTYPE* p = stb_text_createundo(state: &state->Stb.undostate, pos: first_diff, insert_len: delete_len, delete_len: insert_len))
4225 for (int i = 0; i < delete_len; i++)
4226 p[i] = ImStb::STB_TEXTEDIT_GETCHAR(obj: state, idx: first_diff + i);
4227}
4228
4229// As InputText() retain textual data and we currently provide a path for user to not retain it (via local variables)
4230// we need some form of hook to reapply data back to user buffer on deactivation frame. (#4714)
4231// It would be more desirable that we discourage users from taking advantage of the "user not retaining data" trick,
4232// but that more likely be attractive when we do have _NoLiveEdit flag available.
4233void ImGui::InputTextDeactivateHook(ImGuiID id)
4234{
4235 ImGuiContext& g = *GImGui;
4236 ImGuiInputTextState* state = &g.InputTextState;
4237 if (id == 0 || state->ID != id)
4238 return;
4239 g.InputTextDeactivatedState.ID = state->ID;
4240 if (state->Flags & ImGuiInputTextFlags_ReadOnly)
4241 {
4242 g.InputTextDeactivatedState.TextA.resize(new_size: 0); // In theory this data won't be used, but clear to be neat.
4243 }
4244 else
4245 {
4246 IM_ASSERT(state->TextA.Data != 0);
4247 g.InputTextDeactivatedState.TextA.resize(new_size: state->CurLenA + 1);
4248 memcpy(dest: g.InputTextDeactivatedState.TextA.Data, src: state->TextA.Data, n: state->CurLenA + 1);
4249 }
4250}
4251
4252// Edit a string of text
4253// - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!".
4254// This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match
4255// Note that in std::string world, capacity() would omit 1 byte used by the zero-terminator.
4256// - When active, hold on a privately held copy of the text (and apply back to 'buf'). So changing 'buf' while the InputText is active has no effect.
4257// - If you want to use ImGui::InputText() with std::string, see misc/cpp/imgui_stdlib.h
4258// (FIXME: Rather confusing and messy function, among the worse part of our codebase, expecting to rewrite a V2 at some point.. Partly because we are
4259// doing UTF8 > U16 > UTF8 conversions on the go to easily interface with stb_textedit. Ideally should stay in UTF-8 all the time. See https://github.com/nothings/stb/issues/188)
4260bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_size, const ImVec2& size_arg, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* callback_user_data)
4261{
4262 ImGuiWindow* window = GetCurrentWindow();
4263 if (window->SkipItems)
4264 return false;
4265
4266 IM_ASSERT(buf != NULL && buf_size >= 0);
4267 IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys)
4268 IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key)
4269
4270 ImGuiContext& g = *GImGui;
4271 ImGuiIO& io = g.IO;
4272 const ImGuiStyle& style = g.Style;
4273
4274 const bool RENDER_SELECTION_WHEN_INACTIVE = false;
4275 const bool is_multiline = (flags & ImGuiInputTextFlags_Multiline) != 0;
4276
4277 if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope (including the scrollbar)
4278 BeginGroup();
4279 const ImGuiID id = window->GetID(str: label);
4280 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
4281 const ImVec2 frame_size = CalcItemSize(size: size_arg, default_w: CalcItemWidth(), default_h: (is_multiline ? g.FontSize * 8.0f : label_size.y) + style.FramePadding.y * 2.0f); // Arbitrary default of 8 lines high for multi-line
4282 const ImVec2 total_size = ImVec2(frame_size.x + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), frame_size.y);
4283
4284 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
4285 const ImRect total_bb(frame_bb.Min, frame_bb.Min + total_size);
4286
4287 ImGuiWindow* draw_window = window;
4288 ImVec2 inner_size = frame_size;
4289 ImGuiLastItemData item_data_backup;
4290 if (is_multiline)
4291 {
4292 ImVec2 backup_pos = window->DC.CursorPos;
4293 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
4294 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: ImGuiItemFlags_Inputable))
4295 {
4296 EndGroup();
4297 return false;
4298 }
4299 item_data_backup = g.LastItemData;
4300 window->DC.CursorPos = backup_pos;
4301
4302 // Prevent NavActivation from Tabbing when our widget accepts Tab inputs: this allows cycling through widgets without stopping.
4303 if (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_FromTabbing) && (flags & ImGuiInputTextFlags_AllowTabInput))
4304 g.NavActivateId = 0;
4305
4306 // Prevent NavActivate reactivating in BeginChild() when we are already active.
4307 const ImGuiID backup_activate_id = g.NavActivateId;
4308 if (g.ActiveId == id) // Prevent reactivation
4309 g.NavActivateId = 0;
4310
4311 // We reproduce the contents of BeginChildFrame() in order to provide 'label' so our window internal data are easier to read/debug.
4312 PushStyleColor(idx: ImGuiCol_ChildBg, col: style.Colors[ImGuiCol_FrameBg]);
4313 PushStyleVar(idx: ImGuiStyleVar_ChildRounding, val: style.FrameRounding);
4314 PushStyleVar(idx: ImGuiStyleVar_ChildBorderSize, val: style.FrameBorderSize);
4315 PushStyleVar(idx: ImGuiStyleVar_WindowPadding, val: ImVec2(0, 0)); // Ensure no clip rect so mouse hover can reach FramePadding edges
4316 bool child_visible = BeginChildEx(name: label, id, size_arg: frame_bb.GetSize(), child_flags: ImGuiChildFlags_Border, window_flags: ImGuiWindowFlags_NoMove);
4317 g.NavActivateId = backup_activate_id;
4318 PopStyleVar(count: 3);
4319 PopStyleColor();
4320 if (!child_visible)
4321 {
4322 EndChild();
4323 EndGroup();
4324 return false;
4325 }
4326 draw_window = g.CurrentWindow; // Child window
4327 draw_window->DC.NavLayersActiveMaskNext |= (1 << draw_window->DC.NavLayerCurrent); // This is to ensure that EndChild() will display a navigation highlight so we can "enter" into it.
4328 draw_window->DC.CursorPos += style.FramePadding;
4329 inner_size.x -= draw_window->ScrollbarSizes.x;
4330 }
4331 else
4332 {
4333 // Support for internal ImGuiInputTextFlags_MergedItem flag, which could be redesigned as an ItemFlags if needed (with test performed in ItemAdd)
4334 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
4335 if (!(flags & ImGuiInputTextFlags_MergedItem))
4336 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: ImGuiItemFlags_Inputable))
4337 return false;
4338 }
4339 const bool hovered = ItemHoverable(bb: frame_bb, id, item_flags: g.LastItemData.InFlags);
4340 if (hovered)
4341 g.MouseCursor = ImGuiMouseCursor_TextInput;
4342
4343 // We are only allowed to access the state if we are already the active widget.
4344 ImGuiInputTextState* state = GetInputTextState(id);
4345
4346 if (g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly)
4347 flags |= ImGuiInputTextFlags_ReadOnly;
4348 const bool is_readonly = (flags & ImGuiInputTextFlags_ReadOnly) != 0;
4349 const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0;
4350 const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0;
4351 const bool is_resizable = (flags & ImGuiInputTextFlags_CallbackResize) != 0;
4352 if (is_resizable)
4353 IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag!
4354
4355 const bool input_requested_by_nav = (g.ActiveId != id) && ((g.NavActivateId == id) && ((g.NavActivateFlags & ImGuiActivateFlags_PreferInput) || (g.NavInputSource == ImGuiInputSource_Keyboard)));
4356
4357 const bool user_clicked = hovered && io.MouseClicked[0];
4358 const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == GetWindowScrollbarID(window: draw_window, axis: ImGuiAxis_Y);
4359 const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == GetWindowScrollbarID(window: draw_window, axis: ImGuiAxis_Y);
4360 bool clear_active_id = false;
4361 bool select_all = false;
4362
4363 float scroll_y = is_multiline ? draw_window->Scroll.y : FLT_MAX;
4364
4365 const bool init_reload_from_user_buf = (state != NULL && state->ReloadUserBuf);
4366 const bool init_changed_specs = (state != NULL && state->Stb.single_line != !is_multiline); // state != NULL means its our state.
4367 const bool init_make_active = (user_clicked || user_scroll_finish || input_requested_by_nav);
4368 const bool init_state = (init_make_active || user_scroll_active);
4369 if ((init_state && g.ActiveId != id) || init_changed_specs || init_reload_from_user_buf)
4370 {
4371 // Access state even if we don't own it yet.
4372 state = &g.InputTextState;
4373 state->CursorAnimReset();
4374 state->ReloadUserBuf = false;
4375
4376 // Backup state of deactivating item so they'll have a chance to do a write to output buffer on the same frame they report IsItemDeactivatedAfterEdit (#4714)
4377 InputTextDeactivateHook(id: state->ID);
4378
4379 // From the moment we focused we are normally ignoring the content of 'buf' (unless we are in read-only mode)
4380 const int buf_len = (int)strlen(s: buf);
4381 if (!init_reload_from_user_buf)
4382 {
4383 // Take a copy of the initial buffer value.
4384 state->InitialTextA.resize(new_size: buf_len + 1); // UTF-8. we use +1 to make sure that .Data is always pointing to at least an empty string.
4385 memcpy(dest: state->InitialTextA.Data, src: buf, n: buf_len + 1);
4386 }
4387
4388 // Preserve cursor position and undo/redo stack if we come back to same widget
4389 // FIXME: Since we reworked this on 2022/06, may want to differentiate recycle_cursor vs recycle_undostate?
4390 bool recycle_state = (state->ID == id && !init_changed_specs && !init_reload_from_user_buf);
4391 if (recycle_state && (state->CurLenA != buf_len || (state->TextAIsValid && strncmp(s1: state->TextA.Data, s2: buf, n: buf_len) != 0)))
4392 recycle_state = false;
4393
4394 // Start edition
4395 const char* buf_end = NULL;
4396 state->ID = id;
4397 state->TextW.resize(new_size: buf_size + 1); // wchar count <= UTF-8 count. we use +1 to make sure that .Data is always pointing to at least an empty string.
4398 state->TextA.resize(new_size: 0);
4399 state->TextAIsValid = false; // TextA is not valid yet (we will display buf until then)
4400 state->CurLenW = ImTextStrFromUtf8(out_buf: state->TextW.Data, out_buf_size: buf_size, in_text: buf, NULL, in_remaining: &buf_end);
4401 state->CurLenA = (int)(buf_end - buf); // We can't get the result from ImStrncpy() above because it is not UTF-8 aware. Here we'll cut off malformed UTF-8.
4402
4403 if (recycle_state)
4404 {
4405 // Recycle existing cursor/selection/undo stack but clamp position
4406 // Note a single mouse click will override the cursor/position immediately by calling stb_textedit_click handler.
4407 state->CursorClamp();
4408 }
4409 else
4410 {
4411 state->ScrollX = 0.0f;
4412 stb_textedit_initialize_state(state: &state->Stb, is_single_line: !is_multiline);
4413 }
4414
4415 if (init_reload_from_user_buf)
4416 {
4417 state->Stb.select_start = state->ReloadSelectionStart;
4418 state->Stb.cursor = state->Stb.select_end = state->ReloadSelectionEnd;
4419 state->CursorClamp();
4420 }
4421 else if (!is_multiline)
4422 {
4423 if (flags & ImGuiInputTextFlags_AutoSelectAll)
4424 select_all = true;
4425 if (input_requested_by_nav && (!recycle_state || !(g.NavActivateFlags & ImGuiActivateFlags_TryToPreserveState)))
4426 select_all = true;
4427 if (user_clicked && io.KeyCtrl)
4428 select_all = true;
4429 }
4430
4431 if (flags & ImGuiInputTextFlags_AlwaysOverwrite)
4432 state->Stb.insert_mode = 1; // stb field name is indeed incorrect (see #2863)
4433 }
4434
4435 const bool is_osx = io.ConfigMacOSXBehaviors;
4436 if (g.ActiveId != id && init_make_active)
4437 {
4438 IM_ASSERT(state && state->ID == id);
4439 SetActiveID(id, window);
4440 SetFocusID(id, window);
4441 FocusWindow(window);
4442 }
4443 if (g.ActiveId == id)
4444 {
4445 // Declare some inputs, the other are registered and polled via Shortcut() routing system.
4446 if (user_clicked)
4447 SetKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id);
4448 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
4449 if (is_multiline || (flags & ImGuiInputTextFlags_CallbackHistory))
4450 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
4451 SetKeyOwner(key: ImGuiKey_Enter, owner_id: id);
4452 SetKeyOwner(key: ImGuiKey_KeypadEnter, owner_id: id);
4453 SetKeyOwner(key: ImGuiKey_Home, owner_id: id);
4454 SetKeyOwner(key: ImGuiKey_End, owner_id: id);
4455 if (is_multiline)
4456 {
4457 SetKeyOwner(key: ImGuiKey_PageUp, owner_id: id);
4458 SetKeyOwner(key: ImGuiKey_PageDown, owner_id: id);
4459 }
4460 // FIXME: May be a problem to always steal Alt on OSX, would ideally still allow an uninterrupted Alt down-up to toggle menu
4461 if (is_osx)
4462 SetKeyOwner(key: ImGuiMod_Alt, owner_id: id);
4463 }
4464
4465 // We have an edge case if ActiveId was set through another widget (e.g. widget being swapped), clear id immediately (don't wait until the end of the function)
4466 if (g.ActiveId == id && state == NULL)
4467 ClearActiveID();
4468
4469 // Release focus when we click outside
4470 if (g.ActiveId == id && io.MouseClicked[0] && !init_state && !init_make_active) //-V560
4471 clear_active_id = true;
4472
4473 // Lock the decision of whether we are going to take the path displaying the cursor or selection
4474 bool render_cursor = (g.ActiveId == id) || (state && user_scroll_active);
4475 bool render_selection = state && (state->HasSelection() || select_all) && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
4476 bool value_changed = false;
4477 bool validated = false;
4478
4479 // When read-only we always use the live data passed to the function
4480 // FIXME-OPT: Because our selection/cursor code currently needs the wide text we need to convert it when active, which is not ideal :(
4481 if (is_readonly && state != NULL && (render_cursor || render_selection))
4482 {
4483 const char* buf_end = NULL;
4484 state->TextW.resize(new_size: buf_size + 1);
4485 state->CurLenW = ImTextStrFromUtf8(out_buf: state->TextW.Data, out_buf_size: state->TextW.Size, in_text: buf, NULL, in_remaining: &buf_end);
4486 state->CurLenA = (int)(buf_end - buf);
4487 state->CursorClamp();
4488 render_selection &= state->HasSelection();
4489 }
4490
4491 // Select the buffer to render.
4492 const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state && state->TextAIsValid;
4493 const bool is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0);
4494
4495 // Password pushes a temporary font with only a fallback glyph
4496 if (is_password && !is_displaying_hint)
4497 {
4498 const ImFontGlyph* glyph = g.Font->FindGlyph(c: '*');
4499 ImFont* password_font = &g.InputTextPasswordFont;
4500 password_font->FontSize = g.Font->FontSize;
4501 password_font->Scale = g.Font->Scale;
4502 password_font->Ascent = g.Font->Ascent;
4503 password_font->Descent = g.Font->Descent;
4504 password_font->ContainerAtlas = g.Font->ContainerAtlas;
4505 password_font->FallbackGlyph = glyph;
4506 password_font->FallbackAdvanceX = glyph->AdvanceX;
4507 IM_ASSERT(password_font->Glyphs.empty() && password_font->IndexAdvanceX.empty() && password_font->IndexLookup.empty());
4508 PushFont(font: password_font);
4509 }
4510
4511 // Process mouse inputs and character inputs
4512 int backup_current_text_length = 0;
4513 if (g.ActiveId == id)
4514 {
4515 IM_ASSERT(state != NULL);
4516 backup_current_text_length = state->CurLenA;
4517 state->Edited = false;
4518 state->BufCapacityA = buf_size;
4519 state->Flags = flags;
4520
4521 // Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget.
4522 // Down the line we should have a cleaner library-wide concept of Selected vs Active.
4523 g.ActiveIdAllowOverlap = !io.MouseDown[0];
4524
4525 // Edit in progress
4526 const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + state->ScrollX;
4527 const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y) : (g.FontSize * 0.5f));
4528
4529 if (select_all)
4530 {
4531 state->SelectAll();
4532 state->SelectedAllMouseLock = true;
4533 }
4534 else if (hovered && io.MouseClickedCount[0] >= 2 && !io.KeyShift)
4535 {
4536 stb_textedit_click(str: state, state: &state->Stb, x: mouse_x, y: mouse_y);
4537 const int multiclick_count = (io.MouseClickedCount[0] - 2);
4538 if ((multiclick_count % 2) == 0)
4539 {
4540 // Double-click: Select word
4541 // We always use the "Mac" word advance for double-click select vs CTRL+Right which use the platform dependent variant:
4542 // FIXME: There are likely many ways to improve this behavior, but there's no "right" behavior (depends on use-case, software, OS)
4543 const bool is_bol = (state->Stb.cursor == 0) || ImStb::STB_TEXTEDIT_GETCHAR(obj: state, idx: state->Stb.cursor - 1) == '\n';
4544 if (STB_TEXT_HAS_SELECTION(&state->Stb) || !is_bol)
4545 state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT);
4546 //state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
4547 if (!STB_TEXT_HAS_SELECTION(&state->Stb))
4548 ImStb::stb_textedit_prep_selection_at_cursor(state: &state->Stb);
4549 state->Stb.cursor = ImStb::STB_TEXTEDIT_MOVEWORDRIGHT_MAC(obj: state, idx: state->Stb.cursor);
4550 state->Stb.select_end = state->Stb.cursor;
4551 ImStb::stb_textedit_clamp(str: state, state: &state->Stb);
4552 }
4553 else
4554 {
4555 // Triple-click: Select line
4556 const bool is_eol = ImStb::STB_TEXTEDIT_GETCHAR(obj: state, idx: state->Stb.cursor) == '\n';
4557 state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART);
4558 state->OnKeyPressed(STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT);
4559 state->OnKeyPressed(STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT);
4560 if (!is_eol && is_multiline)
4561 {
4562 ImSwap(a&: state->Stb.select_start, b&: state->Stb.select_end);
4563 state->Stb.cursor = state->Stb.select_end;
4564 }
4565 state->CursorFollow = false;
4566 }
4567 state->CursorAnimReset();
4568 }
4569 else if (io.MouseClicked[0] && !state->SelectedAllMouseLock)
4570 {
4571 if (hovered)
4572 {
4573 if (io.KeyShift)
4574 stb_textedit_drag(str: state, state: &state->Stb, x: mouse_x, y: mouse_y);
4575 else
4576 stb_textedit_click(str: state, state: &state->Stb, x: mouse_x, y: mouse_y);
4577 state->CursorAnimReset();
4578 }
4579 }
4580 else if (io.MouseDown[0] && !state->SelectedAllMouseLock && (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f))
4581 {
4582 stb_textedit_drag(str: state, state: &state->Stb, x: mouse_x, y: mouse_y);
4583 state->CursorAnimReset();
4584 state->CursorFollow = true;
4585 }
4586 if (state->SelectedAllMouseLock && !io.MouseDown[0])
4587 state->SelectedAllMouseLock = false;
4588
4589 // We expect backends to emit a Tab key but some also emit a Tab character which we ignore (#2467, #1336)
4590 // (For Tab and Enter: Win32/SFML/Allegro are sending both keys and chars, GLFW and SDL are only sending keys. For Space they all send all threes)
4591 if ((flags & ImGuiInputTextFlags_AllowTabInput) && !is_readonly)
4592 {
4593 if (Shortcut(key_chord: ImGuiKey_Tab, flags: ImGuiInputFlags_Repeat, owner_id: id))
4594 {
4595 unsigned int c = '\t'; // Insert TAB
4596 if (InputTextFilterCharacter(ctx: &g, p_char: &c, flags, callback, user_data: callback_user_data))
4597 state->OnKeyPressed(key: (int)c);
4598 }
4599 // FIXME: Implement Shift+Tab
4600 /*
4601 if (Shortcut(ImGuiKey_Tab | ImGuiMod_Shift, ImGuiInputFlags_Repeat, id))
4602 {
4603 }
4604 */
4605 }
4606
4607 // Process regular text input (before we check for Return because using some IME will effectively send a Return?)
4608 // We ignore CTRL inputs, but need to allow ALT+CTRL as some keyboards (e.g. German) use AltGR (which _is_ Alt+Ctrl) to input certain characters.
4609 const bool ignore_char_inputs = (io.KeyCtrl && !io.KeyAlt) || (is_osx && io.KeyCtrl);
4610 if (io.InputQueueCharacters.Size > 0)
4611 {
4612 if (!ignore_char_inputs && !is_readonly && !input_requested_by_nav)
4613 for (int n = 0; n < io.InputQueueCharacters.Size; n++)
4614 {
4615 // Insert character if they pass filtering
4616 unsigned int c = (unsigned int)io.InputQueueCharacters[n];
4617 if (c == '\t') // Skip Tab, see above.
4618 continue;
4619 if (InputTextFilterCharacter(ctx: &g, p_char: &c, flags, callback, user_data: callback_user_data))
4620 state->OnKeyPressed(key: (int)c);
4621 }
4622
4623 // Consume characters
4624 io.InputQueueCharacters.resize(new_size: 0);
4625 }
4626 }
4627
4628 // Process other shortcuts/key-presses
4629 bool revert_edit = false;
4630 if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id)
4631 {
4632 IM_ASSERT(state != NULL);
4633
4634 const int row_count_per_page = ImMax(lhs: (int)((inner_size.y - style.FramePadding.y) / g.FontSize), rhs: 1);
4635 state->Stb.row_count_per_page = row_count_per_page;
4636
4637 const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0);
4638 const bool is_wordmove_key_down = is_osx ? io.KeyAlt : io.KeyCtrl; // OS X style: Text editing cursor movement using Alt instead of Ctrl
4639 const bool is_startend_key_down = is_osx && io.KeyCtrl && !io.KeySuper && !io.KeyAlt; // OS X style: Line/Text Start and End using Cmd+Arrows instead of Home/End
4640
4641 // Using Shortcut() with ImGuiInputFlags_RouteFocused (default policy) to allow routing operations for other code (e.g. calling window trying to use CTRL+A and CTRL+B: formet would be handled by InputText)
4642 // Otherwise we could simply assume that we own the keys as we are active.
4643 const ImGuiInputFlags f_repeat = ImGuiInputFlags_Repeat;
4644 const bool is_cut = (Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiKey_X, flags: f_repeat, owner_id: id) || Shortcut(key_chord: ImGuiMod_Shift | ImGuiKey_Delete, flags: f_repeat, owner_id: id)) && !is_readonly && !is_password && (!is_multiline || state->HasSelection());
4645 const bool is_copy = (Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiKey_C, flags: 0, owner_id: id) || Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiKey_Insert, flags: 0, owner_id: id)) && !is_password && (!is_multiline || state->HasSelection());
4646 const bool is_paste = (Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiKey_V, flags: f_repeat, owner_id: id) || Shortcut(key_chord: ImGuiMod_Shift | ImGuiKey_Insert, flags: f_repeat, owner_id: id)) && !is_readonly;
4647 const bool is_undo = (Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiKey_Z, flags: f_repeat, owner_id: id)) && !is_readonly && is_undoable;
4648 const bool is_redo = (Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiKey_Y, flags: f_repeat, owner_id: id) || (is_osx && Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Z, flags: f_repeat, owner_id: id))) && !is_readonly && is_undoable;
4649 const bool is_select_all = Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiKey_A, flags: 0, owner_id: id);
4650
4651 // We allow validate/cancel with Nav source (gamepad) to makes it easier to undo an accidental NavInput press with no keyboard wired, but otherwise it isn't very useful.
4652 const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0;
4653 const bool is_enter_pressed = IsKeyPressed(key: ImGuiKey_Enter, repeat: true) || IsKeyPressed(key: ImGuiKey_KeypadEnter, repeat: true);
4654 const bool is_gamepad_validate = nav_gamepad_active && (IsKeyPressed(ImGuiKey_NavGamepadActivate, repeat: false) || IsKeyPressed(ImGuiKey_NavGamepadInput, repeat: false));
4655 const bool is_cancel = Shortcut(key_chord: ImGuiKey_Escape, flags: f_repeat, owner_id: id) || (nav_gamepad_active && Shortcut(ImGuiKey_NavGamepadCancel, flags: f_repeat, owner_id: id));
4656
4657 // FIXME: Should use more Shortcut() and reduce IsKeyPressed()+SetKeyOwner(), but requires modifiers combination to be taken account of.
4658 // FIXME-OSX: Missing support for Alt(option)+Right/Left = go to end of line, or next line if already in end of line.
4659 if (IsKeyPressed(key: ImGuiKey_LeftArrow)) { state->OnKeyPressed(key: (is_startend_key_down ? STB_TEXTEDIT_K_LINESTART : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDLEFT : STB_TEXTEDIT_K_LEFT) | k_mask); }
4660 else if (IsKeyPressed(key: ImGuiKey_RightArrow)) { state->OnKeyPressed(key: (is_startend_key_down ? STB_TEXTEDIT_K_LINEEND : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDRIGHT : STB_TEXTEDIT_K_RIGHT) | k_mask); }
4661 else if (IsKeyPressed(key: ImGuiKey_UpArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(window: draw_window, scroll_y: ImMax(lhs: draw_window->Scroll.y - g.FontSize, rhs: 0.0f)); else state->OnKeyPressed(key: (is_startend_key_down ? STB_TEXTEDIT_K_TEXTSTART : STB_TEXTEDIT_K_UP) | k_mask); }
4662 else if (IsKeyPressed(key: ImGuiKey_DownArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(window: draw_window, scroll_y: ImMin(lhs: draw_window->Scroll.y + g.FontSize, rhs: GetScrollMaxY())); else state->OnKeyPressed(key: (is_startend_key_down ? STB_TEXTEDIT_K_TEXTEND : STB_TEXTEDIT_K_DOWN) | k_mask); }
4663 else if (IsKeyPressed(key: ImGuiKey_PageUp) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGUP | k_mask); scroll_y -= row_count_per_page * g.FontSize; }
4664 else if (IsKeyPressed(key: ImGuiKey_PageDown) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGDOWN | k_mask); scroll_y += row_count_per_page * g.FontSize; }
4665 else if (IsKeyPressed(key: ImGuiKey_Home)) { state->OnKeyPressed(key: io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); }
4666 else if (IsKeyPressed(key: ImGuiKey_End)) { state->OnKeyPressed(key: io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); }
4667 else if (IsKeyPressed(key: ImGuiKey_Delete) && !is_readonly && !is_cut)
4668 {
4669 if (!state->HasSelection())
4670 {
4671 // OSX doesn't seem to have Super+Delete to delete until end-of-line, so we don't emulate that (as opposed to Super+Backspace)
4672 if (is_wordmove_key_down)
4673 state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
4674 }
4675 state->OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask);
4676 }
4677 else if (IsKeyPressed(key: ImGuiKey_Backspace) && !is_readonly)
4678 {
4679 if (!state->HasSelection())
4680 {
4681 if (is_wordmove_key_down)
4682 state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT);
4683 else if (is_osx && io.KeyCtrl && !io.KeyAlt && !io.KeySuper)
4684 state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT);
4685 }
4686 state->OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask);
4687 }
4688 else if (is_enter_pressed || is_gamepad_validate)
4689 {
4690 // Determine if we turn Enter into a \n character
4691 bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0;
4692 if (!is_multiline || is_gamepad_validate || (ctrl_enter_for_new_line && !io.KeyCtrl) || (!ctrl_enter_for_new_line && io.KeyCtrl))
4693 {
4694 validated = true;
4695 if (io.ConfigInputTextEnterKeepActive && !is_multiline)
4696 state->SelectAll(); // No need to scroll
4697 else
4698 clear_active_id = true;
4699 }
4700 else if (!is_readonly)
4701 {
4702 unsigned int c = '\n'; // Insert new line
4703 if (InputTextFilterCharacter(ctx: &g, p_char: &c, flags, callback, user_data: callback_user_data))
4704 state->OnKeyPressed(key: (int)c);
4705 }
4706 }
4707 else if (is_cancel)
4708 {
4709 if (flags & ImGuiInputTextFlags_EscapeClearsAll)
4710 {
4711 if (buf[0] != 0)
4712 {
4713 revert_edit = true;
4714 }
4715 else
4716 {
4717 render_cursor = render_selection = false;
4718 clear_active_id = true;
4719 }
4720 }
4721 else
4722 {
4723 clear_active_id = revert_edit = true;
4724 render_cursor = render_selection = false;
4725 }
4726 }
4727 else if (is_undo || is_redo)
4728 {
4729 state->OnKeyPressed(key: is_undo ? STB_TEXTEDIT_K_UNDO : STB_TEXTEDIT_K_REDO);
4730 state->ClearSelection();
4731 }
4732 else if (is_select_all)
4733 {
4734 state->SelectAll();
4735 state->CursorFollow = true;
4736 }
4737 else if (is_cut || is_copy)
4738 {
4739 // Cut, Copy
4740 if (io.SetClipboardTextFn)
4741 {
4742 const int ib = state->HasSelection() ? ImMin(lhs: state->Stb.select_start, rhs: state->Stb.select_end) : 0;
4743 const int ie = state->HasSelection() ? ImMax(lhs: state->Stb.select_start, rhs: state->Stb.select_end) : state->CurLenW;
4744 const int clipboard_data_len = ImTextCountUtf8BytesFromStr(in_text: state->TextW.Data + ib, in_text_end: state->TextW.Data + ie) + 1;
4745 char* clipboard_data = (char*)IM_ALLOC(clipboard_data_len * sizeof(char));
4746 ImTextStrToUtf8(out_buf: clipboard_data, out_buf_size: clipboard_data_len, in_text: state->TextW.Data + ib, in_text_end: state->TextW.Data + ie);
4747 SetClipboardText(clipboard_data);
4748 MemFree(ptr: clipboard_data);
4749 }
4750 if (is_cut)
4751 {
4752 if (!state->HasSelection())
4753 state->SelectAll();
4754 state->CursorFollow = true;
4755 stb_textedit_cut(str: state, state: &state->Stb);
4756 }
4757 }
4758 else if (is_paste)
4759 {
4760 if (const char* clipboard = GetClipboardText())
4761 {
4762 // Filter pasted buffer
4763 const int clipboard_len = (int)strlen(s: clipboard);
4764 ImWchar* clipboard_filtered = (ImWchar*)IM_ALLOC((clipboard_len + 1) * sizeof(ImWchar));
4765 int clipboard_filtered_len = 0;
4766 for (const char* s = clipboard; *s != 0; )
4767 {
4768 unsigned int c;
4769 s += ImTextCharFromUtf8(out_char: &c, in_text: s, NULL);
4770 if (!InputTextFilterCharacter(ctx: &g, p_char: &c, flags, callback, user_data: callback_user_data, input_source_is_clipboard: true))
4771 continue;
4772 clipboard_filtered[clipboard_filtered_len++] = (ImWchar)c;
4773 }
4774 clipboard_filtered[clipboard_filtered_len] = 0;
4775 if (clipboard_filtered_len > 0) // If everything was filtered, ignore the pasting operation
4776 {
4777 stb_textedit_paste(str: state, state: &state->Stb, ctext: clipboard_filtered, len: clipboard_filtered_len);
4778 state->CursorFollow = true;
4779 }
4780 MemFree(ptr: clipboard_filtered);
4781 }
4782 }
4783
4784 // Update render selection flag after events have been handled, so selection highlight can be displayed during the same frame.
4785 render_selection |= state->HasSelection() && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
4786 }
4787
4788 // Process callbacks and apply result back to user's buffer.
4789 const char* apply_new_text = NULL;
4790 int apply_new_text_length = 0;
4791 if (g.ActiveId == id)
4792 {
4793 IM_ASSERT(state != NULL);
4794 if (revert_edit && !is_readonly)
4795 {
4796 if (flags & ImGuiInputTextFlags_EscapeClearsAll)
4797 {
4798 // Clear input
4799 IM_ASSERT(buf[0] != 0);
4800 apply_new_text = "";
4801 apply_new_text_length = 0;
4802 value_changed = true;
4803 IMSTB_TEXTEDIT_CHARTYPE empty_string;
4804 stb_textedit_replace(str: state, state: &state->Stb, text: &empty_string, text_len: 0);
4805 }
4806 else if (strcmp(s1: buf, s2: state->InitialTextA.Data) != 0)
4807 {
4808 // Restore initial value. Only return true if restoring to the initial value changes the current buffer contents.
4809 // Push records into the undo stack so we can CTRL+Z the revert operation itself
4810 apply_new_text = state->InitialTextA.Data;
4811 apply_new_text_length = state->InitialTextA.Size - 1;
4812 value_changed = true;
4813 ImVector<ImWchar> w_text;
4814 if (apply_new_text_length > 0)
4815 {
4816 w_text.resize(new_size: ImTextCountCharsFromUtf8(in_text: apply_new_text, in_text_end: apply_new_text + apply_new_text_length) + 1);
4817 ImTextStrFromUtf8(out_buf: w_text.Data, out_buf_size: w_text.Size, in_text: apply_new_text, in_text_end: apply_new_text + apply_new_text_length);
4818 }
4819 stb_textedit_replace(str: state, state: &state->Stb, text: w_text.Data, text_len: (apply_new_text_length > 0) ? (w_text.Size - 1) : 0);
4820 }
4821 }
4822
4823 // Apply ASCII value
4824 if (!is_readonly)
4825 {
4826 state->TextAIsValid = true;
4827 state->TextA.resize(new_size: state->TextW.Size * 4 + 1);
4828 ImTextStrToUtf8(out_buf: state->TextA.Data, out_buf_size: state->TextA.Size, in_text: state->TextW.Data, NULL);
4829 }
4830
4831 // When using 'ImGuiInputTextFlags_EnterReturnsTrue' as a special case we reapply the live buffer back to the input buffer
4832 // before clearing ActiveId, even though strictly speaking it wasn't modified on this frame.
4833 // If we didn't do that, code like InputInt() with ImGuiInputTextFlags_EnterReturnsTrue would fail.
4834 // This also allows the user to use InputText() with ImGuiInputTextFlags_EnterReturnsTrue without maintaining any user-side storage
4835 // (please note that if you use this property along ImGuiInputTextFlags_CallbackResize you can end up with your temporary string object
4836 // unnecessarily allocating once a frame, either store your string data, either if you don't then don't use ImGuiInputTextFlags_CallbackResize).
4837 const bool apply_edit_back_to_user_buffer = !revert_edit || (validated && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0);
4838 if (apply_edit_back_to_user_buffer)
4839 {
4840 // Apply new value immediately - copy modified buffer back
4841 // Note that as soon as the input box is active, the in-widget value gets priority over any underlying modification of the input buffer
4842 // FIXME: We actually always render 'buf' when calling DrawList->AddText, making the comment above incorrect.
4843 // FIXME-OPT: CPU waste to do this every time the widget is active, should mark dirty state from the stb_textedit callbacks.
4844
4845 // User callback
4846 if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackEdit | ImGuiInputTextFlags_CallbackAlways)) != 0)
4847 {
4848 IM_ASSERT(callback != NULL);
4849
4850 // The reason we specify the usage semantic (Completion/History) is that Completion needs to disable keyboard TABBING at the moment.
4851 ImGuiInputTextFlags event_flag = 0;
4852 ImGuiKey event_key = ImGuiKey_None;
4853 if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && Shortcut(key_chord: ImGuiKey_Tab, flags: 0, owner_id: id))
4854 {
4855 event_flag = ImGuiInputTextFlags_CallbackCompletion;
4856 event_key = ImGuiKey_Tab;
4857 }
4858 else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(key: ImGuiKey_UpArrow))
4859 {
4860 event_flag = ImGuiInputTextFlags_CallbackHistory;
4861 event_key = ImGuiKey_UpArrow;
4862 }
4863 else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(key: ImGuiKey_DownArrow))
4864 {
4865 event_flag = ImGuiInputTextFlags_CallbackHistory;
4866 event_key = ImGuiKey_DownArrow;
4867 }
4868 else if ((flags & ImGuiInputTextFlags_CallbackEdit) && state->Edited)
4869 {
4870 event_flag = ImGuiInputTextFlags_CallbackEdit;
4871 }
4872 else if (flags & ImGuiInputTextFlags_CallbackAlways)
4873 {
4874 event_flag = ImGuiInputTextFlags_CallbackAlways;
4875 }
4876
4877 if (event_flag)
4878 {
4879 ImGuiInputTextCallbackData callback_data;
4880 callback_data.Ctx = &g;
4881 callback_data.EventFlag = event_flag;
4882 callback_data.Flags = flags;
4883 callback_data.UserData = callback_user_data;
4884
4885 char* callback_buf = is_readonly ? buf : state->TextA.Data;
4886 callback_data.EventKey = event_key;
4887 callback_data.Buf = callback_buf;
4888 callback_data.BufTextLen = state->CurLenA;
4889 callback_data.BufSize = state->BufCapacityA;
4890 callback_data.BufDirty = false;
4891
4892 // We have to convert from wchar-positions to UTF-8-positions, which can be pretty slow (an incentive to ditch the ImWchar buffer, see https://github.com/nothings/stb/issues/188)
4893 ImWchar* text = state->TextW.Data;
4894 const int utf8_cursor_pos = callback_data.CursorPos = ImTextCountUtf8BytesFromStr(in_text: text, in_text_end: text + state->Stb.cursor);
4895 const int utf8_selection_start = callback_data.SelectionStart = ImTextCountUtf8BytesFromStr(in_text: text, in_text_end: text + state->Stb.select_start);
4896 const int utf8_selection_end = callback_data.SelectionEnd = ImTextCountUtf8BytesFromStr(in_text: text, in_text_end: text + state->Stb.select_end);
4897
4898 // Call user code
4899 callback(&callback_data);
4900
4901 // Read back what user may have modified
4902 callback_buf = is_readonly ? buf : state->TextA.Data; // Pointer may have been invalidated by a resize callback
4903 IM_ASSERT(callback_data.Buf == callback_buf); // Invalid to modify those fields
4904 IM_ASSERT(callback_data.BufSize == state->BufCapacityA);
4905 IM_ASSERT(callback_data.Flags == flags);
4906 const bool buf_dirty = callback_data.BufDirty;
4907 if (callback_data.CursorPos != utf8_cursor_pos || buf_dirty) { state->Stb.cursor = ImTextCountCharsFromUtf8(in_text: callback_data.Buf, in_text_end: callback_data.Buf + callback_data.CursorPos); state->CursorFollow = true; }
4908 if (callback_data.SelectionStart != utf8_selection_start || buf_dirty) { state->Stb.select_start = (callback_data.SelectionStart == callback_data.CursorPos) ? state->Stb.cursor : ImTextCountCharsFromUtf8(in_text: callback_data.Buf, in_text_end: callback_data.Buf + callback_data.SelectionStart); }
4909 if (callback_data.SelectionEnd != utf8_selection_end || buf_dirty) { state->Stb.select_end = (callback_data.SelectionEnd == callback_data.SelectionStart) ? state->Stb.select_start : ImTextCountCharsFromUtf8(in_text: callback_data.Buf, in_text_end: callback_data.Buf + callback_data.SelectionEnd); }
4910 if (buf_dirty)
4911 {
4912 IM_ASSERT(!is_readonly);
4913 IM_ASSERT(callback_data.BufTextLen == (int)strlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text!
4914 InputTextReconcileUndoStateAfterUserCallback(state, new_buf_a: callback_data.Buf, new_length_a: callback_data.BufTextLen); // FIXME: Move the rest of this block inside function and rename to InputTextReconcileStateAfterUserCallback() ?
4915 if (callback_data.BufTextLen > backup_current_text_length && is_resizable)
4916 state->TextW.resize(new_size: state->TextW.Size + (callback_data.BufTextLen - backup_current_text_length)); // Worse case scenario resize
4917 state->CurLenW = ImTextStrFromUtf8(out_buf: state->TextW.Data, out_buf_size: state->TextW.Size, in_text: callback_data.Buf, NULL);
4918 state->CurLenA = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen()
4919 state->CursorAnimReset();
4920 }
4921 }
4922 }
4923
4924 // Will copy result string if modified
4925 if (!is_readonly && strcmp(s1: state->TextA.Data, s2: buf) != 0)
4926 {
4927 apply_new_text = state->TextA.Data;
4928 apply_new_text_length = state->CurLenA;
4929 value_changed = true;
4930 }
4931 }
4932 }
4933
4934 // Handle reapplying final data on deactivation (see InputTextDeactivateHook() for details)
4935 if (g.InputTextDeactivatedState.ID == id)
4936 {
4937 if (g.ActiveId != id && IsItemDeactivatedAfterEdit() && !is_readonly && strcmp(s1: g.InputTextDeactivatedState.TextA.Data, s2: buf) != 0)
4938 {
4939 apply_new_text = g.InputTextDeactivatedState.TextA.Data;
4940 apply_new_text_length = g.InputTextDeactivatedState.TextA.Size - 1;
4941 value_changed = true;
4942 //IMGUI_DEBUG_LOG("InputText(): apply Deactivated data for 0x%08X: \"%.*s\".\n", id, apply_new_text_length, apply_new_text);
4943 }
4944 g.InputTextDeactivatedState.ID = 0;
4945 }
4946
4947 // Copy result to user buffer. This can currently only happen when (g.ActiveId == id)
4948 if (apply_new_text != NULL)
4949 {
4950 // We cannot test for 'backup_current_text_length != apply_new_text_length' here because we have no guarantee that the size
4951 // of our owned buffer matches the size of the string object held by the user, and by design we allow InputText() to be used
4952 // without any storage on user's side.
4953 IM_ASSERT(apply_new_text_length >= 0);
4954 if (is_resizable)
4955 {
4956 ImGuiInputTextCallbackData callback_data;
4957 callback_data.Ctx = &g;
4958 callback_data.EventFlag = ImGuiInputTextFlags_CallbackResize;
4959 callback_data.Flags = flags;
4960 callback_data.Buf = buf;
4961 callback_data.BufTextLen = apply_new_text_length;
4962 callback_data.BufSize = ImMax(lhs: buf_size, rhs: apply_new_text_length + 1);
4963 callback_data.UserData = callback_user_data;
4964 callback(&callback_data);
4965 buf = callback_data.Buf;
4966 buf_size = callback_data.BufSize;
4967 apply_new_text_length = ImMin(lhs: callback_data.BufTextLen, rhs: buf_size - 1);
4968 IM_ASSERT(apply_new_text_length <= buf_size);
4969 }
4970 //IMGUI_DEBUG_PRINT("InputText(\"%s\"): apply_new_text length %d\n", label, apply_new_text_length);
4971
4972 // If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size.
4973 ImStrncpy(dst: buf, src: apply_new_text, count: ImMin(lhs: apply_new_text_length + 1, rhs: buf_size));
4974 }
4975
4976 // Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value)
4977 // Otherwise request text input ahead for next frame.
4978 if (g.ActiveId == id && clear_active_id)
4979 ClearActiveID();
4980 else if (g.ActiveId == id)
4981 g.WantTextInputNextFrame = 1;
4982
4983 // Render frame
4984 if (!is_multiline)
4985 {
4986 RenderNavHighlight(bb: frame_bb, id);
4987 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: GetColorU32(idx: ImGuiCol_FrameBg), border: true, rounding: style.FrameRounding);
4988 }
4989
4990 const ImVec4 clip_rect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + inner_size.x, frame_bb.Min.y + inner_size.y); // Not using frame_bb.Max because we have adjusted size
4991 ImVec2 draw_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding;
4992 ImVec2 text_size(0.0f, 0.0f);
4993
4994 // Set upper limit of single-line InputTextEx() at 2 million characters strings. The current pathological worst case is a long line
4995 // without any carriage return, which would makes ImFont::RenderText() reserve too many vertices and probably crash. Avoid it altogether.
4996 // Note that we only use this limit on single-line InputText(), so a pathologically large line on a InputTextMultiline() would still crash.
4997 const int buf_display_max_length = 2 * 1024 * 1024;
4998 const char* buf_display = buf_display_from_state ? state->TextA.Data : buf; //-V595
4999 const char* buf_display_end = NULL; // We have specialized paths below for setting the length
5000 if (is_displaying_hint)
5001 {
5002 buf_display = hint;
5003 buf_display_end = hint + strlen(s: hint);
5004 }
5005
5006 // Render text. We currently only render selection when the widget is active or while scrolling.
5007 // FIXME: We could remove the '&& render_cursor' to keep rendering selection when inactive.
5008 if (render_cursor || render_selection)
5009 {
5010 IM_ASSERT(state != NULL);
5011 if (!is_displaying_hint)
5012 buf_display_end = buf_display + state->CurLenA;
5013
5014 // Render text (with cursor and selection)
5015 // This is going to be messy. We need to:
5016 // - Display the text (this alone can be more easily clipped)
5017 // - Handle scrolling, highlight selection, display cursor (those all requires some form of 1d->2d cursor position calculation)
5018 // - Measure text height (for scrollbar)
5019 // We are attempting to do most of that in **one main pass** to minimize the computation cost (non-negligible for large amount of text) + 2nd pass for selection rendering (we could merge them by an extra refactoring effort)
5020 // FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8.
5021 const ImWchar* text_begin = state->TextW.Data;
5022 ImVec2 cursor_offset, select_start_offset;
5023
5024 {
5025 // Find lines numbers straddling 'cursor' (slot 0) and 'select_start' (slot 1) positions.
5026 const ImWchar* searches_input_ptr[2] = { NULL, NULL };
5027 int searches_result_line_no[2] = { -1000, -1000 };
5028 int searches_remaining = 0;
5029 if (render_cursor)
5030 {
5031 searches_input_ptr[0] = text_begin + state->Stb.cursor;
5032 searches_result_line_no[0] = -1;
5033 searches_remaining++;
5034 }
5035 if (render_selection)
5036 {
5037 searches_input_ptr[1] = text_begin + ImMin(lhs: state->Stb.select_start, rhs: state->Stb.select_end);
5038 searches_result_line_no[1] = -1;
5039 searches_remaining++;
5040 }
5041
5042 // Iterate all lines to find our line numbers
5043 // In multi-line mode, we never exit the loop until all lines are counted, so add one extra to the searches_remaining counter.
5044 searches_remaining += is_multiline ? 1 : 0;
5045 int line_count = 0;
5046 //for (const ImWchar* s = text_begin; (s = (const ImWchar*)wcschr((const wchar_t*)s, (wchar_t)'\n')) != NULL; s++) // FIXME-OPT: Could use this when wchar_t are 16-bit
5047 for (const ImWchar* s = text_begin; *s != 0; s++)
5048 if (*s == '\n')
5049 {
5050 line_count++;
5051 if (searches_result_line_no[0] == -1 && s >= searches_input_ptr[0]) { searches_result_line_no[0] = line_count; if (--searches_remaining <= 0) break; }
5052 if (searches_result_line_no[1] == -1 && s >= searches_input_ptr[1]) { searches_result_line_no[1] = line_count; if (--searches_remaining <= 0) break; }
5053 }
5054 line_count++;
5055 if (searches_result_line_no[0] == -1)
5056 searches_result_line_no[0] = line_count;
5057 if (searches_result_line_no[1] == -1)
5058 searches_result_line_no[1] = line_count;
5059
5060 // Calculate 2d position by finding the beginning of the line and measuring distance
5061 cursor_offset.x = InputTextCalcTextSizeW(ctx: &g, text_begin: ImStrbolW(buf_mid_line: searches_input_ptr[0], buf_begin: text_begin), text_end: searches_input_ptr[0]).x;
5062 cursor_offset.y = searches_result_line_no[0] * g.FontSize;
5063 if (searches_result_line_no[1] >= 0)
5064 {
5065 select_start_offset.x = InputTextCalcTextSizeW(ctx: &g, text_begin: ImStrbolW(buf_mid_line: searches_input_ptr[1], buf_begin: text_begin), text_end: searches_input_ptr[1]).x;
5066 select_start_offset.y = searches_result_line_no[1] * g.FontSize;
5067 }
5068
5069 // Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224)
5070 if (is_multiline)
5071 text_size = ImVec2(inner_size.x, line_count * g.FontSize);
5072 }
5073
5074 // Scroll
5075 if (render_cursor && state->CursorFollow)
5076 {
5077 // Horizontal scroll in chunks of quarter width
5078 if (!(flags & ImGuiInputTextFlags_NoHorizontalScroll))
5079 {
5080 const float scroll_increment_x = inner_size.x * 0.25f;
5081 const float visible_width = inner_size.x - style.FramePadding.x;
5082 if (cursor_offset.x < state->ScrollX)
5083 state->ScrollX = IM_TRUNC(ImMax(0.0f, cursor_offset.x - scroll_increment_x));
5084 else if (cursor_offset.x - visible_width >= state->ScrollX)
5085 state->ScrollX = IM_TRUNC(cursor_offset.x - visible_width + scroll_increment_x);
5086 }
5087 else
5088 {
5089 state->ScrollX = 0.0f;
5090 }
5091
5092 // Vertical scroll
5093 if (is_multiline)
5094 {
5095 // Test if cursor is vertically visible
5096 if (cursor_offset.y - g.FontSize < scroll_y)
5097 scroll_y = ImMax(lhs: 0.0f, rhs: cursor_offset.y - g.FontSize);
5098 else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y)
5099 scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f;
5100 const float scroll_max_y = ImMax(lhs: (text_size.y + style.FramePadding.y * 2.0f) - inner_size.y, rhs: 0.0f);
5101 scroll_y = ImClamp(v: scroll_y, mn: 0.0f, mx: scroll_max_y);
5102 draw_pos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag
5103 draw_window->Scroll.y = scroll_y;
5104 }
5105
5106 state->CursorFollow = false;
5107 }
5108
5109 // Draw selection
5110 const ImVec2 draw_scroll = ImVec2(state->ScrollX, 0.0f);
5111 if (render_selection)
5112 {
5113 const ImWchar* text_selected_begin = text_begin + ImMin(lhs: state->Stb.select_start, rhs: state->Stb.select_end);
5114 const ImWchar* text_selected_end = text_begin + ImMax(lhs: state->Stb.select_start, rhs: state->Stb.select_end);
5115
5116 ImU32 bg_color = GetColorU32(idx: ImGuiCol_TextSelectedBg, alpha_mul: render_cursor ? 1.0f : 0.6f); // FIXME: current code flow mandate that render_cursor is always true here, we are leaving the transparent one for tests.
5117 float bg_offy_up = is_multiline ? 0.0f : -1.0f; // FIXME: those offsets should be part of the style? they don't play so well with multi-line selection.
5118 float bg_offy_dn = is_multiline ? 0.0f : 2.0f;
5119 ImVec2 rect_pos = draw_pos + select_start_offset - draw_scroll;
5120 for (const ImWchar* p = text_selected_begin; p < text_selected_end; )
5121 {
5122 if (rect_pos.y > clip_rect.w + g.FontSize)
5123 break;
5124 if (rect_pos.y < clip_rect.y)
5125 {
5126 //p = (const ImWchar*)wmemchr((const wchar_t*)p, '\n', text_selected_end - p); // FIXME-OPT: Could use this when wchar_t are 16-bit
5127 //p = p ? p + 1 : text_selected_end;
5128 while (p < text_selected_end)
5129 if (*p++ == '\n')
5130 break;
5131 }
5132 else
5133 {
5134 ImVec2 rect_size = InputTextCalcTextSizeW(ctx: &g, text_begin: p, text_end: text_selected_end, remaining: &p, NULL, stop_on_new_line: true);
5135 if (rect_size.x <= 0.0f) rect_size.x = IM_TRUNC(g.Font->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines
5136 ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_size.x, bg_offy_dn));
5137 rect.ClipWith(r: clip_rect);
5138 if (rect.Overlaps(r: clip_rect))
5139 draw_window->DrawList->AddRectFilled(p_min: rect.Min, p_max: rect.Max, col: bg_color);
5140 }
5141 rect_pos.x = draw_pos.x - draw_scroll.x;
5142 rect_pos.y += g.FontSize;
5143 }
5144 }
5145
5146 // We test for 'buf_display_max_length' as a way to avoid some pathological cases (e.g. single-line 1 MB string) which would make ImDrawList crash.
5147 if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
5148 {
5149 ImU32 col = GetColorU32(idx: is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
5150 draw_window->DrawList->AddText(font: g.Font, font_size: g.FontSize, pos: draw_pos - draw_scroll, col, text_begin: buf_display, text_end: buf_display_end, wrap_width: 0.0f, cpu_fine_clip_rect: is_multiline ? NULL : &clip_rect);
5151 }
5152
5153 // Draw blinking cursor
5154 if (render_cursor)
5155 {
5156 state->CursorAnim += io.DeltaTime;
5157 bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f;
5158 ImVec2 cursor_screen_pos = ImTrunc(v: draw_pos + cursor_offset - draw_scroll);
5159 ImRect cursor_screen_rect(cursor_screen_pos.x, cursor_screen_pos.y - g.FontSize + 0.5f, cursor_screen_pos.x + 1.0f, cursor_screen_pos.y - 1.5f);
5160 if (cursor_is_visible && cursor_screen_rect.Overlaps(r: clip_rect))
5161 draw_window->DrawList->AddLine(p1: cursor_screen_rect.Min, p2: cursor_screen_rect.GetBL(), col: GetColorU32(idx: ImGuiCol_Text));
5162
5163 // Notify OS of text input position for advanced IME (-1 x offset so that Windows IME can cover our cursor. Bit of an extra nicety.)
5164 if (!is_readonly)
5165 {
5166 g.PlatformImeData.WantVisible = true;
5167 g.PlatformImeData.InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize);
5168 g.PlatformImeData.InputLineHeight = g.FontSize;
5169 }
5170 }
5171 }
5172 else
5173 {
5174 // Render text only (no selection, no cursor)
5175 if (is_multiline)
5176 text_size = ImVec2(inner_size.x, InputTextCalcTextLenAndLineCount(text_begin: buf_display, out_text_end: &buf_display_end) * g.FontSize); // We don't need width
5177 else if (!is_displaying_hint && g.ActiveId == id)
5178 buf_display_end = buf_display + state->CurLenA;
5179 else if (!is_displaying_hint)
5180 buf_display_end = buf_display + strlen(s: buf_display);
5181
5182 if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
5183 {
5184 ImU32 col = GetColorU32(idx: is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
5185 draw_window->DrawList->AddText(font: g.Font, font_size: g.FontSize, pos: draw_pos, col, text_begin: buf_display, text_end: buf_display_end, wrap_width: 0.0f, cpu_fine_clip_rect: is_multiline ? NULL : &clip_rect);
5186 }
5187 }
5188
5189 if (is_password && !is_displaying_hint)
5190 PopFont();
5191
5192 if (is_multiline)
5193 {
5194 // For focus requests to work on our multiline we need to ensure our child ItemAdd() call specifies the ImGuiItemFlags_Inputable (ref issue #4761)...
5195 Dummy(size: ImVec2(text_size.x, text_size.y + style.FramePadding.y));
5196 g.NextItemData.ItemFlags |= ImGuiItemFlags_Inputable | ImGuiItemFlags_NoTabStop;
5197 EndChild();
5198 item_data_backup.StatusFlags |= (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredWindow);
5199
5200 // ...and then we need to undo the group overriding last item data, which gets a bit messy as EndGroup() tries to forward scrollbar being active...
5201 // FIXME: This quite messy/tricky, should attempt to get rid of the child window.
5202 EndGroup();
5203 if (g.LastItemData.ID == 0)
5204 {
5205 g.LastItemData.ID = id;
5206 g.LastItemData.InFlags = item_data_backup.InFlags;
5207 g.LastItemData.StatusFlags = item_data_backup.StatusFlags;
5208 }
5209 }
5210
5211 // Log as text
5212 if (g.LogEnabled && (!is_password || is_displaying_hint))
5213 {
5214 LogSetNextTextDecoration(prefix: "{", suffix: "}");
5215 LogRenderedText(ref_pos: &draw_pos, text: buf_display, text_end: buf_display_end);
5216 }
5217
5218 if (label_size.x > 0)
5219 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), text: label);
5220
5221 if (value_changed && !(flags & ImGuiInputTextFlags_NoMarkEdited))
5222 MarkItemEdited(id);
5223
5224 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
5225 if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0)
5226 return validated;
5227 else
5228 return value_changed;
5229}
5230
5231void ImGui::DebugNodeInputTextState(ImGuiInputTextState* state)
5232{
5233#ifndef IMGUI_DISABLE_DEBUG_TOOLS
5234 ImGuiContext& g = *GImGui;
5235 ImStb::STB_TexteditState* stb_state = &state->Stb;
5236 ImStb::StbUndoState* undo_state = &stb_state->undostate;
5237 Text(fmt: "ID: 0x%08X, ActiveID: 0x%08X", state->ID, g.ActiveId);
5238 DebugLocateItemOnHover(target_id: state->ID);
5239 Text(fmt: "CurLenW: %d, CurLenA: %d, Cursor: %d, Selection: %d..%d", state->CurLenW, state->CurLenA, stb_state->cursor, stb_state->select_start, stb_state->select_end);
5240 Text(fmt: "has_preferred_x: %d (%.2f)", stb_state->has_preferred_x, stb_state->preferred_x);
5241 Text(fmt: "undo_point: %d, redo_point: %d, undo_char_point: %d, redo_char_point: %d", undo_state->undo_point, undo_state->redo_point, undo_state->undo_char_point, undo_state->redo_char_point);
5242 if (BeginChild(str_id: "undopoints", size: ImVec2(0.0f, GetTextLineHeight() * 10), child_flags: ImGuiChildFlags_Border | ImGuiChildFlags_ResizeY)) // Visualize undo state
5243 {
5244 PushStyleVar(idx: ImGuiStyleVar_ItemSpacing, val: ImVec2(0, 0));
5245 for (int n = 0; n < IMSTB_TEXTEDIT_UNDOSTATECOUNT; n++)
5246 {
5247 ImStb::StbUndoRecord* undo_rec = &undo_state->undo_rec[n];
5248 const char undo_rec_type = (n < undo_state->undo_point) ? 'u' : (n >= undo_state->redo_point) ? 'r' : ' ';
5249 if (undo_rec_type == ' ')
5250 BeginDisabled();
5251 char buf[64] = "";
5252 if (undo_rec_type != ' ' && undo_rec->char_storage != -1)
5253 ImTextStrToUtf8(out_buf: buf, IM_ARRAYSIZE(buf), in_text: undo_state->undo_char + undo_rec->char_storage, in_text_end: undo_state->undo_char + undo_rec->char_storage + undo_rec->insert_length);
5254 Text(fmt: "%c [%02d] where %03d, insert %03d, delete %03d, char_storage %03d \"%s\"",
5255 undo_rec_type, n, undo_rec->where, undo_rec->insert_length, undo_rec->delete_length, undo_rec->char_storage, buf);
5256 if (undo_rec_type == ' ')
5257 EndDisabled();
5258 }
5259 PopStyleVar();
5260 }
5261 EndChild();
5262#else
5263 IM_UNUSED(state);
5264#endif
5265}
5266
5267//-------------------------------------------------------------------------
5268// [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc.
5269//-------------------------------------------------------------------------
5270// - ColorEdit3()
5271// - ColorEdit4()
5272// - ColorPicker3()
5273// - RenderColorRectWithAlphaCheckerboard() [Internal]
5274// - ColorPicker4()
5275// - ColorButton()
5276// - SetColorEditOptions()
5277// - ColorTooltip() [Internal]
5278// - ColorEditOptionsPopup() [Internal]
5279// - ColorPickerOptionsPopup() [Internal]
5280//-------------------------------------------------------------------------
5281
5282bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags)
5283{
5284 return ColorEdit4(label, col, flags: flags | ImGuiColorEditFlags_NoAlpha);
5285}
5286
5287static void ColorEditRestoreH(const float* col, float* H)
5288{
5289 ImGuiContext& g = *GImGui;
5290 IM_ASSERT(g.ColorEditCurrentID != 0);
5291 if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(in: ImVec4(col[0], col[1], col[2], 0)))
5292 return;
5293 *H = g.ColorEditSavedHue;
5294}
5295
5296// ColorEdit supports RGB and HSV inputs. In case of RGB input resulting color may have undefined hue and/or saturation.
5297// Since widget displays both RGB and HSV values we must preserve hue and saturation to prevent these values resetting.
5298static void ColorEditRestoreHS(const float* col, float* H, float* S, float* V)
5299{
5300 ImGuiContext& g = *GImGui;
5301 IM_ASSERT(g.ColorEditCurrentID != 0);
5302 if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(in: ImVec4(col[0], col[1], col[2], 0)))
5303 return;
5304
5305 // When S == 0, H is undefined.
5306 // When H == 1 it wraps around to 0.
5307 if (*S == 0.0f || (*H == 0.0f && g.ColorEditSavedHue == 1))
5308 *H = g.ColorEditSavedHue;
5309
5310 // When V == 0, S is undefined.
5311 if (*V == 0.0f)
5312 *S = g.ColorEditSavedSat;
5313}
5314
5315// Edit colors components (each component in 0.0f..1.0f range).
5316// See enum ImGuiColorEditFlags_ for available options. e.g. Only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
5317// With typical options: Left-click on color square to open color picker. Right-click to open option menu. CTRL-Click over input fields to edit them and TAB to go to next item.
5318bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags)
5319{
5320 ImGuiWindow* window = GetCurrentWindow();
5321 if (window->SkipItems)
5322 return false;
5323
5324 ImGuiContext& g = *GImGui;
5325 const ImGuiStyle& style = g.Style;
5326 const float square_sz = GetFrameHeight();
5327 const char* label_display_end = FindRenderedTextEnd(text: label);
5328 float w_full = CalcItemWidth();
5329 g.NextItemData.ClearFlags();
5330
5331 BeginGroup();
5332 PushID(str_id: label);
5333 const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0);
5334 if (set_current_color_edit_id)
5335 g.ColorEditCurrentID = window->IDStack.back();
5336
5337 // If we're not showing any slider there's no point in doing any HSV conversions
5338 const ImGuiColorEditFlags flags_untouched = flags;
5339 if (flags & ImGuiColorEditFlags_NoInputs)
5340 flags = (flags & (~ImGuiColorEditFlags_DisplayMask_)) | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_NoOptions;
5341
5342 // Context menu: display and modify options (before defaults are applied)
5343 if (!(flags & ImGuiColorEditFlags_NoOptions))
5344 ColorEditOptionsPopup(col, flags);
5345
5346 // Read stored options
5347 if (!(flags & ImGuiColorEditFlags_DisplayMask_))
5348 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DisplayMask_);
5349 if (!(flags & ImGuiColorEditFlags_DataTypeMask_))
5350 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DataTypeMask_);
5351 if (!(flags & ImGuiColorEditFlags_PickerMask_))
5352 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_);
5353 if (!(flags & ImGuiColorEditFlags_InputMask_))
5354 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_InputMask_);
5355 flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_));
5356 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check that only 1 is selected
5357 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
5358
5359 const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0;
5360 const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0;
5361 const int components = alpha ? 4 : 3;
5362 const float w_button = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x);
5363 const float w_inputs = ImMax(lhs: w_full - w_button, rhs: 1.0f);
5364 w_full = w_inputs + w_button;
5365
5366 // Convert to the formats we need
5367 float f[4] = { col[0], col[1], col[2], alpha ? col[3] : 1.0f };
5368 if ((flags & ImGuiColorEditFlags_InputHSV) && (flags & ImGuiColorEditFlags_DisplayRGB))
5369 ColorConvertHSVtoRGB(h: f[0], s: f[1], v: f[2], out_r&: f[0], out_g&: f[1], out_b&: f[2]);
5370 else if ((flags & ImGuiColorEditFlags_InputRGB) && (flags & ImGuiColorEditFlags_DisplayHSV))
5371 {
5372 // Hue is lost when converting from grayscale rgb (saturation=0). Restore it.
5373 ColorConvertRGBtoHSV(r: f[0], g: f[1], b: f[2], out_h&: f[0], out_s&: f[1], out_v&: f[2]);
5374 ColorEditRestoreHS(col, H: &f[0], S: &f[1], V: &f[2]);
5375 }
5376 int i[4] = { IM_F32_TO_INT8_UNBOUND(f[0]), IM_F32_TO_INT8_UNBOUND(f[1]), IM_F32_TO_INT8_UNBOUND(f[2]), IM_F32_TO_INT8_UNBOUND(f[3]) };
5377
5378 bool value_changed = false;
5379 bool value_changed_as_float = false;
5380
5381 const ImVec2 pos = window->DC.CursorPos;
5382 const float inputs_offset_x = (style.ColorButtonPosition == ImGuiDir_Left) ? w_button : 0.0f;
5383 window->DC.CursorPos.x = pos.x + inputs_offset_x;
5384
5385 if ((flags & (ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5386 {
5387 // RGB/HSV 0..255 Sliders
5388 const float w_items = w_inputs - style.ItemInnerSpacing.x * (components - 1);
5389
5390 const bool hide_prefix = (IM_TRUNC(w_items / components) <= CalcTextSize(text: (flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x);
5391 static const char* ids[4] = { "##X", "##Y", "##Z", "##W" };
5392 static const char* fmt_table_int[3][4] =
5393 {
5394 { "%3d", "%3d", "%3d", "%3d" }, // Short display
5395 { "R:%3d", "G:%3d", "B:%3d", "A:%3d" }, // Long display for RGBA
5396 { "H:%3d", "S:%3d", "V:%3d", "A:%3d" } // Long display for HSVA
5397 };
5398 static const char* fmt_table_float[3][4] =
5399 {
5400 { "%0.3f", "%0.3f", "%0.3f", "%0.3f" }, // Short display
5401 { "R:%0.3f", "G:%0.3f", "B:%0.3f", "A:%0.3f" }, // Long display for RGBA
5402 { "H:%0.3f", "S:%0.3f", "V:%0.3f", "A:%0.3f" } // Long display for HSVA
5403 };
5404 const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_DisplayHSV) ? 2 : 1;
5405
5406 float prev_split = 0.0f;
5407 for (int n = 0; n < components; n++)
5408 {
5409 if (n > 0)
5410 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
5411 float next_split = IM_TRUNC(w_items * (n + 1) / components);
5412 SetNextItemWidth(ImMax(lhs: next_split - prev_split, rhs: 1.0f));
5413 prev_split = next_split;
5414
5415 // FIXME: When ImGuiColorEditFlags_HDR flag is passed HS values snap in weird ways when SV values go below 0.
5416 if (flags & ImGuiColorEditFlags_Float)
5417 {
5418 value_changed |= DragFloat(label: ids[n], v: &f[n], v_speed: 1.0f / 255.0f, v_min: 0.0f, v_max: hdr ? 0.0f : 1.0f, format: fmt_table_float[fmt_idx][n]);
5419 value_changed_as_float |= value_changed;
5420 }
5421 else
5422 {
5423 value_changed |= DragInt(label: ids[n], v: &i[n], v_speed: 1.0f, v_min: 0, v_max: hdr ? 0 : 255, format: fmt_table_int[fmt_idx][n]);
5424 }
5425 if (!(flags & ImGuiColorEditFlags_NoOptions))
5426 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5427 }
5428 }
5429 else if ((flags & ImGuiColorEditFlags_DisplayHex) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5430 {
5431 // RGB Hexadecimal Input
5432 char buf[64];
5433 if (alpha)
5434 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "#%02X%02X%02X%02X", ImClamp(v: i[0], mn: 0, mx: 255), ImClamp(v: i[1], mn: 0, mx: 255), ImClamp(v: i[2], mn: 0, mx: 255), ImClamp(v: i[3], mn: 0, mx: 255));
5435 else
5436 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "#%02X%02X%02X", ImClamp(v: i[0], mn: 0, mx: 255), ImClamp(v: i[1], mn: 0, mx: 255), ImClamp(v: i[2], mn: 0, mx: 255));
5437 SetNextItemWidth(w_inputs);
5438 if (InputText(label: "##Text", buf, IM_ARRAYSIZE(buf), flags: ImGuiInputTextFlags_CharsUppercase))
5439 {
5440 value_changed = true;
5441 char* p = buf;
5442 while (*p == '#' || ImCharIsBlankA(c: *p))
5443 p++;
5444 i[0] = i[1] = i[2] = 0;
5445 i[3] = 0xFF; // alpha default to 255 is not parsed by scanf (e.g. inputting #FFFFFF omitting alpha)
5446 int r;
5447 if (alpha)
5448 r = sscanf(s: p, format: "%02X%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2], (unsigned int*)&i[3]); // Treat at unsigned (%X is unsigned)
5449 else
5450 r = sscanf(s: p, format: "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]);
5451 IM_UNUSED(r); // Fixes C6031: Return value ignored: 'sscanf'.
5452 }
5453 if (!(flags & ImGuiColorEditFlags_NoOptions))
5454 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5455 }
5456
5457 ImGuiWindow* picker_active_window = NULL;
5458 if (!(flags & ImGuiColorEditFlags_NoSmallPreview))
5459 {
5460 const float button_offset_x = ((flags & ImGuiColorEditFlags_NoInputs) || (style.ColorButtonPosition == ImGuiDir_Left)) ? 0.0f : w_inputs + style.ItemInnerSpacing.x;
5461 window->DC.CursorPos = ImVec2(pos.x + button_offset_x, pos.y);
5462
5463 const ImVec4 col_v4(col[0], col[1], col[2], alpha ? col[3] : 1.0f);
5464 if (ColorButton(desc_id: "##ColorButton", col: col_v4, flags))
5465 {
5466 if (!(flags & ImGuiColorEditFlags_NoPicker))
5467 {
5468 // Store current color and open a picker
5469 g.ColorPickerRef = col_v4;
5470 OpenPopup(str_id: "picker");
5471 SetNextWindowPos(pos: g.LastItemData.Rect.GetBL() + ImVec2(0.0f, style.ItemSpacing.y));
5472 }
5473 }
5474 if (!(flags & ImGuiColorEditFlags_NoOptions))
5475 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5476
5477 if (BeginPopup(str_id: "picker"))
5478 {
5479 if (g.CurrentWindow->BeginCount == 1)
5480 {
5481 picker_active_window = g.CurrentWindow;
5482 if (label != label_display_end)
5483 {
5484 TextEx(text: label, text_end: label_display_end);
5485 Spacing();
5486 }
5487 ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar;
5488 ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf;
5489 SetNextItemWidth(square_sz * 12.0f); // Use 256 + bar sizes?
5490 value_changed |= ColorPicker4(label: "##picker", col, flags: picker_flags, ref_col: &g.ColorPickerRef.x);
5491 }
5492 EndPopup();
5493 }
5494 }
5495
5496 if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel))
5497 {
5498 // Position not necessarily next to last submitted button (e.g. if style.ColorButtonPosition == ImGuiDir_Left),
5499 // but we need to use SameLine() to setup baseline correctly. Might want to refactor SameLine() to simplify this.
5500 SameLine(offset_from_start_x: 0.0f, spacing: style.ItemInnerSpacing.x);
5501 window->DC.CursorPos.x = pos.x + ((flags & ImGuiColorEditFlags_NoInputs) ? w_button : w_full + style.ItemInnerSpacing.x);
5502 TextEx(text: label, text_end: label_display_end);
5503 }
5504
5505 // Convert back
5506 if (value_changed && picker_active_window == NULL)
5507 {
5508 if (!value_changed_as_float)
5509 for (int n = 0; n < 4; n++)
5510 f[n] = i[n] / 255.0f;
5511 if ((flags & ImGuiColorEditFlags_DisplayHSV) && (flags & ImGuiColorEditFlags_InputRGB))
5512 {
5513 g.ColorEditSavedHue = f[0];
5514 g.ColorEditSavedSat = f[1];
5515 ColorConvertHSVtoRGB(h: f[0], s: f[1], v: f[2], out_r&: f[0], out_g&: f[1], out_b&: f[2]);
5516 g.ColorEditSavedID = g.ColorEditCurrentID;
5517 g.ColorEditSavedColor = ColorConvertFloat4ToU32(in: ImVec4(f[0], f[1], f[2], 0));
5518 }
5519 if ((flags & ImGuiColorEditFlags_DisplayRGB) && (flags & ImGuiColorEditFlags_InputHSV))
5520 ColorConvertRGBtoHSV(r: f[0], g: f[1], b: f[2], out_h&: f[0], out_s&: f[1], out_v&: f[2]);
5521
5522 col[0] = f[0];
5523 col[1] = f[1];
5524 col[2] = f[2];
5525 if (alpha)
5526 col[3] = f[3];
5527 }
5528
5529 if (set_current_color_edit_id)
5530 g.ColorEditCurrentID = 0;
5531 PopID();
5532 EndGroup();
5533
5534 // Drag and Drop Target
5535 // NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test.
5536 if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget())
5537 {
5538 bool accepted_drag_drop = false;
5539 if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F))
5540 {
5541 memcpy(dest: (float*)col, src: payload->Data, n: sizeof(float) * 3); // Preserve alpha if any //-V512 //-V1086
5542 value_changed = accepted_drag_drop = true;
5543 }
5544 if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F))
5545 {
5546 memcpy(dest: (float*)col, src: payload->Data, n: sizeof(float) * components);
5547 value_changed = accepted_drag_drop = true;
5548 }
5549
5550 // Drag-drop payloads are always RGB
5551 if (accepted_drag_drop && (flags & ImGuiColorEditFlags_InputHSV))
5552 ColorConvertRGBtoHSV(r: col[0], g: col[1], b: col[2], out_h&: col[0], out_s&: col[1], out_v&: col[2]);
5553 EndDragDropTarget();
5554 }
5555
5556 // When picker is being actively used, use its active id so IsItemActive() will function on ColorEdit4().
5557 if (picker_active_window && g.ActiveId != 0 && g.ActiveIdWindow == picker_active_window)
5558 g.LastItemData.ID = g.ActiveId;
5559
5560 if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
5561 MarkItemEdited(id: g.LastItemData.ID);
5562
5563 return value_changed;
5564}
5565
5566bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags)
5567{
5568 float col4[4] = { col[0], col[1], col[2], 1.0f };
5569 if (!ColorPicker4(label, col: col4, flags: flags | ImGuiColorEditFlags_NoAlpha))
5570 return false;
5571 col[0] = col4[0]; col[1] = col4[1]; col[2] = col4[2];
5572 return true;
5573}
5574
5575// Helper for ColorPicker4()
5576static void RenderArrowsForVerticalBar(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w, float alpha)
5577{
5578 ImU32 alpha8 = IM_F32_TO_INT8_SAT(alpha);
5579 ImGui::RenderArrowPointingAt(draw_list, pos: ImVec2(pos.x + half_sz.x + 1, pos.y), half_sz: ImVec2(half_sz.x + 2, half_sz.y + 1), direction: ImGuiDir_Right, IM_COL32(0,0,0,alpha8));
5580 ImGui::RenderArrowPointingAt(draw_list, pos: ImVec2(pos.x + half_sz.x, pos.y), half_sz, direction: ImGuiDir_Right, IM_COL32(255,255,255,alpha8));
5581 ImGui::RenderArrowPointingAt(draw_list, pos: ImVec2(pos.x + bar_w - half_sz.x - 1, pos.y), half_sz: ImVec2(half_sz.x + 2, half_sz.y + 1), direction: ImGuiDir_Left, IM_COL32(0,0,0,alpha8));
5582 ImGui::RenderArrowPointingAt(draw_list, pos: ImVec2(pos.x + bar_w - half_sz.x, pos.y), half_sz, direction: ImGuiDir_Left, IM_COL32(255,255,255,alpha8));
5583}
5584
5585// Note: ColorPicker4() only accesses 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
5586// (In C++ the 'float col[4]' notation for a function argument is equivalent to 'float* col', we only specify a size to facilitate understanding of the code.)
5587// FIXME: we adjust the big color square height based on item width, which may cause a flickering feedback loop (if automatic height makes a vertical scrollbar appears, affecting automatic width..)
5588// FIXME: this is trying to be aware of style.Alpha but not fully correct. Also, the color wheel will have overlapping glitches with (style.Alpha < 1.0)
5589bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags, const float* ref_col)
5590{
5591 ImGuiContext& g = *GImGui;
5592 ImGuiWindow* window = GetCurrentWindow();
5593 if (window->SkipItems)
5594 return false;
5595
5596 ImDrawList* draw_list = window->DrawList;
5597 ImGuiStyle& style = g.Style;
5598 ImGuiIO& io = g.IO;
5599
5600 const float width = CalcItemWidth();
5601 const bool is_readonly = ((g.NextItemData.ItemFlags | g.CurrentItemFlags) & ImGuiItemFlags_ReadOnly) != 0;
5602 g.NextItemData.ClearFlags();
5603
5604 PushID(str_id: label);
5605 const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0);
5606 if (set_current_color_edit_id)
5607 g.ColorEditCurrentID = window->IDStack.back();
5608 BeginGroup();
5609
5610 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5611 flags |= ImGuiColorEditFlags_NoSmallPreview;
5612
5613 // Context menu: display and store options.
5614 if (!(flags & ImGuiColorEditFlags_NoOptions))
5615 ColorPickerOptionsPopup(ref_col: col, flags);
5616
5617 // Read stored options
5618 if (!(flags & ImGuiColorEditFlags_PickerMask_))
5619 flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_PickerMask_;
5620 if (!(flags & ImGuiColorEditFlags_InputMask_))
5621 flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_InputMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_InputMask_;
5622 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check that only 1 is selected
5623 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
5624 if (!(flags & ImGuiColorEditFlags_NoOptions))
5625 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_AlphaBar);
5626
5627 // Setup
5628 int components = (flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4;
5629 bool alpha_bar = (flags & ImGuiColorEditFlags_AlphaBar) && !(flags & ImGuiColorEditFlags_NoAlpha);
5630 ImVec2 picker_pos = window->DC.CursorPos;
5631 float square_sz = GetFrameHeight();
5632 float bars_width = square_sz; // Arbitrary smallish width of Hue/Alpha picking bars
5633 float sv_picker_size = ImMax(lhs: bars_width * 1, rhs: width - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box
5634 float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x;
5635 float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x;
5636 float bars_triangles_half_sz = IM_TRUNC(bars_width * 0.20f);
5637
5638 float backup_initial_col[4];
5639 memcpy(dest: backup_initial_col, src: col, n: components * sizeof(float));
5640
5641 float wheel_thickness = sv_picker_size * 0.08f;
5642 float wheel_r_outer = sv_picker_size * 0.50f;
5643 float wheel_r_inner = wheel_r_outer - wheel_thickness;
5644 ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width)*0.5f, picker_pos.y + sv_picker_size * 0.5f);
5645
5646 // Note: the triangle is displayed rotated with triangle_pa pointing to Hue, but most coordinates stays unrotated for logic.
5647 float triangle_r = wheel_r_inner - (int)(sv_picker_size * 0.027f);
5648 ImVec2 triangle_pa = ImVec2(triangle_r, 0.0f); // Hue point.
5649 ImVec2 triangle_pb = ImVec2(triangle_r * -0.5f, triangle_r * -0.866025f); // Black point.
5650 ImVec2 triangle_pc = ImVec2(triangle_r * -0.5f, triangle_r * +0.866025f); // White point.
5651
5652 float H = col[0], S = col[1], V = col[2];
5653 float R = col[0], G = col[1], B = col[2];
5654 if (flags & ImGuiColorEditFlags_InputRGB)
5655 {
5656 // Hue is lost when converting from grayscale rgb (saturation=0). Restore it.
5657 ColorConvertRGBtoHSV(r: R, g: G, b: B, out_h&: H, out_s&: S, out_v&: V);
5658 ColorEditRestoreHS(col, H: &H, S: &S, V: &V);
5659 }
5660 else if (flags & ImGuiColorEditFlags_InputHSV)
5661 {
5662 ColorConvertHSVtoRGB(h: H, s: S, v: V, out_r&: R, out_g&: G, out_b&: B);
5663 }
5664
5665 bool value_changed = false, value_changed_h = false, value_changed_sv = false;
5666
5667 PushItemFlag(option: ImGuiItemFlags_NoNav, enabled: true);
5668 if (flags & ImGuiColorEditFlags_PickerHueWheel)
5669 {
5670 // Hue wheel + SV triangle logic
5671 InvisibleButton(str_id: "hsv", size_arg: ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size));
5672 if (IsItemActive() && !is_readonly)
5673 {
5674 ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center;
5675 ImVec2 current_off = g.IO.MousePos - wheel_center;
5676 float initial_dist2 = ImLengthSqr(lhs: initial_off);
5677 if (initial_dist2 >= (wheel_r_inner - 1) * (wheel_r_inner - 1) && initial_dist2 <= (wheel_r_outer + 1) * (wheel_r_outer + 1))
5678 {
5679 // Interactive with Hue wheel
5680 H = ImAtan2(current_off.y, current_off.x) / IM_PI * 0.5f;
5681 if (H < 0.0f)
5682 H += 1.0f;
5683 value_changed = value_changed_h = true;
5684 }
5685 float cos_hue_angle = ImCos(-H * 2.0f * IM_PI);
5686 float sin_hue_angle = ImSin(-H * 2.0f * IM_PI);
5687 if (ImTriangleContainsPoint(a: triangle_pa, b: triangle_pb, c: triangle_pc, p: ImRotate(v: initial_off, cos_a: cos_hue_angle, sin_a: sin_hue_angle)))
5688 {
5689 // Interacting with SV triangle
5690 ImVec2 current_off_unrotated = ImRotate(v: current_off, cos_a: cos_hue_angle, sin_a: sin_hue_angle);
5691 if (!ImTriangleContainsPoint(a: triangle_pa, b: triangle_pb, c: triangle_pc, p: current_off_unrotated))
5692 current_off_unrotated = ImTriangleClosestPoint(a: triangle_pa, b: triangle_pb, c: triangle_pc, p: current_off_unrotated);
5693 float uu, vv, ww;
5694 ImTriangleBarycentricCoords(a: triangle_pa, b: triangle_pb, c: triangle_pc, p: current_off_unrotated, out_u&: uu, out_v&: vv, out_w&: ww);
5695 V = ImClamp(v: 1.0f - vv, mn: 0.0001f, mx: 1.0f);
5696 S = ImClamp(v: uu / V, mn: 0.0001f, mx: 1.0f);
5697 value_changed = value_changed_sv = true;
5698 }
5699 }
5700 if (!(flags & ImGuiColorEditFlags_NoOptions))
5701 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5702 }
5703 else if (flags & ImGuiColorEditFlags_PickerHueBar)
5704 {
5705 // SV rectangle logic
5706 InvisibleButton(str_id: "sv", size_arg: ImVec2(sv_picker_size, sv_picker_size));
5707 if (IsItemActive() && !is_readonly)
5708 {
5709 S = ImSaturate(f: (io.MousePos.x - picker_pos.x) / (sv_picker_size - 1));
5710 V = 1.0f - ImSaturate(f: (io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5711 ColorEditRestoreH(col, H: &H); // Greatly reduces hue jitter and reset to 0 when hue == 255 and color is rapidly modified using SV square.
5712 value_changed = value_changed_sv = true;
5713 }
5714 if (!(flags & ImGuiColorEditFlags_NoOptions))
5715 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5716
5717 // Hue bar logic
5718 SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y));
5719 InvisibleButton(str_id: "hue", size_arg: ImVec2(bars_width, sv_picker_size));
5720 if (IsItemActive() && !is_readonly)
5721 {
5722 H = ImSaturate(f: (io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5723 value_changed = value_changed_h = true;
5724 }
5725 }
5726
5727 // Alpha bar logic
5728 if (alpha_bar)
5729 {
5730 SetCursorScreenPos(ImVec2(bar1_pos_x, picker_pos.y));
5731 InvisibleButton(str_id: "alpha", size_arg: ImVec2(bars_width, sv_picker_size));
5732 if (IsItemActive())
5733 {
5734 col[3] = 1.0f - ImSaturate(f: (io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5735 value_changed = true;
5736 }
5737 }
5738 PopItemFlag(); // ImGuiItemFlags_NoNav
5739
5740 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5741 {
5742 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
5743 BeginGroup();
5744 }
5745
5746 if (!(flags & ImGuiColorEditFlags_NoLabel))
5747 {
5748 const char* label_display_end = FindRenderedTextEnd(text: label);
5749 if (label != label_display_end)
5750 {
5751 if ((flags & ImGuiColorEditFlags_NoSidePreview))
5752 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
5753 TextEx(text: label, text_end: label_display_end);
5754 }
5755 }
5756
5757 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5758 {
5759 PushItemFlag(option: ImGuiItemFlags_NoNavDefaultFocus, enabled: true);
5760 ImVec4 col_v4(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
5761 if ((flags & ImGuiColorEditFlags_NoLabel))
5762 Text(fmt: "Current");
5763
5764 ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf | ImGuiColorEditFlags_NoTooltip;
5765 ColorButton(desc_id: "##current", col: col_v4, flags: (flags & sub_flags_to_forward), size: ImVec2(square_sz * 3, square_sz * 2));
5766 if (ref_col != NULL)
5767 {
5768 Text(fmt: "Original");
5769 ImVec4 ref_col_v4(ref_col[0], ref_col[1], ref_col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : ref_col[3]);
5770 if (ColorButton(desc_id: "##original", col: ref_col_v4, flags: (flags & sub_flags_to_forward), size: ImVec2(square_sz * 3, square_sz * 2)))
5771 {
5772 memcpy(dest: col, src: ref_col, n: components * sizeof(float));
5773 value_changed = true;
5774 }
5775 }
5776 PopItemFlag();
5777 EndGroup();
5778 }
5779
5780 // Convert back color to RGB
5781 if (value_changed_h || value_changed_sv)
5782 {
5783 if (flags & ImGuiColorEditFlags_InputRGB)
5784 {
5785 ColorConvertHSVtoRGB(h: H, s: S, v: V, out_r&: col[0], out_g&: col[1], out_b&: col[2]);
5786 g.ColorEditSavedHue = H;
5787 g.ColorEditSavedSat = S;
5788 g.ColorEditSavedID = g.ColorEditCurrentID;
5789 g.ColorEditSavedColor = ColorConvertFloat4ToU32(in: ImVec4(col[0], col[1], col[2], 0));
5790 }
5791 else if (flags & ImGuiColorEditFlags_InputHSV)
5792 {
5793 col[0] = H;
5794 col[1] = S;
5795 col[2] = V;
5796 }
5797 }
5798
5799 // R,G,B and H,S,V slider color editor
5800 bool value_changed_fix_hue_wrap = false;
5801 if ((flags & ImGuiColorEditFlags_NoInputs) == 0)
5802 {
5803 PushItemWidth(item_width: (alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x);
5804 ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoSmallPreview | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf;
5805 ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker;
5806 if (flags & ImGuiColorEditFlags_DisplayRGB || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
5807 if (ColorEdit4(label: "##rgb", col, flags: sub_flags | ImGuiColorEditFlags_DisplayRGB))
5808 {
5809 // FIXME: Hackily differentiating using the DragInt (ActiveId != 0 && !ActiveIdAllowOverlap) vs. using the InputText or DropTarget.
5810 // For the later we don't want to run the hue-wrap canceling code. If you are well versed in HSV picker please provide your input! (See #2050)
5811 value_changed_fix_hue_wrap = (g.ActiveId != 0 && !g.ActiveIdAllowOverlap);
5812 value_changed = true;
5813 }
5814 if (flags & ImGuiColorEditFlags_DisplayHSV || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
5815 value_changed |= ColorEdit4(label: "##hsv", col, flags: sub_flags | ImGuiColorEditFlags_DisplayHSV);
5816 if (flags & ImGuiColorEditFlags_DisplayHex || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
5817 value_changed |= ColorEdit4(label: "##hex", col, flags: sub_flags | ImGuiColorEditFlags_DisplayHex);
5818 PopItemWidth();
5819 }
5820
5821 // Try to cancel hue wrap (after ColorEdit4 call), if any
5822 if (value_changed_fix_hue_wrap && (flags & ImGuiColorEditFlags_InputRGB))
5823 {
5824 float new_H, new_S, new_V;
5825 ColorConvertRGBtoHSV(r: col[0], g: col[1], b: col[2], out_h&: new_H, out_s&: new_S, out_v&: new_V);
5826 if (new_H <= 0 && H > 0)
5827 {
5828 if (new_V <= 0 && V != new_V)
5829 ColorConvertHSVtoRGB(h: H, s: S, v: new_V <= 0 ? V * 0.5f : new_V, out_r&: col[0], out_g&: col[1], out_b&: col[2]);
5830 else if (new_S <= 0)
5831 ColorConvertHSVtoRGB(h: H, s: new_S <= 0 ? S * 0.5f : new_S, v: new_V, out_r&: col[0], out_g&: col[1], out_b&: col[2]);
5832 }
5833 }
5834
5835 if (value_changed)
5836 {
5837 if (flags & ImGuiColorEditFlags_InputRGB)
5838 {
5839 R = col[0];
5840 G = col[1];
5841 B = col[2];
5842 ColorConvertRGBtoHSV(r: R, g: G, b: B, out_h&: H, out_s&: S, out_v&: V);
5843 ColorEditRestoreHS(col, H: &H, S: &S, V: &V); // Fix local Hue as display below will use it immediately.
5844 }
5845 else if (flags & ImGuiColorEditFlags_InputHSV)
5846 {
5847 H = col[0];
5848 S = col[1];
5849 V = col[2];
5850 ColorConvertHSVtoRGB(h: H, s: S, v: V, out_r&: R, out_g&: G, out_b&: B);
5851 }
5852 }
5853
5854 const int style_alpha8 = IM_F32_TO_INT8_SAT(style.Alpha);
5855 const ImU32 col_black = IM_COL32(0,0,0,style_alpha8);
5856 const ImU32 col_white = IM_COL32(255,255,255,style_alpha8);
5857 const ImU32 col_midgrey = IM_COL32(128,128,128,style_alpha8);
5858 const ImU32 col_hues[6 + 1] = { IM_COL32(255,0,0,style_alpha8), IM_COL32(255,255,0,style_alpha8), IM_COL32(0,255,0,style_alpha8), IM_COL32(0,255,255,style_alpha8), IM_COL32(0,0,255,style_alpha8), IM_COL32(255,0,255,style_alpha8), IM_COL32(255,0,0,style_alpha8) };
5859
5860 ImVec4 hue_color_f(1, 1, 1, style.Alpha); ColorConvertHSVtoRGB(h: H, s: 1, v: 1, out_r&: hue_color_f.x, out_g&: hue_color_f.y, out_b&: hue_color_f.z);
5861 ImU32 hue_color32 = ColorConvertFloat4ToU32(in: hue_color_f);
5862 ImU32 user_col32_striped_of_alpha = ColorConvertFloat4ToU32(in: ImVec4(R, G, B, style.Alpha)); // Important: this is still including the main rendering/style alpha!!
5863
5864 ImVec2 sv_cursor_pos;
5865
5866 if (flags & ImGuiColorEditFlags_PickerHueWheel)
5867 {
5868 // Render Hue Wheel
5869 const float aeps = 0.5f / wheel_r_outer; // Half a pixel arc length in radians (2pi cancels out).
5870 const int segment_per_arc = ImMax(lhs: 4, rhs: (int)wheel_r_outer / 12);
5871 for (int n = 0; n < 6; n++)
5872 {
5873 const float a0 = (n) /6.0f * 2.0f * IM_PI - aeps;
5874 const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps;
5875 const int vert_start_idx = draw_list->VtxBuffer.Size;
5876 draw_list->PathArcTo(center: wheel_center, radius: (wheel_r_inner + wheel_r_outer)*0.5f, a_min: a0, a_max: a1, num_segments: segment_per_arc);
5877 draw_list->PathStroke(col: col_white, flags: 0, thickness: wheel_thickness);
5878 const int vert_end_idx = draw_list->VtxBuffer.Size;
5879
5880 // Paint colors over existing vertices
5881 ImVec2 gradient_p0(wheel_center.x + ImCos(a0) * wheel_r_inner, wheel_center.y + ImSin(a0) * wheel_r_inner);
5882 ImVec2 gradient_p1(wheel_center.x + ImCos(a1) * wheel_r_inner, wheel_center.y + ImSin(a1) * wheel_r_inner);
5883 ShadeVertsLinearColorGradientKeepAlpha(draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, col0: col_hues[n], col1: col_hues[n + 1]);
5884 }
5885
5886 // Render Cursor + preview on Hue Wheel
5887 float cos_hue_angle = ImCos(H * 2.0f * IM_PI);
5888 float sin_hue_angle = ImSin(H * 2.0f * IM_PI);
5889 ImVec2 hue_cursor_pos(wheel_center.x + cos_hue_angle * (wheel_r_inner + wheel_r_outer) * 0.5f, wheel_center.y + sin_hue_angle * (wheel_r_inner + wheel_r_outer) * 0.5f);
5890 float hue_cursor_rad = value_changed_h ? wheel_thickness * 0.65f : wheel_thickness * 0.55f;
5891 int hue_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(radius: hue_cursor_rad); // Lock segment count so the +1 one matches others.
5892 draw_list->AddCircleFilled(center: hue_cursor_pos, radius: hue_cursor_rad, col: hue_color32, num_segments: hue_cursor_segments);
5893 draw_list->AddCircle(center: hue_cursor_pos, radius: hue_cursor_rad + 1, col: col_midgrey, num_segments: hue_cursor_segments);
5894 draw_list->AddCircle(center: hue_cursor_pos, radius: hue_cursor_rad, col: col_white, num_segments: hue_cursor_segments);
5895
5896 // Render SV triangle (rotated according to hue)
5897 ImVec2 tra = wheel_center + ImRotate(v: triangle_pa, cos_a: cos_hue_angle, sin_a: sin_hue_angle);
5898 ImVec2 trb = wheel_center + ImRotate(v: triangle_pb, cos_a: cos_hue_angle, sin_a: sin_hue_angle);
5899 ImVec2 trc = wheel_center + ImRotate(v: triangle_pc, cos_a: cos_hue_angle, sin_a: sin_hue_angle);
5900 ImVec2 uv_white = GetFontTexUvWhitePixel();
5901 draw_list->PrimReserve(idx_count: 3, vtx_count: 3);
5902 draw_list->PrimVtx(pos: tra, uv: uv_white, col: hue_color32);
5903 draw_list->PrimVtx(pos: trb, uv: uv_white, col: col_black);
5904 draw_list->PrimVtx(pos: trc, uv: uv_white, col: col_white);
5905 draw_list->AddTriangle(p1: tra, p2: trb, p3: trc, col: col_midgrey, thickness: 1.5f);
5906 sv_cursor_pos = ImLerp(a: ImLerp(a: trc, b: tra, t: ImSaturate(f: S)), b: trb, t: ImSaturate(f: 1 - V));
5907 }
5908 else if (flags & ImGuiColorEditFlags_PickerHueBar)
5909 {
5910 // Render SV Square
5911 draw_list->AddRectFilledMultiColor(p_min: picker_pos, p_max: picker_pos + ImVec2(sv_picker_size, sv_picker_size), col_upr_left: col_white, col_upr_right: hue_color32, col_bot_right: hue_color32, col_bot_left: col_white);
5912 draw_list->AddRectFilledMultiColor(p_min: picker_pos, p_max: picker_pos + ImVec2(sv_picker_size, sv_picker_size), col_upr_left: 0, col_upr_right: 0, col_bot_right: col_black, col_bot_left: col_black);
5913 RenderFrameBorder(p_min: picker_pos, p_max: picker_pos + ImVec2(sv_picker_size, sv_picker_size), rounding: 0.0f);
5914 sv_cursor_pos.x = ImClamp(IM_ROUND(picker_pos.x + ImSaturate(S) * sv_picker_size), mn: picker_pos.x + 2, mx: picker_pos.x + sv_picker_size - 2); // Sneakily prevent the circle to stick out too much
5915 sv_cursor_pos.y = ImClamp(IM_ROUND(picker_pos.y + ImSaturate(1 - V) * sv_picker_size), mn: picker_pos.y + 2, mx: picker_pos.y + sv_picker_size - 2);
5916
5917 // Render Hue Bar
5918 for (int i = 0; i < 6; ++i)
5919 draw_list->AddRectFilledMultiColor(p_min: ImVec2(bar0_pos_x, picker_pos.y + i * (sv_picker_size / 6)), p_max: ImVec2(bar0_pos_x + bars_width, picker_pos.y + (i + 1) * (sv_picker_size / 6)), col_upr_left: col_hues[i], col_upr_right: col_hues[i], col_bot_right: col_hues[i + 1], col_bot_left: col_hues[i + 1]);
5920 float bar0_line_y = IM_ROUND(picker_pos.y + H * sv_picker_size);
5921 RenderFrameBorder(p_min: ImVec2(bar0_pos_x, picker_pos.y), p_max: ImVec2(bar0_pos_x + bars_width, picker_pos.y + sv_picker_size), rounding: 0.0f);
5922 RenderArrowsForVerticalBar(draw_list, pos: ImVec2(bar0_pos_x - 1, bar0_line_y), half_sz: ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bar_w: bars_width + 2.0f, alpha: style.Alpha);
5923 }
5924
5925 // Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range)
5926 float sv_cursor_rad = value_changed_sv ? wheel_thickness * 0.55f : wheel_thickness * 0.40f;
5927 int sv_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(radius: sv_cursor_rad); // Lock segment count so the +1 one matches others.
5928 draw_list->AddCircleFilled(center: sv_cursor_pos, radius: sv_cursor_rad, col: user_col32_striped_of_alpha, num_segments: sv_cursor_segments);
5929 draw_list->AddCircle(center: sv_cursor_pos, radius: sv_cursor_rad + 1, col: col_midgrey, num_segments: sv_cursor_segments);
5930 draw_list->AddCircle(center: sv_cursor_pos, radius: sv_cursor_rad, col: col_white, num_segments: sv_cursor_segments);
5931
5932 // Render alpha bar
5933 if (alpha_bar)
5934 {
5935 float alpha = ImSaturate(f: col[3]);
5936 ImRect bar1_bb(bar1_pos_x, picker_pos.y, bar1_pos_x + bars_width, picker_pos.y + sv_picker_size);
5937 RenderColorRectWithAlphaCheckerboard(draw_list, p_min: bar1_bb.Min, p_max: bar1_bb.Max, fill_col: 0, grid_step: bar1_bb.GetWidth() / 2.0f, grid_off: ImVec2(0.0f, 0.0f));
5938 draw_list->AddRectFilledMultiColor(p_min: bar1_bb.Min, p_max: bar1_bb.Max, col_upr_left: user_col32_striped_of_alpha, col_upr_right: user_col32_striped_of_alpha, col_bot_right: user_col32_striped_of_alpha & ~IM_COL32_A_MASK, col_bot_left: user_col32_striped_of_alpha & ~IM_COL32_A_MASK);
5939 float bar1_line_y = IM_ROUND(picker_pos.y + (1.0f - alpha) * sv_picker_size);
5940 RenderFrameBorder(p_min: bar1_bb.Min, p_max: bar1_bb.Max, rounding: 0.0f);
5941 RenderArrowsForVerticalBar(draw_list, pos: ImVec2(bar1_pos_x - 1, bar1_line_y), half_sz: ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bar_w: bars_width + 2.0f, alpha: style.Alpha);
5942 }
5943
5944 EndGroup();
5945
5946 if (value_changed && memcmp(s1: backup_initial_col, s2: col, n: components * sizeof(float)) == 0)
5947 value_changed = false;
5948 if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
5949 MarkItemEdited(id: g.LastItemData.ID);
5950
5951 if (set_current_color_edit_id)
5952 g.ColorEditCurrentID = 0;
5953 PopID();
5954
5955 return value_changed;
5956}
5957
5958// A little color square. Return true when clicked.
5959// FIXME: May want to display/ignore the alpha component in the color display? Yet show it in the tooltip.
5960// 'desc_id' is not called 'label' because we don't display it next to the button, but only in the tooltip.
5961// Note that 'col' may be encoded in HSV if ImGuiColorEditFlags_InputHSV is set.
5962bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, const ImVec2& size_arg)
5963{
5964 ImGuiWindow* window = GetCurrentWindow();
5965 if (window->SkipItems)
5966 return false;
5967
5968 ImGuiContext& g = *GImGui;
5969 const ImGuiID id = window->GetID(str: desc_id);
5970 const float default_size = GetFrameHeight();
5971 const ImVec2 size(size_arg.x == 0.0f ? default_size : size_arg.x, size_arg.y == 0.0f ? default_size : size_arg.y);
5972 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
5973 ItemSize(bb, text_baseline_y: (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f);
5974 if (!ItemAdd(bb, id))
5975 return false;
5976
5977 bool hovered, held;
5978 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held);
5979
5980 if (flags & ImGuiColorEditFlags_NoAlpha)
5981 flags &= ~(ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf);
5982
5983 ImVec4 col_rgb = col;
5984 if (flags & ImGuiColorEditFlags_InputHSV)
5985 ColorConvertHSVtoRGB(h: col_rgb.x, s: col_rgb.y, v: col_rgb.z, out_r&: col_rgb.x, out_g&: col_rgb.y, out_b&: col_rgb.z);
5986
5987 ImVec4 col_rgb_without_alpha(col_rgb.x, col_rgb.y, col_rgb.z, 1.0f);
5988 float grid_step = ImMin(lhs: size.x, rhs: size.y) / 2.99f;
5989 float rounding = ImMin(lhs: g.Style.FrameRounding, rhs: grid_step * 0.5f);
5990 ImRect bb_inner = bb;
5991 float off = 0.0f;
5992 if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
5993 {
5994 off = -0.75f; // The border (using Col_FrameBg) tends to look off when color is near-opaque and rounding is enabled. This offset seemed like a good middle ground to reduce those artifacts.
5995 bb_inner.Expand(amount: off);
5996 }
5997 if ((flags & ImGuiColorEditFlags_AlphaPreviewHalf) && col_rgb.w < 1.0f)
5998 {
5999 float mid_x = IM_ROUND((bb_inner.Min.x + bb_inner.Max.x) * 0.5f);
6000 RenderColorRectWithAlphaCheckerboard(draw_list: window->DrawList, p_min: ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), p_max: bb_inner.Max, fill_col: GetColorU32(col: col_rgb), grid_step, grid_off: ImVec2(-grid_step + off, off), rounding, flags: ImDrawFlags_RoundCornersRight);
6001 window->DrawList->AddRectFilled(p_min: bb_inner.Min, p_max: ImVec2(mid_x, bb_inner.Max.y), col: GetColorU32(col: col_rgb_without_alpha), rounding, flags: ImDrawFlags_RoundCornersLeft);
6002 }
6003 else
6004 {
6005 // Because GetColorU32() multiplies by the global style Alpha and we don't want to display a checkerboard if the source code had no alpha
6006 ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaPreview) ? col_rgb : col_rgb_without_alpha;
6007 if (col_source.w < 1.0f)
6008 RenderColorRectWithAlphaCheckerboard(draw_list: window->DrawList, p_min: bb_inner.Min, p_max: bb_inner.Max, fill_col: GetColorU32(col: col_source), grid_step, grid_off: ImVec2(off, off), rounding);
6009 else
6010 window->DrawList->AddRectFilled(p_min: bb_inner.Min, p_max: bb_inner.Max, col: GetColorU32(col: col_source), rounding);
6011 }
6012 RenderNavHighlight(bb, id);
6013 if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
6014 {
6015 if (g.Style.FrameBorderSize > 0.0f)
6016 RenderFrameBorder(p_min: bb.Min, p_max: bb.Max, rounding);
6017 else
6018 window->DrawList->AddRect(p_min: bb.Min, p_max: bb.Max, col: GetColorU32(idx: ImGuiCol_FrameBg), rounding); // Color button are often in need of some sort of border
6019 }
6020
6021 // Drag and Drop Source
6022 // NB: The ActiveId test is merely an optional micro-optimization, BeginDragDropSource() does the same test.
6023 if (g.ActiveId == id && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropSource())
6024 {
6025 if (flags & ImGuiColorEditFlags_NoAlpha)
6026 SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F, data: &col_rgb, sz: sizeof(float) * 3, cond: ImGuiCond_Once);
6027 else
6028 SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, data: &col_rgb, sz: sizeof(float) * 4, cond: ImGuiCond_Once);
6029 ColorButton(desc_id, col, flags);
6030 SameLine();
6031 TextEx(text: "Color");
6032 EndDragDropSource();
6033 }
6034
6035 // Tooltip
6036 if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered && IsItemHovered(flags: ImGuiHoveredFlags_ForTooltip))
6037 ColorTooltip(text: desc_id, col: &col.x, flags: flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf));
6038
6039 return pressed;
6040}
6041
6042// Initialize/override default color options
6043void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags)
6044{
6045 ImGuiContext& g = *GImGui;
6046 if ((flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6047 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DisplayMask_;
6048 if ((flags & ImGuiColorEditFlags_DataTypeMask_) == 0)
6049 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DataTypeMask_;
6050 if ((flags & ImGuiColorEditFlags_PickerMask_) == 0)
6051 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_PickerMask_;
6052 if ((flags & ImGuiColorEditFlags_InputMask_) == 0)
6053 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_InputMask_;
6054 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check only 1 option is selected
6055 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DataTypeMask_)); // Check only 1 option is selected
6056 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check only 1 option is selected
6057 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check only 1 option is selected
6058 g.ColorEditOptions = flags;
6059}
6060
6061// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
6062void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags)
6063{
6064 ImGuiContext& g = *GImGui;
6065
6066 if (!BeginTooltipEx(tooltip_flags: ImGuiTooltipFlags_OverridePrevious, extra_window_flags: ImGuiWindowFlags_None))
6067 return;
6068 const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text;
6069 if (text_end > text)
6070 {
6071 TextEx(text, text_end);
6072 Separator();
6073 }
6074
6075 ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2);
6076 ImVec4 cf(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6077 int cr = IM_F32_TO_INT8_SAT(col[0]), cg = IM_F32_TO_INT8_SAT(col[1]), cb = IM_F32_TO_INT8_SAT(col[2]), ca = (flags & ImGuiColorEditFlags_NoAlpha) ? 255 : IM_F32_TO_INT8_SAT(col[3]);
6078 ColorButton(desc_id: "##preview", col: cf, flags: (flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)) | ImGuiColorEditFlags_NoTooltip, size_arg: sz);
6079 SameLine();
6080 if ((flags & ImGuiColorEditFlags_InputRGB) || !(flags & ImGuiColorEditFlags_InputMask_))
6081 {
6082 if (flags & ImGuiColorEditFlags_NoAlpha)
6083 Text(fmt: "#%02X%02X%02X\nR: %d, G: %d, B: %d\n(%.3f, %.3f, %.3f)", cr, cg, cb, cr, cg, cb, col[0], col[1], col[2]);
6084 else
6085 Text(fmt: "#%02X%02X%02X%02X\nR:%d, G:%d, B:%d, A:%d\n(%.3f, %.3f, %.3f, %.3f)", cr, cg, cb, ca, cr, cg, cb, ca, col[0], col[1], col[2], col[3]);
6086 }
6087 else if (flags & ImGuiColorEditFlags_InputHSV)
6088 {
6089 if (flags & ImGuiColorEditFlags_NoAlpha)
6090 Text(fmt: "H: %.3f, S: %.3f, V: %.3f", col[0], col[1], col[2]);
6091 else
6092 Text(fmt: "H: %.3f, S: %.3f, V: %.3f, A: %.3f", col[0], col[1], col[2], col[3]);
6093 }
6094 EndTooltip();
6095}
6096
6097void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags)
6098{
6099 bool allow_opt_inputs = !(flags & ImGuiColorEditFlags_DisplayMask_);
6100 bool allow_opt_datatype = !(flags & ImGuiColorEditFlags_DataTypeMask_);
6101 if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup(str_id: "context"))
6102 return;
6103 ImGuiContext& g = *GImGui;
6104 g.LockMarkEdited++;
6105 ImGuiColorEditFlags opts = g.ColorEditOptions;
6106 if (allow_opt_inputs)
6107 {
6108 if (RadioButton(label: "RGB", active: (opts & ImGuiColorEditFlags_DisplayRGB) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayRGB;
6109 if (RadioButton(label: "HSV", active: (opts & ImGuiColorEditFlags_DisplayHSV) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHSV;
6110 if (RadioButton(label: "Hex", active: (opts & ImGuiColorEditFlags_DisplayHex) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHex;
6111 }
6112 if (allow_opt_datatype)
6113 {
6114 if (allow_opt_inputs) Separator();
6115 if (RadioButton(label: "0..255", active: (opts & ImGuiColorEditFlags_Uint8) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Uint8;
6116 if (RadioButton(label: "0.00..1.00", active: (opts & ImGuiColorEditFlags_Float) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Float;
6117 }
6118
6119 if (allow_opt_inputs || allow_opt_datatype)
6120 Separator();
6121 if (Button(label: "Copy as..", size_arg: ImVec2(-1, 0)))
6122 OpenPopup(str_id: "Copy");
6123 if (BeginPopup(str_id: "Copy"))
6124 {
6125 int cr = IM_F32_TO_INT8_SAT(col[0]), cg = IM_F32_TO_INT8_SAT(col[1]), cb = IM_F32_TO_INT8_SAT(col[2]), ca = (flags & ImGuiColorEditFlags_NoAlpha) ? 255 : IM_F32_TO_INT8_SAT(col[3]);
6126 char buf[64];
6127 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "(%.3ff, %.3ff, %.3ff, %.3ff)", col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6128 if (Selectable(label: buf))
6129 SetClipboardText(buf);
6130 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "(%d,%d,%d,%d)", cr, cg, cb, ca);
6131 if (Selectable(label: buf))
6132 SetClipboardText(buf);
6133 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "#%02X%02X%02X", cr, cg, cb);
6134 if (Selectable(label: buf))
6135 SetClipboardText(buf);
6136 if (!(flags & ImGuiColorEditFlags_NoAlpha))
6137 {
6138 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "#%02X%02X%02X%02X", cr, cg, cb, ca);
6139 if (Selectable(label: buf))
6140 SetClipboardText(buf);
6141 }
6142 EndPopup();
6143 }
6144
6145 g.ColorEditOptions = opts;
6146 EndPopup();
6147 g.LockMarkEdited--;
6148}
6149
6150void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags)
6151{
6152 bool allow_opt_picker = !(flags & ImGuiColorEditFlags_PickerMask_);
6153 bool allow_opt_alpha_bar = !(flags & ImGuiColorEditFlags_NoAlpha) && !(flags & ImGuiColorEditFlags_AlphaBar);
6154 if ((!allow_opt_picker && !allow_opt_alpha_bar) || !BeginPopup(str_id: "context"))
6155 return;
6156 ImGuiContext& g = *GImGui;
6157 g.LockMarkEdited++;
6158 if (allow_opt_picker)
6159 {
6160 ImVec2 picker_size(g.FontSize * 8, ImMax(lhs: g.FontSize * 8 - (GetFrameHeight() + g.Style.ItemInnerSpacing.x), rhs: 1.0f)); // FIXME: Picker size copied from main picker function
6161 PushItemWidth(item_width: picker_size.x);
6162 for (int picker_type = 0; picker_type < 2; picker_type++)
6163 {
6164 // Draw small/thumbnail version of each picker type (over an invisible button for selection)
6165 if (picker_type > 0) Separator();
6166 PushID(int_id: picker_type);
6167 ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoSidePreview | (flags & ImGuiColorEditFlags_NoAlpha);
6168 if (picker_type == 0) picker_flags |= ImGuiColorEditFlags_PickerHueBar;
6169 if (picker_type == 1) picker_flags |= ImGuiColorEditFlags_PickerHueWheel;
6170 ImVec2 backup_pos = GetCursorScreenPos();
6171 if (Selectable(label: "##selectable", selected: false, flags: 0, size: picker_size)) // By default, Selectable() is closing popup
6172 g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags_PickerMask_) | (picker_flags & ImGuiColorEditFlags_PickerMask_);
6173 SetCursorScreenPos(backup_pos);
6174 ImVec4 previewing_ref_col;
6175 memcpy(dest: &previewing_ref_col, src: ref_col, n: sizeof(float) * ((picker_flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4));
6176 ColorPicker4(label: "##previewing_picker", col: &previewing_ref_col.x, flags: picker_flags);
6177 PopID();
6178 }
6179 PopItemWidth();
6180 }
6181 if (allow_opt_alpha_bar)
6182 {
6183 if (allow_opt_picker) Separator();
6184 CheckboxFlags(label: "Alpha Bar", flags: &g.ColorEditOptions, flags_value: ImGuiColorEditFlags_AlphaBar);
6185 }
6186 EndPopup();
6187 g.LockMarkEdited--;
6188}
6189
6190//-------------------------------------------------------------------------
6191// [SECTION] Widgets: TreeNode, CollapsingHeader, etc.
6192//-------------------------------------------------------------------------
6193// - TreeNode()
6194// - TreeNodeV()
6195// - TreeNodeEx()
6196// - TreeNodeExV()
6197// - TreeNodeBehavior() [Internal]
6198// - TreePush()
6199// - TreePop()
6200// - GetTreeNodeToLabelSpacing()
6201// - SetNextItemOpen()
6202// - CollapsingHeader()
6203//-------------------------------------------------------------------------
6204
6205bool ImGui::TreeNode(const char* str_id, const char* fmt, ...)
6206{
6207 va_list args;
6208 va_start(args, fmt);
6209 bool is_open = TreeNodeExV(str_id, flags: 0, fmt, args);
6210 va_end(args);
6211 return is_open;
6212}
6213
6214bool ImGui::TreeNode(const void* ptr_id, const char* fmt, ...)
6215{
6216 va_list args;
6217 va_start(args, fmt);
6218 bool is_open = TreeNodeExV(ptr_id, flags: 0, fmt, args);
6219 va_end(args);
6220 return is_open;
6221}
6222
6223bool ImGui::TreeNode(const char* label)
6224{
6225 ImGuiWindow* window = GetCurrentWindow();
6226 if (window->SkipItems)
6227 return false;
6228 ImGuiID id = window->GetID(str: label);
6229 return TreeNodeBehavior(id, flags: ImGuiTreeNodeFlags_None, label, NULL);
6230}
6231
6232bool ImGui::TreeNodeV(const char* str_id, const char* fmt, va_list args)
6233{
6234 return TreeNodeExV(str_id, flags: 0, fmt, args);
6235}
6236
6237bool ImGui::TreeNodeV(const void* ptr_id, const char* fmt, va_list args)
6238{
6239 return TreeNodeExV(ptr_id, flags: 0, fmt, args);
6240}
6241
6242bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags)
6243{
6244 ImGuiWindow* window = GetCurrentWindow();
6245 if (window->SkipItems)
6246 return false;
6247 ImGuiID id = window->GetID(str: label);
6248 return TreeNodeBehavior(id, flags, label, NULL);
6249}
6250
6251bool ImGui::TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
6252{
6253 va_list args;
6254 va_start(args, fmt);
6255 bool is_open = TreeNodeExV(str_id, flags, fmt, args);
6256 va_end(args);
6257 return is_open;
6258}
6259
6260bool ImGui::TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
6261{
6262 va_list args;
6263 va_start(args, fmt);
6264 bool is_open = TreeNodeExV(ptr_id, flags, fmt, args);
6265 va_end(args);
6266 return is_open;
6267}
6268
6269bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
6270{
6271 ImGuiWindow* window = GetCurrentWindow();
6272 if (window->SkipItems)
6273 return false;
6274
6275 ImGuiID id = window->GetID(str: str_id);
6276 const char* label, *label_end;
6277 ImFormatStringToTempBufferV(out_buf: &label, out_buf_end: &label_end, fmt, args);
6278 return TreeNodeBehavior(id, flags, label, label_end);
6279}
6280
6281bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
6282{
6283 ImGuiWindow* window = GetCurrentWindow();
6284 if (window->SkipItems)
6285 return false;
6286
6287 ImGuiID id = window->GetID(ptr: ptr_id);
6288 const char* label, *label_end;
6289 ImFormatStringToTempBufferV(out_buf: &label, out_buf_end: &label_end, fmt, args);
6290 return TreeNodeBehavior(id, flags, label, label_end);
6291}
6292
6293bool ImGui::TreeNodeGetOpen(ImGuiID storage_id)
6294{
6295 ImGuiContext& g = *GImGui;
6296 ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
6297 return storage->GetInt(key: storage_id, default_val: 0) != 0;
6298}
6299
6300void ImGui::TreeNodeSetOpen(ImGuiID storage_id, bool open)
6301{
6302 ImGuiContext& g = *GImGui;
6303 ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
6304 storage->SetInt(key: storage_id, val: open ? 1 : 0);
6305}
6306
6307bool ImGui::TreeNodeUpdateNextOpen(ImGuiID storage_id, ImGuiTreeNodeFlags flags)
6308{
6309 if (flags & ImGuiTreeNodeFlags_Leaf)
6310 return true;
6311
6312 // We only write to the tree storage if the user clicks, or explicitly use the SetNextItemOpen function
6313 ImGuiContext& g = *GImGui;
6314 ImGuiWindow* window = g.CurrentWindow;
6315 ImGuiStorage* storage = window->DC.StateStorage;
6316
6317 bool is_open;
6318 if (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasOpen)
6319 {
6320 if (g.NextItemData.OpenCond & ImGuiCond_Always)
6321 {
6322 is_open = g.NextItemData.OpenVal;
6323 TreeNodeSetOpen(storage_id, open: is_open);
6324 }
6325 else
6326 {
6327 // We treat ImGuiCond_Once and ImGuiCond_FirstUseEver the same because tree node state are not saved persistently.
6328 const int stored_value = storage->GetInt(key: storage_id, default_val: -1);
6329 if (stored_value == -1)
6330 {
6331 is_open = g.NextItemData.OpenVal;
6332 TreeNodeSetOpen(storage_id, open: is_open);
6333 }
6334 else
6335 {
6336 is_open = stored_value != 0;
6337 }
6338 }
6339 }
6340 else
6341 {
6342 is_open = storage->GetInt(key: storage_id, default_val: (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0;
6343 }
6344
6345 // When logging is enabled, we automatically expand tree nodes (but *NOT* collapsing headers.. seems like sensible behavior).
6346 // NB- If we are above max depth we still allow manually opened nodes to be logged.
6347 if (g.LogEnabled && !(flags & ImGuiTreeNodeFlags_NoAutoOpenOnLog) && (window->DC.TreeDepth - g.LogDepthRef) < g.LogDepthToExpand)
6348 is_open = true;
6349
6350 return is_open;
6351}
6352
6353// Store ImGuiTreeNodeStackData for just submitted node.
6354// Currently only supports 32 level deep and we are fine with (1 << Depth) overflowing into a zero, easy to increase.
6355static void TreeNodeStoreStackData(ImGuiTreeNodeFlags flags)
6356{
6357 ImGuiContext& g = *GImGui;
6358 ImGuiWindow* window = g.CurrentWindow;
6359
6360 g.TreeNodeStack.resize(new_size: g.TreeNodeStack.Size + 1);
6361 ImGuiTreeNodeStackData* tree_node_data = &g.TreeNodeStack.back();
6362 tree_node_data->ID = g.LastItemData.ID;
6363 tree_node_data->TreeFlags = flags;
6364 tree_node_data->InFlags = g.LastItemData.InFlags;
6365 tree_node_data->NavRect = g.LastItemData.NavRect;
6366 window->DC.TreeHasStackDataDepthMask |= (1 << window->DC.TreeDepth);
6367}
6368
6369// When using public API, currently 'id == storage_id' is always true, but we separate the values to facilitate advanced user code doing storage queries outside of UI loop.
6370bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end)
6371{
6372 ImGuiWindow* window = GetCurrentWindow();
6373 if (window->SkipItems)
6374 return false;
6375
6376 ImGuiContext& g = *GImGui;
6377 const ImGuiStyle& style = g.Style;
6378 const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0;
6379 const ImVec2 padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, ImMin(lhs: window->DC.CurrLineTextBaseOffset, rhs: style.FramePadding.y));
6380
6381 if (!label_end)
6382 label_end = FindRenderedTextEnd(text: label);
6383 const ImVec2 label_size = CalcTextSize(text: label, text_end: label_end, hide_text_after_double_hash: false);
6384
6385 const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapsing arrow width + Spacing
6386 const float text_offset_y = ImMax(lhs: padding.y, rhs: window->DC.CurrLineTextBaseOffset); // Latch before ItemSize changes it
6387 const float text_width = g.FontSize + label_size.x + padding.x * 2; // Include collapsing arrow
6388
6389 // We vertically grow up to current line height up the typical widget height.
6390 const float frame_height = ImMax(lhs: ImMin(lhs: window->DC.CurrLineSize.y, rhs: g.FontSize + style.FramePadding.y * 2), rhs: label_size.y + padding.y * 2);
6391 const bool span_all_columns = (flags & ImGuiTreeNodeFlags_SpanAllColumns) != 0 && (g.CurrentTable != NULL);
6392 ImRect frame_bb;
6393 frame_bb.Min.x = span_all_columns ? window->ParentWorkRect.Min.x : (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x;
6394 frame_bb.Min.y = window->DC.CursorPos.y;
6395 frame_bb.Max.x = span_all_columns ? window->ParentWorkRect.Max.x : (flags & ImGuiTreeNodeFlags_SpanTextWidth) ? window->DC.CursorPos.x + text_width + padding.x : window->WorkRect.Max.x;
6396 frame_bb.Max.y = window->DC.CursorPos.y + frame_height;
6397 if (display_frame)
6398 {
6399 const float outer_extend = IM_TRUNC(window->WindowPadding.x * 0.5f); // Framed header expand a little outside of current limits
6400 frame_bb.Min.x -= outer_extend;
6401 frame_bb.Max.x += outer_extend;
6402 }
6403
6404 ImVec2 text_pos(window->DC.CursorPos.x + text_offset_x, window->DC.CursorPos.y + text_offset_y);
6405 ItemSize(size: ImVec2(text_width, frame_height), text_baseline_y: padding.y);
6406
6407 // For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing
6408 ImRect interact_bb = frame_bb;
6409 if ((flags & (ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_SpanTextWidth | ImGuiTreeNodeFlags_SpanAllColumns)) == 0)
6410 interact_bb.Max.x = frame_bb.Min.x + text_width + (label_size.x > 0.0f ? style.ItemSpacing.x * 2.0f : 0.0f);
6411
6412 // Compute open and multi-select states before ItemAdd() as it clear NextItem data.
6413 ImGuiID storage_id = (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasStorageID) ? g.NextItemData.StorageId : id;
6414 bool is_open = TreeNodeUpdateNextOpen(storage_id, flags);
6415
6416 bool is_visible;
6417 if (span_all_columns)
6418 {
6419 // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable..
6420 const float backup_clip_rect_min_x = window->ClipRect.Min.x;
6421 const float backup_clip_rect_max_x = window->ClipRect.Max.x;
6422 window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
6423 window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
6424 is_visible = ItemAdd(bb: interact_bb, id);
6425 window->ClipRect.Min.x = backup_clip_rect_min_x;
6426 window->ClipRect.Max.x = backup_clip_rect_max_x;
6427 }
6428 else
6429 {
6430 is_visible = ItemAdd(bb: interact_bb, id);
6431 }
6432 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect;
6433 g.LastItemData.DisplayRect = frame_bb;
6434
6435 // If a NavLeft request is happening and ImGuiTreeNodeFlags_NavLeftJumpsBackHere enabled:
6436 // Store data for the current depth to allow returning to this node from any child item.
6437 // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop().
6438 // It will become tempting to enable ImGuiTreeNodeFlags_NavLeftJumpsBackHere by default or move it to ImGuiStyle.
6439 bool store_tree_node_stack_data = false;
6440 if (!(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6441 {
6442 if ((flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && is_open && !g.NavIdIsAlive)
6443 if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
6444 store_tree_node_stack_data = true;
6445 }
6446
6447 const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0;
6448 if (!is_visible)
6449 {
6450 if (store_tree_node_stack_data && is_open)
6451 TreeNodeStoreStackData(flags); // Call before TreePushOverrideID()
6452 if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6453 TreePushOverrideID(id);
6454 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
6455 return is_open;
6456 }
6457
6458 if (span_all_columns)
6459 {
6460 TablePushBackgroundChannel();
6461 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect;
6462 g.LastItemData.ClipRect = window->ClipRect;
6463 }
6464
6465 ImGuiButtonFlags button_flags = ImGuiTreeNodeFlags_None;
6466 if ((flags & ImGuiTreeNodeFlags_AllowOverlap) || (g.LastItemData.InFlags & ImGuiItemFlags_AllowOverlap))
6467 button_flags |= ImGuiButtonFlags_AllowOverlap;
6468 if (!is_leaf)
6469 button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
6470
6471 // We allow clicking on the arrow section with keyboard modifiers held, in order to easily
6472 // allow browsing a tree while preserving selection with code implementing multi-selection patterns.
6473 // When clicking on the rest of the tree node we always disallow keyboard modifiers.
6474 const float arrow_hit_x1 = (text_pos.x - text_offset_x) - style.TouchExtraPadding.x;
6475 const float arrow_hit_x2 = (text_pos.x - text_offset_x) + (g.FontSize + padding.x * 2.0f) + style.TouchExtraPadding.x;
6476 const bool is_mouse_x_over_arrow = (g.IO.MousePos.x >= arrow_hit_x1 && g.IO.MousePos.x < arrow_hit_x2);
6477
6478 // Open behaviors can be altered with the _OpenOnArrow and _OnOnDoubleClick flags.
6479 // Some alteration have subtle effects (e.g. toggle on MouseUp vs MouseDown events) due to requirements for multi-selection and drag and drop support.
6480 // - Single-click on label = Toggle on MouseUp (default, when _OpenOnArrow=0)
6481 // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=0)
6482 // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=1)
6483 // - Double-click on label = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1)
6484 // - Double-click on arrow = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1 and _OpenOnArrow=0)
6485 // It is rather standard that arrow click react on Down rather than Up.
6486 // We set ImGuiButtonFlags_PressedOnClickRelease on OpenOnDoubleClick because we want the item to be active on the initial MouseDown in order for drag and drop to work.
6487 if (is_mouse_x_over_arrow)
6488 button_flags |= ImGuiButtonFlags_PressedOnClick;
6489 else if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick)
6490 button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick;
6491 else
6492 button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
6493
6494 bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0;
6495 const bool was_selected = selected;
6496
6497 // Multi-selection support (header)
6498 const bool is_multi_select = (g.LastItemData.InFlags & ImGuiItemFlags_IsMultiSelect) != 0;
6499 if (is_multi_select)
6500 {
6501 // Handle multi-select + alter button flags for it
6502 MultiSelectItemHeader(id, p_selected: &selected, p_button_flags: &button_flags);
6503 if (is_mouse_x_over_arrow)
6504 button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease;
6505
6506 // We absolutely need to distinguish open vs select so comes by default
6507 flags |= ImGuiTreeNodeFlags_OpenOnArrow;
6508 }
6509 else
6510 {
6511 if (window != g.HoveredWindow || !is_mouse_x_over_arrow)
6512 button_flags |= ImGuiButtonFlags_NoKeyModifiers;
6513 }
6514
6515 bool hovered, held;
6516 bool pressed = ButtonBehavior(bb: interact_bb, id, out_hovered: &hovered, out_held: &held, flags: button_flags);
6517 bool toggled = false;
6518 if (!is_leaf)
6519 {
6520 if (pressed && g.DragDropHoldJustPressedId != id)
6521 {
6522 if ((flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) == 0 || (g.NavActivateId == id && !is_multi_select))
6523 toggled = true;
6524 if (flags & ImGuiTreeNodeFlags_OpenOnArrow)
6525 toggled |= is_mouse_x_over_arrow && !g.NavDisableMouseHover; // Lightweight equivalent of IsMouseHoveringRect() since ButtonBehavior() already did the job
6526 if ((flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) && g.IO.MouseClickedCount[0] == 2)
6527 toggled = true;
6528 }
6529 else if (pressed && g.DragDropHoldJustPressedId == id)
6530 {
6531 IM_ASSERT(button_flags & ImGuiButtonFlags_PressedOnDragDropHold);
6532 if (!is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again.
6533 toggled = true;
6534 }
6535
6536 if (g.NavId == id && g.NavMoveDir == ImGuiDir_Left && is_open)
6537 {
6538 toggled = true;
6539 NavClearPreferredPosForAxis(axis: ImGuiAxis_X);
6540 NavMoveRequestCancel();
6541 }
6542 if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right && !is_open) // If there's something upcoming on the line we may want to give it the priority?
6543 {
6544 toggled = true;
6545 NavClearPreferredPosForAxis(axis: ImGuiAxis_X);
6546 NavMoveRequestCancel();
6547 }
6548
6549 if (toggled)
6550 {
6551 is_open = !is_open;
6552 window->DC.StateStorage->SetInt(key: storage_id, val: is_open);
6553 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledOpen;
6554 }
6555 }
6556
6557 // Multi-selection support (footer)
6558 if (is_multi_select)
6559 {
6560 bool pressed_copy = pressed && !toggled;
6561 MultiSelectItemFooter(id, p_selected: &selected, p_pressed: &pressed_copy);
6562 if (pressed)
6563 SetNavID(id, nav_layer: window->DC.NavLayerCurrent, focus_scope_id: g.CurrentFocusScopeId, rect_rel: interact_bb);
6564 }
6565
6566 if (selected != was_selected)
6567 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
6568
6569 // Render
6570 {
6571 const ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
6572 ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_Compact;
6573 if (is_multi_select)
6574 nav_highlight_flags |= ImGuiNavHighlightFlags_AlwaysDraw; // Always show the nav rectangle
6575 if (display_frame)
6576 {
6577 // Framed type
6578 const ImU32 bg_col = GetColorU32(idx: (held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
6579 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: bg_col, border: true, rounding: style.FrameRounding);
6580 RenderNavHighlight(bb: frame_bb, id, flags: nav_highlight_flags);
6581 if (flags & ImGuiTreeNodeFlags_Bullet)
6582 RenderBullet(draw_list: window->DrawList, pos: ImVec2(text_pos.x - text_offset_x * 0.60f, text_pos.y + g.FontSize * 0.5f), col: text_col);
6583 else if (!is_leaf)
6584 RenderArrow(draw_list: window->DrawList, pos: ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y), col: text_col, dir: is_open ? ((flags & ImGuiTreeNodeFlags_UpsideDownArrow) ? ImGuiDir_Up : ImGuiDir_Down) : ImGuiDir_Right, scale: 1.0f);
6585 else // Leaf without bullet, left-adjusted text
6586 text_pos.x -= text_offset_x - padding.x;
6587 if (flags & ImGuiTreeNodeFlags_ClipLabelForTrailingButton)
6588 frame_bb.Max.x -= g.FontSize + style.FramePadding.x;
6589 if (g.LogEnabled)
6590 LogSetNextTextDecoration(prefix: "###", suffix: "###");
6591 }
6592 else
6593 {
6594 // Unframed typed for tree nodes
6595 if (hovered || selected)
6596 {
6597 const ImU32 bg_col = GetColorU32(idx: (held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
6598 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: bg_col, border: false);
6599 }
6600 RenderNavHighlight(bb: frame_bb, id, flags: nav_highlight_flags);
6601 if (flags & ImGuiTreeNodeFlags_Bullet)
6602 RenderBullet(draw_list: window->DrawList, pos: ImVec2(text_pos.x - text_offset_x * 0.5f, text_pos.y + g.FontSize * 0.5f), col: text_col);
6603 else if (!is_leaf)
6604 RenderArrow(draw_list: window->DrawList, pos: ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y + g.FontSize * 0.15f), col: text_col, dir: is_open ? ((flags & ImGuiTreeNodeFlags_UpsideDownArrow) ? ImGuiDir_Up : ImGuiDir_Down) : ImGuiDir_Right, scale: 0.70f);
6605 if (g.LogEnabled)
6606 LogSetNextTextDecoration(prefix: ">", NULL);
6607 }
6608
6609 if (span_all_columns)
6610 TablePopBackgroundChannel();
6611
6612 // Label
6613 if (display_frame)
6614 RenderTextClipped(pos_min: text_pos, pos_max: frame_bb.Max, text: label, text_end: label_end, text_size_if_known: &label_size);
6615 else
6616 RenderText(pos: text_pos, text: label, text_end: label_end, hide_text_after_hash: false);
6617 }
6618
6619 if (store_tree_node_stack_data && is_open)
6620 TreeNodeStoreStackData(flags); // Call before TreePushOverrideID()
6621 if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6622 TreePushOverrideID(id); // Could use TreePush(label) but this avoid computing twice
6623
6624 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
6625 return is_open;
6626}
6627
6628void ImGui::TreePush(const char* str_id)
6629{
6630 ImGuiWindow* window = GetCurrentWindow();
6631 Indent();
6632 window->DC.TreeDepth++;
6633 PushID(str_id);
6634}
6635
6636void ImGui::TreePush(const void* ptr_id)
6637{
6638 ImGuiWindow* window = GetCurrentWindow();
6639 Indent();
6640 window->DC.TreeDepth++;
6641 PushID(ptr_id);
6642}
6643
6644void ImGui::TreePushOverrideID(ImGuiID id)
6645{
6646 ImGuiContext& g = *GImGui;
6647 ImGuiWindow* window = g.CurrentWindow;
6648 Indent();
6649 window->DC.TreeDepth++;
6650 PushOverrideID(id);
6651}
6652
6653void ImGui::TreePop()
6654{
6655 ImGuiContext& g = *GImGui;
6656 ImGuiWindow* window = g.CurrentWindow;
6657 Unindent();
6658
6659 window->DC.TreeDepth--;
6660 ImU32 tree_depth_mask = (1 << window->DC.TreeDepth);
6661
6662 if (window->DC.TreeHasStackDataDepthMask & tree_depth_mask) // Only set during request
6663 {
6664 ImGuiTreeNodeStackData* data = &g.TreeNodeStack.back();
6665 IM_ASSERT(data->ID == window->IDStack.back());
6666 if (data->TreeFlags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere)
6667 {
6668 // Handle Left arrow to move to parent tree node (when ImGuiTreeNodeFlags_NavLeftJumpsBackHere is enabled)
6669 if (g.NavIdIsAlive && g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
6670 NavMoveRequestResolveWithPastTreeNode(result: &g.NavMoveResultLocal, tree_node_data: data);
6671 }
6672 g.TreeNodeStack.pop_back();
6673 window->DC.TreeHasStackDataDepthMask &= ~tree_depth_mask;
6674 }
6675
6676 IM_ASSERT(window->IDStack.Size > 1); // There should always be 1 element in the IDStack (pushed during window creation). If this triggers you called TreePop/PopID too much.
6677 PopID();
6678}
6679
6680// Horizontal distance preceding label when using TreeNode() or Bullet()
6681float ImGui::GetTreeNodeToLabelSpacing()
6682{
6683 ImGuiContext& g = *GImGui;
6684 return g.FontSize + (g.Style.FramePadding.x * 2.0f);
6685}
6686
6687// Set next TreeNode/CollapsingHeader open state.
6688void ImGui::SetNextItemOpen(bool is_open, ImGuiCond cond)
6689{
6690 ImGuiContext& g = *GImGui;
6691 if (g.CurrentWindow->SkipItems)
6692 return;
6693 g.NextItemData.Flags |= ImGuiNextItemDataFlags_HasOpen;
6694 g.NextItemData.OpenVal = is_open;
6695 g.NextItemData.OpenCond = (ImU8)(cond ? cond : ImGuiCond_Always);
6696}
6697
6698// Set next TreeNode/CollapsingHeader storage id.
6699void ImGui::SetNextItemStorageID(ImGuiID storage_id)
6700{
6701 ImGuiContext& g = *GImGui;
6702 if (g.CurrentWindow->SkipItems)
6703 return;
6704 g.NextItemData.Flags |= ImGuiNextItemDataFlags_HasStorageID;
6705 g.NextItemData.StorageId = storage_id;
6706}
6707
6708// CollapsingHeader returns true when opened but do not indent nor push into the ID stack (because of the ImGuiTreeNodeFlags_NoTreePushOnOpen flag).
6709// This is basically the same as calling TreeNodeEx(label, ImGuiTreeNodeFlags_CollapsingHeader). You can remove the _NoTreePushOnOpen flag if you want behavior closer to normal TreeNode().
6710bool ImGui::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags)
6711{
6712 ImGuiWindow* window = GetCurrentWindow();
6713 if (window->SkipItems)
6714 return false;
6715 ImGuiID id = window->GetID(str: label);
6716 return TreeNodeBehavior(id, flags: flags | ImGuiTreeNodeFlags_CollapsingHeader, label);
6717}
6718
6719// p_visible == NULL : regular collapsing header
6720// p_visible != NULL && *p_visible == true : show a small close button on the corner of the header, clicking the button will set *p_visible = false
6721// p_visible != NULL && *p_visible == false : do not show the header at all
6722// Do not mistake this with the Open state of the header itself, which you can adjust with SetNextItemOpen() or ImGuiTreeNodeFlags_DefaultOpen.
6723bool ImGui::CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags)
6724{
6725 ImGuiWindow* window = GetCurrentWindow();
6726 if (window->SkipItems)
6727 return false;
6728
6729 if (p_visible && !*p_visible)
6730 return false;
6731
6732 ImGuiID id = window->GetID(str: label);
6733 flags |= ImGuiTreeNodeFlags_CollapsingHeader;
6734 if (p_visible)
6735 flags |= ImGuiTreeNodeFlags_AllowOverlap | (ImGuiTreeNodeFlags)ImGuiTreeNodeFlags_ClipLabelForTrailingButton;
6736 bool is_open = TreeNodeBehavior(id, flags, label);
6737 if (p_visible != NULL)
6738 {
6739 // Create a small overlapping close button
6740 // FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc.
6741 // FIXME: CloseButton can overlap into text, need find a way to clip the text somehow.
6742 ImGuiContext& g = *GImGui;
6743 ImGuiLastItemData last_item_backup = g.LastItemData;
6744 float button_size = g.FontSize;
6745 float button_x = ImMax(lhs: g.LastItemData.Rect.Min.x, rhs: g.LastItemData.Rect.Max.x - g.Style.FramePadding.x - button_size);
6746 float button_y = g.LastItemData.Rect.Min.y + g.Style.FramePadding.y;
6747 ImGuiID close_button_id = GetIDWithSeed(str_id_begin: "#CLOSE", NULL, seed: id);
6748 if (CloseButton(id: close_button_id, pos: ImVec2(button_x, button_y)))
6749 *p_visible = false;
6750 g.LastItemData = last_item_backup;
6751 }
6752
6753 return is_open;
6754}
6755
6756//-------------------------------------------------------------------------
6757// [SECTION] Widgets: Selectable
6758//-------------------------------------------------------------------------
6759// - Selectable()
6760//-------------------------------------------------------------------------
6761
6762// Tip: pass a non-visible label (e.g. "##hello") then you can use the space to draw other text or image.
6763// But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID or use ##unique_id.
6764// With this scheme, ImGuiSelectableFlags_SpanAllColumns and ImGuiSelectableFlags_AllowOverlap are also frequently used flags.
6765// FIXME: Selectable() with (size.x == 0.0f) and (SelectableTextAlign.x > 0.0f) followed by SameLine() is currently not supported.
6766bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
6767{
6768 ImGuiWindow* window = GetCurrentWindow();
6769 if (window->SkipItems)
6770 return false;
6771
6772 ImGuiContext& g = *GImGui;
6773 const ImGuiStyle& style = g.Style;
6774
6775 // Submit label or explicit size to ItemSize(), whereas ItemAdd() will submit a larger/spanning rectangle.
6776 ImGuiID id = window->GetID(str: label);
6777 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
6778 ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y);
6779 ImVec2 pos = window->DC.CursorPos;
6780 pos.y += window->DC.CurrLineTextBaseOffset;
6781 ItemSize(size, text_baseline_y: 0.0f);
6782
6783 // Fill horizontal space
6784 // We don't support (size < 0.0f) in Selectable() because the ItemSpacing extension would make explicitly right-aligned sizes not visibly match other widgets.
6785 const bool span_all_columns = (flags & ImGuiSelectableFlags_SpanAllColumns) != 0;
6786 const float min_x = span_all_columns ? window->ParentWorkRect.Min.x : pos.x;
6787 const float max_x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x;
6788 if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_SpanAvailWidth))
6789 size.x = ImMax(lhs: label_size.x, rhs: max_x - min_x);
6790
6791 // Text stays at the submission position, but bounding box may be extended on both sides
6792 const ImVec2 text_min = pos;
6793 const ImVec2 text_max(min_x + size.x, pos.y + size.y);
6794
6795 // Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable.
6796 // FIXME: Not part of layout so not included in clipper calculation, but ItemSize currently doesn't allow offsetting CursorPos.
6797 ImRect bb(min_x, pos.y, text_max.x, text_max.y);
6798 if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0)
6799 {
6800 const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x;
6801 const float spacing_y = style.ItemSpacing.y;
6802 const float spacing_L = IM_TRUNC(spacing_x * 0.50f);
6803 const float spacing_U = IM_TRUNC(spacing_y * 0.50f);
6804 bb.Min.x -= spacing_L;
6805 bb.Min.y -= spacing_U;
6806 bb.Max.x += (spacing_x - spacing_L);
6807 bb.Max.y += (spacing_y - spacing_U);
6808 }
6809 //if (g.IO.KeyCtrl) { GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(0, 255, 0, 255)); }
6810
6811 const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0;
6812 const ImGuiItemFlags extra_item_flags = disabled_item ? (ImGuiItemFlags)ImGuiItemFlags_Disabled : ImGuiItemFlags_None;
6813 bool is_visible;
6814 if (span_all_columns)
6815 {
6816 // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable..
6817 const float backup_clip_rect_min_x = window->ClipRect.Min.x;
6818 const float backup_clip_rect_max_x = window->ClipRect.Max.x;
6819 window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
6820 window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
6821 is_visible = ItemAdd(bb, id, NULL, extra_flags: extra_item_flags);
6822 window->ClipRect.Min.x = backup_clip_rect_min_x;
6823 window->ClipRect.Max.x = backup_clip_rect_max_x;
6824 }
6825 else
6826 {
6827 is_visible = ItemAdd(bb, id, NULL, extra_flags: extra_item_flags);
6828 }
6829
6830 const bool is_multi_select = (g.LastItemData.InFlags & ImGuiItemFlags_IsMultiSelect) != 0;
6831 if (!is_visible)
6832 if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(r: bb)) // Extra layer of "no logic clip" for box-select support (would be more overhead to add to ItemAdd)
6833 return false;
6834
6835 const bool disabled_global = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0;
6836 if (disabled_item && !disabled_global) // Only testing this as an optimization
6837 BeginDisabled();
6838
6839 // FIXME: We can standardize the behavior of those two, we could also keep the fast path of override ClipRect + full push on render only,
6840 // which would be advantageous since most selectable are not selected.
6841 if (span_all_columns)
6842 {
6843 if (g.CurrentTable)
6844 TablePushBackgroundChannel();
6845 else if (window->DC.CurrentColumns)
6846 PushColumnsBackground();
6847 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect;
6848 g.LastItemData.ClipRect = window->ClipRect;
6849 }
6850
6851 // We use NoHoldingActiveID on menus so user can click and _hold_ on a menu then drag to browse child entries
6852 ImGuiButtonFlags button_flags = 0;
6853 if (flags & ImGuiSelectableFlags_NoHoldingActiveID) { button_flags |= ImGuiButtonFlags_NoHoldingActiveId; }
6854 if (flags & ImGuiSelectableFlags_NoSetKeyOwner) { button_flags |= ImGuiButtonFlags_NoSetKeyOwner; }
6855 if (flags & ImGuiSelectableFlags_SelectOnClick) { button_flags |= ImGuiButtonFlags_PressedOnClick; }
6856 if (flags & ImGuiSelectableFlags_SelectOnRelease) { button_flags |= ImGuiButtonFlags_PressedOnRelease; }
6857 if (flags & ImGuiSelectableFlags_AllowDoubleClick) { button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; }
6858 if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.InFlags & ImGuiItemFlags_AllowOverlap)) { button_flags |= ImGuiButtonFlags_AllowOverlap; }
6859
6860 // Multi-selection support (header)
6861 const bool was_selected = selected;
6862 if (is_multi_select)
6863 {
6864 // Handle multi-select + alter button flags for it
6865 MultiSelectItemHeader(id, p_selected: &selected, p_button_flags: &button_flags);
6866 }
6867
6868 bool hovered, held;
6869 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags: button_flags);
6870
6871 // Multi-selection support (footer)
6872 if (is_multi_select)
6873 {
6874 MultiSelectItemFooter(id, p_selected: &selected, p_pressed: &pressed);
6875 }
6876 else
6877 {
6878 // Auto-select when moved into
6879 // - This will be more fully fleshed in the range-select branch
6880 // - This is not exposed as it won't nicely work with some user side handling of shift/control
6881 // - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons
6882 // - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope())
6883 // - (2) usage will fail with clipped items
6884 // The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API.
6885 if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId)
6886 if (g.NavJustMovedToId == id)
6887 selected = pressed = true;
6888 }
6889
6890 // Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with gamepad/keyboard
6891 if (pressed || (hovered && (flags & ImGuiSelectableFlags_SetNavIdOnHover)))
6892 {
6893 if (!g.NavDisableMouseHover && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent)
6894 {
6895 SetNavID(id, nav_layer: window->DC.NavLayerCurrent, focus_scope_id: g.CurrentFocusScopeId, rect_rel: WindowRectAbsToRel(window, r: bb)); // (bb == NavRect)
6896 g.NavDisableHighlight = true;
6897 }
6898 }
6899 if (pressed)
6900 MarkItemEdited(id);
6901
6902 if (selected != was_selected)
6903 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
6904
6905 // Render
6906 if (is_visible)
6907 {
6908 const bool highlighted = hovered || (flags & ImGuiSelectableFlags_Highlight);
6909 if (highlighted || selected)
6910 {
6911 // FIXME-MULTISELECT: Styling: Color for 'selected' elements? ImGuiCol_HeaderSelected
6912 ImU32 col;
6913 if (selected && !highlighted)
6914 col = GetColorU32(col: ImLerp(a: GetStyleColorVec4(idx: ImGuiCol_Header), b: GetStyleColorVec4(idx: ImGuiCol_HeaderHovered), t: 0.5f));
6915 else
6916 col = GetColorU32(idx: (held && highlighted) ? ImGuiCol_HeaderActive : highlighted ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
6917 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: col, border: false, rounding: 0.0f);
6918 }
6919 if (g.NavId == id)
6920 {
6921 ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_Compact | ImGuiNavHighlightFlags_NoRounding;
6922 if (is_multi_select)
6923 nav_highlight_flags |= ImGuiNavHighlightFlags_AlwaysDraw; // Always show the nav rectangle
6924 RenderNavHighlight(bb, id, flags: nav_highlight_flags);
6925 }
6926 }
6927
6928 if (span_all_columns)
6929 {
6930 if (g.CurrentTable)
6931 TablePopBackgroundChannel();
6932 else if (window->DC.CurrentColumns)
6933 PopColumnsBackground();
6934 }
6935
6936 if (is_visible)
6937 RenderTextClipped(pos_min: text_min, pos_max: text_max, text: label, NULL, text_size_if_known: &label_size, align: style.SelectableTextAlign, clip_rect: &bb);
6938
6939 // Automatically close popups
6940 if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_NoAutoClosePopups) && (g.LastItemData.InFlags & ImGuiItemFlags_AutoClosePopups))
6941 CloseCurrentPopup();
6942
6943 if (disabled_item && !disabled_global)
6944 EndDisabled();
6945
6946 // Selectable() always returns a pressed state!
6947 // Users of BeginMultiSelect()/EndMultiSelect() scope: you may call ImGui::IsItemToggledSelection() to retrieve
6948 // selection toggle, only useful if you need that state updated (e.g. for rendering purpose) before reaching EndMultiSelect().
6949 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
6950 return pressed; //-V1020
6951}
6952
6953bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
6954{
6955 if (Selectable(label, selected: *p_selected, flags, size_arg))
6956 {
6957 *p_selected = !*p_selected;
6958 return true;
6959 }
6960 return false;
6961}
6962
6963
6964//-------------------------------------------------------------------------
6965// [SECTION] Widgets: Typing-Select support
6966//-------------------------------------------------------------------------
6967
6968// [Experimental] Currently not exposed in public API.
6969// Consume character inputs and return search request, if any.
6970// This would typically only be called on the focused window or location you want to grab inputs for, e.g.
6971// if (ImGui::IsWindowFocused(...))
6972// if (ImGuiTypingSelectRequest* req = ImGui::GetTypingSelectRequest())
6973// focus_idx = ImGui::TypingSelectFindMatch(req, my_items.size(), [](void*, int n) { return my_items[n]->Name; }, &my_items, -1);
6974// However the code is written in a way where calling it from multiple locations is safe (e.g. to obtain buffer).
6975ImGuiTypingSelectRequest* ImGui::GetTypingSelectRequest(ImGuiTypingSelectFlags flags)
6976{
6977 ImGuiContext& g = *GImGui;
6978 ImGuiTypingSelectState* data = &g.TypingSelectState;
6979 ImGuiTypingSelectRequest* out_request = &data->Request;
6980
6981 // Clear buffer
6982 const float TYPING_SELECT_RESET_TIMER = 1.80f; // FIXME: Potentially move to IO config.
6983 const int TYPING_SELECT_SINGLE_CHAR_COUNT_FOR_LOCK = 4; // Lock single char matching when repeating same char 4 times
6984 if (data->SearchBuffer[0] != 0)
6985 {
6986 bool clear_buffer = false;
6987 clear_buffer |= (g.NavFocusScopeId != data->FocusScope);
6988 clear_buffer |= (data->LastRequestTime + TYPING_SELECT_RESET_TIMER < g.Time);
6989 clear_buffer |= g.NavAnyRequest;
6990 clear_buffer |= g.ActiveId != 0 && g.NavActivateId == 0; // Allow temporary SPACE activation to not interfere
6991 clear_buffer |= IsKeyPressed(key: ImGuiKey_Escape) || IsKeyPressed(key: ImGuiKey_Enter);
6992 clear_buffer |= IsKeyPressed(key: ImGuiKey_Backspace) && (flags & ImGuiTypingSelectFlags_AllowBackspace) == 0;
6993 //if (clear_buffer) { IMGUI_DEBUG_LOG("GetTypingSelectRequest(): Clear SearchBuffer.\n"); }
6994 if (clear_buffer)
6995 data->Clear();
6996 }
6997
6998 // Append to buffer
6999 const int buffer_max_len = IM_ARRAYSIZE(data->SearchBuffer) - 1;
7000 int buffer_len = (int)strlen(s: data->SearchBuffer);
7001 bool select_request = false;
7002 for (ImWchar w : g.IO.InputQueueCharacters)
7003 {
7004 const int w_len = ImTextCountUtf8BytesFromStr(in_text: &w, in_text_end: &w + 1);
7005 if (w < 32 || (buffer_len == 0 && ImCharIsBlankW(c: w)) || (buffer_len + w_len > buffer_max_len)) // Ignore leading blanks
7006 continue;
7007 char w_buf[5];
7008 ImTextCharToUtf8(out_buf: w_buf, c: (unsigned int)w);
7009 if (data->SingleCharModeLock && w_len == out_request->SingleCharSize && memcmp(s1: w_buf, s2: data->SearchBuffer, n: w_len) == 0)
7010 {
7011 select_request = true; // Same character: don't need to append to buffer.
7012 continue;
7013 }
7014 if (data->SingleCharModeLock)
7015 {
7016 data->Clear(); // Different character: clear
7017 buffer_len = 0;
7018 }
7019 memcpy(dest: data->SearchBuffer + buffer_len, src: w_buf, n: w_len + 1); // Append
7020 buffer_len += w_len;
7021 select_request = true;
7022 }
7023 g.IO.InputQueueCharacters.resize(new_size: 0);
7024
7025 // Handle backspace
7026 if ((flags & ImGuiTypingSelectFlags_AllowBackspace) && IsKeyPressed(key: ImGuiKey_Backspace, flags: ImGuiInputFlags_Repeat))
7027 {
7028 char* p = (char*)(void*)ImTextFindPreviousUtf8Codepoint(in_text_start: data->SearchBuffer, in_text_curr: data->SearchBuffer + buffer_len);
7029 *p = 0;
7030 buffer_len = (int)(p - data->SearchBuffer);
7031 }
7032
7033 // Return request if any
7034 if (buffer_len == 0)
7035 return NULL;
7036 if (select_request)
7037 {
7038 data->FocusScope = g.NavFocusScopeId;
7039 data->LastRequestFrame = g.FrameCount;
7040 data->LastRequestTime = (float)g.Time;
7041 }
7042 out_request->Flags = flags;
7043 out_request->SearchBufferLen = buffer_len;
7044 out_request->SearchBuffer = data->SearchBuffer;
7045 out_request->SelectRequest = (data->LastRequestFrame == g.FrameCount);
7046 out_request->SingleCharMode = false;
7047 out_request->SingleCharSize = 0;
7048
7049 // Calculate if buffer contains the same character repeated.
7050 // - This can be used to implement a special search mode on first character.
7051 // - Performed on UTF-8 codepoint for correctness.
7052 // - SingleCharMode is always set for first input character, because it usually leads to a "next".
7053 if (flags & ImGuiTypingSelectFlags_AllowSingleCharMode)
7054 {
7055 const char* buf_begin = out_request->SearchBuffer;
7056 const char* buf_end = out_request->SearchBuffer + out_request->SearchBufferLen;
7057 const int c0_len = ImTextCountUtf8BytesFromChar(in_text: buf_begin, in_text_end: buf_end);
7058 const char* p = buf_begin + c0_len;
7059 for (; p < buf_end; p += c0_len)
7060 if (memcmp(s1: buf_begin, s2: p, n: (size_t)c0_len) != 0)
7061 break;
7062 const int single_char_count = (p == buf_end) ? (out_request->SearchBufferLen / c0_len) : 0;
7063 out_request->SingleCharMode = (single_char_count > 0 || data->SingleCharModeLock);
7064 out_request->SingleCharSize = (ImS8)c0_len;
7065 data->SingleCharModeLock |= (single_char_count >= TYPING_SELECT_SINGLE_CHAR_COUNT_FOR_LOCK); // From now on we stop search matching to lock to single char mode.
7066 }
7067
7068 return out_request;
7069}
7070
7071static int ImStrimatchlen(const char* s1, const char* s1_end, const char* s2)
7072{
7073 int match_len = 0;
7074 while (s1 < s1_end && ImToUpper(c: *s1++) == ImToUpper(c: *s2++))
7075 match_len++;
7076 return match_len;
7077}
7078
7079// Default handler for finding a result for typing-select. You may implement your own.
7080// You might want to display a tooltip to visualize the current request SearchBuffer
7081// When SingleCharMode is set:
7082// - it is better to NOT display a tooltip of other on-screen display indicator.
7083// - the index of the currently focused item is required.
7084// if your SetNextItemSelectionUserData() values are indices, you can obtain it from ImGuiMultiSelectIO::NavIdItem, otherwise from g.NavLastValidSelectionUserData.
7085int ImGui::TypingSelectFindMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx)
7086{
7087 if (req == NULL || req->SelectRequest == false) // Support NULL parameter so both calls can be done from same spot.
7088 return -1;
7089 int idx = -1;
7090 if (req->SingleCharMode && (req->Flags & ImGuiTypingSelectFlags_AllowSingleCharMode))
7091 idx = TypingSelectFindNextSingleCharMatch(req, items_count, get_item_name_func, user_data, nav_item_idx);
7092 else
7093 idx = TypingSelectFindBestLeadingMatch(req, items_count, get_item_name_func, user_data);
7094 if (idx != -1)
7095 NavRestoreHighlightAfterMove();
7096 return idx;
7097}
7098
7099// Special handling when a single character is repeated: perform search on a single letter and goes to next.
7100int ImGui::TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx)
7101{
7102 // FIXME: Assume selection user data is index. Would be extremely practical.
7103 //if (nav_item_idx == -1)
7104 // nav_item_idx = (int)g.NavLastValidSelectionUserData;
7105
7106 int first_match_idx = -1;
7107 bool return_next_match = false;
7108 for (int idx = 0; idx < items_count; idx++)
7109 {
7110 const char* item_name = get_item_name_func(user_data, idx);
7111 if (ImStrimatchlen(s1: req->SearchBuffer, s1_end: req->SearchBuffer + req->SingleCharSize, s2: item_name) < req->SingleCharSize)
7112 continue;
7113 if (return_next_match) // Return next matching item after current item.
7114 return idx;
7115 if (first_match_idx == -1 && nav_item_idx == -1) // Return first match immediately if we don't have a nav_item_idx value.
7116 return idx;
7117 if (first_match_idx == -1) // Record first match for wrapping.
7118 first_match_idx = idx;
7119 if (nav_item_idx == idx) // Record that we encountering nav_item so we can return next match.
7120 return_next_match = true;
7121 }
7122 return first_match_idx; // First result
7123}
7124
7125int ImGui::TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data)
7126{
7127 int longest_match_idx = -1;
7128 int longest_match_len = 0;
7129 for (int idx = 0; idx < items_count; idx++)
7130 {
7131 const char* item_name = get_item_name_func(user_data, idx);
7132 const int match_len = ImStrimatchlen(s1: req->SearchBuffer, s1_end: req->SearchBuffer + req->SearchBufferLen, s2: item_name);
7133 if (match_len <= longest_match_len)
7134 continue;
7135 longest_match_idx = idx;
7136 longest_match_len = match_len;
7137 if (match_len == req->SearchBufferLen)
7138 break;
7139 }
7140 return longest_match_idx;
7141}
7142
7143void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data)
7144{
7145#ifndef IMGUI_DISABLE_DEBUG_TOOLS
7146 Text(fmt: "SearchBuffer = \"%s\"", data->SearchBuffer);
7147 Text(fmt: "SingleCharMode = %d, Size = %d, Lock = %d", data->Request.SingleCharMode, data->Request.SingleCharSize, data->SingleCharModeLock);
7148 Text(fmt: "LastRequest = time: %.2f, frame: %d", data->LastRequestTime, data->LastRequestFrame);
7149#else
7150 IM_UNUSED(data);
7151#endif
7152}
7153
7154//-------------------------------------------------------------------------
7155// [SECTION] Widgets: Box-Select support
7156// This has been extracted away from Multi-Select logic in the hope that it could eventually be used elsewhere, but hasn't been yet.
7157//-------------------------------------------------------------------------
7158// Extra logic in MultiSelectItemFooter() and ImGuiListClipper::Step()
7159//-------------------------------------------------------------------------
7160// - BoxSelectPreStartDrag() [Internal]
7161// - BoxSelectActivateDrag() [Internal]
7162// - BoxSelectDeactivateDrag() [Internal]
7163// - BoxSelectScrollWithMouseDrag() [Internal]
7164// - BeginBoxSelect() [Internal]
7165// - EndBoxSelect() [Internal]
7166//-------------------------------------------------------------------------
7167
7168// Call on the initial click.
7169static void BoxSelectPreStartDrag(ImGuiID id, ImGuiSelectionUserData clicked_item)
7170{
7171 ImGuiContext& g = *GImGui;
7172 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7173 bs->ID = id;
7174 bs->IsStarting = true; // Consider starting box-select.
7175 bs->IsStartedFromVoid = (clicked_item == ImGuiSelectionUserData_Invalid);
7176 bs->IsStartedSetNavIdOnce = bs->IsStartedFromVoid;
7177 bs->KeyMods = g.IO.KeyMods;
7178 bs->StartPosRel = bs->EndPosRel = ImGui::WindowPosAbsToRel(window: g.CurrentWindow, p: g.IO.MousePos);
7179 bs->ScrollAccum = ImVec2(0.0f, 0.0f);
7180}
7181
7182static void BoxSelectActivateDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window)
7183{
7184 ImGuiContext& g = *GImGui;
7185 IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Activate\n", bs->ID);
7186 bs->IsActive = true;
7187 bs->Window = window;
7188 bs->IsStarting = false;
7189 ImGui::SetActiveID(id: bs->ID, window);
7190 ImGui::SetActiveIdUsingAllKeyboardKeys();
7191 if (bs->IsStartedFromVoid && (bs->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0)
7192 bs->RequestClear = true;
7193}
7194
7195static void BoxSelectDeactivateDrag(ImGuiBoxSelectState* bs)
7196{
7197 ImGuiContext& g = *GImGui;
7198 bs->IsActive = bs->IsStarting = false;
7199 if (g.ActiveId == bs->ID)
7200 {
7201 IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Deactivate\n", bs->ID);
7202 ImGui::ClearActiveID();
7203 }
7204 bs->ID = 0;
7205}
7206
7207static void BoxSelectScrollWithMouseDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window, const ImRect& inner_r)
7208{
7209 ImGuiContext& g = *GImGui;
7210 IM_ASSERT(bs->Window == window);
7211 for (int n = 0; n < 2; n++) // each axis
7212 {
7213 const float mouse_pos = g.IO.MousePos[n];
7214 const float dist = (mouse_pos > inner_r.Max[n]) ? mouse_pos - inner_r.Max[n] : (mouse_pos < inner_r.Min[n]) ? mouse_pos - inner_r.Min[n] : 0.0f;
7215 const float scroll_curr = window->Scroll[n];
7216 if (dist == 0.0f || (dist < 0.0f && scroll_curr < 0.0f) || (dist > 0.0f && scroll_curr >= window->ScrollMax[n]))
7217 continue;
7218
7219 const float speed_multiplier = ImLinearRemapClamp(s0: g.FontSize, s1: g.FontSize * 5.0f, d0: 1.0f, d1: 4.0f, x: ImAbs(x: dist)); // x1 to x4 depending on distance
7220 const float scroll_step = g.FontSize * 35.0f * speed_multiplier * ImSign(x: dist) * g.IO.DeltaTime;
7221 bs->ScrollAccum[n] += scroll_step;
7222
7223 // Accumulate into a stored value so we can handle high-framerate
7224 const float scroll_step_i = ImFloor(f: bs->ScrollAccum[n]);
7225 if (scroll_step_i == 0.0f)
7226 continue;
7227 if (n == 0)
7228 ImGui::SetScrollX(window, scroll_x: scroll_curr + scroll_step_i);
7229 else
7230 ImGui::SetScrollY(window, scroll_y: scroll_curr + scroll_step_i);
7231 bs->ScrollAccum[n] -= scroll_step_i;
7232 }
7233}
7234
7235bool ImGui::BeginBoxSelect(const ImRect& scope_rect, ImGuiWindow* window, ImGuiID box_select_id, ImGuiMultiSelectFlags ms_flags)
7236{
7237 ImGuiContext& g = *GImGui;
7238 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7239 KeepAliveID(id: box_select_id);
7240 if (bs->ID != box_select_id)
7241 return false;
7242
7243 // IsStarting is set by MultiSelectItemFooter() when considering a possible box-select. We validate it here and lock geometry.
7244 bs->UnclipMode = false;
7245 bs->RequestClear = false;
7246 if (bs->IsStarting && IsMouseDragPastThreshold(button: 0))
7247 BoxSelectActivateDrag(bs, window);
7248 else if ((bs->IsStarting || bs->IsActive) && g.IO.MouseDown[0] == false)
7249 BoxSelectDeactivateDrag(bs);
7250 if (!bs->IsActive)
7251 return false;
7252
7253 // Current frame absolute prev/current rectangles are used to toggle selection.
7254 // They are derived from positions relative to scrolling space.
7255 ImVec2 start_pos_abs = WindowPosRelToAbs(window, p: bs->StartPosRel);
7256 ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, p: bs->EndPosRel); // Clamped already
7257 ImVec2 curr_end_pos_abs = g.IO.MousePos;
7258 if (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) // Box-select scrolling only happens with ScopeWindow
7259 curr_end_pos_abs = ImClamp(v: curr_end_pos_abs, mn: scope_rect.Min, mx: scope_rect.Max);
7260 bs->BoxSelectRectPrev.Min = ImMin(lhs: start_pos_abs, rhs: prev_end_pos_abs);
7261 bs->BoxSelectRectPrev.Max = ImMax(lhs: start_pos_abs, rhs: prev_end_pos_abs);
7262 bs->BoxSelectRectCurr.Min = ImMin(lhs: start_pos_abs, rhs: curr_end_pos_abs);
7263 bs->BoxSelectRectCurr.Max = ImMax(lhs: start_pos_abs, rhs: curr_end_pos_abs);
7264
7265 // Box-select 2D mode detects horizontal changes (vertical ones are already picked by Clipper)
7266 // Storing an extra rect used by widgets supporting box-select.
7267 if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d)
7268 if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x)
7269 {
7270 bs->UnclipMode = true;
7271 bs->UnclipRect = bs->BoxSelectRectPrev; // FIXME-OPT: UnclipRect x coordinates could be intersection of Prev and Curr rect on X axis.
7272 bs->UnclipRect.Add(r: bs->BoxSelectRectCurr);
7273 }
7274
7275 //GetForegroundDrawList()->AddRect(bs->UnclipRect.Min, bs->UnclipRect.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f);
7276 //GetForegroundDrawList()->AddRect(bs->BoxSelectRectPrev.Min, bs->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f);
7277 //GetForegroundDrawList()->AddRect(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f);
7278 return true;
7279}
7280
7281void ImGui::EndBoxSelect(const ImRect& scope_rect, ImGuiMultiSelectFlags ms_flags)
7282{
7283 ImGuiContext& g = *GImGui;
7284 ImGuiWindow* window = g.CurrentWindow;
7285 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7286 IM_ASSERT(bs->IsActive);
7287 bs->UnclipMode = false;
7288
7289 // Render selection rectangle
7290 bs->EndPosRel = WindowPosAbsToRel(window, p: ImClamp(v: g.IO.MousePos, mn: scope_rect.Min, mx: scope_rect.Max)); // Clamp stored position according to current scrolling view
7291 ImRect box_select_r = bs->BoxSelectRectCurr;
7292 box_select_r.ClipWith(r: scope_rect);
7293 window->DrawList->AddRectFilled(p_min: box_select_r.Min, p_max: box_select_r.Max, col: GetColorU32(idx: ImGuiCol_SeparatorHovered, alpha_mul: 0.30f)); // FIXME-MULTISELECT: Styling
7294 window->DrawList->AddRect(p_min: box_select_r.Min, p_max: box_select_r.Max, col: GetColorU32(idx: ImGuiCol_NavHighlight)); // FIXME-MULTISELECT: Styling
7295
7296 // Scroll
7297 const bool enable_scroll = (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms_flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0;
7298 if (enable_scroll)
7299 {
7300 ImRect scroll_r = scope_rect;
7301 scroll_r.Expand(amount: -g.FontSize);
7302 //GetForegroundDrawList()->AddRect(scroll_r.Min, scroll_r.Max, IM_COL32(0, 255, 0, 255));
7303 if (!scroll_r.Contains(p: g.IO.MousePos))
7304 BoxSelectScrollWithMouseDrag(bs, window, inner_r: scroll_r);
7305 }
7306}
7307
7308//-------------------------------------------------------------------------
7309// [SECTION] Widgets: Multi-Select support
7310//-------------------------------------------------------------------------
7311// - DebugLogMultiSelectRequests() [Internal]
7312// - CalcScopeRect() [Internal]
7313// - BeginMultiSelect()
7314// - EndMultiSelect()
7315// - SetNextItemSelectionUserData()
7316// - MultiSelectItemHeader() [Internal]
7317// - MultiSelectItemFooter() [Internal]
7318// - DebugNodeMultiSelectState() [Internal]
7319//-------------------------------------------------------------------------
7320
7321static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* io)
7322{
7323 ImGuiContext& g = *GImGui;
7324 for (const ImGuiSelectionRequest& req : io->Requests)
7325 {
7326 if (req.Type == ImGuiSelectionRequestType_SetAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetAll %d (= %s)\n", function, req.Selected, req.Selected ? "SelectAll" : "Clear");
7327 if (req.Type == ImGuiSelectionRequestType_SetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetRange %" IM_PRId64 "..%" IM_PRId64 " (0x%" IM_PRIX64 "..0x%" IM_PRIX64 ") = %d (dir %d)\n", function, req.RangeFirstItem, req.RangeLastItem, req.RangeFirstItem, req.RangeLastItem, req.Selected, req.RangeDirection);
7328 }
7329}
7330
7331static ImRect CalcScopeRect(ImGuiMultiSelectTempData* ms, ImGuiWindow* window)
7332{
7333 if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)
7334 {
7335 // Warning: this depends on CursorMaxPos so it means to be called by EndMultiSelect() only
7336 return ImRect(ms->ScopeRectMin, ImMax(lhs: window->DC.CursorMaxPos, rhs: ms->ScopeRectMin));
7337 }
7338 else
7339 {
7340 // Add inner table decoration (#7821) // FIXME: Why not baking in InnerClipRect?
7341 ImRect scope_rect = window->InnerClipRect;
7342 scope_rect.Min = ImMin(lhs: scope_rect.Min + ImVec2(window->DecoInnerSizeX1, window->DecoInnerSizeY1), rhs: scope_rect.Max);
7343 return scope_rect;
7344 }
7345}
7346
7347// Return ImGuiMultiSelectIO structure.
7348// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect().
7349// Passing 'selection_size' and 'items_count' parameters is currently optional.
7350// - 'selection_size' is useful to disable some shortcut routing: e.g. ImGuiMultiSelectFlags_ClearOnEscape won't claim Escape key when selection_size 0,
7351// allowing a first press to clear selection THEN the second press to leave child window and return to parent.
7352// - 'items_count' is stored in ImGuiMultiSelectIO which makes it a convenient way to pass the information to your ApplyRequest() handler (but you may pass it differently).
7353// - If they are costly for you to compute (e.g. external intrusive selection without maintaining size), you may avoid them and pass -1.
7354// - If you can easily tell if your selection is empty or not, you may pass 0/1, or you may enable ImGuiMultiSelectFlags_ClearOnEscape flag dynamically.
7355ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int selection_size, int items_count)
7356{
7357 ImGuiContext& g = *GImGui;
7358 ImGuiWindow* window = g.CurrentWindow;
7359
7360 if (++g.MultiSelectTempDataStacked > g.MultiSelectTempData.Size)
7361 g.MultiSelectTempData.resize(new_size: g.MultiSelectTempDataStacked, v: ImGuiMultiSelectTempData());
7362 ImGuiMultiSelectTempData* ms = &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1];
7363 IM_STATIC_ASSERT(offsetof(ImGuiMultiSelectTempData, IO) == 0); // Clear() relies on that.
7364 g.CurrentMultiSelect = ms;
7365 if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0)
7366 flags |= ImGuiMultiSelectFlags_ScopeWindow;
7367 if (flags & ImGuiMultiSelectFlags_SingleSelect)
7368 flags &= ~(ImGuiMultiSelectFlags_BoxSelect2d | ImGuiMultiSelectFlags_BoxSelect1d);
7369 if (flags & ImGuiMultiSelectFlags_BoxSelect2d)
7370 flags &= ~ImGuiMultiSelectFlags_BoxSelect1d;
7371
7372 // FIXME: BeginFocusScope()
7373 const ImGuiID id = window->IDStack.back();
7374 ms->Clear();
7375 ms->FocusScopeId = id;
7376 ms->Flags = flags;
7377 ms->IsFocused = (ms->FocusScopeId == g.NavFocusScopeId);
7378 ms->BackupCursorMaxPos = window->DC.CursorMaxPos;
7379 ms->ScopeRectMin = window->DC.CursorMaxPos = window->DC.CursorPos;
7380 PushFocusScope(id: ms->FocusScopeId);
7381 if (flags & ImGuiMultiSelectFlags_ScopeWindow) // Mark parent child window as navigable into, with highlight. Assume user will always submit interactive items.
7382 window->DC.NavLayersActiveMask |= 1 << ImGuiNavLayer_Main;
7383
7384 // Use copy of keyboard mods at the time of the request, otherwise we would requires mods to be held for an extra frame.
7385 ms->KeyMods = g.NavJustMovedToId ? (g.NavJustMovedToIsTabbing ? 0 : g.NavJustMovedToKeyMods) : g.IO.KeyMods;
7386 if (flags & ImGuiMultiSelectFlags_NoRangeSelect)
7387 ms->KeyMods &= ~ImGuiMod_Shift;
7388
7389 // Bind storage
7390 ImGuiMultiSelectState* storage = g.MultiSelectStorage.GetOrAddByKey(key: id);
7391 storage->ID = id;
7392 storage->LastFrameActive = g.FrameCount;
7393 storage->LastSelectionSize = selection_size;
7394 storage->Window = window;
7395 ms->Storage = storage;
7396
7397 // Output to user
7398 ms->IO.Requests.resize(new_size: 0);
7399 ms->IO.RangeSrcItem = storage->RangeSrcItem;
7400 ms->IO.NavIdItem = storage->NavIdItem;
7401 ms->IO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false;
7402 ms->IO.ItemsCount = items_count;
7403
7404 // Clear when using Navigation to move within the scope
7405 // (we compare FocusScopeId so it possible to use multiple selections inside a same window)
7406 bool request_clear = false;
7407 bool request_select_all = false;
7408 if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData)
7409 {
7410 if (ms->KeyMods & ImGuiMod_Shift)
7411 ms->IsKeyboardSetRange = true;
7412 if (ms->IsKeyboardSetRange)
7413 IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid); // Not ready -> could clear?
7414 if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0)
7415 request_clear = true;
7416 }
7417 else if (g.NavJustMovedFromFocusScopeId == ms->FocusScopeId)
7418 {
7419 // Also clear on leaving scope (may be optional?)
7420 if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0)
7421 request_clear = true;
7422 }
7423
7424 // Box-select handling: update active state.
7425 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7426 if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7427 {
7428 ms->BoxSelectId = GetID(str_id: "##BoxSelect");
7429 if (BeginBoxSelect(scope_rect: CalcScopeRect(ms, window), window, box_select_id: ms->BoxSelectId, ms_flags: flags))
7430 request_clear |= bs->RequestClear;
7431 }
7432
7433 if (ms->IsFocused)
7434 {
7435 // Shortcut: Clear selection (Escape)
7436 // - Only claim shortcut if selection is not empty, allowing further presses on Escape to e.g. leave current child window.
7437 // - Box select also handle Escape and needs to pass an id to bypass ActiveIdUsingAllKeyboardKeys lock.
7438 if (flags & ImGuiMultiSelectFlags_ClearOnEscape)
7439 {
7440 if (selection_size != 0 || bs->IsActive)
7441 if (Shortcut(key_chord: ImGuiKey_Escape, flags: ImGuiInputFlags_None, owner_id: bs->IsActive ? bs->ID : 0))
7442 {
7443 request_clear = true;
7444 if (bs->IsActive)
7445 BoxSelectDeactivateDrag(bs);
7446 }
7447 }
7448
7449 // Shortcut: Select all (CTRL+A)
7450 if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll))
7451 if (Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiKey_A))
7452 request_select_all = true;
7453 }
7454
7455 if (request_clear || request_select_all)
7456 {
7457 MultiSelectAddSetAll(ms, selected: request_select_all);
7458 if (!request_select_all)
7459 storage->LastSelectionSize = 0;
7460 }
7461 ms->LoopRequestSetAll = request_select_all ? 1 : request_clear ? 0 : -1;
7462 ms->LastSubmittedItem = ImGuiSelectionUserData_Invalid;
7463
7464 if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)
7465 DebugLogMultiSelectRequests(function: "BeginMultiSelect", io: &ms->IO);
7466
7467 return &ms->IO;
7468}
7469
7470// Return updated ImGuiMultiSelectIO structure.
7471// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect().
7472ImGuiMultiSelectIO* ImGui::EndMultiSelect()
7473{
7474 ImGuiContext& g = *GImGui;
7475 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
7476 ImGuiMultiSelectState* storage = ms->Storage;
7477 ImGuiWindow* window = g.CurrentWindow;
7478 IM_ASSERT(ms->FocusScopeId == g.CurrentFocusScopeId);
7479 IM_ASSERT(g.CurrentMultiSelect != NULL && storage->Window == g.CurrentWindow);
7480 IM_ASSERT(g.MultiSelectTempDataStacked > 0 && &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] == g.CurrentMultiSelect);
7481
7482 ImRect scope_rect = CalcScopeRect(ms, window);
7483 if (ms->IsFocused)
7484 {
7485 // We currently don't allow user code to modify RangeSrcItem by writing to BeginIO's version, but that would be an easy change here.
7486 if (ms->IO.RangeSrcReset || (ms->RangeSrcPassedBy == false && ms->IO.RangeSrcItem != ImGuiSelectionUserData_Invalid)) // Can't read storage->RangeSrcItem here -> we want the state at begining of the scope (see tests for easy failure)
7487 {
7488 IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset RangeSrcItem.\n"); // Will set be to NavId.
7489 storage->RangeSrcItem = ImGuiSelectionUserData_Invalid;
7490 }
7491 if (ms->NavIdPassedBy == false && storage->NavIdItem != ImGuiSelectionUserData_Invalid)
7492 {
7493 IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset NavIdItem.\n");
7494 storage->NavIdItem = ImGuiSelectionUserData_Invalid;
7495 storage->NavIdSelected = -1;
7496 }
7497
7498 if ((ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) && GetBoxSelectState(id: ms->BoxSelectId))
7499 EndBoxSelect(scope_rect, ms_flags: ms->Flags);
7500 }
7501
7502 if (ms->IsEndIO == false)
7503 ms->IO.Requests.resize(new_size: 0);
7504
7505 // Clear selection when clicking void?
7506 // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection!
7507 // The InnerRect test is necessary for non-child/decorated windows.
7508 bool scope_hovered = IsWindowHovered() && window->InnerRect.Contains(p: g.IO.MousePos);
7509 if (scope_hovered && (ms->Flags & ImGuiMultiSelectFlags_ScopeRect))
7510 scope_hovered &= scope_rect.Contains(p: g.IO.MousePos);
7511 if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0)
7512 {
7513 if (ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7514 {
7515 if (!g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && g.IO.MouseClickedCount[0] == 1)
7516 {
7517 BoxSelectPreStartDrag(id: ms->BoxSelectId, ImGuiSelectionUserData_Invalid);
7518 FocusWindow(window, flags: ImGuiFocusRequestFlags_UnlessBelowModal);
7519 SetHoveredID(ms->BoxSelectId);
7520 if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)
7521 SetNavID(id: 0, nav_layer: ImGuiNavLayer_Main, focus_scope_id: ms->FocusScopeId, rect_rel: ImRect(g.IO.MousePos, g.IO.MousePos)); // Automatically switch FocusScope for initial click from void to box-select.
7522 }
7523 }
7524
7525 if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid)
7526 if (IsMouseReleased(button: 0) && IsMouseDragPastThreshold(button: 0) == false && g.IO.KeyMods == ImGuiMod_None)
7527 MultiSelectAddSetAll(ms, selected: false);
7528 }
7529
7530 // Courtesy nav wrapping helper flag
7531 if (ms->Flags & ImGuiMultiSelectFlags_NavWrapX)
7532 {
7533 IM_ASSERT(ms->Flags & ImGuiMultiSelectFlags_ScopeWindow); // Only supported at window scope
7534 ImGui::NavMoveRequestTryWrapping(window: ImGui::GetCurrentWindow(), move_flags: ImGuiNavMoveFlags_WrapX);
7535 }
7536
7537 // Unwind
7538 window->DC.CursorMaxPos = ImMax(lhs: ms->BackupCursorMaxPos, rhs: window->DC.CursorMaxPos);
7539 PopFocusScope();
7540
7541 if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)
7542 DebugLogMultiSelectRequests(function: "EndMultiSelect", io: &ms->IO);
7543
7544 ms->FocusScopeId = 0;
7545 ms->Flags = ImGuiMultiSelectFlags_None;
7546 g.CurrentMultiSelect = (--g.MultiSelectTempDataStacked > 0) ? &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] : NULL;
7547
7548 return &ms->IO;
7549}
7550
7551void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data)
7552{
7553 // Note that flags will be cleared by ItemAdd(), so it's only useful for Navigation code!
7554 // This designed so widgets can also cheaply set this before calling ItemAdd(), so we are not tied to MultiSelect api.
7555 ImGuiContext& g = *GImGui;
7556 g.NextItemData.SelectionUserData = selection_user_data;
7557 g.NextItemData.FocusScopeId = g.CurrentFocusScopeId;
7558
7559 if (ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect)
7560 {
7561 // Auto updating RangeSrcPassedBy for cases were clipper is not used (done before ItemAdd() clipping)
7562 g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData | ImGuiItemFlags_IsMultiSelect;
7563 if (ms->IO.RangeSrcItem == selection_user_data)
7564 ms->RangeSrcPassedBy = true;
7565 }
7566 else
7567 {
7568 g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData;
7569 }
7570}
7571
7572// In charge of:
7573// - Applying SetAll for submitted items.
7574// - Applying SetRange for submitted items and record end points.
7575// - Altering button behavior flags to facilitate use with drag and drop.
7576void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags)
7577{
7578 ImGuiContext& g = *GImGui;
7579 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
7580
7581 bool selected = *p_selected;
7582 if (ms->IsFocused)
7583 {
7584 ImGuiMultiSelectState* storage = ms->Storage;
7585 ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData;
7586 IM_ASSERT(g.NextItemData.FocusScopeId == g.CurrentFocusScopeId && "Forgot to call SetNextItemSelectionUserData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope");
7587
7588 // Apply SetAll (Clear/SelectAll) requests requested by BeginMultiSelect().
7589 // This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper.
7590 // If you are using a clipper you need to process the SetAll request after calling BeginMultiSelect()
7591 if (ms->LoopRequestSetAll != -1)
7592 selected = (ms->LoopRequestSetAll == 1);
7593
7594 // When using SHIFT+Nav: because it can incur scrolling we cannot afford a frame of lag with the selection highlight (otherwise scrolling would happen before selection)
7595 // For this to work, we need someone to set 'RangeSrcPassedBy = true' at some point (either clipper either SetNextItemSelectionUserData() function)
7596 if (ms->IsKeyboardSetRange)
7597 {
7598 IM_ASSERT(id != 0 && (ms->KeyMods & ImGuiMod_Shift) != 0);
7599 const bool is_range_dst = (ms->RangeDstPassedBy == false) && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped.
7600 if (is_range_dst)
7601 ms->RangeDstPassedBy = true;
7602 if (is_range_dst && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) // If we don't have RangeSrc, assign RangeSrc = RangeDst
7603 {
7604 storage->RangeSrcItem = item_data;
7605 storage->RangeSelected = selected ? 1 : 0;
7606 }
7607 const bool is_range_src = storage->RangeSrcItem == item_data;
7608 if (is_range_src || is_range_dst || ms->RangeSrcPassedBy != ms->RangeDstPassedBy)
7609 {
7610 // Apply range-select value to visible items
7611 IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid && storage->RangeSelected != -1);
7612 selected = (storage->RangeSelected != 0);
7613 }
7614 else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0 && (ms->Flags & ImGuiMultiSelectFlags_NoAutoClear) == 0)
7615 {
7616 // Clear other items
7617 selected = false;
7618 }
7619 }
7620 *p_selected = selected;
7621 }
7622
7623 // Alter button behavior flags
7624 // To handle drag and drop of multiple items we need to avoid clearing selection on click.
7625 // Enabling this test makes actions using CTRL+SHIFT delay their effect on MouseUp which is annoying, but it allows drag and drop of multiple items.
7626 if (p_button_flags != NULL)
7627 {
7628 ImGuiButtonFlags button_flags = *p_button_flags;
7629 button_flags |= ImGuiButtonFlags_NoHoveredOnFocus;
7630 if ((!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) && !(ms->Flags & ImGuiMultiSelectFlags_SelectOnClickRelease))
7631 button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease;
7632 else
7633 button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
7634 *p_button_flags = button_flags;
7635 }
7636}
7637
7638// In charge of:
7639// - Auto-select on navigation.
7640// - Box-select toggle handling.
7641// - Right-click handling.
7642// - Altering selection based on Ctrl/Shift modifiers, both for keyboard and mouse.
7643// - Record current selection state for RangeSrc
7644// This is all rather complex, best to run and refer to "widgets_multiselect_xxx" tests in imgui_test_suite.
7645void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed)
7646{
7647 ImGuiContext& g = *GImGui;
7648 ImGuiWindow* window = g.CurrentWindow;
7649
7650 bool selected = *p_selected;
7651 bool pressed = *p_pressed;
7652 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
7653 ImGuiMultiSelectState* storage = ms->Storage;
7654 if (pressed)
7655 ms->IsFocused = true;
7656
7657 bool hovered = false;
7658 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect)
7659 hovered = IsItemHovered(flags: ImGuiHoveredFlags_AllowWhenBlockedByPopup);
7660 if (!ms->IsFocused && !hovered)
7661 return;
7662
7663 ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData;
7664
7665 ImGuiMultiSelectFlags flags = ms->Flags;
7666 const bool is_singleselect = (flags & ImGuiMultiSelectFlags_SingleSelect) != 0;
7667 bool is_ctrl = (ms->KeyMods & ImGuiMod_Ctrl) != 0;
7668 bool is_shift = (ms->KeyMods & ImGuiMod_Shift) != 0;
7669
7670 bool apply_to_range_src = false;
7671
7672 if (g.NavId == id && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid)
7673 apply_to_range_src = true;
7674 if (ms->IsEndIO == false)
7675 {
7676 ms->IO.Requests.resize(new_size: 0);
7677 ms->IsEndIO = true;
7678 }
7679
7680 // Auto-select as you navigate a list
7681 if (g.NavJustMovedToId == id)
7682 {
7683 if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
7684 {
7685 if (is_ctrl && is_shift)
7686 pressed = true;
7687 else if (!is_ctrl)
7688 selected = pressed = true;
7689 }
7690 else
7691 {
7692 // With NoAutoSelect, using Shift+keyboard performs a write/copy
7693 if (is_shift)
7694 pressed = true;
7695 else if (!is_ctrl)
7696 apply_to_range_src = true; // Since if (pressed) {} main block is not running we update this
7697 }
7698 }
7699
7700 if (apply_to_range_src)
7701 {
7702 storage->RangeSrcItem = item_data;
7703 storage->RangeSelected = selected; // Will be updated at the end of this function anyway.
7704 }
7705
7706 // Box-select toggle handling
7707 if (ms->BoxSelectId != 0)
7708 if (ImGuiBoxSelectState* bs = GetBoxSelectState(id: ms->BoxSelectId))
7709 {
7710 const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(r: g.LastItemData.Rect);
7711 const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(r: g.LastItemData.Rect);
7712 if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr))
7713 {
7714 if (storage->LastSelectionSize <= 0 && bs->IsStartedSetNavIdOnce)
7715 {
7716 pressed = true; // First item act as a pressed: code below will emit selection request and set NavId (whatever we emit here will be overridden anyway)
7717 bs->IsStartedSetNavIdOnce = false;
7718 }
7719 else
7720 {
7721 selected = !selected;
7722 MultiSelectAddSetRange(ms, selected, range_dir: +1, first_item: item_data, last_item: item_data);
7723 }
7724 storage->LastSelectionSize = ImMax(lhs: storage->LastSelectionSize + 1, rhs: 1);
7725 }
7726 }
7727
7728 // Right-click handling.
7729 // FIXME-MULTISELECT: Currently filtered out by ImGuiMultiSelectFlags_NoAutoSelect but maybe should be moved to Selectable(). See https://github.com/ocornut/imgui/pull/5816
7730 if (hovered && IsMouseClicked(button: 1) && (flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
7731 {
7732 if (g.ActiveId != 0 && g.ActiveId != id)
7733 ClearActiveID();
7734 SetFocusID(id, window);
7735 if (!pressed && !selected)
7736 {
7737 pressed = true;
7738 is_ctrl = is_shift = false;
7739 }
7740 }
7741
7742 // Unlike Space, Enter doesn't alter selection (but can still return a press) unless current item is not selected.
7743 // The later, "unless current item is not select", may become optional? It seems like a better default if Enter doesn't necessarily open something
7744 // (unlike e.g. Windows explorer). For use case where Enter always open something, we might decide to make this optional?
7745 const bool enter_pressed = pressed && (g.NavActivateId == id) && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput);
7746
7747 // Alter selection
7748 if (pressed && (!enter_pressed || !selected))
7749 {
7750 // Box-select
7751 ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse;
7752 if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7753 if (selected == false && !g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1)
7754 BoxSelectPreStartDrag(id: ms->BoxSelectId, clicked_item: item_data);
7755
7756 //----------------------------------------------------------------------------------------
7757 // ACTION | Begin | Pressed/Activated | End
7758 //----------------------------------------------------------------------------------------
7759 // Keys Navigated: | Clear | Src=item, Sel=1 SetRange 1
7760 // Keys Navigated: Ctrl | n/a | n/a
7761 // Keys Navigated: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1
7762 // Keys Navigated: Ctrl+Shift | n/a | Dst=item, Sel=Src => Clear + SetRange Src-Dst
7763 // Keys Activated: | n/a | Src=item, Sel=1 => Clear + SetRange 1
7764 // Keys Activated: Ctrl | n/a | Src=item, Sel=!Sel => SetSange 1
7765 // Keys Activated: Shift | n/a | Dst=item, Sel=1 => Clear + SetSange 1
7766 //----------------------------------------------------------------------------------------
7767 // Mouse Pressed: | n/a | Src=item, Sel=1, => Clear + SetRange 1
7768 // Mouse Pressed: Ctrl | n/a | Src=item, Sel=!Sel => SetRange 1
7769 // Mouse Pressed: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1
7770 // Mouse Pressed: Ctrl+Shift | n/a | Dst=item, Sel=!Sel => SetRange Src-Dst
7771 //----------------------------------------------------------------------------------------
7772
7773 if ((flags & ImGuiMultiSelectFlags_NoAutoClear) == 0)
7774 {
7775 bool request_clear = false;
7776 if (is_singleselect)
7777 request_clear = true;
7778 else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl)
7779 request_clear = (flags & ImGuiMultiSelectFlags_NoAutoClearOnReselect) ? !selected : true;
7780 else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl)
7781 request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again.
7782 if (request_clear)
7783 MultiSelectAddSetAll(ms, selected: false);
7784 }
7785
7786 int range_direction;
7787 bool range_selected;
7788 if (is_shift && !is_singleselect)
7789 {
7790 //IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue);
7791 if (storage->RangeSrcItem == ImGuiSelectionUserData_Invalid)
7792 storage->RangeSrcItem = item_data;
7793 if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
7794 {
7795 // Shift+Arrow always select
7796 // Ctrl+Shift+Arrow copy source selection state (already stored by BeginMultiSelect() in storage->RangeSelected)
7797 range_selected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true;
7798 }
7799 else
7800 {
7801 // Shift+Arrow copy source selection state
7802 // Shift+Click always copy from target selection state
7803 if (ms->IsKeyboardSetRange)
7804 range_selected = (storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true;
7805 else
7806 range_selected = !selected;
7807 }
7808 range_direction = ms->RangeSrcPassedBy ? +1 : -1;
7809 }
7810 else
7811 {
7812 // Ctrl inverts selection, otherwise always select
7813 if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
7814 selected = is_ctrl ? !selected : true;
7815 else
7816 selected = !selected;
7817 storage->RangeSrcItem = item_data;
7818 range_selected = selected;
7819 range_direction = +1;
7820 }
7821 MultiSelectAddSetRange(ms, selected: range_selected, range_dir: range_direction, first_item: storage->RangeSrcItem, last_item: item_data);
7822 }
7823
7824 // Update/store the selection state of the Source item (used by CTRL+SHIFT, when Source is unselected we perform a range unselect)
7825 if (storage->RangeSrcItem == item_data)
7826 storage->RangeSelected = selected ? 1 : 0;
7827
7828 // Update/store the selection state of focused item
7829 if (g.NavId == id)
7830 {
7831 storage->NavIdItem = item_data;
7832 storage->NavIdSelected = selected ? 1 : 0;
7833 }
7834 if (storage->NavIdItem == item_data)
7835 ms->NavIdPassedBy = true;
7836 ms->LastSubmittedItem = item_data;
7837
7838 *p_selected = selected;
7839 *p_pressed = pressed;
7840}
7841
7842void ImGui::MultiSelectAddSetAll(ImGuiMultiSelectTempData* ms, bool selected)
7843{
7844 ImGuiSelectionRequest req = { .Type: ImGuiSelectionRequestType_SetAll, .Selected: selected, .RangeDirection: 0, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid };
7845 ms->IO.Requests.resize(new_size: 0); // Can always clear previous requests
7846 ms->IO.Requests.push_back(v: req); // Add new request
7847}
7848
7849void ImGui::MultiSelectAddSetRange(ImGuiMultiSelectTempData* ms, bool selected, int range_dir, ImGuiSelectionUserData first_item, ImGuiSelectionUserData last_item)
7850{
7851 // Merge contiguous spans into same request (unless NoRangeSelect is set which guarantees single-item ranges)
7852 if (ms->IO.Requests.Size > 0 && first_item == last_item && (ms->Flags & ImGuiMultiSelectFlags_NoRangeSelect) == 0)
7853 {
7854 ImGuiSelectionRequest* prev = &ms->IO.Requests.Data[ms->IO.Requests.Size - 1];
7855 if (prev->Type == ImGuiSelectionRequestType_SetRange && prev->RangeLastItem == ms->LastSubmittedItem && prev->Selected == selected)
7856 {
7857 prev->RangeLastItem = last_item;
7858 return;
7859 }
7860 }
7861
7862 ImGuiSelectionRequest req = { .Type: ImGuiSelectionRequestType_SetRange, .Selected: selected, .RangeDirection: (ImS8)range_dir, .RangeFirstItem: (range_dir > 0) ? first_item : last_item, .RangeLastItem: (range_dir > 0) ? last_item : first_item };
7863 ms->IO.Requests.push_back(v: req); // Add new request
7864}
7865
7866void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage)
7867{
7868#ifndef IMGUI_DISABLE_DEBUG_TOOLS
7869 const bool is_active = (storage->LastFrameActive >= GetFrameCount() - 2); // Note that fully clipped early out scrolling tables will appear as inactive here.
7870 if (!is_active) { PushStyleColor(idx: ImGuiCol_Text, col: GetStyleColorVec4(idx: ImGuiCol_TextDisabled)); }
7871 bool open = TreeNode(ptr_id: (void*)(intptr_t)storage->ID, fmt: "MultiSelect 0x%08X in '%s'%s", storage->ID, storage->Window ? storage->Window->Name : "N/A", is_active ? "" : " *Inactive*");
7872 if (!is_active) { PopStyleColor(); }
7873 if (!open)
7874 return;
7875 Text(fmt: "RangeSrcItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), RangeSelected = %d", storage->RangeSrcItem, storage->RangeSrcItem, storage->RangeSelected);
7876 Text(fmt: "NavIdItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), NavIdSelected = %d", storage->NavIdItem, storage->NavIdItem, storage->NavIdSelected);
7877 Text(fmt: "LastSelectionSize = %d", storage->LastSelectionSize); // Provided by user
7878 TreePop();
7879#else
7880 IM_UNUSED(storage);
7881#endif
7882}
7883
7884//-------------------------------------------------------------------------
7885// [SECTION] Widgets: Multi-Select helpers
7886//-------------------------------------------------------------------------
7887// - ImGuiSelectionBasicStorage
7888// - ImGuiSelectionExternalStorage
7889//-------------------------------------------------------------------------
7890
7891ImGuiSelectionBasicStorage::ImGuiSelectionBasicStorage()
7892{
7893 Size = 0;
7894 PreserveOrder = false;
7895 UserData = NULL;
7896 AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; };
7897 _SelectionOrder = 1; // Always >0
7898}
7899
7900void ImGuiSelectionBasicStorage::Clear()
7901{
7902 Size = 0;
7903 _SelectionOrder = 1; // Always >0
7904 _Storage.Data.resize(new_size: 0);
7905}
7906
7907void ImGuiSelectionBasicStorage::Swap(ImGuiSelectionBasicStorage& r)
7908{
7909 ImSwap(a&: Size, b&: r.Size);
7910 ImSwap(a&: _SelectionOrder, b&: r._SelectionOrder);
7911 _Storage.Data.swap(rhs&: r._Storage.Data);
7912}
7913
7914bool ImGuiSelectionBasicStorage::Contains(ImGuiID id) const
7915{
7916 return _Storage.GetInt(key: id, default_val: 0) != 0;
7917}
7918
7919static int IMGUI_CDECL PairComparerByValueInt(const void* lhs, const void* rhs)
7920{
7921 int lhs_v = ((const ImGuiStoragePair*)lhs)->val_i;
7922 int rhs_v = ((const ImGuiStoragePair*)rhs)->val_i;
7923 return (lhs_v > rhs_v ? +1 : lhs_v < rhs_v ? -1 : 0);
7924}
7925
7926// GetNextSelectedItem() is an abstraction allowing us to change our underlying actual storage system without impacting user.
7927// (e.g. store unselected vs compact down, compact down on demand, use raw ImVector<ImGuiID> instead of ImGuiStorage...)
7928bool ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it, ImGuiID* out_id)
7929{
7930 ImGuiStoragePair* it = (ImGuiStoragePair*)*opaque_it;
7931 ImGuiStoragePair* it_end = _Storage.Data.Data + _Storage.Data.Size;
7932 if (PreserveOrder && it == NULL && it_end != NULL)
7933 ImQsort(base: _Storage.Data.Data, count: (size_t)_Storage.Data.Size, size_of_element: sizeof(ImGuiStoragePair), compare_func: PairComparerByValueInt); // ~ImGuiStorage::BuildSortByValueInt()
7934 if (it == NULL)
7935 it = _Storage.Data.Data;
7936 IM_ASSERT(it >= _Storage.Data.Data && it <= it_end);
7937 if (it != it_end)
7938 while (it->val_i == 0 && it < it_end)
7939 it++;
7940 const bool has_more = (it != it_end);
7941 *opaque_it = has_more ? (void**)(it + 1) : (void**)(it);
7942 *out_id = has_more ? it->key : 0;
7943 if (PreserveOrder && !has_more)
7944 _Storage.BuildSortByKey();
7945 return has_more;
7946}
7947
7948void ImGuiSelectionBasicStorage::SetItemSelected(ImGuiID id, bool selected)
7949{
7950 int* p_int = _Storage.GetIntRef(key: id, default_val: 0);
7951 if (selected && *p_int == 0) { *p_int = _SelectionOrder++; Size++; }
7952 else if (!selected && *p_int != 0) { *p_int = 0; Size--; }
7953}
7954
7955// Optimized for batch edits (with same value of 'selected')
7956static void ImGuiSelectionBasicStorage_BatchSetItemSelected(ImGuiSelectionBasicStorage* selection, ImGuiID id, bool selected, int size_before_amends, int selection_order)
7957{
7958 ImGuiStorage* storage = &selection->_Storage;
7959 ImGuiStoragePair* it = ImLowerBound(in_begin: storage->Data.Data, in_end: storage->Data.Data + size_before_amends, key: id);
7960 const bool is_contained = (it != storage->Data.Data + size_before_amends) && (it->key == id);
7961 if (selected == (is_contained && it->val_i != 0))
7962 return;
7963 if (selected && !is_contained)
7964 storage->Data.push_back(v: ImGuiStoragePair(id, selection_order)); // Push unsorted at end of vector, will be sorted in SelectionMultiAmendsFinish()
7965 else if (is_contained)
7966 it->val_i = selected ? selection_order : 0; // Modify in-place.
7967 selection->Size += selected ? +1 : -1;
7968}
7969
7970static void ImGuiSelectionBasicStorage_BatchFinish(ImGuiSelectionBasicStorage* selection, bool selected, int size_before_amends)
7971{
7972 ImGuiStorage* storage = &selection->_Storage;
7973 if (selected && selection->Size != size_before_amends)
7974 storage->BuildSortByKey(); // When done selecting: sort everything
7975}
7976
7977// Apply requests coming from BeginMultiSelect() and EndMultiSelect().
7978// - Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen.
7979// - Honoring SetRange requests requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem.
7980// - In this demo we often submit indices to SetNextItemSelectionUserData() + store the same indices in persistent selection.
7981// - Your code may do differently. If you store pointers or objects ID in ImGuiSelectionUserData you may need to perform
7982// a lookup in order to have some way to iterate/interpolate between two items.
7983// - A full-featured application is likely to allow search/filtering which is likely to lead to using indices
7984// and constructing a view index <> object id/ptr data structure anyway.
7985// WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THIS UNNECESSARY 'ImGuiSelectionBasicStorage' INDIRECTION LOGIC.
7986// Notice that with the simplest adapter (using indices everywhere), all functions return their parameters.
7987// The most simple implementation (using indices everywhere) would look like:
7988// for (ImGuiSelectionRequest& req : ms_io->Requests)
7989// {
7990// if (req.Type == ImGuiSelectionRequestType_SetAll) { Clear(); if (req.Selected) { for (int n = 0; n < items_count; n++) { SetItemSelected(n, true); } }
7991// if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { SetItemSelected(n, ms_io->Selected); } }
7992// }
7993void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
7994{
7995 // For convenience we obtain ItemsCount as passed to BeginMultiSelect(), which is optional.
7996 // It makes sense when using ImGuiSelectionBasicStorage to simply pass your items count to BeginMultiSelect().
7997 // Other scheme may handle SetAll differently.
7998 IM_ASSERT(ms_io->ItemsCount != -1 && "Missing value for items_count in BeginMultiSelect() call!");
7999 IM_ASSERT(AdapterIndexToStorageId != NULL);
8000
8001 // This is optimized/specialized to cope with very large selections (e.g. 100k+ items)
8002 // - A simpler version could call SetItemSelected() directly instead of ImGuiSelectionBasicStorage_BatchSetItemSelected() + ImGuiSelectionBasicStorage_BatchFinish().
8003 // - Optimized select can append unsorted, then sort in a second pass. Optimized unselect can clear in-place then compact in a second pass.
8004 // - A more optimal version wouldn't even use ImGuiStorage but directly a ImVector<ImGuiID> to reduce bandwidth, but this is a reasonable trade off to reuse code.
8005 // - There are many ways this could be better optimized. The worse case scenario being: using BoxSelect2d in a grid, box-select scrolling down while wiggling
8006 // left and right: it affects coarse clipping + can emit multiple SetRange with 1 item each.)
8007 // FIXME-OPT: For each block of consecutive SetRange request:
8008 // - add all requests to a sorted list, store ID, selected, offset in ImGuiStorage.
8009 // - rewrite sorted storage a single time.
8010 for (ImGuiSelectionRequest& req : ms_io->Requests)
8011 {
8012 if (req.Type == ImGuiSelectionRequestType_SetAll)
8013 {
8014 Clear();
8015 if (req.Selected)
8016 {
8017 _Storage.Data.reserve(new_capacity: ms_io->ItemsCount);
8018 const int size_before_amends = _Storage.Data.Size;
8019 for (int idx = 0; idx < ms_io->ItemsCount; idx++, _SelectionOrder++)
8020 ImGuiSelectionBasicStorage_BatchSetItemSelected(selection: this, id: GetStorageIdFromIndex(idx), selected: req.Selected, size_before_amends, selection_order: _SelectionOrder);
8021 ImGuiSelectionBasicStorage_BatchFinish(selection: this, selected: req.Selected, size_before_amends);
8022 }
8023 }
8024 else if (req.Type == ImGuiSelectionRequestType_SetRange)
8025 {
8026 const int selection_changes = (int)req.RangeLastItem - (int)req.RangeFirstItem + 1;
8027 //ImGuiContext& g = *GImGui; IMGUI_DEBUG_LOG_SELECTION("Req %d/%d: set %d to %d\n", ms_io->Requests.index_from_ptr(&req), ms_io->Requests.Size, selection_changes, req.Selected);
8028 if (selection_changes == 1 || (selection_changes < Size / 100))
8029 {
8030 // Multiple sorted insertion + copy likely to be faster.
8031 // Technically we could do a single copy with a little more work (sort sequential SetRange requests)
8032 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++)
8033 SetItemSelected(id: GetStorageIdFromIndex(idx), selected: req.Selected);
8034 }
8035 else
8036 {
8037 // Append insertion + single sort likely be faster.
8038 // Use req.RangeDirection to set order field so that shift+clicking from 1 to 5 is different than shift+clicking from 5 to 1
8039 const int size_before_amends = _Storage.Data.Size;
8040 int selection_order = _SelectionOrder + ((req.RangeDirection < 0) ? selection_changes - 1 : 0);
8041 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++, selection_order += req.RangeDirection)
8042 ImGuiSelectionBasicStorage_BatchSetItemSelected(selection: this, id: GetStorageIdFromIndex(idx), selected: req.Selected, size_before_amends, selection_order);
8043 if (req.Selected)
8044 _SelectionOrder += selection_changes;
8045 ImGuiSelectionBasicStorage_BatchFinish(selection: this, selected: req.Selected, size_before_amends);
8046 }
8047 }
8048 }
8049}
8050
8051//-------------------------------------------------------------------------
8052
8053ImGuiSelectionExternalStorage::ImGuiSelectionExternalStorage()
8054{
8055 UserData = NULL;
8056 AdapterSetItemSelected = NULL;
8057}
8058
8059// Apply requests coming from BeginMultiSelect() and EndMultiSelect().
8060// We also pull 'ms_io->ItemsCount' as passed for BeginMultiSelect() for consistency with ImGuiSelectionBasicStorage
8061// This makes no assumption about underlying storage.
8062void ImGuiSelectionExternalStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
8063{
8064 IM_ASSERT(AdapterSetItemSelected);
8065 for (ImGuiSelectionRequest& req : ms_io->Requests)
8066 {
8067 if (req.Type == ImGuiSelectionRequestType_SetAll)
8068 for (int idx = 0; idx < ms_io->ItemsCount; idx++)
8069 AdapterSetItemSelected(this, idx, req.Selected);
8070 if (req.Type == ImGuiSelectionRequestType_SetRange)
8071 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++)
8072 AdapterSetItemSelected(this, idx, req.Selected);
8073 }
8074}
8075
8076//-------------------------------------------------------------------------
8077// [SECTION] Widgets: ListBox
8078//-------------------------------------------------------------------------
8079// - BeginListBox()
8080// - EndListBox()
8081// - ListBox()
8082//-------------------------------------------------------------------------
8083
8084// This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label.
8085// Tip: To have a list filling the entire window width, use size.x = -FLT_MIN and pass an non-visible label e.g. "##empty"
8086// Tip: If your vertical size is calculated from an item count (e.g. 10 * item_height) consider adding a fractional part to facilitate seeing scrolling boundaries (e.g. 10.25 * item_height).
8087bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg)
8088{
8089 ImGuiContext& g = *GImGui;
8090 ImGuiWindow* window = GetCurrentWindow();
8091 if (window->SkipItems)
8092 return false;
8093
8094 const ImGuiStyle& style = g.Style;
8095 const ImGuiID id = GetID(str_id: label);
8096 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
8097
8098 // Size default to hold ~7.25 items.
8099 // Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar.
8100 ImVec2 size = ImTrunc(v: CalcItemSize(size: size_arg, default_w: CalcItemWidth(), default_h: GetTextLineHeightWithSpacing() * 7.25f + style.FramePadding.y * 2.0f));
8101 ImVec2 frame_size = ImVec2(size.x, ImMax(lhs: size.y, rhs: label_size.y));
8102 ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
8103 ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
8104 g.NextItemData.ClearFlags();
8105
8106 if (!IsRectVisible(rect_min: bb.Min, rect_max: bb.Max))
8107 {
8108 ItemSize(size: bb.GetSize(), text_baseline_y: style.FramePadding.y);
8109 ItemAdd(bb, id: 0, nav_bb: &frame_bb);
8110 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
8111 return false;
8112 }
8113
8114 // FIXME-OPT: We could omit the BeginGroup() if label_size.x == 0.0f but would need to omit the EndGroup() as well.
8115 BeginGroup();
8116 if (label_size.x > 0.0f)
8117 {
8118 ImVec2 label_pos = ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y);
8119 RenderText(pos: label_pos, text: label);
8120 window->DC.CursorMaxPos = ImMax(lhs: window->DC.CursorMaxPos, rhs: label_pos + label_size);
8121 AlignTextToFramePadding();
8122 }
8123
8124 BeginChild(id, size: frame_bb.GetSize(), child_flags: ImGuiChildFlags_FrameStyle);
8125 return true;
8126}
8127
8128void ImGui::EndListBox()
8129{
8130 ImGuiContext& g = *GImGui;
8131 ImGuiWindow* window = g.CurrentWindow;
8132 IM_ASSERT((window->Flags & ImGuiWindowFlags_ChildWindow) && "Mismatched BeginListBox/EndListBox calls. Did you test the return value of BeginListBox?");
8133 IM_UNUSED(window);
8134
8135 EndChild();
8136 EndGroup(); // This is only required to be able to do IsItemXXX query on the whole ListBox including label
8137}
8138
8139bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items)
8140{
8141 const bool value_changed = ListBox(label, current_item, getter: Items_ArrayGetter, user_data: (void*)items, items_count, height_in_items: height_items);
8142 return value_changed;
8143}
8144
8145// This is merely a helper around BeginListBox(), EndListBox().
8146// Considering using those directly to submit custom data or store selection differently.
8147bool ImGui::ListBox(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), void* user_data, int items_count, int height_in_items)
8148{
8149 ImGuiContext& g = *GImGui;
8150
8151 // Calculate size from "height_in_items"
8152 if (height_in_items < 0)
8153 height_in_items = ImMin(lhs: items_count, rhs: 7);
8154 float height_in_items_f = height_in_items + 0.25f;
8155 ImVec2 size(0.0f, ImTrunc(f: GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f));
8156
8157 if (!BeginListBox(label, size_arg: size))
8158 return false;
8159
8160 // Assume all items have even height (= 1 line of text). If you need items of different height,
8161 // you can create a custom version of ListBox() in your code without using the clipper.
8162 bool value_changed = false;
8163 ImGuiListClipper clipper;
8164 clipper.Begin(items_count, items_height: GetTextLineHeightWithSpacing()); // We know exactly our line height here so we pass it as a minor optimization, but generally you don't need to.
8165 clipper.IncludeItemByIndex(item_index: *current_item);
8166 while (clipper.Step())
8167 for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
8168 {
8169 const char* item_text = getter(user_data, i);
8170 if (item_text == NULL)
8171 item_text = "*Unknown item*";
8172
8173 PushID(int_id: i);
8174 const bool item_selected = (i == *current_item);
8175 if (Selectable(label: item_text, selected: item_selected))
8176 {
8177 *current_item = i;
8178 value_changed = true;
8179 }
8180 if (item_selected)
8181 SetItemDefaultFocus();
8182 PopID();
8183 }
8184 EndListBox();
8185
8186 if (value_changed)
8187 MarkItemEdited(id: g.LastItemData.ID);
8188
8189 return value_changed;
8190}
8191
8192//-------------------------------------------------------------------------
8193// [SECTION] Widgets: PlotLines, PlotHistogram
8194//-------------------------------------------------------------------------
8195// - PlotEx() [Internal]
8196// - PlotLines()
8197// - PlotHistogram()
8198//-------------------------------------------------------------------------
8199// Plot/Graph widgets are not very good.
8200// Consider writing your own, or using a third-party one, see:
8201// - ImPlot https://github.com/epezent/implot
8202// - others https://github.com/ocornut/imgui/wiki/Useful-Extensions
8203//-------------------------------------------------------------------------
8204
8205int ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, const ImVec2& size_arg)
8206{
8207 ImGuiContext& g = *GImGui;
8208 ImGuiWindow* window = GetCurrentWindow();
8209 if (window->SkipItems)
8210 return -1;
8211
8212 const ImGuiStyle& style = g.Style;
8213 const ImGuiID id = window->GetID(str: label);
8214
8215 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
8216 const ImVec2 frame_size = CalcItemSize(size: size_arg, default_w: CalcItemWidth(), default_h: label_size.y + style.FramePadding.y * 2.0f);
8217
8218 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
8219 const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding);
8220 const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0));
8221 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
8222 if (!ItemAdd(bb: total_bb, id: 0, nav_bb: &frame_bb))
8223 return -1;
8224 const bool hovered = ItemHoverable(bb: frame_bb, id, item_flags: g.LastItemData.InFlags);
8225
8226 // Determine scale from values if not specified
8227 if (scale_min == FLT_MAX || scale_max == FLT_MAX)
8228 {
8229 float v_min = FLT_MAX;
8230 float v_max = -FLT_MAX;
8231 for (int i = 0; i < values_count; i++)
8232 {
8233 const float v = values_getter(data, i);
8234 if (v != v) // Ignore NaN values
8235 continue;
8236 v_min = ImMin(lhs: v_min, rhs: v);
8237 v_max = ImMax(lhs: v_max, rhs: v);
8238 }
8239 if (scale_min == FLT_MAX)
8240 scale_min = v_min;
8241 if (scale_max == FLT_MAX)
8242 scale_max = v_max;
8243 }
8244
8245 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: GetColorU32(idx: ImGuiCol_FrameBg), border: true, rounding: style.FrameRounding);
8246
8247 const int values_count_min = (plot_type == ImGuiPlotType_Lines) ? 2 : 1;
8248 int idx_hovered = -1;
8249 if (values_count >= values_count_min)
8250 {
8251 int res_w = ImMin(lhs: (int)frame_size.x, rhs: values_count) + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
8252 int item_count = values_count + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
8253
8254 // Tooltip on hover
8255 if (hovered && inner_bb.Contains(p: g.IO.MousePos))
8256 {
8257 const float t = ImClamp(v: (g.IO.MousePos.x - inner_bb.Min.x) / (inner_bb.Max.x - inner_bb.Min.x), mn: 0.0f, mx: 0.9999f);
8258 const int v_idx = (int)(t * item_count);
8259 IM_ASSERT(v_idx >= 0 && v_idx < values_count);
8260
8261 const float v0 = values_getter(data, (v_idx + values_offset) % values_count);
8262 const float v1 = values_getter(data, (v_idx + 1 + values_offset) % values_count);
8263 if (plot_type == ImGuiPlotType_Lines)
8264 SetTooltip("%d: %8.4g\n%d: %8.4g", v_idx, v0, v_idx + 1, v1);
8265 else if (plot_type == ImGuiPlotType_Histogram)
8266 SetTooltip("%d: %8.4g", v_idx, v0);
8267 idx_hovered = v_idx;
8268 }
8269
8270 const float t_step = 1.0f / (float)res_w;
8271 const float inv_scale = (scale_min == scale_max) ? 0.0f : (1.0f / (scale_max - scale_min));
8272
8273 float v0 = values_getter(data, (0 + values_offset) % values_count);
8274 float t0 = 0.0f;
8275 ImVec2 tp0 = ImVec2( t0, 1.0f - ImSaturate(f: (v0 - scale_min) * inv_scale) ); // Point in the normalized space of our target rectangle
8276 float histogram_zero_line_t = (scale_min * scale_max < 0.0f) ? (1 + scale_min * inv_scale) : (scale_min < 0.0f ? 0.0f : 1.0f); // Where does the zero line stands
8277
8278 const ImU32 col_base = GetColorU32(idx: (plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLines : ImGuiCol_PlotHistogram);
8279 const ImU32 col_hovered = GetColorU32(idx: (plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLinesHovered : ImGuiCol_PlotHistogramHovered);
8280
8281 for (int n = 0; n < res_w; n++)
8282 {
8283 const float t1 = t0 + t_step;
8284 const int v1_idx = (int)(t0 * item_count + 0.5f);
8285 IM_ASSERT(v1_idx >= 0 && v1_idx < values_count);
8286 const float v1 = values_getter(data, (v1_idx + values_offset + 1) % values_count);
8287 const ImVec2 tp1 = ImVec2( t1, 1.0f - ImSaturate(f: (v1 - scale_min) * inv_scale) );
8288
8289 // NB: Draw calls are merged together by the DrawList system. Still, we should render our batch are lower level to save a bit of CPU.
8290 ImVec2 pos0 = ImLerp(a: inner_bb.Min, b: inner_bb.Max, t: tp0);
8291 ImVec2 pos1 = ImLerp(a: inner_bb.Min, b: inner_bb.Max, t: (plot_type == ImGuiPlotType_Lines) ? tp1 : ImVec2(tp1.x, histogram_zero_line_t));
8292 if (plot_type == ImGuiPlotType_Lines)
8293 {
8294 window->DrawList->AddLine(p1: pos0, p2: pos1, col: idx_hovered == v1_idx ? col_hovered : col_base);
8295 }
8296 else if (plot_type == ImGuiPlotType_Histogram)
8297 {
8298 if (pos1.x >= pos0.x + 2.0f)
8299 pos1.x -= 1.0f;
8300 window->DrawList->AddRectFilled(p_min: pos0, p_max: pos1, col: idx_hovered == v1_idx ? col_hovered : col_base);
8301 }
8302
8303 t0 = t1;
8304 tp0 = tp1;
8305 }
8306 }
8307
8308 // Text overlay
8309 if (overlay_text)
8310 RenderTextClipped(pos_min: ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), pos_max: frame_bb.Max, text: overlay_text, NULL, NULL, align: ImVec2(0.5f, 0.0f));
8311
8312 if (label_size.x > 0.0f)
8313 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), text: label);
8314
8315 // Return hovered index or -1 if none are hovered.
8316 // This is currently not exposed in the public API because we need a larger redesign of the whole thing, but in the short-term we are making it available in PlotEx().
8317 return idx_hovered;
8318}
8319
8320struct ImGuiPlotArrayGetterData
8321{
8322 const float* Values;
8323 int Stride;
8324
8325 ImGuiPlotArrayGetterData(const float* values, int stride) { Values = values; Stride = stride; }
8326};
8327
8328static float Plot_ArrayGetter(void* data, int idx)
8329{
8330 ImGuiPlotArrayGetterData* plot_data = (ImGuiPlotArrayGetterData*)data;
8331 const float v = *(const float*)(const void*)((const unsigned char*)plot_data->Values + (size_t)idx * plot_data->Stride);
8332 return v;
8333}
8334
8335void ImGui::PlotLines(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride)
8336{
8337 ImGuiPlotArrayGetterData data(values, stride);
8338 PlotEx(plot_type: ImGuiPlotType_Lines, label, values_getter: &Plot_ArrayGetter, data: (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, size_arg: graph_size);
8339}
8340
8341void ImGui::PlotLines(const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size)
8342{
8343 PlotEx(plot_type: ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, size_arg: graph_size);
8344}
8345
8346void ImGui::PlotHistogram(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride)
8347{
8348 ImGuiPlotArrayGetterData data(values, stride);
8349 PlotEx(plot_type: ImGuiPlotType_Histogram, label, values_getter: &Plot_ArrayGetter, data: (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, size_arg: graph_size);
8350}
8351
8352void ImGui::PlotHistogram(const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size)
8353{
8354 PlotEx(plot_type: ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, size_arg: graph_size);
8355}
8356
8357//-------------------------------------------------------------------------
8358// [SECTION] Widgets: Value helpers
8359// Those is not very useful, legacy API.
8360//-------------------------------------------------------------------------
8361// - Value()
8362//-------------------------------------------------------------------------
8363
8364void ImGui::Value(const char* prefix, bool b)
8365{
8366 Text(fmt: "%s: %s", prefix, (b ? "true" : "false"));
8367}
8368
8369void ImGui::Value(const char* prefix, int v)
8370{
8371 Text(fmt: "%s: %d", prefix, v);
8372}
8373
8374void ImGui::Value(const char* prefix, unsigned int v)
8375{
8376 Text(fmt: "%s: %d", prefix, v);
8377}
8378
8379void ImGui::Value(const char* prefix, float v, const char* float_format)
8380{
8381 if (float_format)
8382 {
8383 char fmt[64];
8384 ImFormatString(buf: fmt, IM_ARRAYSIZE(fmt), fmt: "%%s: %s", float_format);
8385 Text(fmt, prefix, v);
8386 }
8387 else
8388 {
8389 Text(fmt: "%s: %.3f", prefix, v);
8390 }
8391}
8392
8393//-------------------------------------------------------------------------
8394// [SECTION] MenuItem, BeginMenu, EndMenu, etc.
8395//-------------------------------------------------------------------------
8396// - ImGuiMenuColumns [Internal]
8397// - BeginMenuBar()
8398// - EndMenuBar()
8399// - BeginMainMenuBar()
8400// - EndMainMenuBar()
8401// - BeginMenu()
8402// - EndMenu()
8403// - MenuItemEx() [Internal]
8404// - MenuItem()
8405//-------------------------------------------------------------------------
8406
8407// Helpers for internal use
8408void ImGuiMenuColumns::Update(float spacing, bool window_reappearing)
8409{
8410 if (window_reappearing)
8411 memset(s: Widths, c: 0, n: sizeof(Widths));
8412 Spacing = (ImU16)spacing;
8413 CalcNextTotalWidth(update_offsets: true);
8414 memset(s: Widths, c: 0, n: sizeof(Widths));
8415 TotalWidth = NextTotalWidth;
8416 NextTotalWidth = 0;
8417}
8418
8419void ImGuiMenuColumns::CalcNextTotalWidth(bool update_offsets)
8420{
8421 ImU16 offset = 0;
8422 bool want_spacing = false;
8423 for (int i = 0; i < IM_ARRAYSIZE(Widths); i++)
8424 {
8425 ImU16 width = Widths[i];
8426 if (want_spacing && width > 0)
8427 offset += Spacing;
8428 want_spacing |= (width > 0);
8429 if (update_offsets)
8430 {
8431 if (i == 1) { OffsetLabel = offset; }
8432 if (i == 2) { OffsetShortcut = offset; }
8433 if (i == 3) { OffsetMark = offset; }
8434 }
8435 offset += width;
8436 }
8437 NextTotalWidth = offset;
8438}
8439
8440float ImGuiMenuColumns::DeclColumns(float w_icon, float w_label, float w_shortcut, float w_mark)
8441{
8442 Widths[0] = ImMax(lhs: Widths[0], rhs: (ImU16)w_icon);
8443 Widths[1] = ImMax(lhs: Widths[1], rhs: (ImU16)w_label);
8444 Widths[2] = ImMax(lhs: Widths[2], rhs: (ImU16)w_shortcut);
8445 Widths[3] = ImMax(lhs: Widths[3], rhs: (ImU16)w_mark);
8446 CalcNextTotalWidth(update_offsets: false);
8447 return (float)ImMax(lhs: TotalWidth, rhs: NextTotalWidth);
8448}
8449
8450// FIXME: Provided a rectangle perhaps e.g. a BeginMenuBarEx() could be used anywhere..
8451// Currently the main responsibility of this function being to setup clip-rect + horizontal layout + menu navigation layer.
8452// Ideally we also want this to be responsible for claiming space out of the main window scrolling rectangle, in which case ImGuiWindowFlags_MenuBar will become unnecessary.
8453// Then later the same system could be used for multiple menu-bars, scrollbars, side-bars.
8454bool ImGui::BeginMenuBar()
8455{
8456 ImGuiWindow* window = GetCurrentWindow();
8457 if (window->SkipItems)
8458 return false;
8459 if (!(window->Flags & ImGuiWindowFlags_MenuBar))
8460 return false;
8461
8462 IM_ASSERT(!window->DC.MenuBarAppending);
8463 BeginGroup(); // Backup position on layer 0 // FIXME: Misleading to use a group for that backup/restore
8464 PushID(str_id: "##menubar");
8465
8466 // We don't clip with current window clipping rectangle as it is already set to the area below. However we clip with window full rect.
8467 // We remove 1 worth of rounding to Max.x to that text in long menus and small windows don't tend to display over the lower-right rounded area, which looks particularly glitchy.
8468 ImRect bar_rect = window->MenuBarRect();
8469 ImRect clip_rect(IM_ROUND(bar_rect.Min.x + window->WindowBorderSize), IM_ROUND(bar_rect.Min.y + window->WindowBorderSize), IM_ROUND(ImMax(bar_rect.Min.x, bar_rect.Max.x - ImMax(window->WindowRounding, window->WindowBorderSize))), IM_ROUND(bar_rect.Max.y));
8470 clip_rect.ClipWith(r: window->OuterRectClipped);
8471 PushClipRect(clip_rect_min: clip_rect.Min, clip_rect_max: clip_rect.Max, intersect_with_current_clip_rect: false);
8472
8473 // We overwrite CursorMaxPos because BeginGroup sets it to CursorPos (essentially the .EmitItem hack in EndMenuBar() would need something analogous here, maybe a BeginGroupEx() with flags).
8474 window->DC.CursorPos = window->DC.CursorMaxPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffset.x, bar_rect.Min.y + window->DC.MenuBarOffset.y);
8475 window->DC.LayoutType = ImGuiLayoutType_Horizontal;
8476 window->DC.IsSameLine = false;
8477 window->DC.NavLayerCurrent = ImGuiNavLayer_Menu;
8478 window->DC.MenuBarAppending = true;
8479 AlignTextToFramePadding();
8480 return true;
8481}
8482
8483void ImGui::EndMenuBar()
8484{
8485 ImGuiWindow* window = GetCurrentWindow();
8486 if (window->SkipItems)
8487 return;
8488 ImGuiContext& g = *GImGui;
8489
8490 // Nav: When a move request within one of our child menu failed, capture the request to navigate among our siblings.
8491 if (NavMoveRequestButNoResultYet() && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) && (g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu))
8492 {
8493 // Try to find out if the request is for one of our child menu
8494 ImGuiWindow* nav_earliest_child = g.NavWindow;
8495 while (nav_earliest_child->ParentWindow && (nav_earliest_child->ParentWindow->Flags & ImGuiWindowFlags_ChildMenu))
8496 nav_earliest_child = nav_earliest_child->ParentWindow;
8497 if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0)
8498 {
8499 // To do so we claim focus back, restore NavId and then process the movement request for yet another frame.
8500 // This involve a one-frame delay which isn't very problematic in this situation. We could remove it by scoring in advance for multiple window (probably not worth bothering)
8501 const ImGuiNavLayer layer = ImGuiNavLayer_Menu;
8502 IM_ASSERT(window->DC.NavLayersActiveMaskNext & (1 << layer)); // Sanity check (FIXME: Seems unnecessary)
8503 FocusWindow(window);
8504 SetNavID(id: window->NavLastIds[layer], nav_layer: layer, focus_scope_id: 0, rect_rel: window->NavRectRel[layer]);
8505 g.NavDisableHighlight = true; // Hide highlight for the current frame so we don't see the intermediary selection.
8506 g.NavDisableMouseHover = g.NavMousePosDirty = true;
8507 NavMoveRequestForward(move_dir: g.NavMoveDir, clip_dir: g.NavMoveClipDir, move_flags: g.NavMoveFlags, scroll_flags: g.NavMoveScrollFlags); // Repeat
8508 }
8509 }
8510
8511 IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'"
8512 IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar);
8513 IM_ASSERT(window->DC.MenuBarAppending);
8514 PopClipRect();
8515 PopID();
8516 window->DC.MenuBarOffset.x = window->DC.CursorPos.x - window->Pos.x; // Save horizontal position so next append can reuse it. This is kinda equivalent to a per-layer CursorPos.
8517
8518 // FIXME: Extremely confusing, cleanup by (a) working on WorkRect stack system (b) not using a Group confusingly here.
8519 ImGuiGroupData& group_data = g.GroupStack.back();
8520 group_data.EmitItem = false;
8521 ImVec2 restore_cursor_max_pos = group_data.BackupCursorMaxPos;
8522 window->DC.IdealMaxPos.x = ImMax(lhs: window->DC.IdealMaxPos.x, rhs: window->DC.CursorMaxPos.x - window->Scroll.x); // Convert ideal extents for scrolling layer equivalent.
8523 EndGroup(); // Restore position on layer 0 // FIXME: Misleading to use a group for that backup/restore
8524 window->DC.LayoutType = ImGuiLayoutType_Vertical;
8525 window->DC.IsSameLine = false;
8526 window->DC.NavLayerCurrent = ImGuiNavLayer_Main;
8527 window->DC.MenuBarAppending = false;
8528 window->DC.CursorMaxPos = restore_cursor_max_pos;
8529}
8530
8531// Important: calling order matters!
8532// FIXME: Somehow overlapping with docking tech.
8533// FIXME: The "rect-cut" aspect of this could be formalized into a lower-level helper (rect-cut: https://halt.software/dead-simple-layouts)
8534bool ImGui::BeginViewportSideBar(const char* name, ImGuiViewport* viewport_p, ImGuiDir dir, float axis_size, ImGuiWindowFlags window_flags)
8535{
8536 IM_ASSERT(dir != ImGuiDir_None);
8537
8538 ImGuiWindow* bar_window = FindWindowByName(name);
8539 if (bar_window == NULL || bar_window->BeginCount == 0)
8540 {
8541 // Calculate and set window size/position
8542 ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)(viewport_p ? viewport_p : GetMainViewport());
8543 ImRect avail_rect = viewport->GetBuildWorkRect();
8544 ImGuiAxis axis = (dir == ImGuiDir_Up || dir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X;
8545 ImVec2 pos = avail_rect.Min;
8546 if (dir == ImGuiDir_Right || dir == ImGuiDir_Down)
8547 pos[axis] = avail_rect.Max[axis] - axis_size;
8548 ImVec2 size = avail_rect.GetSize();
8549 size[axis] = axis_size;
8550 SetNextWindowPos(pos);
8551 SetNextWindowSize(size);
8552
8553 // Report our size into work area (for next frame) using actual window size
8554 if (dir == ImGuiDir_Up || dir == ImGuiDir_Left)
8555 viewport->BuildWorkOffsetMin[axis] += axis_size;
8556 else if (dir == ImGuiDir_Down || dir == ImGuiDir_Right)
8557 viewport->BuildWorkOffsetMax[axis] -= axis_size;
8558 }
8559
8560 window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
8561 PushStyleVar(idx: ImGuiStyleVar_WindowRounding, val: 0.0f);
8562 PushStyleVar(idx: ImGuiStyleVar_WindowMinSize, val: ImVec2(0, 0)); // Lift normal size constraint
8563 bool is_open = Begin(name, NULL, flags: window_flags);
8564 PopStyleVar(count: 2);
8565
8566 return is_open;
8567}
8568
8569bool ImGui::BeginMainMenuBar()
8570{
8571 ImGuiContext& g = *GImGui;
8572 ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport();
8573
8574 // For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set.
8575 // FIXME: This could be generalized as an opt-in way to clamp window->DC.CursorStartPos to avoid SafeArea?
8576 // FIXME: Consider removing support for safe area down the line... it's messy. Nowadays consoles have support for TV calibration in OS settings.
8577 g.NextWindowData.MenuBarOffsetMinVal = ImVec2(g.Style.DisplaySafeAreaPadding.x, ImMax(lhs: g.Style.DisplaySafeAreaPadding.y - g.Style.FramePadding.y, rhs: 0.0f));
8578 ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar;
8579 float height = GetFrameHeight();
8580 bool is_open = BeginViewportSideBar(name: "##MainMenuBar", viewport_p: viewport, dir: ImGuiDir_Up, axis_size: height, window_flags);
8581 g.NextWindowData.MenuBarOffsetMinVal = ImVec2(0.0f, 0.0f);
8582
8583 if (is_open)
8584 BeginMenuBar();
8585 else
8586 End();
8587 return is_open;
8588}
8589
8590void ImGui::EndMainMenuBar()
8591{
8592 EndMenuBar();
8593
8594 // When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window
8595 // FIXME: With this strategy we won't be able to restore a NULL focus.
8596 ImGuiContext& g = *GImGui;
8597 if (g.CurrentWindow == g.NavWindow && g.NavLayer == ImGuiNavLayer_Main && !g.NavAnyRequest)
8598 FocusTopMostWindowUnderOne(under_this_window: g.NavWindow, NULL, NULL, flags: ImGuiFocusRequestFlags_UnlessBelowModal | ImGuiFocusRequestFlags_RestoreFocusedChild);
8599
8600 End();
8601}
8602
8603static bool IsRootOfOpenMenuSet()
8604{
8605 ImGuiContext& g = *GImGui;
8606 ImGuiWindow* window = g.CurrentWindow;
8607 if ((g.OpenPopupStack.Size <= g.BeginPopupStack.Size) || (window->Flags & ImGuiWindowFlags_ChildMenu))
8608 return false;
8609
8610 // Initially we used 'upper_popup->OpenParentId == window->IDStack.back()' to differentiate multiple menu sets from each others
8611 // (e.g. inside menu bar vs loose menu items) based on parent ID.
8612 // This would however prevent the use of e.g. PushID() user code submitting menus.
8613 // Previously this worked between popup and a first child menu because the first child menu always had the _ChildWindow flag,
8614 // making hovering on parent popup possible while first child menu was focused - but this was generally a bug with other side effects.
8615 // Instead we don't treat Popup specifically (in order to consistently support menu features in them), maybe the first child menu of a Popup
8616 // doesn't have the _ChildWindow flag, and we rely on this IsRootOfOpenMenuSet() check to allow hovering between root window/popup and first child menu.
8617 // In the end, lack of ID check made it so we could no longer differentiate between separate menu sets. To compensate for that, we at least check parent window nav layer.
8618 // This fixes the most common case of menu opening on hover when moving between window content and menu bar. Multiple different menu sets in same nav layer would still
8619 // open on hover, but that should be a lesser problem, because if such menus are close in proximity in window content then it won't feel weird and if they are far apart
8620 // it likely won't be a problem anyone runs into.
8621 const ImGuiPopupData* upper_popup = &g.OpenPopupStack[g.BeginPopupStack.Size];
8622 if (window->DC.NavLayerCurrent != upper_popup->ParentNavLayer)
8623 return false;
8624 return upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu) && ImGui::IsWindowChildOf(window: upper_popup->Window, potential_parent: window, popup_hierarchy: true);
8625}
8626
8627bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
8628{
8629 ImGuiWindow* window = GetCurrentWindow();
8630 if (window->SkipItems)
8631 return false;
8632
8633 ImGuiContext& g = *GImGui;
8634 const ImGuiStyle& style = g.Style;
8635 const ImGuiID id = window->GetID(str: label);
8636 bool menu_is_open = IsPopupOpen(id, popup_flags: ImGuiPopupFlags_None);
8637
8638 // Sub-menus are ChildWindow so that mouse can be hovering across them (otherwise top-most popup menu would steal focus and not allow hovering on parent menu)
8639 // The first menu in a hierarchy isn't so hovering doesn't get across (otherwise e.g. resizing borders with ImGuiButtonFlags_FlattenChildren would react), but top-most BeginMenu() will bypass that limitation.
8640 ImGuiWindowFlags window_flags = ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus;
8641 if (window->Flags & ImGuiWindowFlags_ChildMenu)
8642 window_flags |= ImGuiWindowFlags_ChildWindow;
8643
8644 // If a menu with same the ID was already submitted, we will append to it, matching the behavior of Begin().
8645 // We are relying on a O(N) search - so O(N log N) over the frame - which seems like the most efficient for the expected small amount of BeginMenu() calls per frame.
8646 // If somehow this is ever becoming a problem we can switch to use e.g. ImGuiStorage mapping key to last frame used.
8647 if (g.MenusIdSubmittedThisFrame.contains(v: id))
8648 {
8649 if (menu_is_open)
8650 menu_is_open = BeginPopupEx(id, extra_window_flags: window_flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display)
8651 else
8652 g.NextWindowData.ClearFlags(); // we behave like Begin() and need to consume those values
8653 return menu_is_open;
8654 }
8655
8656 // Tag menu as used. Next time BeginMenu() with same ID is called it will append to existing menu
8657 g.MenusIdSubmittedThisFrame.push_back(v: id);
8658
8659 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
8660
8661 // Odd hack to allow hovering across menus of a same menu-set (otherwise we wouldn't be able to hover parent without always being a Child window)
8662 // This is only done for items for the menu set and not the full parent window.
8663 const bool menuset_is_open = IsRootOfOpenMenuSet();
8664 if (menuset_is_open)
8665 PushItemFlag(option: ImGuiItemFlags_NoWindowHoverableCheck, enabled: true);
8666
8667 // The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu,
8668 // However the final position is going to be different! It is chosen by FindBestWindowPosForPopup().
8669 // e.g. Menus tend to overlap each other horizontally to amplify relative Z-ordering.
8670 ImVec2 popup_pos, pos = window->DC.CursorPos;
8671 PushID(str_id: label);
8672 if (!enabled)
8673 BeginDisabled();
8674 const ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
8675 bool pressed;
8676
8677 // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
8678 const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SelectOnClick | ImGuiSelectableFlags_NoAutoClosePopups;
8679 if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
8680 {
8681 // Menu inside an horizontal menu bar
8682 // Selectable extend their highlight by half ItemSpacing in each direction.
8683 // For ChildMenu, the popup position will be overwritten by the call to FindBestWindowPosForPopup() in Begin()
8684 popup_pos = ImVec2(pos.x - 1.0f - IM_TRUNC(style.ItemSpacing.x * 0.5f), pos.y - style.FramePadding.y + window->MenuBarHeight);
8685 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f);
8686 PushStyleVar(idx: ImGuiStyleVar_ItemSpacing, val: ImVec2(style.ItemSpacing.x * 2.0f, style.ItemSpacing.y));
8687 float w = label_size.x;
8688 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
8689 pressed = Selectable(label: "", selected: menu_is_open, flags: selectable_flags, size_arg: ImVec2(w, label_size.y));
8690 RenderText(pos: text_pos, text: label);
8691 PopStyleVar();
8692 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar().
8693 }
8694 else
8695 {
8696 // Menu inside a regular/vertical menu
8697 // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
8698 // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.
8699 popup_pos = ImVec2(pos.x, pos.y - style.WindowPadding.y);
8700 float icon_w = (icon && icon[0]) ? CalcTextSize(text: icon, NULL).x : 0.0f;
8701 float checkmark_w = IM_TRUNC(g.FontSize * 1.20f);
8702 float min_w = window->DC.MenuColumns.DeclColumns(w_icon: icon_w, w_label: label_size.x, w_shortcut: 0.0f, w_mark: checkmark_w); // Feedback to next frame
8703 float extra_w = ImMax(lhs: 0.0f, rhs: GetContentRegionAvail().x - min_w);
8704 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
8705 pressed = Selectable(label: "", selected: menu_is_open, flags: selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, size_arg: ImVec2(min_w, label_size.y));
8706 RenderText(pos: text_pos, text: label);
8707 if (icon_w > 0.0f)
8708 RenderText(pos: pos + ImVec2(offsets->OffsetIcon, 0.0f), text: icon);
8709 RenderArrow(draw_list: window->DrawList, pos: pos + ImVec2(offsets->OffsetMark + extra_w + g.FontSize * 0.30f, 0.0f), col: GetColorU32(idx: ImGuiCol_Text), dir: ImGuiDir_Right);
8710 }
8711 if (!enabled)
8712 EndDisabled();
8713
8714 const bool hovered = (g.HoveredId == id) && enabled && !g.NavDisableMouseHover;
8715 if (menuset_is_open)
8716 PopItemFlag();
8717
8718 bool want_open = false;
8719 bool want_open_nav_init = false;
8720 bool want_close = false;
8721 if (window->DC.LayoutType == ImGuiLayoutType_Vertical) // (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu))
8722 {
8723 // Close menu when not hovering it anymore unless we are moving roughly in the direction of the menu
8724 // Implement http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown to avoid using timers, so menus feels more reactive.
8725 bool moving_toward_child_menu = false;
8726 ImGuiPopupData* child_popup = (g.BeginPopupStack.Size < g.OpenPopupStack.Size) ? &g.OpenPopupStack[g.BeginPopupStack.Size] : NULL; // Popup candidate (testing below)
8727 ImGuiWindow* child_menu_window = (child_popup && child_popup->Window && child_popup->Window->ParentWindow == window) ? child_popup->Window : NULL;
8728 if (g.HoveredWindow == window && child_menu_window != NULL)
8729 {
8730 const float ref_unit = g.FontSize; // FIXME-DPI
8731 const float child_dir = (window->Pos.x < child_menu_window->Pos.x) ? 1.0f : -1.0f;
8732 const ImRect next_window_rect = child_menu_window->Rect();
8733 ImVec2 ta = (g.IO.MousePos - g.IO.MouseDelta);
8734 ImVec2 tb = (child_dir > 0.0f) ? next_window_rect.GetTL() : next_window_rect.GetTR();
8735 ImVec2 tc = (child_dir > 0.0f) ? next_window_rect.GetBL() : next_window_rect.GetBR();
8736 const float pad_farmost_h = ImClamp(ImFabs(ta.x - tb.x) * 0.30f, mn: ref_unit * 0.5f, mx: ref_unit * 2.5f); // Add a bit of extra slack.
8737 ta.x += child_dir * -0.5f;
8738 tb.x += child_dir * ref_unit;
8739 tc.x += child_dir * ref_unit;
8740 tb.y = ta.y + ImMax(lhs: (tb.y - pad_farmost_h) - ta.y, rhs: -ref_unit * 8.0f); // Triangle has maximum height to limit the slope and the bias toward large sub-menus
8741 tc.y = ta.y + ImMin(lhs: (tc.y + pad_farmost_h) - ta.y, rhs: +ref_unit * 8.0f);
8742 moving_toward_child_menu = ImTriangleContainsPoint(a: ta, b: tb, c: tc, p: g.IO.MousePos);
8743 //GetForegroundDrawList()->AddTriangleFilled(ta, tb, tc, moving_toward_child_menu ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); // [DEBUG]
8744 }
8745
8746 // The 'HovereWindow == window' check creates an inconsistency (e.g. moving away from menu slowly tends to hit same window, whereas moving away fast does not)
8747 // But we also need to not close the top-menu menu when moving over void. Perhaps we should extend the triangle check to a larger polygon.
8748 // (Remember to test this on BeginPopup("A")->BeginMenu("B") sequence which behaves slightly differently as B isn't a Child of A and hovering isn't shared.)
8749 if (menu_is_open && !hovered && g.HoveredWindow == window && !moving_toward_child_menu && !g.NavDisableMouseHover && g.ActiveId == 0)
8750 want_close = true;
8751
8752 // Open
8753 // (note: at this point 'hovered' actually includes the NavDisableMouseHover == false test)
8754 if (!menu_is_open && pressed) // Click/activate to open
8755 want_open = true;
8756 else if (!menu_is_open && hovered && !moving_toward_child_menu) // Hover to open
8757 want_open = true;
8758 else if (!menu_is_open && hovered && g.HoveredIdTimer >= 0.30f && g.MouseStationaryTimer >= 0.30f) // Hover to open (timer fallback)
8759 want_open = true;
8760 if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open
8761 {
8762 want_open = want_open_nav_init = true;
8763 NavMoveRequestCancel();
8764 NavRestoreHighlightAfterMove();
8765 }
8766 }
8767 else
8768 {
8769 // Menu bar
8770 if (menu_is_open && pressed && menuset_is_open) // Click an open menu again to close it
8771 {
8772 want_close = true;
8773 want_open = menu_is_open = false;
8774 }
8775 else if (pressed || (hovered && menuset_is_open && !menu_is_open)) // First click to open, then hover to open others
8776 {
8777 want_open = true;
8778 }
8779 else if (g.NavId == id && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open
8780 {
8781 want_open = true;
8782 NavMoveRequestCancel();
8783 }
8784 }
8785
8786 if (!enabled) // explicitly close if an open menu becomes disabled, facilitate users code a lot in pattern such as 'if (BeginMenu("options", has_object)) { ..use object.. }'
8787 want_close = true;
8788 if (want_close && IsPopupOpen(id, popup_flags: ImGuiPopupFlags_None))
8789 ClosePopupToLevel(remaining: g.BeginPopupStack.Size, restore_focus_to_window_under_popup: true);
8790
8791 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Openable | (menu_is_open ? ImGuiItemStatusFlags_Opened : 0));
8792 PopID();
8793
8794 if (want_open && !menu_is_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size)
8795 {
8796 // Don't reopen/recycle same menu level in the same frame if it is a different menu ID, first close the other menu and yield for a frame.
8797 OpenPopup(str_id: label);
8798 }
8799 else if (want_open)
8800 {
8801 menu_is_open = true;
8802 OpenPopup(str_id: label, popup_flags: ImGuiPopupFlags_NoReopen);// | (want_open_nav_init ? ImGuiPopupFlags_NoReopenAlwaysNavInit : 0));
8803 }
8804
8805 if (menu_is_open)
8806 {
8807 ImGuiLastItemData last_item_in_parent = g.LastItemData;
8808 SetNextWindowPos(pos: popup_pos, cond: ImGuiCond_Always); // Note: misleading: the value will serve as reference for FindBestWindowPosForPopup(), not actual pos.
8809 PushStyleVar(idx: ImGuiStyleVar_ChildRounding, val: style.PopupRounding); // First level will use _PopupRounding, subsequent will use _ChildRounding
8810 menu_is_open = BeginPopupEx(id, extra_window_flags: window_flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display)
8811 PopStyleVar();
8812 if (menu_is_open)
8813 {
8814 // Implement what ImGuiPopupFlags_NoReopenAlwaysNavInit would do:
8815 // Perform an init request in the case the popup was already open (via a previous mouse hover)
8816 if (want_open && want_open_nav_init && !g.NavInitRequest)
8817 {
8818 FocusWindow(window: g.CurrentWindow, flags: ImGuiFocusRequestFlags_UnlessBelowModal);
8819 NavInitWindow(window: g.CurrentWindow, force_reinit: false);
8820 }
8821
8822 // Restore LastItemData so IsItemXXXX functions can work after BeginMenu()/EndMenu()
8823 // (This fixes using IsItemClicked() and IsItemHovered(), but IsItemHovered() also relies on its support for ImGuiItemFlags_NoWindowHoverableCheck)
8824 g.LastItemData = last_item_in_parent;
8825 if (g.HoveredWindow == window)
8826 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow;
8827 }
8828 }
8829 else
8830 {
8831 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
8832 }
8833
8834 return menu_is_open;
8835}
8836
8837bool ImGui::BeginMenu(const char* label, bool enabled)
8838{
8839 return BeginMenuEx(label, NULL, enabled);
8840}
8841
8842void ImGui::EndMenu()
8843{
8844 // Nav: When a left move request our menu failed, close ourselves.
8845 ImGuiContext& g = *GImGui;
8846 ImGuiWindow* window = g.CurrentWindow;
8847 IM_ASSERT(window->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginMenu()/EndMenu() calls
8848 ImGuiWindow* parent_window = window->ParentWindow; // Should always be != NULL is we passed assert.
8849 if (window->BeginCount == window->BeginCountPreviousFrame)
8850 if (g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet())
8851 if (g.NavWindow && (g.NavWindow->RootWindowForNav == window) && parent_window->DC.LayoutType == ImGuiLayoutType_Vertical)
8852 {
8853 ClosePopupToLevel(remaining: g.BeginPopupStack.Size - 1, restore_focus_to_window_under_popup: true);
8854 NavMoveRequestCancel();
8855 }
8856
8857 EndPopup();
8858}
8859
8860bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut, bool selected, bool enabled)
8861{
8862 ImGuiWindow* window = GetCurrentWindow();
8863 if (window->SkipItems)
8864 return false;
8865
8866 ImGuiContext& g = *GImGui;
8867 ImGuiStyle& style = g.Style;
8868 ImVec2 pos = window->DC.CursorPos;
8869 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
8870
8871 // See BeginMenuEx() for comments about this.
8872 const bool menuset_is_open = IsRootOfOpenMenuSet();
8873 if (menuset_is_open)
8874 PushItemFlag(option: ImGuiItemFlags_NoWindowHoverableCheck, enabled: true);
8875
8876 // We've been using the equivalent of ImGuiSelectableFlags_SetNavIdOnHover on all Selectable() since early Nav system days (commit 43ee5d73),
8877 // but I am unsure whether this should be kept at all. For now moved it to be an opt-in feature used by menus only.
8878 bool pressed;
8879 PushID(str_id: label);
8880 if (!enabled)
8881 BeginDisabled();
8882
8883 // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
8884 const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_SelectOnRelease | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SetNavIdOnHover;
8885 const ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
8886 if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
8887 {
8888 // Mimic the exact layout spacing of BeginMenu() to allow MenuItem() inside a menu bar, which is a little misleading but may be useful
8889 // Note that in this situation: we don't render the shortcut, we render a highlight instead of the selected tick mark.
8890 float w = label_size.x;
8891 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f);
8892 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
8893 PushStyleVar(idx: ImGuiStyleVar_ItemSpacing, val: ImVec2(style.ItemSpacing.x * 2.0f, style.ItemSpacing.y));
8894 pressed = Selectable(label: "", selected, flags: selectable_flags, size_arg: ImVec2(w, 0.0f));
8895 PopStyleVar();
8896 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
8897 RenderText(pos: text_pos, text: label);
8898 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar().
8899 }
8900 else
8901 {
8902 // Menu item inside a vertical menu
8903 // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
8904 // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.
8905 float icon_w = (icon && icon[0]) ? CalcTextSize(text: icon, NULL).x : 0.0f;
8906 float shortcut_w = (shortcut && shortcut[0]) ? CalcTextSize(text: shortcut, NULL).x : 0.0f;
8907 float checkmark_w = IM_TRUNC(g.FontSize * 1.20f);
8908 float min_w = window->DC.MenuColumns.DeclColumns(w_icon: icon_w, w_label: label_size.x, w_shortcut: shortcut_w, w_mark: checkmark_w); // Feedback for next frame
8909 float stretch_w = ImMax(lhs: 0.0f, rhs: GetContentRegionAvail().x - min_w);
8910 pressed = Selectable(label: "", selected: false, flags: selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, size_arg: ImVec2(min_w, label_size.y));
8911 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
8912 {
8913 RenderText(pos: pos + ImVec2(offsets->OffsetLabel, 0.0f), text: label);
8914 if (icon_w > 0.0f)
8915 RenderText(pos: pos + ImVec2(offsets->OffsetIcon, 0.0f), text: icon);
8916 if (shortcut_w > 0.0f)
8917 {
8918 PushStyleColor(idx: ImGuiCol_Text, col: style.Colors[ImGuiCol_TextDisabled]);
8919 RenderText(pos: pos + ImVec2(offsets->OffsetShortcut + stretch_w, 0.0f), text: shortcut, NULL, hide_text_after_hash: false);
8920 PopStyleColor();
8921 }
8922 if (selected)
8923 RenderCheckMark(draw_list: window->DrawList, pos: pos + ImVec2(offsets->OffsetMark + stretch_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), col: GetColorU32(idx: ImGuiCol_Text), sz: g.FontSize * 0.866f);
8924 }
8925 }
8926 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0));
8927 if (!enabled)
8928 EndDisabled();
8929 PopID();
8930 if (menuset_is_open)
8931 PopItemFlag();
8932
8933 return pressed;
8934}
8935
8936bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled)
8937{
8938 return MenuItemEx(label, NULL, shortcut, selected, enabled);
8939}
8940
8941bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled)
8942{
8943 if (MenuItemEx(label, NULL, shortcut, selected: p_selected ? *p_selected : false, enabled))
8944 {
8945 if (p_selected)
8946 *p_selected = !*p_selected;
8947 return true;
8948 }
8949 return false;
8950}
8951
8952//-------------------------------------------------------------------------
8953// [SECTION] Widgets: BeginTabBar, EndTabBar, etc.
8954//-------------------------------------------------------------------------
8955// - BeginTabBar()
8956// - BeginTabBarEx() [Internal]
8957// - EndTabBar()
8958// - TabBarLayout() [Internal]
8959// - TabBarCalcTabID() [Internal]
8960// - TabBarCalcMaxTabWidth() [Internal]
8961// - TabBarFindTabById() [Internal]
8962// - TabBarFindTabByOrder() [Internal]
8963// - TabBarGetCurrentTab() [Internal]
8964// - TabBarGetTabName() [Internal]
8965// - TabBarRemoveTab() [Internal]
8966// - TabBarCloseTab() [Internal]
8967// - TabBarScrollClamp() [Internal]
8968// - TabBarScrollToTab() [Internal]
8969// - TabBarQueueFocus() [Internal]
8970// - TabBarQueueReorder() [Internal]
8971// - TabBarProcessReorderFromMousePos() [Internal]
8972// - TabBarProcessReorder() [Internal]
8973// - TabBarScrollingButtons() [Internal]
8974// - TabBarTabListPopupButton() [Internal]
8975//-------------------------------------------------------------------------
8976
8977struct ImGuiTabBarSection
8978{
8979 int TabCount; // Number of tabs in this section.
8980 float Width; // Sum of width of tabs in this section (after shrinking down)
8981 float Spacing; // Horizontal spacing at the end of the section.
8982
8983 ImGuiTabBarSection() { memset(s: this, c: 0, n: sizeof(*this)); }
8984};
8985
8986namespace ImGui
8987{
8988 static void TabBarLayout(ImGuiTabBar* tab_bar);
8989 static ImU32 TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window);
8990 static float TabBarCalcMaxTabWidth();
8991 static float TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling);
8992 static void TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections);
8993 static ImGuiTabItem* TabBarScrollingButtons(ImGuiTabBar* tab_bar);
8994 static ImGuiTabItem* TabBarTabListPopupButton(ImGuiTabBar* tab_bar);
8995}
8996
8997ImGuiTabBar::ImGuiTabBar()
8998{
8999 memset(s: this, c: 0, n: sizeof(*this));
9000 CurrFrameVisible = PrevFrameVisible = -1;
9001 LastTabItemIdx = -1;
9002}
9003
9004static inline int TabItemGetSectionIdx(const ImGuiTabItem* tab)
9005{
9006 return (tab->Flags & ImGuiTabItemFlags_Leading) ? 0 : (tab->Flags & ImGuiTabItemFlags_Trailing) ? 2 : 1;
9007}
9008
9009static int IMGUI_CDECL TabItemComparerBySection(const void* lhs, const void* rhs)
9010{
9011 const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
9012 const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
9013 const int a_section = TabItemGetSectionIdx(tab: a);
9014 const int b_section = TabItemGetSectionIdx(tab: b);
9015 if (a_section != b_section)
9016 return a_section - b_section;
9017 return (int)(a->IndexDuringLayout - b->IndexDuringLayout);
9018}
9019
9020static int IMGUI_CDECL TabItemComparerByBeginOrder(const void* lhs, const void* rhs)
9021{
9022 const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
9023 const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
9024 return (int)(a->BeginOrder - b->BeginOrder);
9025}
9026
9027static ImGuiTabBar* GetTabBarFromTabBarRef(const ImGuiPtrOrIndex& ref)
9028{
9029 ImGuiContext& g = *GImGui;
9030 return ref.Ptr ? (ImGuiTabBar*)ref.Ptr : g.TabBars.GetByIndex(n: ref.Index);
9031}
9032
9033static ImGuiPtrOrIndex GetTabBarRefFromTabBar(ImGuiTabBar* tab_bar)
9034{
9035 ImGuiContext& g = *GImGui;
9036 if (g.TabBars.Contains(p: tab_bar))
9037 return ImGuiPtrOrIndex(g.TabBars.GetIndex(p: tab_bar));
9038 return ImGuiPtrOrIndex(tab_bar);
9039}
9040
9041bool ImGui::BeginTabBar(const char* str_id, ImGuiTabBarFlags flags)
9042{
9043 ImGuiContext& g = *GImGui;
9044 ImGuiWindow* window = g.CurrentWindow;
9045 if (window->SkipItems)
9046 return false;
9047
9048 ImGuiID id = window->GetID(str: str_id);
9049 ImGuiTabBar* tab_bar = g.TabBars.GetOrAddByKey(key: id);
9050 ImRect tab_bar_bb = ImRect(window->DC.CursorPos.x, window->DC.CursorPos.y, window->WorkRect.Max.x, window->DC.CursorPos.y + g.FontSize + g.Style.FramePadding.y * 2);
9051 tab_bar->ID = id;
9052 tab_bar->SeparatorMinX = tab_bar->BarRect.Min.x - IM_TRUNC(window->WindowPadding.x * 0.5f);
9053 tab_bar->SeparatorMaxX = tab_bar->BarRect.Max.x + IM_TRUNC(window->WindowPadding.x * 0.5f);
9054 //if (g.NavWindow && IsWindowChildOf(g.NavWindow, window, false, false))
9055 flags |= ImGuiTabBarFlags_IsFocused;
9056 return BeginTabBarEx(tab_bar, bb: tab_bar_bb, flags);
9057}
9058
9059bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImGuiTabBarFlags flags)
9060{
9061 ImGuiContext& g = *GImGui;
9062 ImGuiWindow* window = g.CurrentWindow;
9063 if (window->SkipItems)
9064 return false;
9065
9066 IM_ASSERT(tab_bar->ID != 0);
9067 if ((flags & ImGuiTabBarFlags_DockNode) == 0)
9068 PushOverrideID(id: tab_bar->ID);
9069
9070 // Add to stack
9071 g.CurrentTabBarStack.push_back(v: GetTabBarRefFromTabBar(tab_bar));
9072 g.CurrentTabBar = tab_bar;
9073
9074 // Append with multiple BeginTabBar()/EndTabBar() pairs.
9075 tab_bar->BackupCursorPos = window->DC.CursorPos;
9076 if (tab_bar->CurrFrameVisible == g.FrameCount)
9077 {
9078 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
9079 tab_bar->BeginCount++;
9080 return true;
9081 }
9082
9083 // Ensure correct ordering when toggling ImGuiTabBarFlags_Reorderable flag, or when a new tab was added while being not reorderable
9084 if ((flags & ImGuiTabBarFlags_Reorderable) != (tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (tab_bar->TabsAddedNew && !(flags & ImGuiTabBarFlags_Reorderable)))
9085 ImQsort(base: tab_bar->Tabs.Data, count: tab_bar->Tabs.Size, size_of_element: sizeof(ImGuiTabItem), compare_func: TabItemComparerByBeginOrder);
9086 tab_bar->TabsAddedNew = false;
9087
9088 // Flags
9089 if ((flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0)
9090 flags |= ImGuiTabBarFlags_FittingPolicyDefault_;
9091
9092 tab_bar->Flags = flags;
9093 tab_bar->BarRect = tab_bar_bb;
9094 tab_bar->WantLayout = true; // Layout will be done on the first call to ItemTab()
9095 tab_bar->PrevFrameVisible = tab_bar->CurrFrameVisible;
9096 tab_bar->CurrFrameVisible = g.FrameCount;
9097 tab_bar->PrevTabsContentsHeight = tab_bar->CurrTabsContentsHeight;
9098 tab_bar->CurrTabsContentsHeight = 0.0f;
9099 tab_bar->ItemSpacingY = g.Style.ItemSpacing.y;
9100 tab_bar->FramePadding = g.Style.FramePadding;
9101 tab_bar->TabsActiveCount = 0;
9102 tab_bar->LastTabItemIdx = -1;
9103 tab_bar->BeginCount = 1;
9104
9105 // Set cursor pos in a way which only be used in the off-chance the user erroneously submits item before BeginTabItem(): items will overlap
9106 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
9107
9108 // Draw separator
9109 // (it would be misleading to draw this in EndTabBar() suggesting that it may be drawn over tabs, as tab bar are appendable)
9110 const ImU32 col = GetColorU32(idx: (flags & ImGuiTabBarFlags_IsFocused) ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected);
9111 if (g.Style.TabBarBorderSize > 0.0f)
9112 {
9113 const float y = tab_bar->BarRect.Max.y;
9114 window->DrawList->AddRectFilled(p_min: ImVec2(tab_bar->SeparatorMinX, y - g.Style.TabBarBorderSize), p_max: ImVec2(tab_bar->SeparatorMaxX, y), col);
9115 }
9116 return true;
9117}
9118
9119void ImGui::EndTabBar()
9120{
9121 ImGuiContext& g = *GImGui;
9122 ImGuiWindow* window = g.CurrentWindow;
9123 if (window->SkipItems)
9124 return;
9125
9126 ImGuiTabBar* tab_bar = g.CurrentTabBar;
9127 if (tab_bar == NULL)
9128 {
9129 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Mismatched BeginTabBar()/EndTabBar()!");
9130 return;
9131 }
9132
9133 // Fallback in case no TabItem have been submitted
9134 if (tab_bar->WantLayout)
9135 TabBarLayout(tab_bar);
9136
9137 // Restore the last visible height if no tab is visible, this reduce vertical flicker/movement when a tabs gets removed without calling SetTabItemClosed().
9138 const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
9139 if (tab_bar->VisibleTabWasSubmitted || tab_bar->VisibleTabId == 0 || tab_bar_appearing)
9140 {
9141 tab_bar->CurrTabsContentsHeight = ImMax(lhs: window->DC.CursorPos.y - tab_bar->BarRect.Max.y, rhs: tab_bar->CurrTabsContentsHeight);
9142 window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->CurrTabsContentsHeight;
9143 }
9144 else
9145 {
9146 window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->PrevTabsContentsHeight;
9147 }
9148 if (tab_bar->BeginCount > 1)
9149 window->DC.CursorPos = tab_bar->BackupCursorPos;
9150
9151 tab_bar->LastTabItemIdx = -1;
9152 if ((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0)
9153 PopID();
9154
9155 g.CurrentTabBarStack.pop_back();
9156 g.CurrentTabBar = g.CurrentTabBarStack.empty() ? NULL : GetTabBarFromTabBarRef(ref: g.CurrentTabBarStack.back());
9157}
9158
9159// Scrolling happens only in the central section (leading/trailing sections are not scrolling)
9160static float TabBarCalcScrollableWidth(ImGuiTabBar* tab_bar, ImGuiTabBarSection* sections)
9161{
9162 return tab_bar->BarRect.GetWidth() - sections[0].Width - sections[2].Width - sections[1].Spacing;
9163}
9164
9165// This is called only once a frame before by the first call to ItemTab()
9166// The reason we're not calling it in BeginTabBar() is to leave a chance to the user to call the SetTabItemClosed() functions.
9167static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar)
9168{
9169 ImGuiContext& g = *GImGui;
9170 tab_bar->WantLayout = false;
9171
9172 // Garbage collect by compacting list
9173 // Detect if we need to sort out tab list (e.g. in rare case where a tab changed section)
9174 int tab_dst_n = 0;
9175 bool need_sort_by_section = false;
9176 ImGuiTabBarSection sections[3]; // Layout sections: Leading, Central, Trailing
9177 for (int tab_src_n = 0; tab_src_n < tab_bar->Tabs.Size; tab_src_n++)
9178 {
9179 ImGuiTabItem* tab = &tab_bar->Tabs[tab_src_n];
9180 if (tab->LastFrameVisible < tab_bar->PrevFrameVisible || tab->WantClose)
9181 {
9182 // Remove tab
9183 if (tab_bar->VisibleTabId == tab->ID) { tab_bar->VisibleTabId = 0; }
9184 if (tab_bar->SelectedTabId == tab->ID) { tab_bar->SelectedTabId = 0; }
9185 if (tab_bar->NextSelectedTabId == tab->ID) { tab_bar->NextSelectedTabId = 0; }
9186 continue;
9187 }
9188 if (tab_dst_n != tab_src_n)
9189 tab_bar->Tabs[tab_dst_n] = tab_bar->Tabs[tab_src_n];
9190
9191 tab = &tab_bar->Tabs[tab_dst_n];
9192 tab->IndexDuringLayout = (ImS16)tab_dst_n;
9193
9194 // We will need sorting if tabs have changed section (e.g. moved from one of Leading/Central/Trailing to another)
9195 int curr_tab_section_n = TabItemGetSectionIdx(tab);
9196 if (tab_dst_n > 0)
9197 {
9198 ImGuiTabItem* prev_tab = &tab_bar->Tabs[tab_dst_n - 1];
9199 int prev_tab_section_n = TabItemGetSectionIdx(tab: prev_tab);
9200 if (curr_tab_section_n == 0 && prev_tab_section_n != 0)
9201 need_sort_by_section = true;
9202 if (prev_tab_section_n == 2 && curr_tab_section_n != 2)
9203 need_sort_by_section = true;
9204 }
9205
9206 sections[curr_tab_section_n].TabCount++;
9207 tab_dst_n++;
9208 }
9209 if (tab_bar->Tabs.Size != tab_dst_n)
9210 tab_bar->Tabs.resize(new_size: tab_dst_n);
9211
9212 if (need_sort_by_section)
9213 ImQsort(base: tab_bar->Tabs.Data, count: tab_bar->Tabs.Size, size_of_element: sizeof(ImGuiTabItem), compare_func: TabItemComparerBySection);
9214
9215 // Calculate spacing between sections
9216 sections[0].Spacing = sections[0].TabCount > 0 && (sections[1].TabCount + sections[2].TabCount) > 0 ? g.Style.ItemInnerSpacing.x : 0.0f;
9217 sections[1].Spacing = sections[1].TabCount > 0 && sections[2].TabCount > 0 ? g.Style.ItemInnerSpacing.x : 0.0f;
9218
9219 // Setup next selected tab
9220 ImGuiID scroll_to_tab_id = 0;
9221 if (tab_bar->NextSelectedTabId)
9222 {
9223 tab_bar->SelectedTabId = tab_bar->NextSelectedTabId;
9224 tab_bar->NextSelectedTabId = 0;
9225 scroll_to_tab_id = tab_bar->SelectedTabId;
9226 }
9227
9228 // Process order change request (we could probably process it when requested but it's just saner to do it in a single spot).
9229 if (tab_bar->ReorderRequestTabId != 0)
9230 {
9231 if (TabBarProcessReorder(tab_bar))
9232 if (tab_bar->ReorderRequestTabId == tab_bar->SelectedTabId)
9233 scroll_to_tab_id = tab_bar->ReorderRequestTabId;
9234 tab_bar->ReorderRequestTabId = 0;
9235 }
9236
9237 // Tab List Popup (will alter tab_bar->BarRect and therefore the available width!)
9238 const bool tab_list_popup_button = (tab_bar->Flags & ImGuiTabBarFlags_TabListPopupButton) != 0;
9239 if (tab_list_popup_button)
9240 if (ImGuiTabItem* tab_to_select = TabBarTabListPopupButton(tab_bar)) // NB: Will alter BarRect.Min.x!
9241 scroll_to_tab_id = tab_bar->SelectedTabId = tab_to_select->ID;
9242
9243 // Leading/Trailing tabs will be shrink only if central one aren't visible anymore, so layout the shrink data as: leading, trailing, central
9244 // (whereas our tabs are stored as: leading, central, trailing)
9245 int shrink_buffer_indexes[3] = { 0, sections[0].TabCount + sections[2].TabCount, sections[0].TabCount };
9246 g.ShrinkWidthBuffer.resize(new_size: tab_bar->Tabs.Size);
9247
9248 // Compute ideal tabs widths + store them into shrink buffer
9249 ImGuiTabItem* most_recently_selected_tab = NULL;
9250 int curr_section_n = -1;
9251 bool found_selected_tab_id = false;
9252 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
9253 {
9254 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
9255 IM_ASSERT(tab->LastFrameVisible >= tab_bar->PrevFrameVisible);
9256
9257 if ((most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) && !(tab->Flags & ImGuiTabItemFlags_Button))
9258 most_recently_selected_tab = tab;
9259 if (tab->ID == tab_bar->SelectedTabId)
9260 found_selected_tab_id = true;
9261 if (scroll_to_tab_id == 0 && g.NavJustMovedToId == tab->ID)
9262 scroll_to_tab_id = tab->ID;
9263
9264 // Refresh tab width immediately, otherwise changes of style e.g. style.FramePadding.x would noticeably lag in the tab bar.
9265 // Additionally, when using TabBarAddTab() to manipulate tab bar order we occasionally insert new tabs that don't have a width yet,
9266 // and we cannot wait for the next BeginTabItem() call. We cannot compute this width within TabBarAddTab() because font size depends on the active window.
9267 const char* tab_name = TabBarGetTabName(tab_bar, tab);
9268 const bool has_close_button_or_unsaved_marker = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) == 0 || (tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
9269 tab->ContentWidth = (tab->RequestedWidth >= 0.0f) ? tab->RequestedWidth : TabItemCalcSize(label: tab_name, has_close_button_or_unsaved_marker).x;
9270
9271 int section_n = TabItemGetSectionIdx(tab);
9272 ImGuiTabBarSection* section = &sections[section_n];
9273 section->Width += tab->ContentWidth + (section_n == curr_section_n ? g.Style.ItemInnerSpacing.x : 0.0f);
9274 curr_section_n = section_n;
9275
9276 // Store data so we can build an array sorted by width if we need to shrink tabs down
9277 IM_MSVC_WARNING_SUPPRESS(6385);
9278 ImGuiShrinkWidthItem* shrink_width_item = &g.ShrinkWidthBuffer[shrink_buffer_indexes[section_n]++];
9279 shrink_width_item->Index = tab_n;
9280 shrink_width_item->Width = shrink_width_item->InitialWidth = tab->ContentWidth;
9281 tab->Width = ImMax(lhs: tab->ContentWidth, rhs: 1.0f);
9282 }
9283
9284 // Compute total ideal width (used for e.g. auto-resizing a window)
9285 tab_bar->WidthAllTabsIdeal = 0.0f;
9286 for (int section_n = 0; section_n < 3; section_n++)
9287 tab_bar->WidthAllTabsIdeal += sections[section_n].Width + sections[section_n].Spacing;
9288
9289 // Horizontal scrolling buttons
9290 // (note that TabBarScrollButtons() will alter BarRect.Max.x)
9291 if ((tab_bar->WidthAllTabsIdeal > tab_bar->BarRect.GetWidth() && tab_bar->Tabs.Size > 1) && !(tab_bar->Flags & ImGuiTabBarFlags_NoTabListScrollingButtons) && (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll))
9292 if (ImGuiTabItem* scroll_and_select_tab = TabBarScrollingButtons(tab_bar))
9293 {
9294 scroll_to_tab_id = scroll_and_select_tab->ID;
9295 if ((scroll_and_select_tab->Flags & ImGuiTabItemFlags_Button) == 0)
9296 tab_bar->SelectedTabId = scroll_to_tab_id;
9297 }
9298
9299 // Shrink widths if full tabs don't fit in their allocated space
9300 float section_0_w = sections[0].Width + sections[0].Spacing;
9301 float section_1_w = sections[1].Width + sections[1].Spacing;
9302 float section_2_w = sections[2].Width + sections[2].Spacing;
9303 bool central_section_is_visible = (section_0_w + section_2_w) < tab_bar->BarRect.GetWidth();
9304 float width_excess;
9305 if (central_section_is_visible)
9306 width_excess = ImMax(lhs: section_1_w - (tab_bar->BarRect.GetWidth() - section_0_w - section_2_w), rhs: 0.0f); // Excess used to shrink central section
9307 else
9308 width_excess = (section_0_w + section_2_w) - tab_bar->BarRect.GetWidth(); // Excess used to shrink leading/trailing section
9309
9310 // With ImGuiTabBarFlags_FittingPolicyScroll policy, we will only shrink leading/trailing if the central section is not visible anymore
9311 if (width_excess >= 1.0f && ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyResizeDown) || !central_section_is_visible))
9312 {
9313 int shrink_data_count = (central_section_is_visible ? sections[1].TabCount : sections[0].TabCount + sections[2].TabCount);
9314 int shrink_data_offset = (central_section_is_visible ? sections[0].TabCount + sections[2].TabCount : 0);
9315 ShrinkWidths(items: g.ShrinkWidthBuffer.Data + shrink_data_offset, count: shrink_data_count, width_excess);
9316
9317 // Apply shrunk values into tabs and sections
9318 for (int tab_n = shrink_data_offset; tab_n < shrink_data_offset + shrink_data_count; tab_n++)
9319 {
9320 ImGuiTabItem* tab = &tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index];
9321 float shrinked_width = IM_TRUNC(g.ShrinkWidthBuffer[tab_n].Width);
9322 if (shrinked_width < 0.0f)
9323 continue;
9324
9325 shrinked_width = ImMax(lhs: 1.0f, rhs: shrinked_width);
9326 int section_n = TabItemGetSectionIdx(tab);
9327 sections[section_n].Width -= (tab->Width - shrinked_width);
9328 tab->Width = shrinked_width;
9329 }
9330 }
9331
9332 // Layout all active tabs
9333 int section_tab_index = 0;
9334 float tab_offset = 0.0f;
9335 tab_bar->WidthAllTabs = 0.0f;
9336 for (int section_n = 0; section_n < 3; section_n++)
9337 {
9338 ImGuiTabBarSection* section = &sections[section_n];
9339 if (section_n == 2)
9340 tab_offset = ImMin(lhs: ImMax(lhs: 0.0f, rhs: tab_bar->BarRect.GetWidth() - section->Width), rhs: tab_offset);
9341
9342 for (int tab_n = 0; tab_n < section->TabCount; tab_n++)
9343 {
9344 ImGuiTabItem* tab = &tab_bar->Tabs[section_tab_index + tab_n];
9345 tab->Offset = tab_offset;
9346 tab->NameOffset = -1;
9347 tab_offset += tab->Width + (tab_n < section->TabCount - 1 ? g.Style.ItemInnerSpacing.x : 0.0f);
9348 }
9349 tab_bar->WidthAllTabs += ImMax(lhs: section->Width + section->Spacing, rhs: 0.0f);
9350 tab_offset += section->Spacing;
9351 section_tab_index += section->TabCount;
9352 }
9353
9354 // Clear name buffers
9355 tab_bar->TabsNames.Buf.resize(new_size: 0);
9356
9357 // If we have lost the selected tab, select the next most recently active one
9358 if (found_selected_tab_id == false)
9359 tab_bar->SelectedTabId = 0;
9360 if (tab_bar->SelectedTabId == 0 && tab_bar->NextSelectedTabId == 0 && most_recently_selected_tab != NULL)
9361 scroll_to_tab_id = tab_bar->SelectedTabId = most_recently_selected_tab->ID;
9362
9363 // Lock in visible tab
9364 tab_bar->VisibleTabId = tab_bar->SelectedTabId;
9365 tab_bar->VisibleTabWasSubmitted = false;
9366
9367 // Apply request requests
9368 if (scroll_to_tab_id != 0)
9369 TabBarScrollToTab(tab_bar, tab_id: scroll_to_tab_id, sections);
9370 else if ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll) && IsMouseHoveringRect(r_min: tab_bar->BarRect.Min, r_max: tab_bar->BarRect.Max, clip: true) && IsWindowContentHoverable(window: g.CurrentWindow))
9371 {
9372 const float wheel = g.IO.MouseWheelRequestAxisSwap ? g.IO.MouseWheel : g.IO.MouseWheelH;
9373 const ImGuiKey wheel_key = g.IO.MouseWheelRequestAxisSwap ? ImGuiKey_MouseWheelY : ImGuiKey_MouseWheelX;
9374 if (TestKeyOwner(key: wheel_key, owner_id: tab_bar->ID) && wheel != 0.0f)
9375 {
9376 const float scroll_step = wheel * TabBarCalcScrollableWidth(tab_bar, sections) / 3.0f;
9377 tab_bar->ScrollingTargetDistToVisibility = 0.0f;
9378 tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, scrolling: tab_bar->ScrollingTarget - scroll_step);
9379 }
9380 SetKeyOwner(key: wheel_key, owner_id: tab_bar->ID);
9381 }
9382
9383 // Update scrolling
9384 tab_bar->ScrollingAnim = TabBarScrollClamp(tab_bar, scrolling: tab_bar->ScrollingAnim);
9385 tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, scrolling: tab_bar->ScrollingTarget);
9386 if (tab_bar->ScrollingAnim != tab_bar->ScrollingTarget)
9387 {
9388 // Scrolling speed adjust itself so we can always reach our target in 1/3 seconds.
9389 // Teleport if we are aiming far off the visible line
9390 tab_bar->ScrollingSpeed = ImMax(lhs: tab_bar->ScrollingSpeed, rhs: 70.0f * g.FontSize);
9391 tab_bar->ScrollingSpeed = ImMax(lhs: tab_bar->ScrollingSpeed, ImFabs(tab_bar->ScrollingTarget - tab_bar->ScrollingAnim) / 0.3f);
9392 const bool teleport = (tab_bar->PrevFrameVisible + 1 < g.FrameCount) || (tab_bar->ScrollingTargetDistToVisibility > 10.0f * g.FontSize);
9393 tab_bar->ScrollingAnim = teleport ? tab_bar->ScrollingTarget : ImLinearSweep(current: tab_bar->ScrollingAnim, target: tab_bar->ScrollingTarget, speed: g.IO.DeltaTime * tab_bar->ScrollingSpeed);
9394 }
9395 else
9396 {
9397 tab_bar->ScrollingSpeed = 0.0f;
9398 }
9399 tab_bar->ScrollingRectMinX = tab_bar->BarRect.Min.x + sections[0].Width + sections[0].Spacing;
9400 tab_bar->ScrollingRectMaxX = tab_bar->BarRect.Max.x - sections[2].Width - sections[1].Spacing;
9401
9402 // Actual layout in host window (we don't do it in BeginTabBar() so as not to waste an extra frame)
9403 ImGuiWindow* window = g.CurrentWindow;
9404 window->DC.CursorPos = tab_bar->BarRect.Min;
9405 ItemSize(size: ImVec2(tab_bar->WidthAllTabs, tab_bar->BarRect.GetHeight()), text_baseline_y: tab_bar->FramePadding.y);
9406 window->DC.IdealMaxPos.x = ImMax(lhs: window->DC.IdealMaxPos.x, rhs: tab_bar->BarRect.Min.x + tab_bar->WidthAllTabsIdeal);
9407}
9408
9409// Dockable windows uses Name/ID in the global namespace. Non-dockable items use the ID stack.
9410static ImU32 ImGui::TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window)
9411{
9412 IM_ASSERT(docked_window == NULL); // master branch only
9413 IM_UNUSED(docked_window);
9414 if (tab_bar->Flags & ImGuiTabBarFlags_DockNode)
9415 {
9416 ImGuiID id = ImHashStr(data: label);
9417 KeepAliveID(id);
9418 return id;
9419 }
9420 else
9421 {
9422 ImGuiWindow* window = GImGui->CurrentWindow;
9423 return window->GetID(str: label);
9424 }
9425}
9426
9427static float ImGui::TabBarCalcMaxTabWidth()
9428{
9429 ImGuiContext& g = *GImGui;
9430 return g.FontSize * 20.0f;
9431}
9432
9433ImGuiTabItem* ImGui::TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id)
9434{
9435 if (tab_id != 0)
9436 for (int n = 0; n < tab_bar->Tabs.Size; n++)
9437 if (tab_bar->Tabs[n].ID == tab_id)
9438 return &tab_bar->Tabs[n];
9439 return NULL;
9440}
9441
9442// Order = visible order, not submission order! (which is tab->BeginOrder)
9443ImGuiTabItem* ImGui::TabBarFindTabByOrder(ImGuiTabBar* tab_bar, int order)
9444{
9445 if (order < 0 || order >= tab_bar->Tabs.Size)
9446 return NULL;
9447 return &tab_bar->Tabs[order];
9448}
9449
9450ImGuiTabItem* ImGui::TabBarGetCurrentTab(ImGuiTabBar* tab_bar)
9451{
9452 if (tab_bar->LastTabItemIdx < 0 || tab_bar->LastTabItemIdx >= tab_bar->Tabs.Size)
9453 return NULL;
9454 return &tab_bar->Tabs[tab_bar->LastTabItemIdx];
9455}
9456
9457const char* ImGui::TabBarGetTabName(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
9458{
9459 if (tab->NameOffset == -1)
9460 return "N/A";
9461 IM_ASSERT(tab->NameOffset < tab_bar->TabsNames.Buf.Size);
9462 return tab_bar->TabsNames.Buf.Data + tab->NameOffset;
9463}
9464
9465// The *TabId fields are already set by the docking system _before_ the actual TabItem was created, so we clear them regardless.
9466void ImGui::TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id)
9467{
9468 if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
9469 tab_bar->Tabs.erase(it: tab);
9470 if (tab_bar->VisibleTabId == tab_id) { tab_bar->VisibleTabId = 0; }
9471 if (tab_bar->SelectedTabId == tab_id) { tab_bar->SelectedTabId = 0; }
9472 if (tab_bar->NextSelectedTabId == tab_id) { tab_bar->NextSelectedTabId = 0; }
9473}
9474
9475// Called on manual closure attempt
9476void ImGui::TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
9477{
9478 if (tab->Flags & ImGuiTabItemFlags_Button)
9479 return; // A button appended with TabItemButton().
9480
9481 if ((tab->Flags & (ImGuiTabItemFlags_UnsavedDocument | ImGuiTabItemFlags_NoAssumedClosure)) == 0)
9482 {
9483 // This will remove a frame of lag for selecting another tab on closure.
9484 // However we don't run it in the case where the 'Unsaved' flag is set, so user gets a chance to fully undo the closure
9485 tab->WantClose = true;
9486 if (tab_bar->VisibleTabId == tab->ID)
9487 {
9488 tab->LastFrameVisible = -1;
9489 tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = 0;
9490 }
9491 }
9492 else
9493 {
9494 // Actually select before expecting closure attempt (on an UnsavedDocument tab user is expect to e.g. show a popup)
9495 if (tab_bar->VisibleTabId != tab->ID)
9496 TabBarQueueFocus(tab_bar, tab);
9497 }
9498}
9499
9500static float ImGui::TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling)
9501{
9502 scrolling = ImMin(lhs: scrolling, rhs: tab_bar->WidthAllTabs - tab_bar->BarRect.GetWidth());
9503 return ImMax(lhs: scrolling, rhs: 0.0f);
9504}
9505
9506// Note: we may scroll to tab that are not selected! e.g. using keyboard arrow keys
9507static void ImGui::TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections)
9508{
9509 ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id);
9510 if (tab == NULL)
9511 return;
9512 if (tab->Flags & ImGuiTabItemFlags_SectionMask_)
9513 return;
9514
9515 ImGuiContext& g = *GImGui;
9516 float margin = g.FontSize * 1.0f; // When to scroll to make Tab N+1 visible always make a bit of N visible to suggest more scrolling area (since we don't have a scrollbar)
9517 int order = TabBarGetTabOrder(tab_bar, tab);
9518
9519 // Scrolling happens only in the central section (leading/trailing sections are not scrolling)
9520 float scrollable_width = TabBarCalcScrollableWidth(tab_bar, sections);
9521
9522 // We make all tabs positions all relative Sections[0].Width to make code simpler
9523 float tab_x1 = tab->Offset - sections[0].Width + (order > sections[0].TabCount - 1 ? -margin : 0.0f);
9524 float tab_x2 = tab->Offset - sections[0].Width + tab->Width + (order + 1 < tab_bar->Tabs.Size - sections[2].TabCount ? margin : 1.0f);
9525 tab_bar->ScrollingTargetDistToVisibility = 0.0f;
9526 if (tab_bar->ScrollingTarget > tab_x1 || (tab_x2 - tab_x1 >= scrollable_width))
9527 {
9528 // Scroll to the left
9529 tab_bar->ScrollingTargetDistToVisibility = ImMax(lhs: tab_bar->ScrollingAnim - tab_x2, rhs: 0.0f);
9530 tab_bar->ScrollingTarget = tab_x1;
9531 }
9532 else if (tab_bar->ScrollingTarget < tab_x2 - scrollable_width)
9533 {
9534 // Scroll to the right
9535 tab_bar->ScrollingTargetDistToVisibility = ImMax(lhs: (tab_x1 - scrollable_width) - tab_bar->ScrollingAnim, rhs: 0.0f);
9536 tab_bar->ScrollingTarget = tab_x2 - scrollable_width;
9537 }
9538}
9539
9540void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
9541{
9542 tab_bar->NextSelectedTabId = tab->ID;
9543}
9544
9545void ImGui::TabBarQueueReorder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab, int offset)
9546{
9547 IM_ASSERT(offset != 0);
9548 IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
9549 tab_bar->ReorderRequestTabId = tab->ID;
9550 tab_bar->ReorderRequestOffset = (ImS16)offset;
9551}
9552
9553void ImGui::TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, ImGuiTabItem* src_tab, ImVec2 mouse_pos)
9554{
9555 ImGuiContext& g = *GImGui;
9556 IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
9557 if ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) == 0)
9558 return;
9559
9560 const bool is_central_section = (src_tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
9561 const float bar_offset = tab_bar->BarRect.Min.x - (is_central_section ? tab_bar->ScrollingTarget : 0);
9562
9563 // Count number of contiguous tabs we are crossing over
9564 const int dir = (bar_offset + src_tab->Offset) > mouse_pos.x ? -1 : +1;
9565 const int src_idx = tab_bar->Tabs.index_from_ptr(it: src_tab);
9566 int dst_idx = src_idx;
9567 for (int i = src_idx; i >= 0 && i < tab_bar->Tabs.Size; i += dir)
9568 {
9569 // Reordered tabs must share the same section
9570 const ImGuiTabItem* dst_tab = &tab_bar->Tabs[i];
9571 if (dst_tab->Flags & ImGuiTabItemFlags_NoReorder)
9572 break;
9573 if ((dst_tab->Flags & ImGuiTabItemFlags_SectionMask_) != (src_tab->Flags & ImGuiTabItemFlags_SectionMask_))
9574 break;
9575 dst_idx = i;
9576
9577 // Include spacing after tab, so when mouse cursor is between tabs we would not continue checking further tabs that are not hovered.
9578 const float x1 = bar_offset + dst_tab->Offset - g.Style.ItemInnerSpacing.x;
9579 const float x2 = bar_offset + dst_tab->Offset + dst_tab->Width + g.Style.ItemInnerSpacing.x;
9580 //GetForegroundDrawList()->AddRect(ImVec2(x1, tab_bar->BarRect.Min.y), ImVec2(x2, tab_bar->BarRect.Max.y), IM_COL32(255, 0, 0, 255));
9581 if ((dir < 0 && mouse_pos.x > x1) || (dir > 0 && mouse_pos.x < x2))
9582 break;
9583 }
9584
9585 if (dst_idx != src_idx)
9586 TabBarQueueReorder(tab_bar, tab: src_tab, offset: dst_idx - src_idx);
9587}
9588
9589bool ImGui::TabBarProcessReorder(ImGuiTabBar* tab_bar)
9590{
9591 ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_id: tab_bar->ReorderRequestTabId);
9592 if (tab1 == NULL || (tab1->Flags & ImGuiTabItemFlags_NoReorder))
9593 return false;
9594
9595 //IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_Reorderable); // <- this may happen when using debug tools
9596 int tab2_order = TabBarGetTabOrder(tab_bar, tab: tab1) + tab_bar->ReorderRequestOffset;
9597 if (tab2_order < 0 || tab2_order >= tab_bar->Tabs.Size)
9598 return false;
9599
9600 // Reordered tabs must share the same section
9601 // (Note: TabBarQueueReorderFromMousePos() also has a similar test but since we allow direct calls to TabBarQueueReorder() we do it here too)
9602 ImGuiTabItem* tab2 = &tab_bar->Tabs[tab2_order];
9603 if (tab2->Flags & ImGuiTabItemFlags_NoReorder)
9604 return false;
9605 if ((tab1->Flags & ImGuiTabItemFlags_SectionMask_) != (tab2->Flags & ImGuiTabItemFlags_SectionMask_))
9606 return false;
9607
9608 ImGuiTabItem item_tmp = *tab1;
9609 ImGuiTabItem* src_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 + 1 : tab2;
9610 ImGuiTabItem* dst_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 : tab2 + 1;
9611 const int move_count = (tab_bar->ReorderRequestOffset > 0) ? tab_bar->ReorderRequestOffset : -tab_bar->ReorderRequestOffset;
9612 memmove(dest: dst_tab, src: src_tab, n: move_count * sizeof(ImGuiTabItem));
9613 *tab2 = item_tmp;
9614
9615 if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings)
9616 MarkIniSettingsDirty();
9617 return true;
9618}
9619
9620static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar)
9621{
9622 ImGuiContext& g = *GImGui;
9623 ImGuiWindow* window = g.CurrentWindow;
9624
9625 const ImVec2 arrow_button_size(g.FontSize - 2.0f, g.FontSize + g.Style.FramePadding.y * 2.0f);
9626 const float scrolling_buttons_width = arrow_button_size.x * 2.0f;
9627
9628 const ImVec2 backup_cursor_pos = window->DC.CursorPos;
9629 //window->DrawList->AddRect(ImVec2(tab_bar->BarRect.Max.x - scrolling_buttons_width, tab_bar->BarRect.Min.y), ImVec2(tab_bar->BarRect.Max.x, tab_bar->BarRect.Max.y), IM_COL32(255,0,0,255));
9630
9631 int select_dir = 0;
9632 ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
9633 arrow_col.w *= 0.5f;
9634
9635 PushStyleColor(idx: ImGuiCol_Text, col: arrow_col);
9636 PushStyleColor(idx: ImGuiCol_Button, col: ImVec4(0, 0, 0, 0));
9637 const float backup_repeat_delay = g.IO.KeyRepeatDelay;
9638 const float backup_repeat_rate = g.IO.KeyRepeatRate;
9639 g.IO.KeyRepeatDelay = 0.250f;
9640 g.IO.KeyRepeatRate = 0.200f;
9641 float x = ImMax(lhs: tab_bar->BarRect.Min.x, rhs: tab_bar->BarRect.Max.x - scrolling_buttons_width);
9642 window->DC.CursorPos = ImVec2(x, tab_bar->BarRect.Min.y);
9643 if (ArrowButtonEx(str_id: "##<", dir: ImGuiDir_Left, size: arrow_button_size, flags: ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_Repeat))
9644 select_dir = -1;
9645 window->DC.CursorPos = ImVec2(x + arrow_button_size.x, tab_bar->BarRect.Min.y);
9646 if (ArrowButtonEx(str_id: "##>", dir: ImGuiDir_Right, size: arrow_button_size, flags: ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_Repeat))
9647 select_dir = +1;
9648 PopStyleColor(count: 2);
9649 g.IO.KeyRepeatRate = backup_repeat_rate;
9650 g.IO.KeyRepeatDelay = backup_repeat_delay;
9651
9652 ImGuiTabItem* tab_to_scroll_to = NULL;
9653 if (select_dir != 0)
9654 if (ImGuiTabItem* tab_item = TabBarFindTabByID(tab_bar, tab_id: tab_bar->SelectedTabId))
9655 {
9656 int selected_order = TabBarGetTabOrder(tab_bar, tab: tab_item);
9657 int target_order = selected_order + select_dir;
9658
9659 // Skip tab item buttons until another tab item is found or end is reached
9660 while (tab_to_scroll_to == NULL)
9661 {
9662 // If we are at the end of the list, still scroll to make our tab visible
9663 tab_to_scroll_to = &tab_bar->Tabs[(target_order >= 0 && target_order < tab_bar->Tabs.Size) ? target_order : selected_order];
9664
9665 // Cross through buttons
9666 // (even if first/last item is a button, return it so we can update the scroll)
9667 if (tab_to_scroll_to->Flags & ImGuiTabItemFlags_Button)
9668 {
9669 target_order += select_dir;
9670 selected_order += select_dir;
9671 tab_to_scroll_to = (target_order < 0 || target_order >= tab_bar->Tabs.Size) ? tab_to_scroll_to : NULL;
9672 }
9673 }
9674 }
9675 window->DC.CursorPos = backup_cursor_pos;
9676 tab_bar->BarRect.Max.x -= scrolling_buttons_width + 1.0f;
9677
9678 return tab_to_scroll_to;
9679}
9680
9681static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar)
9682{
9683 ImGuiContext& g = *GImGui;
9684 ImGuiWindow* window = g.CurrentWindow;
9685
9686 // We use g.Style.FramePadding.y to match the square ArrowButton size
9687 const float tab_list_popup_button_width = g.FontSize + g.Style.FramePadding.y;
9688 const ImVec2 backup_cursor_pos = window->DC.CursorPos;
9689 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x - g.Style.FramePadding.y, tab_bar->BarRect.Min.y);
9690 tab_bar->BarRect.Min.x += tab_list_popup_button_width;
9691
9692 ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
9693 arrow_col.w *= 0.5f;
9694 PushStyleColor(idx: ImGuiCol_Text, col: arrow_col);
9695 PushStyleColor(idx: ImGuiCol_Button, col: ImVec4(0, 0, 0, 0));
9696 bool open = BeginCombo(label: "##v", NULL, flags: ImGuiComboFlags_NoPreview | ImGuiComboFlags_HeightLargest);
9697 PopStyleColor(count: 2);
9698
9699 ImGuiTabItem* tab_to_select = NULL;
9700 if (open)
9701 {
9702 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
9703 {
9704 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
9705 if (tab->Flags & ImGuiTabItemFlags_Button)
9706 continue;
9707
9708 const char* tab_name = TabBarGetTabName(tab_bar, tab);
9709 if (Selectable(label: tab_name, selected: tab_bar->SelectedTabId == tab->ID))
9710 tab_to_select = tab;
9711 }
9712 EndCombo();
9713 }
9714
9715 window->DC.CursorPos = backup_cursor_pos;
9716 return tab_to_select;
9717}
9718
9719//-------------------------------------------------------------------------
9720// [SECTION] Widgets: BeginTabItem, EndTabItem, etc.
9721//-------------------------------------------------------------------------
9722// - BeginTabItem()
9723// - EndTabItem()
9724// - TabItemButton()
9725// - TabItemEx() [Internal]
9726// - SetTabItemClosed()
9727// - TabItemCalcSize() [Internal]
9728// - TabItemBackground() [Internal]
9729// - TabItemLabelAndCloseButton() [Internal]
9730//-------------------------------------------------------------------------
9731
9732bool ImGui::BeginTabItem(const char* label, bool* p_open, ImGuiTabItemFlags flags)
9733{
9734 ImGuiContext& g = *GImGui;
9735 ImGuiWindow* window = g.CurrentWindow;
9736 if (window->SkipItems)
9737 return false;
9738
9739 ImGuiTabBar* tab_bar = g.CurrentTabBar;
9740 if (tab_bar == NULL)
9741 {
9742 IM_ASSERT_USER_ERROR(tab_bar, "Needs to be called between BeginTabBar() and EndTabBar()!");
9743 return false;
9744 }
9745 IM_ASSERT(!(flags & ImGuiTabItemFlags_Button)); // BeginTabItem() Can't be used with button flags, use TabItemButton() instead!
9746
9747 bool ret = TabItemEx(tab_bar, label, p_open, flags, NULL);
9748 if (ret && !(flags & ImGuiTabItemFlags_NoPushId))
9749 {
9750 ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
9751 PushOverrideID(id: tab->ID); // We already hashed 'label' so push into the ID stack directly instead of doing another hash through PushID(label)
9752 }
9753 return ret;
9754}
9755
9756void ImGui::EndTabItem()
9757{
9758 ImGuiContext& g = *GImGui;
9759 ImGuiWindow* window = g.CurrentWindow;
9760 if (window->SkipItems)
9761 return;
9762
9763 ImGuiTabBar* tab_bar = g.CurrentTabBar;
9764 if (tab_bar == NULL)
9765 {
9766 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
9767 return;
9768 }
9769 IM_ASSERT(tab_bar->LastTabItemIdx >= 0);
9770 ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
9771 if (!(tab->Flags & ImGuiTabItemFlags_NoPushId))
9772 PopID();
9773}
9774
9775bool ImGui::TabItemButton(const char* label, ImGuiTabItemFlags flags)
9776{
9777 ImGuiContext& g = *GImGui;
9778 ImGuiWindow* window = g.CurrentWindow;
9779 if (window->SkipItems)
9780 return false;
9781
9782 ImGuiTabBar* tab_bar = g.CurrentTabBar;
9783 if (tab_bar == NULL)
9784 {
9785 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
9786 return false;
9787 }
9788 return TabItemEx(tab_bar, label, NULL, flags: flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder, NULL);
9789}
9790
9791bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags, ImGuiWindow* docked_window)
9792{
9793 // Layout whole tab bar if not already done
9794 ImGuiContext& g = *GImGui;
9795 if (tab_bar->WantLayout)
9796 {
9797 ImGuiNextItemData backup_next_item_data = g.NextItemData;
9798 TabBarLayout(tab_bar);
9799 g.NextItemData = backup_next_item_data;
9800 }
9801 ImGuiWindow* window = g.CurrentWindow;
9802 if (window->SkipItems)
9803 return false;
9804
9805 const ImGuiStyle& style = g.Style;
9806 const ImGuiID id = TabBarCalcTabID(tab_bar, label, docked_window);
9807
9808 // If the user called us with *p_open == false, we early out and don't render.
9809 // We make a call to ItemAdd() so that attempts to use a contextual popup menu with an implicit ID won't use an older ID.
9810 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
9811 if (p_open && !*p_open)
9812 {
9813 ItemAdd(bb: ImRect(), id, NULL, extra_flags: ImGuiItemFlags_NoNav);
9814 return false;
9815 }
9816
9817 IM_ASSERT(!p_open || !(flags & ImGuiTabItemFlags_Button));
9818 IM_ASSERT((flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)) != (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)); // Can't use both Leading and Trailing
9819
9820 // Store into ImGuiTabItemFlags_NoCloseButton, also honor ImGuiTabItemFlags_NoCloseButton passed by user (although not documented)
9821 if (flags & ImGuiTabItemFlags_NoCloseButton)
9822 p_open = NULL;
9823 else if (p_open == NULL)
9824 flags |= ImGuiTabItemFlags_NoCloseButton;
9825
9826 // Acquire tab data
9827 ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id: id);
9828 bool tab_is_new = false;
9829 if (tab == NULL)
9830 {
9831 tab_bar->Tabs.push_back(v: ImGuiTabItem());
9832 tab = &tab_bar->Tabs.back();
9833 tab->ID = id;
9834 tab_bar->TabsAddedNew = tab_is_new = true;
9835 }
9836 tab_bar->LastTabItemIdx = (ImS16)tab_bar->Tabs.index_from_ptr(it: tab);
9837
9838 // Calculate tab contents size
9839 ImVec2 size = TabItemCalcSize(label, has_close_button_or_unsaved_marker: (p_open != NULL) || (flags & ImGuiTabItemFlags_UnsavedDocument));
9840 tab->RequestedWidth = -1.0f;
9841 if (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasWidth)
9842 size.x = tab->RequestedWidth = g.NextItemData.Width;
9843 if (tab_is_new)
9844 tab->Width = ImMax(lhs: 1.0f, rhs: size.x);
9845 tab->ContentWidth = size.x;
9846 tab->BeginOrder = tab_bar->TabsActiveCount++;
9847
9848 const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
9849 const bool tab_bar_focused = (tab_bar->Flags & ImGuiTabBarFlags_IsFocused) != 0;
9850 const bool tab_appearing = (tab->LastFrameVisible + 1 < g.FrameCount);
9851 const bool tab_just_unsaved = (flags & ImGuiTabItemFlags_UnsavedDocument) && !(tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
9852 const bool is_tab_button = (flags & ImGuiTabItemFlags_Button) != 0;
9853 tab->LastFrameVisible = g.FrameCount;
9854 tab->Flags = flags;
9855
9856 // Append name _WITH_ the zero-terminator
9857 if (docked_window != NULL)
9858 {
9859 IM_ASSERT(docked_window == NULL); // master branch only
9860 }
9861 else
9862 {
9863 tab->NameOffset = (ImS32)tab_bar->TabsNames.size();
9864 tab_bar->TabsNames.append(str: label, str_end: label + strlen(s: label) + 1);
9865 }
9866
9867 // Update selected tab
9868 if (!is_tab_button)
9869 {
9870 if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0)
9871 if (!tab_bar_appearing || tab_bar->SelectedTabId == 0)
9872 TabBarQueueFocus(tab_bar, tab); // New tabs gets activated
9873 if ((flags & ImGuiTabItemFlags_SetSelected) && (tab_bar->SelectedTabId != id)) // _SetSelected can only be passed on explicit tab bar
9874 TabBarQueueFocus(tab_bar, tab);
9875 }
9876
9877 // Lock visibility
9878 // (Note: tab_contents_visible != tab_selected... because CTRL+TAB operations may preview some tabs without selecting them!)
9879 bool tab_contents_visible = (tab_bar->VisibleTabId == id);
9880 if (tab_contents_visible)
9881 tab_bar->VisibleTabWasSubmitted = true;
9882
9883 // On the very first frame of a tab bar we let first tab contents be visible to minimize appearing glitches
9884 if (!tab_contents_visible && tab_bar->SelectedTabId == 0 && tab_bar_appearing)
9885 if (tab_bar->Tabs.Size == 1 && !(tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs))
9886 tab_contents_visible = true;
9887
9888 // Note that tab_is_new is not necessarily the same as tab_appearing! When a tab bar stops being submitted
9889 // and then gets submitted again, the tabs will have 'tab_appearing=true' but 'tab_is_new=false'.
9890 if (tab_appearing && (!tab_bar_appearing || tab_is_new))
9891 {
9892 ItemAdd(bb: ImRect(), id, NULL, extra_flags: ImGuiItemFlags_NoNav);
9893 if (is_tab_button)
9894 return false;
9895 return tab_contents_visible;
9896 }
9897
9898 if (tab_bar->SelectedTabId == id)
9899 tab->LastFrameSelected = g.FrameCount;
9900
9901 // Backup current layout position
9902 const ImVec2 backup_main_cursor_pos = window->DC.CursorPos;
9903
9904 // Layout
9905 const bool is_central_section = (tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
9906 size.x = tab->Width;
9907 if (is_central_section)
9908 window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_TRUNC(tab->Offset - tab_bar->ScrollingAnim), 0.0f);
9909 else
9910 window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(tab->Offset, 0.0f);
9911 ImVec2 pos = window->DC.CursorPos;
9912 ImRect bb(pos, pos + size);
9913
9914 // We don't have CPU clipping primitives to clip the CloseButton (until it becomes a texture), so need to add an extra draw call (temporary in the case of vertical animation)
9915 const bool want_clip_rect = is_central_section && (bb.Min.x < tab_bar->ScrollingRectMinX || bb.Max.x > tab_bar->ScrollingRectMaxX);
9916 if (want_clip_rect)
9917 PushClipRect(clip_rect_min: ImVec2(ImMax(lhs: bb.Min.x, rhs: tab_bar->ScrollingRectMinX), bb.Min.y - 1), clip_rect_max: ImVec2(tab_bar->ScrollingRectMaxX, bb.Max.y), intersect_with_current_clip_rect: true);
9918
9919 ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos;
9920 ItemSize(size: bb.GetSize(), text_baseline_y: style.FramePadding.y);
9921 window->DC.CursorMaxPos = backup_cursor_max_pos;
9922
9923 if (!ItemAdd(bb, id))
9924 {
9925 if (want_clip_rect)
9926 PopClipRect();
9927 window->DC.CursorPos = backup_main_cursor_pos;
9928 return tab_contents_visible;
9929 }
9930
9931 // Click to Select a tab
9932 // Allow the close button to overlap
9933 ImGuiButtonFlags button_flags = ((is_tab_button ? ImGuiButtonFlags_PressedOnClickRelease : ImGuiButtonFlags_PressedOnClick) | ImGuiButtonFlags_AllowOverlap);
9934 if (g.DragDropActive)
9935 button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
9936 bool hovered, held;
9937 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags: button_flags);
9938 if (pressed && !is_tab_button)
9939 TabBarQueueFocus(tab_bar, tab);
9940
9941 // Drag and drop: re-order tabs
9942 if (held && !tab_appearing && IsMouseDragging(button: 0))
9943 {
9944 if (!g.DragDropActive && (tab_bar->Flags & ImGuiTabBarFlags_Reorderable))
9945 {
9946 // While moving a tab it will jump on the other side of the mouse, so we also test for MouseDelta.x
9947 if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < bb.Min.x)
9948 {
9949 TabBarQueueReorderFromMousePos(tab_bar, src_tab: tab, mouse_pos: g.IO.MousePos);
9950 }
9951 else if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > bb.Max.x)
9952 {
9953 TabBarQueueReorderFromMousePos(tab_bar, src_tab: tab, mouse_pos: g.IO.MousePos);
9954 }
9955 }
9956 }
9957
9958#if 0
9959 if (hovered && g.HoveredIdNotActiveTimer > TOOLTIP_DELAY && bb.GetWidth() < tab->ContentWidth)
9960 {
9961 // Enlarge tab display when hovering
9962 bb.Max.x = bb.Min.x + IM_TRUNC(ImLerp(bb.GetWidth(), tab->ContentWidth, ImSaturate((g.HoveredIdNotActiveTimer - 0.40f) * 6.0f)));
9963 display_draw_list = GetForegroundDrawList(window);
9964 TabItemBackground(display_draw_list, bb, flags, GetColorU32(ImGuiCol_TitleBgActive));
9965 }
9966#endif
9967
9968 // Render tab shape
9969 ImDrawList* display_draw_list = window->DrawList;
9970 const ImU32 tab_col = GetColorU32(idx: (held || hovered) ? ImGuiCol_TabHovered : tab_contents_visible ? (tab_bar_focused ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected) : (tab_bar_focused ? ImGuiCol_Tab : ImGuiCol_TabDimmed));
9971 TabItemBackground(draw_list: display_draw_list, bb, flags, col: tab_col);
9972 if (tab_contents_visible && (tab_bar->Flags & ImGuiTabBarFlags_DrawSelectedOverline) && style.TabBarOverlineSize > 0.0f)
9973 {
9974 float x_offset = IM_TRUNC(0.4f * style.TabRounding);
9975 if (x_offset < 2.0f * g.CurrentDpiScale)
9976 x_offset = 0.0f;
9977 float y_offset = 1.0f * g.CurrentDpiScale;
9978 display_draw_list->AddLine(p1: bb.GetTL() + ImVec2(x_offset, y_offset), p2: bb.GetTR() + ImVec2(-x_offset, y_offset), col: GetColorU32(idx: tab_bar_focused ? ImGuiCol_TabSelectedOverline : ImGuiCol_TabDimmedSelectedOverline), thickness: style.TabBarOverlineSize);
9979 }
9980 RenderNavHighlight(bb, id);
9981
9982 // Select with right mouse button. This is so the common idiom for context menu automatically highlight the current widget.
9983 const bool hovered_unblocked = IsItemHovered(flags: ImGuiHoveredFlags_AllowWhenBlockedByPopup);
9984 if (hovered_unblocked && (IsMouseClicked(button: 1) || IsMouseReleased(button: 1)) && !is_tab_button)
9985 TabBarQueueFocus(tab_bar, tab);
9986
9987 if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)
9988 flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton;
9989
9990 // Render tab label, process close button
9991 const ImGuiID close_button_id = p_open ? GetIDWithSeed(str_id_begin: "#CLOSE", NULL, seed: id) : 0;
9992 bool just_closed;
9993 bool text_clipped;
9994 TabItemLabelAndCloseButton(draw_list: display_draw_list, bb, flags: tab_just_unsaved ? (flags & ~ImGuiTabItemFlags_UnsavedDocument) : flags, frame_padding: tab_bar->FramePadding, label, tab_id: id, close_button_id, is_contents_visible: tab_contents_visible, out_just_closed: &just_closed, out_text_clipped: &text_clipped);
9995 if (just_closed && p_open != NULL)
9996 {
9997 *p_open = false;
9998 TabBarCloseTab(tab_bar, tab);
9999 }
10000
10001 // Restore main window position so user can draw there
10002 if (want_clip_rect)
10003 PopClipRect();
10004 window->DC.CursorPos = backup_main_cursor_pos;
10005
10006 // Tooltip
10007 // (Won't work over the close button because ItemOverlap systems messes up with HoveredIdTimer-> seems ok)
10008 // (We test IsItemHovered() to discard e.g. when another item is active or drag and drop over the tab bar, which g.HoveredId ignores)
10009 // FIXME: This is a mess.
10010 // FIXME: We may want disabled tab to still display the tooltip?
10011 if (text_clipped && g.HoveredId == id && !held)
10012 if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip) && !(tab->Flags & ImGuiTabItemFlags_NoTooltip))
10013 SetItemTooltip("%.*s", (int)(FindRenderedTextEnd(text: label) - label), label);
10014
10015 IM_ASSERT(!is_tab_button || !(tab_bar->SelectedTabId == tab->ID && is_tab_button)); // TabItemButton should not be selected
10016 if (is_tab_button)
10017 return pressed;
10018 return tab_contents_visible;
10019}
10020
10021// [Public] This is call is 100% optional but it allows to remove some one-frame glitches when a tab has been unexpectedly removed.
10022// To use it to need to call the function SetTabItemClosed() between BeginTabBar() and EndTabBar().
10023// Tabs closed by the close button will automatically be flagged to avoid this issue.
10024void ImGui::SetTabItemClosed(const char* label)
10025{
10026 ImGuiContext& g = *GImGui;
10027 bool is_within_manual_tab_bar = g.CurrentTabBar && !(g.CurrentTabBar->Flags & ImGuiTabBarFlags_DockNode);
10028 if (is_within_manual_tab_bar)
10029 {
10030 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10031 ImGuiID tab_id = TabBarCalcTabID(tab_bar, label, NULL);
10032 if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
10033 tab->WantClose = true; // Will be processed by next call to TabBarLayout()
10034 }
10035}
10036
10037ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button_or_unsaved_marker)
10038{
10039 ImGuiContext& g = *GImGui;
10040 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
10041 ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x, label_size.y + g.Style.FramePadding.y * 2.0f);
10042 if (has_close_button_or_unsaved_marker)
10043 size.x += g.Style.FramePadding.x + (g.Style.ItemInnerSpacing.x + g.FontSize); // We use Y intentionally to fit the close button circle.
10044 else
10045 size.x += g.Style.FramePadding.x + 1.0f;
10046 return ImVec2(ImMin(lhs: size.x, rhs: TabBarCalcMaxTabWidth()), size.y);
10047}
10048
10049ImVec2 ImGui::TabItemCalcSize(ImGuiWindow*)
10050{
10051 IM_ASSERT(0); // This function exists to facilitate merge with 'docking' branch.
10052 return ImVec2(0.0f, 0.0f);
10053}
10054
10055void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImU32 col)
10056{
10057 // While rendering tabs, we trim 1 pixel off the top of our bounding box so they can fit within a regular frame height while looking "detached" from it.
10058 ImGuiContext& g = *GImGui;
10059 const float width = bb.GetWidth();
10060 IM_UNUSED(flags);
10061 IM_ASSERT(width > 0.0f);
10062 const float rounding = ImMax(lhs: 0.0f, rhs: ImMin(lhs: (flags & ImGuiTabItemFlags_Button) ? g.Style.FrameRounding : g.Style.TabRounding, rhs: width * 0.5f - 1.0f));
10063 const float y1 = bb.Min.y + 1.0f;
10064 const float y2 = bb.Max.y - g.Style.TabBarBorderSize;
10065 draw_list->PathLineTo(pos: ImVec2(bb.Min.x, y2));
10066 draw_list->PathArcToFast(center: ImVec2(bb.Min.x + rounding, y1 + rounding), radius: rounding, a_min_of_12: 6, a_max_of_12: 9);
10067 draw_list->PathArcToFast(center: ImVec2(bb.Max.x - rounding, y1 + rounding), radius: rounding, a_min_of_12: 9, a_max_of_12: 12);
10068 draw_list->PathLineTo(pos: ImVec2(bb.Max.x, y2));
10069 draw_list->PathFillConvex(col);
10070 if (g.Style.TabBorderSize > 0.0f)
10071 {
10072 draw_list->PathLineTo(pos: ImVec2(bb.Min.x + 0.5f, y2));
10073 draw_list->PathArcToFast(center: ImVec2(bb.Min.x + rounding + 0.5f, y1 + rounding + 0.5f), radius: rounding, a_min_of_12: 6, a_max_of_12: 9);
10074 draw_list->PathArcToFast(center: ImVec2(bb.Max.x - rounding - 0.5f, y1 + rounding + 0.5f), radius: rounding, a_min_of_12: 9, a_max_of_12: 12);
10075 draw_list->PathLineTo(pos: ImVec2(bb.Max.x - 0.5f, y2));
10076 draw_list->PathStroke(col: GetColorU32(idx: ImGuiCol_Border), flags: 0, thickness: g.Style.TabBorderSize);
10077 }
10078}
10079
10080// Render text label (with custom clipping) + Unsaved Document marker + Close Button logic
10081// We tend to lock style.FramePadding for a given tab-bar, hence the 'frame_padding' parameter.
10082void ImGui::TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImVec2 frame_padding, const char* label, ImGuiID tab_id, ImGuiID close_button_id, bool is_contents_visible, bool* out_just_closed, bool* out_text_clipped)
10083{
10084 ImGuiContext& g = *GImGui;
10085 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
10086
10087 if (out_just_closed)
10088 *out_just_closed = false;
10089 if (out_text_clipped)
10090 *out_text_clipped = false;
10091
10092 if (bb.GetWidth() <= 1.0f)
10093 return;
10094
10095 // In Style V2 we'll have full override of all colors per state (e.g. focused, selected)
10096 // But right now if you want to alter text color of tabs this is what you need to do.
10097#if 0
10098 const float backup_alpha = g.Style.Alpha;
10099 if (!is_contents_visible)
10100 g.Style.Alpha *= 0.7f;
10101#endif
10102
10103 // Render text label (with clipping + alpha gradient) + unsaved marker
10104 ImRect text_pixel_clip_bb(bb.Min.x + frame_padding.x, bb.Min.y + frame_padding.y, bb.Max.x - frame_padding.x, bb.Max.y);
10105 ImRect text_ellipsis_clip_bb = text_pixel_clip_bb;
10106
10107 // Return clipped state ignoring the close button
10108 if (out_text_clipped)
10109 {
10110 *out_text_clipped = (text_ellipsis_clip_bb.Min.x + label_size.x) > text_pixel_clip_bb.Max.x;
10111 //draw_list->AddCircle(text_ellipsis_clip_bb.Min, 3.0f, *out_text_clipped ? IM_COL32(255, 0, 0, 255) : IM_COL32(0, 255, 0, 255));
10112 }
10113
10114 const float button_sz = g.FontSize;
10115 const ImVec2 button_pos(ImMax(lhs: bb.Min.x, rhs: bb.Max.x - frame_padding.x - button_sz), bb.Min.y + frame_padding.y);
10116
10117 // Close Button & Unsaved Marker
10118 // We are relying on a subtle and confusing distinction between 'hovered' and 'g.HoveredId' which happens because we are using ImGuiButtonFlags_AllowOverlapMode + SetItemAllowOverlap()
10119 // 'hovered' will be true when hovering the Tab but NOT when hovering the close button
10120 // 'g.HoveredId==id' will be true when hovering the Tab including when hovering the close button
10121 // 'g.ActiveId==close_button_id' will be true when we are holding on the close button, in which case both hovered booleans are false
10122 bool close_button_pressed = false;
10123 bool close_button_visible = false;
10124 if (close_button_id != 0)
10125 if (is_contents_visible || bb.GetWidth() >= ImMax(lhs: button_sz, rhs: g.Style.TabMinWidthForCloseButton))
10126 if (g.HoveredId == tab_id || g.HoveredId == close_button_id || g.ActiveId == tab_id || g.ActiveId == close_button_id)
10127 close_button_visible = true;
10128 bool unsaved_marker_visible = (flags & ImGuiTabItemFlags_UnsavedDocument) != 0 && (button_pos.x + button_sz <= bb.Max.x);
10129
10130 if (close_button_visible)
10131 {
10132 ImGuiLastItemData last_item_backup = g.LastItemData;
10133 if (CloseButton(id: close_button_id, pos: button_pos))
10134 close_button_pressed = true;
10135 g.LastItemData = last_item_backup;
10136
10137 // Close with middle mouse button
10138 if (!(flags & ImGuiTabItemFlags_NoCloseWithMiddleMouseButton) && IsMouseClicked(button: 2))
10139 close_button_pressed = true;
10140 }
10141 else if (unsaved_marker_visible)
10142 {
10143 const ImRect bullet_bb(button_pos, button_pos + ImVec2(button_sz, button_sz));
10144 RenderBullet(draw_list, pos: bullet_bb.GetCenter(), col: GetColorU32(idx: ImGuiCol_Text));
10145 }
10146
10147 // This is all rather complicated
10148 // (the main idea is that because the close button only appears on hover, we don't want it to alter the ellipsis position)
10149 // FIXME: if FramePadding is noticeably large, ellipsis_max_x will be wrong here (e.g. #3497), maybe for consistency that parameter of RenderTextEllipsis() shouldn't exist..
10150 float ellipsis_max_x = close_button_visible ? text_pixel_clip_bb.Max.x : bb.Max.x - 1.0f;
10151 if (close_button_visible || unsaved_marker_visible)
10152 {
10153 text_pixel_clip_bb.Max.x -= close_button_visible ? (button_sz) : (button_sz * 0.80f);
10154 text_ellipsis_clip_bb.Max.x -= unsaved_marker_visible ? (button_sz * 0.80f) : 0.0f;
10155 ellipsis_max_x = text_pixel_clip_bb.Max.x;
10156 }
10157 RenderTextEllipsis(draw_list, pos_min: text_ellipsis_clip_bb.Min, pos_max: text_ellipsis_clip_bb.Max, clip_max_x: text_pixel_clip_bb.Max.x, ellipsis_max_x, text: label, NULL, text_size_if_known: &label_size);
10158
10159#if 0
10160 if (!is_contents_visible)
10161 g.Style.Alpha = backup_alpha;
10162#endif
10163
10164 if (out_just_closed)
10165 *out_just_closed = close_button_pressed;
10166}
10167
10168
10169#endif // #ifndef IMGUI_DISABLE
10170

source code of qt3d/src/3rdparty/imgui/imgui_widgets.cpp